Laying It All Out, Part 1

Figure 1. Oops, the programmer didn't expect someone to change to size 18 font!

Besides font sensitivity it is also tedious to have to explicitly design a GUI with precise locations and sizes for each component. It makes adding new components or moving things around more difficult than it should be.

The solution to these problems is something called a layout manager. What a layout manager does is manage laying out all the components of your application based on various criteria such as the type of layout as well as the minimum, preferred and maximum sizes of the GUI components. If the user resizes the window or changes the font, the layout manager gracefully resizes and realigns all the components under its management.

As implied in the first paragraph, BeOS never had a built-in layout management system. Several third parties did come to the rescue, such as Marco Nelissen with his award winning liblayout, as well as Brian Tietz in his Santa's Gift Bag library and Angry Red Planet in their ARPCommon library. Of course while these systems all achieved the same basic goal, their implementation and usage was quite different. "Porting" an application from one layout system to the other would be fairly difficult.

Fortunately for us, Ingo Weinhold came to the rescue by committing a layout management system into the Haiku source tree back in August 2006. It has been tweaked quite a bit since then but so far has not been put to much use. I figure that is because not many know about it. Ingo is a busy man, so I decided to learn the layout system and write these articles.

This first article will describe the layout system from a higher level, and then in one or more future articles I will go into more detail.

First things first, Ingo fully admits that much of the design of this layout system was inspired by the QLayout and friends in the Qt library. So it might be helpful to read about that too, though I hope to tell you everything you need to know to work with Haiku's classes eventually.

Types of Layout Managers

I will start by describing the various types of layout managers that are currently included in Haiku and how they layout the views under their control:

  • BCardLayout: the contained views are layed out like a deck of cards, one on top of the other and all in the same area, and only one can show at a time. This would be useful in implementing a tab-based interface for showing multiple documents in one window, or for settings windows with multiple sections (like in the settings for BeIDE or FireFox.)
  • BGridLayout: the contained views are layed out in a grid, with each view getting the same general amount of space (though there are options to allow a view to span multiple rows and/or columns.) This would be useful in implementing a thumbnailing application for example, with each thumbnail getting it's own equal sized space.
  • BGroupLayout: the contained views are layed out in a group, either horizontally or vertically, depending on what orientation is wanted for the BGroupLayout. When used in combination (multiple horizontal BGroupLayouts contained in one vertical BGroupLayout for example) this is the most flexible and useful of the layouts. I imagine many future Haiku applications will make much use of this layout class.
  • BSplitLayout: this acts much like a BGroupLayout, except each view is separated by a splitter view that allows its contained area to be resized. Also optionally the area can be resized such that the view disappears completely, something called collapsing. You won't ever actually use this layout directly, but only through the BSplitView class.
  • BALMLayout: the newest kid on the block, this was added in February 2008 from code created by Christof Lutteroth and James Kim. ALM stands for Auckland Layout Model, which is a new kind of layout invented by Christof at The University of Auckland. More information about ALM can be read on its SourceForge project page. I suspect I will dig into this layout more in the future and may dedicate an entire article to it. But the previous link provides a lot of information about the concepts behind ALM.

How To Build a GUI Using a Layout

Figure 2. The MidiPlayer at default (size 12) and size 18 fonts. Even the size 12 version could benefit from the layout system.

To build a Haiku GUI that uses the layout system, you first need to figure out what kind of layout manager or managers you might need. For an existing GUI it is probably fairly obvious from what you can see on the screen. For a new GUI it would probably be helpful to draw it out, though this would probably be done even if there was no layout system involved.

Once you know the kind of layouts you plan to use, you need to make sure all the BViews you are using have the needed methods to be used in the layout system. All of the Haiku views and controls should already be compatible with the layout system (and if they are not, log a bug.) For custom BViews some extra work is required, which I will explore in a future article. For the purposes of this article I will only use views that are already well-supported by the layout system.

For the purposes of an example I could put together a simple example GUI, but I think it will be more instructive to show an actual GUI that already exists in Haiku that is not font sensitive. The E-mail Preferences applet shown above is one example, but I don't want the complication of its GUI to interfere with showing how the layout system works. Luckily I have another good option in the MidiPlayer app. See Figure 2.

So based on the GUI in Figure 2, what layout managers will we need? Well the first thing I see is that there are essentially four sections arranged vertically:

  1. The "Drop MIDI file here" control.
  2. The labels and controls in the middle.
  3. A separator.
  4. The Play button.

