TweetFollow Us on Twitter

June 94 - Displaying Hierarchical Lists

Displaying Hierarchical Lists

MARTIN MINOW

[IMAGE 058-078_Minow_REV1.GIF]

Much of the data you manage on a Macintosh has a hierarchical nature. This article shows how your application can provide a clear, coherent data organization with a user-controlled display by using classic linked lists, storing the list data in handles, and displaying it with the List Manager. A triangular button mechanism, similar to that used in the Finder to open and close folders in list views, lets the user decide how much data to view on the screen.

If your application makes its hierarchical data accessible but not overwhelming, it will have an advantage over applications that provide only two alternatives -- "throw it all on the screen" or "hide everything." You can find many examples of flexible organization: The Finder presents file and folder information in a variety of display formats revealing more or less information according to the user's own desires. Document-based applications such as NewsWatcher provide a hierarchy of text information. The JMP statistical application provides buttons that reveal more detailed information about an analysis. Programming languages such as Frontier allow the programmer to display as much of a module's code as is needed.

This article shows how to do the following:

  • store hierarchical data in a linked list along with the information needed to display it properly
  • extend the List Manager by storing a button object in a List Manager cell that controls how the data hierarchy is displayed
  • build the data hierarchy and display it
  • let the user manipulate the buttons to view more or less of the data

The accompanying code on this issue's CD includes a sample library that stores data, displays it, and manages the buttons, and a simple application that uses the library. The techniques described in the article are appropriate for displaying and organizing moderate amounts of data; they're less useful for static data or large amounts of data and are inefficient with small amounts of data.

Figure 1 shows a Finder-like window that was created with this library; the files displayed are from the sample library. I've called the libraryTwistDown to emphasize how the display acts when you click the buttons. The Finder development team calls the buttonstriangular buttons .

[IMAGE 058-078_Minow_REV2.GIF]

Figure 1. Window created with TwistDown library


STORING DATA IN A LINKED LIST

There isn't much in this article on linked lists or handles; I assume you struggled with "classical" list processing when you learned to program and have done enough programming on the Macintosh to understand how handle-based storage operates. Keep in mind that two kinds of lists are discussed here: linked lists and Macintosh List Manager display lists. To keep confusion to a minimum,element refers to a component of a linked list andcell refers to a component of a List Manager list. Note also that linked lists contain the data, while the List Manager list only controls the appearance of that data. A good understanding of the List Manager is needed to follow the code examples later in the article. (For details on the List Manager, seeInside Macintosh: More Macintosh Toolbox , or Inside Macintosh Volume IV.)

In the context of this article, a list element is a chunk of data, a few flags, and two linkages. This section discusses the linkages, which connect list elements into sequences and hierarchies, and the creation and disposal of list elements. The flags, which simplify formatting the data, are discussed later in the section "Controlling Data Appearance."

Figure 2 illustrates the linkages that connect list elements. The important thing to remember about the hierarchical linked lists we're using is that any element may have asuccessor -- the sibling element that follows it -- and adescendant -- a child element that begins a lower level of the hierarchy. For example, a document outline has a sequence of chapters (siblings) and each chapter has, as its descendants, a sequence of sections.

In the sample code, each list element is stored in a handle. This allows the Memory Manager to reorganize memory to store data efficiently. However, don't forget that the application program is responsible for disposing of data that's no longer needed.

Two library functions manage the list elements: MakeTwistDownElement creates an element and connects it to the list hierarchy and DisposeTwistDownElement deletes an element along with its descendants and successors.

[IMAGE 058-078_Minow_REV3.GIF]

Figure 2. List element organization

CREATING A LIST ELEMENT
Listing 1 shows the definition of our element structure, TwistDownRecord. Each field of this structure will be explained as it's encountered in the sample code.


Listing 1. TwistDownRecord

struct TwistDownRecord {
  struct TwistDownRecord  **nextElement; /* -> successor element  */
  struct TwistDownRecord  **subElement;  /* -> descendant element */
  short                   indentLevel;   /* Indentation depth */
  unsigned short          flag;          /* Control flags */
  unsigned short          dataLength;    /* Actual data length */
  unsigned char           data[1];       /* Data to display */
};
typedef struct TwistDownRecord  TwistDownRecord,
        *TwistDownPtr, **TwistDownHandle;

MakeTwistDownElement (Listing 2) is called with the data to store in the element and a handle to its predecessor. The predecessor is either NULL or the previous (elder) sibling element. For example, when creating element 3 in the list shown in Figure 2, the previous element is element 1.

TwistDownRecord is a variable-length structure and NewHandle creates an instance that's large enough to hold the caller's data record. The structure definition, however, contains one bit of trickery that's required by the ANSI C standard -- it must specify at least one byte for the data [] placeholder. This is why the parameter to NewHandle adjusts the handle size to eliminate the extra byte.

DISPOSING OF A LIST ELEMENT
The DisposeTwistDownHandle function disposes of a list element and then disposes of its descendant and successor lists. To dispose of an entire list, call this function with the first list element.

A simplified version of DisposeTwistDownHandle is shown in Listing 3. The library implementation allows the application developer to specify a function that's called when disposing of each element in the list. This is needed if an application has to store complex structures -- themselves containing Ptr or Handle references -- in a TwistDownHandle.



Listing 2. MakeTwistDownElement

OSErr MakeTwistDownElement(TwistDownHandle  previousElement,
                           short            indentLevel,
                           unsigned short   dataLength,
                           Ptr              dataPtr,
                           TwistDownHandle  *result)
{
    TwistDownHandle twistDownHandle;

    twistDownHandle =
        (TwistDownHandle) NewHandle(sizeof(TwistDownRecord) 
                        - sizeof(unsigned char) + dataLength);
    *result = twistDownHandle;
    if (twistDownHandle != NULL) {
        if (previousElement != NULL)
            (**previousElement).nextElement = twistDownHandle;
        (**twistDownHandle).nextElement = NULL;
        (**twistDownHandle).subElement = NULL;
        (**twistDownHandle).indentLevel = indentLevel;
        (**twistDownHandle).flag = 0;
        (**twistDownHandle).dataLength = dataLength;
        if (dataPtr != NULL)
            BlockMove(dataPtr, (**twistDownHandle).data, dataLength);
    }
    return (MemError());
}

Listing 3. DisposeTwistDownHandle

void DisposeTwistDownHandle(TwistDownHandle twistDownHandle)
{
    TwistDownHandle nextElement, subElement;

    while (twistDownHandle != NULL) {
        nextElement = (**twistDownHandle).nextElement;
        subElement = (**twistDownHandle).subElement;
        DisposeHandle((Handle) twistDownHandle);
        if (subElement != NULL)
            DisposeTwistDownHandle(subElement);
        twistDownHandle = nextElement;
    }
}

Note that DisposeTwistDownHandle is a recursive function; it calls itself to dispose of the descendants of a hierarchy. If it's called with the list shown in Figure 2, it disposes of elements in the order 1, 2, and 3.

