From 078e59a44387d60638a904d1919e638f365869f9 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 12 Jan 2025 18:16:40 +0100 Subject: [PATCH] System File Chooser: support "approve" callback and system message dialog on macOS (not yet used in `SystemFileChooser` --- .../flatlaf/ui/FlatNativeMacLibrary.java | 30 +++- .../flatlaf/util/SystemFileChooser.java | 4 +- .../src/main/headers/JNIUtils.h | 38 +++- ..._formdev_flatlaf_ui_FlatNativeMacLibrary.h | 12 +- .../src/main/objcpp/MacFileChooser.mm | 168 ++++++++++++++++-- .../testing/FlatSystemFileChooserMacTest.java | 26 ++- .../testing/FlatSystemFileChooserMacTest.jfd | 6 + 7 files changed, 252 insertions(+), 32 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java index 9308a680..362f8ee3 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java @@ -18,6 +18,7 @@ package com.formdev.flatlaf.ui; import java.awt.Rectangle; import java.awt.Window; +import javax.swing.JOptionPane; import com.formdev.flatlaf.util.SystemInfo; /** @@ -122,5 +123,32 @@ public class FlatNativeMacLibrary public native static String[] showFileChooser( boolean open, String title, String prompt, String message, String filterFieldLabel, String nameFieldLabel, String nameFieldStringValue, String directoryURL, - int optionsSet, int optionsClear, int fileTypeIndex, String... fileTypes ); + int optionsSet, int optionsClear, FileChooserCallback callback, + int fileTypeIndex, String... fileTypes ); + + /** @since 3.6 */ + public interface FileChooserCallback { + boolean approve( String[] files, long hwndFileDialog ); + } + + /** + * Shows a macOS alert + * NSAlert. + *

+ * For use in {@link FileChooserCallback} only. + * + * @param hwndParent the parent of the message box + * @param alertStyle type of alert being displayed: + * {@link JOptionPane#ERROR_MESSAGE}, {@link JOptionPane#INFORMATION_MESSAGE} or + * {@link JOptionPane#WARNING_MESSAGE} + * @param messageText main message of the alert + * @param informativeText additional information about the alert; shown below of main message; or {@code null} + * @param defaultButton index of the default button, which can be pressed using ENTER key + * @param buttons texts of the buttons; if no buttons given the a default "OK" button is shown + * @return index of pressed button + * + * @since 3.6 + */ + public native static int showMessageDialog( long hwndParent, int alertStyle, + String messageText, String informativeText, int defaultButton, String... buttons ); } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemFileChooser.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemFileChooser.java index b4528f62..f3740102 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemFileChooser.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemFileChooser.java @@ -616,8 +616,8 @@ public class SystemFileChooser // show system file dialog return FlatNativeMacLibrary.showFileChooser( open, fc.getDialogTitle(), fc.getApproveButtonText(), null, null, null, - nameFieldStringValue, directoryURL, - optionsSet, optionsClear, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); + nameFieldStringValue, directoryURL, optionsSet, optionsClear, null, + fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); } } diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h index 36b1bd4c..b2dfe674 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h @@ -29,15 +29,43 @@ #endif +#define JNI_COCOA_TRY() \ + @try { + +#define JNI_COCOA_CATCH() \ + } @catch( NSException *ex ) { \ + NSLog( @"Exception: %@\nReason: %@\nUser Info: %@\nStack: %@", \ + [ex name], [ex reason], [ex userInfo], [ex callStackSymbols] ); \ + } + #define JNI_COCOA_ENTER() \ @autoreleasepool { \ - @try { + JNI_COCOA_TRY() #define JNI_COCOA_EXIT() \ - } @catch( NSException *ex ) { \ - NSLog( @"Exception: %@\nReason: %@\nUser Info: %@\nStack: %@", \ - [ex name], [ex reason], [ex userInfo], [ex callStackSymbols] ); \ - } \ + JNI_COCOA_CATCH() \ + } + +#define JNI_THREAD_ENTER( jvm, returnValue ) \ + JNIEnv *env; \ + bool detach = false; \ + switch( jvm->GetEnv( (void**) &env, JNI_VERSION_1_6 ) ) { \ + case JNI_OK: break; \ + case JNI_EDETACHED: \ + if( jvm->AttachCurrentThread( (void**) &env, NULL ) != JNI_OK ) \ + return returnValue; \ + detach = true; \ + break; \ + default: return returnValue; \ + } \ + @try { + +#define JNI_THREAD_EXIT( jvm ) \ + } @finally { \ + if( env->ExceptionCheck() ) \ + env->ExceptionDescribe(); \ + if( detach ) \ + jvm->DetachCurrentThread(); \ } diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h index 1e581622..329085b0 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h @@ -82,10 +82,18 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_togg /* * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary * Method: showFileChooser - * Signature: (ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;III[Ljava/lang/String;)[Ljava/lang/String; + * Signature: (ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILcom/formdev/flatlaf/ui/FlatNativeMacLibrary/FileChooserCallback;I[Ljava/lang/String;)[Ljava/lang/String; */ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_showFileChooser - (JNIEnv *, jclass, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jint, jint, jint, jobjectArray); + (JNIEnv *, jclass, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jint, jint, jobject, jint, jobjectArray); + +/* + * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary + * Method: showMessageDialog + * Signature: (JILjava/lang/String;Ljava/lang/String;I[Ljava/lang/String;)I + */ +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_showMessageDialog + (JNIEnv *, jclass, jlong, jint, jstring, jstring, jint, jobjectArray); #ifdef __cplusplus } diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm index d07caec2..a782ca74 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -26,10 +26,19 @@ * @since 3.6 */ +// declare internal methods +static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ); +static jobjectArray urlsToStringArray( JNIEnv* env, NSArray* urls ); +static NSArray* getDialogURLs( NSSavePanel* dialog ); + //---- class FileChooserDelegate ---------------------------------------------- -@interface FileChooserDelegate : NSObject { +@interface FileChooserDelegate : NSObject { NSArray* _filters; + + JavaVM* _jvm; + jobject _callback; + NSMutableSet* _urlsSet; } @property (nonatomic, assign) NSSavePanel* dialog; @@ -118,6 +127,53 @@ _dialog.allowedFileTypes = [fileTypes containsObject:@"*"] ? nil : fileTypes; } + //---- NSOpenSavePanelDelegate ---- + + - (void)initCallback: (JavaVM*)jvm :(jobject)callback { + _jvm = jvm; + _callback = callback; + } + + - (BOOL) panel:(id) sender validateURL:(NSURL*) url error:(NSError**) outError { + JNI_COCOA_TRY() + + NSArray* urls = getDialogURLs( sender ); + + // if multiple files are selected for opening, then the validateURL method + // is invoked for earch file, but our callback should be invoked only once for all files + if( urls != NULL && urls.count > 1 ) { + if( _urlsSet == NULL ) { + // invoked for first selected file --> invoke callback + _urlsSet = [NSMutableSet setWithArray:urls]; + [_urlsSet removeObject:url]; + } else { + // invoked for other selected files --> do not invoke callback + [_urlsSet removeObject:url]; + if( _urlsSet.count == 0 ) + _urlsSet = NULL; + return true; + } + } + + JNI_THREAD_ENTER( _jvm, true ) + + jobjectArray files = urlsToStringArray( env, urls ); + jlong window = (jlong) sender; + + // invoke callback: boolean approve( String[] files, long hwnd ); + jclass cls = env->GetObjectClass( _callback ); + jmethodID approveID = env->GetMethodID( cls, "approve", "([Ljava/lang/String;J)Z" ); + if( approveID != NULL && !env->CallBooleanMethod( _callback, approveID, files, window ) ) { + _urlsSet = NULL; + return false; // keep dialog open + } + + JNI_THREAD_EXIT( _jvm ) + JNI_COCOA_CATCH() + + return true; + } + @end //---- helper ----------------------------------------------------------------- @@ -126,9 +182,6 @@ #define isOptionClear( option ) ((optionsClear & com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ## option) != 0) #define isOptionSetOrClear( option ) isOptionSet( option ) || isOptionClear( option ) -// declare external methods -extern NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window ); - static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { jclass stringClass = env->FindClass( "java/lang/String" ); return env->NewObjectArray( count, stringClass, NULL ); @@ -182,10 +235,14 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ( JNIEnv* env, jclass cls, jboolean open, jstring title, jstring prompt, jstring message, jstring filterFieldLabel, jstring nameFieldLabel, jstring nameFieldStringValue, jstring directoryURL, - jint optionsSet, jint optionsClear, jint fileTypeIndex, jobjectArray fileTypes ) + jint optionsSet, jint optionsClear, jobject callback, jint fileTypeIndex, jobjectArray fileTypes ) { JNI_COCOA_ENTER() + JavaVM* jvm; + if( env->GetJavaVM( &jvm ) != JNI_OK ) + return NULL; + // convert Java strings to NSString (on Java thread) NSString* nsTitle = JavaToNSString( env, title ); NSString* nsPrompt = JavaToNSString( env, prompt ); @@ -198,11 +255,11 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ NSArray* urls = NULL; NSArray** purls = &urls; - NSURL* url = NULL; - NSURL** purl = &url; // show file dialog on macOS thread [FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){ + JNI_COCOA_TRY() + NSSavePanel* dialog = open ? [NSOpenPanel openPanel] : [NSSavePanel savePanel]; if( nsTitle != NULL ) @@ -262,35 +319,108 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ((NSOpenPanel*)dialog).accessoryViewDisclosed = isOptionSet( FC_accessoryViewDisclosed ); } + // initialize callback + if( callback != NULL ) { + [delegate initCallback :jvm :callback]; + dialog.delegate = delegate; + } + // show dialog NSModalResponse response = [dialog runModal]; [delegate release]; - if( response != NSModalResponseOK ) + if( response != NSModalResponseOK ) { + *purls = @[]; return; + } - if( open ) - *purls = ((NSOpenPanel*)dialog).URLs; - else - *purl = dialog.URL; + *purls = getDialogURLs( dialog ); + + JNI_COCOA_CATCH() }]; - if( url != NULL ) - urls = @[url]; - if( urls == NULL ) - return newJavaStringArray( env, 0 ); + return NULL; // convert URLs to Java string array - jsize count = urls.count; + return urlsToStringArray( env, urls ); + + JNI_COCOA_EXIT() +} + +static NSArray* getDialogURLs( NSSavePanel* dialog ) { + if( [dialog isKindOfClass:[NSOpenPanel class]] ) + return static_cast(dialog).URLs; + + NSURL* url = dialog.URL; + // use '[[NSArray alloc] initWithObject:url]' here because '@[url]' crashes on macOS 10.14 + return (url != NULL) ? [[NSArray alloc] initWithObject:url] : @[]; +} + +static jobjectArray urlsToStringArray( JNIEnv* env, NSArray* urls ) { + jsize count = (urls != NULL) ? urls.count : 0; jobjectArray array = newJavaStringArray( env, count ); for( int i = 0; i < count; i++ ) { jstring filename = NormalizedPathJavaFromNSString( env, [urls[i] path] ); env->SetObjectArrayElement( array, i, filename ); env->DeleteLocalRef( filename ); } - return array; +} + +extern "C" +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_showMessageDialog + ( JNIEnv* env, jclass cls, jlong hwndParent, jint alertStyle, jstring messageText, jstring informativeText, + jint defaultButton, jobjectArray buttons ) +{ + JNI_COCOA_ENTER() + + // convert Java strings to NSString (on Java thread) + NSString* nsMessageText = JavaToNSString( env, messageText ); + NSString* nsInformativeText = JavaToNSString( env, informativeText ); + + jint buttonCount = env->GetArrayLength( buttons ); + NSMutableArray* nsButtons = [NSMutableArray array]; + for( int i = 0; i < buttonCount; i++ ) { + NSString* nsButton = JavaToNSString( env, (jstring) env->GetObjectArrayElement( buttons, i ) ); + [nsButtons addObject:nsButton]; + } + + jint result = -1; + jint* presult = &result; + + // show alert on macOS thread + [FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){ + NSAlert* alert = [[NSAlert alloc] init]; + + // use empty string because if alert.messageText is not set it displays "Alert" + alert.messageText = (nsMessageText != NULL) ? nsMessageText : @""; + if( nsInformativeText != NULL ) + alert.informativeText = nsInformativeText; + + // alert style + switch( alertStyle ) { + case /* JOptionPane.ERROR_MESSAGE */ 0: alert.alertStyle = NSAlertStyleCritical; break; + default: + case /* JOptionPane.INFORMATION_MESSAGE */ 1: alert.alertStyle = NSAlertStyleInformational; break; + case /* JOptionPane.WARNING_MESSAGE */ 2: alert.alertStyle = NSAlertStyleWarning; break; + } + + // add buttons + for( int i = 0; i < nsButtons.count; i++ ) { + NSButton* b = [alert addButtonWithTitle:nsButtons[i]]; + if( i == defaultButton ) + alert.window.defaultButtonCell = b.cell; + } + + // show alert + NSInteger response = [alert runModal]; + + // if no buttons added, which shows a single OK button, the response is 0 when clicking OK + // if buttons added, response is 1000+buttonIndex + *presult = MAX( response - NSAlertFirstButtonReturn, 0 ); + }]; + + return result; JNI_COCOA_EXIT() } - diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.java index 99d2297b..de339d50 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.java @@ -59,7 +59,7 @@ public class FlatSystemFileChooserMacTest } FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserMacTest" ); - addListeners( frame ); +// addListeners( frame ); frame.showFrame( FlatSystemFileChooserMacTest::new ); } ); } @@ -133,11 +133,24 @@ public class FlatSystemFileChooserMacTest } int fileTypeIndex = fileTypeIndexSlider.getValue(); + FlatNativeMacLibrary.FileChooserCallback callback = (files, hwndFileDialog) -> { + System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) ); + if( showMessageDialogOnOKCheckBox.isSelected() ) { + int result = FlatNativeMacLibrary.showMessageDialog( hwndFileDialog, + JOptionPane.INFORMATION_MESSAGE, + "primary text", "secondary text", 0, "Yes", "No" ); + System.out.println( " result " + result ); + if( result != 0 ) + return false; + } + return true; + }; + if( direct ) { String[] files = FlatNativeMacLibrary.showFileChooser( open, title, prompt, message, filterFieldLabel, nameFieldLabel, nameFieldStringValue, directoryURL, - optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes ); + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes ); filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); } else { @@ -148,7 +161,7 @@ public class FlatSystemFileChooserMacTest String[] files = FlatNativeMacLibrary.showFileChooser( open, title, prompt, message, filterFieldLabel, nameFieldLabel, nameFieldStringValue, directoryURL, - optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes2 ); + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 ); System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); @@ -173,6 +186,7 @@ public class FlatSystemFileChooserMacTest optionsClear.set( optionsClear.get() | option ); } + @SuppressWarnings( "unused" ) private static void addListeners( Window w ) { w.addWindowListener( new WindowListener() { @Override @@ -270,6 +284,7 @@ public class FlatSystemFileChooserMacTest saveButton = new JButton(); openDirectButton = new JButton(); saveDirectButton = new JButton(); + showMessageDialogOnOKCheckBox = new JCheckBox(); filesScrollPane = new JScrollPane(); filesField = new JTextArea(); @@ -468,6 +483,10 @@ public class FlatSystemFileChooserMacTest saveDirectButton.addActionListener(e -> saveDirect()); add(saveDirectButton, "cell 0 10 3 1"); + //---- showMessageDialogOnOKCheckBox ---- + showMessageDialogOnOKCheckBox.setText("show message dialog on OK"); + add(showMessageDialogOnOKCheckBox, "cell 0 10 3 1"); + //======== filesScrollPane ======== { @@ -519,6 +538,7 @@ public class FlatSystemFileChooserMacTest private JButton saveButton; private JButton openDirectButton; private JButton saveDirectButton; + private JCheckBox showMessageDialogOnOKCheckBox; private JScrollPane filesScrollPane; private JTextArea filesField; // JFormDesigner - End of variables declaration //GEN-END:variables diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.jfd index 02b47359..83d73f63 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.jfd @@ -257,6 +257,12 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 10 3 1" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showMessageDialogOnOKCheckBox" + "text": "show message dialog on OK" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 10 3 1" + } ) add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "filesScrollPane" add( new FormComponent( "javax.swing.JTextArea" ) {