Chat GUI

Introduction

In this section we'll look at those areas of the chat GUI that we have not already at least touched on, including the MVC used and Inq's handling of styled text.

The chat client script resides in two files - chat.inq which we have looked at already and chattab.inq which we concentrate on here. chat.inq is the initial client script. It loads chattab.inq when it runs with the line

loadclient("chattab.inq");

where the argument to loadclient() is a url resolved by the Inq server relative to the current module.

Note
The following sections explain the salient points of the chat GUI. Issues are covered to the extent appropriate to this example, however you may infer the existence of more reference-level documentation that is yet to be written up. Inqwell thanks you for your patience.

Creating the Channel GUI

The GUI subtree to handle a chat channel is created by the function chatTab(), called from the service joinChannelConfirm. It creates one of two forms, depending on whether the channel is a 1-1 conversation or a conference

Chat GUIs

and these are laid-out in stages.

The gList Properties

When the GUI displays the participants it uses a gList. In the GUI Basics section we discussed how simple components use the renderinfo property to establish their MVC relationship. More complex components like lists also support MVC but require more property settings to set this up.

A gList references a node set to provide it with a vector of items. The modelRoot property is set with a path() that selects the participants list within the node space we designed for a chat channel:

userList.properties.modelRoot = path($this.chatInsts.participantsList);

Beneath the node set root, the model property uses the simplest form of renderinfo expression that implies the default path of $this.ChannelParticipant.OnLine.

any listmodel.internal    = renderinfo(typedef=ChannelParticipant.OnLine);
userList.properties.model = listmodel;

Notice that there is no explicit dimensioning of the list. It takes a width hint from the ChannelParticipant.OnLine field, which was defined in its typedef by reference to OnLine.OnLine, which in turn has a specified width of 8 characters.

Keeping The List Sorted

The participants list is dynamic - users may enter and leave the channel at any time. It would be nice if the node set could remain ordered as this happens. The ordering properties of hmap were introduced in the discussion of that data type, however there are further considerations when such a structure is being rendered by a GUI component.

  • Inq's MVC handling does not require the underlying node set to be present initially and it may be replaced at any time. Should this happen gList will re-evaluate the model root node (using the modelRoot property) and re-render the entire list.
  • Inq does not preserve node set order or any collating information placed within it by a prior sort when the node set is copied between client and server. In any case, we are not interested in keeping the node set ordered within the server as there is no requirement to do so.

For these reasons, GUI components that render node sets support the modelSort property, whose value is a func containing a suitable sort expression.

userList.properties.modelSort =
  func f = sort($this, $loop.ChannelParticipant.OnLine, ignorecase=true);

Inq uses this expression to sort the underlying node set when it is replaced and to find the appropriate insertion position when individual children are added.

More Complex GUI Events

There are two examples of specifying GUI events using modifiers to characterise the event more.

The text entry component, taSend, looks for the ENTER key and consumes the event:

// TextAreas don't generate action events on CR so we
// use a key press event.  We consume the event so that
// the newline is not inserted into the text area's
// underlying document. Actions would be better but they
// have not yet been implemented in Inq.
gEvent(taSend, call sendChat(), event=(gKpressed),
                                gModify=(keycode=VK_ENTER),
                                consume=true);

The participants list sets up a double-click to create a private chat with the selected user:

gEvent(userList, call spawnPrivate(), event=(gMclicked),
                                      gModify=(count=2));

Using funcs To Define Context Entry Points

The chatTab function includes two arguments chatFuncs and tabFuncs. These are maps that are set up in the joinChannelConfirm service, chat.inq refers:

service joinChannelConfirm(any ChatChannel, any ChannelParticipant)
{
  // Create the GUI and set up the structures for this
  // chat session.

  smap tabFuncs;   // funcs into the channel context
  smap chatFuncs;  // funcs into the chat context

  func chatFuncs.spawnPrivate = call newPrivate(OnLine);


  any newTab = call chatTab(ChatChannel,
                            ChannelParticipant,
                            parent=$this.channels,
                            tabFuncs,
                            chatFuncs,
                            $this.vars.icons);

  // Save the (now filled in) tabFuncs map in our context using the
  // unique key of the given ChatChannel instance. Then we can use
  // the appropriate set of tabFuncs to vector to the appropriate
  // context
  any k = getuniquekey(ChatChannel);
  any $this.vars.{k}.tabFuncs = tabFuncs;
}

These maps contain func variables initialised with entry points to the top and channel-level contexts. When joinChannelConfirm runs it does so in the top-level context. Creating func chatFuncs.spawnPrivate provides a statement (a call statement is typical but any script is valid) that runs in this context when invoked with xfunc(). The chatTab function retains chatFuncs beneath its context node:

// Remember the funcs to the chat context we have been given
any topLevel.vars.chatFuncs = chatFuncs;

and calls spawnPrivate in the double-click callback on the participants list:

local function spawnPrivate()
{
  // Function points to newPrivate in the top-level window context
  xfunc($this.vars.chatFuncs.spawnPrivate,
        $this.userList.model.selection[0].OnLine);
}

Similarly, tabFuncs is passed (by reference) to chatTab and retained in its context:

// Remember also the map given to us. This is populated
// when contextExtablished() is called also.
any topLevel.vars.tabFuncs = tabFuncs;