Using recursion simplifies the list-management algorithms in the TwistDown function library. However, it's not without its pitfalls in the real world. Each time a function such as DisposeTwistDownHandle encounters a subelement list, it calls itself to dispose of that list, and each of these calls uses stack space. While this isn't usually a problem for applications, you should avoid recursive algorithms in device drivers or other nonapplication code segments because they must work within the constraints of some other application's stack.*

CONTROLLING DATA APPEARANCE

Once the data is organized as a hierarchical list, the application could simply display the whole list by just storing handles to the list elements in List Manager cells. However, if your application lets the user control how much of the data is displayed, there must be a way for the user to browse through the data and to specify which elements are visible and which are hidden.

A familiar mechanism for doing this exists in the Finder, where small buttons indicate which cells have subhierarchies and whether the subhierarchy is visible. These triangular buttons have two stable states: one for closed (invisible) hierarchies and one for open (visible) hierarchies. There are also three transient states: an intermediate button is displayed briefly when the user clicks a triangular button to change between the open and closed states; and the closed and open buttons are drawn filled when a mouse-down event is located on the button. To manage these buttons and the display of visible data, each list element needs a few flags and an indentation variable. These are stored in the TwistDownRecord structure.

The indentation variable -- indentLevel -- specifies the hierarchical depth of an element and is used to display sublists so that the data for an element appears under its parent, but indented to show its place in the hierarchy.

The bits in the flag field are used to record the record's state and to communicate between the application and the List Manager's list definition function (LDEF):

/* These are the values that can appear in the flag word. */
#define kHasTwistDown      0x0001  /* This element has a sublist */
#define kShowSublist       0x0002  /* Display the sublist content */
#define kOldShowSublist    0x0004  /* Saved kShowSublist state */
#define kSelectedElement   0x0008  /* Copy "selected" from list */
#define kDrawButtonFilled  0x0010  /* Signal "mouseDown" in button */
#define kOnlyRedrawButton  0x0020  /* Signal "tracking mouse" */
#define kDrawIntermediate  0x0040  /* Draw the animation polygon */
#define kEraseButtonArea   0x0080  /* Need complete button redraw */

The flag field is defined as an unsigned short with explicitly defined bits rather than as a bitfield, which would have made the program slightly easier to read. However, the ANSI C standard doesn't specify how the bits in a bitfield are arranged, and different compilers are free to choose their own organization of the bits. This means that if you write parts of your code using several compilers, or distribute modules in object form for others to use, you may cause a debugging nightmare. This is especially true if you use bitfields to construct data records that are sent in network messages between different systems. The flag bits could have been defined as an enum, but this can also cause portability problems. Using explicit bit definitions will also make it easier to convert your code to run on the Power Macintosh. *

The first four flag bits have the following meanings:

  • kHasTwistDown is set if the element should have a triangular button when it's drawn. While you might assume that the existence of a non-null subElement pointer would be sufficient, the Finder illustrates a better design: it displays triangular buttons for all folders, even if there are no files in a folder. This immediately tells the user that the line on the display represents a folder, rather than a file.
  • kShowSublist is set if the sublist should be displayed. This is normally controlled by the user clicking a triangular button, which changes the kShowSublist state.
  • kOldShowSublist is used to save the old kShowSublist setting in case you need to temporarily change the display hierarchy. This lets you undo a display state change or provide a Show All Hierarchies command. It's not used in the sample code.
  • kSelectedElement records the selection state of the list cell. It's needed to properly retain the selection status of visible cells.

The other flag bits are needed to handle mouse events. They're set by the mouse-down event handler and the LDEF references them to control its actions:

  • kDrawButtonFilled is set when the user presses a button (the cursor is over a triangular button while the user has the mouse buttton held down). It causes the LDEF to fill the triangular button to indicate that the user is pressing it.
  • kOnlyRedrawButton is set to constrain the LDEF so that the entire display line doesn't blink when the user presses a button. As the user moves the cursor in and out of the button, the drawing procedure must redraw the button to show whether the user is pressing the button. However, there's no need to redraw the actual data contents. This flag tells the LDEF to redraw only the button.
  • kDrawIntermediate is set when the user releases the mouse button within the button area: the button state changes from closed to open or vice versa. To indicate this change, the LDEF draws the button in an intermediate state, delays for an instant, and then draws the button in its new, stable state.
  • kEraseButtonArea is set to erase the button area before it's drawn. If not set, the button is redrawn in its new form, but not erased; this eliminates unnecessary button flicker.

The TwistDown library uses the following four macros internally to access the flag word:

#define SetTDFlag(tdHandle, mask)      ((**tdHandle).flag |= (mask))
#define ClearTDFlag(tdHandle, mask)    ((**tdHandle).flag &= ~(mask))
#define InvertTDFlag(tdHandle, mask)   ((**tdHandle).flag ^= (mask))
#define TestTDFlag(tdHandle, mask) (((**tdHandle).flag &(mask)) != 0)

CREATING THE LIST RECORD

When you first look at the List Manager, it may appear to be the solution to all your display needs. Unfortunately, it has a number of characteristics that restrict its usefulness. It's designed to store limited amounts of data, and performance slows appreciably as you increase the number of cells or the amount of data stored in the cells. Also, if your list cells are not all the same size or your application needs fine control over scrolling, you'll probably find life simpler if you create your own function library. For example, both MacApp and the THINK Class Library offer flexible libraries for displaying and managing structured data. However, the List Manager serves well for straightforward lists of a small number of items -- and with the addition of the triangular buttons it becomes a very useful tool.

The TwistDown subroutine library creates a one-column list with a vertical scroll bar. The code has only two unusual features:

  • NewTwistDownList stores a small amount of private information in a handle that's stored in the userHandle field of the list record. This includes a pointer to a user-defined drawing function, the display font and font size, and a flag that signals whether clicking on cell data should highlight the cell contents.
  • It establishes a private LDEF that manages the visual display. Normally, the LDEF is stored in a resource. In the sample code, however, it's linked into the application and a stub resource is created for the benefit of the List Manager. This stub, a three-instruction procedure that the List Manager calls, jumps to the twist- down LDEF. This is not necessary for this library -- it could have been separately compiled -- but is useful for debugging and for LDEF procedures that need to access application globals.

When you recompile this program to run on a Power Macintosh as a "native" application (rather than in 680x0-compatibility mode), you'll have to redo this sequence slightly. The time to worry about conversion is now, before your customers are tapping you on the shoulder asking, "Not today? How about next Tuesday?" There's more on converting for Power Macintosh at the end of this article. *

Note that there are two separate drawing procedures: the twist-down LDEF manages the buttons and drawing for simple text displays, while the application program can specify its own drawing function to draw more complex data.

THE TWISTDOWNPRIVATERECORD
The twist-down LDEF requires a small amount of global information to properly process the list. This is stored in a handle-based structure defined as shown in Listin g 4. Two Boolean variables in this record haven't been described: canHiliteSelection and isLeftJustify. The canHiliteSelection field controls whether the LDEF highlights selected cells. The isLeftJustify flag is set for left-to-right languages (such as English) and cleared for languages such as Arabic and Hebrew. This flag isn't used in the code shown in this article, but the TwistDown library on the CD shows how an application might handle a right-to-left language.

