System File Chooser: support "approve" callback and system message dialog on macOS (not yet used in SystemFileChooser
Some checks failed
CI / build (11, ) (push) Has been cancelled
CI / build (17, ) (push) Has been cancelled
CI / build (21, ) (push) Has been cancelled
CI / build (23, ) (push) Has been cancelled
CI / build (8, ) (push) Has been cancelled
Native Libraries / Natives (macos) (push) Has been cancelled
Native Libraries / Natives (ubuntu) (push) Has been cancelled
Native Libraries / Natives (windows) (push) Has been cancelled
CI / snapshot (push) Has been cancelled
CI / release (push) Has been cancelled

This commit is contained in:
Karl Tauber
2025-01-12 18:16:40 +01:00
parent d49282dfe8
commit 078e59a443
7 changed files with 252 additions and 32 deletions

View File

@@ -18,6 +18,7 @@ package com.formdev.flatlaf.ui;
import java.awt.Rectangle; import java.awt.Rectangle;
import java.awt.Window; import java.awt.Window;
import javax.swing.JOptionPane;
import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.SystemInfo;
/** /**
@@ -122,5 +123,32 @@ public class FlatNativeMacLibrary
public native static String[] showFileChooser( boolean open, public native static String[] showFileChooser( boolean open,
String title, String prompt, String message, String filterFieldLabel, String title, String prompt, String message, String filterFieldLabel,
String nameFieldLabel, String nameFieldStringValue, String directoryURL, 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
* <a href="https://developer.apple.com/documentation/appkit/nsalert?language=objc">NSAlert</a>.
* <p>
* 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 );
} }

View File

@@ -616,8 +616,8 @@ public class SystemFileChooser
// show system file dialog // show system file dialog
return FlatNativeMacLibrary.showFileChooser( open, return FlatNativeMacLibrary.showFileChooser( open,
fc.getDialogTitle(), fc.getApproveButtonText(), null, null, null, fc.getDialogTitle(), fc.getApproveButtonText(), null, null, null,
nameFieldStringValue, directoryURL, nameFieldStringValue, directoryURL, optionsSet, optionsClear, null,
optionsSet, optionsClear, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) );
} }
} }

View File

@@ -29,15 +29,43 @@
#endif #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() \ #define JNI_COCOA_ENTER() \
@autoreleasepool { \ @autoreleasepool { \
@try { JNI_COCOA_TRY()
#define JNI_COCOA_EXIT() \ #define JNI_COCOA_EXIT() \
} @catch( NSException *ex ) { \ JNI_COCOA_CATCH() \
NSLog( @"Exception: %@\nReason: %@\nUser Info: %@\nStack: %@", \ }
[ex name], [ex reason], [ex userInfo], [ex callStackSymbols] ); \
} \ #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(); \
} }

View File

@@ -82,10 +82,18 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_togg
/* /*
* Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary
* Method: showFileChooser * 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 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 #ifdef __cplusplus
} }

View File

@@ -26,10 +26,19 @@
* @since 3.6 * @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 ---------------------------------------------- //---- class FileChooserDelegate ----------------------------------------------
@interface FileChooserDelegate : NSObject { @interface FileChooserDelegate : NSObject <NSOpenSavePanelDelegate> {
NSArray* _filters; NSArray* _filters;
JavaVM* _jvm;
jobject _callback;
NSMutableSet* _urlsSet;
} }
@property (nonatomic, assign) NSSavePanel* dialog; @property (nonatomic, assign) NSSavePanel* dialog;
@@ -118,6 +127,53 @@
_dialog.allowedFileTypes = [fileTypes containsObject:@"*"] ? nil : fileTypes; _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 @end
//---- helper ----------------------------------------------------------------- //---- helper -----------------------------------------------------------------
@@ -126,9 +182,6 @@
#define isOptionClear( option ) ((optionsClear & com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ## option) != 0) #define isOptionClear( option ) ((optionsClear & com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ## option) != 0)
#define isOptionSetOrClear( option ) isOptionSet( option ) || isOptionClear( option ) #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 ) { static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) {
jclass stringClass = env->FindClass( "java/lang/String" ); jclass stringClass = env->FindClass( "java/lang/String" );
return env->NewObjectArray( count, stringClass, NULL ); 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, ( JNIEnv* env, jclass cls, jboolean open,
jstring title, jstring prompt, jstring message, jstring filterFieldLabel, jstring title, jstring prompt, jstring message, jstring filterFieldLabel,
jstring nameFieldLabel, jstring nameFieldStringValue, jstring directoryURL, 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() JNI_COCOA_ENTER()
JavaVM* jvm;
if( env->GetJavaVM( &jvm ) != JNI_OK )
return NULL;
// convert Java strings to NSString (on Java thread) // convert Java strings to NSString (on Java thread)
NSString* nsTitle = JavaToNSString( env, title ); NSString* nsTitle = JavaToNSString( env, title );
NSString* nsPrompt = JavaToNSString( env, prompt ); NSString* nsPrompt = JavaToNSString( env, prompt );
@@ -198,11 +255,11 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_
NSArray* urls = NULL; NSArray* urls = NULL;
NSArray** purls = &urls; NSArray** purls = &urls;
NSURL* url = NULL;
NSURL** purl = &url;
// show file dialog on macOS thread // show file dialog on macOS thread
[FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){ [FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){
JNI_COCOA_TRY()
NSSavePanel* dialog = open ? [NSOpenPanel openPanel] : [NSSavePanel savePanel]; NSSavePanel* dialog = open ? [NSOpenPanel openPanel] : [NSSavePanel savePanel];
if( nsTitle != NULL ) if( nsTitle != NULL )
@@ -262,35 +319,108 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_
((NSOpenPanel*)dialog).accessoryViewDisclosed = isOptionSet( FC_accessoryViewDisclosed ); ((NSOpenPanel*)dialog).accessoryViewDisclosed = isOptionSet( FC_accessoryViewDisclosed );
} }
// initialize callback
if( callback != NULL ) {
[delegate initCallback :jvm :callback];
dialog.delegate = delegate;
}
// show dialog // show dialog
NSModalResponse response = [dialog runModal]; NSModalResponse response = [dialog runModal];
[delegate release]; [delegate release];
if( response != NSModalResponseOK ) if( response != NSModalResponseOK ) {
*purls = @[];
return; return;
}
if( open ) *purls = getDialogURLs( dialog );
*purls = ((NSOpenPanel*)dialog).URLs;
else JNI_COCOA_CATCH()
*purl = dialog.URL;
}]; }];
if( url != NULL )
urls = @[url];
if( urls == NULL ) if( urls == NULL )
return newJavaStringArray( env, 0 ); return NULL;
// convert URLs to Java string array // 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<NSOpenPanel*>(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 ); jobjectArray array = newJavaStringArray( env, count );
for( int i = 0; i < count; i++ ) { for( int i = 0; i < count; i++ ) {
jstring filename = NormalizedPathJavaFromNSString( env, [urls[i] path] ); jstring filename = NormalizedPathJavaFromNSString( env, [urls[i] path] );
env->SetObjectArrayElement( array, i, filename ); env->SetObjectArrayElement( array, i, filename );
env->DeleteLocalRef( filename ); env->DeleteLocalRef( filename );
} }
return array; 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() JNI_COCOA_EXIT()
} }

View File

@@ -59,7 +59,7 @@ public class FlatSystemFileChooserMacTest
} }
FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserMacTest" ); FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserMacTest" );
addListeners( frame ); // addListeners( frame );
frame.showFrame( FlatSystemFileChooserMacTest::new ); frame.showFrame( FlatSystemFileChooserMacTest::new );
} ); } );
} }
@@ -133,11 +133,24 @@ public class FlatSystemFileChooserMacTest
} }
int fileTypeIndex = fileTypeIndexSlider.getValue(); 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 ) { if( direct ) {
String[] files = FlatNativeMacLibrary.showFileChooser( open, String[] files = FlatNativeMacLibrary.showFileChooser( open,
title, prompt, message, filterFieldLabel, title, prompt, message, filterFieldLabel,
nameFieldLabel, nameFieldStringValue, directoryURL, 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" ); filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" );
} else { } else {
@@ -148,7 +161,7 @@ public class FlatSystemFileChooserMacTest
String[] files = FlatNativeMacLibrary.showFileChooser( open, String[] files = FlatNativeMacLibrary.showFileChooser( open,
title, prompt, message, filterFieldLabel, title, prompt, message, filterFieldLabel,
nameFieldLabel, nameFieldStringValue, directoryURL, nameFieldLabel, nameFieldStringValue, directoryURL,
optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes2 ); optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 );
System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() );
@@ -173,6 +186,7 @@ public class FlatSystemFileChooserMacTest
optionsClear.set( optionsClear.get() | option ); optionsClear.set( optionsClear.get() | option );
} }
@SuppressWarnings( "unused" )
private static void addListeners( Window w ) { private static void addListeners( Window w ) {
w.addWindowListener( new WindowListener() { w.addWindowListener( new WindowListener() {
@Override @Override
@@ -270,6 +284,7 @@ public class FlatSystemFileChooserMacTest
saveButton = new JButton(); saveButton = new JButton();
openDirectButton = new JButton(); openDirectButton = new JButton();
saveDirectButton = new JButton(); saveDirectButton = new JButton();
showMessageDialogOnOKCheckBox = new JCheckBox();
filesScrollPane = new JScrollPane(); filesScrollPane = new JScrollPane();
filesField = new JTextArea(); filesField = new JTextArea();
@@ -468,6 +483,10 @@ public class FlatSystemFileChooserMacTest
saveDirectButton.addActionListener(e -> saveDirect()); saveDirectButton.addActionListener(e -> saveDirect());
add(saveDirectButton, "cell 0 10 3 1"); add(saveDirectButton, "cell 0 10 3 1");
//---- showMessageDialogOnOKCheckBox ----
showMessageDialogOnOKCheckBox.setText("show message dialog on OK");
add(showMessageDialogOnOKCheckBox, "cell 0 10 3 1");
//======== filesScrollPane ======== //======== filesScrollPane ========
{ {
@@ -519,6 +538,7 @@ public class FlatSystemFileChooserMacTest
private JButton saveButton; private JButton saveButton;
private JButton openDirectButton; private JButton openDirectButton;
private JButton saveDirectButton; private JButton saveDirectButton;
private JCheckBox showMessageDialogOnOKCheckBox;
private JScrollPane filesScrollPane; private JScrollPane filesScrollPane;
private JTextArea filesField; private JTextArea filesField;
// JFormDesigner - End of variables declaration //GEN-END:variables // JFormDesigner - End of variables declaration //GEN-END:variables

View File

@@ -257,6 +257,12 @@ new FormModel {
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 10 3 1" "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 ) ) { add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) {
name: "filesScrollPane" name: "filesScrollPane"
add( new FormComponent( "javax.swing.JTextArea" ) { add( new FormComponent( "javax.swing.JTextArea" ) {