I have a Java project that I compile both into an exe with launch4j and a mac app with universalJavaApplicationStub. On Windows I can open a file with the exe (right-click→open with) and it will be passed as the first command line argument (in String[] args
).
On macOS I even registered DocumentTypes in the Info.plist. This activates the ability to drop files onto the app icon which was not possible before – so I at least know this part is set up correctly. However when I drop a file onto the app icon, do right-click→open with, or I run open myfile.txt -a myapp.app
the main function receives an empty list of arguments.
The open --help
command has the following line:
--args: All remaining arguments are passed in argv to the application's main() function instead of opened.
Using the parameter finally opens the file correctly. What does this description mean? How can I get the opened files instead of the arguments, so it works with right-click or dropping onto the app icon?
(I am using macOS Sonoma)
Using the comment from @Sweeper, I managed to figure out a solution using the OpenFilesHandler
API.
This is the minimal working example:
package myPackage;
import java.awt.Desktop;
import java.io.File;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.Timer;
import com.formdev.flatlaf.util.SystemInfo; // You probably need to do this differently
public class MyApp extends JFrame {
/**
* This is required since on macOS a file open handler function gets called <em>after</em> the end
* of the main function.
*/
private static boolean hasOpenedAnyFileYet = false;
public static void main(String[] args) {
if (SystemInfo.isMacOS) {
// On macOS this function is called when a new file is opened.
Desktop.getDesktop().setOpenFileHandler(new OpenMyAppFilesHandler());
}
System.out.println("Startup with arguments: " + Arrays.toString(args));
if (args.length == 0) {
// No file selected, ask user to open one. On macOS there is no argument, instead the opener's
// event function gets called. Thus we wait for a short while to let that function trigger and
// set a flag
Timer timer =
new Timer(
10,
ae -> {
if (!MyApp.hasOpenedAnyFileYet) MyApp.newFromFileDialogue();
});
timer.setRepeats(false);
timer.start();
} else {
// File selected, open it.
new MyApp (args[0].replace('/', File.separatorChar).replace('\\', File.separatorChar));
}
}
/** Ask the user to open a file and return its path. Might return {@link null}. */
private static String queryFilePath() {
JFileChooser fileChooser = new JFileChooser();
// If the user cancels, return null
if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
return fileChooser.getSelectedFile().getAbsolutePath();
} else {
return null;
}
}
/**
* Ask the user to open a file and display it. Return the MyApp object. {@link null} if the user
* cancels the interaction.
*/
public static MyApp newFromFileDialogue() {
String path = queryFilePath();
if (path != null) return new MyApp(path);
return null;
}
public MyApp(String path) {
MyApp.hasOpenedAnyFileYet = true;
// All the setup happens here
this.setVisible(true);
}
}
package myPackage;
import java.awt.desktop.OpenFilesEvent;
import java.awt.desktop.OpenFilesHandler;
import java.io.File;
/** This is what gets notified when a file is opened on macOS. */
public class OpenMyAppFilesHandlerimplements OpenFilesHandler {
@Override
public void openFiles(OpenFilesEvent e) {
for (File file : e.getFiles()) {
new MyApp(file.getAbsolutePath());
}
}
}