CREATING TRIANGULAR BUTTON POLYGONS
The triangular buttons are defined as QuickDraw polygons, rather than as bitmaps, with the advantage that the function is independent of the list cell size and script direction. This is useful for localization or for programs used by people who are visually impaired or have diminished motor skills: the program will display larger buttons if the application or user chooses a large font. It also lets the program draw the closed and intermediate buttons pointing in the proper direction for right- to-left script systems such as Arabic and Hebrew. Figure 3 illustrates an expanded view of the triangular buttons. As an example, the code in Listing 5 shows how you would create the polygons for a left-to-right script. See the sample on the CD for more general code, which accounts for the writing direction of the script.


Listing 4. TwistDownPrivateRecord

struct TwistDownPrivateRecord {
  TwistDownDrawProc   drawProc;   /* User-defined drawing function */
  PolyHandle          openTriangle;         /* The expanded button */
  PolyHandle          closedTriangle;         /* The closed button */
  PolyHandle          intermediateTriangle;   /* Animation */
  short               tabIndent;              /* Child indentation */
  short               fontSize;               /* For TextSize */
  short               fontNumber;             /* For TextFont */
  Boolean             canHiliteSelection;     /* Highlight cell OK? */
  Boolean             isLeftJustify;          /* GetSystJust value */
  short               triangleWidth;          /* Button width */
};
typedef struct TwistDownPrivateRecord   TwistDownPrivateRecord,
        *TwistDownPrivatePtr, **TwistDownPrivateHandle;

[IMAGE 058-078_Minow_REV10.GIF]

Figure 3. The triangular buttons

PUTTING DATA INTO THE LIST

After creating the List Manager list, the application builds its hierarchical structure (the linked list). List elements are created by the MakeTwistDownElement function. As described earlier in "Creating a List Element," MakeTwistDownElement obtains the necessary (handle) storage, initializes all flags, and stores the application data in the list element. It also links the new element to the previous (elder sibling) element in the list.

Normally, a twist-down list is built by a recursive function such as the one shown in Listing 6, MyBuildHierarchy. MyBuildHierarchy calls a function named MyGetInfo that stores a small amountof data into a structure called MyInfoRecord. Neither of these is defined here: they're application specific.

CREATING THE VISIBLE DISPLAY

After you've built the data hierarchy, the next step is to determine which elements are visible initially and build the visible list. The CreateVisibleList function constructs a new visible display given the head of a hierarchical list and a List Manager handle. It stores the head in the first cell (cell [0, 0]) and calls BuildVisibleList to store the visible elements in the subsequent cells.

Listing 5. Creating triangular button polygons

GetFontInfo(&info);
buttonSize = info.ascent;          /* The button height */
buttonSize &= ~1;              /* Round down to an even number */
halfSize = buttonSize / 2;         /* For 45-degree triangles */
intermediateSize = (buttonSize * 3) / 4;
(**privateHdl).openTriangle = OpenPoly();
    MoveTo(0, halfSize);
    LineTo(buttonSize, halfSize);
    LineTo(halfSize, buttonSize);
    LineTo(0, halfSize);
ClosePoly();
(**privateHdl).closedTriangle = OpenPoly();
    MoveTo(halfSize, 0);
    LineTo(buttonSize, halfSize);
    LineTo(halfSize, buttonSize);
    LineTo(halfSize, 0);
ClosePoly();
(**privateHdl).intermediateTriangle = OpenPoly();
    MoveTo(intermediateSize, 0);
    LineTo(intermediateSize, intermediateSize);
    LineTo(0, intermediateSize);
    LineTo(intermediateSize, 0);
ClosePoly();

Listing 6. Building a twist-down list

TwistDownHandle MyBuildHierarchy(ListHandle theList,
                    short indentLevel)
{
    OSErr           status;
    TwistDownHandle previousElement, thisElement, firstElement;
    MyInfoRecord    myInfoRecord;
    Boolean         isHierarchy;
    EventRecord     currentEvent;

    firstElement = NULL;
    previousElement = NULL;
    /*** Other initialization here */
    do {
        /*** Call EventAvail here to give time to background tasks.*/
        EventAvail(everyEvent, &currentEvent);
        status = MyGetInfo(&myInfoRecord, &isHierarchy);
        if (status == noErr)
            status = MakeTwistDownElement(previousElement,
                indentLevel, sizeof(MyInfoRecord),
                (Ptr) &myInfoRecord, &thisElement);
        if (status == noErr) {
            /*** Remember the first element in this sibling        */
            /*** sequence;                                         */
            /*** it's needed by our caller.                        */
            if (firstElement == NULL)
                firstElement = thisElement;
            /*** If this data begins a hierarchy, descend by       */
            /*** calling this function recursively. Store the      */
            /*** first element of the new sublist in the           */
            /*** subElement pointer. (flag & kHasTwistDown)    */
            /*** will be TRUE even if the child list is empty.     */
            if (isHierarchy) {
                SetTDFlag(thisElement, kHasTwistDown);
                (**thisElement).subElement =
                        MyBuildHierarchy(theList, indentLevel + 1);
            }
            /*** Set sibling linkage for next element.             */
            previousElement = thisElement;
        }
    } while (status == noErr);
    return (firstElement);
}

BuildVisibleList is called in two situations: when the application first constructs the list and when the user changes the visual hierarchy by clicking a triangular button. It calls CountVisibleElements to determine the number of cells needed, adjusts the size of the list to the desired number, and calls a recursive function, SetElementsInList, to do the actual storage. SetElementsInList needs what is essentially a global counter to know which cell will receive the current list element.

BuildVisibleList associates list cells with elements in the hierarchical list as follows:

  1. It copies the current selection status of each list cell from the List Manager cell to the associated TwistDownHandle element.
  2. It counts the number of elements that will be displayed and adds or removes rows from the List Manager list as needed.
  3. Finally, it stores references to the visible elements in the list cells, updating the selection status as needed.

The utility functions used in adding and removing elements from the List Manager list aren't shown here but may be examined in the sample library. BuildVisibleList uses several local, recursive functions to process the hierarchical list that all have a similar overall structure. For example, CountVisibleElements (Listing 7) computes the number of list elements that should be displayed.

HANDLING MOUSE EVENTS

Now that we have a visible list, we're ready to let the user manipulate the hierarchy by clicking the triangular buttons. When the user presses the mouse button, the application decides whether the cursor is in one of its windows and whether this window might just happen to have a twist-down list. If so, the application calls DoTwistDownClick with the list handle, a pointer to an event record, and a pointer to a Cell structure that identifies the selected cell on exit. DoTwistDownClick returns one of five action states, as shown in Table 1.


Listing 7. CountVisibleElements

