0

Adding File Associations to Java Desktop Apps

 2 years ago
source link: https://jdeploy.substack.com/p/file-associations?s=w
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Adding File Associations to Java Desktop Apps

Help your desktop app be a good citizen by adding file associations.

File associations are a great way to integrate your desktop app into the native desktop platform. In fact, if your app allows users to view or edit local files, but it doesn’t include file associations, then it is hostile to the user’s experience, and is effectively a 2nd-class citizen. Deploying to the desktop means integrating with the desktop. If your desktop app doesn’t include file associations, then why not?!

As someone who has shipped several small desktop apps that didn’t include file associations, I think I can answer the “why not”. It’s because it is a pain in the donkey to set up properly. This is partly a derivative of the main issue of native deployment being painful. E.g., If you’re shipping your app as an executable jar file, then native file associations just aren’t in the cards. But many apps that do ship as native bundles still don’t include file associations.

First, let me back up a little and explain what I mean by “file associations”. A file association is an association between a file type and your app. For example, if your app allows users to view .png files, then users expect to see your app listed in the “Open With…” context menu when they right-click on a .png file. Similarly, users expect to be able to set your app as the default app for viewing .png files, in which case when the user double-clicks a .png file, it automatically opens the image in your app. Additionally, users should be able to drag a .png file onto your app’s icon, and have it opened in your app.

It sounds like such a small detail, but it is the difference between your app getting used, and your app getting trashed.

Since jDeploy aims to produce a truly native experience for Java desktop apps, it is a given that it must include support for file associations. In this article I will go behind the scenes and describe the mechanics of how file associations work on Windows, Mac, and Linux. Each platform handles them in their own unique way. I’ll also describe how you can add file associations to your own apps in a cross-platform way, using jDeploy. In fact, I’ll begin with the latter (how to add file associations), and then peel back the curtain and show how it works on each platform.

How to add file associations in jDeploy

File associations are managed through the documentTypes property of the jdeploy objectof the package.json file. E.g., the following configuration in the package.json file will associate the app as a viewer for .html and .jdtext files, and an editor for .txt files.

...
"jdeploy" : {

    ...
    "documentTypes" : [
      {
        "extension" : "txt",
        "mimetype" : "text/plain",
        "editor" : true
      }, {
        "extension" : "html",
        "mimetype" : "text/html"
      }, {
        "extension" : "jdtext",
        "mimetype" : "application/x-jdeploy-demo-texteditor-jdtext"
      }, ....
    ],
}
...

In the case of the .jdtext file extension, it is a custom extension that I created for that app so that it can have its own file type.

If you don’t want to mess with JSON files, you can also manage these associations in the “Filetypes” tab of the jDeploy GUI as shown below.

The “Custom” checkbox in each row allows you to explicitly mark a file type as a custom type for your app - as opposed to a well-known type like html, txt, or png. If the mimetype starts with “application/x-jdeploy-”, then it is automatically treated as a “custom” type. The concept of a custom type is helpful in Linux specifically, as it needs to register custom mimetypes with the system.

Handling file associations in your app

Adding the file associations in jDeploy will ensure that your app is launched when the user tries to open a file of the specified type, but your app still needs to do the right thing. For example, your app must detect when it is being opened via a file association, and act accordingly. This usually means opening the file that triggered the launch so that the user can view or edit it.

On Windows and Linux, the file that triggered your app launch will be provided as arguments of your main(String[]) method. On Mac, you will need to use the java.awt.desktop.OpenFilesHandler interface1 to receive the openFiles event.

The following listing shows an example application that will handle the “open file” event on all platforms:

import java.awt.*;
import java.awt.desktop.OpenFilesHandler;
import java.awt.desktop.OpenFilesEvent;

public class HelloApplication {
    static {
        try {
            Desktop.getDesktop().setOpenFileHandler(new FileHandler());
        } catch (Exception ex){}
    }

    public static class FileHandler implements OpenFilesHandler {

        @Override
        public void openFiles(OpenFilesEvent e) {
            // Handle file open event on Mac
            System.out.println("Received open files event "+e.toString());
        }
    }

