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.