short CountVisibleElements(TwistDownHandle twistDownHandle)
{
    short       result;

    result = 0;
    while (twistDownHandle != NULL) {
        ++result;
        if (TestTDFlag(twistDownHandle, kShowSublist))
            result += 
                CountVisibleElements((**twistDownHandle).subElement);
    }
    return (result);
}

Table 1
DoTwistDownClick action states

Action StateMeaning
kTwistDownNotInListThe mouse-down event was not in the list area. Your application should handle this event.
kTwistDownNoClickThe user pressed the mouse button in a triangular button but released it outside the button. Your application should ignore this click.
kTwistDownButtonClickThe mouse click was in the triangular button. DoTwistDownClick has handled this, but your application may need to do further processing.
kTwistDownClickThe user clicked, once, on list data. Your application may need to do further processing.
kTwistDownDoubleClick The user double-clicked on list data. Your application may need to do further processing.

The techniques described here for handling mouse events can be used to create lists whose cells contain other kinds of active elements, such as buttons or checkboxes . *

DoTwistDownClick, together with the subroutines it calls, hides a fairly complex process consisting of the following steps; these steps are described further in the following sections and illustrated in the simplified version of DoTwistClick shown in Listing 8.

  1. Check that the mouse-down event is in the list area.
  2. Check that the user pressed a triangular button.
  3. Track the mouse while it's held down.
  4. Take appropriate action when the mouse button is released.

Did the user press in the list rectangle?Get the mouse location in local coordinates and the window rectangle that contains the list and its scroll bar. If the mouse location is not in the list, just return.

Did the user press a triangular button?The user pressed in the list area; is it in a button? The code sample on the CD has a test for left or right alignment (so that you can use the function with Arabic or Hebrew script systems) but that test is ignored here. The central algorithm determines the rectangle that encloses all the cell buttons. If the cursor is in that area, it then checks whether there is a cell under the cursor and, if so, whether this cell actually displays a button.

Track the mouse while it's in the button area.If we get past all that, we know that the user pressed a triangular button. The click-handler sets and clears flag bits that the LDEF references when redrawing the list cell. The LDEF starts by drawing the button in its active (filled) state. Note that each call to LDraw redraws the list cell -- but, as pointed out earlier, the kOnlyRedrawButton flag prevents the entire display line from blinking.

The sequence beginning with "if (StillDown ())" in the code shows how you can track your own visual elements, such as icons, as if they were normal buttons. You can also use this technique to add checkboxes or other button-like objects to list cells.

The user released the mouse button.When the user releases the mouse button in the triangular button area, the application changes the button state (for example, from [IMAGE 058-078_Minow_REV14.GIF] to [IMAGE 058-078_Minow_REV15.GIF] ). This is a two-step process that briefly flashes an intermediate button ( [IMAGE 058-078_Minow_REV16.GIF] ) to give the user the illusion of change. While your application would certainly work without this subtle touch, it wouldn't look as good. Call theExpandOrCollapseTwistDownList function after flashing the intermediate button to redraw the button in its new state. Note that kEraseButtonArea is set so that the intermediate button is drawn properly.


Listing 8. DoTwistDownClick

TwistDownClickState
    DoTwistDownClick(ListHandle          theList,
                     const EventRecord   *eventRecordPtr,
                     Cell                *selectedListCell)
{
    Cell             theCell;      /* Current list cell            */
    Rect             hitRect;      /* The button area in this cell */
    Boolean          inHitRect;    /* Cursor is in the button area */
    Boolean          newInHitRect; /* Cursor moved into the button */
    short            cellHeight;   /* Height of a list cell        */
    short            visibleTop;   /* Top pixel in the list area   */
    TwistDownHandle  twistDownHandle;/* Current twist-down element */
    TwistDownPrivateHandle  privateHandle;  /* Private data        */
    Point            mousePt;      /* Where the mouse is located   */
    TwistDownClickState  result;   /* Function result              */
    long             finalTicks;   /* For the Delay function       */

    /*** 1. Did the user press in the list rectangle?              */
    mousePt = eventRecordPtr->where;
    GlobalToLocal(&mousePt);     /* Mouse in local coordinates */
    hitRect = (**theList).rView;           /* Here's the list area */
    hitRect.right += kScrollBarWidth;/* Include the scroll bar, too*/
    if (PtInRect(mousePt, &hitRect) == FALSE) {
        result = kTwistDownNotInList;
        return (result);
    }

    /*** 2. Did the user press a triangular button?                */
    privateHdl = (TwistDownPrivateHandle) (**theList).userHandle;
    hitRect.right = (**theList).rView.left + (**theList).indent.h
                        + (**privateHdl).triangleWidth;
    inHitRect = FALSE;
    if (PtInRect(mousePt, &hitRect)) {
        /*** The mouse is in the button area; is there a cell?     */
        cellHeight = (**theList).cellSize.v;
        theCell.h = 0;
        theCell.v = ((mousePt.v - (**theList).rView.top) / cellHeight
                    + (**theList).visible.top;
        /*** This is a list cell that should have data. Get the    */
        /*** twistdown element handle. If there's no data, or no   */
        /*** hierarchy, the click will be ignored.                 */
        twistDownHandle = GetTwistDownElementHandle(theList, theCell);
        if ((twistDownHandle != NULL)
         && TestTDFlag(twistDownHandle, kHasTwistDown))
            inHitRect = TRUE;
    }
    if (inHitRect == FALSE) {
        /*** There's no button here, or the user didn't click it.  */
        /*** Just call the normal list click-handler and return    */
        /*** its value.  This is needed to handle scroll bars      */
        /*** correctly.                                            */
        if (LClick(mousePt, eventRecordPtr->modifiers, theList))
            return (kTwistDownDoubleClick);
        else {
            return (kTwistDownClick);
        }
    }

    /*** 3. Track the mouse while it's in the button area.         */
    SetTDFlag(twistDownHandle,
        kDrawButtonFilled | kOnlyRedrawButton);
    LDraw(theCell, theList);
    /*** Set hitRect to the triangular button dimensions.          */
    hitRect.top = (theCell.v - (**theList).visible.top) * cellHeight
                    + (**theList).rView.top;
    hitRect.bottom = hitRect.top + cellHeight;
    /*** Track the mouse while it's still down: if it moves into   */
    /*** the rectangle, redraw it filled; if it moves out, redraw  */
    /*** it unfilled.                                              */
    if (StillDown()) {
        while (WaitMouseUp()) {
            GetMouse(&mousePt);
            newInHitRect = PtInRect(mousePt, &hitRect);
            if (newInHitRect != inHitRect) {
                /*** The cursor moved into or out of the triangle. */
                InvertTDFlag(twistDownHandle, kDrawButtonFilled);
                LDraw(theCell, theList);
                inHitRect = newInHitRect;
            }
        }
    }
    
    /*** 4. The user released the mouse button.                    */
    if (inHitRect == FALSE) {
        /*** The user canceled the operation by releasing the      */
        /*** mouse outside the triangular button area.             */
        /*** drawButtonFilled will normally be clear. It can be    */
        /*** set, however, if the user clicks so briefly that the  */
        /*** StillDown() test above is FALSE                       */
        if (TestTDFlag(twistDownHandle, kDrawButtonFilled)) {
            ClearTDFlag(twistDownHandle), kDrawButtonFilled);
            LDraw(theCell, theList);
        }
        ClearTDFlag(twistDownHandle), kOnlyRedrawButton);
        return (kTwistDownNoClick);
    }
    SetTDFlag(twistDownHandle,
        (kDrawIntermediate | kEraseButtonArea));
    LDraw(theCell, theList);
    Delay(kAnimationDelay, &finalTicks);
    ClearTDFlag(twistDownHandle,
        (kDrawIntermediate | kDrawButtonFilled | kEraseButtonArea));
    ExpandOrCollapseTwistDownList(theList, theCell);
    *selectedListCell = theCell;
    ClearTDFlag(twistDownHandle, kOnlyRedrawButton);
    return (kTwistDownButtonClick);
}