    public static void main(String[] args) {

        if (args.length > 0) {
            // Handle open event on Windows and Linux
            System.out.println("Received "+args.length+" file arguments");
            for (int i=0; i<arg.length; i++) {
    
                System.out.println("File: "+args[i]);
            }
        }
    }
}

For more information about adding file associations using jDeploy, see the File Associations section of the jDeploy Developer’s guide.

If you only read this article to find out how to add file associations to your Java app, then you’re done. You can skip to the end, where I encourage you to comment, like and subscribe :)

The rest of the article is for those of you who want to know how it all works. What follows is a semi-deep dive into how Mac, Windows, and Linux deal with file associations.

Under the Hood: How File Associations are Implemented

Mac OS

On Mac, adding file associations was pretty straight forward. I simply needed to add some entries into the app’s Info.plist file. There was no need to update any shared system files. Mac OS seems to register the file associations automatically the first time you launch the app.

The example associations I described in the previous section (for .txt, .html., and .jdtext) would result in the following snippet being included in the Info.plist file.

<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>CFBundleTypeExtensions</key>
        <array>
            <string>txt</string>
        </array>
        <key>CFBundleTypeName</key>
        <string>txt</string>
        <key>CFBundleTypeMIMETypes</key>
        <array>
            <string>text/plain</string>
        </array>
        <key>CFBundleTypeRole</key>
        <string>Viewer</string>
    </dict>
    <dict>
        <key>CFBundleTypeExtensions</key>
        <array>
            <string>html</string>
        </array>
        <key>CFBundleTypeName</key>
        <string>html</string>
        <key>CFBundleTypeMIMETypes</key>
        <array>
            <string>text/html</string>
        </array>
        <key>CFBundleTypeRole</key>
        <string>Viewer</string>
    </dict>
    <dict>
        <key>CFBundleTypeExtensions</key>
        <array>
            <string>jdtext</string>
        </array>
        <key>CFBundleTypeName</key>
        <string>jdtext</string>
        <key>CFBundleTypeMIMETypes</key>
        <array>
            <string>application/x-jdeploy-demo-texteditor-jdtext</string>
        </array>
        <key>CFBundleTypeRole</key>
        <string>Viewer</string>
    </dict>
</array>

Windows

I had a terrible time figuring out how to implement file associations on Windows. Unlike Mac, which allowed me to simply add some configuration directives inside the app, Windows required me to, …*gasp!*…, modify the system registry!

I sifted through pages of Google results, Stack Overflow threads, and message forums from the early 2000’s to try to find the winning formula. I found lots of information, on the topic, but much of it was contradictory, and likely out-dated. I even bought a couple of thick Microsoft Press books on Windows internals in the hope that they might offer some guidance. I must have bought the wrong books, as I was disappointed to open my Amazon parcels to find that, despite the books claiming to provide authoritative information on the Windows registry, they included next to nothing on file associations, and how they need to be defined in the registry.

In the end, I was forced to resort to exploring my own system registry using regedit to see how other apps handled their associations. Here’s what I found.

For each file extension I want to associate, I needed to generate a “program ID” which identifies a mapping between my app and that file extension. This program ID, which will be used as the name of a child node under the HKEY_CURRENT_USER\SOFTWARE\Classes node, can be any string you like, as long as it doesn’t clobber any existing node names under HKEY_CURRENT_USER\SOFTWARE\Classes. For jDeploy I used the pattern “jdeploy.{{package-name}}.{{extension}}” for my program IDs. E.g. for the “.txt” association in my app which is deployed with the package name “jdeploy-demo-texteditor”, the program ID is “jdeploy.jdeploy-demo-texteditor.txt”. This program ID will be used in several different parts of the registry to refer to my app.

The following screenshot shows the structure of this node in regedit. The node itself provides a default string entry with a brief description of the file type. “TextEditor txt File”. This string was generated based on the app name, “TextEditor” and the “.txt” file extension.

This node includes two child nodes: “DefaultIcon”, which specifies the path to an icon to use for files of this type, and “shell”, which specifies the command that will be used to launch my app when users try to open files of this type using my app.

Below is a screenshot of the “DefaultIcon” node entries:

It simply includes a default entry with the path to the app’s icon.

