Radio Groups in iHD

One of the things you take for granted in many programming environments is the default "widget" set that includes things like buttons, text boxes, and so on. These widgets make up the basic UI that your applications displays to users to display data and receive input. iHD provides these basic building blocks, but it doesn't provide higher-level concepts like radio-groups or list boxes. Today we'll look at how you might go about building a radio-group, and hopefully soon I'll show you an example list box as well (I've written the code; I just need to write the blog to go with it ;-) ).

Download the Code

You can download the ZIP file that accompanies this blog here. Note that you will need to copy a font named 'font.ttf' into the ADV_OBJ folder for the sample to work; see README.TXT for more info.

Note that the example here is purely visual; it doesn't actually change subtitles or audio tracks because there is no audio / video in the download.

The sample looks a bit like this when running:

Screenshot of radio group sample

The General Approach

The general approach I have taken with the group boxes is to use the built-in notion of a "class" to identify related elements. This would be similar to a "group name" in some other common widget sets. The example code contains two radio groups, "audio" and "subtitle", and the markup elements that pertain to those groups have their class attribute set appropriately.

Although the markup is group-specific (elements must have the correct class attribute, and you probably want to display different graphics, etc. for each group), the script code is generic and can handle any number of groups. The script code maintains three states for each radio group -- the current item, the last item, and the processed item. These three states are used to set XPath variables that then trigger animations in the markup to highlight the correct elements.

Radio buttons can be in one of four states:

1) Not selected and not focused

2) Not selected and focused

3) Selected and not focused

4) Selected and focused

You could also throw "disabled" into the mix (Disabled and not selected; Disabled and selected) but for this example I don't have that concept. Using the XPath variables, the animations ensure that each element looks correct based on the choices the user has made.

The Markup

I'll only show "relevant" markup here; you can read the boring stuff in the download :-). The radio groups consist of many blocks of markup like this:

<div style="radioholder"> 

  <button style="radiobutton" class="audio" id="english" state:focused="true" /> 

  <div id="english_div" class="audio"
style="radiotextholder" style:opacity="0.4"> 

    <input style="radiotext" mode="display" state:value="English" />

  </div>
</div>

Quick explanation:

·The first div is simply a place holder to hold the sub-elements for a single radio item

·The button has its class attribute set to the name of the radio group (in this case, "audio") and has the id attribute set to the value of the radio item (in this case, "english")

·The nested div also has the class attribute set, and it has the same id as the button followed by "_div". This enables easy matching up of buttons-to-content in the script code

·Finally, the input is used to display the human-readable value of the radio item (typically you would have a graphic here rather than the text)

The Animation

Again, I am only showing relevant timing and animation here. The block of XML for the "audio" radio group is as follows:

<parbegin="id($audiocurrent)[@id!=$audioprocessed]"
  end="defaultNode()[@id=$audiolast]">

  <cue select="id($audiocurrent)" dur="1f" fill="hold">

    <set style:opacity="1" style:backgroundColor="rgba(127,0,0,255)" /> 

    <event name="RadioSelected" />

  </cue>
</par>

Quick explanation:

·The par begins when the current audio selection ($audiocurrent) is not the same as the most recently processed audio selection ($audioprocessed) and ends when the originally-selected element becomes the previously selected item ($audiolast)

·Note that you should be able to just use the expression begin="($audiocurrent!=$audioprocessed)" but a bug in the current version of iHDSim won't let you do that

·The cue gets the current audio selection and sets its backgroundColor property to dark red for the duration of the parent (ie, until this element becomes the previously-selected element)

·The cue also fires an event to script, which will update the XPath variables accordingly

Additionally, the following markup is used to handle "click" processing on a radio button:

<cue begin="//button[@style='radiobutton' and state:actioned()]" dur="1f">

  <event name="RadioClicked" />

</cue>

This is pretty basic: when a button is actioned (clicked), fire an event to script.

The Script

The script is not very complicated; I'll break down the three basic parts. (Note that the script contains comments that I have stripped out of the text here, since I basically explain the same thing in the narrative).

Here's a (slightly edited) copy of the code used during initialisation:

setMarkupLoadedHandler(OnLoad);

var radioGroups =
{
audio : { current : "english_div" , last : "" , processed : "" }
, subtitle : { current : "no_st_div" , last : "" , processed : "" }
};

function OnLoad()
{
  for (var name in radioGroups)
  {
    var group = radioGroups[name];
    for (var state in group)
      document.setXPathVariable(name + state, group[state]);
  }
}

Quick explanation:

· setMarkupLoadedHandler is an API call that tells iHD to call our function (OnLoad) when the markup has been loaded (but before the first "tick" has been processed). This is very important because you must set all XPath variables before the timing section is first evaluated, but you can't do it in global startup code because the document object won't exist until the markup has been loaded. The "markup loaded handler" is the only time where both these conditions hold, so you must set your XPath variables here.

· radioGroups is an object that contains a property for each of the radio groups in the markup (in this case, audio and subtitle). Each property has as its value another object that stores the three states we mentioned above. The initially-selected items are specified in this declaration.

·The OnLoad function (our markup loaded handler) loops through the properties of the radioGroups object, and for each of the properties of those objects, it sets an appropriate XPath variable -- audiocurrent, audiolast, and so on through to subtitleprocessed.

The event handler for the "radio button clicked" event looks like this:

function OnRadioClicked(evt)
{
  var name = evt.target.getAttribute("class");
  var group = radioGroups[name]; 

  var clicked = evt.target.core.id + "_div" ;
  if (clicked == group.current)
    return ;

group.last = group.current;
  group.current = clicked;

document.setXPathVariable(name + "last", group.last);
  document.setXPathVariable(name + "current", group.current);
}

Quick explanation:

·First we get the name of the radio group (from its class attribute) and then retrieve the appropriate group object from the radioGroups object

·Next we check the id of the clicked button (the event's target) and if it's the same as the currently-selected item then we have no work to do and return

·Next we update the last item to be the old current item, and set the current item to be the thing that was clicked

·Finally, we update the appropriate XPath variables to trigger the changes in the markup

Last but not least, the "radio item selected" event handler looks like this:

function OnRadioSelected(evt)
{
  var name = evt.target.getAttribute("class");
  var group = radioGroups[name];

  group.processed = evt.target.core.id;
  document.setXPathVariable(name + "processed", group.processed);
}

Here we once again get the name and group of the selected item based on its class attribute, and then set the appropriate XPath variable to note that this selection has just been processed.

Why do we need the 'processed' state?

This is due to the 'a begin must become false before it can become true again' rule. Imagine if we just said:

<par begin="id($audiocurrent)" end="defaultNode()[@id=$audiolast]" >

We would start off alright -- iHD would select the current audio item on the first tick, and the animation would live on until that item became the previous (last) item... but then what? A new item becomes the current audio item, but there was never a time when iHD could not find the current audio item. Because the begin never becomes false, the animation will never start again.

Throwing the "and also not the most recently processed audio item" condition into the mix ensures that the begin becomes false on the next tick (when script updates the processed variable to be the same as the current variable), making the animation eligible to begin again the next time a not-just-processed item becomes the current item.

A similar trick is used for focus management, but instead of the processed state we use a mutex (of sorts) to ensure that focus events only happen every couple of frames (another way to solve the "the user clicks too fast" problem).

RadioGroups.zip