EXPAND OR COLLAPSE THE HIERARCHY
ExpandOrCollapseTwistDownList (Listing 9) is normally called directly by DoTwistDownClick, as shown in the preceding section. It can also be called directly by the application. When called, it inverts the "expansion" state of the designated list cell, redraws the triangular button, and calls BuildVisibleList (described earlier in "Creating the Visible Display") to revise the visible hierarchy. Note that BuildVisibleList will modify the display starting with the current cell: the cells above will not change and thus need not be modified or redrawn.

DRAWING THE LIST CELL

When the contents of a list cell change or the display requires updating, the List Manager calls the TwistDownLDEF function. This function draws the button in its current state and either draws the list cell (for simple text cells) or calls a user-defined drawing function to draw more complex cells. The code is generally straightforward (again, ignoring right or left considerations). Basically, it examines the state of the kOnlyRedrawButton flag and proceeds as follows: * If the flag is set, "shrink" the display rectangle so that only the button is redrawn. Choose the correct triangular polygon and draw it in its proper state. * If the flag is clear, draw the cell data and the triangular polygon.

Let's look more closely at the TwistDownLDEF drawing code (Listing 10):

  1. First we determine what to draw. To begin drawing the list, we first need the cell content. This is the handle that contains the list element. We also check that userHandle has been set up correctly. Note that we don't use the List Manager's LFind function because the data might not be aligned in the list cell storage. (Actually, the data is aligned, because only handles are stored in the cells, but it doesn't hurt to be suspicious.) If the handle contains a list element, the values of the flags determine what to draw.
  2. Next, we call DrawTriangle to draw the triangular button. The value of theFlag determines the button state and its location.
  3. Finally, after checking to be sure kOnlyRedrawButton is not set and that we have the data, TwistDownLDEF redraws the cell data with the proper indentations. Here's where the code allows you to specify a user-defined drawing function.


Listing 9. ExpandOrCollapseTwistDownList

twistDownHandle = GetTwistDownElementHandle(theList, theCell);
if ((twistDownHandle != NULL)
 && TestTDFlag(twistDownHandle, kHasTwistDown)) {
    InvertTDFlag(twistDownHandle, kShowSublist);
    /*** Redraw the triangular button in its new state.            */
    ClearTDFlag(twistDownHandle, kDrawButtonFilled);
    SetTDFlag(twistDownHandle,
        (kOnlyRedrawButton | kEraseButtonArea));
    LDraw(theCell, theList);
    ClearTDFlag(twistDownHandle,
        (kOnlyRedrawButton | kEraseButtonArea));
    /*** If some other part of the list will change, rebuild the   */
    /*** List Manager cells and redraw the list.                   */
    if ((**twistDownHandle).subElement != NULL)
        BuildVisibleList(theList, theCell.v);
}

Listing 10. TwistDownLDEF drawing code

pascal void TwistDownLDEF(
        short               listMessage,
        Boolean             listSelect,
        Rect                *listRect,
        Cell                listCell,           /* Unused          */
        short               listDataOffset,     /* Unused          */
        short               listDataLen,
        ListHandle          theList
    )
{
    short           indent;           /* Cell data indentation     */
    TwistDownHandle twistDownHandle;  /* The cell data             */
    TwistDownPtr    twistDownPtr;     /* Cell data (locked handle) */
    short           cellSize;         /* sizeof(TwistDownHandle)   */
    PolyHandle      polyHandle;       /* Button polygon            */
    Point           polyPoint;        /* Where to draw the button  */
    Rect            viewRect;         /* Actual cell drawing area  */
    signed char     elementLockState; /* twistDownHandle lock state*/

    #define TestFlag(flagBit) ((theFlag & (flagBit)) != 0)

    . . . /*** Other LDEF processing isn't shown.                  */

    /*** 1. Determine what to draw.                                */
    cellSize = sizeof twistDownHandle;
    LGetCell(&twistDownHandle, &cellSize, listCell, theList);
    if ((cellSize == sizeof twistDownHandle)
         && twistDownHandle != NULL) {
        /*** There is a list element. (This if statement extends   */
        /*** all the way to the end of the sequence.) Lock the     */
        /*** element in memory and look at the flag values. Set    */
        /*** viewRect to the part of the List Manager cell that    */
        /*** will be drawn.                                        */
        elementLockState = HGetState((Handle) twistDownHandle);
        HLock((Handle) twistDownHandle);
        twistDownPtr = (*twistDownHandle);
        privateHdl = (TwistDownPrivateHandle) (**theList).userHandle;
        viewRect = *listRect;
        theFlag = (*twistDownPtr).flag;
        if (TestFlag(kOnlyRedrawButton)) { 
            /*** Shrink the display area when only the button is   */
            /*** redrawn.                                          */
            viewRect.right = viewRect.left + (**theList).indent.h
                                + (**privateHdl).triangleWidth;
        }
        if (TestFlag(kOnlyRedrawButton) == FALSE 
         || TestFlag(kEraseButtonArea))
            EraseRect(&viewRect);

        /*** 2. Draw the triangular button.                        */ 
        if (TestFlag(kHasTwistDown)) {
            polyPoint.v = listRect->top + 1;
            polyPoint.h = listRect->left + (**theList).indent.h
                            + kTriangleOutsideGap;
            if (TestFlag(kDrawIntermediate))
                polyHandle = (**privateHdl).intermediateTriangle;
            else if (TestFlag(kShowSublist))
                polyHandle = (**privateHdl).openTriangle;
            else
                polyHandle = (**privateHdl).closedTriangle;
            DrawTriangle(polyHandle, polyPoint,
                         theFlag & kDrawButtonFilled);
        }
        /*** 3. Draw the cell data.                                */
        if (TestFlag(kOnlyRedrawButton) == FALSE
         && (*twistDownPtr).dataLength > 0) {
            /*** Indent the text to show the depth of the          */
            /*** hierarchy. Then build a display rectangle for the */
            /*** cell text and set the pen to the leftmost         */
            /*** position of the text.                             */
            indent = (**theList).indent.h
                    + (**privateHdl).triangleWidth
                    + ((**privateHdl).tabIndent
                       * (*twistDownPtr).indentLevel);
            viewRect = *listRect;
            viewRect.left += indent;
            TextFont((**privateHdl).fontNumber);
            TextSize((**privateHdl).fontSize);
            /*** If the user didn't provide a drawing procedure,   */
            /*** draw a text string. Otherwise, call the user's    */
            /*** procedure.                                        */
            if ((**privateHdl).drawProc == NULL) {
                MoveTo(viewRect.left, viewRect.top
                    + (**theList).indent.v);
                DrawText((*twistDownPtr).data, 0,
                    (*twistDownPtr).dataLength);
            }
            else {
                /* Call user's drawing function */
                (*(**privateHdl).drawProc)( 
                  theList,                     /* The list handle  */
                  (const Ptr) (*twistDownPtr).data,/* Data to draw */
                  (*twistDownPtr).dataLength,  /* Size of the data */
                  &viewRect);              /* Where to draw it */
            }
        }                            /* If we're drawing cell data */
        HSetState((Handle) twistDownHandle, elementLockState);
    }                                      /* If we have cell data */
}

THE DRAWTRIANGLE FUNCTION
If you look closely at the triangular buttons on a color or grayscale display, you'll notice that the button is filled with a grayish background color. (The Finder uses the color the user assigned to the file, while we use a light gray color.) The DrawTriangle function called by TwistDownLDEF takes three parameters: the polygon, where it's to be drawn, and a Boolean that specifies whether the user is currently pressing the triangular button. DrawTriangle uses the DeviceLoop procedure, DrawThisTriangle, which calls the actual drawing function for each type of device so that drawing can be optimized for different screen depths. (See Listing 11.)

The DeviceLoop procedure is described in "DeviceLoop Meets the Interface Designer," develop  Issue 13, and in Inside Macintosh  Volume VI.*

THE SAMPLE PROGRAM

The sample program illustrates how you can use twist-down lists to display a directory of all files on a volume. It's a very simple program and you would be well advised not to use it on a huge disk with many folders and files, because there's no protection against storage overflow.

The sample program compiles and runs in five environments: THINK C 6.0, Metrowerks DR1, and MPW 3.2 for the 680x0-based Macintosh; and, for the Power Macintosh, Metrowerks DR1 and the MPW provided in the Macintosh on RISC Software Developer's Kit. Converting the code for Power Macintosh took about one day (it was my lab exercise when I took the Apple Developer University "PowerPC BootCamp" course). To learn more about what I did to accomplish this conversion, see "Converting for Power Macintosh."

When you start up the sample program, it begins enumerating the disk; you can click to stop it at any time. The hierarchical list is built using the algorithm illustrated bythe MyBuildHierarchy function, described in the section "Putting Data Into the List."

Listing 11. DrawTriangle and DrawThisTriangle

typedef struct      TriangleInfo {   /* Passed to DrawThisTriangle */
    PolyHandle      polyHandle;      /* The polygon to draw        */
    Point           polyPoint;       /* Where to draw it         */
} TriangleInfo, *TriangleInfoPtr;

static void DrawTriangle(PolyHandle   polyHandle,
                         Point        polyPoint,
                         Boolean      isSelected
    )
{
    TriangleInfo        triangleInfo;
    RgnHandle           drawingRgn;
    long                savedA5;

    /*** Refresh A5 so that we can use the current QuickDraw       */
    /*** globals.                                                  */
    savedA5 = SetCurrentA5();
    triangleInfo.polyHandle = polyHandle;      /* Save our drawing */
    triangleInfo.polyPoint = polyPoint;        /*  parameters.     */
    /*** Position the polygon properly on the display.             */
    OffsetPoly(polyHandle, polyPoint.h, polyPoint.v);
    if (isSelected)
        FillPoly(polyHandle, &qd.black);
    else {
        /*** Get drawing region and call DeviceLoop to do the work */
        drawingRgn = NewRgn();
        OpenRgn();
        FramePoly(polyHandle);
        CloseRgn(drawingRgn);
        DeviceLoop(
            drawingRgn,              /* Region to draw into        */
            (DeviceLoopDrawingProcPtr) DrawThisTriangle,
            (long) &triangleInfo,       /* Drawing parameters  */
            0                        /* DeviceLoop flags (ignored) */
        );
        DisposeRgn(drawingRgn);
    }
    /*** Frame the button in black and move the polygon back to    */
    /*** its default [0,0] position.                               */
    FramePoly(polyHandle);
    OffsetPoly(polyHandle, -polyPoint.h, -polyPoint.v);
    SetA5(savedA5);
}
static pascal void DrawThisTriangle(      /* Called by DeviceLoop  */
        short            depth,           /* Screen pixel depth    */
        short            deviceFlags,     /* Device info (ignored) */
        GDHandle         targetDevice,    /* The display (ignored) */
        TriangleInfoPtr  triangleInfoPtr  /* The data to be drawn  */
    )
{
    RGBColor                    foreColor;
    RGBColor                    saveForeColor;
    RGBColor                    backColor;
    short                       i;
    Rect                        polyRect;

    polyRect = (**(*triangleInfoPtr).polyHandle).polyBBox;
    LocalToGlobal(& ((Point *) &polyRect)[0]);
    LocalToGlobal(& ((Point *) &polyRect)[1]);
    if (depth > 1) {
        /*** Drawing in color or grays: fill the triangle with a   */
        /*** very light gray.                                      */
        GetForeColor(&foreColor);
        saveForeColor = foreColor;
        GetBackColor(&backColor);
        /*** This loop sets foreColor to a very light gray.        */
        for (i = 0; i < 8; i++) {
            if (GetGray(GetGDevice(), &backColor,
                    &foreColor) == FALSE)
                break;
        }
        RGBForeColor(&foreColor);
        FillPoly((*triangleInfoPtr).polyHandle, &qd.black);
        RGBForeColor(&saveForeColor);
    }
    else {
        /*** Monochrome: erase the interior of the polygon.        */
        ErasePoly((*triangleInfoPtr).polyHandle);
    }
}

CONVERTING FOR POWER MACINTOSH

Here's a simplified checklist of the kinds of things you'll need to do to convert code for the Power Macintosh, based on what I did to convert my program (see the sample code on the CD, especially TwistDownList.c). I borrowed heavily from the document "Moving Your Source to PowerPC" on the Macintosh on RISC Software Developer's Kit CD.
  • Convert to standard C using the most restrictive environment so that the application runs correctly in THINK C and MPW with no compiler or linker errors or warnings. In THINK C, for example, you would enable the Check Pointer Types and Require Prototypes options and remove the MacHeaders option. In all cases, you should add prototypes and explicit function return types and fix potential trigraph problems.
  • Replace all instances of int and unsigned by explicit short and long declarations. Be very careful about structure definitions that are shared among code modules (or written to files or resources), as the Power Macintosh aligns structures differently from the 680x0-based Macintosh: you may need to add #pragma statements to override the compiler.
  • Create a makefile for the MPW development system. Try to build "fat binaries" that will run native on both the 680x0-based Macintosh and Power Macintosh.
  • If at all possible, convert to the universal interfaces provided in the Software Developer's Kit (and on this issue's CD). In particular, ProcPtr references must be converted to UniversalProcPtr. Also, all low-memory references must be replaced by the access functions provided as part of the universal interfaces.
  • Isolate system and compiler dependencies by using #ifdef statements. For MPW-based compilers, add -d MPW=1 to your makefiles. This lets you add compiler- and system-dependent #pragmas and code sequences without encountering compiler warnings.

