Browse Items

Introduction

The Browse Items window expands on the examples presented so far, presenting the following:

Using the Attribute Editor
An Inq bundled dialog that, like the Item Chooser, offers significant GUI functionality applications can use through a simple programming interface. Browse Items uses the Attribute Editor to adjust the Status, Price and Attr1 fields of a set of Items.
Timers
Inq supports timers with its timer data type. Browse Items uses a timer to flash an icon in the table's Last Price column, providing a subtle alert that a pet's price has gone up or down.
Database-enabled combo boxes
In the examples presented so far, combo boxes have been used to select from a static list of enumerations. Browse Items shows a pattern for creating a combo box whose list is a node set.
itemsbrowser

The items browser is implemented in gui/browseItems.inq.

Using The Attribute Editor

The Attribute Editor is a reusable element contained within the Inq distribution Jar file. It is an example of how significant levels of (in this case GUI) functionality can be written ahead of time and applied in unrelated applications designed much later.

Attribute Editor is a dialog that supports the bulk-update of specified attributes within tabular data. Depending on their data type, attributes can be increased or decreased by an absolute amount or a percentage, or set to a value (which must be amongst the enumerations when present). Its deployment in petstore shows all these features.

Note
To use the Attribute Editor the application must first have parsed its source. In petstore this is done in gui/userStart.inq with the statement exec("cp:///inq/gui/attrEditor.inq");.

Attribute Editor's Interface

The Attribute Editor has only one global function:

function editAttrs(any  parent,
                   any  table,
                   any  columns,
                   any  renderers,
                   any  title,
                   any  keyri,
                   func okExpr,
                   func cancelExpr)
Attribute Editor Arguments
Name Description
parent The Attribute Editor is a dialog, so it needs a parent window - see About Dialogs. The dialog is made PARENT_MODAL.
table The gTable rendering the data being edited. The row selection defines the set of items.
columns An array of column names, where the names are those used in the map supplied as the table's columns property. These names identify the attributes among those rendered by the table to be edited.
renderers An array of renderinfo expressions. When the desired attributes are not being rendered by the table (and their meta data is therefore not available from the table) they can instead be specified this way. Either of columns or renderers may be absent but at least one attribute should be specified.
title The title for the dialog
keyri A renderinfo the Attribute Editor will use to label the items being edited.
okExpr A func Attribute Editor calls when OK is clicked.
cancelExpr A func Attribute Editor calls when Cancel is clicked. Optional argument.

Browse Items uses the Attribute Editor to adjust the rendered columns itemstatus (Item.Status), listprice (Item.ListPrice) and Item.Attr1, which is not part of the table:

array columns = ("itemstatus", "listprice");
array renderers = (renderinfo(typedef=Item.Attr1));

call inq.gui.util:editAttrs(parent = $this,
                            table  = $this.itemTable,
                            columns,
                            renderers,
                            title  = $catalog.{$root.i18n}.ps.title.EDIT_ITEMS,
                            keyri  = renderinfo(typedef=Item.Item),
                            okExpr = func f = call attrCb(results));

Say we want to adjust the price of all cats. The user can group the cats by sorting the table by category (or filter by this category), select those rows and use File->Edit to bring up the attribute editor:

attredit

The Attribute Editor GUI

To change an attribute, an action must be selected. If the attribute is numeric the choices are increase, decrease and set. The value to use is entered into the editable Value cell.

attredit1

The Units cell is a drop-down selection to determine the value's meaning:

attredit2

If the attribute is an enumeration then the value is a selection:

attredit3

Individual values can be edited in the New table or reset to their original value by double-clicking in the cell of the Old. The current state of the Attributes settings can be replayed using the arrow button.

The Callback Interface

When inq.gui.util:editAttrs() was called the okExpr argument used was call attrCb(results). Attribute Editor calls the user-supplied function passing results, which is a new node set containing instances of the typedef(s) being edited.

local function attrCb(any results)
{
  send updateItems(items = results,
                   thing = path($this.Item));
}

A general updateItems service is called - here is what that does:

service updateItems(any items, any thing)
{
  foreach(items)
  {
    // Get the managed instance
    any i = read(typeof($loop.{thing}), $loop.{thing});
    i = $loop.{thing};
  }
}

If there are no exceptions thrown then normal Inq event propagation will update the affected cells in the parent table view.

Database-Enabled Combo Boxes

The filter bar in Browse Items includes a combo-box to select a category of pets. This is driven by the available instances of the Category typedef.

category

