Subclassing List Class

From MorphOS Library

Grzegorz Kraszewski

List class is one of the most complex MUI classes. Its purpose is to display data as a list or table. Objects of List can be found in almost any MUI application. An example shown below is a List object from the Media Logger application, which shows Reggae event log. The event log list is the main item of the program window. The most important visible elements of a List object are marked with digits.

Listclass1.png


  1. A bar of column titles. This is an optional element, used usually when a list has multiple columns.
  2. Data rows. Usually contain textual information. Simple formatting like italicizing, emboldening, color change (shown on the example) may be applied. List class does not allow for more advanced formatting, for example font cannot be changed. There is a possibility for adding images however.
  3. Selected data rows. Selection can be done with mouse, keyboard, or from inside application using object attributes. There is a possibility of optional multiselection.
  4. The active row. It is selected as well usually, however in theory an item can be active, but not selected. There is even a separate setting in MUI preferences for that case.
  5. Horizontal scroller. Used when data do not fit horizontally. Horizontal scroller can be disabled, set to automatic (appears when needed), or always visible.
  6. Vertical scroller. Used when data do not fit vertically. The srcroller may be disabled.

Basic Usage

The simplest case of List class usage is a single column list containing plain text strings. Then, the simplest way to define these texts is a static array. Such a List object is a static one. An array of strings is passed to the object with MUIA_List_SourceArray attribute. The attribute may be used only as an argument for a constructor. The array may be for example declared as a global variable:

STRPTR ListItems[] = { "first", "second", "third", "fourth", NULL };

The array must be terminated with a NULL pointer. The object is created as follows:

ListObj = MUI_NewObject(MUIC_List,
  MUIA_List_SourceArray, (ULONG)ListItems,
  MUIA_Frame, MUIV_Frame_ReadList,
  MUIA_Background, MUII_ReadListBack,
  MUIA_Font, MUIV_Font_List, 
TAG_END);

Except of MUIA_List_SourceArray attribute mentioned above, there are three attributes related to the object's appearance. They should be never omitted. MUIA_Frame defines the object frame. If not specified, the object will be frameless, which does not look good. There are two kinds of frame defined in the user preferences. MUIV_Frame_ReadList should be used for read-only lists (ones, which cannot be edited by user). Editable lists should have MUIV_Frame_InputList frame. In most of legacy Amiga GUI toolkits, read-only lists had recessed frames, while editable lists had embossed ones. Of course in MUI it is up to user, but on the application side the difference between two kinds of list frames should be maintained. The picture below shows an example appearance of a List object defined with the above code:


Listclass.1.png


source code for this example


Setting the MUIA_Frame attribute of a List object to one of special values listed above, has an unexpected and undocumented sideeffect. The background for the object is set automatically according to the frame type. What is strange, this automatic setting cannot be overridden by passing MUIA_Background explicitly, the attribute is ignored.

A List object can change its both dimensions freely when a containing window is resized. For static lists, where the contents is known in advance, one can request that the object width is locked to the width of the longest item. It is also possible to lock the object height to the total height of all the items. To do this MUIA_List_AdjustWidth and MUIA_List_AdjustHeight attributes may be specified for the object construction. Both of them are boolean attributes, with the default value FALSE. Let's add them to our List object:

  MUIA_List_AdjustWidth, TRUE,
  MUIA_List_AdjustHeight, TRUE,


Listclass2.png


source code for this example


Now the list object dimensions are fixed. As the list is the only object inside the window, the window is no more resizable (there is no bottom bar and sizing gadget). As the list now always show all its items, the vertical scroller is superfluous. In theory it can be removed, specifying MUIA_List_ScrollerPos attribute as MUIV_List_ScrollerPos_None, but MorphOS 2.7 MUI seems to ignore it.

Dynamic Adding and Removing Items

It is obvious that a static list is useful only in rare cases. Usually items are added and removed with user actions or because of other external events. The simplest method for adding a list item dynamically is MUIM_List_InsertSingle. It inserts a single item in a specified position:

DoMethod(ListObj, MUIM_List_InsertSingle, (IPTR)"fifth", MUIV_List_Insert_Bottom);

