Issue 3-19, May 13, 1998

Be Engineering Insights: The Tracker Is Your Friend

By Scott Barta

When I came aboard at Be in January to begin my headlong dive into the guts of NetPositive, I quickly learned that one of the features most often requested was a hierarchical bookmark menu, along with better bookmark management than what PR2 offered (which was essentially nothing). Based on experience with other browsers and platforms, my usual response was to stare at my shoes and mumble something about how I should someday implement a bookmark manager à la Netscape or Internet Explorer. Most people's response was, "No, use the Tracker instead. It's the Be way of doing things."

For the Release 3 release, at Pavel's prodding, I changed the bookmark data format to use the attribute defined by the x.vnd-Be.bookmark file type for storing the bookmark URL, and made sure that bookmarks were given this type when saved. This opened up NetPositive bookmarks for editing in Tracker windows by allowing users to display and edit the URL attribute of bookmarks in the Tracker list view of NetPositive's Bookmarks directory. In retrospect, I should probably have modified the code to use the Title bookmark attribute as well, instead of relying on the filename for the bookmark's title, as this would have prevented a few weird quirks in bookmark naming.

In my post-Release 3 work (which you'll see in the R4 release of NetPositive, or very likely sooner in an interim release after Release 3.1), I've extended the relationship between NetPositive and the Tracker for handling bookmarks—I use NodeMonitors to keep informed of changes to the Bookmarks directory so I can update my Bookmarks menu on the fly. I also allow the user to create hierarchies in the Bookmarks menu by creating subdirectories in the Bookmarks directory.

Having done this work, I'm now a complete convert to using the Tracker to manage your data. I've found that combining file directories with custom attributes, Tracker management of those files, and NodeMonitors to keep you up to date on changes, is a powerful way to manage lists of information with minimal work on your part, while providing users with a lot of power over that data through an already familiar interface. I plan to extend NetPositive to manage site passwords, cookies, and other information in this way, without the need to come up with a new user interface and storage format for each type of information.

So where should you use this sort of mechanism in your applications? Anywhere you need user-maintained lists of information; examples include bookmarks, recent or favorite documents (you could maintain a directory of symlinks to do this), shortcuts, macros, plug-ins, etc. It's probably best suited to applications where the list of information is relatively short (i.e., dozens of items); it's not be the best way to maintain a user spelling dictionary.

How is it done? Follow these steps:

  1. Define a file type with the custom attributes you need. For those attributes, set up the information that will make the attributes visible to the Tracker and editable by it. For more information on how to do this, see Doug Fulton's article in Be Newsletter Volume 3 Issue 11, "How To Get Tracker to See Your Attributes".

  2. Save those files in a directory someplace meaningful to your application.

  3. Set up a Node Monitor for that directory that will catch additions, deletions, and name changes inside the directory.

  4. Since the directory Node Monitor will not receive notifications of attribute changes to files inside the directory, you need to set up a separate Node Monitor for each file in the directory to do this. Since Node Monitors are a limited system resource, this is a good reason to use this mechanism only for relatively small lists. Whenever the directory Node Monitor receives notification for added, deleted, and moved files, add and remove Node Monitors for those files as appropriate.

  5. At program launch, read through the files in the directory, slurp out their attributes, and store them in a meaningful internal representation. When you receive notification from one of the Node Monitors of a change, update your data and take appropriate action.

  6. When you receive a Node Monitor message, munge the low-level information it gives you into a meaningful format that you can deal with. This is actually a pretty hard thing to do because the Node Monitor interface is so low-level as to be an extreme pain in the neck.

"So where's some code I can copy and paste?" you ask. I've written a handy FolderWatcher class that does most of these tasks for you. Due to its length (close to 1,000 lines of code), it's not included here, but you can get it from our sample code archives at ftp://ftp.be.com/pub/samples/storage_kit/FolderWatcher.zip

The sample code includes the FolderWatcher class and a simple application that creates some folders in your home directory, adds a few bookmark files to them, and then spits out any messages it gets back from the FolderWatcher. It's a command line program and has no GUI, so the application itself isn't very useful, but it should show you how to use the FolderWatcher.

To use this class, create a separate FolderWatcher instantiation for each folder that you wish to watch. In the constructor, pass in a BEntry to the directory to be observed, the file type of the file of interest, the signature of the preferred application for the files, and the BLooper to be notified when something interesting happens. If you want, the FolderWatcher can create the folder if it doesn't already exist.

Once the FolderWatcher is created, call Init() on it to kick it off. It will set up the Node Monitor on the folder, iterate through each matching file in the folder, set a Node Monitor on it, and send your application a FolderWatcher::FileAdded message for it so you can add it to your internal list. Init() creates a BMessageFilter and inserts it into the BLooper's message loop so that it can spy on the Node Monitor messages that come back, and convert them to a friendlier form. It should behave itself and not eat Node Monitor messages that don't pertain to the folders it's watching.

I've found that for simple cases like I'm assuming here, a BMessage is a good way to represent attribute data, since BMessages can contain the type/name/value triplets that attributes use. Therefore, I use BMessages for passing attributes back and forth in the FolderWatcher interface. Note, however, that FolderWatcher will become extremely inefficient if you store large amounts of attribute data in these files.

To create new files in the folder, you can do it yourself or call FolderWatcher::AddFile(), passing in the filename a BMessage containing the attribute data to be put into the file. Similarly, to remove a file, you can call RemoveFile(), passing the filename; to change a file's attribute, call ChangeFile(), passing the filename and a BMessage. These functions will cause notification messages to be sent back to inform you of the change, so be ready for it.

If you don't need to watch the folder anymore, delete the FolderWatcher instance; it will clean up silently (without sending further messages back to you).

All other communication with the FolderWatcher occurs as messages sent back to your BLooper when something happens:

As it is written, the class doesn't recurse into subdirectories, so that might be a good way to modify it if you want to enhance its capabilities. (In case you're wondering, this class was written after I implemented hierarchical bookmarks in NetPositive; I'm going to go back and shoehorn in a modified version of this class later.) Another suggestion would be to modify AddFile() to let it automatically generate a filename for a file if you don't want to do it yourself.

As a final warning, the code hasn't been stringently tested, so caveat emptor. If you find problems, though, let me know and I'll fix them and update the sample code archive. Happy coding!


Be Acquires StarCode

Be's success depends on the creation of a solid BeOS applications market and online commerce model. We strive to give our developers the best engineering resources and support, so they can develop the best applications for the BeOS. Now we will also provide them with the tools they need to successfully market their product. To this end, Be has recently acquired StarCode, Inc. of Redwood City, CA.

StarCode's products, BeDepot, Software Valet 1.5 for Intel and PPC, and PackageBuilder will be integrated into the line of Be products and services. This integration will allow Be to provide developers and end-users with software management solutions and with an online commerce system for selling and buying BeOS applications over the web.

Developers will have the tools to market their BeOS applications efficiently and conveniently, and users will have the means to easily and quickly access a wide range of BeOS products. StarCode and Be are working together to ensure a smooth transition and integration. Over time, Be will incorporate these products and services into our developer program.

Software Valet is an installer/deinstaller technology for the Beos, and will provide developers and users with a consistent reliable means of installing applications, updates, drivers, and other good stuff.

Package Builder provides tools for creating downloadable and installable/deinstallable applications and updates.

BeDepot is an online commerce system for selling applications over the web.

Tune in to our web site, as more information will progressively be available, or write to info@be.com.


Developers' Workshop: BeOS Programming Basics: Part 3

By Eric Shepherd

Last time, we created a simple application with a menu bar that the user could play with. We also poked a toe into the waters of the BMessage class, but I skimmed over the details, leaving you high and dry until this week's exciting episode. If you haven't read parts 1 and 2 of this series, you really ought to do so: Issue 3-#7 Issue 3-#13

As promised, this week we're going to delve a little more deeply into messaging on the BeOS. In particular, we're going to add support for multiple windows to the MenuWorld application from last time, using messages to keep track of how many windows are open, and to ensure that no two untitled windows get the same name.

To accomplish this magnificent feat of software engineering, we're going to extend the HelloApp class to serve as a registry for our windows. Whenever a new window is opened within the MessageWorld application, that window tells the HelloApp object that a new window has been opened. HelloApp, in return, lets the new window know what its name should be.

As always, you might want to have your Web browser aimed at the Be Book so you can read the more detailed descriptions of the functions discussed here; we won't be covering anything in complete detail, but just taking a look at how things might be done, to give you a place to start.

We begin with the MenuWorld application from last time. Before getting into messaging, let's take a look at a minor change to the HelloWindow constructor. It was pointed out on the BeDevTalk mailing list that MenuWorld was assuming that the menu bar was always going to be 20 pixels tall, which is not a safe assumption to make.

Additionally, there are a couple of minor changes needed so the window can register itself with the application object.

So the HelloWindow constructor now does the following:

HelloWindow::HelloWindow(BRect frame)
           : BWindow(frame, "Untitled ", B_TITLED_WINDOW,
               B_NOT_RESIZABLE|B_NOT_ZOOMABLE) {
  BRect r;
  BMenu *menu;
  BMenuItem *item;

  // Add the menu bar

  r = Bounds();
  menubar = new BMenuBar(r, "menu_bar");
  AddChild(menubar);

  // Add File menu to menu bar

  /* same code as last time */

  // Add Options menu to menu bar

  /* same code as last time */

  // Add the drawing view

  r.top = menubar->Bounds().bottom+1;
  AddChild(helloview = new HelloView(r));

  Register(true);
  Show();
}

The menu bar is now added using the full bounds rectangle of the window. This is safe to do because, as described in the Be Book, the BMenuBar constructor resizes the height of a menu bar to the correct value. Then we add all the menus to the menu bar. Once that's done, the drawing view is created. The frame rectangle of the drawing view is determined by taking the bounds rectangle of the window, and setting the top edge of that rectangle to one greater than the bottom edge of the menu bar's bounds rectangle—this results in a view that fills the remainder of the window.

We've also added a call to the new Register() function (which we'll look at later). This registers the new window's existence with the application, and causes the application to assign the new window a number.

