Search code examples
javamacoscocoajava-native-interface

How can I properly call NSOpenPanel from a JNI method to run on the proper thread?


I can get my JNI class to only partially execute due to my unfamiliarity with Objective-C and JNF. What I've tried so far is as follows:

Here's Open.java, which an MCV for a more significant Java Swing app that deserves a better-looking better-featured open file dialog than unreasonable java.awt.FileDialog or the ugly and less-featured javax.swing.JFileChooser.

import java.awt.event.*;
import javax.swing.*;
public class Open extends JFrame {
   public Open () {
      getContentPane().add(new JButton(new AbstractAction() {
         public void actionPerformed (ActionEvent e) {
            try {
               NativeOpenFileDialog.run();
            } catch (Throwable ex) {
               System.err.println(ex.getMessage());
            }
         }
      }));
      pack();
      setVisible(true);
   }
   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() { new Open(); }
      });
   }
}

Here's NativeOpenFileDialog.java

import javax.swing.*;

public class NativeOpenFileDialog {
   static {
       System.loadLibrary("natopndlg");
   };
   public static native void run ();
   public static void main (String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() { NativeOpenFileDialog.run(); }
      });

   }
}

Here's NativeOpenFileDialog.m

#import <Cocoa/Cocoa.h>

#include <jni.h>
#include "NativeOpenFileDialog.h"

JNIEXPORT void JNICALL Java_NativeOpenFileDialog_run (JNIEnv *thisEnv, jclass jcls) {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSOpenPanel *panel = [NSOpenPanel openPanel];
    [panel setCanChooseFiles:YES];
    [panel setCanChooseDirectories:YES];
    [panel setAllowsMultipleSelection:YES];
    if ([panel runModal] != NSModalResponseOK)
        NSLog(@"User did not press OK");
    [pool release];
}

And here's the makefile (I'm using Xcode 15.2):

SDK = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk

Open.class: Open.java NativeOpenFileDialog.java NativeOpenFileDialog.m
    javac -h . NativeOpenFileDialog.java
    javac Open.java
    gcc -dynamiclib -o libnatopndlg.dylib -framework Cocoa -isysroot $(SDK) -I $(JAVA_HOME)/include -I $(JAVA_HOME)/include/darwin -fobjc-exceptions -std=c99 NativeOpenFileDialog.m

And here's how I execute it after executing make.

java Open

Clicking the button raises the exception:

NSInternalInconsistencyException', reason: 'NSWindow should only be instantiated on the main thread!

Which is understandable knowing that the panel is supposed to be opened on the UI thread. I believe all I have to do is make sure the window created by NSOpenPanel is a child of the JFrame and opened on the UI thread. And I believe that at leasat means I would have to execute a Java function to get the NSWindow of the JFrame.


Solution

  • You can execute things on the UI thread using dispatch_async() and dispatch_sync(). NSObject also includes a method performSelectorOnMainThread:withObject:waitUntilDone: that will perform a selector on the main thread.

    This is a gist using the Java objective-c bridge that does something similar to this:

    https://gist.github.com/anonymous/3966989

    For your specific problem you could wrap the method body in dispatch_async

    JNIEXPORT void JNICALL Java_NativeOpenFileDialog_run (JNIEnv *thisEnv, jclass jcls) {
    dispatch_async(dispatch_get_main_queue(), ^{
      NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
      NSOpenPanel *panel = [NSOpenPanel openPanel];
      [panel setCanChooseFiles:YES];
      [panel setCanChooseDirectories:YES];
      [panel setAllowsMultipleSelection:YES];
      if ([panel runModal] != NSModalResponseOK)
          NSLog(@"User did not press OK");
      [pool release];
    });
    

    }

    Of course, this just opens the dialog and doesn't do anything with the file that the user selects, so you'll need to build on this.