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 viewBRect
aRect
(Bounds
());BScrollView
*aScroller
; // reduce by size of vertical scroll baraRect
.right
-=B_V_SCROLL_BAR_WIDTH
; // construct a BListViewfList
= newTEZLauncherView
(aRect
); // construct a scroll view containing the list view //and add it to the windowAddChild
(aScroller
= newBScrollView
("",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...
voidTEZLauncherWindow
::BuildList
() {BDirectory
dir
;BEntry
entry
;BPath
path
; // walk through the apps directory adding //all apps to the listfind_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 BListItemfList
->AddItem
(newTListItem
(&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 nameentry
->GetName
(fName
); // create bitmap large enough for iconfIcon
= newBBitmap
(BRect
(0, 0,B_LARGE_ICON
- 1,B_LARGE_ICON
- 1),B_COLOR_8_BIT
); // cache the iconnode_info
.GetIcon
(fIcon
); // adjust size of item to fit iconSetHeight
(fIcon
->Bounds
().Height
() +kITEM_MARGIN
); // cache refentry
->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...
voidTListItem
::DrawItem
(BView
*view
,BRect
rect
, bool /* complete */) { floatoffset
= 10;BFont
font
=be_plain_font
; font_heightfinfo
; // set background color if (IsSelected
()) { // fill colorview
->SetHighColor
(kSELECTED_ITEM_COLOR
); // anti-alias colorview
->SetLowColor
(kSELECTED_ITEM_COLOR
); } else {view
->SetHighColor
(kLIST_COLOR
);view
->SetLowColor
(kLIST_COLOR
); } // fill item's rectview
->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 fontfont
.SetSize
(12);font
.GetHeight
(&finfo
);view
->SetFont
(&font
); // position penview
->MovePenTo
(offset
,rect
.top
+ ((rect
.Height
() - (finfo
.ascent
+finfo
.descent
+finfo
.leading
)) / 2) + (finfo
.ascent
+finfo
.descent
) - 2); // and draw labelview
->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...
voidTEZLauncherView
::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 int32item
=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 itemSelect
(item
); // do an async-popupmenufMenu
->Go
(point
,true
,false
,true
); return; } // clicked with primary button else { int32clicks
; // see how many times we've //been clickedWindow
()->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 itemSelect
(item
); // create a structure of // useful datalist_tracking_data *
data
= newlist_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 tasksresume_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 itBListView
::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_tTEZLauncherView
::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
; int32index
=data
->view
->CurrentSelection
();TListItem
*item
; // get the selected itemitem
= dynamic_cast<TListItem
*> (data
->view
->ItemAt
(index
)); if (item
) { // init drag message with //some useful informationdrag_msg
.AddInt32
("index",index
); // we can even include the itemdrag_msg
.AddRef
("entry ref",item
->Ref
()); // get bitmap from current itemsrc_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
= newBBitmap
(src_bits
->Bounds
(),B_RGBA32
,true
); // we need a view // so we can drawoffscreen_view
= newBView
(drag_bits
->Bounds
(), "",B_FOLLOW_NONE
, 0);drag_bits
->AddChild
(offscreen_view
); // lock it so we can drawdrag_bits
->Lock
(); // fill bitmap with blackoffscreen_view
->SetHighColor
(0, 0, 0, 0);offscreen_view
->FillRect
(offscreen_view
->Bounds
()); // set the alpha leveloffscreen_view
->SetDrawingMode
(B_OP_ALPHA
);offscreen_view
->SetHighColor
(0, 0, 0, 128);offscreen_view
->SetBlendingMode
(B_CONSTANT_ALPHA
,B_ALPHA_COMPOSITE
); // blend in bitmapoffscreen_view
->DrawBitmap
(src_bits
);drag_bits
->Unlock
(); // initiate drag from center // of bitmapdata
->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 rectdata
->view
->DragMessage
(&drag_msg
,BRect
(0, 0,B_LARGE_ICON
- 1,B_LARGE_ICON
- 1)); } } // endif itemdata
->view
->Window
()->Unlock
(); } // endif window lock break; } // endif drag start // take a breathersnooze
(10000); } // while button // free resourcefree
(data
); returnB_NO_ERROR
; }
The only thing left is to wait for a launch message to arrive at the window...
voidTEZLauncherWindow
::MessageReceived
(BMessage
*msg
) { charstring
[512]; int32index
; entry_refentry
; entry_ref *ref
=NULL
; status_tresult
;TListItem
*item
; switch (msg
->what
) { caseeItemDblClicked
: // item was dbl-clicked. //from the message we can find the itemmsg
->FindInt32
("index", &index
);item
= dynamic_cast<TListItem
*> (fList
->ItemAt
(index
)); if (item
)ref
=item
->Ref
(); break; caseeItemMenuSelected
: // item was selected with menu. //find item using CurrentSelectionindex
=fList
->CurrentSelection
();item
= dynamic_cast<TListItem
*> (fList
->ItemAt
(index
)); if (item
)ref
=item
->Ref
(); break; caseeItemDragged
: // 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 itresult
=be_roster
->Launch
(ref
); if (result
!=B_NO_ERROR
) {sprintf
(string
, "Error launching: %s", strerror(result
)); (newBAlert
("",string
, "OK"))->Go
(); } } }
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).
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:
voidArthriticCtrl
::MouseDown
(BPoint
where
) { // handle mouse downBPoint
prev
=where
; uint32buttons
; 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:
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.
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.
Here are four simple steps to asynchronous nirvana if you're deriving
from BControl
:
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.
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
from within SetTracking
(true
)MouseDown()
, and
you'll be on your way.
Override MouseMoved()
, and if you're currently tracking the mouse
(
,
do whatever you need to do when the mouse moves.
IsTracking()
== true
)
Override MouseUp
, and if you're currently tracking the mouse
(
,
call IsTracking()
== true
)
after you've finished
handling the mouse up event, to mark that you've finished tracking the
mouse.
SetTracking
(false
)
BPot
, my twiddle-happy control example, shows how this is done.
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.
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.)
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...
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.