Building Custom Widgets with the Zinzala SDK

In this article, we will explore the creation of a custom widget for an automotive application using the Zinzala SDK. Introduced in April 2003, Zinzala is a BeOS flavored Software Development Kit for QNX. It has improved since then, taking on small features inspired by the Symbian OS. Because Zinzala brings in the benefits of object oriented programming, it can leverage the quality of your QNX RTOS based embedded products.

Intended audience

This article is intended for C++ developers who are familiar with Graphical User Interface development. No specific knowledge of QNX is required. However, some knowledge of the Zinzala SDK will be useful, so check out the updated version of Introducing Zinzala.

Note to the reader


The latest version of the Zinzala SDK can be downloaded from the hexaZen web site. In the demo folder of this release, there is a ready to build version of the application described in this article. Feel free to play with the demo after reading this article.


1. Overview

Over the past few years, QNX has been making strong in-roads into the automotive market. Cars manufactured by Audi, DaimlerChrysler and Saab are currently being shipped with infotainment systems built on top of the RTOS made by QNX Software Systems (QSS). The recent purchase of QSS by Harman International Industries can be seen as proof of the pioneering position taken by QNX over the recent years. This endorsement by Harman is a sign of the quality of this embedded Operating System.

Like many other embedded systems, an infotainment system relies on a custom Graphical User Interface (GUI); most often built on top of an existing GUI framework. For example, in the smartphone world, the Symbian OS provides Uikon, which is a basic framework that each licensee can use to build their own set of widgets or customize the existing ones. In this very competitive market, differentiating your products from the concurrent ones is an absolute priority. Through different UI styles and attractive application offerings, each Symbian licensee will try to inforce its own branding. This also applies to the automotive industry as well. However, the look of the infotainment may not be at the top of the buyer’s list of considerations during a car purchase.

In this article, we are going to explore the creation of a custom widget for QNX using the basic components provided by the Zinzala SDK. Here is a screenshot of what we will build:



This widget is only one element of a multimedia console, much like the one we would expect to find in any kind of multimedia player. However, its implementation will offer us an interesting ground to explore in this article.

The big picture

Let’s suppose that we are building the application framework for a QNX based in-car device, similar to the ones made by G-Net. Our console widget is going to be one of the many custom widgets that will be available to the application developers. For instance, it could be used by the music and video player. Each application will use class derivations in order to program the console widget to perform specific actions when the user interacts with the GUI. For instance, causing the current audio track to start playing with the user presses the Play button.

The application framework (let’s call it Carrozza) that we will be using to build our applications is not limited to the GUI side of the embedded system. It could also provide libraries for audio playback, data persistence or networking. This framework is simply an extra layer, intended to unify the development of every application by offering architects and developers all the components they will need. Of course, this doesn’t forbid the use of some of the classes available from the lower layers. Providing a set of components designed to fit the common needs of all applications, help reduce the risk of duplication of code. This also leads to a modular solution where components can be selected, combined and reused. This provides a powerful, consistent and efficient platform.

Here’s a representation of the layering:



Although Photon is part of the QNX platform, I have placed it on its own layer to show Zinzala’s dependencies. We will expect application developers to only go as low as the Zinzala layer, which is composed of Cincinella and Farfalla. Low level components, such as a CD player control class, will probably require access to the QNX API. However, they will be made available to the application developers from the top layer.

Zooming in …

Now that we have pitched the overall view, let’s go back to the console widget. We will walk through the construction of the widget, and then show how to integrate it to a simple test application. Here’s what we are going to look at:

  • How to create a shared object from the raw skin graphics
  • How the widget will use the skin
  • How to render the console and each button’s states
  • How to render an animation effect for the active button
  • How to handle user inputs
  • How to support the change of skin on the fly

Graphics

All the graphics we will be using have been created by hexaZen for this article. In a real system, images such as these would be provided to the development teams by the UI designers to ensure consistency across the entire product line.

Here is a green skin for example:

Button states (normal,
dimmed and pressed)


64×64 pixels
Active button animation


64×64 pixels
Button labels (normal)



25×25 pixels
Button labels (dimmed)



25×25 pixels
Console background 321×98 pixels

The following figure indicates the position of each of the 4 buttons:



Like many other custom UI, the size and position of our UI elements will be fixed, even when we change skin. This means that if we were to reuse the same widget on a different product, perhaps one with a bigger screen, we may have to adapt it instead of just reusing it. A better solution would be to use a scalable UI framework, which would allow each element of the UI to adapt its position and size according to the form factor and UI layout of the product. Such frameworks are not widely in use, although they do increase the reusability of the applications (develop once for any screen size and layout) from a product to another one. This is about to change for some embedded systems such as the smartphones based on Symbian Series 60.

A few considerations

Before we get down to business, let’s talk a little about the coding conventions and practices that we are going to use. The coding style might be new to you, unless you have already been exposed to some Symbian programming. We will also be using exceptions for error handling associated with two very interesting things called two-phase construction and cleanup stack.

The need for both mechanisms arise from the fact that an embedded system must be stable and must run for a long period of time. This puts pressure on the applications to avoid memory leak, and to be robust to situations where resources are low. Error conditions must also be handled in a safe and appropriate fashion.

The cleanup stack provides a way of cleaning objects allocated on the heap, whose pointers will be lost when an exception occurs. Because a class destructor will not be called on a partially created object, a C++ constructor should not throw an exception in order to avoid memory leak. It is recommended that you use a two-phase construction. This also gives the opportunity to push objects on the cleanup stack. If you have some Symbian experience, it will look very familiar…

Since we are only focusing on building a custom widget, we are not going to discuss the purpose and usefulness of the two-phase construction and cleanup stack in detail. Instead, you may want to read the first Zinzala newsletter or a document on Symbian Application Development to learn more about them.

The code sequences that we will be addressing in this article have been annotated in order to explain particular points. Don’t get confused if it doesn’t look like it could compile πŸ˜‰ A link to the complete version will be given before each annotated sequence.

2. Implementing skinability

In order to support skinability in the widget, we need to answer these two questions:

  1. How do I transform a graphic into something useful for the widget?
  2. How will the widget access it ?

Turning a GIF into a resource

Instead of letting the widget load each graphic file, we are going to process and store them into one shared object (a .so). This solution saves the widget the need of transforming each image from a GIF file to a cBitmap object each time we construct it. It also presents a more elegant solution, since all the graphics will be in one file. Now, we are going to transform all our graphics into a set of resources. For that purpose we are going to create a simple tool, which outputs given GIF files into a C++ file to be compiled. We will be calling it img2res.

Loading an image into a cBitmap

First, we will see how we can load a given graphic file into a cBitmap object which defines a basic bitmap. The current version of the Zinzala SDK does not provide any mechanism to do so in the way BeOS was using translators, so we are going to use a little bit of pure Photon API.

Here’s the implementation of the function LoadBitmap():

cBitmap *LoadBitmap(const tChar *aFilename)
{
   PhImage_t*     lImage = NULL;
   PiLoadInfo_t   lInfo;
   PiIoHandler_t* lHandler;
   const tChar*   lExt;
   tUint16        lPos;

   // extract extension of the image
   if(sStrTools::FindLast(aFilename,'.',&lPos))
   {

In order to load an image, we need to find out what the image format is from its file extension. By using the static class sStrTools, which provides many methods to manipulate strings, we can search for the position of the last dot in the string. The file extension will be everything after the last dot. If that character cannot be found, sStrTools::FindLast() will return false.

      // get the handler for the image format
      lExt     = &aFilename[lPos+1];
      lHandler = PiIoGetHandlerByExt(lExt);

      if(lHandler)
      {

Once we have got the file extension in the variable lExt, we can call the Photon function PiIoGetHandlerByExt() to give us the handler that will be used to load the image. If the file format is not supported lHandler will be set to NULL. Photon currently provide only handlers for GIF, JPEG, BMP and PNG.

         memset(&lInfo,NULL,sizeof(PiLoadInfo_t));

         lInfo.flags           = Pi_IO_SHMEM;
         lInfo.shmem_threshold = 100;
         lInfo.handler         = lHandler;
         lInfo.filename        = aFilename;

         // load the image
         lImage = PiLoadImage(&lInfo);

PiLoadImage() loads the image from the file into a Photon image structure PhImage_t. A structure of type PiLoadInfo_t is used as an argument to pass all the required information to the function, such as the filename and the handler to be used. The flag Pi_IO_SHMEM indicates that the image to be created should use shared memory, if its size is above a given threshold. Using shared memory for the image data, which are rows of pixels, is a nice idea when the bitmap is to be displayed. The Photon server will access the image’s data in the shared memory when it is draw, instead of having the application send it. This is not really required here, since we will not be displaying the image.

         if(lImage)
         {
            cBitmap* lBitmap;

            // create a cBitmap and return it
            lBitmap = new cBitmap(lImage);
            if(!cBitmap::VerifyD(lBitmap))
               return lBitmap;
            else
               return NULL;
         }
         else
            return NULL;

If the image was successfully loaded into lImage, we can use it to create a cBitmap object. The static method VerifyD() checks the validity of the object. If the object has been allocated and is invalid, it will be deleted by that method. Otherwise, the method will return kErrNone. Since the cBitmap assume ownership of the Photon image, we don’t need to delete it.

      }
      else
         return NULL;
   }
   else
      return NULL;
}

From cBitmap to resource

Earlier, we decided that all the graphics are going to be stored as resources in a shared object. For that reason, we need to store all the required information in a C++ file. The following type will provide everything we need to instantiate a cBitmap object:

