Goal

An application based on the Lucy framework often requires extra UI to offer custom functionality to the end-user of the application. This tutorial illustrates how you can add an extra panel to the Lucy UI using the ALcyApplicationPaneTool class.

This tutorial is based on the code of the "Adding extra panels to Lucy" sample samples.lucy.applicationpane.Main. Note that the UI of the sample was deliberately kept simple, because the sample focuses on using the ALcyApplicationPaneTool class, not on a nice-looking UI.

Next to adding UI, modifying the existing UI is a frequently recurring use case. The samples.lucy.lightspeed.map.guifactory.Main sample illustrates how you can customize the UI of the Lightspeed maps.

The role of the Lucy front-end and the add-ons

The Lucy framework separates the creation of the UI panels and the application frame from the creation of the content of those panels:

  • The add-ons are responsible for creating the content of the Lucy panels.

  • The Lucy front-end is responsible for creating the actual panel that contains that content.

The major benefit of such a separation is that you can replace the front-end of Lucy, the part responsible for the creation of the panels, without making any changes to the code responsible for the creation of the content.

lucy frontend backend
Figure 1. Separation between the front-end and back-end.

The mechanism is illustrated in the samples. The LuciadLightspeed release contains a few sample versions of Lucy, each using a distinct front-end. The two most popular front-ends are the Lucy docking front-end, available from Lucy.bat, and the Lucy map-centric front-end, available from LucyMapCentric.bat. Both versions of Lucy use the same add-ons, which are responsible for the contents of the panels. By altering the front-end, however, the location and functionality of the panels can be modified to fit into either a docking UI or map-centric UI.

API classes

The panels in Lucy are ILcyApplicationPane instances, and the front-end must provide a factory for those panels, an ILcyApplicationPaneFactory. When an add-on wants to create a panel, it will ask the ILcyApplicationPaneFactory to create a panel, and the add-on will add the contents to the panel.

To facilitate that common use case, the Lucy framework contains the utility class ALcyApplicationPaneTool. This class is designed to add an extra ILcyApplicationPane to Lucy, and provides:

  • Configuration options to change the location, title and icon of the panel.

  • A configuration option to show or hide the panel at start-up.

  • A configurable menu item that allows the user to show or hide the panel. In this case, configurable means that you can define whether the menu entry is present or not, where the entry is located in the UI, and which icon and text the UI entry uses.

  • API to show and hide the application pane.

  • Workspace support for the presence of the panel. If the application displayed the panel when the workspace was saved, loading the workspace will restore the panel.

The only thing left to the user of this class is to provide the actual contents of the panel.

Optionally, you can add workspace support for the state of the panel contents as well. The ALcyApplicationPaneTool provides workspace support for the presence of the panel only.

Creating the panel

Creating the application pane tool

To create the application pane tool, you must create an extension of the ALcyApplicationPaneTool and implement one abstract method in which the panel content is created, as explained in Creating the contents of the panel.

Once you created the extension, you must call the plugInto method on this application:

Program: Creating of the application pane tool (from samples/lucy/applicationpane/ShowApplicationPaneAddOn)
//Create and plug in an application pane tool to show the extra panel
CustomApplicationPaneTool applicationPaneTool = new CustomApplicationPaneTool(this);
applicationPaneTool.plugInto(aLucyEnv);

The configuration file of the addon must contain the necessary properties to define the location of the panel, the title, the icon, and so on. All those options are documented in the class Javadoc of the ALcyApplicationPaneTool class.

Creating the contents of the panel

Each time the application pane is shown, it calls the ALcyApplicationPaneTool.createContent method. The return value of that method is a java.awt.Component which will be shown in the panel.

This sample uses a very simple UI that contains only a title, a label, and a combo box:

Program: Creating the contents of the application pane. (from samples/lucy/applicationpane/CustomApplicationPaneTool)
@Override
protected Component createContent() {
  JPanel contents = new JPanel();
  TLcyTwoColumnLayoutBuilder.newBuilder()
                            .addTitledSeparator("Custom UI")
                            .row()
                            .columnOne(new JLabel("Select a value"), createComboBox())
                            .build()
                            .populate(contents);
  return contents;
}

