Issue 3-46, November 18, 1998

Be Engineering Insights: The Kitchen Sink

By Robert Polic

What started as simple tutorial on how to do context-sensitive menus and drag-n-drop in a list view has turned into a light- weight application launcher. The full source (all 520 lines including headers and comments) for "EZ Launcher" can be found on our ftp site...

ftp://ftp.be.com/pub/samples/application_kit/EZLauncher.zip

EZ Launcher is a simple BApplication that constructs a single window with a scrolling list view containing icons and labels for all applications in the /boot/apps folder. Users can launch an application either by double-clicking the item, right-clicking the mouse to access a context-sensitive menu, or dragging and dropping back onto the window. All operations are done asynchronously to limit the amount of time the window is locked (and therefor unresponsive). Overkill for this app? You bet, but with the BeOS, it's almost as simple to spawn a thread to handle user actions as not to.

So I'll assume everyone here is familiar with constructing an application and window and will skip over that part and get to the meat, which in this case is the list. In this app I'll use a scrolling BListView to both maintain my list items and allow the user to select them...

TEZLauncherWindow::TEZLauncherWindow(BRect frame)
    :BWindow(frame, "EZ Launcher", B_TITLED_WINDOW,
        B_NOT_ZOOMABLE | B_WILL_ACCEPT_FIRST_CLICK)
{
    // set up a rectangle and instantiate a new view
    BRect       aRect(Bounds());
    BScrollView *aScroller;


    // reduce by size of vertical scroll bar
    aRect.right -= B_V_SCROLL_BAR_WIDTH;
    // construct a BListView
    fList = new TEZLauncherView(aRect);
    // construct a scroll view containing the list view
    //and add it to the window
    AddChild(aScroller = new BScrollView("", fList,
        B_FOLLOW_ALL, B_WILL_DRAW, true,
        true, B_PLAIN_BORDER));
    BuildList();
}

BuildList() is the method that actually adds all the items from the /boot/apps directory to the list...

void TEZLauncherWindow::BuildList()
{
    BDirectory  dir;
    BEntry  entry;
    BPath       path;


    // walk through the apps directory adding
    //all apps to the list
    find_directory(B_APPS_DIRECTORY, &path, true);
    dir.SetTo(path.Path());
    // loop until we get them all
    while (dir.GetNextEntry(&entry, true) ==
            B_NO_ERROR)
    {
        if (entry.IsFile())
            // construct a new BListItem
            fList->AddItem(new TListItem(&entry));
    }
}

TListItem is derived from from BListItem and takes a single parameter, an entry_ref. Through this entry_ref, TListItem will extract and cache the application name and icon...

TListItem::TListItem(BEntry *entry)
    :BListItem()
{
    BNode   node;
    BNodeInfo   node_info;


    // try to get node info for this entry
    if ((node.SetTo(entry) == B_NO_ERROR) &&
        (node_info.SetTo(&node) == B_NO_ERROR)) {
        // cache name
        entry->GetName(fName);
        // create bitmap large enough for icon
        fIcon = new BBitmap(
            BRect(0, 0, B_LARGE_ICON - 1,
                    B_LARGE_ICON - 1), B_COLOR_8_BIT);
        // cache the icon
        node_info.GetIcon(fIcon);
        // adjust size of item to fit icon
        SetHeight(fIcon->Bounds().Height() +
                    kITEM_MARGIN);
        // cache ref
        entry->GetRef(&fref);
    }
    else {
        fIcon = NULL;
        strcpy(fName, "<Lost File>");
        SetHeight(kDEFAULT_ITEM_HEIGHT);
    }
}

TListItem is also responsible for drawing the item in the BListView's view...