typedef struct tBitmapResource {
   tUint32  iWidth;   // width of the bitmap
   tUint32  iHeight;  // height of the bitmap
   tUint32  iIndex;   // index of the transparent color in the palette
   tUint16  iColors;  // number of color in the palette
   tUint32  iLength;  // length in bytes of the bitmap's data
   tUint8*  iBits;    // bitmap's data
   tUint32* iPalette; // palette of colors
} tBitmapResource;

Here’s the function DumpBitmapToFile() that will output a bitmap into a C++ compilable form:

void DumpBitmapToFile(cFile &aHeader,cFile &aCode,cBitmap *aBitmap,tColor *aPalette,
                      tUint16 aColors,const char *aName)
{
   tUint16   lWidth,lHeight;
   tUint8    lIndex;
   tUint32   lLength;
   tUint8*   lBits;
   PgColor_t lColor;

   aBitmap->GetTransparent(lIndex);
   aBitmap->GetSize(lWidth,lHeight);
   lLength = aBitmap->BitsLength();
   lBits   = aBitmap->GetBits();

First, we retrieve the size, transparency and pixel data from aBitmap. The local variable lLength will contain the length of the data in bytes.

   aHeader.Write("extern const tBitmapResource kRscBitmap%s;\n\n",aName);

aHeader is a reference to the cFile object pointing to the C++ header file. Here, we define a constant of type tBitmapResource for the bitmap we are currently outputting.

   aCode.Write("tUint8 kBitmap%sBits [] = {\n\n\t",aName);

   for(tUint16 k=0;k<lLength;k++)
   {
      lIndex = lBits[k];

      if(k + 1 == lLength)
         aCode.Write("0x%02x",lIndex);
      else
         aCode.Write("0x%02x,",lIndex);

      if(!((1+k) % 16))
         aCode.Write("\n\t");
   }

   aCode.Write("};\n\n");

Writing each pixel index color is fairly easy. Just as a reminder, we are using palette based graphics. We simply loop over all the pixels and write their hexadecimal value. For better legibility, we output these values into lines 16 pixels wide.

   aCode.Write("tUint32 kBitmap%sPalette[%d] = {\n\n\t",aName,aColors);

   for(tUint16 i=0;i<aColors;i++)
   {
      lColor = PgARGB(aPalette[i].iAlpha,aPalette[i].iRed,aPalette[i].iGreen,
	                   aPalette[i].iBlue);
      aCode.Write("0x%06x,",(tUint32)lColor);
      if(!((1+i) % 8))
         aCode.Write("\n\t");
   }

Outputting the palette of color used by the bitmap is not very complicated. However, there is a little trick to know. If we write the RGB color in the form of an unsigned 32 bit integer, we can use them directly when instantiating the bitmap. By using the Photon macro PgARGB(), we insure that the color will be coded in a format that can be directly understood by the underlying Photon. To improve the legibility of the C++ file, we will also output only 8 colors per lines.

   aCode.Write("\n};\n\n");

   aCode.Write("const tBitmapResource kRscBitmap%s\t= {\n",aName);
   aCode.Write("\t%d,%d,0x%02x,%d,%d,kBitmap%sBits,kBitmap%sPalette\n};\n",lWidth,
               lHeight,lIndex,aColors,lLength,aName,aName);
}

Last but not least, in the C++ file we declare the same constant that we have previously defined in the header file. This time, we fill in all the data specifics to the bitmap.

Now, how will it look like in the C++ file ?

tUint8 kBitmapStopDBits [] = {

   0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,
   0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   ...
   ...
   ...
   0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,
   0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00
};

tUint32 kBitmapStopDPalette[4] = {
   0x88c188,0x82bd82,0x77b577,0x6bad6b,
};

const tBitmapResource kRscBitmapStopD	= {
   25,25,0x02,4,800,kBitmapStopDBits,kBitmapStopDPalette
};	

The final touch to img2res

What’s left to do in img2res is to use the 2 functions we implemented above in the main:

int main(int argc,char **argv)
{
   tColor   lPalette[256];
   tChar    lFilename[256];
   cBitmap* lBitmap;
   cFile    lHeader;
   cFile    lCode;
   tChar*   lName;
   tErr     lErr;

   if(argc<3)
   {
      printf("Usage : <filename> (<image> <label>)+\n");
      return 1;
   }

   lName = argv[1];

   // create header file
   sprintf(lFilename,"%s.h",lName);
   lErr = lHeader.Open(lFilename,kFileWrite | kFileTrunc);
   if(lErr)
      return 1;

   // create code file
   sprintf(lFilename,"%s.cpp",lName);
   lErr = lCode.Open(lFilename,kFileWrite | kFileTrunc);
   if(lErr)
   {
      lHeader.Close();
      return 1;
   }	

To create and fill both C++ and header files, we use the class cFile. This class provides writing and/or reading in a file.

   lHeader.Write("#ifndef _RES_%s\n",lName);
   lHeader.Write("#define _RES_%s\n\n",lName);
   lHeader.Write("#include <Cincinella/Types.h>\n\n");
   lHeader.Write("using namespace zSDK::Cincinella;\n\n");
   lHeader.Write("typedef struct tBitmapResource {\n");
   lHeader.Write("\ttUint32  iWidth;\n");
   lHeader.Write("\ttUint32  iHeight;\n");
   lHeader.Write("\ttUint32  iIndex;\n");
   lHeader.Write("\ttUint16  iColors;\n");
   lHeader.Write("\ttUint32  iLength;\n");
   lHeader.Write("\ttUint8*  iBits;\n");
   lHeader.Write("\ttUint32* iPalette;\n");
   lHeader.Write("} tBitmapResource;\n\n");

In the header file we output the definition of the type tBitmapResource.

   lCode.Write("#include \n\n");
   lCode.Write("#include <%s.h>\n\n",lName);
   lCode.Write("using namespace zSDK::Cincinella;\n\n");

   for(int i=2;i<argc;i+=2)
   {
      printf("loading %s ...\n",argv[i]);
      lBitmap = LoadBitmap(argv[i]);

For each graphic file given in the command line, we will try to load it. If this is successful and the bitmap is palette based, then we will process it further:

      if(lBitmap)
      {
         if(lBitmap->GetColorSpace() == eSpaceColor8Bit)
         {
            tUint16 lWidth,lHeight;
            tUint16 lColors = lBitmap->CountColors();

            lBitmap->GetSize(lWidth,lHeight);

            printf("bitmap is %dx%d pixels and have %d colors\n",lWidth,lHeight,lColors);

            if(!lBitmap->GetPalette(lPalette))
            {
               DumpBitmapToFile(lHeader,lCode,lBitmap,lPalette,lColors,argv[i+1]);
               printf("dumped\n");
            }
            else
               printf("failed to get the colors palette\n");
         }
         else
            printf("sorry this isn't a palette based image!\n");
      }
      else
         printf("failed\n");	
   }

   // fill files with closing stuff
   lHeader.Write("\n");
   lHeader.Write("#endif\n");

   // and close them
   lHeader.Close();
   lCode.Close();

   return 0;
}

To simplify things, I created a simple shell script to rebuild a C++ file from the graphics whenever we needed to. We would need to do this when the graphic team modifies some of them, or when we create a new skin:

#/bin/sh

img2res Bitmaps \
   Images/background.gif  Background   \
   Images/dimmed.gif      Dimmed       \
   Images/normal.gif      Normal       \
   Images/pressed.gif     Pressed      \
   Images/animate2.gif    Animate1     \
   Images/animate3.gif    Animate2     \
   Images/animate4.gif    Animate3     \
   Images/forward.gif     ForwardN     \
   Images/forward_dim.gif ForwardD     \
   Images/pause.gif       PauseN       \
   Images/pause_dim.gif   PauseD       \
   Images/play.gif        PlayN        \
   Images/play_dim.gif    PlayD        \
   Images/rewind.gif      BackwardN    \
   Images/rewind_dim.gif  BackwardD    \
   Images/stop.gif        StopN        \
   Images/stop_dim.gif    StopD 

When creating a new skin, even if the name of each graphic file needs to be changed, the resource name should stay the same in order to keep the necessary adaptations from a skin to another as small as possible.

You can have a look at the complete source code of img2res, as well as an example of the header and source file generated by this tool.

Skin as shared object

Now that we have all our graphics neatly packed away in a C++ file, it is time to see how we are going to integrate it into to a shared
object. To help us load a shared object later on, we will be using the class cAddOn, provided by the Zinzala SDK. This class is a simple wrapper around the famous function dlopen().

Shared object contents

Each skin shared object must provide a function through which a given resource can be retrieved. A simple tUint16 is enough to identify each resource defined in the C++ file we have created from the graphics:

const tBitmapResource *gResources[] = {
   &kRscBitmapBackground,
   &kRscBitmapDimmed,
   &kRscBitmapNormal,
   &kRscBitmapPressed,
   &kRscBitmapAnimate1,
   &kRscBitmapAnimate2,
   &kRscBitmapAnimate3,
   &kRscBitmapForwardN,
   &kRscBitmapForwardD,
   &kRscBitmapPauseN,
   &kRscBitmapPauseD,
   &kRscBitmapPlayN,
   &kRscBitmapPlayD,
   &kRscBitmapBackwardN,
   &kRscBitmapBackwardD,
   &kRscBitmapStopN,
   &kRscBitmapStopD
};

tUint16 kResourcesCount = 17;

extern "C" const tBitmapResource *GetResource(tUint16 aResourceID)
{
   if(aResourceID < kResourcesCount)
      return gResources[aResourceID];
   else
      return NULL;
}

The array gResources contains a pointer to each graphic resource that we created earlier. The function GetResource() accesses the array and returns a resource from a position in the array.

You may have a look at the skin C++ file. The functions _AddOn_* are mandatory since they are required by the class cAddOn.

Loading the shared object