When multiple items are to be inserted, it can be done in one go with MUIM_List_Insert. Items should be grouped in an array. Number of inserted items may be either given explicitly, or the array may be terminated with NULL item (similarly as for MUIA_List_SourceArray), then −1 is passed as quantity. These two ways are shown in the example below, both calls are equivalent:

DoMethod(ListObj, MUIM_List_Insert, (IPTR)ListElements, 4, MUIV_List_Insert_Bottom);
DoMethod(ListObj, MUIM_List_Insert, (IPTR)ListElements, -1, MUIV_List_Insert_Bottom);

For the first call, ListItems table does not need to have a NULL element at the end. Then, the first form may be used to insert any continuous fragment of a source array. Let's insert only the second and the third element of the array:

DoMethod(ListObj, MUIM_List_Insert, (IPTR)&ListElements[1], 2, MUIV_List_Insert_Bottom);

Now, let's discuss the insertion position, which is the last argument of both the two described methods. The position may be specified explicitly as an index (counting from 0). Then the new element is inserted before the one specified. Except of this, there are four predefined constants:

  • MUIV_List_Insert_Top – inserts element(s) at the top of the list. It gives the same result as inserting at position 0, and indeed this constant has a value of 0.
  • MUIV_List_Insert_Bottom – inserts elements at bottom of the list.
  • MUIV_List_Insert_Active – inserts elements above the active element (the list cursor).
  • MUIV_List_Insert_Sorted – inserts elements according to the list sorting order.

There are two importants things, which should be observed when inserting arrays of items. The first one is that insertion position is not applied to the inserted array as a whole. Instead it is applied to every single item in turn. It leads to surprising results. For example when one inserts an array of items with MUIV_List_Insert_Top, the inserted array will appear in the list in reversed order. The first element of array will be of course inserted at top on the list, then the second element of array will be also inserted at top, so it will appear above the first, and so on. It also works this way when insertion position is given as a number. On the other hand, the order of an original array is preserved, when it is inserted at bottom, or when it is inserted above the list cursor (because the list cursor is moved down after adding each item).

The second important thing is list sorting. If one wants to keep the list sorted, every element must be inserted with MUIV_List_Insert_Sorted. This is because inserting an element as sorted does not sort the current contents of the list. The constant just means that insert position will be determined using current sorting order with assumption that the list is currently sorted. If it is not the case, results may be unpredictable. Then, if for some reason a list cannot be kept sorted, it may be sorted manually with MUIM_List_Sort() method.

The default sorting method for lists of plain text strings is alphabetical order, ascending, case insensitive. It is based on ASCII codes of characters, so works reliably only for English and other languages not using characters above the base ASCII range (up to code 127). For many languages, such sorting does not provide dictionary order. A solution for this will be shown later, when subclassing the List class will be discussed.

Removing items from a list is much easier. MUIM_List_Remove() method is used for this. Similarly as for insert, one can specify item index as an explicit number, or indirectly with a predefined constant. These constants are very similar to ones used for inserting, and allow for removing the first, the last or the active list item. They can be found in the List class autodoc file in the MorphOS SDK. An additional constant is MUIV_List_Remove_Selected, which removes all the selected items, assuming a list allows for multiselection. This is the only case when MUIM_List_Remove() can remove more than one item.

Every insert or remove operation causes the list object redraw, if the object is visible. Redrawing may slow down inserting huge arrays of items. The List class provides MUIA_List_Quiet attribute which temporarily disables object redrawing on inserting, removing or reordering operations. The attribute may be set to TRUE before intensive operations on the list, then set to FALSE at the end, so the list will be refreshed only once, showing the final result. There is also MUIM_List_Clear() method, which removes all the items in one go and is of course much faster than removing items in a loop.

List Reading

The problem of reading a List object consists of two parts: reading items and reading object state. Let's start from reading items. The MUIM_List_GetEntry() method gets a pointer to an item. In case of plain lists it is just a pointer to a string. It is used as follows:

STRPTR item;

DoMethod(ListObj, MUIM_List_GetEntry, index, (IPTR)&item);

The index argument may be just a number, or a predefined constant MUIV_List_GetEntry_Active to read the active item. The index counts from zero. What is important, a pointer to the item is not the method result. This pointer is placed in a variable. Address of this variable is passed as the last method argument. If the list does not contain an item of specified index, NULL is placed in the variable. Then, an example loop reading all the items may be organized as shown:

STRPTR item;
LONG i;

for (i = 0; ; i++)
{
  DoMethod(ListObj, MUIM_List_GetEntry, i, (IPTR)&item);
  if (item) Printf("Item %ld is '%s'.\n", i, item);
  else break;
}
  • MUIA_List_Active – index of the active item.
  • MUIA_List_Entries – total number of items.
  • MUIA_List_First – index of the first visible item.
  • MUIA_List_Visible – number of possibly visible items.
  • MUIA_List_DoubleClick – is set to TRUE when a left mouse button doubleclick is done over the list.

MUIA_List_Active and MUIA_List_First are also settable, so list cursor may be moved and list may be scrolled from inside the application. The other three are read-only for obvious reasons. Notifications may be set on attributes, which change directly after user actions: MUIA_List_Active, MUIA_List_Entries and MUIA_List_DoubleClick. Attributes MUIA_List_First and MUIA_List_Visible are describing object geometry rather than its contents. Values of these attributes may be only changed indirtectly by scrolling or window resizing. Then notifications on these two attributes do not work. As MUIA_List_Visible in fact describes list object height, its value may be higher than the number of items on the list, in case when the list is short. For example if the object is tall enough to display 5 items, but there are only 3 items on the list, the attribute will still be equal to 5, so it will be higher than MUIA_List_Entries.

The MUIA_List_DoubleClick attribute may seem to be useless at the first glance, as it does not provide information which item has been doubleclicked. A doubleclick however moves also the list cursor, so MUIA_List_Active is set to the index of the doubleclicked item. In case of multicolumn lists it is also possible to get an index of clicked column, it will be discussed later.

There are more List attributes handling following features:

  • multicolumn lists,
  • list title bar,
  • multiselection support.

As these are advanced topics, they will be explained later.

Dynamic String Copies

It is assumed from the start of this article, that text strings inserted into List object are static, they exists in memory during the whole execution time of a program. It can be true sometimes, but in many cases these strings are dynamic, for example entered by user. Sometimes it is just more comfortable to add strings from a buffer being a local variable. Then the list object has to create copies of inserted items. Fortunately this task (and also task of freeing these copies later) can be automated. Two attributes turn on feature of automatic copying:

MUIA_List_ConstructHook, MUIV_List_ConstructHook_String,
MUIA_List_DestructHook, MUIV_List_DestructHook_String,

These attributes must be always used together. Trashed list items and memory leaks will happen otherwise.

A More Complex Example

The third example program for MUI List class is more complex. It demonstrates typical operations done on a list object: inserting, removing and editing items, notification on list cursor movements and on a doubleclick. The interface of the example is shown below. The main element of the GUI is a list with its "editor" – a set of gadgets for inserting, removing and editing list items. A group of gadgets below displays the list state and allows for making changes in it. Such gadgets are rarely found in typical applications, they have been added here for demonstrational purpose.


Listclass3.png


source code for this example


The source code of the example is contained in three files. File start.c contains a custom startup code. It can be replaced with a standard one if needed, it will just result in increasing the compiled executable a bit. The file main.c contains the main code of the program. The file application.c is a code of a custom MUI class. This example uses a modern technique of creating a MUI application, where application actions are implemented as methods of an Application subclass. This way messing with callback hooks and cluttering the main event loop with code is avoided. For this reason the GUI is just built in the constructor of the subclass. Notifications setup and even the main event loop are implemented as methods of the subclass too. This systematic approach may look like overkill for such a simple project, but it pays off when the project is being extended. What is worth noting, such a design does not add neither executable size nor execution time overhead.

The list editor consists of a string gadget, "Add" button and "Delete" button. The "Add" button inserts a new list item (at the cursor position) with contents taken from the string gadget. Pressing enter key in the string gadget replaces the active list item with the content of the gadget. Let's take a closer look at this operation. The example uses dynamic string copies. There is no way to replace the text directly. One can try to hack it, reading the string pointer with MUIM_List_GetEntry() method and putting the new text there. It would be a critical mistake however. A memory area for the old content has been allocated automatically by the object. Overwriting it with new, possibly longer text will cause buffer overflow and memory trash. The final result may be a program crash, or even system crash if the damaged memory area is big enough. Then, when a list object creates copies of inserted elements on its own, the only right way to edit an item is to delete it, create a new item and insert it at the same position. The APPM_UpdateItem() method in the example does just this. Additionally, to hide the operation from user, even on very slow (or loaded) computer, all this happens while MUIA_List_Quiet attribute is set to TRUE.