void TListItem::DrawItem(BView *view, BRect rect,
                bool /* complete */)
{
    float       offset = 10;
    BFont       font = be_plain_font;
    font_height finfo;


    // set background color
    if (IsSelected()) {
        // fill color
        view->SetHighColor(kSELECTED_ITEM_COLOR);
        // anti-alias color
        view->SetLowColor(kSELECTED_ITEM_COLOR);
    }
    else {
        view->SetHighColor(kLIST_COLOR);
        view->SetLowColor(kLIST_COLOR);
    }
    // fill item's rect
    view->FillRect(rect);


    // if we have an icon, draw it
    if (fIcon) {
        view->SetDrawingMode(B_OP_OVER);
        view->DrawBitmap(fIcon,
            BPoint(rect.left + 2, rect.top + 3));
        view->SetDrawingMode(B_OP_COPY);
        offset = fIcon->Bounds().Width() + 10;
    }


    // set text color
    (IsEnabled()) ?  view->SetHighColor(kTEXT_COLOR) :
            view->SetHighColor(kDISABLED_TEXT_COLOR);


    // set up font
    font.SetSize(12);
    font.GetHeight(&finfo);
    view->SetFont(&font);


    // position pen
    view->MovePenTo(offset,
        rect.top + ((rect.Height() - (finfo.ascent +
        finfo.descent + finfo.leading)) / 2) +
        (finfo.ascent + finfo.descent) - 2);
    // and draw label
    view->DrawString(fName);
}

All mouse actions are directed to our ListView and from here we decide whether to display a context-sensitive menu, spawn a task to see if we need to initiate a drag, or do nothing and let the base class handle it...

void TEZLauncherView::MouseDown(BPoint where)
{
    uint32  buttons;


    // retrieve the button state from the
    // MouseDown message
    if (Window()->CurrentMessage()->FindInt32(
         "buttons", (int32 *)&buttons) == B_NO_ERROR)
    {
        // find item at the mouse location
        int32 item = IndexOf(where);
        // make sure item is valid
        if ((item >= 0) && (item < CountItems()))
        {
            // if clicked with second mouse button,
            // let's do a context-sensitive menu
            if (buttons & B_SECONDARY_MOUSE_BUTTON) {
                BPoint  point = where;
                ConvertToScreen(&point);
                // select this item
                Select(item);
                // do an async-popupmenu
                fMenu->Go(point, true, false, true);
                return;
            }
            // clicked with primary button
            else
            {
                int32 clicks;
                // see how many times we've
                //been clicked


                Window()->CurrentMessage()->
                        FindInt32("clicks", &clicks);
                // if we've only been clicked once
                // on this item, see if user
                // intends to drag
                if ((clicks == 1) ||
                    (item!=CurrentSelection()))
                {
                    // select this item
                    Select(item);


                    // create a structure of
                    // useful data
                    list_tracking_data *data =
                        new list_tracking_data();
                    data->start = where;
                    data->view = this;


                    // spawn a thread that watches
                    // the mouse to see if a drag
                    // should occur.  this will free
                    // up the window for more
                    // important tasks


                    resume_thread(spawn_thread(
                        (status_t (*)(void *)) TrackItem,
                        "list tracking",
                        B_DISPLAY_PRIORITY, data));
                    return;
                }
            }
        }
    }
    // either the user dbl-clicked an item or
    //clicked in an area with no
    // items.  either way, let BListView take care of it
    BListView::MouseDown(where);
}

If we've determined that mouse down was a single-click on the item, we'll spawn a thread that monitors the mouse position and if the mouse moves more than kDRAG_SLOP in any direction, we'll initiate a DragMessage...

