2009-12-04

Separating UI and Data Management - Lister Improvements

In my last post I discussed the possibility of ripping out the data management from the CycleButton and ContextMenu classes and replacing it with the ListData class. I was slightly dubious about this because of the potential extra overhead involved, but then realised that the current system did not offer any way to remove items from the CycleButton. In fact, the only possible operation that could be performed on CycleButton data was adding to it.

Worse, the ContextMenu duplicated the functionality of the ListBox but uses a gadget for each item in its list. This isn’t the fastest way of maintaining a list of options (I threw that model out in the early stages of ListBox development), and the option gadgets were being passed around as the arguments to ContextMenu events. Why pass around the entire gadget when only the data it contains is in any way worthwhile?

Based on these observations I’ve made a number of changes. The CycleButton now includes a ListData object to manage its options and exposes all relevant methods via a set of facade functions. The options can be sorted, added to, removed from, and so on.

The ContextMenu includes a ListBox gadget that does practically everything the ContextMenu tried to do; the ContextMenu is now little more than a wrapper that can raise relevant events and resize/reposition itself as necessary. It doesn’t expose any more methods, but it is more efficient now that each option isn’t an entire gadget. It also includes a getPreferredDimension() method that produces the correct values, and the resizing routine uses that instead of duplicating the functionality.

The ListData class has had several improvements and bugfixes. Its swapItems() method no longer raises a data changed event, as it is only called by one function - swap(). That method raises the event instead.

I’ve removed the ListDataItem struct from the listdata.h file and turned it into a separate class in its own file. That allows me to subclass it where necessary to change its behavioury. ListData’s sort() method uses a Java-esque “compareTo()” method in the ListDataIten class in order to sort its data, meaning subclasses of ListDataItem can override the default sort behaviour. This has proven useful in the FileRequester, which is now several times faster. The new sorting system means it doesn’t need to create two intermediate sorted arrays of files and directories in order to keep them separately ordered when displaying them.

Whilst on the subject of the FileRequester, I’ve split that into two classes. The FileRequester is still there, but most of the functionality has been moved out of it and into a new FileListBox class. This works just like the ListBox, but it automatically populates itself with a list of files when pointed at a directory. The FileRequester includes an instance of this new class in order to display its list of files.

Working with the ListBox so much made several bugs obvious. It now draws correctly - previously, a single rogue pixel was visible below the options in the list. It raises a double-click event when double-clicked, and ignores double-clicks if they occur on separate items within the list. It also redraws every time data in the list changes, does not overwrite items at the top of the list with those that have wrapped-around from the bottom (oops, clipping bug introduced in the last release), and adjusts its scroll position appropriately when items are removed.

Since the ListBox inherits from the ScrollingPanel and ScrollableBase classes, I inevitably found improvements to make there too. ScrollableBase and ScrollingPanel include functionality to disable scrolling either horizontally, vertically, or both. The ListBox uses this to prevent horizontal scrolling.

Aside from this, I’ve made some general improvements. Gadgets no longer respond to double-clicks if the first click actually falls on a different gadget. A new Gadget::isDoubleClick() method helps with this, and I’ve removed the unused Gadget::_doubleClickTime member. The Woopsi class does not attempt to retrieve a pointer to the system font before the font has been initialised. The WoopsiString class includes a new copyToCharArray() method for getting a copy of its internal string data. The TextWriter’s methods have been moved into the Graphics set of classes, making the class redundant so I have deleted it. Lastly, I’ve removed all of the individual colour variables from the Gadget class (_back, _highlight, _shine, etc) and merged them into a new GadgetColours struct.

I hope to get a new release out in the next few days.

2009-11-26

Multiple Inheritance and the Dreaded Diamond

Woopsi refactoring continues apace. Today’s latest changes were an attempt to consolidate the ListBox, ContextMenu and CycleButton gadgets.

Each of these gadgets is a different kind of view onto a list of items. The ContextMenu is the most primitive - it shows all of the items in its list as a vertical list. The ListBox is basically the same thing, except it introduces scrolling into the mix. The CycleButton shows just one option at a time; the other options can be paged through by clicking the button.

At present, the ListBox has the most advanced data mechanism. It leaves all of the data functionality to the ListData class, which wraps around a WoopsiArray (ie. vector) and provides such handy functions as sorting and item selection/deselection (enforcing single-only or multiple selection rules). It raises events to indicate changes to the data or selections within the data.

Meanwhile, the CycleButton and ContextMenu classes have very primitive data manipulation functionality. They both include an instance of the WoopsiArray and work with it directly. Wouldn’t it be better if they could benefit from the wealth of functionality in the ListData class?