To load and access the skin shared object, we are now going to derive the class cAddOn and add to it a method that will
retrieve the resource when given a resource ID. It will simply use the GetResource() function defined in every skin shared object.

Here’s the declaration of the class:

class cDSkin : public cAddOn
{
   public:
      cDSkin(const tChar *aPath);

      const tBitmapResource &GetResourceL(tSkinResource aResource);

   private:
      const tBitmapResource *(*iFunction)(tUint16 aResourceID);
};

The member data iFunction will be set in the constructor to point to the shared object function we implemented earlier. The method GetResourceL() relies on it to get the tBitmapResource associated with the requested resource. Note that if the resource doesn’t exist in the skin, this method will throw an exception. This is implied by the L, as in Leave, that is added to the end of its name. This is a common practice in Symbian. This naming convention allows developers to easily recognize which methods can throw an exception.

The type tSkinResource defines all the resources that should be accessible from any skin:

enum tSkinResource {
   eResBackground = 0,
   eResDimmed,
   eResNormal,
   eResPressed,
   eResAnimate1,
   eResAnimate2,
   eResAnimate3,
   eResForwardN,
   eResForwardD,
   eResPauseN,
   eResPauseD,
   eResPlayN,
   eResPlayD,
   eResBackwardN,
   eResBackwardD,
   eResStopN,
   eResStopD
};

Here’s the code of the class constructor:

cDSkin::cDSkin(const tChar *aPath)
   : cAddOn(aPath)
{
   if(!iFault)
   {
      iFunction = (const tBitmapResource *(*)(tUint16 aResourceID))Find("GetResource");
      if(!iFunction)
         iFault = kErrNotFound;
   }
}

If the skin shared object is correctly loaded, the data member iFault will be kErrNone (whose value is 0). We can then use the method cAddOn::Find() to find the function GetResource() implemented in the shared object. If we cannot find it, iFault will indicate the error. It is the responsability of the calling code to check the validity of the cDSkin object. Because we have used extern "C" when defining the function GetResource(), we don’t have to deal with mangled name.

The method GetResourceL() shows how the resource will be retrieved from the shared object:

const tBitmapResource &cDSkin::GetResourceL(tSkinResource aResource)
{
   if(!iFault)
   {
      const tBitmapResource *lRes;

      lRes = (*iFunction)((tUint16)aResource);

      if(lRes)
         return *lRes;
      else
         throw uException(kErrNotFound,"cDSkin::GetResourceL");
   }
   else
      throw uException(iFault,"cDSkin::GetResourceL");
}

If the skin is not valid or if we cannot find the specified skin, an exception will be raised. The class uException is the basic exception class in the Zinzala SDK. It takes an error code and a string indicating where the exception was raised as its arguments.

You may have a look at the complete header and C++ files if you like.

3. The cDConsoleView class

To build the multimedia console widget, we are going to derive the widget cDrawView. This class defines and implements a very basic type of widget, making it the perfect starting point for building a custom widget. It provides various drawing methods that we will be using to render the console contents.

Here’s the declaration of cDConsoleView:

class cDConsoleView : public cDrawView
{
   public:
      static cDConsoleView *NewL(cDSkin &aSkin);
      virtual ~cDConsoleView();

      void SetState(tConsoleState aState);
      tConsoleState GetState() const {return iState;};
	
   protected:
      virtual void PlayPressed(tBool aActive) 	{};
      virtual void PausePressed(tBool aActive) 	{};
      virtual void StopPressed(tBool aActive) 	{};
      virtual void FwdPressed(tBool aActive) 	{};
      virtual void BwdPressed(tBool aActive) 	{};

   protected:
      void OnScreen();
      void Beat();
	
      void MouseUp(uPoint aPoint);
      void MouseDown(uPoint aPoint);
      void OutBound(uPoint aPoint);

   protected:
      cDConsoleView();
      virtual void ConstructL(cDSkin &aSkin);

   private:
      tUint8 FindButton(uPoint aPoint) const;
      void Render(tUint8 aButton,tBool aBackground = false);
      void Pressed(tUint8 aButton);
      void Invoked();

   private:
      typedef struct tButton; 

   private:
      tConsoleState  iState;           // Console state
      cBitmap*       iBackground;      // Background bitmap
      cBitmap**      iBStates;         // Button's states bitmaps
      cBitmap**      iBLabelsN;        // Button's labels bitmaps (non dimmed)
      cBitmap**      iBLabelsD;        // Button's labels bitmaps (dimmed)
      tButton*       iButtons;         // Button's data
      tUint8         iPressedButton;   // ID of the button currently pressed
      tUint8         iActiveButton;    // ID of the button currently active
      tUint8         iPrevState;       // Previous state of the current pressed button
      tInt8          iActiveDir;       // Direction of the Active button animation
};

The console defines five callback methods to be implemented by derived classes. PlayPressed() will respond to a user action on the Play button, PausePressed() on the Pause button, and so on. The console implements how to handle user actions in the methods MouseDown(), MouseUp() and OutBound().

The state of the console can be changed at anytime with the method SetState(). The console itself will not change its state, however; the state will be used when rendering the console. Here’s the definition of tConsoleState:

enum tConsoleState {
   ePlaying,
   eStopped,
   ePaused,
   eForward,
   eBackward
};

Construction & Destruction

Implementing a two-phase construction is not difficult. However, it does require a bit more coding than just a standard C++ constructor. The following methods are involved in the construction of an object: cDConsoleView(), ConstructL() and NewL(). NewL() is a factory function, the frontend of the creation of an object. Instead of using the new C++ operator, we will call this method to instantiate a new object. To inforce that NewL() is the only possibilty for object instantiation, we have set the constructor as a protected method.


Let’s see the code of the C++ constructor cDConsoleView():

cDConsoleView::cDConsoleView()
   : cDrawView(uRect(kConsoleWidth,kConsoleHeight),"console",kFollowNone,
               kFlgBeatNeeded | kFlgInteractive | kFlgOffscreen)
{
   iBackground    = NULL;
   iBStates       = NULL;
   iBLabelsN      = NULL;
   iBLabelsD      = NULL;
   iButtons       = NULL;
   iPressedButton = kButtonsCount;
   iActiveButton  = kButtonsCount;
   iActiveDir     = 1;
   iState         = eStopped;
	
   if(!iFault)
   {
      iButtons = new tButton[kButtonsCount];
      if(!iButtons)
         iFault = kErrOutOfMemory;
      else
      {
         iBStates = new (cBitmap *)[kStatesCount];
         if(!iBStates)
            iFault = kErrOutOfMemory;
         else
         {
            memset(iBStates,0,sizeof(cBitmap *) * kStatesCount);
            iBLabelsN = new (cBitmap *)[kLabelsCount];
            if(!iBLabelsN)
               iFault = kErrOutOfMemory;
            else
            {
               memset(iBLabelsN,0,sizeof(cBitmap *) * kLabelsCount);
               iBLabelsD = new (cBitmap *)[kLabelsCount];
               if(!iBLabelsD)
                  iFault = kErrOutOfMemory;
               else
                  memset(iBLabelsD,0,sizeof(cBitmap *) * kLabelsCount);
            }
         }
      }
   }
}

Using the two-phase construction idiom doesn’t mean that no memory allocation should ever be done in the C++ constructor. But if we choose to do so, we must make sure that it can still be deleted if the construction fails and that the partially constructed object is destroyed.

After some initialization, we try to allocate the 4 arrays that will contain all the buttons data (iButtons) and the various corresponding bitmaps (iBStates, iBLabelsN, iBLabelsD). If any of the memory allocation fails, we will mark the object as faulty by setting the data member iFault to kErrOutOfMemory. This member variable is inherited from deep inside the Zinzala SDK (class pBase from the Cincinella Kit). Because the constructor of the base class cDrawView could lead to an invalid object, it is recommended that you test whether or not iFault already indicates a fault. There is no point in allocating more memory if we going to delete the object right away because it is faulty.

The flags passed to the base class cDrawView sets a couple of behaviors for the widget. kFlgBeatNeeded indicates that the widget will make use of the window beat. At a given interval, the method Beat() of the widget will be called. We need this flag to render the animation of the active button. kFlgInteractive specifies that the widget will accept user inputs. The flag kFlgOffscreen is specific to the widget cDrawView, and indicates that we will be using an offscreen bitmap to render the widget in order to avoid any flickering. For instance, when drawing the animation.

Here’s the definition of the various constants we have used in the constructor:

const tUint16 kConsoleWidth   = 321;
const tUint16 kConsoleHeight  =  98;
const tUint8  kButtonsCount   =   4;
const tUint8  kStatesCount    =   7;
const tUint8  kLabelsCount    =   5;

We also need to define tButton whose implementation is hidden from the class users:

typedef struct cDConsoleView::tButton 
{
   uRect    iBFrame; // frame of the button
   uRect    iLFrame; // frame of the label
   tUint8   iState;  // button state
   tUint8   iLabel;  // label displayed on the button
	
} tButton;

This type holds all the button specific information, such as its position within the console or its state.

The method ConstructL() contains the rest of the widget’s construction where exceptions can be raised. Here’s its
implementation:

void cDConsoleView::ConstructL(cDSkin &aSkin)
{
   uPoint lPoint;

   // If the view is already faulty, we will leave right away
   sEnv::LeaveIfError(iFault);

Because this method will be called right after the class constructor is called, the widget might already be faulty. If this is the case, there is no need to proceed, we will leave right away.

   // Instantiate the background bitmap
   iBackground             = CreateBitmapFromResourceL(aSkin.GetResourceL(eResBackground),false);

   // Instantiate the button bitmaps
   iBStates[kStateNormal]  = CreateBitmapFromResourceL(aSkin.GetResourceL(eResNormal));
   iBStates[kStateDimmed]  = CreateBitmapFromResourceL(aSkin.GetResourceL(eResDimmed));
   iBStates[kStatePressed] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResPressed));
   iBStates[kStateActive]  = iBStates[kStateNormal]; // use same bitmap as the normal state
   iBStates[kStateActive1] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResAnimate1));
   iBStates[kStateActive2] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResAnimate2));
   iBStates[kStateActive3] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResAnimate3));
   // Instantiate the label bitmaps
   iBLabelsN[kLabelBwd]    = CreateBitmapFromResourceL(aSkin.GetResourceL(eResBackwardN));
   iBLabelsN[kLabelFwd]    = CreateBitmapFromResourceL(aSkin.GetResourceL(eResForwardN));
   iBLabelsN[kLabelStop]   = CreateBitmapFromResourceL(aSkin.GetResourceL(eResStopN));
   iBLabelsN[kLabelPlay]   = CreateBitmapFromResourceL(aSkin.GetResourceL(eResPlayN));
   iBLabelsN[kLabelPause]  = CreateBitmapFromResourceL(aSkin.GetResourceL(eResPauseN));
   iBLabelsD[kLabelBwd]    = CreateBitmapFromResourceL(aSkin.GetResourceL(eResBackwardD));
   iBLabelsD[kLabelFwd]    = CreateBitmapFromResourceL(aSkin.GetResourceL(eResForwardD));
   iBLabelsD[kLabelStop]   = CreateBitmapFromResourceL(aSkin.GetResourceL(eResStopD));
   iBLabelsD[kLabelPlay]   = CreateBitmapFromResourceL(aSkin.GetResourceL(eResPlayD));
   iBLabelsD[kLabelPause]  = CreateBitmapFromResourceL(aSkin.GetResourceL(eResPauseD));

The function CreateBitmapFromResourceL() that we will see later, instantiates a cBitmap from the resource contained in the skin. If something goes wrong, it will leave.

   tUint16 lX = kPositionX;

   // initialise the buttons data (all are dimmed)
   for(tUint8 i=0;i<kButtonsCount;i++)
   {
      iButtons[i].iState = kStateDimmed;
      iButtons[i].iLabel = i;
      iButtons[i].iBFrame.Set(lX,kPositionY,lX + kButtonWidth,kPositionY + kButtonHeight);
      lPoint.Set(lX + kButtonWidth / 2,kPositionY + kButtonHeight / 2);
      lPoint.iX -= kLabelWidth / 2;
      lPoint.iY -= kLabelHeight / 2;
      iButtons[i].iLFrame.Set(lPoint.iX,lPoint.iY,lPoint.iX+kLabelWidth,lPoint.iY+kLabelHeight);
      lX += kButtonWidth + kSpacing;
   }

Beside initializing the button’s data, we compute its position within the console and its label’s position, which is always centered within the button.

   // The play button is not dimmed by default
   iButtons[kButtonPlay].iState = kStateNormal;
}

Here’s the definition of the various constants we have used in ConstructL():

const tUint16 kButtonWidth    = 64;
const tUint16 kButtonHeight   = 64;
const tUint16 kLabelWidth     = 25;
const tUint16 kLabelHeight    = 25;
const tUint16 kPositionX      = 18;
const tUint16 kPositionY      = 17;
const tUint16 kSpacing        = 10;

const tUint8  kButtonBwd      =  0;
const tUint8  kButtonPlay     =  1;
const tUint8  kButtonPause    =  2;
const tUint8  kButtonFwd      =  3;

const tUint8  kStateDimmed    =  0;
const tUint8  kStatePressed   =  1;
const tUint8  kStateNormal    =  2;
const tUint8  kStateActive    =  3;
const tUint8  kStateActive1   =  4;
const tUint8  kStateActive2   =  5;
const tUint8  kStateActive3   =  6;

const tUint8  kLabelBwd       =  0;
const tUint8  kLabelPlay      =  1;
const tUint8  kLabelPause     =  2;
const tUint8  kLabelFwd       =  3;
const tUint8  kLabelStop      =  4;

In order to instantiate the class cDConsoleView, we now need to implement the method NewL(). It is simple and short:

cDConsoleView *cDConsoleView::NewL(cDSkin &aSkin)
{
   cDConsoleView *lView = new cDConsoleView();
   cDConsoleView::VerifyLC(lView);
   lView->ConstructL(aSkin);
   sCleanupStack::PopL(lView);
   return lView;
}

As you can see, we first construct a new object using the standard C++ operator new. Then we use the method VerifyLC(), inherited from the base class pBase, to verify that the object is valid. The LC at the end of the method’s name indicates that it can leave. If it doesn’t leave, the object in lView will be left on the cleanup stack. We need the object to be on the stack before we call the ConstructL() method on the object. Since this method can leave, we need to make sure that it will be deleted automatically when the exception is handled. If there is no problem in the construction of the object, we will pop the object from the stack then return it.

We already saw the method cSkin::GetResourceL() earlier, so here’s CreateBitmapFromResourceL():

cBitmap *CreateBitmapFromResourceL(const tBitmapResource &aRes,tBool aTransparent = true)
{
   tErr     lErr;
   cBitmap *lBitmap = NULL;

   lBitmap  = new cBitmap(aRes.iWidth,aRes.iHeight,eSpaceColor8Bit,true,aRes.iColors);
   lErr     = cBitmap::VerifyD(lBitmap);
	
   if(!lErr)
   {	
      lBitmap->SetPalette(aRes.iPalette);
      lBitmap->SetBits(aRes.iBits,aRes.iLength,0);
      if(aTransparent)
         lBitmap->MakeTransparent(aRes.iIndex);
      return lBitmap;
   }
   else
      throw uException(lErr,"CreateBitmapFromResourceL()");
}

Its implementation is quite straightforward. A bitmap is created with the size and number of colors required. Then we use VerifyD() to test the validity of the bitmap. If the bitmap is valid, we will set the palette of colors and copy its pixel data. If invalid, the object will be deleted by VerifyD(). Now, if the bitmap shall be transparent, we will apply the transparency. If the bitmap cannot be created succesfully, due to a lack of memory or some other reason, the method will throw an exception.

In the class destructor, we need to delete all the memory that was allocated (if any). Here is the implementation:

cDConsoleView::~cDConsoleView()
{
   if(iButtons)
      delete [] iButtons;

   if(iBackground)
      delete iBackground;
	
   if(iBStates)
   {
      for(tUint8 i=0;i<kStatesCount;i++)
         if(iBStates[i] && i!=kStateActive) // Active and Normal states share the same bitmap
            delete iBStates[i];
	
      delete [] iBStates;
   }

   if(iBLabelsN)
   {
      for(tUint8 i=0;i<kLabelsCount;i++)
         if(iBLabelsN[i])
            delete iBLabelsN[i];
	
   delete [] iBLabelsN;
   }

   if(iBLabelsD)
   {
      for(tUint8 i=0;i<kLabelsCount;i++)
         if(iBLabelsD[i])
            delete iBLabelsD[i];

      delete [] iBLabelsD;
   }
}

Drawing

The method Render() is responsible for drawing the console on the widget. Because the widget is using an offscreen bitmap, we do not have to worry about redrawing any part of the widget that is damaged. This is handled by the cDrawView.

The methods StartDrawing() and EndDrawing() from the class cDrawView are required before and after every block that perform some drawing:

void cDConsoleView::Render(tUint8 aButton,tBool aBackground)
{
   StartDrawing();
	
   if(aButton < kButtonsCount)
   {

We will be using this method to render either the whole console or just a given button. When aButton equal kButtonsCount the whole console will be redrawn.

      // Render only one button
      DrawBitmap(iBStates[iButtons[aButton].iState],iButtons[aButton].iBFrame.GetLeftTop());
      if(iButtons[aButton].iState == kStateDimmed)
         DrawBitmap(iBLabelsD[iButtons[aButton].iLabel],iButtons[aButton].iLFrame.GetLeftTop());
      else
         DrawBitmap(iBLabelsN[iButtons[aButton].iLabel],iButtons[aButton].iLFrame.GetLeftTop());

The method cDrawView::DrawBitmap() draws a bitmap at a given location. We use it here to render the button using the bitmap associated with the button’s state, followed by the label of the button.

   }
   else 
   {
      // Render the background
      if(aBackground)
         DrawBitmap(iBackground);
	
      // Render all the button
      for(tUint8 i=0;i<kButtonsCount;i++)
      {
         DrawBitmap(iBStates[iButtons[i].iState],iButtons[i].iBFrame.GetLeftTop());
         if(iButtons[i].iState == kStateDimmed)
            DrawBitmap(iBLabelsD[iButtons[i].iLabel],iButtons[i].iLFrame.GetLeftTop());
         else
            DrawBitmap(iBLabelsN[iButtons[i].iLabel],iButtons[i].iLFrame.GetLeftTop());
      }

When rendering the whole console, we first draw the background bitmap if required, then the 4 buttons with their labels.

   }

   EndDrawing();	
}

In order to render the console for the first time, we implement the method OnScreen(). This method is executed when the widget is put onscreen, following the creation of the parent window:

void cDConsoleView::OnScreen()
{
   cDrawView::OnScreen();
   // Render the whole console (for the first time)
   Render(kButtonsCount,true);
}

User’s inputs

Although the user may think that our console contains 4 independent button widgets, it is in fact only one widget. Handling the user actions is going to require the need to know which one of the buttons was acted upon.

When given a position within the widget, the method FindButton() will give us the related button:

tUint8 cDConsoleView::FindButton(uPoint aPoint) const
{
   tUint8 lButton;

   for(lButton=0;lButton<kButtonsCount;lButton++)
   {
      if(iButtons[lButton].iBFrame.Contains(aPoint))
         break;
   }

   return lButton;
}