status_t TEZLauncherView::TrackItem(
                    list_tracking_data *data)
{
    uint32  buttons;
    BPoint  point;


    // we're going to loop as long as the mouse
    //is down and hasn't moved
    // more than kDRAG SLOP pixels
    while (1) {
        // make sure window is still valid
        if (data->view->Window()->Lock()) {
            data->view->GetMouse(&point, &buttons);
            data->view->Window()->Unlock();
        }
        // not?  then why bother tracking
        else
            break;
        // button up?  then don't do anything
        if (!buttons)
            break;
        // check to see if mouse has moved more
        // than kDRAG SLOP pixels in any direction
        if ((abs((int)(data->start.x - point.x))
                > kDRAG_SLOP) ||
        (abs((int)(data->start.y - point.y))
                > kDRAG_SLOP))
        {
            // make sure window is still valid
            if (data->view->Window()->Lock()) {
                BBitmap  *drag_bits;
                BBitmap  *src_bits;
                BMessage drag_msg(eItemDragged);
                BView    *offscreen_view;
                int32    index =
                    data->view->CurrentSelection();
                TListItem *item;


                // get the selected item
                item = dynamic_cast<TListItem *>
                    (data->view->ItemAt(index));
                if (item) {
                    // init drag message with
                    //some useful information
                    drag_msg.AddInt32("index",index);
                    // we can even include the item
                    drag_msg.AddRef("entry ref",
                                    item->Ref());


                    // get bitmap from current item
                    src_bits = item->Bitmap();
                    // make sure bitmap is valid
                    if (src_bits)
                    {
                        // create a new bitmap based on
                        // the one in the list (we
                        // can't just use the bitmap we
                        // get passed because the
                        // app server owns it after we
                        // call DragMessage, besides
                        // we wan't to create that cool
                        // semi-transparent look)
                        drag_bits = new BBitmap(
                            src_bits->Bounds(),
                            B_RGBA32, true);
                        // we need a view
                        // so we can draw
                        offscreen_view =
                            new BView(
                              drag_bits->Bounds(), "",
                              B_FOLLOW_NONE, 0);
                        drag_bits->
                            AddChild(offscreen_view);


                        // lock it so we can draw
                        drag_bits->Lock();
                        // fill bitmap with black
                        offscreen_view->
                            SetHighColor(0, 0, 0, 0);
                        offscreen_view->FillRect(
                            offscreen_view->Bounds());
                        // set the alpha level
                        offscreen_view->
                            SetDrawingMode(B_OP_ALPHA);


                        offscreen_view->
                            SetHighColor(0, 0, 0, 128);


                        offscreen_view->
                            SetBlendingMode(
                                B_CONSTANT_ALPHA,
                                B_ALPHA_COMPOSITE);
                        // blend in bitmap
                        offscreen_view->
                            DrawBitmap(src_bits);
                        drag_bits->Unlock();


                        // initiate drag from center
                        // of bitmap


                        data->view->DragMessage(
                          &drag_msg, drag_bits,
                          B_OP_ALPHA,
                          BPoint(
                          drag_bits->Bounds().Height()/2,
                          drag_bits->Bounds().Width()/2 ));
                    } // endif src bits
                    else
                    {
                        // no src bitmap?
                        // then just drag a rect
                        data->view->DragMessage(&drag_msg,
                            BRect(0, 0, B_LARGE_ICON - 1,
                              B_LARGE_ICON - 1));
                    }
                } // endif item
                data->view->Window()->Unlock();
            } // endif window lock
            break;
        } // endif drag start
        // take a breather
        snooze(10000);
    } // while button
    // free resource
    free(data);
    return B_NO_ERROR;
}

The only thing left is to wait for a launch message to arrive at the window...

void TEZLauncherWindow::MessageReceived(BMessage *msg)
{
    char        string[512];
    int32       index;
    entry_ref   entry;
    entry_ref   *ref = NULL;
    status_t    result;
    TListItem   *item;


    switch (msg->what) {
        case eItemDblClicked:
            // item was dbl-clicked.
            //from the message we can find the item
            msg->FindInt32("index", &index);
            item = dynamic_cast<TListItem *>
                (fList->ItemAt(index));
            if (item)
                ref = item->Ref();
            break;


        case eItemMenuSelected:
            // item was selected with menu.
            //find item using CurrentSelection
            index = fList->CurrentSelection();
            item = dynamic_cast<TListItem *>
                (fList->ItemAt(index));
            if (item)
                ref = item->Ref();
            break;


        case eItemDragged:
            // item was dropped on us.
            //get ref from message
            if (msg->HasRef("entry ref")) {
                msg->FindRef("entry ref", &entry);
                ref = &entry;
            }
            break;


        default:
            BWindow::MessageReceived(msg);
    }
    if (ref) {
        // if we got a ref, try launching it
        result = be_roster->Launch(ref);
        if (result != B_NO_ERROR) {
            sprintf(string,
            "Error launching: %s", strerror(result));
            (new BAlert("", string, "OK"))->Go();
        }
    }
}

