2. Command Framework

The command framework in the ReportDesigner is used to create menubars, toolbars and popup menus. The design of the framework simplifies the separation of model, view and control.

The main advantage lies in the updating of the presentations (actual menubar, toolbar) depending on the context. The command framework is responsible to detect whenever the underlying model, selection or focused element changes and makes the commands update its presentations. Commands don't have to register listeners in the model nor must commands know the exact structure of the application their working on. Only the data is important to a command. A command which moves the selected ReportElement 10 pixels up can simply ask for the selected element and call setPosition on this element. The element  can be selected in the structure tree or the main report panel or somewhere else, it does not matter to the command. The command does not even have to know that a structure tree is existing in the application.

Main classes involved when using the framework

org.pentaho.reportdesigner.lib.client.commands.Command

Interface implemented by all commands used to create a toolbarbutton or menu item. Implementations should be stateless. The command class implements the controller part of an action. One command can appear multiple times on the user interface and should therefore be stateless.
Samples of commands: OpenReportCommand, SaveReportCommand, ExitCommand

org.pentaho.reportdesigner.lib.client.commands.Presentation

The presentation is the view of a command. It holds localized text, descriptions, icons, accelerator. Multiple presentations of a command can exist at the same time. e.g one presentation in the menubar, one in the toolbar and one temporarily in a popup menu. The command is responsible to update the presentation whenever the update method is called. A command usually disables the presentation when the required data is not available. The command can also change the text/icon/visibility of the presentation. e.g The undo command might change the text to contain a description of the operation to undo ("Undo change background").

org.pentaho.reportdesigner.lib.client.commands.CommandGroup

A CommandGroup can contain a set of commands belonging together.
Sample of a command group: Align Left/Center/Right etc. form a group. The group itself can be specified to be of type popup or normal to change the appearance type in the menubar or toolbar.
Samples of a command group specified to be of type popup in the menubar and toolbar:

A command group of type normal will produce regular menu items and toolbar buttons.

org.pentaho.reportdesigner.lib.client.commands.DataContext

Interface used by commands to request data of the current context. The context is usually focus dependent and changes whenever the user selects another component/element or the reporting model changes by an automated action from a background thread.
The datacontext is determined by the command framework by hierarchically walking the component tree from the deepest focused component upwards to the frame. The first encountered DataProvider is used to return the datacontext.

org.pentaho.reportdesigner.lib.client.commands.CommandManager

Central class to register command groups using a key. This class is used to create a menubar, toolbar or popup menu from the previously defined key.
e.g a command group containing all commands destined for the toolbar can be registerd using the key "Toolbar" (the key is completely user defined and has no special meaning).

How to create a command from scratch

Step 1: Create the command class (controller)

public class MoveUpCommand extends AbstractCommand
{
    public SampleMoveUpCommand()
    {
        super("MoveUpCommand");
        getTemplatePresentation().setText("Move 10 pixels up");
    }


    public void update(@NotNull CommandEvent event)
    {
        ReportElement[] selectedElements = (ReportElement[]) event.getDataContext().getData(CommandKeys.KEY_SELECTED_ELEMENTS_ARRAY);
        event.getPresentation().setEnabled(selectedElements != null && selectedElements.length > 0);//only enable when something is selected
    }


    public void execute(@NotNull CommandEvent event)
    {
        ReportDialog reportDialog = (ReportDialog) event.getPresentation().getCommandApplicationRoot();
        reportDialog.getUndo().startTransaction(UndoConstants.MOVE_UP);
        ReportElement[] reportElements = (ReportElement[]) event.getDataContext().getData(CommandKeys.KEY_SELECTED_ELEMENTS_ARRAY);
        if (reportElements != null)
        {
            for (ReportElement reportElement : reportElements)
            {
                double x = reportElement.getPosition().x;
                double y = reportElement.getPosition().y - 10;
                reportElement.setPosition(new Point2D.Double(x, y));
            }
        }
        reportDialog.getUndo().endTransaction();
    }
}