Its algorithm is simple. We loop over all the buttons and check if aPoint is contained in the button frame. If none of the buttons contain the position being tested, the method will return kButtonsCount. Although simple, there is one little issue … What if the buttons are circular?

Because we are using the rectangular frame of the button, if we were to tap in a corner of the frame, which is dearly outside of the button, it will still act as if we clicked in the center of the button. Can we live with it ? If our UI is to be used on a touch-screen in a moving vehicle, it wouldn’t matter much since the user’s input will not be too precise. However, if the input is triggered by a pen, then we should do something to improve it.

One solution, on top of what we’ve already done, is to use the button’s bitmap to find out if the position where the user has clicked, is transparent or not. If it is transparent, then we are outside of the button.

Here’s the improved method:

tUint8 cDConsoleView::FindButton(uPoint aPoint) const
{
   tUint8 lButton;

   for(lButton=0;lButton<kButtonsCount;lButton++)
   {
      if(iButtons[lButton].iBFrame.Contains(aPoint))
      {
         tUint8 lIndex;
         tUint8 lTrans;
         uPoint lPoint;

         // calculate position within the button frame
         lPoint.iX = aPoint.iX - iButtons[lButton].iBFrame.iLeft; 
         lPoint.iY = aPoint.iY - iButtons[lButton].iBFrame.iTop; 
         // get transparent color index
         iBStates[kStateNormal]->GetTransparent(lTrans);
         // get pixel color index
         iBStates[kStateNormal]->GetPixelColor(lPoint.iX,lPoint.iY,&lIndex);
         // if the color is transparent, then we are outside of the button
         if(lIndex != lTrans)
            break;
      }
   }

   return lButton;
}

Now, there are two main events that we are going to handle: the user presses down on a button, and the user releases the button. The first one will be handled in the method MouseDown(), the second in MouseUp():

void cDConsoleView::MouseDown(uPoint aPoint)
{
   Pressed(FindButton(aPoint));
}

void cDConsoleView::MouseUp(uPoint aPoint)
{
   if(FindButton(aPoint) == iPressedButton)
      Invoked();
   else
   {
      if(iButtons[iPressedButton].iState == kStatePressed)
      {
         // revert the state of the button
         iButtons[iPressedButton].iState = iPrevState;
         // redraw the button
         Render(iPressedButton);
      }

      iPressedButton = kButtonsCount;
   }
}

When the user clicks, taps or presses somewhere on the console, the method MouseDown() will be called. This method will execute Pressed() with the identifier of the button pressed. If a button was indeed pressed, we will mark the button as pressed. It is only when the user releases the button, that we will invoke it from MouseUp(). If the button is released outside of its boundary, we will not invoke it.

Here is the method Pressed():

void cDConsoleView::Pressed(tUint8 aButton)
{
   // if the mouse event was on a button
   if(aButton < kButtonsCount)
   {
      iPressedButton = aButton;
      // if the button is not dimmed
      if(iButtons[aButton].iState == kStateNormal || iButtons[aButton].iState >= kStateActive)
      {
         // set the button as pressed
         iPrevState = iButtons[aButton].iState;
         iButtons[aButton].iState = kStatePressed;
         // redraw the button
         Render(aButton);
      }
   }
}

Whether the button is dimmed or not, we mark it as pressed. However, we only really set it as pressed and redraw it, if it is not dimmed.

The purpose of the method Invoked() is to accept and acknowledge the user action on a given button. In order to improve the illusion of a physical button, we are going to redraw the button with a bit of a delay. That way it will look pressed a little bit longer. Here it goes:

void cDConsoleView::Invoked()
{
   tBool lGood = false;

   if(iPressedButton < kButtonsCount)
   {
      if(iButtons[iPressedButton].iState == kStatePressed)
      {
         // delay a bit to have a better effect
         sSnooze::SomeMS(kDelayPressed);

         // restore the previous state of the button
         iButtons[iPressedButton].iState = iPrevState;
         // redraw the button
         Render(iPressedButton);

         lGood = true;
      }
		
      // call the hook method linked to the pressed button
      switch(iPressedButton)
      {
         case kButtonPlay:
         {
            if(iButtons[kButtonPlay].iLabel == kLabelStop)
               StopPressed(lGood);
            else
               PlayPressed(lGood);
            break;
         }
         case kButtonPause:
         {
            PausePressed(lGood);
            break;
         }
         case kButtonBwd:
         {
            BwdPressed(lGood);
            break;
         }
         case kButtonFwd:
         {
            FwdPressed(lGood);
            break;
         }
      }
	
      iPressedButton = kButtonsCount;
   }
}

It is only if the button can be pressed, not dimmed, that we will redraw it and restore its state. The button callback will be called in both cases, however; lGood will be false when the button is dimmed.

