The lack of debugging tools on the BeOS often means that you have to roll your own. Writing good debugging tools isn't an easy proposition, but some simple stuff is within reach.
Call stack tracing (like MALLOC_DEBUG
does for blocks) is
one of the easiest and most useful tricks to have in your
toolbox. It's pretty nice to be able to attach a call stack
to an object so that when something goes wrong with it later
on, you can tell where it came from. This technique is good
not only for memory trashers, but synchronization bugs, too.
Stack crawling inside your own program is made possible by a
quick hack that Ficus worked up one afternoon, being put to
use in the leak checker and in MALLOC_DEBUG
. It consists of
a function that looks a given depth into the call stack and
returns the address it finds there. It does this by walking
each stack frame and grabbing return addresses from the
specific point where they live in that architecture.
A limitation of MALLOC_DEBUG
is the fact that it doesn't
resolve the return addresses into symbols. This is pretty
easy to do, however, so we'll explain the process here.
The Kernel Kit provides a set of image functions intended to help you extract symbols not only from shared libraries, but executable files as well.
Given an image id obtained from get next image info, you can
run through all the symbols contained in a loaded image and
extract them using get_nth_image_symbol
, which gives you
addresses, symbol names, and symbol types. Once stored in a
table, it's a fairly simple task for your program to resolve
each return address in a stack crawl and dump the results to
stdout or to your favorite place for debug output and other
trivia.
Responsible Be engineers always include some usable snippets of code in their articles; you can download a copy of the code for this article from the address below. I'll try to keep the explanations brief, as the code is fairly simple.
<ftp://ftp.be.com/pub/samples/kernel kit/StackHack.zip>
A call to init_sym_table()
from main()
is the all that is
necessary to read symbols from loaded images and store the
data in a global table (which you need to free yourself).
Return addresses located at a given depth in the stack are
obtained through get_caller_address()
. It walks the stack,
looking for valid return addresses in the stack frames. The
level parameter specifies which return address to find.
Be aware that GCC's -fomit-frame-pointer option will cause a nasty crash if try to do this, because there will be no stack frames to walk. Use this only when building your debugging builds with -fomit-frame-pointer turned off.
Once you have an address, you can use lookup_symbol()
to
find the symbol name and offset from the function beginning.
A quick look at the function shows that, given an address,
lookup_symbol()
does a modified binary search of its sorted
list of symbols to find a match and calculate the offset.
That's about all there is to it. Feel free to use the code in your own programs to try to track down various and sundry memory problems. They can be really tough to find and fix, especially given the sparse set of BeOS debugging tools.
This article offers some tips and techniques for creating a clean, font-sensitive, consistent user interface. Putting together a good UI isn't difficult, but it requires a little extra thought about sizing and placement of UI elements and a basic understanding of how our view system works.
To have a consistent interface you must first understand what the views
will use for layout and placement. The BBox
is the only Interface Kit
class that, by default, sets its view font to be_bold_font
; all other
views use be_plain_font
unless they're set otherwise. So—what is be
plain font or be_bold_font
? In the first panel of the Fonts preference
application you'll see the current system settings for the plain, bold,
and fixed fonts the system is using. Next, open the
Fonts.h
interface
file. Near the bottom of the file you'll find this:
extern IMPEXP_BE constBFont
*be_plain_font
; extern IMPEXP_BE constBFont
*be_bold_font
; extern IMPEXP_BE constBFont
*be_fixed_font
;
You can use these three globals to reference the current fonts for each of the three system settings. Two items to note are calculating the height of a control and the width of text based on the desired font. You can do this easily by using
floatstringwidth
=be_plain_font
->StringWidth
("some piece of text");
which will return the width necessary to draw "some piece of text" in whatever the current plain font might be. To calculate the font height, try using
static floatFontHeight
(constBFont
*font
, boolfull
) { font_heightfinfo
;font
->GetHeight
(&finfo
); floath
=finfo
.ascent
+finfo
.descent
; if (full
)h
+=finfo
.leading
; returnh
; }
This will return the correct height necessary for all the text to be visible for any font.
Please note that the font settings for the following examples aren't necessarily recommended, but are used here as an example of how settings affect view layout.
We'll assume that these views have the following code as their parent, and that the code is added to a window:
BBox
*box
= newBBox
(Bounds
(), "background",B_FOLLOW_ALL
,B_WILL_DRAW
|B_FRAME_EVENTS
,B_PLAIN_BORDER
);AddChild
(box
);
Many classes in the Interface Kit size themselves based on the plain font setting, although some do not. Most initially use some basic default values for layout, and many of these values should be overridden to achieve a clean interface. Now then, let's take a look at a number of the Interface Kit classes.
A BStringView
is a simple view that displays static, noneditable text.
It's often used to display information such as labels or the status of
some process. Remember when you're using a BStringView
that it requires
adequate space, both width and height, to display completely any text
that it might hold.
To properly size a BStringView
, try this:
BRect
frame
(10, 10, 10 +be_plain_font
->StringWidth
("Just a string"), 10 +FontHeight
(be_plain_font
,true
));BStringView
*stringview
= newBStringView
(frame
, "string", "Just a string");box
->AddChild
(stringview
);
If you do this the string will always be completely displayed to the user, regardless of the current plain font settings.
A BTextView
, unlike a BStringView
,
can be an editable text field for
displaying multi-line text. As with sizing the BStringView
, correct
initial sizing of a BTextView
can also be done quite easily:
frame
.Set
(10,stringview
->Frame
().bottom
+10,Bounds
().Width
()-10,stringview
->Frame
().bottom
+10 + 3 + (3 *FontHeight
(be_plain_font
,true
)) + 3); // // The text rect is inset a bit so that there // is a reasonable gutter surrounding the text //BRect
textrect
(frame
);textrect
.OffsetTo
(0,0);textrect
.InsetBy
(2,2);BTextView
*textview
= newBTextView
(frame
, "text",textrect
,B_FOLLOW_LEFT_RIGHT
|B_FOLLOW_TOP
,B_WILL_DRAW
);box
->AddChild
(textview
);
This creates a simple editable text area that displays three full lines.
A BTextControl
is a mixed control; the left portion is a static label,
the right portion is a field that allows text entry and can send a
message notifying the host that the text has been modified. A BMenuField
is similar, but has a pop-up menu on the right.
As with the previous classes, sizing is important for display, but for
this class, the height is set by the class once it is added to a parent.
Note that both of these classes include a label that displays static
text. Rather than just placing multiple BTextControl
s
or BMenuField
s with
their left edges aligned, you can get a more pleasing effect by aligning
the controls at their Divider, the line that divides the label from the
actual control. Once again, you can accomplish this by doing some initial
calculations and by understanding the controls themselves.
const char* constkAnythingStr
= "Anything"; const char* constkNumbersOnlyStr
= "10 Numbers Only"; float maxlabelwidth =be_bold_font
->StringWidth
(kNumbersOnlyStr
) + 5; float labelwidth =be_plain_font
->StringWidth
(kAnythingStr
) + 5; // // based on the longest label "Anything" // simply calculate from that controls divider // to where a smaller label should start //frame
.left
= (10 +maxlabelwidth
) -labelwidth
; // // make this view relative to the last view added // it will resize itself vertically // so, just create a valid BRect //frame
.top
=textview
->Frame
().bottom
+ 10;frame
.bottom
=frame
.top
+ 1;BTextControl
*tc1
= newBTextControl
(frame
, "any text",kAnythingStr
,kAnythingStr
,NULL
,B_FOLLOW_LEFT_RIGHT
|B_FOLLOW_TOP
);box
->AddChild
(tc1
); // // set the divider and alignment // so that it looks good //tc1
->SetDivider
(be_plain_font
->StringWidth
(kAnythingStr
) + 5);tc1
->SetAlignment
(B_ALIGN_RIGHT
,B_ALIGN_LEFT
); // // once again, place the next view below the last //frame
.top
=tc1
->Frame
().bottom
+ 5;frame
.bottom
=frame
.top
+ 1;frame
.left
= 10; // // calculate the actual width based on the // text and font //frame
.right
=be_bold_font
->StringWidth
(kNumbersOnlyStr
) + 5 + 10 + (be_bold_font
->StringWidth
("0")*10) + 10;BTextControl
* tc2 = newBTextControl
(frame
, "numbers only",kNumbersOnlyStr
, "0987654321",NULL
);box
->AddChild
(tc2
);tc2
->SetFont
(be_bold_font
);tc2
->SetDivider
(be_bold_font
->StringWidth
(kNumbersOnlyStr
) + 5);tc2
->SetAlignment
(B_ALIGN_RIGHT
,B_ALIGN_LEFT
); // // here is a bit of extra code that // shows how to limit a BTextView to // a specific set of characters //BTextView
*tv
=tc2
->TextView
();tv
->SetMaxBytes
(10); for (longi
= 0;i
< 256;i
++)tv
->DisallowChar(i); for (longi
= '0';i
<= '9';i
++)tv
->AllowChar
(i
);tv
->AllowChar
(B_BACKSPACE
); // and, now for a couple BMenuFields // // build up a sample menu //BPopUpMenu
*menu
= newBPopUpMenu
("a simple menu");menu
->AddItem
(newBMenuItem
("First Item",NULL
));menu
->AddItem
(newBMenuItem
("Second Item",NULL
));menu
->ItemAt
(0)->SetMarked
(true
); // // place this control relative to the BTextControls //frame
.top
=tc2
->Frame
().bottom
+ 10;frame
.bottom
=frame
.top
+ 1;frame
.left
= (tc2
->Frame
().left
+tc2
->Divider()) - (be_plain_font
->StringWidth
("A Longer Label")+5); // // and, again, calculate the correct width based // on the text and font //frame
.right
=frame
.left
+be_plain_font
->StringWidth
("A Longer Label") +be_plain_font
->StringWidth
("Second Item") + 30;BMenuField
*menubtn1
= newBMenuField
(frame
, "menu/menufield", "A Longer Label",menu
);box
->AddChild
(menubtn1
);menubtn1
->SetDivider
(be_plain_font
->StringWidth
("A Longer Label") + 5); // // and, create a second one //menu
= newBPopUpMenu
("a simple menu");menu
->AddItem
(newBMenuItem
("Some Item",NULL
));menu
->AddItem
(newBMenuItem
("Another Item",NULL
));menu
->ItemAt
(1)->SetMarked
(true
);menu
->SetFont
(be_bold_font
);frame
.top
=menubtn1
->Frame
().bottom
;frame
.bottom
=frame
.top
+ 1;frame
.left
= (menubtn1
->Frame
().left
+menubtn1
->Divider
()) - (be_bold_font
->StringWidth
("Menu")+5);frame
.right
=frame
.left
+be_bold_font
->StringWidth
("Menu") +be_plain_font
->StringWidth
("Another Item") + 30;BMenuField
*menubtn2
= newBMenuField
(frame
, "menu/menufield", "Menu",menu
);box
->AddChild
(menubtn2
);menubtn2
->SetFont
(be_bold_font
);menubtn2
->SetDivider
(be_bold_font
->StringWidth
("Menu")+5);
Not only will the dividers be aligned, but there will be no dead space to the left of the label. The effect is that only the label and the control will be clickable, resulting in a moderately cleaner UI.
A BButton
is a simple push button. It's easy to create and use, but
tricky to have it show its display label correctly. As with a
BTextControl
, a BButton
's
height is based on the current plain font. Its
width, though, should be calculated and padded with respect to the plain
font.
// the default minimum width of a button used by Be // const floatkMinimumButtonWidth
= 75.0; const char* constkOkayStr
= "Okay"; const char* constkLongStr
= "Some extra long button name"; float width =be_plain_font
->StringWidth
(kOkayStr
) + 20;frame
.top
=menubtn2
->Frame
().bottom
+ 10;frame
.bottom
=frame
.top
+ 1;frame
.left
= 10; // // accommodate a longer width, depending on the text // and the font, else use the default minimum size //frame
.right
=frame
.left
+ ((width
>kMinimumButtonWidth
) ?width
:kMinimumButtonWidth
);BButton
*btn1
= newBButton
(frame
, "button",kOkayStr
,NULL
);box
->AddChild
(btn1
); // // and, add another wider button //width
=be_bold_font
->StringWidth
(kLongStr
) + 20;frame
.left
=frame
.right
+ 10;frame
.right
=frame
.left
+ ((width
>kMinimumButtonWidth
) ?width
:kMinimumButtonWidth
);BButton
*btn2
= newBButton
(frame
, "button",kLongStr
,NULL
);box
->AddChild
(btn2
);btn2
->SetFont
(be_bold_font
);
The last two controls I'll address are BCheckBox
and BRadioButton
. Both
are standard and, like BButton
, their height is sized appropriately,
based on the current plain font. Note that to create a more intuitive UI,
sizing the width appropriately will make your controls a bit cleaner.
Once again, use StringWidth()
and some padding for the frame when creating
these controls.
frame
.left
= 10;frame
.right
=frame
.left
+ 20 +be_bold_font
->StringWidth
("Bold RadioButton");frame
.top
=btn2
->Frame
().bottom
+ 10;frame
.bottom
=frame
.top
+1;BRadioButton
*rb1
= newBRadioButton
(frame
, "radio button", "Bold RadioButton",NULL
);box
->AddChild
(rb1
);rb1
->SetFont
(be_bold_font
);frame
.top
=rb1
->Frame
().bottom
;frame
.bottom
=frame
.top
+ 1;frame
.right
=frame
.left
+ 20 +be_bold_font
->StringWidth
("RadioButton");BRadioButton
*rb2
= newBRadioButton
(frame
, "radio button", "RadioButton",NULL
);box
->AddChild
(rb2
);rb2
->SetFont
(be_bold_font
);frame
.top
=rb1
->Frame
().top
;frame
.bottom
=frame
.top
+ 1;frame
.left
=rb1
->Frame
().right
+ 20;frame
.right
=frame
.left
+ 20 +be_plain_font
->StringWidth
("CheckBox");BCheckBox
*cb1
= newBCheckBox
(frame
, "checkbox", "CheckBox",NULL
);box
->AddChild
(cb1
);frame
.top
=rb2
->Frame
().top
;frame
.bottom
=frame
.top
+ 1;frame
.right
=frame
.left
+ 20 +be_plain_font
->StringWidth
("Longer CheckBox Title");BCheckBox
*cb2
= newBCheckBox
(frame
, "checkbox", "Longer CheckBox Title",NULL
);box
->AddChild
(cb2
);
As with the labels for a BTextControl
or BMenuField
, the only portion of
the BCheckBox
or BRadioButton
that is clickable is now the label and the
control itself.
Notice that many of these examples use a previous control to initially place the next control. Doing this makes your layout sensitive to the font height; also, controls set up this way will not collide with each other upon display.
Lastly, the window should flow with the font sensitivity of the views that it contains. Here, a basic heuristic is used to determine what the height and width should be:
// // the height of the window will be relative // to the last control added, in this case the // last BCheckBox // the width of the window will be based on the // control that has its right edge furthest to // the right, in this case either the last // BCheckBox or the second BButton // floatright
= (cb2
->Frame
().right
>btn2
->Frame
().right
) ?cb2
->Frame
().right
:btn2
->Frame
().right
;ResizeTo
(right
+ 10,cb2
->Frame
().bottom
+ 10);
Add the above code to a window of an application and run it. All the controls will be visible and sized appropriately. The views themselves will not overrun their siblings and only the visible portions of the controls will be active. Now open the Fonts preference application and select any set of fonts, close the panel, and open the test application again. While the panel may be larger, or possibly smaller, the same criteria will hold true.
So, why bother with these extra calculations and concerns? Simple—to make your interface consistent and always usable. Since the user can configure his system in many ways, particularly the choice of fonts, applications should defer to the user's wishes.
The BeOS Pulse utility has—until now—actually been two and a half programs: the standard app you open in the Tracker, and a MiniPulse version that works as either an application or a replicant. In this article (and the accompanying modifications) I'll explain the consolidation of these two apps into one that's tied together through the GUI. Combining the two boosts responsiveness, adds a preferences panel, and uses some newer APIs. The techniques I'll discuss could spice up any old application, without requiring drastic changes. You can grab the new source code here:
<ftp://ftp.be.com/pub/samples/application kit/Pulse.zip>
The first thing you notice when you build and launch Pulse is... nothing different. That's because this version intentionally retains the visual elements that made it a classic. When you right-click in the window, though, you'll start to see the changes. The organizational theme of the new Pulse is that it can be run in three different modes, one of them a replicant in the Deskbar. The pop-up context menu presents you with the other two available choices.
Taking a step back, let's run through how Pulse got its name. BView
s that
need to do regular (but not precisely timed) work can be constructed with
the B_PULSE_NEEDED
flag. The hook function
BView
::Pulse()
is then called
at intervals determined by its window, which can be set with
BWindow
::SetPulseRate()
. This setting affects all views attached to that
window.
All three modes contain a view that descends from a class called
PulseView
. Its job is to build the parts of the pop-up menu that they all
share, and to launch the menu when the view is clicked. Consider this:
voidPulseView
::MouseDown
(BPoint
point
) {BPoint
cursor
; uint32buttons
;MakeFocus
(true
);GetMouse
(&cursor
, &buttons
,true
); if (buttons
&B_SECONDARY_MOUSE_BUTTON
) {ConvertToScreen
(&point
);popupmenu
->Go
(point
,true
,false
,true
); } }
What's important here is that the menu is launched asynchronously, and
the Go()
call returns immediately. The
BMenuItem
that is chosen (if one
is chosen) sends a copy of its message when it is invoked. If the Go()
method were synchronous, no Pulse()
events would be received as long as
the menu was held down, and hence there would be no redraws. As we say
around here, B_DONT_DO_THAT
. Similarly, the
BAlert
-based About boxes in
Pulse are launched with
,
the asynchronous version. In
general, if you need to know which button the user clicked, give a valid
alert
->Go
(NULL
)BInvoker
and check the "which" field when the message arrives.
The real work of PulseView
is to calculate system activity. This is done
by examining the system_info struct as follows:
voidPulseView
::Update
() { system_infosys_info
;get_system_info
(&sys_info
); bigtime_tnow
=system_time
(); for (intx
= 0;x
<sys_info
.cpu_count
;x
++) { doublecpu_time
= (double) (sys_info
.cpu_infos
[x
].active_time
-prev_active
[x
]) / (now
-prev_time
);prev_active
[x
] =sys_info
.cpu_infos
[x
].active_time
; if (cpu_time
< 0)cpu_time
= 0; if (cpu_time
> 1)cpu_time
= 1;cpu_times
[x
] =cpu_time
; }prev_time
=now
; }
The active time variable is the number of microseconds spent doing work
on this CPU since the machine was booted. The activity for this processor
is determined by the change in this figure divided by the amount of time
that has passed since the last call to Update()
.
For lucky users with multiple CPUs, this class also controls processor enabling and disabling. This is accomplished by telling the scheduler not to assign any work to that CPU. In the BeBox days, you could turn off both processors. This had two purposes: the geek thrill that you could do it, and to show how fast the BeOS can reboot. As much fun as that used to be, Pulse now does a sanity check to prevent disabling the last enabled CPU.
Going back to NormalPulseView
, there are now two
ways to show off disabling CPUs: with the CPUButton
s
(which are also replicants), or through the pop-up menu. To make sure we
stay consistent, the Update()
function keeps the
BMenuItem
s current, and the
CPUButtons
take care of themselves, as is necessary
when they're not attached to our window.
If you've dived into the Pulse source before, you might notice that
CPUButton
used to be a BPictureButton
.
It's now been rewritten to inherit
from BControl
directly and do its own drawing, as you'll see later on.
For now, check out how CPUButton
tracks the mouse asynchronously (does a
pattern begin to emerge?):
voidCPUButton
::MouseDown
(BPoint
point
) {SetValue
(!Value
());SetTracking
(true
);SetMouseEventMask
(B_POINTER_EVENTS
,B_LOCK_WINDOW_FOCUS
); } voidCPUButton
::MouseMoved
(BPoint
point
, uint32transit
, constBMessage
*message
) { if (IsTracking
()) { if (transit
==B_ENTERED_VIEW
||transit
==B_EXITED_VIEW
)SetValue
(!Value
()); } } voidCPUButton
::MouseUp
(BPoint
point
) { if (Bounds
().Contains
(point
))Invoke
();SetTracking
(false
); }
That's all you need for the guts of a two-state button that doesn't hog the window's thread while the mouse button is down.
Rather than set your options from the command line, Pulse now uses a GUI
to set (and remember) your choices. Pulse will launch in the mode you
last left it in, unless you launch from the command line and force either
-normal , -mini , or
-deskbar modes . This is for the sake of compatibility
and for users who like to launch Pulse as a replicant from
~/config/boot/UserBootscript
.
Preferences are stored through the Prefs
class, which writes them as
attributes to a file in the user's settings directory. This is important
-- while BeOS is single-user at the moment, it may not always be so, and
from now on it will be considered bad form (and soon it will be
impossible) to attach your settings as attributes to the application
itself. To get started, you need to find the user's settings directory:
BPath
path
;find_directory
(B_USER_SETTINGS_DIRECTORY
, &path
);path
.Append
("Pulse settings");file
= newBFile
(path
.Path
(),B_READ_WRITE
|B_CREATE_FILE
);
Always use find_directory()
for this purpose; do not hard code a path to
~/config/settings
. Because
BFile
subclasses BNode
, you can then read and
write your attributes using the standard ReadAttr()
and WriteAttr()
functions. The Prefs class in Pulse has a number of wrapper methods that
will handle common errors and insert a default value if a given attribute
was not found. I'd be happy to round out this class with the other
popular data types and put it in the Developer Library. Drop me a line if
this would be helpful.
So—what's so great about GUI preferences? The ability to change
Pulse's colors in real time! At the moment, BColorControl
ignores the B_ASYNCHRONOUS_CONTROLS
flag, so
for real time updates you have to subclass
it and override one important method:
voidRTColorControl
::SetValue
(int32color
) {BColorControl
::SetValue
(color
);Invoke
(); }
This forces the color control to send its model message every time
there's a change, rather than only when the mouse button is released. Its
parent, a ConfigView
, then packages all the
PulseView
settings that we're
updating. But how does this message find its way to the views to do their
drawing? Pulse uses a simple message model to make sure everything is
notified correctly: the window or view that spawns the PrefsWindow
passes
a BMessenger
to itself in the constructor, so messages can find their way
home. The ConfigViews
just borrow this messenger.
On the receiving end, the appropriate MessageReceived()
passes the
message to UpdateColors()
There the message
is decoded and Draw()
is
called to make the new settings take effect. Update()
is not called
because we don't want to recalculate system activity as we drag—this
would make the ProgressBars
flicker and change rapidly.
These real time color changes explain why CPUButton
now does its own
drawing—you wouldn't want to create a new BPicture
and redraw it each
time a new message arrived. We want to encourage programs whose settings
take effect immediately. Only changes that involve a significant amount
of work should require an Apply button.
MiniPulseView
is based on Arve's MiniPulse application, as is
DeskbarPulseView
. It implements its own UpdateColors()
method to set all
three variables when messages arrive. Note that you must specify the
target for your BMenuItem
s in
AttachedToWindow()
. Because BPopUpMenu
s
never get attached to a window, their targets can't default to the
window's handler. And since we launch the pop-up menu asynchronously, we
don't wait around to find out which item was selected.
To reduce flicker, we call
to prevent
the view from being redrawn in its background color. This is only useful
if your SetViewColor
(B_TRANSPARENT_COLOR
)Draw()
method will touch every pixel of the view, which it does.
To facilitate smooth resizing, PulseView
passes the B_FRAME_EVENTS
flag
to the BView
constructor. Taking advantage of this is as simple as
voidMiniPulseView
::FrameResized
(floatwidth
, floatheight
) {Draw
(Bounds
()); }
DeskbarPulseView
essentially drops a
MiniPulseView
into the Deskbar, so
you can see what your system is up to at a glance. For a refresher course
in replicants, check out Eric Shepherd's tutorial:
Developers' Workshop: Stepping Up To the Deskbar
The replicant is constructed and added with the BDeskbar
class, which is
new in R4.5. This is done by building an instance of the view with the
standard constructor, which gets archived. Two important notes here are
that standard Deskbar replicants should be 16x16 pixels (remember, that's
,
and that you should delete the new instance after
calling BRect
r
(0, 0, 15, 15))BDeskbar
::AddItem()
. The Pulse replicant permits variable width,
but the height is always fixed. If you want your replicant to be wider
than 16 pixels, try to keep it reasonable so that you don't overwrite the
time. A good test is to move the Deskbar to one of the vertical
orientations, where the tray width is fixed. Pulse makes 50 pixels the
upper limit for width. The lower limit is based on the number of CPUs you
have, such that each one will have at least a one-pixel-wide activity bar.
Since replicants have to handle their own messages, AttachedToWindow()
sets all targets to itself:
BMessenger
messenger
(this
);mode1
->SetTarget
(messenger
); // etc.
And now for the nonintuitive part. The Pulse replicant doesn't use
Pulse()
. The reasoning goes like this: if you want to receive regular
events, you probably want to control how often they occur, and that
setting affects all views attached to your window. From now on, you
should consider BWindow
::SetPulseRate()
off limits as a Deskbar
replicant, because the Deskbar itself may need to do periodic work at its
own pace. This may be enforced in the future. In general it's a bad idea
to the set the pulse rate from any replicant that might be used in
someone else's window—it's not courteous.
But fear not—we can still respond to periodic events by constructing a
BMessageRunner
, an often unnoticed addition from R4. Using the messenger
from above in AttachedToWindow()
, use
messagerunner
= newBMessageRunner
(messenger
, newBMessage
(PV_REPLICANT_PULSE
), 200000, -1);
This delivers messages to DeskbarPulseView
::MessageReceived()
every 1/5th
of a second until messagerunner is deleted. To prevent any work from
being done in Pulse()
, override
MiniPulseView
's version with an empty
function.
Two final points about replicants: on PowerPC, you'll need to use specific linker settings. From BeIDE choose either "All Globals" or "Use .exp file". From a makefile add the following after the makefile engine #include:
ifeq ($(CPU),ppc) LDFLAGS += -export all endif
The other option is to force your replicant class to be exported. In that case you want the "Use #pragma" option, so you don't expose your naming conventions to the world. The code looks like this:
#include <interface/BView.h> #include <BeBuild.h> class EXPORTMyView
: publicBView
{ // the usual stuff goes here }
The other thing to remember is that you'll have to kill and restart the Deskbar each time you want to try a new version of your replicant. This is because the code runs in the Deskbar's memory space. When you try to launch a new version, the old code is still cached and is used instead.
I hope this article provides some good tips on how to write BeOS apps that are responsive and use the newest APIs. Feel free to contact me with questions, comments, and requests. And no, you can't disable all CPUs -- the kernel no longer allows it. The sanity check used to perform this function; now it just warns the user that the request can't be completed.
Rephrase inches towards usability. This week—opening files from the command line in Terminal. To make each file open up in its own phrase window, type:
Rephrase <space separated file names>
You can find this version of Rephrase at:
<ftp://ftp.be.com/pub/samples/tutorials/rephrase/rephrase0.1d2.zip>
Rephrase 0.1d2 New Features
Open Files from command line arguments at launch
Multiple Windows
You access files in BeOS through the classes of the Storage Kit. Conceptually, the Storage Kit consists of two types of objects: those representing a location in a filesystem, and those representing the content at a specific location.
The objects representing a location include:
entry_ref—a location expressed through a name, a directory token, and a volume token. entry_refs are usually the most efficient way to represent a location, but they must be converted to a more useful format to do much real work.
BPath
—a convenience class representing a location as a string. It
provides the means to manipulate and parse the path string. A path
string is the appropriate method to persistently store a location, as
an entry_ref's tokens can change across a reboot.
BEntry
—a class representing a location. It provides information
about that location, including whether a file exists there. It also
provides facilities to rename, move, or remove a file at that location.
The content at a specific location is represented by subclasses of BNode
.
This version of Rephrase deals with one such class, the BFile
, and only
with it's creation. The BFile
is manipulated
through the BTextView
.
For more detailed information on the Storage Kit or on BeOS installation
see:
TheStorageKit.html
BApplication
has a hook function ArgvReceived()
that passes in any
command line arguments passed to the application. Rephrase interprets
these arguments as a list of files to open. In ArgvReceived()
Rephrase
translates each file name to a BEntry
, and if there is a file at that
location, puts the appropriate entry_ref in a
B_REFS_RECEIVED
message,
which it passes to RefsReceived()
.
RefsReceived()
is a BApplication
hook function that presents a BMessage
with one or more entry_refs for the app to process. In Rephrase, the
appropriate action is to open a new window for each specified ref. In the
future, the system will call RefsReceived()
when files are dropped onto
the Rephrase binary. It made sense to put the file opening code where it
will be used.
If you accept command line arguments to open files, you should call
RefsRecieved()
directly from ArgvReceived()
.
Why? The ReadyToRun()
hook provides a last chance to prepare for user interaction. It is
called after all launch-time messages have been handled. Posting the
B_REFS_RECEIVED
message results in ReadyToRun()
being called before
RefsReceived()
. This creates the default phrase editing window in
addition to any windows opened by RefsReceived()
. This is not the
intended behavior. [Rephrase.cpp
:84-85]
Windows can call Show()
in their constructor. This ensures that the
window will be shown when created. Try to be consistent—pick a
place to call Show()
and stick with it. [pDisplay.cpp
:75]
An application that opens multiple windows should make the effort
to ensure that they don't all open right on top of one another. The
pDisplay class keeps track of the next position for a window. It tiles
the windows across the screen in rows, until the screen is filled,
then starts over. [pDisplay.cpp
:140-167]
The BWindow
and BView
system lets you find a view by its name.
BWindow
::FindView()
and BView
::FindView()
return a BView
* with the
given name if one exists. Use a safe casting mechanism to the
appropriate BView
subclass, and always make sure you actually have a
view before acting on it. FindView()
returns NULL
if no view is found
and dynamic_cast()
returns NULL
if the view is not of the specified
type. When you need to refer to a specific view often, it's wiser to
cache a pointer to the view when it is created. The BTextView
will be
an obvious choice as Rephrase develops. [pDisplay.cpp
:123]
You can also search a BMenu
hierarchy
for BMenuItem
s with a given
name. Accordingly, we don't hold onto a pointer to the "About
Rephrase" item any longer, but simply look it up. Submenus added to a
BMenu
(which includes BMenuBar
s)
have a BMenuItem
created for them. To
find a submenu, look for the BMenuItem
with the correct name and call
Submenu()
to get the menu object. [pDisplay.cpp
:60-66]
Instead of reading contents out of the file directly, Rephrase
currently uses the version of BTextView
::SetText()
that accepts a
BFile
pointer as an argument. This is a matter of convenience in these
early stages. In an upcoming installment we'll show how to read and
write to the BFile
. [pDisplay.cpp
:131]
BMessage
is a class that acts as a data container. It provides easy
methods to add and find data and to query what types of data are
available. [Rephrase.cpp
:88-108]
I've fixed an off-by-one bug from 01.d1. Views should not overlap:
accordingly the BTextView
should start one pixel below the end of the
menu. [pDisplay.cpp
:47]
Next Week: Resizing and Scroll bars
We're now out of the customary 25-day period after the effective date of an IPO—July 20th in our case—during which we were embargoed from making public statements. The idea behind the silence is to let the market "digest" the news of an IPO before a company can offer information not contained in its prospectus. The prospectus, edited under the watchful eye of the SEC, contains the data investors use to evaluate the pros and cons of buying stock in a company.
The rule is that you're not supposed to add anything to a prospectus during the IPO process for 25 days after the offering is made. The very simple, sensible idea behind this rule is to level the investing field and make sure all investors operate with the same set of data. This makes investors trust the workings of the market and is good for the investing business.
Moving from lofty principles to practical consequences, maintaining this atmosphere of good faith creates certain restrictions that every public company operates under. For instance, when discussing the future of our business, in this newsletter or elsewhere, we have to be careful not to impart information that would create an imbalance between individuals who received the data and the public at large. So, in the interest of balance we shouldn't make revenue or earnings projections or issue statements that would lead one to infer such a forecast.
More specifically, we can say this new release is terrific because it contains these new features and fixes these embarrassing bugs. We can even say it's better than the Sumo Wrestler OS for digital media over broadband, but we cannot say that it will increase our revenue and earnings by such and such percentage. If the spirit of the moment carries us away and moves us to make such an imprudent statement, we're obliged to immediately put out a press release disclosing the information to the general public, in order to restore a level playing field.
Today's story doesn't say how the SEC and others would view repeat or seriously disruptive incidents. In the same vein—giving investors equal access to information and enough time to process it—earnings reports are released right after the market closes, giving investors time to sleep on the news.
Lastly, a word about a paragraph you might have seen in many press releases, including our recent earnings statement. The paragraph starts like this: "The statements contained in this Press Release may contain 'forward-looking statements.' Actual events or results may differ materially as a result of risks...," etc. The intent is to remind readers of press releases, or audiences listening to speeches, not to mistake some forward-looking statements for actual forecasts. For instance, discussion of future releases may or may not contain forward-looking statements, and companies entertaining such discussions may or may not want to remind readers of the limits and context.