Haiku API Bindings
Menus
Not logged in

Documentation | Manual: Menus

Chapter 4: Menus

In this chapter, we are going to look at Haiku's various Menu classes.

This program for this chapter is called Menus.ext.

Imports

Like in the previous chapter, we begin by importing the modules we need.

Perl

# Window will pull in InterfaceKit and ApplicationKit and all their
# classes; only constants need to be imported into the package, not
# class names
use HaikuR1::Window qw(:window_type :window_flags);

Python

from HaikuR1.InterfaceKit import Window, View, Menu, MenuItem, MenuBar, MenuField, PopUpMenu
from HaikuR1.InterfaceKit import B_TITLED_WINDOW, B_QUIT_ON_WINDOW_CLOSE, B_SECONDARY_MOUSE_BUTTON, B_CONTROL_KEY, B_FOLLOW_ALL, B_FOLLOW_BOTTOM, B_FOLLOW_LEFT_RIGHT, B_WILL_DRAW
from HaikuR1.SupportKit import code_to_int

ColorView

We are going to show PopUpMenu by responding to a right click. Unfortunately for our purposes, Window's DispatchMessage reroutes click messages before they ever get to MessageReceived, so we have two choices: override Window's DispatchMessage hook, or override the View hook the message gets rerouted to.

The first way means we have to examine every single message that passes through the Window, so we will go with the second way instead. The View hook we need to override is MouseDown.

The PopUpMenu class is designed to be used once, and the system expects you to delete it once Go has returned. The glue code will take care of deleting the underlying C++ object once the native object goes out of scope, but we still need to create an entirely new menu for each right click.

Perl

package ColorView;
use parent 'HaikuR1::View';

# bring in B_SECONDARY_MOUSE_BUTTON constant
use HaikuR1::View qw(B_SECONDARY_MOUSE_BUTTON);

sub MouseDown {
    my ($self, $point) = @_;

    # hook doesn't give us buttons, so use CurrentMessage to get that info
    my $window = $self->Window();
    my $message = $window->CurrentMessage();
    my $button = $message->FindInt32("buttons");
    return unless $button == B_SECONDARY_MOUSE_BUTTON;

    my $popup = HaikuR1::PopUpMenu->new(
        name => 'ColorPopUp'
    );
    MenuWindow::populate_menu($popup);
    $popup->SetTargetForItems($window);
    $popup->Go(
        where           => $self->ConvertToScreen($point),
        deliversMessage => 1,
    );
}

Python

class ColorView(View):

    # don't need B_SECONDARY_MOUSE_BUTTON here because we pulled it in
    # file-globally above

    def MouseDown(self, point):
        # hook doesn't give us buttons, so use CurrentMessage to get that info
        window = self.Window()
        message = window.CurrentMessage()
        button = message.FindInt32("buttons")
        if button != B_SECONDARY_MOUSE_BUTTON:
            return

        popup = PopUpMenu(
            name = 'ColorPopUp'
        )
        MenuWindow.populate_menu(popup)
        popup.SetTargetForItems(window)
        popup.Go(
            where           = self.ConvertToScreen(point),
            deliversMessage = 1,
        )

(Note that in the View subclass we use a Perl class method / Python static method populate_menu, which we will define below in the Window suclass.)

MenuWindow

Now we define our Window subclass. First we create some variables that will be used to control the size of our views.

Perl

package MenuWindow;
use parent 'HaikuR1::Window';
use HaikuR1::SupportKit qw(code_to_int);
use HaikuR1::InterfaceKit qw(B_CONTROL_KEY);
use HaikuR1::View qw(B_FOLLOW_ALL B_FOLLOW_BOTTOM B_FOLLOW_LEFT_RIGHT B_WILL_DRAW);
use strict;

my $spacer = 5;
my $view_width = 150;
my $view_height = 150;
my $control_height = 25;

Python

class MenuWindow(Window):

    spacer = 5
    view_width = 150
    view_height = 150
    control_height = 25

This tutorial demonstrates the use of menus by changing the color of a view. So we need to define a message constant that tells the window to change the view color, as well as the options for the view color.