The last part of handling the user input goes into specific case where the user clicks or taps down on a button, but never releases the mouse button or its finger (when there is no mouse or pen). How could that be? Imagine you are driving a four wheel suv on a bumpy road (…. okay, trail πŸ™‚ … Now, you reach for the console in order to stop the playback, but just as you touch the on-screen button, the car rocks and your finger moves off the button. The problem is that the console widget may register the pressed down action on the button, but because you released your finger outside of the widget, the pressed up event will not be processed by the widget.

A simple solution is to use the method OutBound() which is called every time the mouse or finger leaves the widget while still pressed down. When this occurs, we will revert its state and redraw it:

void cDConsoleView::OutBound(uPoint aPoint)
{
   if(iPressedButton < kButtonsCount)
   {
      if(iButtons[iPressedButton].iState == kStatePressed)
      {
	 // revert the state of the button
         iButtons[iPressedButton].iState = iPrevState;
	 // redraw the button
         Render(iPressedButton);
      }

      iPressedButton = kButtonsCount;
   }
}

Animation

When a button is active, we want to display a pulsating effect on the active button. To render this, we need to redraw the button at a given interval in time. We have used the flag kFlgBeatNeeded in the cDConsoleView base class initialization. Now let’s have a look at the implementation of the method Beat(), which will be called at the rate set by the parent window:

void cDConsoleView::Beat()
{
   // if there is an active button
   if(iActiveButton < kButtonsCount)
   {
      if(iButtons[iActiveButton].iState >= kStateActive)
      {

Because this method will still be called even when there is no button active, we will only draw the animation effect if there is an active button.

	 // set the state of the button
         iButtons[iActiveButton].iState += iActiveDir;

The pulsating effect is rendered by incrementing the state of the button from kStateActive to kStateActive2 and then reversing back to kStateActive.

         if(iButtons[iActiveButton].iState > kStateActive2)
         {
            iButtons[iActiveButton].iState = kStateActive2;
            iActiveDir = -1;
         }
         else
         if(iButtons[iActiveButton].iState < kStateActive)
         {
            iButtons[iActiveButton].iState = kStateActive;
            iActiveDir = 1;
         }

We use the data member iActiveDir to store the direction we are going. The only thing left to do, is to redraw the button. We simply call the method Render() which use the button state to render it:

	 // redraw the button
         Render(iActiveButton);
      }
   }
}

Later, we will see how to setup the window to beat.

Changing state

All that is left to be done, is SetState(). This method will be used from outside the class in order to change the state of the console. For example, when the user presses the Play button and playback successfully starts, the state should be changed to ePlaying.

Here’s the method implementation:

void cDConsoleView::SetState(tConsoleState aState)
{
   if(!iFault)
   {
      TheLooperMustBeLocked();

Since we do not know who will be calling this method and when, we need to insure that the looper (the window) is locked. The macro TheLooperMustBeLocked() will check this for us. If it is not locked, an exception will be raised and the application will likely be terminated. This helps us to easily detect situations where we are executing code that should only be executed when the window is locked. Don’t forget that the Zinzala SDK, is multithreaded and that only one thread should be changing the button’s state or widget internals at any given time.

      switch(aState)
      {
         case ePlaying:
         {
	    // enable the buttons (all of them)
            for(tUint8 i=0;i<kButtonsCount;i++)
               iButtons[i].iState = kStateNormal;
				
	    // change the label of the play button
            iButtons[kButtonPlay].iLabel = kLabelStop;
	    // and set it as active
            iButtons[kButtonPlay].iState = kStateActive;
            iActiveButton = kButtonPlay;
		
            break;
         }
         case eStopped:
         {
	    // disable the buttons (all of them)
            for(tUint8 i=0;i<kButtonsCount;i++)
               iButtons[i].iState = kStateDimmed;

	    // change the label of the play button
            iButtons[kButtonPlay].iLabel = kLabelPlay;
	    // and enable it
            iButtons[kButtonPlay].iState = kStateNormal;
	    // there is no active button
            iActiveButton = kButtonsCount;
	
            break;
         }
         case ePaused:
         {
	    // disable some of the buttons
            iButtons[kButtonBwd].iState = kStateDimmed;
            iButtons[kButtonFwd].iState = kStateDimmed;
	
	    // and set it as active
            iButtons[kButtonPause].iState = kStateActive;
            iActiveButton = kButtonPause;
		
            break;
         }
         case eForward:
         {
	    // disable some of the buttons
            iButtons[kButtonBwd].iState = kStateDimmed;
            iButtons[kButtonPause].iState = kStateDimmed;
				
	    // and set it as active
            iButtons[kButtonFwd].iState = kStateActive;
            iActiveButton = kButtonFwd;
		
            break;
         }
         case eBackward:
         {
	    // disable some of the buttons
            iButtons[kButtonFwd].iState = kStateDimmed;
            iButtons[kButtonPause].iState = kStateDimmed;
				
	    // and set it as active
            iButtons[kButtonBwd].iState = kStateActive;
            iActiveButton = kButtonBwd;

            break;
         }
      }

      iState = aState;

Because several button states may have changed, we simply redraw them all:

      // render all the buttons
      Render(kButtonsCount);
   }
}

As you have noticed, this method does not check the logic of state changes. The derived class or whoever else called this method, is responsible for this check.

If you would like to see the complete source code of the cDConsoleView class, here’s the header and the C++ file.

4. Using cMyConsoleView

In order to test the console class we just created, we are going to derive it and implement the button pressed callbacks. Then, we will assemble a simple test application.

Derivating

Here’s the declaration of the class cMyConsoleView:

class cMyConsoleView : public cDConsoleView
{
   public:
      static cMyConsoleView *NewL(cDSkin &aSkin);

      void Reset();

   protected:
      void PlayPressed(tBool aActive);
      void PausePressed(tBool aActive);
      void StopPressed(tBool aActive);
      void FwdPressed(tBool aActive);
      void BwdPressed(tBool aActive);

   protected:
      cMyConsoleView();
};

No surprise here. The class will implement the callback for each button of the console.

The method NewL() needs to be re-implemented, even though it is already in the base class:

cMyConsoleView::cMyConsoleView()
	: cDConsoleView()
{
}

cMyConsoleView *cMyConsoleView::NewL(cDSkin &aSkin)
{
   cMyConsoleView *lView = new cMyConsoleView();
   cMyConsoleView::VerifyLC(lView);
   lView->ConstructL(aSkin);
   sCleanupStack::PopL(lView);
   return lView;
}

The implementation of NewL() is very close to the one we have in the base class. In fact, such methods are always very similar, since their purpose is to create a new object and construct it. The method NewL() from the base class is not really necessary because it is very unlikely that someone will want to use that class without derivating it, but you never know … πŸ™‚


I have added the method Reset() to the class. Its purpose is to reset the console to its stopped state:

void cMyConsoleView::Reset()
{
   SetState(eStopped);
}

Our implementation of the button callbacks that will be presented, is as simple as it can get. Real world implementations will likely be a bit more complex, although they will perform the same basic actions as here, such as changing the console state:

void cMyConsoleView::PlayPressed(tBool aActive)
{
   if(aActive)
      SetState(ePlaying);	
}

void cMyConsoleView::PausePressed(tBool aActive)
{
   if(aActive)
   {
      if(GetState() != ePaused)
         SetState(ePaused);	
      else
         SetState(ePlaying);	
   }
}

void cMyConsoleView::StopPressed(tBool aActive)
{
   if(aActive)
      SetState(eStopped);	
}

void cMyConsoleView::FwdPressed(tBool aActive)
{
   if(aActive)
   {
      if(GetState() != eForward)
         SetState(eForward);	
      else
         SetState(ePlaying);
   }
}

void cMyConsoleView::BwdPressed(tBool aActive)
{
   if(aActive)
   {
      if(GetState() != eBackward)
         SetState(eBackward);	
      else
         SetState(ePlaying);
   }
}

If you would like to see the complete source code of the cMyConsoleView class, here’s the header and the C++ file.

Window and Application

Now that we have implemented the console class and specialized it, we can move on to the window and application. We will also have a look at the main of the test program.

cDWindow

Here’s the window declaration from DWindow.h:

class cDWindow : public cWindow
{
   public:
      static cDWindow *NewL(cSettings *aSettings,cDSkin &aSkin);
      virtual ~cDWindow();
		
   protected:
      void Ready();
      bool CloseRequested();
		
   protected:
      cDWindow(cSettings *aSettings);
      void ConstructL(cDSkin &aSkin);

   private:
      cSettings*        iSettings;
      cMyConsoleView*   iConsole;
};

The cSettings class provides access to the application specific settings, which will be saved and restored each time the application runs. We will use it to save the position of the window on screen. This is not a requirement for any window or application with the Zinzala SDK, but rather a personal preference. I prefer that my window always appears at the last place I left it.

As you might guess by now, our window class will use the two-phase construction idiom. Note, although the SDK provides everything that is needed to use this idiom, it does not force the developer to use it, nor does it enforce the use of exceptions as the means of error handling.

Here’s the C++ constructor of the class:

cDWindow::cDWindow(cSettings *aSettings)
   : cWindow(uRect(0,0),"window",eWinNormal,kFlgResizeToFit | kFlgNotResizable,
             kCurrentWorkspace)
{
   iSettings = aSettings;
   iConsole	 = NULL;
	
   if(!iFault)
   {
      uPoint *lPos;

      SetTitle("Zinzala : Custom Widget Demo ...");
      SetPadding(5,5);
      SetBGColor(kColorWhite);

If the base class construction turns out to be a success, we set the title of the window, then specify a 5×5 padding for the widget area. This padding doesn’t mean that each widget placed on the window will be surrounded by 5 pixels of empty space. It simply means that there will be an inset of 5 pixels to the window bounds. We also set the background color to white.

      // Get position of the window is possible
      if(iSettings->HasItem("pos"))
      {
         iSettings->GetData("pos",eDataBuffer,(char **)&lPos,sizeof(uPoint));
         MoveTo(*lPos);
      }

If the demo application was already used once, the settings should contain the position of the window on screen. If this is the case, we read that position from the settings and move the window accordingly.

   }
}

The instantiation of our console will be placed in the ConstructL() method of the window, as it can throw an exception:

void cDWindow::ConstructL(cDSkin &aSkin)
{
   sEnv::LeaveIfError(iFault);

   iConsole = cMyConsoleView::NewL(aSkin);
   AddChild(iConsole);
}

If the cWindow class does not delete all its children views when it is destroyed, we would have had to add the destruction of iConsole in it. However since it does, the class destructor is empty.

Here is the method NewL() of the window class:

cDWindow *cDWindow::NewL(cSettings *aSettings,cDSkin &aSkin)
{
   cDWindow *lWin = new cDWindow(aSettings);
   cDWindow::VerifyLC(lWin);
   lWin->ConstructL(aSkin);
   sCleanupStack::PopL(lWin);
   return lWin;
}

The method Ready() and CloseRequested() are implemented in our class and defined in cWindow. The first one is
called after the window is displayed on screen. We will be using it to set the beat rate of the window, which our console widget needs for the active button animation:

void cDWindow::Ready()
{
   SetBeatRate(kBeatRate);
   cWindow::Ready();
}

The constant kBeatRate is defined as const tUint32 kBeatRate = 150 and is expressed in milliseconds.

When the user asks to close the window, the method CloseRequested() will be executed. If it returns true, the window will be closed, else it will remain open. This is the perfect time for us to save the current position of the window on screen, for future usage:

tBool cDWindow::CloseRequested()
{
   // Store current position of the window in the settings
   uPoint pos = GetPosition();
   iSettings->SetData("pos",eDataBuffer,(char *)&pos,sizeof(uPoint));
   return true;
}

Here’s the header and the C++ file of cDWindow.

cDApplication

We are now going to derive the application class to create and display the window. We will also be retreiving the application settings. Here’s the application declaration from DApplication.h:

class cDApplication : public cApplication
{
   public:
      cDApplication();
      ~cDApplication();

   protected:	
      void Ready();

   private:
      cDWindow*   iWindow;
      cSettings*  iSettings;
};

The method Ready() is called once the application starts to run. We will use this method to construct and display the window. First, let’s see the class constructor:

cDApplication::cDApplication()
   : cApplication("hexaZen/Custom",0,0)
{
   iWindow = NULL;

   if(!iFault)
   {
      // Create the object to read/write application settings
      iSettings = new cSettings("Custom");

      if(!iSettings)
         iFault = kErrOutOfMemory;
      else
         iFault = iSettings->GetFault();
   }
   else
      iSettings = NULL;
}

The first argument of the base class constructor is the name of the application. This name will be used to identify the application if we wanted to send some scripting messages to it. We will come back to that later. The class cSettings from which we instantiate iSettings provides access to the application settings. If something is wrong with it, or if the object cannot be created, the data member iFault of the application will be set to something other than kErrNone.

If you recall the destructor of cDWindow, you may remember that we didn’t delete the console widget in it, since the window took care of that. The application class, does not take care of deleting the window for us, so we will need to delete iWindow in it:

cDApplication::~cDApplication()
{
   if(iSettings)
   {
      // save settings
      iSettings->Save();
      delete iSettings;
   }

   // delete the window
   if(iWindow)
      delete iWindow;
}

Calling the method Save() on the cSettings object will save the last position of the window in the application settings file. Storing data in it, like we did in the method CloseRequested(), does not automatically save the changes to a file.

Before we look into the method Ready(), which is quiet interesting, we first need to talk a bit about how the skin shared object will be loaded and then passed down to the window. When the application is launched, the skin to be used will be passed in the command line argument. We will be using the switch -s for that. The static class sArgs provided by the SDK allows for an easy parsing of the command line. It will be used in the main of the application. Since it is static, we can use it in the Ready() method to get the path of the skin.

Now, we can start looking at Ready():

void cDApplication::Ready()
{
   // load settings
   if(iSettings)
      iSettings->Load();

Although we have had iSettings since the constructor, we have not loaded the settings file until now. If it is the very first time the application is started, the settings file will not exist. The method Load() will fail, but it doesn’t really matter since we will create the file when we save the settings in the application destructor. Now,
we are going to use the method sArgs::Presents() to test if the switch was present on the command line:

   if(sArgs::Presents("s"))
   {
      // load the skin
      cDSkin lSkin(sArgs::Value("s"));

Using the class cDSkin presented earlier, we have loaded the shared object containing the skin from the path name retrieved from the value associated with the switch -s. However, it is only after testing the validity of lSkin, will we know if the skin object was loaded correctly:

      if(lSkin.IsValid())
      {

If the skin was loaded properly, we can go ahead and create the window. Since the window creation can throw an exception, we will be putting it in a trycatch block:

	 // create & show window
         try
         {
            sCleanupStack::WindL();

Calling the method WindL() is mandatory in the beginning of the block. Its purpose is to prepare the cleanup stack. Note that this method can leave if something is not right. However, since we are already in the trycatch block, it’s okay. The creation of the window is no surprise; we use the method NewL(). If there is no exception, the window is ready to go and we simply show it on screen:

            iWindow = cDWindow::NewL(iSettings,lSkin);
            iWindow->Show();

The last statement inside the try block, must be a call to the method UnWind(). This will un-prepare the cleanup stack and all the objects still present on the stack will be destroyed:

            sCleanupStack::UnWind();
         } catch (uException &lException) {

If an exception is raised in the try block, we will be displaying the exception information and request the application to terminate:

            sEnv::PrintToStream("The exception "%s" was raised in %s\n",
                                sEnv::ErrorToString(lException.iWhy),lException.iWhere);
            Quit();

Like in the try block, we must end the catch block by a call to UnWind(). This will get all the objects on the cleanup stack to be destroyed, which is afterall the whole point of the idiom:

            sCleanupStack::UnWind();
         }
      }
      else
      {
         sEnv::PrintToStream("Invalid skin\n");
         Quit();
      }
   }
   else	
   {
      sEnv::PrintToStream("No skin given\n");
      Quit();
   }

Last but not least, we will call the base class Ready() method as required by the framework:

   cApplication::Ready();
}

Here’s the header and the C++ file of cDApplication.

Main

Finally, we are going to look at the contents of the main. We need to parse the command line arguments, then create and start the application:

int main(tInt32 aArgc,const tChar **aArgv)
{
   tErr           lErr;
   cDApplication  lApp;

   if(!(lErr = lApp.GetFault()))
   {
      // if there is args on the command line,
      // extract them and give them to the app
      if(aArgc>1)
      {
         cCan lCan(0);
	
         if(!cCan::Verify(&lCan))
         {
            sArgs::SetCan(&lCan);	
            sArgs::Extract(aArgc,aArgv);

	    // Start the app
            if(!(lErr = lApp.Run()))
	       // and wait for it to end
               lApp.WaitEnd();
            else
               sEnv::PrintToStream("The application failed to run : %s\n",
                                   sEnv::ErrorToString(lErr));
         }
         else
            sEnv::PrintToStream("The application failed to run : %s\n",
                                sEnv::ErrorToString(kErrOutOfMemory));
      }
      else
         sEnv::PrintToStream("No skin specified.\n");
   }
   else
      sEnv::PrintToStream("The application construction failed : %s\n",
                          sEnv::ErrorToString(lErr));
}

Once the application object is created, we parse the command line arguments then ask the application to run. Once this is done, we just wait for it to end using the WaitEnd() method. The class sArgs requires a cCan object in order to parse the command line. We create one on the stack.

Throughout this article, we have been using the static class sEnv a couple of times. This is an handy static class which provides access to various useful methods, such as PrintToStream(), LeaveIfError() or ErrorToString(). The latter method transforms an error code into a human readable string.

Here’s the C++ file for the main.

Now that our test application is complete, we can try it out with an alternate skin:

#> ./custom -s ./Skins/Orange/Orange.so

Here’s an animated GIF assembled from screenshots:



5. Adding scriptability

A key feature of the Zinzala SDK is the ease with which we can add support for application scripting. We will now see how we can use this feature to add support for an on the fly skin change. This will be done while the application is running.

For testing, we will be using the tool yo which is part of the Zinzala library distribution. However, any application can send scripting messages to another application. For example, in the case of the Carrozza layer we discussed earlier, if requested a configuration panel could be sending a message to all the UI applications running in order to change the overall look of the UI. Because this will be done on the fly, there will be no interruption of services. For example, music or movie playback will remain active.

Without any modification to the application, we can already send some messages to it. For example:

#> yo hexaZen/Custom ping
got answer in 0.000000 ms
cCan::0x8047850 items=0
What    = 'zAPo'
Context = 0x0
#> yo hexaZen/Custom info
got answer in 0.999928 ms
cCan::0x8047850 items=6
What    = 'zARy'
Context = 0x0
Item 'zSDK' have 1 value(s)
        0       string  (4)     '0.8'
Item 'Username' have 1 value(s)
        0       string  (4)     'jlv'
Item 'MaxInstances' have 1 value(s)
        0       tUint8  (1)     125
Item 'Scripting' have 1 value(s)
        0       tUint8  (1)     0
Item 'MaxMSGLength' have 1 value(s)
        0       tUint16 (2)     100
Item 'GUIEngine' have 1 value(s)
        0       string  (7)     'Photon'
#> yo hexaZen/Custom list
window

However, if we ask the application to use a different skin, it will obviously not work:

#> yo hexaZen/Custom tell window do reskin Orange.so
Not allowed

There are 2 raisons for this. By default, scripting is not enabled, and the keyword reskin is unknown to the cWindow class. This can be easily changed. Let’s have a look at what needs to be done in order to support the change of a skin on the fly.

cDApplication

Before adding support in the window class, we need to allow scripting at the application level. This is done by modifying the method Ready() as follow:

void cDApplication::Ready()
{
   // Allow scripting
   AllowScripting(eLocal);
   // Increase the buffer size for messaging
   SetBufferLength(250);

   ...
   ...
   ...
}

Because the path to the skin to be loaded is going to be included in the scripting message, we need to set the size of the receiving buffer to 250 bytes. The default size is 100 bytes. If we didn’t do that, the path could have been truncated. 250 bytes should be enough for most cases. However, a better solution would have been to use the constant PATH_MAX defined in limits.h, and then add more bytes to it for the overhead generated by the transmission.

cDWindow

Next, we are going to add handling for the reskin command. The window should handle this scripting message, then ask each widget to use the new skin. This is what we are going to do.

First, we modify the definition of the cDWindow class:

class cDWindow : public cWindow
{
   public:
      static cDWindow *NewL(cSettings *aSettings,cDSkin &aSkin);
      virtual ~cDWindow();
		
   protected:
      void Ready();
      bool CloseRequested();
      void ScriptingReceived(cMessage *aMessage,cMessage *aReply);
		
   protected:
      cDWindow(cSettings *aSettings);
      void ConstructL(cDSkin &aSkin);

   private:
      void ReSkinL(const tChar *aSkin);

   private:
      cSettings*        iSettings;
      cMyConsoleView*   iConsole;
};

We have added the methods ScriptingReceived() and ReSkinL(). The first method is inherited from the framework and will be called when the window receives a scripting message. We will be using the second method to perform the change of skin.

Just like in the application class, we need to allow scripting for the window. We modify the method Ready() as follows:

void cDWindow::Ready()
{
   AllowScripting(eLocal);
   SetBeatRate(kBeatRate);
   cWindow::Ready();
}

In ScriptingReceived(), we need to detect if the scripting command is do reskin. If it is not, we can pass the scripting message to the base class method and let it deal with it. Here’s how the method looks like:

void cDWindow::ScriptingReceived(cMessage *aMessage,cMessage *aReply)
{	
   tUint32 lCmd;

   aReply->SetBool(kScriptItemDone,true);

   if(!aMessage->GetUint32(kScriptItemCommand,&lCmd))
   {

The string do is converted by the tool yo into a tUint32 command, which we retrieve in the scripting message by using the label kScriptItemCommand. We will expect any other application sending scripting messages to send them in the format outlined in the protocol defined in the framework. If we can’t find the command ID, the reply message will indicate that the script message was malformed.

      switch(lCmd)
      {
         case kScriptCmdDo:
         {
            const tChar *lFunction;

Now that we know that this scripting message is asking the window to do something, we will retrieve the first argument which is the label of the action that must be performed:

            if(!aMessage->GetString(kScriptItemArgs,&lFunction,0))
            {
               if(!strcmp(lFunction,"reskin"))
               {
                  const tChar *lSkin;

If the message asks the window to change the skin it is using, we will try to retrieve the next argument, which should be the path to the new skin:

                  if(!aMessage->GetString(kScriptItemArgs,&lSkin,1))
                  {
                     tErr lErr;

                     TRAP(lErr,ReSkinL(lSkin));

Because the method can leave (ea. if the skin cannot be loaded), we use the macro TRAP() to catch any possible exceptions. The variable lErr will be different from kErrNone if we catch an exception. In that case, in the reply message we will indicates that we failed to perform the command:

                     if(lErr)
                     {
                        aReply->SetBool(kScriptItemDone,false);
                        aReply->SetUint8(kScriptItemReason,kScriptErrFailed);
                     }
                  }
                  else
                  {
                     aReply->SetBool(kScriptItemDone,false);
                     aReply->SetUint8(kScriptItemReason,kScriptErrMissingArg);
                  }
               }
               else
               {
                  aReply->SetBool(kScriptItemDone,false);
                  aReply->SetUint8(kScriptItemReason,kScriptErrIncorrect);
               }
            }
            else
            {
               aReply->SetBool(kScriptItemDone,false);
               aReply->SetUint8(kScriptItemReason,kScriptErrMalformed);
            }
				
            break;
         }

For any other scripting command, we will pass the message to the base class method for handling:

         default:
            cWindow::ScriptingReceived(aMessage,aReply);	
      }
   }
   else
   {
      aReply->SetBool(kScriptItemDone,false);
      aReply->SetUint8(kScriptItemReason,kScriptErrMalformed);
   }
}

The application sending the scripting message expects to receive notice of whether or not it was successful. And if not, what the reason was for the failure. As we have seen in the above code, the reply’s item kScriptItemDone will have true or false for its value. It will indicate if the command was performed or not. If an error occurs, the reply message will also contain the item kScriptItemReason which will contain an error code.

As we are going to see, the method ReSkinL() is far from complicate:

void cDWindow::ReSkinL(const tChar *aSkin)
{
   cDSkin lSkin(aSkin);
							
   sEnv::LeaveIfError(lSkin.GetFault());

   iConsole->ReSkinL(lSkin);
}

It first creates a cDSkin object from the path of the skin we would like to use. If the object is invalid, we will leave right away. If valid, we will be calling the method ReskinL() for all the widgets of the window that needs their skin to change. For this test application,we only have the console widget to update.

cDConsoleView

In the method ReSkinL() of our console class, we perform something similar to what we have done in ConstructL(). We are going to instantiate all the bitmaps we need from the skin, then replace the old ones by the new ones. However, because the method can leave anytime, we only need to replace the old bitmaps once all the new ones have been created. This implies that we must use the cleanup stack to insure that all the bitmaps loaded before will be destroyed if we leave when we are instantiating the last bitmap of the skin. It also requires us to keep the newly created bitmap in some temporary array until they replace the old ones.

Here’s the method implementation:

void cDConsoleView::ReSkinL(cDSkin &aSkin)
{
   cBitmap *lBackground;
   cBitmap *lBStates[kStatesCount];
   cBitmap *lBLabelsN[kLabelsCount];
   cBitmap *lBLabelsD[kLabelsCount];

   // Leave if the widget is faulty
   sEnv::LeaveIfError(iFault);

   // Instantiate the background bitmap
   lBackground             = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResBackground),false);

   // Instantiate the button bitmaps
   lBStates[kStateNormal]  = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResNormal));
   lBStates[kStateDimmed]  = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResDimmed));
   lBStates[kStatePressed] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResPressed));
   lBStates[kStateActive]  = lBStates[kStateNormal]; // use same bitmap as the normal state
   lBStates[kStateActive1] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResAnimate1));
   lBStates[kStateActive2] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResAnimate2));
   lBStates[kStateActive3] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResAnimate3));
   // Instantiate the label bitmaps
   lBLabelsN[kLabelBwd]    = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResBackwardN));
   lBLabelsN[kLabelFwd]    = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResForwardN));
   lBLabelsN[kLabelStop]   = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResStopN));
   lBLabelsN[kLabelPlay]   = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResPlayN));
   lBLabelsN[kLabelPause]  = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResPauseN));
   lBLabelsD[kLabelBwd]    = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResBackwardD));
   lBLabelsD[kLabelFwd]    = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResForwardD));
   lBLabelsD[kLabelStop]   = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResStopD));
   lBLabelsD[kLabelPlay]   = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResPlayD));
   lBLabelsD[kLabelPause]  = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResPauseD));

   // delete all the bitmap we currently use
   // and assign the newly loaded bitmaps
   delete iBackground;
   iBackground = lBackground;

   for(tUint8 i=0;i<kStatesCount;i++)
   {
      if(iBStates[i] && i!=kStateActive) // Active and Normal states share the same bitmap
         delete iBStates[i];
      iBStates[i] = lBStates[i];
   }

   for(tUint8 i=0;i<kLabelsCount;i++)
      if(iBLabelsN[i])
      {
         delete iBLabelsN[i];
         iBLabelsN[i] = lBLabelsN[i];
      }

   for(tUint8 i=0;i<kLabelsCount;i++)
      if(iBLabelsD[i])
      {
         delete iBLabelsD[i];
         iBLabelsD[i] = lBLabelsD[i];
      }
	
   // remove the bitmaps from the cleanupstack
   sCleanupStack::PopL(17,lBackground);

   Render(kButtonsCount,true);
}