and filled in when contextEstablished() is called:

func $this.vars.tabFuncs.leaveChannel  = call leaveChannel();
func $this.vars.tabFuncs.clearChannel  = call clearChannel();
func $this.vars.tabFuncs.inviteChannel = call inviteChannel(OnLine);
func $this.vars.tabFuncs.chatReceived  = call chatReceived(ChatChannel, BackChat, baseStyle, urlRE, active);

Of course, when calling from the top-level context into a chat channel we need to know which channel to choose. The tabFuncs for each channel is retained in the top-level context by the primary key of the ChatChannel instance:

// Save the (now filled in) tabFuncs map in our context using the
// unique key of the given ChatChannel instance. Then we can use
// the appropriate set of tabFuncs to vector to the appropriate
// context
any k = getuniquekey(ChatChannel);
any $this.vars.{k}.tabFuncs = tabFuncs;

Rendering Styled Text

Inq supports styled text using the style data type. Readers unfamiliar with styled text may refer to the relevant section of the Swing Tutorial although of course Inq provides its own methods of access to styled rendering.

Defining The Styles

A style is created by initialising it from a map whose contents comprise values with well-known keys to define the character attributes. The function chatWindow() in chat.inq creates styles that are used by all channel contexts to render received messages.

If a style exists in the Inq hierarchy as the child of another style then the parent is used to supply any attributes not specified in the child. In this way style hierarchies can be created. The chat client creates styles with the following script:

// Set up some styles for rendering the chat.
// First set up some maps with well-known keys describing the style
// properties...
string baseStyleDescr.@fontFamily = "Tahoma";   // basic style
colour unameStyleDescr.@fg        = "#0000FF";  // username has blue added

// ...then create the styles themselves from the above descriptions.
// First the base style...
style chat.vars.baseStyle = baseStyleDescr;

// ...then the base style with added blue by creating the style
// hierarchy baseStyle ---> unameStyle
style chat.vars.baseStyle.unameStyle = unameStyleDescr;

// Reuse blue for urls too.
style chat.vars.baseStyle.urlStyle   = unameStyleDescr;

Notice that unameStyle is a child style of baseStyle so inherits its @fontFamily.

Using The Styles To Render Text

In chattab.inq, a chat message is rendered by the function chatReceived().

Accessing The Document

Inq provides access to a text component's underlying Document and constituent elements using a path. The root element, to which we want to append our styled text, is accessed like this:

any doc = $this.chat.model.root;

This statement creates an alias to the root element, to which we want to append the text. The end point of the document is expressed as the last content element. We can create a path() that expresses this as follows:

any atEnd = path(doc[last]);

Expressing Styled Text

To actually use a style we combine it in a map with the text we want to render in that style.

Chat Message

To do this well-known keys are used to specify the style and text. In chat the first thing we do is render the message time with the base style defined earlier:

// 1) When the message was sent
any styledDate.@style = baseStyle;
any styledDate.@txt   = render(BackChat.MsgTime, format="HH:mm");

The @txt and @style children identify the text to render and style to use respectively. The BackChat.MsgTime field is of type date so we convert it to a formatted string using the render() function. Non-printing characters like tabs do not require style information so they can be added without it:

// A tab - these don't need any style info
add("\t", atEnd);

User Information in Styles

The chat utility implements the commonly found idea of clickable URLs in the text pane. For the sake of completeness here, a URL is found in the chat message by applying a regular expression to it. In Inq, regular expressions are supported by the data type regex. A regex is initialsed with a pattern which only need be set once. A single regex is setup in chat.inq though the pattern is too outrageous to reproduce here.

A regex's sequence property is used to establish the text to which the pattern is applied. Successive reads of the find property returns true if and until there are no more matches. When there is a match, the start and end properties return the position of the match in the sequence.

If a URL is matched then it is rendered with its own style like this:

style baseStyle.urlStyle.theUrl;
any   baseStyle.urlStyle.theUrl.urlText = midstring(BackChat.MsgText, urlRE.properties.start, urlRE.properties.end - 1);
any styledURL.@style  = baseStyle.urlStyle.theUrl;
any styledURL.@txt = baseStyle.urlStyle.theUrl.urlText;
add(styledURL, atEnd);

This is what's happening:

  • A new style, called theUrl is created as a child style of urlStyle.
  • This style is given the child urlText, however this is now a well-known key (which all begin with @). It resides in the style but has no effect.
  • The text to be rendered and theUrl are associated in the same way as above and added to the text pane's document.

All of this makes more sense when we look at the event handlers mouseOverUrl() and clickOverUrl(). In the case of a text pane Inq includes the style at the character position of the mouse coordinates in the event. Thus, we can pick up whether the mouse is over a URL, since the matched text is contained within it and launch something if clicked.

Tab Child Properties

When discussing the Inq GUI hierarchy, we briefly mentioned in GUI Basics the fact that Inq components that are children of tab panes have various properties that then become active and relate to the tab parent. These are as follows:

tabBackground
The tab's background colour
tabForeground
The tab's foreground colour
tabIcon
The tab's icon
tabTitle
The text displayed in the tab
tabToolTip
The tab's tooltip

The chat utility uses some of these properties either directly or with gProperty bindings in chattab.inq.

nextpage