Showing posts with label JavaFX. Show all posts
Showing posts with label JavaFX. Show all posts

Tuesday, April 21, 2020

JavaFX desktop application on macOS - Part III

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

This is the third and final post in this series, which deals with installing a JavaFX desktop application on macOS, when it was developed on a Windows system.  In my first post on this subject, I left two issues unresolved, the first of which I discussed in the second post.

Here, I discuss the second lingering issue left over from that first post, namely that the application's title in the macOS system menu bar shows as simply "java," instead of my application's title, "MapApp." This happens when I merely double click on the app's JAR file to run it.

To solve the problem, I had to create an installation package on the Mac, then run that package so the app is properly placed in the Mac's Applications folder, thus appearing like any other Mac application.


Build Steps on Windows


The key to making all that happen starts back on my Windows machine, and my build steps in NetBeans.  Since my app is made up of an application JAR, a library JAR, and some support files (like image files), I needed to integrate them together in a single JAR for the Mac.

  1.  I included the support files in my NetBeans Java package, along with the java source files for the application. That bundles those support files as Java package resources, instead of separate files on the disk.
  2.  I modified the application package's 'build.xml' file to add a new Run Target named "package-for-store."  It is intended to run after all the sources are compiled into their JARs. I show that new target script below.
  3.  Once the sources were built into JARs, I used NetBeans to execute the new Run Target. This is done by …
    1. Open a Files window on NetBeans
    2. Right-click the application's 'build.xml' file
    3. Select Run Target
    4. Select Other Targets
    5. Select the target "package-for-store," which runs the packaging script
  4. Once the packaging script runs, the resulting application JAR file is found in the folder 'store', which is a peer to the project's 'dist' folder. This is the JAR file that contains everything, including libraries, that can then be copied over to the Mac. The script must be run manually anytime the application is changed.
So, what is in this packaging script that I added to the 'build.xml' that performs this magic?  It is a custom "target" I added, and it looks like this:

<target name="package-for-store" depends="jar">
 <property name="store.jar.name" value="MapApp"/>
 <property name="store.dir" value="store"/>
 <property name="store.jar" value="${store.dir}/${store.jar.name}.jar"/>
 <echo message="Packaging ${application.title}
 into a single JAR at ${store.jar}"/>
 <delete dir="${store.dir}"/>
 <mkdir dir="${store.dir}"/>
 <jar destfile="${store.dir}/temp_final.jar" filesetmanifest="skip">
  <zipgroupfileset dir="dist" includes="*.jar"/>
  <zipgroupfileset dir="dist/lib" includes="*.jar"/>
  <manifest>
   <attribute name="Main-Class" value="mapapp.MapApp"/>
  </manifest>
 </jar>

 <zip destfile="${store.jar}">
  <zipfileset src="${store.dir}/temp_final.jar"
  excludes="META-INF/*.SF, META-INF/*.DSA, META-INF/*.RSA"/>
 </zip>
 <delete file="${store.dir}/temp_final.jar"/>
</target>


Here are key items to note in the target script above.  The properties at the top specify the application name, the 'store' folder, and the jar file to create in the 'store' folder.  The "message" displays in NetBeans when the script is run. The 'store' folder is deleted and remade anew. A temporary JAR is created, which includes all JARs in the 'dist' folder and in the 'dist/lib' folder. The application's main startup class is specified by package name (mapapp) and class name (MapApp) in the "Main-Class" attribute. The finalized MapApp.jar is created from the temp JAR, and the temp is deleted.

When the script is done, "store/MapApp.jar" should exist, ready to be copied to the Mac.

Packaging Steps on Mac


So, I must copy the final JAR from above over to the Mac so it can be made into a macOS .pkg file.

On the Mac, I created a working folder 'MapApp/packaging' under my Documents. In it, I placed the JAR, and a .css file I need in the package. Under the 'packaging' folder, I also created a 'package/macosx' folder. As part of the process, the macOS java packaging utility looks for certain files there (all optional), which it uses to customize the package. The files are:

  • Info.plist - this is a standard macOS bundle configuration file for setting application properties, such as version number and copyright string.
  • MapApp-background.png - this is an image that displays at the lower left of the installation window when the package executed.
  • MapApp.icns - the application's custom icon file.
  • MapApp-post-image.sh - a script to run after the application image is populated.