Instead of using the method CreateBitmapFromResourceL that we implemented earlier, we are using one that keeps the created bitmap on the cleanup stack even if there is no exception. Once we have replaced all the old bitmaps by the new ones, at the end of ReSkinL(), we pop all the 17 objects we have placed on the cleanup stack. The method PopL() verifies that the last object to remove is the background bitmap. If this is not the case, something is wrongx and the method will throw an exception. If everything goes according to plan, we can then redraw the whole console.

The method CreateBitmapFromResourceLC isn’t much different from the one we did earlier. It simply pushes the bitmap on the cleanup stack:

cBitmap *CreateBitmapFromResourceLC(const tBitmapResource &aRes,tBool aTransparent = true)
{
   tErr     lErr;
   cBitmap *lBitmap = NULL;

   lBitmap  = new cBitmap(aRes.iWidth,aRes.iHeight,eSpaceColor8Bit,true,aRes.iColors);
   lErr     = cBitmap::VerifyD(lBitmap);

   if(!lErr)
   {	
      sCleanupStack::PushL(lBitmap);
      lBitmap->SetPalette(aRes.iPalette);
      lBitmap->SetBits(aRes.iBits,aRes.iLength,0);
      if(aTransparent)
         lBitmap->MakeTransparent(aRes.iIndex);
      return lBitmap;
   }
   else
      throw uException(lErr,"CreateBitmapFromResourceLC()");
}