Perl

use constant SET_COLOR => code_to_int('colr');

my @colors = (
    [ 'Red'   => 255,   0,   0 ],
    [ 'Green' =>  0, 255,   0 ],
    [ 'Blue'  =>  0,   0, 255 ],
    [ 'Black' =>  0,   0,   0 ],
);

Python

    SET_COLOR = code_to_int('colr')

    colors = [
        [ 'Red',  255,   0,   0 ],
        [ 'Green',  0, 255,   0 ],
        [ 'Blue',   0,   0, 255 ],
        [ 'Black',  0,   0,   0 ],
    ]

Here we define our populate_menu method, which allows us to create an identical submenu for each of the three menu types we are demonstrating.

Perl

sub populate_menu {
    my ($menu) = @_;

    for my $color (@colors) {
        my ($name, @rgb) = @$color;
        my $msg = HaikuR1::Message->new(SET_COLOR);
        $msg->AddColor("color", \@rgb);
        my $item = HaikuR1::MenuItem->new(
            label     => $name,
            message   => $msg,
        );
        $menu->AddItem($item);
    }
}

Python

    @staticmethod
    def populate_menu(menu):
        for color in MenuWindow.colors:
            name = color[0]
            rgb = color[1:4]
            msg = Message(MenuWindow.SET_COLOR)
            msg.AddColor("color", rgb)
            item = MenuItem(
                label   = name,
                message = msg,
            )
            menu.AddItem(item)

Now, here in our contructor (Perl) or initializer (Python), we create the views this Window subclass uses.

When there is only one MenuBar in a Window, it automatically becomes the main MenuBar, which is called the key MenuBar in Haiku. When the key MenuBar has the resizing mode B_FOLLOW_LEFT_RIGHT (and allowin fixedSize to remain at the default of false), it will resize itself to just the height needed for its items, and to stretch across the entire width of the Window.

(Even though we have not yet added any items to the menu bar, it knows, based on font size and internal margins, how high it needs to be to fit a single row of items.)

Perl

