DEVELOPER WORKSHOP:  Please Do Not Press this Button Again
by Christopher Tate (ctate@be.com)

One question we field regularly in DTS is how to detect and handle multiple mouse clicks. On the face of it, it's not entirely clear what a poor application is supposed to do; after all, the BView class's MouseDown() method doesn't supply any information other than the location of the click.  That alone isn't enough to distinguish two single-clicks from a canonical "double-click"; one needs the timing information as well. Or does one?

The Interface Kit is, indeed, keeping an eye on its internal stopwatch when it reports mouse events to your application.  The user-configurable timing threshold is compared against the time between mouse-down events; as the user accumulates multiple clicks within that threshold, a click count is incremented. That count is passed to your application along with each mouse-down BMessage, in a field called (naturally enough) "clicks."

All this begs the question of how to access the information. Mouse-down events are handled behind the application's back, so to speak; the MouseDown() method is called invisibly from within the BWindow class's message dispatching logic. But there is a solution! BWindows are, like all BeOS message recipients, descendants of the BLooper class, and that class provides a way for its methods to inspect the BMessage currently being dispatched. The way a BView can determine the cumulative click count associated with a MouseDown() call is to look at the current message being handled by the view's looper -- i.e., its window.

You can see an example of this in today's sample application, called DoubleClick, available at this URL:

    ftp://ftp.be.com/pub/samples/intro/DoubleClick.zip

The DoubleClick application displays a window that indicates how many consecutive clicks it receives. That determination is easy under the BeOS, and doesn't require any calculations or state maintenance within the application's code. In the DoubleClickView's MouseDown() method, the following two statements occur:

    BMessage* msg = Window()->CurrentMessage();
    int32 clicks = msg->FindInt32("clicks");

That's all! The application can now choose an action to take based on the click count. This implies that a multiple click consists of several distinct MouseDown() events, the first of which is indistinguishable from an ordinary single- click action. This is deliberate: multiple click actions must always build on their fewer-click predecessors, not replace them. A good example is the traditional word processor click sequence. The first click positions the insertion point, the second highlights the word around that point, the third highlights the entire line, and the fourth selects the entire paragraph. Each stage is an extension of the previous one; this is good design.

There is other information in the mouse-down BMessage, such as the identity of the button that was pressed and the click location in the view's and the main screen's coordinate systems. This raises issues that complicate the subject of multiple-click detection. First, does it make sense to consider two clicks that are "far apart" on the screen to constitute a "double-click," presumably with modified behavior? Second, does it make sense to treat a rapid sequence of clicks by different mouse buttons as a "multiple click?"

DoubleClick doesn't address the first issue, but it does handle the second correctly. Briefly, the Official BeOS User Interface Decree is that a "double-click" means two clicks of the same mouse button, uninterrupted by other mouse buttons. It turns out that supporting this definition involves some extra bookkeeping on the application's part, because the mouse-down messages' "clicks" fields continue to increment even when the user presses several different buttons in quick succession. A fast primary-secondary-primary mouse button sequence winds up with the third mouse-down message indicating a "clicks" value of 3. This is patently wrong; each of these clicks should be treated as a distinct single click. A tangled situation, no?

The way DoubleClick unravels this snarl is to maintain two extra pieces of information that describe the ongoing mouse-click sequence: the identity of the last button pressed and its own click count specifically for that button. Whenever a new button is pressed the ongoing count is reset to one. Similarly, if the "clicks" field in the mouse-down BMessage is ever equal to 1, that means the multiple-click timeout has expired and click counting should be reset. The code that implements this logic looks like this:

    BMessage* msg = Window()->CurrentMessage();
    int32 clicks = msg->FindInt32("clicks");
    int32 button = msg->FindInt32("buttons");

    // is this a continuing click sequence?
    if ((button == mLastButton) && (clicks > 1))
    {
        mClickCount++;
    }
    else mClickCount = 1;
    mLastButton = button;

At this point, "mClickCount" is the correct number of clicks of the relevant mouse button. The application should use that value in deciding the appropriate action.