Monday, March 9, 2020

JavaFX desktop application on macOS - Part II

Copyright © 2020, Steven E. Houchin. All rights reserved.

In Part I of this topic, I showed snippets of Java code that help your JavaFX desktop application run correctly on macOS, even when you've developed and built it on a Windows system, as I have. I left off last time with two unfinished issues for my MapApp program. They occur on macOS when simply executing the application's JAR file by opening it directly, such as double-clicking it:

  1. The application's title in the system menu bar is "java," instead of "MapApp."
  2. The application's dock icon is the standard Java coffee cup image.

The first I will defer to my next post. The second is discussed here.

Setting the Dock Icon


Setting the program icon using "stage.getIcons().add()" in the Java code works fine to set a title bar icon, but doesn't affect the Dock icon. The Dock icon requires calls to a special package from Apple, named "com.apple.eawt", which should exist on your macOS system, but isn't available for development on Windows. Unfortunately, Apple's documentation of this package has gone extinct, or is buried somewhere far, far away. The best docs I have found for it are at:

  https://coderanch.com/how-to/javadoc/appledoc/api/com/apple/eawt/Application.html

In that package is an Application class that contains methods to do interesting Mac-only things, like to set the Dock icon. So, here's the code I used, which requires using Java Reflection since the package is not available on Windows for linking.

First, you must obtain the package's Application class:

    Class<?> applicationClass;
    try
    {
        // get the eawt Application class
        applicationClass = Class.forName(
               "com.apple.eawt.Application");
    }
    catch (ClassNotFoundException ex)
    {
        Alert alert = new Alert(
                Alert.AlertType.ERROR, ex.toString());
        alert.showAndWait();
        return false;
    }

Second, load the icon image you wish to use, which in my case is a 48x48 JPEG embedded in the JAR as a resource. You could also load the icon from a file installed separately with the JAR. The embedding of the image file is accomplished on NetBeans by simply including the JPEG file in the project sources folder.

    // get the dock icon from the jar's embedded resources
    java.awt.image.BufferedImage image =
        ImageIO.read(getClass().getResourceAsStream("MyApp-48.jpg"));

Next, use the Application class (obtained above) to call methods that set the icon, using Reflection:

    try
    {
        // use reflection to access the
        // com.apple.eawt.Application methods
  
        // com.apple.eawt getApplication()
        // factory method to get an instance of
        // com.apple.eawt.Application
        Method getApplicationMethod =
            applicationClass.getMethod("getApplication");
  
        // com.apple.eawt Application.setDockIconImage()
        // Application class instance method to
        // set the Dock icon
        Method setDockIconMethod = applicationClass.getMethod(
                                     "setDockIconImage",
                                     java.awt.Image.class);

        // get an instance of the Appication class
        Object macOSXApplication =
            getApplicationMethod.invoke(null);
  
        // set the image as the dock icon
        setDockIconMethod.invoke(macOSXApplication, image);
    }
    catch (NoSuchMethodException
            | IllegalAccessException
            | IllegalArgumentException
            | InvocationTargetException ex)
    {
        Alert alert = new Alert(
                Alert.AlertType.ERROR, ex.toString());
        alert.showAndWait();
        return false;
    }

My application calls this code right away in the application class's "start(Stage stage)" method. When you run the application, you will see the standard Java coffee cup Dock icon at first, then your application's icon will load and take its place.

Note that all of the above will become moot if you create a package for your application on the Mac, where you can specify your own Mac icon file (.icns) and other things to customize its installation in the Applications folder. More fodder for subsequent posts!

Tuesday, March 3, 2020

JavaFX desktop application on macOS - Part I

Copyright © 2020, Steven E. Houchin. All rights reserved.

This posting is first in a series, whose purpose is to show what is necessary to get a JavaFX desktop application to work reasonably on macOS.

Environment


In my case, I am running macOS High Sierra (10.13.6) with Java 8 (version 1.8.0_231). My development environment is Windows 10, Java 8 (version 1.8.0_241), NetBeans 8.2 and JavaFX Scene Builder 2.0.

The Application


Mine is a desktop application that loads map images and allows for smart panning, zooming, and custom annotations. It is intended to run on Windows and macOS, and thus I chose Java as the language, and JavaFX for the User Interface components.

Code Unique for macOS


In several places in the code, it was necessary to recognize macOS differences. In many cases, to do this, I had to know it was running on macOS, and not Windows. Here's the method I used to do this, which takes advantage of the built-in system properties Java provides:

/**
 * Return true if the running OS is Mac OS,
 * otherwise Windows is assumed.
 *
 * @return - True if Mac OS, else assume Windows.
 */
public static boolean isMacOS()
{
    String os = System.getProperty("os.name");
    return os.startsWith("Mac") || os.startsWith("mac");
}

Also, I had to make sure file paths use the proper path separator character for the OS:
// get fully qualified path to the map file
String fullFilePath = myRootFolderPath +
                 File.separator + "mymap.jpg";
Then there is the application's menu bar. By default, when it runs on macOS, the menus appear atop the application's main window, just like on Windows. But, of course, that isn't what we want on the Mac; it should appear on the standard Mac menu bar at the top of the screen. Here's the trick to make that happen:
@FXML
private MenuBar menuBar;
if (isMacOS())
{
    // place the program menus on the standard
    // macOS menu bar
    menuBar.setUseSystemMenuBar(true);
}
Two more useful snippets of code that help get things right on the Mac.

First, how to find the user's home directory.

/**
 * Get the user's home directory path, appending the
 * system path separator to it.
 *
 * @return - The full path to the user home directory
 *    (ending in a path separator), or null if error
 */
public static String getUserHomeFolderPath()
{
    // get the user home directory from a standard
    // environment variable
    String folder = isMacOS() ? System.getenv("HOME") :
                                System.getenv("HOMEPATH");
    if (null != folder)
    {
        folder += File.separator;
    }
    return folder;
}

Second, how to get the name of the user's Documents folder.


/**
 * Get the user's Documents folder name.
 *
 * @return The folder name for Documents on this OS.
 */
public static String getDocumentsFolderName()
{
    if (!isMacOS())
    {
        String os = System.getProperty("os.version");
        double version = Double.parseDouble(os);
        if (version < 7.0)
        {
            // name of Documents folder for Windows 2000
            // and XP
            return "My Documents";
        }
    }
    // name of Documents folder for macOS and
    // Windows 7.0+
    return "Documents";
}

Installing the Application to Test


My application links with a library, which I also developed, which does the heavy lifting for drawing and manipulating the map. When I copy the application JAR (MapApp.jar) to my test folder on the Mac, my maps library JAR (MapLib.jar) must be placed in a "lib" subfolder below the test folder. At this point, I can execute the app on the Mac by double-clicking MapApp.jar, and it starts up. But, two things show up that aren't yet right:
  1. The application's title in the system menu bar is "java," instead of "MapApp."
  2. The application's dock icon is the standard Java coffee cup image.
I will discuss solving these problems and others in a future post.