Menu Bars

Menu bars are generally used to provide convenient access to major application features. They act as a repository for top-level hierarchical menus, keeping the menus out of sight until they are needed.

Like all other components, menu bars can actually be placed anywhere in an application's user interface. However, they are most often located at the top of an application's main window. Pivot provides framework-level support for simplifying the task of defining a menu bar positioned in this way. The Frame class defines a "menuBar" property that is handled specially by the application using the configureMenuBar() and cleanupMenuBar() methods of the MenuHandler interface. These methods are called by the framework as the focus changes within a window, to allow the application to customize the contents of the menu bar based on the currently focused component.

Note that for a keystroke to be processed (for example assigned to a Menu Item), a component must have the focus to receive the key event and propagate it up the component hierarchy. So, make sure that the window's content contains a focusable component, like a TextInput or PushButton.

The example application below shows a menu bar containing two common top-level menu items: "File" and "Edit" (note that the applet is signed since it makes use of the FileBrowserSheet component, which requires access to the local file system):

Each sub-menu item is associated with an Action that is executed when the item is selected. For example, the action attached to the "File > Open" menu item simulates opening a document by showing a file browser sheet and adding a new tab to the application's tab pane. Each component in the "document" has a menu handler attached to it that configures the menu contents as appropriate for the current selection. When a text input component has the focus, the "Paste" menu item is enabled. If text is selected in the text input, the "Cut" and "Copy" menu items are also enabled.

The BXML source for this example is shown below. It creates the initial menu structure as well as the tab pane that will host the simulated documents. It also defines a set of "action mappings" in the root frame's "actionMappings" sequence. Action mappings associate keystrokes with actions; when a keystroke matching an action in the sequence is processed by the window, the action is invoked. Action mappings are often called "keyboard shortcuts".

Note that the actions in this example are associated with the "CMD" key. This is a Pivot-specific, platform-independent modifier. It maps to the Control key (CTRL) on Windows and Linux and the Command key (META) on Mac OS X:

            
            <menus:MenuBars title="Menu Bars" maximized="true"
                styles="{padding:{top:0, left:4, bottom:4, right:4}, showWindowControls:false}"
                xmlns:bxml="http://pivot.apache.org/bxml"
                xmlns:content="org.apache.pivot.wtk.content"
                xmlns:menus="org.apache.pivot.tutorials.menus"
                xmlns="org.apache.pivot.wtk">
                <bxml:define>
                    <FileBrowserSheet bxml:id="fileBrowserSheet"/>
                </bxml:define>

                <actionMappings>
                    <Window.ActionMapping action="fileNew" keyStroke="CMD-N"/>
                    <Window.ActionMapping action="fileOpen" keyStroke="CMD-O"/>
                </actionMappings>

                <menuBar>
                    <MenuBar>
                        <MenuBar.Item buttonData="File">
                            <Menu>
                                <Menu.Section>
                                    <Menu.Item action="fileNew">
                                        <buttonData>
                                            <content:MenuItemData text="New" keyboardShortcut="CMD-N"/>
                                        </buttonData>
                                    </Menu.Item>

                                    <Menu.Item action="fileOpen">
                                        <buttonData>
                                            <content:MenuItemData text="Open" keyboardShortcut="CMD-O"/>
                                        </buttonData>
                                    </Menu.Item>
                                </Menu.Section>
                            </Menu>
                        </MenuBar.Item>

                        <MenuBar.Item buttonData="Edit">
                            <Menu>
                                <Menu.Section>
                                    <Menu.Item action="cut">
                                        <buttonData>
                                            <content:MenuItemData text="Cut" keyboardShortcut="CMD-X"/>
                                        </buttonData>
                                    </Menu.Item>
                                    <Menu.Item action="copy">
                                        <buttonData>
                                            <content:MenuItemData text="Copy" keyboardShortcut="CMD-C"/>
                                        </buttonData>
                                    </Menu.Item>
                                    <Menu.Item action="paste">
                                        <buttonData>
                                            <content:MenuItemData text="Paste" keyboardShortcut="CMD-V"/>
                                        </buttonData>
                                    </Menu.Item>
                                </Menu.Section>
                            </Menu>
                        </MenuBar.Item>
                    </MenuBar>
                </menuBar>

                <Border styles="{backgroundColor:null, padding:2}">
                    <TabPane bxml:id="tabPane"/>
                </Border>
            </menus:MenuBars>
            
        