Adding workspace support for the contents of the panel

This sample also stores the state of the contents of the panel inside the workspace. In this sample, the value selection possibilities in the combo box are fixed. The only state is the currently selected item of the combo box.

The ShowApplicationPaneAddOn extends from the ALcyPreferencesAddOn. The preferences add-on provides a mechanism to store key-value pairs in the workspace using an ALcyProperties instance. Each key-value pair you store in the workspace preferences is automatically stored in the workspace, and restored during workspace loading. This is perfectly suitable for saving our selected item.

The first step consists of obtaining the ALcyProperties instance that will be saved into the workspace by the ALcyPreferencesAddOn. That instance is available on the preferences tool of the addon:

Program: Retrieving the workspace preferences from the addon. (from samples/lucy/applicationpane/CustomApplicationPaneTool)
fWorkspacePreferences = aShowApplicationPaneAddOn.getPreferencesTool().getWorkspacePreferences();

The next step is to ensure that the initial selected element of the combo box is set to the one stored in the preferences, if one is available.

Program: The initial selection should match the one from the preferences. (from samples/lucy/applicationpane/CustomApplicationPaneTool)
String defaultValue = ComboBoxValues.FIRST.toString();
String selectedAsString = aProperties.getString(aPropertyKey, defaultValue);
aComboBoxModel.setSelectedItem(ComboBoxValues.valueOf(selectedAsString));

Now we need to set up a synchronization mechanism in two directions:

  • When the user changes the selection in the UI, the selected value in the preferences must be updated to match the selection in the UI. That ensures that the most recently selected value is saved in the workspace. You can do so by adding a listener to the combo box model:

    Program: Updating the value in the preferences when the user changes the selection in the UI. (from samples/lucy/applicationpane/CustomApplicationPaneTool)
    comboBoxModel.addListDataListener(new ListDataListener() {
      @Override
      public void intervalAdded(ListDataEvent e) {
        //do nothing
      }
    
      @Override
      public void intervalRemoved(ListDataEvent e) {
        //do nothing
      }
    
      @Override
      public void contentsChanged(ListDataEvent e) {
        //this method is called when the selection is changed in the combobox
        fWorkspacePreferences.putString(propertyKey, comboBoxModel.getSelectedItem().toString());
      }
    });
  • When a workspace is loaded, the selected value in the preferences is automatically updated. Consequently, the selected value in the UI must be adjusted. This also requires a listener, set on the preferences this time:

    Program: Updating the selected value in the UI when the selection in the preferences changes. (from samples/lucy/applicationpane/CustomApplicationPaneTool)
    fWorkspacePreferences.addPropertyChangeListener(new UpdateComboFromPreferencesListener(comboBoxModel, propertyKey, fWorkspacePreferences));

Take care when you add a listener to the preferences. The ALcyProperties instance will remain alive as long as the Lucy application is alive. To prevent memory leaks, you have to make sure that the listener is removed when the combo box is no longer available because the user closed the panel with the combo box, for example. You can avoid writing the code for such checks and still prevent memory leaks by extending from ALcdWeakPropertyChangeListener:

Program: Using an extension of ALcdWeakPropertyChangeListener to prevent memory leaks. (from samples/lucy/applicationpane/CustomApplicationPaneTool)
private static class UpdateComboFromPreferencesListener extends
                                                        ALcdWeakPropertyChangeListener<ComboBoxModel<ComboBoxValues>> {

Such a weak property change listener only keeps a weak reference to the object it must update. As a result, there will be no memory leak when that object is gone.

Note that you may not always need workspace support for the state of the panel contents. When the state of the panel contents just depends on some other state that has already been saved in the workspace, there is no need to save the state a second time. For example, if the panel shows the properties of a selected object on the map, the state of the panel will be restored automatically when the map selection is restored during workspace loading.