The following steps are the pattern for creating such a combo box:

  1. Create the combo-box and configure it with appropriate properties.
  2. Establish a gContext event handler on the combo-box.
  3. In the event handler (which runs in the combo-box's context) invoke a service to yield its item list.

To illustrate how to make such a component a reusable feature of an application's GUI, petstore factors these steps out into a function (local to gui/psItem.inq in this case but perhaps global and in a utilities module more generally). Here are all the related functions:

local function createFilterBar(any context, any i18n)
{
  // Create the data the filter bar components will operate on.
  // This is an instance of Item.Filter
  any context.vars.filter = new(Item.Filter);
  
  // Create a combo box whose contents are the available Category instances
  any cbCategory = call categoryCombo(renderInfo    = renderinfo($this.vars.filter.Category,
                                                                 typedef=Category.Category),
                                      modelRoot     = path($this.vars.categoryList),
                                      anyComboValue = {i18n}.ps.general.ALL
                                     );
     .
     .
     .
}

local function categoryCombo(any     renderInfo,
                             any     modelRoot,
                             string  anyComboValue,
                             boolean load = true)
{
  gComboBox cbCategory;
  
  any model.internal               = renderinfo(typedef = Category.Category);
  any model.external               = renderinfo(typedef = Category.Name);
  cbCategory.properties.model      = model;
  cbCategory.properties.renderInfo = renderInfo;
  
  cbCategory.properties.modelRoot = modelRoot;

  if (anyComboValue)
    cbCategory.properties.nullText = anyComboValue;

  // Order the data
  array order = ( path($loop.Category.Name) );
  cbCategory.properties.modelSort = order;

  // By default send to the server to acquire the data for the
  // combo box list. This can only be done when the component
  // is placed into the context
  if (load)
    gEvent(cbCategory, call categoryInContext(), event=(gContext));

  // returns
  cbCategory;
}

local function categoryInContext()
{
  send loadCategories(at = @component.properties.modelRoot);
}

If you have read the previous sections about petstore then there is nothing new here.

The event handler function, categoryInContext() invokes the service loadCategories(). This service places the list at the specified node path, raising an event to propagate the list to the client with a flow analogous to that discussed in My Orders. The script for this can be found in psCategory.inq. Although not set up in this example, since the list content is server-side state it is not uncommon to establish a listener for the creation of new categories so that the combo-box can be maintained behind the scenes. As things are, if a category is deleted it will be removed from the combo's list automatically.

Using Timers

Inq has the ability to run timers, calling a function at some future time or after a delay, either as a one-shot or, if the timer's period property has been set, at regular intervals. Browse Items uses a timer to alert the user that an item's LastPrice field has changed, flashing an icon in the table cell.

Timer Properties

The timer is set up in setupPriceListener():

local function setupPriceListener()
{
  timer t;
  t.properties.period   = 500;     // timer runs every 1/2 second
  t.properties.syncGui  = true;    // dispatches to GUI thread
  t.properties.userInfo = set s;   // the set of rows being timed
      .
      .

A timer carries an item of data set up with the userInfo property. This data is opaque to the timer itself and can be any Inq type. In this example a set is used to make a note of the rows currently being flashed.

Timers are supported in both client and server environments, but in the client the syncGui property is supported and determines whether the timer will run in the graphics thread. In fact, as we see below, this timer does not directly manipulate the GUI so this property is not important in this case.

The timer will fire repeatedly, every 500ms, by setting the period property. Not used in this example, a future time for the timer to fire can be established by setting the nextRuns property to a date value.

Timer Action

A timer's action is established by setting its func property to a func variable.

Note
A func, not a cfunc, is provided in this example because the action makes reference to $this and the context in which the timer's action will run is encapsulated.

Browse Items sets its timer action with this function:

  // Leave the timer in the context for later access
  any $this.svars.lastPriceTimer = t;
  
  // 1 or 0 whether the icon is on or off
  int $this.svars.timerState;

  t.properties.func = func f = {
                                 any userInfo = fromTimer.properties.userInfo;
  
                                 // Decrement the counter in each rowRoot
                                 // being flashed. If it has reached zero then
                                 // remove it from the set.
                                 foreach(userInfo)
                                 {
                                   if ($loop.ValueHelper.Count)
                                     --$loop.ValueHelper.Count;
                                   else
                                     removeiter();
                                 }
  
                                 // If the userInfo set has no items
                                 // left in it then stop the timer.
                                 // Otherwise leave it running.
                                 if (count(userInfo) == 0)
                                 {
                                   canceltimer(fromTimer);
                                 }
                                 
                                 $this.svars.timerState = !$this.svars.timerState;
                               };

When the timer fires it places itself at $stack.fromTimer so that the action can extract any user information it carries and manipulate the timer as required. As explained further below, this timer's userInfo property is a set of the node set children whose Last Price cell is being flashed. Within each such child a counter has been set up that the timer action decrements. On reaching zero the cell is no longer flashed, so its node is removed from the set. If the set becomes empty the timer is cancelled, meaning it will not run again.

Starting The Timer

A timer is started with Inq's starttimer() function. In the context of this example, how do we know when to do this? To flash the Last Price cell a certain number of times (determined by the counter discussed above) we need to know that Item.LastPrice in any particular row has changed. When discussing My Orders we saw how the client can listen for the order items list being replaced so it could recalculate the order's total value. We can also arrange to listen for specific updates occurring within the node space - Browse Items does that for updates to Item.LastPrice:

listen ($this,
        func f = 
        {
          any nodes = nodesof($this, @eventId.path);
          any rowRoot = nodes[3];
          
          // Put a counter into the row
          any rowRoot.ValueHelper = new(ValueHelper);

          // The icon will flash 5 times. By using timerState we make
          // all icons flash together. May be that is the best effect.
          rowRoot.ValueHelper.Count = $this.svars.timerState + 10;
          
          // Put the rowRoot into the set of those currently being
          // flashed by the timer.
          any userInfo = $this.svars.lastPriceTimer.properties.userInfo;
          userInfo + rowRoot;
          
          // If this was the first entry then start the timer
          if (count(userInfo) == 1)
            starttimer($this.svars.lastPriceTimer, 500);
        },
        event  = (update),
        path   = $this.vars.itemList.%.Item,
        fields = (LastPrice));

The arguments to listen() effect the desired event dispatch as follows:

  1. The specified event type is update.
  2. Discrimination of the event includes the path it has traversed from within the structure. This path is specified relative to the node being listened to - typically $this. The special character % consumes the node-set child (whose map key we don't know and want to wild-card).
  3. The fields argument is a comma-separated list of literal field names. If the fields carried in the event overlap those specified in the argument the listener will fire.
Note
Exactly the same event discrimination would have resulted if the path argument were replaced with typedef = Item.

The environment required by the timer action is established by the listener dispatch function, so its job is to determine the row the event originated in, initialise its counter and add it to the set of rows being flashed. If this is the first such row, the timer must be started.

The dispatch code makes use of the nodesof() function. This function takes two arguments - a node and a path. Starting at the node, the path is applied yielding each successive node and returning them as an array.

When dispatching events to an event listener Inq places something on the stack it calls @eventId. This is a map that includes the following:

type
The basic event type, that is update, create, delete, add, remove or replace.
fields
When the event is of type update the set of fields that were changed in the instance.
path
The path the event took from its point of origination to the point of dispatch.

The dispatch node is $this so applying the event path to it returns the following array

     $this.vars.itemList.%.Item
       ^    ^     ^      ^  ^
nodes[ 0    1     2      3  4 ]

and nodes[3] is the node set child (or the "row root"). The dispatch function can then create a counter for the row, initialise it and, if there are currently no rows being flashed, start the timer.

Rendering The Cell

Having set up the counter and run the timer, how is the cell actually flashed? For this example an up or down arrow icon is used to indicate the direction of the price move. Once the row counter has reached zero and the flashing stops, the cell foreground colour is left to indicate the last direction (modeled by enumeration Item.LastPriceMove).

Accordingly, the cell must be rendered not only when Item.LastPrice changes, but also as the counter decrements. This is achieved with the renderinfo for the cell, which refers to both these fields:

any columns.lastprice   = renderinfo({
                                       $this.ValueHelper.Count;
                                       $this.Item.LastPrice;
                                     },
                                     typedef=Item.LastPrice);  // TODO

The presence of both ValueHelper.Count and Item.LastPrice in the renderinfo's expression (a block statement) means that the cell will be rendered when either of these values change. The value of the expression is the last statement it executed, so this expression returns Item.LastPrice for the cell's value.

To render the icon and value a complex renderer component is establised in a similar way to that used in the New Order window. Similarly the icon and foreground colours are established with a style function:

  itemTable.properties.columns.lastprice.renderer.properties.style = cfunc f =
    call renderLastPrice();
    .
    .

local function renderLastPrice(any component,
                               any rowRoot)
{
  switch
  {
    when (rowRoot.Item.LastPriceMove == enum(LastPriceMove, UP))
    {
      any .style = $catalog.ps.styles.up;
      any icon   = $catalog.icons.arrowup;
    }
    when (rowRoot.Item.LastPriceMove == enum(LastPriceMove, DOWN))
    {
      any .style = $catalog.ps.styles.down;
      any icon   = $catalog.icons.arrowdown;
    }
    otherwise
    {
      any .style = $catalog.ps.styles.none;
      any icon   = $catalog.icons.clear16;
    }
  }
    
  component.price.properties.style = .style;

  if (rowRoot.ValueHelper.Count && $this.svars.timerState)
    component.icon.properties.icon = icon;
  else
  {
    // If we always put an icon in (instead of setting it to null)
    // then the cell width must always include enough space for it.
    component.icon.properties.icon   = $catalog.icons.clear16;
  }
  
  // returns no style for Inq to apply - everything required is already done
  null;
}

nextpage