The Java source for the example is shown below. In the constructor, the application's actions are created and added to the global action dictionary. Note that, since the BXML file refers to the actions by ID, it is essential that the actions be available before the BXML is read.

            
            package org.apache.pivot.tutorials.menus;

            import java.io.IOException;
            import java.net.URL;

            import org.apache.pivot.beans.BXML;
            import org.apache.pivot.beans.BXMLSerializer;
            import org.apache.pivot.beans.Bindable;
            import org.apache.pivot.collections.Map;
            import org.apache.pivot.serialization.SerializationException;
            import org.apache.pivot.util.Resources;
            import org.apache.pivot.wtk.Action;
            import org.apache.pivot.wtk.Border;
            import org.apache.pivot.wtk.Component;
            import org.apache.pivot.wtk.FileBrowserSheet;
            import org.apache.pivot.wtk.Frame;
            import org.apache.pivot.wtk.MenuBar;
            import org.apache.pivot.wtk.MenuHandler;
            import org.apache.pivot.wtk.TabPane;
            import org.apache.pivot.wtk.TextInput;
            import org.apache.pivot.wtk.TextInputSelectionListener;
            import org.apache.pivot.wtk.TextInputContentListener;

            public class MenuBars extends Frame implements Bindable {
                @BXML private FileBrowserSheet fileBrowserSheet;
                @BXML private TabPane tabPane = null;

                private MenuHandler menuHandler = new MenuHandler.Adapter() {
                    TextInputContentListener textInputTextListener = new TextInputContentListener.Adapter() {
                        @Override
                        public void textChanged(TextInput textInput) {
                            updateActionState(textInput);
                        }
                    };

                    TextInputSelectionListener textInputSelectionListener = new TextInputSelectionListener() {
                        @Override
                        public void selectionChanged(TextInput textInput, int previousSelectionStart,
                            int previousSelectionLength) {
                            updateActionState(textInput);
                        }
                    };

                    @Override
                    public void configureMenuBar(Component component, MenuBar menuBar) {
                        if (component instanceof TextInput) {
                            TextInput textInput = (TextInput)component;

                            updateActionState(textInput);
                            Action.getNamedActions().get("paste").setEnabled(true);

                            textInput.getTextInputContentListeners().add(textInputTextListener);
                            textInput.getTextInputSelectionListeners().add(textInputSelectionListener);
                        } else {
                            Action.getNamedActions().get("cut").setEnabled(false);
                            Action.getNamedActions().get("copy").setEnabled(false);
                            Action.getNamedActions().get("paste").setEnabled(false);
                        }
                    }

                    @Override
                    public void cleanupMenuBar(Component component, MenuBar menuBar) {
                        if (component instanceof TextInput) {
                            TextInput textInput = (TextInput)component;
                            textInput.getTextInputContentListeners().remove(textInputTextListener);
                            textInput.getTextInputSelectionListeners().remove(textInputSelectionListener);
                        }
                    }

                    private void updateActionState(TextInput textInput) {
                        Action.getNamedActions().get("cut").setEnabled(textInput.getSelectionLength() > 0);
                        Action.getNamedActions().get("copy").setEnabled(textInput.getSelectionLength() > 0);
                    }
                };

                public MenuBars() {
                    Action.getNamedActions().put("fileNew", new Action() {
                        @Override
                        public void perform(Component source) {
                            BXMLSerializer bxmlSerializer = new BXMLSerializer();
                            bxmlSerializer.getNamespace().put("menuHandler", menuHandler);

                            Component tab;
                            try {
                                tab = new Border((Component)bxmlSerializer.readObject(MenuBars.class, "document.bxml"));
                            } catch (IOException exception) {
                                throw new RuntimeException(exception);
                            } catch (SerializationException exception) {
                                throw new RuntimeException(exception);
                            }

                            tabPane.getTabs().add(tab);
                            TabPane.setTabData(tab, "Document " + tabPane.getTabs().getLength());
                            tabPane.setSelectedIndex(tabPane.getTabs().getLength() - 1);
                        }
                    });

                    Action.getNamedActions().put("fileOpen", new Action() {
                        @Override
                        public void perform(Component source) {
                            fileBrowserSheet.open(MenuBars.this);
                        }
                    });

                    Action.getNamedActions().put("cut", new Action(false) {
                        @Override
                        public void perform(Component source) {
                            TextInput textInput = (TextInput)MenuBars.this.getFocusDescendant();
                            textInput.cut();
                        }
                    });

                    Action.getNamedActions().put("copy", new Action(false) {
                        @Override
                        public void perform(Component source) {
                            TextInput textInput = (TextInput)MenuBars.this.getFocusDescendant();
                            textInput.copy();
                        }
                    });

                    Action.getNamedActions().put("paste", new Action(false) {
                        @Override
                        public void perform(Component source) {
                            TextInput textInput = (TextInput)MenuBars.this.getFocusDescendant();
                            textInput.paste();
                        }
                    });
                }

                @Override
                public void initialize(Map<String, Object> namespace, URL location, Resources resources) {
                }
            }
            
        

The class also defines an anonymous inner implementation of the MenuHandler interface that is used to configure the menu bar based on the focused component. In configureMenuBar(), the actions associated with the "cut", "copy", and "paste" operations are enabled and disabled as appropriate. Listeners are also added to the focused component (if it is a TextInput) to ensure that the action's state accurately reflects the current selection. The listeners are removed in cleanupMenuBar(), if necessary.

Note that menu bar configuration via MenuHandler isn't limited to enabling or disabling actions - new menu items can be dynamically created, menu item selection state can be changed, etc. However, unlike context menus, the framework does not automatically clean up any changes made to the menu bar. It is up to the application to ensure that the menu bar remains in a consistent state using the configureMenuBar() and cleanupMenuBar() methods.

Next: Menu Buttons