So this means that the highest-level layout manager should be a vertical BGroupLayout. The set of labels and controls seems to be arranged in a grid, so for that section we can have another layout manager, a BGridLayout. But before I start showing the code for creating this layout, let's look at the original GUI creation code. It is contained within MidiPlayerWindow.cpp in the CreateViews() method. It also makes use of a few macros. The relevant code (with line numbers) as of r26407 is show below:

 32 #define _W(a) (a->Frame().Width())
 33 #define _H(a) (a->Frame().Height())

...

240 void MidiPlayerWindow::CreateViews()
241 {
242     scopeView = new ScopeView;
243 
244     showScope = new BCheckBox(
245         BRect(0, 0, 1, 1), "showScope", "Scope",
246         new BMessage(MSG_SHOW_SCOPE), B_FOLLOW_LEFT);
247 
248     showScope->SetValue(B_CONTROL_ON);
249     showScope->ResizeToPreferred();
250 
251     CreateInputMenu();
252     CreateReverbMenu();
253 
254     volumeSlider = new BSlider(
255         BRect(0, 0, 1, 1), "volumeSlider", NULL, NULL,
256         0, 100, B_TRIANGLE_THUMB);
257 
258     rgb_color col = { 152, 152, 255 };
259     volumeSlider->UseFillColor(true, &col);
260     volumeSlider->SetModificationMessage(new BMessage(MSG_VOLUME));
261     volumeSlider->ResizeToPreferred();
262     volumeSlider->ResizeTo(_W(scopeView) - 42, _H(volumeSlider));
263 
264     playButton = new BButton(
265         BRect(0, 1, 80, 1), "playButton", "Play", new BMessage(MSG_PLAY_STOP),
266         B_FOLLOW_RIGHT);
267 
268     //playButton->MakeDefault(true);
269     playButton->ResizeToPreferred();
270     playButton->SetEnabled(false);
271 
272     BBox* background = new BBox(
273         BRect(0, 0, 1, 1), B_EMPTY_STRING, B_FOLLOW_ALL_SIDES,
274         B_WILL_DRAW | B_FRAME_EVENTS | B_NAVIGABLE_JUMP,
275         B_PLAIN_BORDER);
276 
277     BBox* divider = new BBox(
278         BRect(0, 0, 1, 1), B_EMPTY_STRING, B_FOLLOW_ALL_SIDES,
279         B_WILL_DRAW | B_FRAME_EVENTS, B_FANCY_BORDER);
280 
281     divider->ResizeTo(_W(scopeView), 1);
282 
283     BStringView* volumeLabel = new BStringView(
284                     BRect(0, 0, 1, 1), NULL, "Volume:");
285 
286     volumeLabel->ResizeToPreferred();
287 
288     float width = 8 + _W(scopeView) + 8;
289 
290     float height =
291           8  + _H(scopeView)
292         + 8  + _H(showScope)
293         + 4  + _H(inputMenu)
294              + _H(reverbMenu)
295         + 2  + _H(volumeSlider)
296         + 10 + _H(divider)
297         + 6  + _H(playButton)
298         + 16;
299 
300     ResizeTo(width, height);
301 
302     AddChild(background);
303     background->ResizeTo(width, height);
304     background->AddChild(scopeView);
305     background->AddChild(showScope);
306     background->AddChild(reverbMenu);
307     background->AddChild(inputMenu);
308     background->AddChild(volumeLabel);
309     background->AddChild(volumeSlider);
310     background->AddChild(divider);
311     background->AddChild(playButton);
312 
313     float y = 8;
314     scopeView->MoveTo(8, y);
315 
316     y += _H(scopeView) + 8;
317     showScope->MoveTo(8 + 55, y);
318 
319     y += _H(showScope) + 4;
320     inputMenu->MoveTo(8, y);
321 
322     y += _H(inputMenu);
323     reverbMenu->MoveTo(8, y);
324 
325     y += _H(reverbMenu) + 2;
326     volumeLabel->MoveTo(8, y);
327     volumeSlider->MoveTo(8 + 49, y);
328 
329     y += _H(volumeSlider) + 10;
330     divider->MoveTo(8, y);
331 
332     y += _H(divider) + 6;
333     playButton->MoveTo((width - _W(playButton)) / 2, y);
334 }

I am not going to walk through the code, but the main thing I want noticed is all the hard-coded numbers used in the manual layout, and all the work required to get things aligned. Another benefit to the Haiku layout system is cleaner code, and I think this example should prove that well. In fact I think I will let the code speak for itself. Here is the above CreateViews() method rewritten to use the layout system:

246 void MidiPlayerWindow::CreateViews()
247 {
248     // Set up needed views
249     scopeView = new ScopeView;
250 
251     showScope = new BCheckBox(
252         BRect(0, 0, 1, 1), "showScope", "Scope",
253         new BMessage(MSG_SHOW_SCOPE), B_FOLLOW_LEFT);
254     showScope->SetValue(B_CONTROL_ON);
255 
256     CreateInputMenu();
257     CreateReverbMenu();
258 
259     volumeSlider = new BSlider(
260         BRect(0, 0, 1, 1), "volumeSlider", NULL, NULL,
261         0, 100, B_TRIANGLE_THUMB);
262     rgb_color col = { 152, 152, 255 };
263     volumeSlider->UseFillColor(true, &col);
264     volumeSlider->SetModificationMessage(new BMessage(MSG_VOLUME));
265 
266     playButton = new BButton(
267         BRect(0, 1, 80, 1), "playButton", "Play", new BMessage(MSG_PLAY_STOP),
268         B_FOLLOW_RIGHT);
269     playButton->SetEnabled(false);
270 
271     BBox* divider = new BBox(
272         BRect(0, 0, 1, 1), B_EMPTY_STRING, B_FOLLOW_ALL_SIDES,
273         B_WILL_DRAW | B_FRAME_EVENTS, B_FANCY_BORDER);
274     divider->SetExplicitMaxSize(
275         BSize(B_SIZE_UNLIMITED, 1));
276 
277     BStringView* volumeLabel = new BStringView(
278                     BRect(0, 0, 1, 1), NULL, "Volume:");
279     volumeLabel->SetAlignment(B_ALIGN_LEFT);
280     volumeLabel->SetExplicitMaxSize(
281         BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
282 
283     // Build the layout
284     SetLayout(new BGroupLayout(B_HORIZONTAL));
285 
286     AddChild(BGroupLayoutBuilder(B_VERTICAL, 10)
287         .Add(scopeView)
288         .Add(BGridLayoutBuilder(10, 10)
289             .Add(BSpaceLayoutItem::CreateGlue(), 0, 0)
290             .Add(showScope, 1, 0)
291 
292             .Add(reverbMenu->CreateLabelLayoutItem(), 0, 1)
293             .Add(reverbMenu->CreateMenuBarLayoutItem(), 1, 1)
294 
295             .Add(inputMenu->CreateLabelLayoutItem(), 0, 2)
296             .Add(inputMenu->CreateMenuBarLayoutItem(), 1, 2)
297 
298             .Add(volumeLabel, 0, 3)
299             .Add(volumeSlider, 1, 3)
300         )
301         .AddGlue()
302         .Add(divider)
303         .AddGlue()
304         .Add(playButton)
305         .AddGlue()
306         .SetInsets(5, 5, 5, 5)
307     );
308 }

The only significant changes in the first section of the method where the views are created is the removal of the various manual layout related method calls, like playButton->ResizeToPreferred();, which was originally on line 269. There have been a few additions to give the layout system some hints on how we want things laid out. For example an unlimited width and max height of 1 is set up for the divider BBox, so it stays looking like a divider. In addition the volumeLabel is set up so it aligns left and fills up all the space available. This keeps it aligned with the labels for the BMenuFields above it.

Next we have the real "meat" of the layout code which starts on line 284. The first thing I do is set a base layout. In this case I have decided to use a sort of "dummy" horizontal group layout so that I can later use the group layout builder to create the vertical layout I really want. It seems to be a quirk of the layout system that you have to have a layout manager set up in the window before you start adding other views that are managed by the layout system. I probably could have created the main vertical group layout outside of the builder and made that the root layout, but I don't think the code would have looked as nice.

Speaking of the layout builders, they were created specifically for uses like the above, with each method returning an instance of the builder allowing essentially unlimited method chaining. With the right indentation it can look quite nice and becomes a good represention of how the GUI is structured. Hence why I like to use them. Currently there is a GridLayoutBuilder, GroupLayoutBuilder, and SplitLayoutBuilder.

Figure 3. The new layout-based MidiPlayer GUI at default (size 12) and size 18 fonts.

On line 286 we have an AddChild() call with the call to the layout builders inside of it. The first argument for the BGroupLayoutBuilder is the orientation of the group layout and the second argument is the spacing used between the layout items. This gives the views contained within the layout a little breathing room so they don't look all crammed together. Once we have the group layout builder we can start adding views to it with the Add() method, and the first thing added is the scope view. Then we have the middle section which we want managed by a grid layout, hence the call to the grid layout builder. The arguments to BGridLayoutBuilder are also for spacing, in this case the horizontal and vertical spacing, respectively.

On line 289 we start adding views to the grid layout by adding something called glue. What is glue? It is basically a filler that takes up space in the layout so that the views we care about are aligned properly. In this case I want the showScope checkbox aligned with the menus of the menu fields below it. More glue is used below in the vertical group layout to provide some space between the grid layout, the divider, the play button and the bottom of the window. This keeps things looking nice and also helps us maintain the look of the original MidiPlayer GUI.

The other arguments to the Add() method for the BGridLayoutBuilder is the column and row we want the given view to appear in. So from this it can be seen that the glue is in column 0, row 0, the showScope checkbox is in column 1, row 0, the reverb menu label is in column 0, row 1, etc. Speaking of the reverb menu item, we can see that there are special methods in the BMenuField class to produce separate layout items for the one view. This is an interesting aspect of the layout system in that views that consist of different components can actually have their separate sections managed individually by the layout system. For another example of this look at the code for the ActivityMonitor, which uses this technique for the graph and legend of the main activity view.

The final call to the group layout builder is SetInsets(), which basically is used to put some padding around the edge of the layout, in the order of left, top, right, and bottom. Five pixels seems to be a pretty standard choice for the insets. I could have used 8 pixels which would have been like what the original GUI had, but I think 5 is fine.

There were a few other code changes needed to get the final GUI shown in Figure 3. First the needed headers were added. Second the constructor for the MidiPlayer window needed the B_AUTO_UPDATE_SIZE_LIMITS so that it gets resized automatically by the layout system:

 41 MidiPlayerWindow::MidiPlayerWindow()
 42     : BWindow(BRect(0, 0, 1, 1), "MidiPlayer", B_TITLED_WINDOW,
 43               B_ASYNCHRONOUS_CONTROLS | B_NOT_RESIZABLE | B_NOT_ZOOMABLE | B_AUTO_UPDATE_SIZE_LIMITS)

Third the methods used to build the menu fields needed to be changed to use the simple layout system friendly contructor. Also any manually layout code was removed. So for example the CreateReverbMenu() was changed so that this:

230     reverbMenu = new BMenuField(
231         BRect(0, 0, 128, 17), "reverbMenu", "Reverb:", reverbPopUp,
232         B_FOLLOW_LEFT | B_FOLLOW_TOP);
233 
234     reverbMenu->SetDivider(55);
235     reverbMenu->ResizeToPreferred();

became this:

235     reverbMenu = new BMenuField("Reverb:", reverbPopUp, NULL);

A similar change was made for the CreateInputMenu() method.

Finally the constructor for the ScopeView was changed to make it a bit wider to accomodate a size 18 font. This was the "cheap" way to do it but in this case it works fine. The "proper" way would be to add the methods needed by the layout system to tell the layout system what the minimum, preferred and maximum sizes would be for that view. For this article I did not want to cover that but will probably go into it in a future article on the layout system.

To see all of this updated code take a look at the MidiPlayer sources which are in the Haiku repository at src/apps/midiplayer. To see the previous code go back to before revision 26408 when I submitted these changes.

Conclusion

To get more ideas and tips on how to use the layout system until more documentation is written take a look at Ingo's test application for the layout system, called LayoutTest1. The source for this is at tests/kits/interface/layout/LayoutTest1.cpp and it can be tested in Haiku by adding the following line to your UserBuildConfig file:

AddFilesToHaikuImage home config bin : LayoutTest1 ;

It can then be run either from Tracker from /boot/home/config/bin or by running it from Terminal. The code also has various examples of how to build a GUI with the layout system, sometimes using builders and sometimes not, depending on what is appropriate.

In addition the code for all the layout classes is in the Haiku Interface Kit, so for those of you who really want to dig into it, take a look at the Interface Kit headers in headers/os/interface and the implementation in src/kits/interface and src/kits/interface/layouter.

Finally a warning: because these classes are new and untested from the perspective of an API, they are considered a private API and for now should only be used inside Haiku core code, such as the included applications or preference applets. This is because the API might change or need to be tweaked and we would not want to break other people's code (we can fix broken code inside the Haiku repository ourselves.) So just keep that in mind and use the layout system classes at the risk of having your code broken.