Developers' Workshop: The Bitchin' Async

By Owen Smith

Recent events, such as, say, the intense effort to get R4 out the door, have inspired me to keep this article short and sweet.

I'll be addressing R4's asynchronous control capabilities in this article. These have already been covered in the R4 Beta release notes (with a few not-quite-correct points that will be cleared up here), and The Animal's most nifty summary article:

Be Engineering Insights: That BeOS is one baaad mother-[Shut your mouth!] ...just talking 'bout BeOS

My simple contribution here is to add some sample code so that you can see these controls in action.

Enter Pot, the kitchen utensil for this week: ftp://ftp.be.com/pub/samples/interface_kit/pot.zip

Actually, Pot doesn't refer to a kitchen utensil, nor to a beefy roast, nor even to that medicinal restorative which entertains countless carefree souls, but rather to a simple BControl-derived class that implements a rotating dial. I've also thrown in, absolutely free of charge, a test application which shows this control in action (and demonstrates the new B_OP_ALPHA mode on the side).

Whither Async?

New programmers and/or programmers coming from MFC or other async-friendly APIs probably won't need much motivation to start taking advantage of asynchronous controls. For those coming from the BeOS R3 world of controls, though, some justification may be in order.

Here's how your control might have handled mouse movements in the past:

void ArthriticCtrl::MouseDown(BPoint where)
{
    // handle mouse down


    BPoint prev = where;
    uint32 buttons;
    do {
        snooze(40000);
        GetMouse(&where, &buttons);
        if (buttons && (where != prev)) {
            // handle mouse moved
        }
    } while (buttons);


    // handle mouse up
}

There are two big wins you can get by moving to asynchronous controls:

  1. Simplicity. In the previous case you have to write a mouse processing loop and call a lower-level mouse handling function to figure out when the mouse is moved and released. With asynchronous controls, almost all of this work is done for you. All you have to do is write the code to handle the mouse moved and mouse button release.

  2. Performance. The code listed above is inefficient because it forces the looper to sleep while it's not handling mouse functions. (Note that simply removing the snooze doesn't alleviate this situation at all, and degrades system performance!) With asynchronous controls, your looper is free to handle other messages while waiting for mouse input, which allows your control to remain responsive to other events.

Implementing Asynchronous Controls

Here are four simple steps to asynchronous nirvana if you're deriving from BControl:

  1. Inside MouseDown(), when you want to track the mouse movement, you need to tell the app server that you want to receive all the generated mouse events while the mouse is moving—including movements outside of your view! Usually you'll want to use SetMouseEventMask(), because the tracking is automatically ended for you when the mouse button is released.

  2. Mouse movements are usually sent to you whenever the mouse is over the view—not necessarily when you're tracking the mouse. So, you also need to keep track inside your class of when you're actively tracking mouse movement. BControl provides two functions, SetTracking() and IsTracking(), that do this for you. Call SetTracking(true) from within MouseDown(), and you'll be on your way.

  3. Override MouseMoved(), and if you're currently tracking the mouse (IsTracking() == true), do whatever you need to do when the mouse moves.

  4. Override MouseUp, and if you're currently tracking the mouse (IsTracking() == true), call SetTracking(false) after you've finished handling the mouse up event, to mark that you've finished tracking the mouse.

BPot, my twiddle-happy control example, shows how this is done.

Using Interface Kit Controls