sub new {
    my $class = shift;
    my $self = $class->SUPER::new(@_);

    my $menubar = HaikuR1::MenuBar->new(
        frame => [0,0,20,1],
        name  => 'MenuBar',
        resizingMode => B_FOLLOW_LEFT_RIGHT,
    );
    $self->AddChild($menubar);

Python

    def __init__(self, *args, **kwargs):
        menubar = MenuBar(
            frame = [0,0,20,1],
            name  = 'MenuBar',
            resizingMode = B_FOLLOW_LEFT_RIGHT,
        )
        self.AddChild(menubar)

Now that it is attached to the Window, we can get the recalculated height of the MenuBar and use that, along with the values we set earlier, to determine the necessary size for the Window.

Perl

    my $menubar_height = $menubar->Frame()->Height();
    my $window_width = $view_width + 2*$spacer;
    my $window_height = $menubar_height + $view_height + $control_height + 3*$spacer;

    $self->ResizeTo($window_width, $window_height);

Python

        menubar_height = menubar.Frame().Height() 
        window_width = MenuWindow.view_width + 2*MenuWindow.spacer
        window_height = menubar_height + MenuWindow.view_height + MenuWindow.control_height + 3*MenuWindow.spacer

        self.ResizeTo(window_width, window_height)

Next we create and populate the submenu for the main menu bar, then add it. (Note that Menu.AddItem can take either a MenuItem or a Menu.

Perl

    my $menu1 = HaikuR1::Menu->new("Colors");
    populate_menu($menu1);
    my $i = 0;
    for (qw(R G B K)) {
        my $item = $menu1->ItemAt($i++);
        $item->SetShortcut($_, B_CONTROL_KEY);
    }
    $menubar->AddItem($menu1);

Python

        menu1 = Menu("Colors")
        MenuWindow.populate_menu(menu1)
        i = 0
        shortcuts = ['R', 'G', 'B', 'K']
        for shortcut in shortcuts:
            item = menu1.ItemAt(i)
            i = i+1
            item.SetShortcut(shortcut, B_CONTROL_KEY)
        menubar.AddItem(menu1)

Then we add an instance of our custom ColorView class.

Perl

    my $left = $spacer;
    my $top = $menubar_height + $spacer;

    $self->{view} = ColorView->new(
        frame => [$left,$top,$left+$view_width,$top+$view_height],
        name  => 'ColorView',
        flags => B_WILL_DRAW,
        resizingMode => B_FOLLOW_ALL,
    );
    $self->AddChild($self->{view});
    $self->{view}->SetViewColor([0,0,160]);

Python

        left = MenuWindow.spacer
        top = menubar_height + MenuWindow.spacer

        self.view = ColorView(
            frame = [left,top,left+MenuWindow.view_width,top+MenuWindow.view_height],
            name  = 'ColorView',
            flags = B_WILL_DRAW,
            resizingMode = B_FOLLOW_ALL,
        )
        self.AddChild(self.view)
        self.view.SetViewColor([0,0,160])

The last view in our Window is a MenuField. Unlike MenuBar and PopUpMenu, MenuField is not a subclass of Menu. Instead, it contains an instance of a Menu.

The Be Book recommends using a PopUpMenu instead of a regular Menu.

Perl

    $top += $view_height + $spacer;

    my $menu2 = HaikuR1::PopUpMenu->new(
        name => 'Colors',
    );
    populate_menu($menu2);
    my $menufield = HaikuR1::MenuField->new(
        frame => [$left,$top,$left+$view_width,$top+$control_height],
        name  => "MenuField",
        menu  => $menu2,
        label => 'Colors',
        resizingMode => B_FOLLOW_BOTTOM,
        fixedSize => 1,
    );
    $self->AddChild($menufield);

    return $self;
} # end of new()

Python

        top += MenuWindow.view_height + MenuWindow.spacer

        menu2 = PopUpMenu(
            name = "Colors"
        )
        MenuWindow.populate_menu(menu2)
        menufield = MenuField(
            frame = [left,top,left+MenuWindow.view_width,top+MenuWindow.control_height],
            name  = "MenuField",
            menu  = menu2,
            label = 'Colors',
            resizingMode = B_FOLLOW_BOTTOM,
            fixedSize = 1,
        )
        self.AddChild(menufield)

The final step in our Window subclass is overriding the MessageReceived hook to respond to our custom message.

Perl

sub MessageReceived {
    my ($self, $message) = @_;

    if ($message->what == SET_COLOR) {
        my $color = $message->FindColor("color");
        $self->{view}->SetViewColor($color);
        $self->{view}->Invalidate();
        return;
    }

    $self->SUPER::MessageReceived($message);
}

Python

    def MessageReceived(self, message):
        if message.what == MenuWindow.SET_COLOR:
            color = message.FindColor("color")
            self.view.SetViewColor(color)
            self.view.Invalidate()
            return

        super(MenuWindow, self).MessageReceived(message)

Running the app

Perl

package main; # leave the subclass namespace

my $app = Haiku::Application->new("application/x.vnd-hab.perl.Menus");

my $window = MenuWindow->new(
    title => "Menus",,
    frame => [50,50,100,100],
    type  => B_TITLED_WINDOW,
    flags => B_QUIT_ON_WINDOW_CLOSE,
);

$window->Show();

$app->Run();

Python

# Python does not allow multiple modules in a single file, so ending the
# MenuWindow subclass returns us to the module's main scope.

app = Application("application/x.vnd-hab.python.Menus")

window = MenuWindow(
    title = "Menus",
    frame = [50,50,100,100],
    type  = B_TITLED_WINDOW,
    flags = B_QUIT_ON_WINDOW_CLOSE,
)

window.Show()

app.Run()

One shortcoming of this class is that menus don't communicate with each other. Even when you use one of the other menus to change the color, the MenuField still retains the last color selected.

If you want some practice, try improving this program so that the other two menus are also in radio mode (like the MenuField) and that selecting an item in any menu results in it being marked in the other menus as well.