The “shell” node itself doesn’t contain any entries, but its “open” child node includes an “Icon” entry that points to the app icon, once again. I think this is used in the “Open With…” context menu when you right click on a file.

Finally, the “command” child node contains a default entry that specifies the command that should be used to launch the app when the user tries to open a file of this type.

Notice the form of the command:

“path\to\MyApp.exe” “%1”.

The “%1” is a placeholder for the file path that the user is trying to open. This effectively passes the file path as the first argument in the main(String[]) method of the app.

We’re not quite done yet. In addition to defining our “program ID” node, we also need to register our “program ID” with the file extension’s node itself. For each file extension, the registry includes an entry like HKEY_CURRENT_USER\SOFTWARE\Classes\.txt. Replace “.txt” with the file extension you wish to associate with your app. To consummate the file association, we need to add our program ID as an entry in the HKEY_CURRENT_USER\SOFTWARE\Classes\.txt\OpenWithProgids node. (Again, replace “.txt” with the file extension you wish to associate with your app). The following screenshot shows the entry that I added for the .txt extension.

The only entry I added for my app was jdeploy.jdeploy-demo-texteditor.txt. The other entries were already there.

If you’re looking for more specifics about how I did the registry modifications, you can check out the source code here.

Linux

On Linux, desktop integration is achieved by creating a .desktop file for your app. This file includes meta-data for your app, including its name, icon, launch command, and file associations. If you place this file inside the $HOME/.local/share/applications directory, then your app will be listed in the “Programs” menu of the desktop manager.

The .desktop file for my TextEditor app looks like the following snippet:

[Desktop Entry]
Version=1.0
Type=Application
Name=TextEditor
Icon=/home/admin/.jdeploy/apps/jdeploy-demo-texteditor/icon.png
Exec="/home/admin/.jdeploy/apps/jdeploy-demo-texteditor/TextEditor" %U
Comment=Launch TextEditor
Terminal=false
MimeType=text/plain;text/html;x-scheme-handler/jdtext

The two entries that relate to file associations are “Exec” and “MimeType”.

The MimeType entry is a list of mime-types that are to be associated with the app, delimited by semi-colons. The Exec entry specifies the launch command for the app. The “%U” placeholder in that command will be replaced by the list of files, directories, or URLs which triggered the launch. This effectively passes the paths of each file as arguments to the main(String[]) method.

For well-known (registered) mime-types, there isn’t anything else required. However, if we are defining our own custom mime-types, we need to register these with the system.

Linux provides a command-line tool named xdg-mime, which will handle the system-level details of mime-type registration. All I needed to do was craft a “shared-mime-info” XML file that conforms to the published specifications, and pass it to xdg-mime, and Linux pretty much took care of the rest.

As an example, a valid shared-mime-info file for the .jdtext association looks like:

<?xml version="1.0"?>
<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>
    <mime-type type="application/x-jdeploy-demo-texteditor-jdtext">
        <comment>Text Editor Document</comment>
        <glob pattern="*.jdtext"/>
    </mime-type>
</mime-info>

Assuming these contents are in a file named “my-mime-info.xml”, then you can register it by running:

xdg-mime install --mode user path/to/my-mime-info.xml

Changes won’t take effect until you either restart the computer, or run the update-mime-database command:

update-mime-database $HOME/share/mime

Now that wasn’t so bad

After trudging through the weeds of native file associations, it’s clear why most Java desktop apps have historically neglected them. It’s painful! However, with jDeploy, it is now so easy to add support for them, that there is no excuse not to. Add them today. Your users will thank you.

I’m writing this newsletter to help raise awareness to the awesomeness that Java still has to offer on the desktop. Articles are a mix of editorials, tutorials, and historical melodramas related to Java and the desktop. I try to post a new one every week. If you enjoy reading this newsletter, please consider subscribing, so that you will be informed first when new articles are published. Also, please comment below if this post has inspired any thoughts, memories, or rants. I always enjoy reading other’s perspectives on these topics.

1

The OpenFilesHandler interface, and many other Desktop integration APIs were added in JDK 11. Prior to that you needed to use a proprietary library developed by Apple to handle most desktop integration.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK