Travel Companion user interface

Handling multiple views

The Travel Companion widget will follow many of the same patterns that the Hello World and RSS Reader widgets have followed. The entry point to the widget is the init() function that we call from the onload event handler in the main HTML file TravelCompanion.html. We will implement the init() function along with the rest of the widget and user interface in the TravelCompanion.js file. Let's start with the init() function so that we get the widget up and running.

From previous widgets we already know a lot of things about the WRTKit and about what we will need. For example we know that we will have a user interface manager and that we'll need a reference to each of the views that our widget uses. And since we have designed the Travel Companion widget we know what views we will have. Before we write the init() function we can go ahead and declare the variables that we will use to refer to the views and the user interface manager. And while we're at it we can also declare the variable that we'll use to refer to the Travel Companion engine. All of these variables are globals and thus go outside the init() function:


// Reference to travel companion engine that implements the business logic.
var engine;

// Reference to the WRTKit user interface manager.
var uiManager;

// References to views.
var mainView;
var infoView;
var converterView;
var weatherView;
var settingsView;

Previously we've written the init() function so that it creates the entire user interface but because the Travel Companion widget has a much larger user interface than anything we've done before, it's probably a good idea to split things up into smaller functions so we don't end up with a huge init() function that will be hard to read and maintain. Let's create a function for the creation of each view, and then one function that will call these view creation functions and setup all other things that we need for the user interface, such as the Options menu. We'll just create stub functions for now but we will soon get to implement the functions:


// Creates the user interface.
function createUI() {
}

// Creates the main view.
function createMainView() {
}

// Creates the info view.
function createInfoView() {
}

// Creates the converter view.
function createConverterView() {
}

// Creates the weather view.
function createWeatherView() {
}

// Creates the settings view.
function createSettingsView() {
}

In the RSS Reader widget we used functions called showMainView() and showSettings() to move between views. That allowed us to implement all the logic that was required when moving between views in one place regardless of what action triggered the change of views. We'll do the same thing in the Travel Companion widget but this time we'll need five functions since we have five views. Let's create stubs for these too at this stage so that the functions exist if we need to refer to them.


// Displays the main view.
function showMainView() {
}

// Displays the info view.
function showInfoView() {
}

// Displays the converter view.
function showConverterView() {
}

// Displays the weather view.
function showWeatherView() {
}

// Displays the settings view.
function showSettingsView() {
}

Since we will create each user interface view in its own function our init() function will be much simpler than in previous widgets. We will create the business logic engine, create the user interface by calling createUI() and then display the main view.


// Called from the onload event handler to initialize the widget.
function init() {
    // initialize the business logic engine
    engine = new Engine();
    
    // create the user interface
    createUI();
    
    // display the main view
    showMainView();
}

Let's get started with creating the user interface by returning to the createUI() function. We'll want to widget to show the softkey bar and we'll want it to be in tab navigation mode instead of the default pointer navigation mode. We then need to create the user interface manager and the five views that this widget has. Of course those five views are actually created by the five create-functions that we just wrote stubs for so we just need to call those functions from createUI().


// Creates the user interface.
function createUI() {
    if (window.widget) {
        // set tab-navigation mode and show softkeys
        widget.setNavigationEnabled(false);
        menu.showSoftkeys();
    }
    
    // create UI manager
    uiManager = new UIManager();
    
    // create views
    createMainView();
    createInfoView();
    createConverterView();
    createWeatherView();
    createSettingsView();
}

Because we created the stub functions for moving between views, we can also create the Options menu now. We'll only add one item to the menu in addition to the default "Exit" menu item. The item we'll add is one that will take the user to the settings view. We'll first need to create a global menu item identifier:


// Constants for menu item identifiers.
var MENU_ITEM_SETTINGS = 0;

Now that we have the menu item identifier we can actually create the menu item. Because the Options menu functionality is only available in the S60 Web Runtime and not in PC browsers we'll add that code in the same if-clause where we set the navigation mode and show the softkey bar.


// create menu
var settingsMenuItem = new MenuItem("Settings", MENU_ITEM_SETTINGS);
settingsMenuItem.onSelect = menuItemSelected;
menu.append(settingsMenuItem);

When the menu item is selected it calls a function called menuItemSelected(). Let's create that function and handle the case when the settings menu item is selected. When it is, we want to call showSettingsView() so that the user ends up in the settings view as expected:


// Callback for when menu items are selected.
function menuItemSelected(id) {
    switch (id) {
        case MENU_ITEM_SETTINGS:
            showSettingsView();
            break;
    }
}

The framework that we'll need when implementing the actual views is now nearly complete. But before we move on let's talk about how we'll be using softkeys in the Travel Companion widget.

In the main view we want the right softkey to be the default "Exit". But in the four functional views we don't want it to exit the widget but rather to return to the main view. We'll thus use "Back" as the title in the functional views, except in the settings view where we'll use "Cancel".

Let's create three functions that will handle setting the widget right softkey to these configurations. The functions will check if we're actually in the S60 Web Runtime before attempting to change the right softkey in order to make the widget also work in a PC browser for testing purposes.


// Sets the softkeys for the main view.
function setMainViewSoftkeys() {
    if (window.widget) {
        // set right softkey to "exit"
        menu.setRightSoftkeyLabel("", null);
    }
}

// Sets the softkeys for sub views.
function setSubViewSoftkeys() {
    if (window.widget) {
        // set right softkey to "back" (to main view)
        menu.setRightSoftkeyLabel("Back", showMainView);
    }
}

// Sets the softkeys for settings view.
function setSettingsViewSoftkeys() {
    if (window.widget) {
        // set right softkey to "cancel" (returns to main view)
        menu.setRightSoftkeyLabel("Cancel", showMainView);
    }
}

Main view

Let's create our main view now so that we get something visible. The main view is of course created in the function createMainView() for which we already created a stub function. The main view will have four WRTKit NavigationButton controls in it, one for each of the views (not counting the main view itself) in the widget. Using the NavigationButton control is just like using a FormButton, with the difference that it looks different and can have an icon image in addition to just text. Earlier when we looked at the files that were in the Examples/TravelCompanion directory we noticed that there were four icons there. Now it's time to use them as icons in the navigation buttons.

We will add event listerners to the buttons so that when they are clicked (when they emit an ActionPerformed event) they will call the functions we have created for moving between views. At this point the functions are just stubs, but we will of course fix that later.

We've had captions for the views that we've created so far in the Hello World and RSS Reader widgets and the Travel Companion will also have view captions. However for the Travel Companion we'll use an image as our view caption. We will set the view caption to an empty string because if it's undefined or null then the view caption area will be hidden. Then we will create a CSS rule for the ListViewCaptionText class that is used by the area where the caption text goes in a list view. Because our CSS rule is defined after the default WRTKit CSS rule for ListViewCaptionText, our rule will be override the default one. Let's open up TravelCompanion.css and create this rule:


/* Place logo in list view caption */
.ListViewCaptionText {
    background: url("ListViewCaptionLogo.png") no-repeat;
    height: 35px;
}

We specified a height in addition to the background image. The height is needed so that the caption text area won't collapse to zero height since it won't contain any text. We're now ready to implement the createMainView() function:


// Creates the main view.
function createMainView() {
    // empty caption text to display the caption bar - custom background using CSS
    mainView = new ListView(null, "");
    
    // info
    var navToInfoButton = new NavigationButton(null, "NavInfoIcon.png", "Information");
    navToInfoButton.addEventListener("ActionPerformed", showInfoView);
    mainView.addControl(navToInfoButton);
    
    // converter
    var navToConverterButton = new NavigationButton(null, "NavConverterIcon.png", "Currency Converter");
    navToConverterButton.addEventListener("ActionPerformed", showConverterView);
    mainView.addControl(navToConverterButton);
    
    // weather
    var navToWeatherButton = new NavigationButton(null, "NavWeatherIcon.png", "Weather Forecast");
    navToWeatherButton.addEventListener("ActionPerformed", showWeatherView);
    mainView.addControl(navToWeatherButton);
    
    // settings
    var navToSettingsButton = new NavigationButton(null, "NavSettingsIcon.png", "Settings");
    navToSettingsButton.addEventListener("ActionPerformed", showSettingsView);
    mainView.addControl(navToSettingsButton);
}

We now have a main view and our widget even calls showMainView() when it starts but nothing will actually show up yet because we haven't implemented the showMainView() function yet. Let's do that now:


// Displays the main view.
function showMainView() {
    setMainViewSoftkeys();
    uiManager.setView(mainView);
}

The function calls the setMainViewSoftkeys() function to put the right softkey in the proper state and then asks the user interface manager to show the main view. Remember that we're already calling the showMainView() function from the init() function so we can go ahead and test this now in a PC browser, emulator or handset. Notice the custom list view caption image and the navigation button icons. Clicking the navigation buttons doesn't do anything yet because we don't have any other views yet and the functions to show other views except the main view aren't implemented. But then again, because we wrote stub functions we are also not getting any error messages.

Figure 1. Travel Companion main view

Settings view

It's a good idea to implement the settings view as early as possible because that allows you to test the rest of the widget more easily. In order to get the settings view up and running we'll need to do three things. First of all we'll need to implement the createSettingsView() function so that the widget actually has a settings view. Second, we'll need to implement the actions that can be performed in that view. There are two actions: "Save" and "Cancel". Of these, save is the tricker one and we'll implement it in a function that we will call saveSettingsClicked(). The cancel action will simply take us back to the main view and will be handled by the showMainView() function that we have already implemented. And third, we need to implement the showSettingsView() function so that clicking on the settings navigation button in the main view takes us to the settings view and sets it up correctly so that the view reflects the current preferences.

The settings view will have four sections that we will separate using a WRTKit Separator control. The first section lets the user select the home city and the whether the home city is in daylight saving time. Here we'll use a SelectionMenu control for the city selection and a SelectionList control with a single option for the daylight saving time option. We could have used a SelectionList with two options "Daylight saving time" and "Normal time". Since the selection would have been a single selection this would have resulted in two radio buttons. But partly for the sake of usability and partly to show how to do it, we'll implement it so that it's a single checkbox instead. Thus, we'll use a SelectionList with a single option but we'll get the checkboxes instead of radio buttons by putting the SelectionList in multiple selection mode. Because there's only one option there will only be one checkbox.

The second section is just like the first one but for the local city. The third section is for the temperature unit selection (Celsius or Fahrenheit) and we'll implement it as a single selection SelectionList control. And finally the fourth section is for the Save and Cancel FormButton controls.

The options for the selection controls will be created just before we create the controls themselves. The daylight saving time and temperature unit options are just static JSON defined option lists since we know what the options are. But for the city selection we'll create it dynamically by asking the business logic engine for a list of all the supported cities. We'll then create one option for each city.

Because we'll need to access the controls in the settings view from outside the creation function we'll need to create global references to them:


// Settings view controls.
var homeCitySelection;
var homeCityDSTSelection;
var localCitySelection;
var localCityDSTSelection;
var temperatureUnitSelection;

We can now implement the settings view creation function:


// Creates the settings view.
function createSettingsView() {
    // empty caption text to display the caption bar - custom background using CSS
    settingsView = new ListView(null, "");
    
    // create city options from cities array
    var cityOptions = [];
    var cities = engine.getCities();
    for (var i = 0; i < cities.length; i++) {
        cityOptions.push({ value: cities[i], text: cities[i].name });
    }
    
    // create DST option
    dstOptions = [ { value: true, text: "DST (+1h)" } ];
    
    // home city
    homeCitySelection = new SelectionMenu(null, "Home City", cityOptions);
    settingsView.addControl(homeCitySelection);
    
    // home city DST (using a multiple selection but only one option to get a single checkbox)
    homeCityDSTSelection = new SelectionList(null, "Daylight Saving Time (home)", dstOptions, true);
    settingsView.addControl(homeCityDSTSelection);
    
    // separator
    settingsView.addControl(new Separator());
    
    // local city
    localCitySelection = new SelectionMenu(null, "Local City", cityOptions);
    settingsView.addControl(localCitySelection);
    
    // local city DST (using a multiple selection but only one option to get a single checkbox)
    localCityDSTSelection = new SelectionList(null, "Daylight Saving Time (local)", dstOptions, true);
    settingsView.addControl(localCityDSTSelection);
    
    // separator
    settingsView.addControl(new Separator());
    
    // temperature unit
    var temperatureUnitOptions = [ { value: "c", text: "Celsius" }, { value: "f", text: "Fahrenheit" } ];
    temperatureUnitSelection = new SelectionList(null, "Temperature Unit", temperatureUnitOptions);
    settingsView.addControl(temperatureUnitSelection);
    
    // separator
    settingsView.addControl(new Separator());
    
    // save button
    var saveSettingsButton = new FormButton(null, "Save");
    saveSettingsButton.addEventListener("ActionPerformed", saveSettingsClicked);
    settingsView.addControl(saveSettingsButton);
    
    // cancel button
    var cancelSettingsButton = new FormButton(null, "Cancel");
    cancelSettingsButton.addEventListener("ActionPerformed", showMainView);
    settingsView.addControl(cancelSettingsButton);
}

Note that we used the actual city as the value for the city options. You can use anything you want for the value property for a selection control option and in this case we used the actual city object because it will come in handy later when we want to set the home and local cities that the user has selected to the engine.

Our save button calls the saveSettingsClicked() function we talked about earlier but we haven't created that function yet. We'll continue implementing the user interface by writing this function.

You'll recall that the business logic engine actually stores all the preferences so all we really need to do is read what values have been selected in the settings view, telling these values to the engine, calling the savePreferences() method in the engine to persist the settings, and then return to the main view.


// Called when the user clicks on the "save" button in the settings view.
function saveSettingsClicked() {
    // update the selected home city
    var selectedHomeCityOption = homeCitySelection.getSelected();
    if (selectedHomeCityOption != null) {
        engine.setHomeCity(selectedHomeCityOption.value);
    }
    
    // update the selected local city
    var selectedLocalCityOption = localCitySelection.getSelected();
    if (selectedLocalCityOption != null) {
        engine.setLocalCity(selectedLocalCityOption.value);
    }
    
    // update home and local city DST
    // there's only one checkbox but we are using a multiple-selection menu.
    // if the selected array has one element then the checkbox is checked.
    engine.setHomeCityDST(homeCityDSTSelection.getSelected().length == 1);
    engine.setLocalCityDST(localCityDSTSelection.getSelected().length == 1);
    
    // update temperature unit
    var selectedTemperatureUnitOption = temperatureUnitSelection.getSelected();
    if (selectedTemperatureUnitOption != null) {
        engine.setTemperatureUnit(selectedTemperatureUnitOption.value);
    }
    
    // save settings
    engine.savePreferences();
    
    // go back to the main view
    showMainView();
}

We're writing defensive code here and making sure that the selection controls actually have anything selected at all. This should never be possible with single selection control if they have one option already selected, but we'll use the principle of "better safe than sorry".

Notice that the daylight saving time selection is checked by asking for the selected options array (we're using a multiple selection control) and checking if there is exactly one selected option. We don't care about the actual selected option because we know that there's only one. Either it's selected or it's not.

We have now implemented two of the three steps we said were necessary to get the settings view fully functional. Let's implement the final step: showing the view. Showing the settings view is done by first updating the settings view controls to reflect the current configuration, setting the right softkey to the state that it should be in for the settings view and then asking the user interface manager to show the settings view.


// Displays the settings view.
function showSettingsView() {
    // update settings view controls to match current configuration
    // the DST selected sets are either the options array with its single option or an empty array
    
    // home city and DST setting
    homeCitySelection.setSelected(homeCitySelection.getOptionForValue(engine.getHomeCity()));
    homeCityDSTSelection.setSelected(engine.getHomeCityDST() ? homeCityDSTSelection.getOptions() : []);
    
    // local city and DST setting
    localCitySelection.setSelected(localCitySelection.getOptionForValue(engine.getLocalCity()));
    localCityDSTSelection.setSelected(engine.getLocalCityDST() ? localCityDSTSelection.getOptions() : []);
    
    // temperature unit
    temperatureUnitSelection.setSelected(temperatureUnitSelection.getOptionForValue(engine.getTemperatureUnit()));
    
    setSettingsViewSoftkeys();
    uiManager.setView(settingsView);
}

We're using the convenient getOptionForValue() function that all selection controls have to retrieve the right option to select for each of the selection controls. The way we handle the daylight saving time selection controls may seem a little strange at first but remember that we only have a single option and thus if that option should be selected then the selected options array is identical with the options array - both are arrays containing the one and same option. Selection controls always copy the options and selected options arrays when the setOptions() and setSelected() methods are called so there is no problem in passing the options array as the selected options array.

The settings view is now completed. You can try it out and play with it, change the settings, open it up again, change some more settings, cancel the changes, etc. to verify that it's working as it should.

Figure 2. Travel Companion settings view