A Codename One app with a native menu bar and file dialog

A desktop app gets judged in the first ten seconds. If the menu bar is wrong, Cmd+Q does nothing or the Save dialog looks like it came from a different decade, people notice, even if they can’t say what bothers them. The word processor from last Friday’s post needed all of that to hold up next to a native app, so those pieces are now part of Codename One. They work with the code you already write for a phone.

The real open and save dialogs

openFileChooser() picks one file. Desktop apps need more: several filters, a suggested name, extra options in the dialog and a preview while the user browses. showOpenDialog() and showSaveDialog() cover that:

Display.getInstance().showOpenDialog(new OpenFileRequest()
        .title("Import photos")
        .multiSelect(true)
        .addFilter(new FileTypeFilter("Images")
                .extensions("png", "jpg").mimeTypes("image/*"))
        .addOption(FileDialogOption.checkbox("strip", "Remove location data", true))
        .preview(path -> {
            try (InputStream in = FileSystemStorage.getInstance().openInputStream(path)) {
                return new ScaleImageLabel(Image.createImage(in));
            } catch (IOException err) {
                return new Label("No preview");
            }
        }),
    result -> {
        if (result.isCancelled()) {
            return;
        }
        boolean strip = "true".equals(result.getOptionValue("strip"));
        for (String path : result.getPaths()) {
            // import each file
        }
    });
An open dialog with an image preview, an Images filter and a Remove location data checkbox
The request above in the simulator, with the preview and the option.

Every platform opens and saves files. The extras appear where the platform has them, and the rest ignore them, so one request works everywhere:

PlatformOpenSave
macOSNSOpenPanelNSSavePanel, with format list, options and tag field
WindowsIFileOpenDialogIFileSaveDialog, with options as dialog controls
LinuxGtkFileChooserGtkFileChooser with overwrite confirmation
AndroidThe system document picker, several files, foldersCreate document, with no storage permission
iOSThe document picker, several files, foldersExported through the document picker
BrowserThe browser’s file pickerA download with the suggested name

A save request writes its content after the user has picked the location, off the EDT, and it can see which format and options were chosen. Platform hints, such as the macOS tag field or the folder Android starts in, are one call each.

A menu bar that follows each platform

A Command already knows its name, icon, and action. Tell it where it goes and it shows up in the native menu bar on macOS, Windows, Linux and in the browser build. Paths make submenus, and check and radio items work the way you’d expect:

String[] zooms = {"50%", "100%", "200%"};
for (String zoom : zooms) {
    Command c = Command.create(zoom, null, e -> setZoom(zoom));
    c.setDesktopMenu("View/Zoom").setDesktopMenuGroup(1).setDesktopRadioGroup("zoom");
    c.setDesktopChecked("100%".equals(zoom));
    form.getToolbar().addCommandToOverflowMenu(c);
}

On a phone the same commands stay in the overflow menu, so there’s nothing to branch on.

The word processor's File menu with an Export submenu on macOS
The word processor's File menu on macOS.

About, Settings and Quit live in different places on every desktop, under different names. Give the command a role and each platform puts it where its own apps do, with its own shortcut:

Command settings = Command.createDesktopRole(Command.DESKTOP_ROLE_SETTINGS, e -> showSettings());
Command quit = Command.createDesktopRole(Command.DESKTOP_ROLE_QUIT, e -> quit());
RolemacOSWindowsLinux
AboutApplication menu, firstHelp, lastHelp, last
Settings“Settings…” in the application menu, Cmd+,“Options” under Tools“Preferences” at the foot of Edit
Quit“Quit” in the application menu, Cmd+Q“Exit” at the foot of File“Quit” at the foot of File, Ctrl+Q

The File and Edit roles work the same way, so Redo is Cmd+Shift+Z on a Mac and Ctrl+Y on Windows without you looking it up. On macOS a role replaces the built-in item, so the menu never shows two Quit items.

The macOS application menu with About, Settings and Quit
Roles landing in the macOS application menu.

One more small thing that makes an editor feel right: form.setDocumentModified(true) puts the dot in the close button on macOS and an asterisk before the title on Windows and Linux, and the Save command enables itself while there’s something to save.

Animated 3D models

The other big addition came from Skirmish. GltfLoader loads glTF 2.0 models, binary .glb or JSON .gltf, together with their textures, so a textured model renders with no extra setup. AnimatedModel brings them to life:

GltfScene knight = GltfLoader.loadScene(in);
AnimatedModel unit = new AnimatedModel(device, knight);
unit.play("Walking_A", true);
unit.addTo(gameView);
// later
unit.crossFade("1H_Melee_Attack_Chop", 0.15f, false);

It plays the model’s animation clips, cross-fades between them, deforms skinned meshes with their skeleton and carries rigid parts, like a sword in a hand, along with their bones. Skinning runs on the CPU, so it looks identical on every port and needs nothing from the shaders. Many instances can share one loaded scene: the vertex data, clips and textures are shared, and only the posed vertices are per instance. That’s how a Skirmish battle fills the arena with troops.

The Khronos BoomBox glTF sample rendered with its own texture
A textured glTF model.
Animated characters in a Skirmish battle
Animated characters in Skirmish.

The desktop integration guide, the file system chapter and the 3D graphics chapter have the details.

Tomorrow, the last post of the week: WebRTC Calls, Key Agreement and ORM Checks at Build Time.