#ifdef THINK_C /* THINK C */
#ifdef __powerc /* Power Macintosh */
#ifdef MPW /* MPW: see above */
#ifdef applec /* Apple compilers */
  • Remove or isolate all assembly language and inline statements. You can probably eliminate all assembly language from your Power Macintosh applications.
  • Power Macintosh will not support a number of obsolete system traps; when you convert your program, you may need to rewrite small sections of your code. Of course, you should check new code on both the 680x0-based Macintosh and Power Macintosh.
  • Applications must explicitly allocate space for the QuickDraw globals. Add the following to your application's main program file:

 #ifdef __powerc
QDGlobalsqd;
#endif
  • Avoid storing application-specific data in your application's data fork: the Power Macintosh stores its code there. (You can reserve a fixed amount of space at the beginning of the data fork, if necessary.)
  • Add a 'cfrg' (code fragment) resource to your application's resource fork. This tells the Process Manager that you've built a Power Macintosh native application.

 #ifdef __powerc
#include "CodeFragmentTypes.r"
resource 'cfrg' (0) {
    {
        kPowerPC,
        kFullLib,
        kNoVersionNum, kNoVersionNum,
        0, 0,
        kIsApp, kOnDiskFlat,
        kZeroOffset, kWholeFork,
        "MyFirstPowerPCApp"
    }
};
#endif
  • Make good use of the compatibility built into the Power Macintosh: your application should run native on Power Macintosh and still run correctly on a 680x0-based machine. The user shouldn't notice any difference.
  • In most cases, native Power Macintosh applications will be about the same size as their 680x0 counterparts. Of course, if you use the compatible "fat binary" capability, the application file size will increase.

