Darken your GUI with Radiance

Published on

The last couple of days I’ve been working on some improvements to the Orbit Image Analysis software. At the moment it’s mostly technical changes to help us be able to switch to using Java 11 (and then the next LTS Java JDKs). A nice side effect of that is being able to use my favourite JDKs from AdoptOpenJDK… The code isn’t available at github just yet, but I hope to get it pushed in the next few days.

You look absolutely Radiant!

Orbit has been using a ‘dark mode’ along with the ‘office’ style ribbon for ages. Maybe in the future we will provide a ‘light mode’ :-D. That is all courtesy of the Insubstantial library. In the meantime Substance and Flamingo are part of the Radiance project. Radiance supports Java 9 (and works for me using Java 11). There are some other nice side benefits, such as a gradle plugin Ignite for converting SVG files to Java classes for use with Radiance. Radiance is also available as a Maven artifact at Maven central.

Initial impressions

I primarily want to take the OrbitMenu.java class and convert it to be able to use the latest Radiance release (currently 2.5.1). Once that’s done it is hopefully just a few more steps to switch OrbitImageAnalysis.java to be able to compile using Gradle 6, and Java 11…

Luckily Radiance has good documentation (although sometimes a little frustrating to Google for…). In the case of Flamingo (the part that provides Swing components for the Ribbon interface), a really useful example application that covers all of the functionality that I need. Thanks Kirill for sharing a really great library!

Code changes for Orbit Image Analysis

Previously the OrbitMenu could be run as a stand-alone program to allow for quick previews of changes, that had quite a few cases where null checks were needed. I decided to skip those for now, which means that the class can no longer be compiled using only the class itself, a working instance of the Orbit Image Analysis class is required.

A command is created for each button that we need to add to the interface. Those commands are listed in the order that the first appear in the menu. Reading tabs left -> right, followed by tab contents left -> right.

Certain buttons should only be useable when the remote image provider is being used. In that case we apply the following method to the command.

.setActionEnabled(!DALConfig.isLocalImageProvider())

All commands reference a CommandAction which is in most cases defined in OrbitImageAnalysis.java. Generally the CommandAction just calls a void method which actually performs the action. It sometimes feels a bit wonky, but it has two nice benefits. The first is that it provides a clean separation between Radiance stuff and Orbit Image Analysis stuff, which should make it easier if we need to switch to a different UI library in the future. Also it allows us to easily make use of the existing action listeners where needed. See the code snippet below.

    final CommandAction CopyImageCommandAction = e -> copyImageToClipboard(false);
    final ActionListener copyImageAction = e -> copyImageToClipboard(false);

    // Edit Tab/Task
    private void copyImageToClipboard(boolean copyFullImage) {
        if (getIFrame() != null) {
            if (OrbitUtils.isSmallImage(getIFrame().recognitionFrame.bimg.getImage())) {
                Transferable t = ((DesktopTransferHandler) desktopTransferHandler).createImageTransferable(getIFrame(), copyFullImage);
                Toolkit.getDefaultToolkit().getSystemClipboard().setContents(t, null);
            } else {
                JOptionPane.showMessageDialog(OrbitImageAnalysis.this,
                        "The image is too large to be copied into the clipboard. " +
                                "\nYou can use ALT-C to copy the currently visible region.",
                        "Image too large", JOptionPane.WARNING_MESSAGE);
            }
        }
    }

There are two places where the code behaves slightly differently in terms of where the ActionListeners live. That is in the case of modules, and the ‘special’ ExclusionModule, and also in the case of the ‘CustomMenu’. Typical modules, when enabled by clicking a button in the ribbon are visible on the right hand panel, as a tab alongside the annotations tab. That allows to create and add an arbritary number of modules. These modules typically then perform some action on the images open in the main window. The one exception to this is the Exclusion module, which contributes several buttons to the ribbon.

To allow easily adding a module without needing to add a dependency of the module to Radiance, the module can extend AbstractOrbitRibbonModule, which itself maintains the Radiance dependency and handles providing the CommandAction and Command required for the ribbon menu. OrbitMenu itself then imports the modules and displays them using the below code snippet.

        for(AbstractOrbitRibbonModule module: oia.getEnabledModules()) {
            CommandButtonProjection<Command> moduleProjection = module.getMenuCommand().project(
                    CommandButtonPresentationModel.builder().build());
            extensionsBand.addRibbonCommand(moduleProjection, JRibbonBand.PresentationPriority.TOP);
        }

ExlusionModule is handled slightly differently, it just extends the plain AbstractOrbitModule, and there is some code in OrbitMenu to display all of the relevant buttons. This is most likely a good candidate for further code cleanup.

In addition to modules there is also the CustomMenu, which can be configured in the config.properties file. The CustomMenu allows to add extensions from an external package, e.g. proprietary software developed within a company. If the CustomMenu is enabled, then an additional tab in the ribbon is shown. The Custom Menu implements the ICustomMenu interface, which must only implement a single method Set<AbstractOrbitAction> getRibbonActionSet(). The AbstractOrbitAction class must itself be extended by any option that should be included into the menu bar.

When creating the ‘Command’ buttons I made extensive use of ResourceBundle files to define the button names, tooltip text etc. That has one immediate benefit that it makes it much easier to define reused text in just one place. It has a nice side benefit that it should allow for very easy internationalization of Orbit Image Analysis to another language (Klingon anyone?).

I made a few other smaller changes. In a couple of cases, I slightly changed the button naming, or tooltip descriptions hoping to improve clarity. All existing Keyboard shortcuts were migrated as is, to the new framework.

Did you know that you could click on this icon and a menu would come up, well not many people did, especially after Microsoft long since dropped this UI element from the Office suite. We’ve decided to do the same with the new UI.

In many cases in the classes that I needed to touch I’ve switched to Lambda Expressions. I found that allows us to drop the number of lines of code, and more importantly it makes the code much more readable.

Gradle stuff

Upgrading from Gradle 4 -> 6, we had to fix a few issues with deprecated functionality. Mostly that was just changing ‘compile’ to either ‘implementation’ or ‘api’. There is a nice description of what exactly that means here but in short we now use implementation for many of the dependencies, which should result in more convenient builds, and a more cleanly defined dependency structure. Also, as mentioned earlier, there is a new task ‘generateIcons’ which uses the Radiance functionality ‘ignite’ to transcode SVG icons to the Java classes that Radiance uses, which is just a bit more slick than the previous method where an Ant build was needed to call a provided jar.

Open Questions

We still don’t have a default checkstyle defined for Orbit Image Analysis. It’s something that is on my todo list, along with adding those checks to the build system, using Sonar to check the code.

I didn’t implement keytips. Which are kind of similar to keyboard shortcuts, but more handy for people (like me), who aren’t good at rememebering them. If you’re interested in seeing keytip functionality enabled, then please drop me a message or open an issue on the Github project.

Where should the ExclusionModule live? I have a feeling that it would improve the readability of OrbitImageAnalysis.java if it were a little shorter (lines of code), which could be acheived by pulling out a few more tasks, e.g. classification, segmentation. It’s probably not quite the right time to tackle this issue, since there are a few open questions about the general structure of Orbit Image Analysis. I’ll follow up on these in a separate blog post.

Feedback

We’ll soon have a new version of Orbit Image Analysis available, it supports the latest Java versions, and features several code changes. If you think that the changes will affect you (negatively or positively!) let us know.