Advanced GUI Scripting – User Defined Dialogs

Contents

Introduction

This post briefly illustrates the features and the capabilities offered by the GUI namespace of the iChrome’s products Scripting interface. More in particular, this post focuses on the definition of a user-defined Dialog shell. The example discussed in the following is built around the following user case:

We want to define an API Macro capable to create an easy Dialog for the final user. Via the Dialog the user will add items to an existing project table recording the sales performed for a specific set of items.

On order to keep this examples the simple possible, let us suppose the table containing the sales to be recorded only has 3 columns:

  • Name: this is the name of the items sold;
  • Description: an optional textual field containing notes about the sold items;
  • Quantity:a numeric (integer) column containing the number of items sold.

Please bear in mind this is a simple example. If you are keen to do more complex things, you can move from this example and build up new features. As an example you can add more information to the sales table such as the date and time of the sale, the overall paid amount, etc. The original (Grapheme) project can be found HERE.
Please also note despite being an relatively simple example, we suppose you are familiar with the general concepts of our Graphical User Interfaces and with our scripting APIs. For this specific example, and without loosing in generality, we opted to write our Macro in JavaScript.

>> Back to Top

Basic Concepts

The scripting module of Grapheme and Nexus collects top-level functions within predefined namespaces:

  • API: the API namespace contains general top-level utilities to operate with the scripting console, external files, tables, charts, etc.
  • GUI: the GUI namespace on the contrary contains specific functions to generate GUI controls such as message dialogs, file browser, etc. A specific feature of the GUI namespace is its capability to create user-defined and customizable Dialogs via the so called Dialog objects



You create a Dialog object via a call to method:
Dialog GUI.createUserDialog()
Once created, you can customize and configure the aspect and the behavior of the dialog object.

interface Dialog {
   
   // creation methods
   Dialog setLayoutColumns(
       int cols, boolean equal)
   Dialog setDefaultSize(int width, int height)
   Dialog setResizable(boolean flag);
   Dialog setTitle(String title);
   
   Dialog addLabel(
      String userID, int nCols, String label)
   Dialog addTextBox(
      String userID, int nCols, boolean mline, 
      String text);
   Dialog addCombo(
      String userID, int nCols, String[] items)
   Dialog addSpinner(
      String userID, int nCols, int min, 
      int max, int step)
   Dialog addCheckBox(
      String userID, int nCols, String text)
   Dialog addButton(
      String userID, int nCols, String text)
   
   // interaction methods  
   Dialog setEnabled(
      String userID, boolean flag)
   Dialog setMessage(String text, int level)
   
   Object getValue(String userID)
   void setValue(String userID, Object value)

   // callback utilities
   void setValidation(String callBackFnc)
   void setValidation(
       String userID, String callbackFnc)

   Dialog setModel(Object model)
   Object getModel()

   int open();
}

>> Back to Top

The Dialog Class Explained

The Dialog class has three main families of methods:

  • creation: these methods should be called before the dialog is opened and are typically used to define the initial graphical aspect of the shell. Many of these methods do not have effect if called once the dialog has been opened.
  • interaction: these methods are designed to allow the caller to interact with the dialog, typically allowing to modify the graphical aspect of the editor on the fly. These methods can be called either before the dialog is opened or within user-defined callback functions.
  • call back: these methods allow the caller to define so-called callback functions, i.e. functions called by the dialog (when opened) to allow the user to interactively operate with the dialog and the current data selection.

Therefore programming a typical Dialog object requires three main phases:

PHASE 1):
DEFINING THE DIALOG COMPONENTS AND OVERALL ASPECT

In this phase the user defines the graphical components of the dialog such as labels, text boxes, spinner, etc. It is stressed that each user-defined component needs to have a unique identified (see parameter userID in the creation function above. The provided userID will be used in the call-back and interaction phase to retrieve the components of the dialog and their actual values. In other words, the user will access the widget created in this phase by the provided names.

PHASE 2):
DEFINING CALLBACKS AND INTERACTIONS

The basic idea around the callback functions is that users may need to programmatically interact with the dialog whilst the dialog is open. A typical example is the need to provide feedbacks (such as validation messages) on the data inserted so far and/or to enable/disable specific widget components (such as combo selectors, input text area, etc) depending on the current status of the dialog.
Within the GUI scripting utilities, this is done using so called *callback* functions. Dialog callback functions are quite simple: these are function defined by the user accordingly to the prototype below:
void function(Dialog dialog) { … }

Every time the status of the dialog changes (for example because the user is typing into a text area or because the user has selected an item from a spinner or a drop-down combo widget), the callback is called and the dialog itself is provided as input. In such a way the developer can build an interactive dialog.
Note that the Dialog interface provides two distinct methods to add so-called callback functions:

  • setValidation(String callback): this fist variant of the function is used to set a single global callback user function. The global callback user function is always called as soon as the dialog is opened and thereafter every time the dialog arise an event that is not intercepted by a local callback function (see below). Note that only a callback function can exist per editor. Hence, subsequent calls to setValidation(String callback) will actually replace the currently defined callback with the new one. Passing a null reference as callback name removes the global callback previously defined (if any).
  • setValidation(String userID, String callback): this allow defining a so-called local callback function, i.e. a specific callback function to be called only if the dialog arise an event that affect the specific GUI components whose userID has been provided as input. Note that if a local callback is defined, no global callback will be called on events already intercepted by the local one. In other words, a local callback function prevent event of its target widget to propagate to the global one. As per the global callback, a single callback function can exist per component. Hence, subsequent calls to setValidation(String userID, String callback) on the same item (userID) will actually replace the currently defined callback with the new one. Passing a null reference as callback name removes the local callback (if previously defined).