The above list isn't complete by any means, but, together with the sample code, it should get you started. Also, there are several Developer University courses available to help bring you up to speed quickly.

TWISTED LISTERS

So, what's this method of displaying data good for? If you have data that's hierarchical, coherent, line-oriented, and not too large, you'll find that the twist-down list functions are both useful and easy to incorporate into your applications. * Hierarchical. If the data doesn't separate into a strict hierarchy, the presence of triangular buttons will only confuse your users: they're expecting your application to operate like the Finder. Also, if the hierarchy is very limited (a single topic with a block of text), you'll probably find some other solution easier to use.
  • Coherent. Your users expect consistency between the parent "title" and child "content" data. Try another technique if clicking a triangular button does something other than reveal a lower-level hierarchy. For example, the JMP statistical package uses standard Macintosh buttons to expand a hierarchy. Clicking a button may reveal a table of data or a graphical element. (JMP hierarchies are also quite shallow.)
  • Line-oriented. Again, the user is expecting Finder-like behavior. If the data is not line-oriented, you'll discover that coaxing the List Manager to deal with your data isn't worth the considerable effort it takes. In particular, avoid varying the height of each line as this makes the triangular buttons look weird (some small, some large) unless you normalize their size.
  • Not too large. This is a restriction of the List Manager. Because of the way it stores data, there's an absolute limit of 32,767 cells in a list, but it becomes very slow and clumsy with more than a few hundred cells.

I wrote the TwistDown library because I wanted to display an AOCE catalog specification that can contain many internal components of varying size and complexity. It offered a friendly interface into a structure that is convoluted, warped, and -- indeed -- twisted.

REFERENCES

  • "Standalone Code on PowerPC" by Tim Nichols, develop  Issue 17.
  • "Making the Leap to PowerPC" by Dave Radcliffe, develop   Issue 16.
  • Inside Macintosh: More Macintosh Toolbox  (Addison-Wesley, 1993), Chapter 4, "List Manager," or Inside Macintosh  Volume IV (Addison-Wesley, 1986), Chapter 30, "The List Manager Package."
  • "DeviceLoop Meets the Interface Designer" by John Powers, develop  Issue 13. DeviceLoop is also described in Inside Macintosh  Volume VI (Addison-Wesley, 1991) on page 21-23.


MARTIN MINOW (AppleLink MINOW, Internet minow@apple.com) is an aged, wrinkled hacker who, having determined that it's far too late to whine about his receding hairline, instead takes perverse delight in informing his young colleagues at excessive length how much better programming was when you had to punch out your programs, one machine word after another, on oily paper tape, back in the good old days when hex digits were KSNJFL, words were 40 bits long, and a supercomputer had 1024 of them. In real life, he works at Apple's Developer Support Center, drinks beer, and runs marathons.*

ACKNOWLEDGMENTS The TwistDown library is generally based on code from the NewsWatcher application by Steve Falkenburg. Steve's code is based on code written by John Norstad, author of Disinfectant. *

Thanks to our technical reviewers Jon Callas, Godfrey DiGiorgi, Steve Falkenburg, Dave Radcliffe, and Dean Yu. And thanks to Richard Clark for the PowerPC BootCamp course. *

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

LaunchBar 6.18.5 - Powerful file/URL/ema...
LaunchBar is an award-winning productivity utility that offers an amazingly intuitive and efficient way to search and access any kind of information stored on your computer or on the Web. It provides... Read more
Affinity Designer 2.3.0 - Vector graphic...
Affinity Designer is an incredibly accurate vector illustrator that feels fast and at home in the hands of creative professionals. It intuitively combines rock solid and crisp vector art with... Read more
Affinity Photo 2.3.0 - Digital editing f...
Affinity Photo - redefines the boundaries for professional photo editing software for the Mac. With a meticulous focus on workflow it offers sophisticated tools for enhancing, editing and retouching... Read more
WhatsApp 23.24.78 - Desktop client for W...
WhatsApp is the desktop client for WhatsApp Messenger, a cross-platform mobile messaging app which allows you to exchange messages without having to pay for SMS. WhatsApp Messenger is available for... Read more
Adobe Photoshop 25.2 - Professional imag...
You can download Adobe Photoshop as a part of Creative Cloud for only $54.99/month Adobe Photoshop is a recognized classic of photo-enhancing software. It offers a broad spectrum of tools that can... Read more
PDFKey Pro 4.5.1 - Edit and print passwo...
PDFKey Pro can unlock PDF documents protected for printing and copying when you've forgotten your password. It can now also protect your PDF files with a password to prevent unauthorized access and/... Read more
Skype 8.109.0.209 - Voice-over-internet...
Skype is a telecommunications app that provides HD video calls, instant messaging, calling to any phone number or landline, and Skype for Business for productive cooperation on the projects. This... Read more
OnyX 4.5.3 - Maintenance and optimizatio...
OnyX is a multifunction utility that you can use to verify the startup disk and the structure of its system files, to run miscellaneous maintenance and cleaning tasks, to configure parameters in the... Read more
CrossOver 23.7.0 - Run Windows apps on y...
CrossOver can get your Windows productivity applications and PC games up and running on your Mac quickly and easily. CrossOver runs the Windows software that you need on Mac at home, in the office,... Read more
Tower 10.2.1 - Version control with Git...
Tower is a Git client for OS X that makes using Git easy and more efficient. Users benefit from its elegant and comprehensive interface and a feature set that lets them enjoy the full power of Git.... Read more

