Search code examples
javamacoscommand-line-argumentsprogram-entry-pointopen-with

Open a file with java application on macOS


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-clickopen 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-clickopen 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)


Solution

  • Using the comment from @Sweeper, I managed to figure out a solution using the OpenFilesHandler API. This is the minimal working example:

    MyApp.java

    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);
      }
    }
    

    OpenMyAppFilesHandler.java

    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());
        }
      }
    }