Notifications on "Add" and "Delete" buttons are simple. Item removing is easier, as MUIM_List_Remove() may be called directly from notification. Adding a new item requires to call an auxiliary method APPM_AddItem(). This is because it is impossible in notification to get an attribute from an object not being the notification source. In this notification the source is the "Add" button, but one needs contents of the string gadget to be added as a list item. There are more such auxiliary methods. For example notifications on "First" and "Active" string gadgets need them, as MUIA_String_Integer attribute does not work as a notification trigger.

At the end of this section I would like to discuss one detail of the program: "Delete" button disabling. The button is disabled initially by specifying MUIA_Disabled, TRUE at construction time. Then the APPM_DeleteControl() method reacts on any change of the active list item and disables the "Delete" button when no item is active, or the list is empty. It is not that important for the program stability, as MUIM_List_Remove() verifies item index anyway. It is very important however to give a proper visual feedback to the user. Very few applications do this consistently, but dynamic disabling of gadgets, which are unusable at the moment, is important as it improves user experience.

Compound Data Items, Multicolumn Lists

Lists consisting of items being a simple text strings are useful, but not enough for many applications. In many cases multicolumn lists are needed, where every column contains some kind of data, which may be represented internally as a field in a structure. Such a structure may be named a compound data item. MUI List class can handle any arbitrary data structures as its items. The general concept of data handling in the List class is shown on the diagram below.


Listclass4.png


As the class can work with unknown data structures, the application has to "tell" the class how to perform basic operations on them. These operations are defined as List class methods, with the intention that application overloads these methods in a subclass. There are also three kinds of data: input data, internal data and displayed content.

  • internal data is the most important structure and is just an internal representation for a data item. Getting an item with MUIM_List_GetEntry() returns a pointer to internal data structure.
  • input data is a structure which is passed to MUIM_List_Insert() and MUIM_List_InsertSingle(). In many cases it may be the same as internal data.
  • displayed content is a set of strings (one per list column) which are to be displayed. These strings may contain control codes for styles, colors and inline images.

There are four methods in the List class to be overloaded. They deal with the three data kinds in the way depicted on the diagram and explained below:

  • MUIM_List_Construct – the method is called when an item is inserted to the list. It gets a pointer to input data structure and converts it somehow into an internal data structure. Then this internal data structure is inserted into a list. The default implementation of this method just passes through the received pointer. It may be the right thing to do, when data are static and simple. Two first examples in this article used the default item constructor. A typical task of the constructor is to create copies of dynamic data, it may also strip unused data off the input data structure or perform some conversions.
  • MUIM_List_Destruct – the method is called when an item is removed (or the object is disposed, which implies removing all items first). The item destructor frees any resources allocated in constructor for an item. It usually means freeing memory buffers allocated for copies of dynamic data. The default implementation of this method does nothing.
  • MUIM_List_Display – converts internal data representation to a set of strings to be displayed in list columns. The default implementation assumes internal data is a text string, which is passed through to be displayed in the first (and only) column.
  • MUIM_List_Compare – compares two internal data structures. This method is used only for list sorting and sorted inserts. The class uses it as a comparision callback for quicksort algorithm. The method need not to be implemented if sorting is not needed. The default implementation assumes it gets pointers to two text strings and calls stricmp() on them.

Note: Older versions of MUI implemented these four methods as callbacks using hooks. These hooks were set via attributes documented in the List class autodoc. Hooks still work for backward compatibility, but are not recommended in new code, especially as dealing with them on MorphOS is a bit cumbersome. The only useful exception is builtin copying string constructor and destructor. We have used them in the third example above.

To Copy Or Not to Copy?