Here is a shell script file I created to automate all of this to conveniently create the package (.pkg) file, which I run from a Bash terminal window:



#/bin/bash
#
# Create a .pkg file for the java MapApp app
#
jdk="/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home"

# Run the packager utlity
$jdk/bin/javapackager -version
$jdk/bin/javapackager -deploy -native pkg -name MapApp
-Bappversion=1.0.0 -srcdir . -srcfiles MapApp.jar:mapapp.css
-appclass mapapp.MapApp -outdir out -outfile MapApp
-description "MapApp description"
-title "MapApp Title"
-vendor "Company Name" -v

# Copy the new package
rm -f ./mapapp-installer.pkg
cp out/bundles/MapApp-*.pkg ./mapapp-installer.pkg

# List the results
ls -l


The key here is the "javapackager" utility, which is part of the installed JDK on my system.  The input sources to this are the MapApp.jar and mapapp.css files, as seen in the -srcfiles option.

The result is the file 'mapapp-installer.pkg' in my 'packaging' folder.  When I execute this, it installs my MapApp application in the usual macOS Applications folder, like any other application.

So, after all this, I've accomplished two things: a standard application installation and, when it executes, the title "MapApp" shows properly in the system menu bar.




Thursday, January 16, 2020

Opening an About dialog box with JavaFX

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

I was recently putting the finishing touches on a demo desktop application written in Java 8 using JavaFX. The UI consists of a single FXML file attached to a controller class. My next task was to add a nice looking About box - something better than an Alert.  So, I created an About.fxml using JavaFX Scene Builder 2.0.  I wanted to attach the About.fxml to the same application controller class I already had, so the About's OK button would dispatch its event there.  That, apparently, was a mistake that caused much grief, because loading the About.fxml kept giving me a non-specific LoadException error.  Postings at online help sites didn't seem to match my situation. Most of them dealt with file path problems to the user's FXML file, resulting in a NullPointerException, but my FXML is embedded within my application JAR file.

I eventually came to the conclusion that the FXMLLoader did not like loading a second FXML specifying the same controller class as my main FXML. So, how to load my About.fxml to get the result I wanted - its use of the same controller class?

Okay, here's what I did.  First, I removed any reference to a controller in About.fxml.  That meant I had to specify the controller some other way.  Here's my code in the controller class handler for the About menu event:


// load the About box FXML document
javafx.fxml.FXMLLoader loader = new javafx.fxml.FXMLLoader(
                    MyMainApp.class.getResource("About.fxml"));
loader.setController(this); // set 'this' as its controller
Parent root = loader.load(); // load UI objects from the FXML document

It turns out the magic I needed was the "setController" method above.  Specifying the controller class in the FXML file tries to create a new instance of the controller class, I assume, which must be the origin of my earlier LoadException error.  So, without that, the above loads fine.

Once the UI was loaded into a root Parent object (above), then it was time to configure the About dialog just the way I wanted: modal, no extra window decorations (i.e. Utility), owned by the main window, always on top, and not resizable ...

// create the dialog stage and set up its attributes
javafx.stage.Window owner = MyMainApp.getStage().getScene().getWindow();
Stage dialog = new Stage();
dialog.initStyle(StageStyle.UTILITY);
dialog.initModality(Modality.WINDOW_MODAL);
dialog.initOwner(owner);
dialog.setAlwaysOnTop(true);
dialog.setTitle("About MyMainApp");
dialog.setResizable(false);

Lastly, it was time to show the dialog:

// create the scene and set onto the stage
Scene scene = new Scene(root);
dialog.setScene(scene);
dialog.sizeToScene();
dialog.showAndWait();

At that point, my nice About box with a title and the system close button pops up!  The underlying main window cannot gain focus.  When I press the About's 'OK' button, I then get its event callback in my same controller class, which then closes the About box:

@FXML
public void onButtonHelpAboutDismissAction(ActionEvent event)
{
    Stage stage = (Stage)buttonHelpAboutDismiss.getScene().getWindow();
    stage.close();
}

Now, I'm sure there's a stupidly simpler way to do this in JavaFX, but I didn't see any other online examples for an About box like this. So, this is my contribution, as a relative Java newbie.

Any comments?