Now, let’s add some extra scripting abilities to the console widget itself by supporting invocations of any of the console’s buttons. By doing so, we allow remote control and automated testing capabilities, since the console can be invoked from anywhere:

#> yo hexaZen/Custom tell window.console invoke play

Because by default the widgets do not allow scripting, we are going to allow it in the window ConstructL() method:

void cDWindow::ConstructL(cDSkin &aSkin)
{
   sEnv::LeaveIfError(iFault);

   iConsole = cMyConsoleView::NewL(aSkin);
   iConsole->AllowScripting(eLocal);
   AddChild(iConsole);
}

Then, we will be declaring the method ScriptingMessageReceived() in the console class and implementing it. Here it is:

void cDConsoleView::ScriptingReceived(cMessage *aMessage,cMessage *aReply)
{	
   tUint32 lCmd;

   aReply->SetBool(kScriptItemDone,true);

   if(!aMessage->GetUint32(kScriptItemCommand,&lCmd))
   {
      switch(lCmd)
      {
         case kScriptCmdInvoke:
         {
            const tChar *lArg;

	    // if a button is already pressed, we will ignore this command
            if(iPressedButton == kButtonsCount)
            {
               if(!aMessage->GetString(kScriptItemArgs,&lArg))
               {
                  if(!strcmp(lArg,kScrButtonPlay) || !strcmp(lArg,kScrButtonStop))
                  {
                     Pressed(kButtonPlay);
                     Invoked();
                  }
                  else
                  if(!strcmp(lArg,kScrButtonPause))
                  {
                     Pressed(kButtonPause);
                     Invoked();
                  }
                  else
                  if(!strcmp(lArg,kScrButtonBwd))
                  {
                     Pressed(kButtonBwd);
                     Invoked();
                  }
                  else
                  if(!strcmp(lArg,kScrButtonFwd))
                  {
                     Pressed(kButtonFwd);
                     Invoked();
                  }
                  else
                  {
                     aReply->SetBool(kScriptItemDone,false);
                     aReply->SetUint8(kScriptItemReason,kScriptErrIncorrect);
                  }
               }
               else
               {
                  aReply->SetBool(kScriptItemDone,false);
                  aReply->SetUint8(kScriptItemReason,kScriptErrMissingArg);
               }
            }
			
            break;
         }
         default:
            cView::ScriptingReceived(aMessage,aReply);	
      }
   }
   else
   {
      aReply->SetBool(kScriptItemDone,false);
      aReply->SetUint8(kScriptItemReason,kScriptErrMalformed);
   }
}

If the command in the scripting message is kScriptCmdInvoke, we retrieve its argument and compare it to the label of the console’s buttons. Here’s the declaration of the constants we are using to distinguish the different buttons invoked:

const tChar *kScrButtonPlay   = "play";
const tChar *kScrButtonStop   = "stop";
const tChar *kScrButtonBwd    = "bwd";
const tChar *kScrButtonFwd    = "fwd";
const tChar *kScrButtonPause  = "pause";

If we were to add scriptability to all the widgets of the UI, we could then support automated testing of the applications. This will allow for a more consistent approach to testing since it is fully documented, repeatable and very flexible. Saving developers from painful manual testing also helps to keep their focus on more rewarding activities. Because it is repeatable, regression testing can be performed on a regular basis. In short, scriptability can help your product achieve a better quality in a relatively painless way. It can also ease the integration of an application in a network of collaborative applications.


6. Conclusion

By using object-oriented programming, we were able to quickly build a custom widget. Inheritance permits the use of the widget in many applications, saving time and effort. The Zinzala SDK can leverage the quality of your embedded systems, while making developers lives easier. If you are developing set-top boxes, using object-oriented techniques will help you lower the time to market of your product. A goal worth the cost of the little overhead that C++ will bring.


7. Acknowledgment


The author would like to thank Chris McKillop and Eugenia Loli-Queru for reviewing this article and giving valuable feedback. Thanks also goes out to Susan for providing the various graphics and for proofreading this article.


If you would like to see your thoughts or experiences with technology published, please consider writing an article for OSNews.

12 Comments

  1. 2004-12-08 10:14 pm
  2. 2004-12-08 11:47 pm
  3. 2004-12-09 12:22 pm
  4. 2004-12-09 5:11 pm
  5. 2004-12-09 7:58 pm
  6. 2004-12-09 8:02 pm
  7. 2004-12-09 8:04 pm
  8. 2004-12-09 10:30 pm
  9. 2004-12-09 10:55 pm
  10. 2004-12-09 11:34 pm
  11. 2004-12-10 12:05 am
  12. 2004-12-10 1:37 am