Latest Forum Discussions

See All

Pour One Out for Black Friday – The Touc...
After taking Thanksgiving week off we’re back with another action-packed episode of The TouchArcade Show! Well, maybe not quite action-packed, but certainly discussion-packed! The topics might sound familiar to you: The new Steam Deck OLED, the... | Read more »
TouchArcade Game of the Week: ‘Hitman: B...
Nowadays, with where I’m at in my life with a family and plenty of responsibilities outside of gaming, I kind of appreciate the smaller-scale mobile games a bit more since more of my “serious" gaming is now done on a Steam Deck or Nintendo Switch.... | Read more »
SwitchArcade Round-Up: ‘Batman: Arkham T...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for December 1st, 2023. We’ve got a lot of big games hitting today, new DLC For Samba de Amigo, and this is probably going to be the last day this year with so many heavy hitters. I... | Read more »
Steam Deck Weekly: Tales of Arise Beyond...
Last week, there was a ton of Steam Deck coverage over here focused on the Steam Deck OLED. | Read more »
World of Tanks Blitz adds celebrity amba...
Wargaming is celebrating the season within World of Tanks Blitz with a new celebrity ambassador joining this year's Holiday Ops. In particular, British footballer and movie star Vinnie Jones will be brightening up the game with plenty of themed in-... | Read more »
KartRider Drift secures collaboration wi...
Nexon and Nitro Studios have kicked off the fifth Season of their platform racer, KartRider Dift, in quite a big way. As well as a bevvy of new tracks to take your skills to, and the new racing pass with its rewards, KartRider has also teamed up... | Read more »
‘SaGa Emerald Beyond’ From Square Enix G...
One of my most-anticipated releases of 2024 is Square Enix’s brand-new SaGa game which was announced during a Nintendo Direct. SaGa Emerald Beyond will launch next year for iOS, Android, Switch, Steam, PS5, and PS4 featuring 17 worlds that can be... | Read more »
Apple Arcade Weekly Round-Up: Updates fo...
This week, there is no new release for Apple Arcade, but many notable games have gotten updates ahead of next week’s holiday set of games. If you haven’t followed it, we are getting a brand-new 3D Sonic game exclusive to Apple Arcade on December... | Read more »
New ‘Honkai Star Rail’ Version 1.5 Phase...
The major Honkai Star Rail’s 1.5 update “The Crepuscule Zone" recently released on all platforms bringing in the Fyxestroll Garden new location in the Xianzhou Luofu which features many paranormal cases, players forming a ghost-hunting squad,... | Read more »
SwitchArcade Round-Up: ‘Arcadian Atlas’,...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for November 30th, 2023. It’s Thursday, and unlike last Thursday this is a regular-sized big-pants release day. If you like video games, and I have to believe you do, you’ll want to... | Read more »

Price Scanner via MacPrices.net

Deal Alert! Apple Smart Folio Keyboard for iP...
Apple iPad Smart Keyboard Folio prices are on Holiday sale for only $79 at Amazon, or 50% off MSRP: – iPad Smart Folio Keyboard for iPad (7th-9th gen)/iPad Air (3rd gen): $79 $79 (50%) off MSRP This... Read more
Apple Watch Series 9 models are now on Holida...
Walmart has Apple Watch Series 9 models now on Holiday sale for $70 off MSRP on their online store. Sale prices available for online orders only, in-store prices may vary. Order online, and choose... Read more
Holiday sale this weekend at Xfinity Mobile:...
Switch to Xfinity Mobile (Mobile Virtual Network Operator..using Verizon’s network) and save $500 instantly on any iPhone 15, 14, or 13 and up to $800 off with eligible trade-in. The total is applied... Read more
13-inch M2 MacBook Airs with 512GB of storage...
Best Buy has the 13″ M2 MacBook Air with 512GB of storage on Holiday sale this weekend for $220 off MSRP on their online store. Sale price is $1179. Price valid for online orders only, in-store price... Read more
B&H Photo has Apple’s 14-inch M3/M3 Pro/M...
B&H Photo has new Gray and Black 14″ M3, M3 Pro, and M3 Max MacBook Pros on Holiday sale this weekend for $100-$200 off MSRP, starting at only $1499. B&H offers free 1-2 day delivery to most... Read more
15-inch M2 MacBook Airs are $200 off MSRP on...
Best Buy has Apple 15″ MacBook Airs with M2 CPUs in stock and on Holiday sale for $200 off MSRP on their online store. Their prices are among the lowest currently available for new 15″ M2 MacBook... Read more
Get a 9th-generation Apple iPad for only $249...
Walmart has Apple’s 9th generation 10.2″ iPads on sale for $80 off MSRP on their online store as part of their Cyber Week Holiday sale, only $249. Their prices are the lowest new prices available for... Read more
Space Gray Apple AirPods Max headphones are o...
Amazon has Apple AirPods Max headphones in stock and on Holiday sale for $100 off MSRP. The sale price is valid for Space Gray at the time of this post. Shipping is free: – AirPods Max (Space Gray... Read more
Apple AirTags 4-Pack back on Holiday sale for...
Amazon has Apple AirTags 4 Pack back on Holiday sale for $79.99 including free shipping. That’s 19% ($20) off Apple’s MSRP. Their price is the lowest available for 4 Pack AirTags from any of the... Read more
New Holiday promo at Verizon: Buy one set of...
Looking for more than one set of Apple AirPods this Holiday shopping season? Verizon has a great deal for you. From today through December 31st, buy one set of AirPods on Verizon’s online store, and... Read more

Jobs Board

Senior Software Engineer - *Apple* Fundamen...
…center of Microsoft's efforts to empower our users to do more. The Apple Fundamentals team focused on defining and improving the end-to-end developer experience in Read more
Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Housekeeper, *Apple* Valley Villa - Cassia...
Apple Valley Villa, part of a senior living community, is hiring entry-level Full-Time Housekeepers to join our team! We will train you for this position and offer a Read more
Senior Manager, Product Management - *Apple*...
…Responsibilities** We are seeking an ambitious, data-driven thinker to assist the Apple Product Development team as our Wireless Product division continues to grow Read more
Mobile Platform Engineer ( *Apple* /AirWatch)...
…systems, installing and maintaining certificates, navigating multiple network segments and Apple /IOS devices, Mobile Device Management systems such as AirWatch, and Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.