BButton, BCheckBox, and all the other Interface Kit classes that derive from BControl, now lead a two-faced existence. For compatibility's sake, they track mouse movement the old way (using the mouse processing loop in MouseDown()) by default. However, they can be told to use the asynchronous method instead. You tell these controls to use the new implementation by passing the flag B_ASYNCHRONOUS_CONTROLS to their parent window. Because of the performance gain that the asynchronous method offers, you'll probably want to enable asynchronous controls in your windows, unless you're doing something special with the controls that depends on their previous mouse handling behavior.

If you're deriving from any of these classes, you can of course completely replace their mouse handling behavior, or leave their mouse handling code alone. However, if you want to augment their existing behavior, you need to be careful that you're working with them correctly:

  • Make sure B_ASYNCHRONOUS_CONTROLS is set in the parent window if you want them to handle things asynchronously—otherwise, you'll be in for a rude shock when the implementation's MouseDown() is called!

  • If you call the inherited MouseDown(), ALWAYS call the inherited MouseMoved() and MouseUp() as well if you override these functions. Many of the Interface Kit controls set up a special state in MouseDown() that needs to be modified or cleaned up when the mouse is moved or released.

  • If you call the inherited MouseDown(), you needn't take the liberty of calling SetMouseEventMask() and SetTracking() in the code—these calls will be taken care of in the inherited function, so long as you've set B_ASYNCHRONOUS_CONTROLS correctly in the window.

Tweaking Asynchronous Behavior

Last week's article covered these, but to recap, there are several ways you can tweak the asynchronous behavior to Do The Right Thing™, depending on what your needs are:

  • You have a burning desire to capture not only MouseMoved() and MouseUp() events which occur outside of your view, but also MouseDown() events. Or, let's say you want to receive mouse events which occur outside of your view all the time. SetMouseEventMask() just won't cut it here, because it gets turned off when the mouse is released, at which point you'd need to concoct some way to turn it back on. Rather, use the more powerful SetEventMask(), which is called in exactly the same way as SetMouseEventMask(), and does essentially the same thing, but stays in effect until you explicitly turn it off (using the call SetEventMask(0)).

  • You'd really rather that Focus Follows Mouse doesn't steal the glory from your window when one of the child views is trying to track the mouse. Pass the flag B_LOCK_WINDOW_FOCUS as an option to SetMouseEventMask(), and you'll ensure that the focus doesn't change while you're tracking the mouse. (This option doesn't have any effect when it's passed to SetEventMask(); only SetMouseEventMask() supports it.)

  • You notice that MouseUp() events don't get handled until several seconds after you release the mouse. The problem here may be that your MouseMoved() implementation is taking too long—I can receive upwards of 90 mouse events per second on my machine, and if each MouseMoved() call takes .1 s to complete, the message queue deficit builds up awfully quickly. One way to keep the queue clean, and keep your application responsive, is to discard intermediate MouseMoved() events while you're busy tracking the mouse. The B_NO_POINTER_HISTORY option in SetMouseEventMask() takes care of this for you, so that your queue only has one pending MouseMoved() event at a time.

    This problem is a symptom of a larger problem that afflicts many Be applications: the more time you spend in message handling functions, the less responsive your looper becomes. In this case, discarding MouseMoved() events may still have the undesirable effect of skipping user input, and suspending other looper activities, while an event is being processed. An even better solution, when it's feasible, is to reduce the time the looper spends processing the messages, by accumulating results before performing expensive operations on them, or passing off expensive calculations to helper threads. Doing this frees up your looper to respond to user events, and improves the visible performance of your application.

  • You want to receive keyboard events, or want to keep the focus views from receiving keyboard events. Add B_KEYBOARD_EVENTS to the mask, or add B_SUSPEND_VIEW_FOCUS to the options you pass to SetMouseEventMask(). (B_SUSPEND_VIEW_FOCUS also doesn't have an effect on SetEventMask(); only SetMouseEventMask() supports it.)

Using Asynchronous Mouse Handling in Doodle

Of course, you can use this shiny new mouse handling behavior in other places than controls. In fact, any BView-derived class can take advantage of the event mask. You'll probably need to conjure up some equivalent to BControl's SetTracking() and IsTracking(), though; a simple bool in your class ought to take care of this.

In trying to keep with the times, I've altered Doodle yet again so that the document view can take advantage of asynchronous mouse handling (which really is a lot closer to the way that the MFC library in Windows does things). The new version of Doodle's source code can be found among the optional items on the up-and-coming R4 CD-ROM, in:

/boot/optional/sample-code/doodle/

One big difference between the BeOS approach and the Windows approach here is the number of simultaneous objects that can "capture the mouse" (i.e., receive all mouse events). In Windows, only one view at a time captures the mouse, and instead of something like IsTracking(), you craft your code in OnMouseMove to check to see whether the current view with the capture is your view. In the BeOS, any number of views can capture the mouse simultaneously, so each view needs to keep track separately of whether they are currently capturing the mouse. Also notice that the event mask in BeOS is more flexible, allowing both mouse and keyboard events to be captured, and giving you ways to tweak the behavior that I've described above.

That wraps it up for this week. Back to my post-release hibernation...


More Notes From the Road: Comdex

By Jean-Louis Gassée

Last week, I wrote from Tokyo, where we had a great time with our partners, Hitachi and Plat'Home. This week, although the neon resembles Tokyo by night—down to the creative syntax—the streets are broader and the cabbies more aggressive, so this must be Vegas.

I'll skip the fashionable complaints about Las Vegas and Comdex. For me, Las Vegas looks like (I'll tone down the metaphor) an experienced diner waitress, fast on her feet, professional, and wise to the ways of human behavior. It's the perfect setting for the excesses of our own profession. I like those excesses; they're the mark of a prosperous and still young industry. Would we prefer a convention of steam engine makers? Put another way, there's no good culture without a dash of bad taste; a monopoly of good taste suggests restraint—you're not pushing the envelope. In this regard, Vegas and Comdex are very reassuring.