Note also that we've changed the title of the HelloWindow to "Untitled ". This isn't important, but it seemed like a good idea.

There are three basic classes in the BeOS messaging system. These are:

When a message is sent, the application server delivers it to a BHandler by calling that BHandler's MessageReceived() function. The message is passed into MessageReceived(), which interprets the message and acts upon it as necessary. The BHandler may choose to reply to the message by calling the message's SendReply() function, but this isn't necessary.

The BView class is derived from BHandler; a view can receive and respond to messages. That's how user commands are received—messages indicating mouse and keyboard activity are delivered to the view, which interprets and acts upon them.

If you flip through the Be Book for a while, you'll notice that BLooper is derived from BHandler. BLooper is a handy class that establishes a thread that runs a message loop repeatedly until the loop's Quit() function is called. But the really good stuff (at least, as far as we're concerned today) happens when you look at some of the classes derived from BLooper, such as BApplication and BWindow.

A BApplication object is instantiated, then you call its Run() function. Once you've done that, it doesn't return until its Quit() function is called. Its MessageReceived() function, which is inherited from BHandler, processes incoming messages.

Likewise, a BWindow loops, handling incoming messages in its MessageReceived() function, until its Quit() function is called. The only difference is that you don't call a Run() function; this is done implicitly for you. It's still a BLooper, and it can still receive messages, just like anything derived from BHandler.

So now we know that BApplication and BWindow are both capable of handling messages (in fact, we saw this last time, when we added code to our HelloWindow class to process incoming messages indicating that the menu bar was being used).

Let's add the registry code to the HelloApp class. We'll need to add the MessageReceived() function, as well as two new private variables:

class HelloApp : public BApplication {
  public:
    HelloApp();
    virtual void MessageReceived(BMessage *message);

  private:
    int32 window_count;
    int32 next_untitled_number;
};

The window_count variable keep tracks of how many windows our application has open (there's actually a BApplication function that does this, but we're going to keep track of it ourselves for educational purposes). next_untitled_number tracks the number to use when naming a new untitled window (such as "Untitled 42"). This number always increases, so we'll never see the same number twice.

The HelloApp constructor needs to be updated to initialize these variables:

HelloApp::HelloApp()
        : BApplication(APP_SIGNATURE) {
  BRect windowRect;
  windowRect.Set(50,50,349,399);

  window_count = 0;      // No windows yet
  next_untitled_number = 1;    // Next window is "Untitled 1"
  new HelloWindow(windowRect);
}

When the application is first run, there aren't any open windows, and the first window is called "Untitled 1".

The window registry—the mechanism by which the application counts and names windows—is handled entirely by accepting and responding to two application-defined messages:

constuint32 WINDOW_REGISTRY_ADD    = 'WRad';
constuint32 WINDOW_REGISTRY_SUB    = 'WRsb';

WINDOW_REGISTRY_ADD messages will be sent to the HelloApp object when a new window is opened and needs to be added to the registry. WINDOW_REGISTRY_SUB messages will be sent to remove a window from the registry as it's being closed.

The HelloApp needs to be able to reply to WINDOW_REGISTRY_ADD messages so that it can tell the window what number to use when giving itself a new "Untitled" name. So we need to define a command code for this reply message:

constuint32 WINDOW_REGISTRY_ADDED = 'WRdd';

Now let's look at HelloApp's MessageReceived() function. This will be called by the application server whenever a message is delivered to the HelloApp object:

void HelloApp::MessageReceived(BMessage *message) {
  switch(message->what) {

The function begins by looking at the BMessage's command code. This 32-bit value is located in the public what variable in the BMessage class, and specifies what type of message has been received. If it's a WINDOW_REGISTRY_ADD message, we do the following:

    case WINDOW_REGISTRY_ADD:
      {
        bool need_id = false;

        if (message->FindBool("need_id", &need_id) == B_OK) {
          if (need_id) {
            BMessage reply(WINDOW_REGISTRY_ADDED);
            reply.AddInt32("new_window_number",
                           next_untitled_number);
            message->SendReply(&reply);
            next_untitled_number++;
          }
          window_count++;
        }
        break;
      }

Before handing the WINDOW_REGISTRY_ADD function, we look at the need_id field of the message. If it's true, the window needs to be assigned an ID number (since the window is untitled). If need_id is false, the window doesn't need an ID. This will be used when we add the ability to open existing documents, which will already have names, and won't need to have an "Untitled" name.

If need_id is false, a new BMessage, called reply, is created, with the command code WINDOW_REGISTRY_ADDED. This is the message we'll send back to the HelloWindow object as a reply once we've processed the WINDOW_REGISTRY_ADD message.

We then add a new field to the reply message by calling AddInt32(). The field is given the name new_window_number and its value is set to the value of next_untitled_number. We could add as many named fields as we want to this message, but for now, this is all we need.

Then the reply is sent by calling the received BMessage's SendReply() function. This directs the reply message to the appropriate BHandler object (in this case, the HelloWindow that sent the WINDOW_REGISTRY_ADD message).

Finally, the next_untitled_number variable is incremented, so that the next new window's number will be one greater than the previous window's.

The window count is then incremented (whether a reply was sent or not), since there's a new window registered. We always want to count up every new window, even if it's not untitled.

    case WINDOW_REGISTRY_SUB:
      window_count--;
      if (!window_count) {
        Quit();
      }
      break;

If the message is a WINDOW_REGISTRY_SUB message, the window count is decremented, thereby indicating that one less window is open. If the count becomes zero, the application's Quit() function is called. This causes the application to terminate when there aren't any open windows left.

    default:
      BApplication::MessageReceived(message);
      break;
  }
}

All other messages are routed back to the inherited BApplication MessageReceived() function.

Now let's look at the changes needed to the HelloWindow class:

class HelloWindow : public BWindow {
  public:
    HelloWindow(BRect frame);
    ~HelloWindow();
    virtual bool  QuitRequested();
    virtual void  MessageReceived(BMessage *message);

  private:
    void  Register(void);
    void  Unregister(void);

    BMenuBar   *menubar;
    HelloView  *helloview;
};

The updated HelloWindow class needs a customized destructor, so that the window can be automatically unregistered whenever it's closed. Two private functions are added as well: Register(), which registers the window with the application, and Unregister(), which unregisters the window.

The destructor is really simple—it just calls the Unregister() function:

HelloWindow::~HelloWindow() {
  Unregister();
}

The Register() function sends a WINDOW_REGISTRY_ADD message to the HelloApp object. To accomplish this, we need to create a BMessenger object to deliver messages to the application. This is done by passing the application's signature to the BMessenger constructor; this creates a BMessenger that delivers messages to the HelloApp.

The WINDOW_REGISTRY_ADD message is created on the stack, and the need_id argument is added to it by calling AddBool(). The message is then sent to the application by calling BMessenger::SendMessage().

SendMessage() lets you specify the handler to which replies should be directed. Since we want replies sent to the HelloWindow object, we specify the object (this) as the reply handler; by default, replies are sent to the BApplication object, which isn't what we want.

void HelloWindow::Register(bool need_id) {
  BMessenger messenger(APP_SIGNATURE);
  BMessage   message(WINDOW_REGISTRY_ADD);

  message.AddBool("need_id", need_id);
  messenger.SendMessage(&message, this);
}

The Unregister() function is nearly identical, except that it sends a WINDOW_REGISTRY_SUB message, and, since no replies are expected, it doesn't specify a reply handler.

void HelloWindow::Unregister(void) {
  BMessenger messenger(APP_SIGNATURE);
  messenger.SendMessage(new BMessage(WINDOW_REGISTRY_SUB));
}

HelloWindow's MessageReceived() function needs to be augmented a bit. We're going to implement the New, Close, and Quit options in the File menu, and we need to handle the WINDOW_REGISTRY_ADDED message (as you recall, this is the reply sent by the HelloApp object when a window is successfully registered).

Here are the constants for the message codes we're adding support for:

constuint32 MENU_FILE_NEW    = 'MFnw';
constuint32 MENU_FILE_CLOSE  = 'MFcl';
constuint32 MENU_FILE_QUIT   = 'MFqu';

The code is as follows:

void HelloWindow::MessageReceived(BMessage *message) {
  switch(message->what) {
    case WINDOW_REGISTRY_ADDED:
      {
        char s[22];
        int32 id = 0;
        if (message->FindInt32("new_window_number",
                               &id) == B_OK) {
          sprintf(s, "Untitled %ld", id);
          SetTitle(s);
        }
      }
      break;

When a WINDOW_REGISTRY_ADDED message is received, we extract the window's number from the message's new_window_number field by calling FindInt32(). This value is passed to the ANSI C sprintf() function to create a string to be used as the window's title, such as "Untitled 1" or "Untitled 42". That string is then passed to the window's SetTitle() function to change the window's title. This ensures that every window has a unique name.

    case MENU_FILE_NEW:
      {
        BRect r;
        r = Frame();
        r.OffsetBy(20,20);
        new HelloWindow(r);
      }
      break;

The File menu's New option creates a new HelloWindow, whose frame rectangle is the same as the current window's, but offset by 20 pixels down and to the left. This staggering effect helps keep the windows orderly on the screen (although in real life you want to be sure the windows don't eventually creep off the bottom or right edge of the screen!).

    case MENU_FILE_CLOSE:
      Quit();
      break;

The Close option simply calls the HelloWindow::Quit() function to close the window. Because the HelloWindow destructor calls Unregister(), the window will be unregistered from the application (thereby reducing the window count).

    case MENU_FILE_QUIT:
      be_app->PostMessage(B_QUIT_REQUESTED);
      break;

The Quit option posts a B_QUIT_REQUESTED message to the application object. This causes the application to close all the windows and terminate itself.

    case MENU_OPT_HELLO:
      /* this is the same as last time */
      break;

    default:
      BWindow::MessageReceived(message);
      break;
  }
}

The rest of the code in HelloWindow::MessageReceived() is unchanged since last time.

Finally, because we don't want closing the window to automatically quit the application, we rewrite the HelloWindow::QuitRequested() function to simply return true—it's always okay to close the window. We no longer want to request that the application quit as well:

bool HelloWindow::QuitRequested() {
  return true;
}

This has been a very brief overview of messaging on the BeOS. Because messaging is so pervasive throughout the entire operating system, we'll learn more about it as we go on. But this will give you the background needed to follow along as we continue to explore. Try adding code to insert more data into the registration messages, or make the WINDOW_REGISTRY_SUB message send a reply.

Although this sample project's messages are mostly transmitted within the application, there's no reason you can't write two applications and send messages back and forth between them (try this—just remember to use the appropriate signature when creating a BMessenger object for sending messages to the other application).

You can download the source code for this week's project >from the Be FTP site: ftp://ftp.be.com/pub/samples/intro/messageworld.zip

In about six weeks, we'll try making the HelloWindow class do something more useful than simply display a string in a window. We'll look at the BTextView class and start down the road toward turning our sample application into a text editor.


StarCode

By Jean-Louis Gassée

Last week, we executed the closing documents to finalize Be's acquisition of StarCode. It is a great pleasure to welcome Carlin Wiegner and his team into Be. Their pioneering spirit and their achievements have been an inspiration and, in more ways than one, that's precisely why the deal got done.

It shouldn't come as news for those (few) who read the original Be business plan that we believe in electronic distribution of software. A section of that plan explains how we'd create a dedicated Be BBS, knitting together the community of Be developers, Be customers, and the company itself. This was written in 1991, when you could buy a few phone lines, an inexpensive PC, and software from Mustang or Galacticom. With a Mac you needed a really nice product aptly called FirstClass, and you were in business. You had your own little Compuserve, your petit MoiOnLine.

There were over 10,000 BBS in the country. The reference magazine for the BBS trade was Boardwatch—still one of the best reads, nicely converted to the Internet under its erudite and opinionated editor-in-chief Jack Rickard http://www.boardwatch.com/.

These BBS systems were, ahem, scalable. The largest one linked over 100 PCs via a Novell network—very messy, but very inexpensive.

McAfee, then reigning king of anti-virus software, used such a contraption for electronic delivery of its software; as a result, its IPO prospectus listed as a major risk the fact only one individual knew how to run the thing.

So, we were going to build such an electronic community, alert our customers every week to new and improved application, utility, game, or even system software available. Developers would advertise their wares on our BBS. The weekly Newsletter would ask politely if we could deposit demo software and updates on their disk.

The word "push" hadn't been invented yet. If customers liked the sampleware, they could call the developer, order with a credit card, and receive the software electronically or via snail mail. Not very sophisticated but, at the time, it was judged a little futuristic and risky. Customers might balk at the idea of having software loaded on their machines without having full control over the process. Cookies hadn't been invented either.

Jump to 1996. Carlin works at Be as a summer intern and starts a company called StarCode, a site called BeDepot, and a product called Software Valet. BeDepot sells and delivers software over the Web. The Web is a still-evolving contraption, but a ubiquitous and inexpensive one. Software Valet sits in your BeOS system, where it downloads, installs, and registers software; it also notifies you of updates. StarCode practiced what we preached.

Two more factors made StarCode attractive to Be: the people and the e-commerce infrastructure. With this in mind, and wanting to make life easy for customers and profitable for developers, the make vs. buy analysis was fairly straightforward. Carlin, his team, and their technology provide us with a better and faster way to build a BeOS software community and, therefore, the Be platform.

Mere implementation details remain...which is a way to say we have much to do to realize the potential this reunion represents, but at least we are started on the right path.

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