The update method is automatically invoked whenever the command framework likes to (don't assume anything). The CommandEvent contains the presentation that should be updated. The update method is usually invoked for every presentation (e.g toolbar and menubar) of a command.

The execute method is invoked when a user selects the menuitem or presses the toolbar button. The datacontext accessible in the execute method returns the same data as the datacontext returned in the last update call. But it's usually a good idea to prevent NPEs by checking for null.

Step 2: Register the command in a group. 

CommandManager.addCommandToGroup(reportDialog, "Toolbar", new MoveUpCommand(), CommandConstraint.LAST);

Additional commands could be added the the "Toolbar" group.

Step 3: Creating a toolbar and adding the JToolbar to the UI.

commandToolBar = CommandManager.createCommandToolBar(this, "Toolbar");
getContentPane().add(commandToolBar.getToolBar(), BorderLayout.NORTH);

How to create a toggle command

The command framework is prepared to handle toggle commands. The appearance is look and feel dependent but will usually result in a toggle button on the toolbar and a checkbox menu item in the menubar.
A toggle command can be created by subclassing AbstractToggleCommand instead of directly subclassing AbstractCommand.

public class SnapToElementsToggleCommand extends AbstractToggleCommand
{
    public SnapToElementsToggleCommand()
    {
        super("SnapToElementsToggleCommand");
        getTemplatePresentation().setText(TranslationManager.getInstance().getTranslation("R", "SnapToElementsToggleCommand.Text"));
        getTemplatePresentation().setDescription(TranslationManager.getInstance().getTranslation("R", "SnapToElementsToggleCommand.Description"));
        getTemplatePresentation().setIcon(CommandSettings.SIZE_16, IconLoader.getInstance().getSnapToElementsIcon());
    }


    public void update(@NotNull CommandEvent event)
    {
        event.getPresentation().setEnabled(true);
        ApplicationSettings applicationSettings = ((ReportDialog) event.getPresentation().getCommandApplicationRoot()).getApplicationSettings();
        event.getPresentation().setClientProperty("selected", Boolean.valueOf(applicationSettings.isSnapToElements()));//NON-NLS
    }


    public void execute(@NotNull CommandEvent event)
    {
        ApplicationSettings applicationSettings = ((ReportDialog) event.getPresentation().getCommandApplicationRoot()).getApplicationSettings();
        applicationSettings.setSnapToElements(!applicationSettings.isSnapToElements());
    }
}

Setting the selected property in the update method will make the presentation show the toolbar button pressed. This property should be set using the underlying model (ApplicationSettings in this case).
The execute method should also use the property from the underlying model and not depend on the selected property set in the presentation. This ensures a separation of model and view.

How to add custom components to the toolbar

Adding custom components (comboboxes, fontchoosers) can also be implemented using the command framework.

public class CustomComponentCommand extends AbstractCommand
{
    public CustomComponentCommand()
    {
        super("CustomComponentCommand");
        getTemplatePresentation().setText(TranslationManager.getInstance().getTranslation("R", "CustomComponentCommand.Text"));
        getTemplatePresentation().setDescription(TranslationManager.getInstance().getTranslation("R", "CustomComponentCommand.Description"));
    }


    public void update(@NotNull CommandEvent event)
    {
        ReportElement[] reportElements = (ReportElement[]) CommandManager.getCurrentCommandProvider().getDataContext().getData(CommandKeys.KEY_SELECTED_ELEMENTS_ARRAY);
        event.getPresentation().setEnabled(reportElements != null && reportElements.length > 0);
    }


    @Nullable
    public JComponent getCustomComponent(@NotNull final Presentation presentation)
    {
        final JComboBox jComboBox = new JComboBox(new Color[]{Color.RED, Color.GREEN, Color.BLUE});
        jComboBox.setMaximumSize(new Dimension(200, jComboBox.getPreferredSize().height));
        jComboBox.setEnabled(false);
        jComboBox.setFocusable(false);

        presentation.addPropertyChangeListener(new PropertyChangeListener()
        {
            public void propertyChange(@NotNull PropertyChangeEvent evt)
            {
                if ("enabled".equals(evt.getPropertyName()))//NON-NLS
                {
                    jComboBox.setEnabled(((Boolean) evt.getNewValue()).booleanValue());
                }
            }
        });

        jComboBox.addActionListener(new ActionListener()
        {
            public void actionPerformed(@NotNull ActionEvent e)
            {
                ReportElement[] reportElements = (ReportElement[]) CommandManager.getCurrentCommandProvider().getDataContext().getData(CommandKeys.KEY_SELECTED_ELEMENTS_ARRAY);
                ReportDialog reportDialog = (ReportDialog) presentation.getCommandApplicationRoot();

                if (reportElements != null && reportElements.length > 0)
                {
                    reportDialog.getUndo().startTransaction("background");//NON-NLS
                    for (int i = 0; i < reportElements.length; i++)
                    {
                        ReportElement reportElement = reportElements[i];
                        reportElement.setBackground((Color) jComboBox.getSelectedItem());
                    }
                    reportDialog.getUndo().endTransaction();
                }

                CommandManager.dataProviderChanged();
            }
        });

        return jComboBox;
    }


    public void execute(@NotNull CommandEvent event)
    {
    }
}

The getCustomComponentCommand can be overridden to return the component. A new instance must be returned for every presentation, since the command framework will directly add this component to the UI and a swing/awt component can only be a child of one parent component.
The presentation and a property listener can be used to pass values between the update method and the custom component. In this sample the only value passed is the "enabled" property. The command can be extended to select the current color of the combobox by adding a "color" property to the presentation:

event.getPresentation().setClientProperty("selectedColor", Color.RED);

The combobox can add a PropertyListener to the presentation:

presentation.addPropertyChangeListener(new PropertyChangeListener()
{
    public void propertyChange(@NotNull PropertyChangeEvent evt)
    {
        if ("selectedColor".equals(evt.getPropertyName()))//NON-NLS
        {
            jComboBox.setSelectedItem(evt.getNewValue());
        }
    }
});

(warning) Custom components should not be focusable. The command manager updates commands based on the current focused component. The command manager will not find a decent DataProvider when a component in the toolbar can become the permanent focus owner.