The title question is one of the most important design decisions to take when one subclasses the List class. The main, obvious rule is that all item data must exist while the item is on the list. If it cannot be ensured, the item constructor has to create copies, which are later free in destructor. In most cases "copy" is the right choice, except of some special cases like:

  • Fixed (for example hardcoded) set of data, a rare case, but happens sometimes.
  • List is only a view of data stored elsewhere in the application, duplicating makes no sense.
  • There are multiple lists or data views sharing the same dataset which may be updated. While having non-copying constructor in this case avoids reinserting item in multiple lists, all the lists must be manually refreshed after update anyway.
  • List is a view of very large (let's say several gigabytes) dataset stored on disk. Data rows are loaded on demand inside MUIM_List_Display() then. In this case data are in fact never inserted, as the internal data structure will only contain, for example, offset in a file.

These cases are only examples, as the real life often gets even more complicated...

When there is a need to create a copy of some data inside MUIM_List_Construct(), the List class helps with memory management by providing automatically created memory pool. Using it is not mandatory however. On classic Amiga systems pooled memory allocations were substantially faster, but it is not the case on MorphOS. An advantage of using pooled allocations is a possibility to skip implementation of item destructor (MUIM_List_Destruct()), as the pool is freed automatically when the list object is disposed. For very long lists (thousands of items and more) freeing just the pool is also much faster than freeing individual allocations. On the other hand skipping the item destructor may be dangerous for programs adding and removing items continuously, as they will be consuming more and more memory, which will not be freed until a program is terminated.

Armed with all this knowledge, one can attempt to implement an example item constructor. A list object will be showing a warehouse inventory. Input data structure is a set of dynamically created strings, for example fetched from a remote database:

struct InputData
{
  STRPTR ItemName;
  STRPTR Quantity;
  STRPTR Price;
};

For internal representation, we prefer to convert numerical data to double precision floating point numbers, which will make any further computations easier:

struct InternalData
{
  STRPTR ItemName;
  DOUBLE Quantity;
  DOUBLE Price;
 };

The item constructor will take a pointer to InputData, allocate InternalData, create a copy of ItemName and convert Quantity and Price.

Example constructor

We have just opted for full copying constructor. The code of it is simple:

IPTR ListConstruct(Class *cl, Object *obj, struct MUIP_List_Construct *msg)
{
  struct InputData *entry = (struct InputData*)msg->entry;
  struct InternalData *idata;

  if (idata = AllocTaskPooled(sizeof(struct InternalData)))
  {
    if (idata->ItemName = AllocVecTaskPooled(strlen(entry->ItemName) + 1))
    {
      strcpy(idata->ItemName, entry->ItemName);
      idata->Quantity = atof(entry->Quantity);
      idata->Price = atof(entry->Price);
      return (IPTR)idata;
    }
    FreeTaskPooled(idata, sizeof(struct InternalData));
  }
  return NULL;
}

The code does not use a memory pool provided by the List class. Instead it uses modern MorphOS specific functions allocating from memory pool assigned to every system process. This pool is automatically disposed when a process terminates. Care should be taken to not return a half-constructed list item. Every allocation is checked, and if the second one, which allocates space for the item name string, fails, item structure is freed and constructor returns NULL. Numeric values are converted to numbers with standard atof() call. If allocations have been succesfull, a newly created and filled InternalData structure is returned.

Example Destructor

The only task of our destructor is to free memory areas allocated in the constructor. Note the order of deallocation, ItemName is freed first. If we freed the InternalData structure first, the pointer to item name would be in free memory and as such could be overwritten before we manage to free it as well. The constructor uses step-down order of construction, it allocates the item first and then its components. The destructor is symmetrical, so it frees components first, then the item. Because I've made sure in the constructor, that Internal Data is always fully constructed, I do not need to check pointers to components against NULL value.

IPTR ListDestruct(Class *cl, Object *obj, struct MUIP_List_Destruct *msg)
{
  struct InternalData *idata = (struct InternalData*)msg->entry;

  if (idata)
  {
    FreeVecTaskPooled(idata->ItemName);
    FreeTaskPooled(idata, sizeof(struct InternalData));
  }
  return 0;
}

In the example the destructor could be ommitted completely. As memory is allocated from the process pool, skipping deallocation does not create a memory leak, just deallocation is postponed to the end of process execution. It makes a practical difference only when large lists are created and disposed repeatedly while the program is running. In such case, destructor should free memory explicitly, so the system memory is not exhausted.