Since the ContextMenu and ListBox work in very similar ways - the ContextMenu is basically a customised ListBox without scrolling - it makes sense to create a base class providing the most basic list view functionality that they can both inherit from. I split up the ListBox into “BasicListBox” (no scrolling) and “ListBox” (inherits from BasicListBox and ScrollingPanel, which adds the scrolling functionality). Here the problems began.

The BasicListBox inherits from the fundamental Gadget class, from which all Woopsi UI components inherit. The ScrollingPanel also inherits from the Gadget class. When the ListBox attempts to inherit from them both, it ends up with two copies of the Gadget class floating around in its inheritance hierarchy. Any attempts at working with the features of the Gadget class are now ambiguous and the compiler throws errors all over the place. This is called a “diamond” inheritance pattern:

           Gadget
       /             \
      /               \
     /                 \
BasicListBox    ScrollingPanel
     \                 /
      \               /
       \            /
           ListBox

The C++ FAQ Lite has a whole page about the diamond inheritance problem. The solution is to declare the inheritance from Gadget as virtual, which eliminates the ambiguities by eliminating the second copy of Gadget. However, attempting to do this has horrible problems in the Woopsi codebase. C-style casting cannot be used (uh oh) and the class at the top of the diamond (in this case, Gadget) should not include data members that need to be initialised (it can, but it shouldn’t).

The upshot of all this is that it is impossible to create a gadget that inherits from two other gadgets in Woopsi. If you think about it, this does make sense. It doesn’t help my goal of rationalising the list display gadgets, though. I could include an instance of the ScrollingPanel inside the ListBox to achieve the same effect, but that would be less efficient than the current solution. I could alternatively make the ListBox the basic list and have the ContextMenu inherit from that, and just not use the scrolling feature (though it would be handy if there are too many items to fit on-screen at once), but again that’s less efficient than the current solution.

I kept some of the changes I made before I reverted everything else back. ListData::swapItems() no longer raises a data changed event, as the only place it is called is in the sort() method. That method now raises the event instead. The ListDataItem struct has been replaced with a ListDataItem class, and it includes a C#/Java-style compareTo() method. Subclassing the ListDataItem and overriding this method allows the ListData class’ sort() method to sort the list differently. The ListBox’s scrolling canvas now has the correct height and the extra 1px at the bottom of every ListBox has now gone.

Finally, there are a couple of fixes in other places. The Woopsi class’ constructor no longer tries to retrieve the system font before it has been initialised, and the ContextMenu does not cast away the const-ness of the ContextMenuItem when raising a selection event to its listeners.

2009-06-13

Cursors, Text Boxes and Other Changes

Some minor Woopsi news. The ListBox removes itself from its ListData object’s list of event handlers when it gets deleted. This is slightly redundant at the moment, as the ListData object is internal within the ListBox and will therefore be deleted once its owner is deleted. However, it’s possible that I’ll allow the ListData class to be defined outside of the ListBox eventually, so this change does make sense in that scenario.

The DimmedScreen is now an “official” gadget. Since I’ve moved all of the dimming code into the GraphicsPort, the DimmedScreen only contains about a dozen lines of code, but creating the dimmed effect requires some knowledge of exactly how Woopsi’s drawing/erasing systems work. Due to this (plus the fact that it’s a neat trick, and isn’t hacky any more) it made sense to promote the DimmedScreen from bonus gadget to official gadget.

Lastly, I’m starting to finish off the cursor support in the text boxes. When the TextBox or MultiLineTextBox gadgets have focus, pressing left or right on the d-pad causes the cursor to move. There are a couple more changes to be made to the MultiLineTextBox:

  • Pressing up or down on the d-pad needs to move the cursor up or down a row;
  • Tapping on the text box with the stylus needs to cause the cursor to jump to the stylus location.

Both of these are rendered slightly more complex due to the MultiLineTextBox’s support for different alignment styles.

I’ve also got a few alignment bugs to fix in the MultiLineTextBox, too. Once these are done, I’m not sure there’s anything left to do. I’ve got all of the gadgets created that I wanted to, the event system is tidier and working (note to self - do I need to refactor the radio button group’s event system?), and I can’t think of any other outstanding bugs. I should probably have made a to-do list before I got distracted by my exam revision (provisional average grade is just over 80%, which should put me well into the “distinction” category - hurrah!).

The next steps will be documenting and testing Woopsi, as well as writing plenty of example programs.