For the BeOS Release 4 coming out party, we have our own booth. Last year, we were in the main hall, guests of our Umax friends. This year, as true Comdex beginners, we're in the basement of the Sands, with a large number of small, aspiring companies. This beginner's status turns out to be a pleasant one. Between people who wanted to see us, and people who didn't expect to, we enjoy pretty good traffic. Also, we can converse and give demonstrations without the deafening noise in the main hall. There, like diners in a noisy restaurant who must talk loudly because of the din from other tables, exhibitors turn up the volume to be heard over their neighbors' loud song and dance acts. While we hope to have that problem in the future, for now we enjoy not having to shout at the top of our lungs.

We approached the first public demonstrations of R4 with the usual level of trepidation. "Undocumented features" have a way of eluding testing, only to manifest themselves at embarrassing moments, in the midst of a trade show demo, preferably with media or OEM dignitaries in the audience for added impact. The first day of this particular phase of public testing went well, no claims expressed or implied, and the demonstrations from our friends at Ro Design, Beatware, MGI, Mediapede, Gobe, Maxon, and Hitachi went smoothly.

Tomorrow, we'll hold a press conference, an opportunity to re-state our basic messages: the Media OS, Release 4 breaking the laws of system software physics (more features and faster), new applications, the specialized Media OS coexisting peacefully with the general-purpose Windows. On the last point, cheeky visitors needled me a little about the latest bit of BeOS PR, from Microsoft this time, at their shareholders meeting last week. It appears Bill Gates mentioned Linux and the BeOS as competitive concerns. To borrow a famous line, I'm shocked. But seriously, I hope this isn't a bedtime story for the DOJ, especially when Apple is nowhere mentioned.

Looking at our respective financial weights, according to the terms of our last round of financing, Be is worth between 1/2000th and 1/3000th of Microsoft market capitalization. So, on the sunny side of Bill's statement, he agrees with our investors, he sees potential. In any event, we appreciate the publicity, we need it. Or, as we say in California, we feel validated.

Creative Commons License
Legal Notice
This work is licensed under a Creative Commons Attribution-Non commercial-No Derivative Works 3.0 License.