Within the callback mechanism, users interacts with the widget defined in the dialog via their unique username. As an example method Object getValue(String userID) is used to retrieve the current value stored in the widget, being this latter a string with the typed text in the case of a Text box, the current selection index for a Combo, the current value for a Spinner, ect.
Similarly method void setValue(String userID, Object value) can be used to set and force a value for a component. The method expects a variable of kind consistent with the target widtget: string for a Text box, integer for a Spinner and a Combo (in this case is the index of the selection among the initially provided items), etc.

PHASE 3):
OPENING AND USING THE DIALOG

Once the dialog has been defined and configured and the all the callback functions are assigned, the user can open the dialog via a call to int open(). The method opens the dialog in blocking mode. When the user closes the dialog, the method returns the identifier of the button pressed to close the dialog: 0 for OK or 1 for cancel/close.

>> Back to Top

Accessing external data structures

The callback mechanism and the methods briefly described above works for the most common king of applications. There are however situation where you may need to access external data from within the dialog and perform specific operations with external objects from within the callback functions. This is still possible by using the two methods setObject(Object object) and Object getObject(). The rationale is that you can assign an external object (complex as deemed required) to the dialog before actually opening the dialog via a call to setObject(…) so to retrieve such data structure potentially from within any callback function via calls to Object getObject( ).

>> Back to Top

The Example

Given the (probably too long) introduction above, let’s now focus on our example. The whole macro is reported below:

var table = API.getTable("items");
if( table==null ) {
  table = API.createTable("items");
  table.createColumn("Name", "string");
  table.createColumn("Description", "string");
  table.createColumn("Quantity", "integer");
}

function callback(dialog) {
  dialog.setEnabled("info", true);
  dialog.setEnabled("count", true);
	
  var name = dialog.getValue("name");
  if(name==null || String(name).length<1) {
    dialog.setMessage( "Please provide a valid name", 3);
    dialog.setEnabled("info", false);
    dialog.setEnabled("count", false);
    return;
  }
	
  var count = dialog.getValue("count");
  if( count==null || Number(count)<1 ) {
   dialog.setMessage( "Items count should be greater than 0", 2);
   return;
 }
	
 dialog.setMessage( "Ready to insert " + name,0);
}

var dialog = GUI.createUserDialog();
dialog.
    setLayoutColumns(2, true).
    setDefaultSize(450, 300).
    setResizable(true).
    setTitle("Item Dialog").
    addLabel("lbl1", 1, "Name of the Item").addTextBox("name", 1).
    addLabel("lbl2", 1, "Description of the Item").addTextBox("info", 1, true).
    addLabel("lbl3", 1, "Counter").addSpinner("count", 1, 1, 100, 1);
    
dialog.setValidation("callback");

while( dialog.open()==0 ) {
  // insert in table
  var name = dialog.getValue("name");
  var info = dialog.getValue("info");
  var count = dialog.getValue("count");
  if( name==null || count==null || 
      String(name).length<1 || Number(count)<1 )
    continue;
	
  var row = table.createRow();
  row.setValue("Name", name);
  row.setValue("Quantity", count);
	
  if( info!=null )
    row.setValue("Description", info);
}

Let’s have a look at the script more in detail:

Compact_06
The lines from 1 to 8 contain standard API calls to initialize the Projact Table items where user data will be stored.
The lines from 31 to 39 define the graphical aspect of dialog: layout (i.e. number of columns), initial size, shell title, resizable flag and components: two text box and a spinner in this specific case. The resulting dialog is shown on the right.
Line 40 set a global callback function for the dialog. The callback function is defined above (from line 9 to line 28). We will see how the callback works below. For now just note it has been assigned before opening the dialog.

The lines from 42 onward form a loop that re-open the editor up until the editor is not closed with a cancel request from the user. At each loop execution, we extract data from the dialog and we add a new row to the table.

while( dialog.open()==0 ) {
 // insert in table
 var name = dialog.getValue("name");
 var info = dialog.getValue("info");
 var count = dialog.getValue("count");
 if( name==null || count==null || 
     String(name).length<1 || Number(count)<1 )
  continue;
	
 var row = table.createRow();
 row.setValue("Name", name);
 row.setValue("Quantity", count);
	
 if( info!=null )
  row.setValue("Description", info);
}

The callback function is defined at line 9. The function takes care of the followings:

  1. reset the contents and the aspect of the dialog, more specifically disable the possibility to modify the description and the quantity. These items will be re-enabled if a valid item name is set or provided as per checks at line 16 and 17.
  2. validate the provided values: name (at line 14) and count of items (at line 22).
  3. set the status message for the dialog, depending on the current validation status as per line 15, 23 and 23

function callback(dialog) {
 dialog.setEnabled("info", true);
 dialog.setEnabled("count", true);
	
 var name = dialog.getValue("name");
 if( name==null || String(name).length<1 ) {
  dialog.setMessage( "Please provide a valid name",3);
  dialog.setEnabled("info", false);
  dialog.setEnabled("count", false);
  return;
 }
	
 var count = dialog.getValue("count");
 if( count==null || Number(count)<1 ) {
  dialog.setMessage("Items count should be greater than 0",2);
  return;
 }
	
 dialog.setMessage( "Ready to insert " + name,0);
}

>> Back to Top

Conclusive Remarks

In this post we have seen the advanced capabilities of the GUI namespace in creating user-defined dialogs. We have seen how customized and advanced dialogs can be created and modified at execution time thanks to the use of so-called callback functions.

>> Back to Top