From 516bd80702dd47cf5d00df4b85fa8a3308ed5113 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 30 Dec 2024 12:46:28 +0100 Subject: [PATCH] System File Chooser: implemented native bindings for NSOpenPanel and NSSavePanel on macOS --- .../flatlaf/ui/FlatNativeMacLibrary.java | 48 +- .../src/main/headers/JNIUtils.h | 4 + ..._formdev_flatlaf_ui_FlatNativeMacLibrary.h | 34 ++ .../src/main/objcpp/ApiVersion.mm | 6 +- .../src/main/objcpp/JNIUtils.mm | 35 ++ .../src/main/objcpp/MacFileChooser.mm | 152 ++++++ .../testing/FlatMacOSFileChooserTest.java | 446 ++++++++++++++++++ .../testing/FlatMacOSFileChooserTest.jfd | 220 +++++++++ 8 files changed, 941 insertions(+), 4 deletions(-) create mode 100644 flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.java create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.jfd 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 7d08376a..60343778 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 @@ -44,7 +44,7 @@ import com.formdev.flatlaf.util.SystemInfo; */ public class FlatNativeMacLibrary { - private static int API_VERSION_MACOS = 2001; + private static int API_VERSION_MACOS = 2002; /** * Checks whether native library is loaded/available. @@ -68,4 +68,50 @@ public class FlatNativeMacLibrary /** @since 3.4 */ public native static Rectangle getWindowButtonsBounds( Window window ); /** @since 3.4 */ public native static boolean isWindowFullScreen( Window window ); /** @since 3.4 */ public native static boolean toggleWindowFullScreen( Window window ); + + + /** @since 3.6 */ + public static final int + // NSOpenPanel + FC_canChooseFiles = 1 << 0, // default + FC_canChooseDirectories = 1 << 1, + FC_resolvesAliases_NO = 1 << 2, // default + FC_allowsMultipleSelection = 1 << 3, + // NSSavePanel + FC_showsTagField_YES = 1 << 8, // default for Save + FC_showsTagField_NO = 1 << 9, // default for Open + FC_canCreateDirectories_YES = 1 << 10, // default for Save + FC_canCreateDirectories_NO = 1 << 11, // default for Open + FC_canSelectHiddenExtension = 1 << 12, + FC_showsHiddenFiles = 1 << 14, + FC_extensionHidden = 1 << 16, + FC_allowsOtherFileTypes = 1 << 18, + FC_treatsFilePackagesAsDirectories = 1 << 20; + + /** + * Shows the macOS system file dialogs + * NSOpenPanel or + * NSSavePanel. + *

+ * Note: This method blocks the current thread until the user closes + * the file dialog. It is highly recommended to invoke it from a new thread + * to avoid blocking the AWT event dispatching thread. + * + * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog + * @param title text displayed at top of save dialog (not used in open dialog) + * @param prompt text displayed in default button + * @param message text displayed at top of open/save dialogs + * @param nameFieldLabel text displayed in front of the filename text field in save dialog (not used in open dialog) + * @param nameFieldStringValue user-editable filename currently shown in the name field in save dialog (not used in open dialog) + * @param directoryURL current directory shown in the dialog + * @param options see {@code FC_*} constants + * @param allowedFileTypes allowed filename extensions (e.g. "txt") + * @return file path(s) that the user selected, or {@code null} if canceled + * + * @since 3.6 + */ + public native static String[] showFileChooser( boolean open, + String title, String prompt, String message, String nameFieldLabel, + String nameFieldStringValue, String directoryURL, int options, + String... allowedFileTypes ); } 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 c4b4e58d..36b1bd4c 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h @@ -44,3 +44,7 @@ jclass findClass( JNIEnv *env, const char* className, bool globalRef ); jfieldID getFieldID( JNIEnv *env, jclass cls, const char* fieldName, const char* fieldSignature, bool staticField ); jmethodID getMethodID( JNIEnv *env, jclass cls, const char* methodName, const char* methodSignature, bool staticMethod ); + +NSString* JavaToNSString( JNIEnv *env, jstring javaString ); +jstring NSToJavaString( JNIEnv *env, NSString *nsString ); +jstring NormalizedPathJavaFromNSString( JNIEnv* env, NSString *nsString ); 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 f99549e5..3c991a3c 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 @@ -13,6 +13,32 @@ extern "C" { #define com_formdev_flatlaf_ui_FlatNativeMacLibrary_BUTTONS_SPACING_MEDIUM 1L #undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_BUTTONS_SPACING_LARGE #define com_formdev_flatlaf_ui_FlatNativeMacLibrary_BUTTONS_SPACING_LARGE 2L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canChooseFiles +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canChooseFiles 1L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canChooseDirectories +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canChooseDirectories 2L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_resolvesAliases_NO +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_resolvesAliases_NO 4L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsMultipleSelection +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsMultipleSelection 8L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsTagField_YES +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsTagField_YES 256L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsTagField_NO +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsTagField_NO 512L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canCreateDirectories_YES +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canCreateDirectories_YES 1024L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canCreateDirectories_NO +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canCreateDirectories_NO 2048L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canSelectHiddenExtension +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canSelectHiddenExtension 4096L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsHiddenFiles +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsHiddenFiles 16384L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_extensionHidden +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_extensionHidden 65536L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsOtherFileTypes +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsOtherFileTypes 262144L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_treatsFilePackagesAsDirectories +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_treatsFilePackagesAsDirectories 1048576L /* * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary * Method: setWindowRoundedBorder @@ -53,6 +79,14 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_isWi JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_toggleWindowFullScreen (JNIEnv *, jclass, jobject); +/* + * 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;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, jint, jobjectArray); + #ifdef __cplusplus } #endif diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm index ef0ff0f3..b9b9a9b4 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm @@ -14,8 +14,8 @@ * limitations under the License. */ -#include -#include "com_formdev_flatlaf_ui_FlatNativeLibrary.h" +#import +#import "com_formdev_flatlaf_ui_FlatNativeLibrary.h" /** * @author Karl Tauber @@ -24,7 +24,7 @@ // increase this version if changing API or functionality of native library // also update version in Java class com.formdev.flatlaf.ui.FlatNativeMacLibrary -#define API_VERSION_MACOS 2001 +#define API_VERSION_MACOS 2002 //---- JNI methods ------------------------------------------------------------ diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNIUtils.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNIUtils.mm index c000a9e2..a5fb10e7 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNIUtils.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNIUtils.mm @@ -75,3 +75,38 @@ jmethodID getMethodID( JNIEnv *env, jclass cls, const char* methodName, const ch return methodID; } + +NSString* JavaToNSString( JNIEnv *env, jstring javaString ) { + if( javaString == NULL ) + return NULL; + + int len = env->GetStringLength( javaString ); + const jchar* chars = env->GetStringChars( javaString, NULL ); + if( chars == NULL ) + return NULL; + + NSString* nsString = [NSString stringWithCharacters:(unichar*)chars length:len]; + env->ReleaseStringChars( javaString, chars ); + return nsString; +} + +jstring NSToJavaString( JNIEnv *env, NSString *nsString ) { + if( nsString == NULL ) + return NULL; + + jsize len = [nsString length]; + unichar* buffer = (unichar*) calloc( len, sizeof( unichar ) ); + if( buffer == NULL ) + return NULL; + + [nsString getCharacters:buffer]; + jstring javaString = env->NewString( buffer, len ); + free( buffer ); + return javaString; +} + +jstring NormalizedPathJavaFromNSString( JNIEnv* env, NSString *nsString ) { + return (nsString != NULL) + ? NSToJavaString( env, [nsString precomposedStringWithCanonicalMapping] ) + : NULL; +} diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm new file mode 100644 index 00000000..d38ac371 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -0,0 +1,152 @@ +/* + * Copyright 2024 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import +#import "JNIUtils.h" +#import "JNFRunLoop.h" +#import "com_formdev_flatlaf_ui_FlatNativeMacLibrary.h" + +/** + * @author Karl Tauber + */ + +#define isOptionSet( option ) ((options & com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ## option) != 0) +#define isYesOrNoOptionSet( option ) isOptionSet( option ## _YES ) || isOptionSet( option ## _NO ) + +// declare internal methods +NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window ); + +//---- JNI methods ------------------------------------------------------------ + +extern "C" +JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_showFileChooser + ( JNIEnv* env, jclass cls, jboolean open, + jstring title, jstring prompt, jstring message, jstring nameFieldLabel, + jstring nameFieldStringValue, jstring directoryURL, + jint options, jobjectArray allowedFileTypes ) +{ + JNI_COCOA_ENTER() + + // convert Java strings to NSString (on Java thread) + NSString* nsTitle = JavaToNSString( env, title ); + NSString* nsPrompt = JavaToNSString( env, prompt ); + NSString* nsMessage = JavaToNSString( env, message ); + NSString* nsNameFieldLabel = JavaToNSString( env, nameFieldLabel ); + NSString* nsNameFieldStringValue = JavaToNSString( env, nameFieldStringValue ); + NSString* nsDirectoryURL = JavaToNSString( env, directoryURL ); + + NSArray* nsAllowedFileTypes = NULL; + jsize len = env->GetArrayLength( allowedFileTypes ); + if( len > 0 ) { + NSMutableArray* nsArray = [NSMutableArray arrayWithCapacity:len]; + for( int i = 0; i < len; i++ ) { + jstring str = (jstring) env->GetObjectArrayElement( allowedFileTypes, i ); + NSString* nsStr = JavaToNSString( env, str ); + nsArray[i] = nsStr; + } + nsAllowedFileTypes = nsArray; + } + + NSArray* urls = NULL; + NSArray** purls = &urls; + NSURL* url = NULL; + NSURL** purl = &url; + + // show file dialog on macOS thread + [FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){ + NSSavePanel* dialog = open ? [NSOpenPanel openPanel] : [NSSavePanel savePanel]; + + if( nsTitle != NULL ) + dialog.title = nsTitle; + if( nsPrompt != NULL ) + dialog.prompt = nsPrompt; + if( nsMessage != NULL ) + dialog.message = nsMessage; + if( nsNameFieldLabel != NULL ) + dialog.nameFieldLabel = nsNameFieldLabel; + if( nsNameFieldStringValue != NULL ) + dialog.nameFieldStringValue = nsNameFieldStringValue; + if( nsDirectoryURL != NULL ) + dialog.directoryURL = [NSURL fileURLWithPath:nsDirectoryURL isDirectory:YES]; + + if( open ) { + NSOpenPanel* openDialog = (NSOpenPanel*) dialog; + + bool canChooseFiles = isOptionSet( FC_canChooseFiles ); + bool canChooseDirectories = isOptionSet( FC_canChooseDirectories ); + if( !canChooseFiles && !canChooseDirectories ) + canChooseFiles = true; + openDialog.canChooseFiles = canChooseFiles; + openDialog.canChooseDirectories = canChooseDirectories; + if( isOptionSet( FC_resolvesAliases_NO ) ) + openDialog.resolvesAliases = NO; + if( isOptionSet( FC_allowsMultipleSelection ) ) + openDialog.allowsMultipleSelection = YES; + } + + if( isYesOrNoOptionSet( FC_showsTagField ) ) + dialog.showsTagField = isOptionSet( FC_showsTagField_YES ); + if( isYesOrNoOptionSet( FC_canCreateDirectories ) ) + dialog.canCreateDirectories = isOptionSet( FC_canCreateDirectories_YES ); + if( isOptionSet( FC_canSelectHiddenExtension ) ) + dialog.canSelectHiddenExtension = YES; + if( isOptionSet( FC_showsHiddenFiles) ) + dialog.showsHiddenFiles = YES; + if( isOptionSet( FC_extensionHidden ) ) + dialog.extensionHidden = YES; + if( isOptionSet( FC_allowsOtherFileTypes ) ) + dialog.allowsOtherFileTypes = YES; + if( isOptionSet( FC_treatsFilePackagesAsDirectories ) ) + dialog.treatsFilePackagesAsDirectories = YES; + + // use deprecated allowedFileTypes instead of newer allowedContentTypes (since macOS 11+) + // to support older macOS versions 10.14+ and because of some problems with allowedContentTypes: + // https://github.com/chromium/chromium/blob/d8e0032963b7ca4728ff4117933c0feb3e479b7a/components/remote_cocoa/app_shim/select_file_dialog_bridge.mm#L209-232 + if( nsAllowedFileTypes != NULL ) + dialog.allowedFileTypes = nsAllowedFileTypes; + + if( [dialog runModal] != NSModalResponseOK ) + return; + + if( open ) + *purls = ((NSOpenPanel*)dialog).URLs; + else + *purl = dialog.URL; + }]; + + if( url != NULL ) + urls = @[url]; + + if( urls == NULL ) + return NULL; + + // convert URLs to Java string array + jsize count = urls.count; + jclass stringClass = env->FindClass( "java/lang/String" ); + jobjectArray result = env->NewObjectArray( count, stringClass, NULL ); + for( int i = 0; i < count; i++ ) { + jstring filename = NormalizedPathJavaFromNSString( env, [urls[i] path] ); + env->SetObjectArrayElement( result, i, filename ); + env->DeleteLocalRef( filename ); + } + + return result; + + JNI_COCOA_EXIT() +} + diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.java new file mode 100644 index 00000000..9f15a9da --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.java @@ -0,0 +1,446 @@ +/* + * Copyright 2024 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.testing; + +import static com.formdev.flatlaf.ui.FlatNativeMacLibrary.*; +import java.awt.EventQueue; +import java.awt.SecondaryLoop; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.WindowEvent; +import java.awt.event.WindowFocusListener; +import java.awt.event.WindowListener; +import java.awt.event.WindowStateListener; +import java.util.Arrays; +import javax.swing.*; +import com.formdev.flatlaf.extras.components.*; +import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; +import com.formdev.flatlaf.ui.FlatNativeMacLibrary; +import com.formdev.flatlaf.util.SystemInfo; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatMacOSFileChooserTest + extends FlatTestPanel +{ + public static void main( String[] args ) { + // macOS (see https://www.formdev.com/flatlaf/macos/) + if( SystemInfo.isMacOS ) { + // appearance of window title bars + // possible values: + // - "system": use current macOS appearance (light or dark) + // - "NSAppearanceNameAqua": use light appearance + // - "NSAppearanceNameDarkAqua": use dark appearance + // (needs to be set on main thread; setting it on AWT thread does not work) + System.setProperty( "apple.awt.application.appearance", "system" ); + } + + SwingUtilities.invokeLater( () -> { + FlatTestFrame frame = FlatTestFrame.create( args, "FlatMacOSFileChooserTest" ); + addListeners( frame ); + frame.showFrame( FlatMacOSFileChooserTest::new ); + } ); + } + + FlatMacOSFileChooserTest() { + initComponents(); + } + + private void open() { + openOrSave( true, false ); + } + + private void save() { + openOrSave( false, false ); + } + + private void openDirect() { + openOrSave( true, true ); + } + + private void saveDirect() { + openOrSave( false, true ); + } + + private void openOrSave( boolean open, boolean direct ) { + String title = n( titleField.getText() ); + String prompt = n( promptField.getText() ); + String message = n( messageField.getText() ); + String nameFieldLabel = n( nameFieldLabelField.getText() ); + String nameFieldStringValue = n( nameFieldStringValueField.getText() ); + String directoryURL = n( directoryURLField.getText() ); + int options = 0; + + // NSOpenPanel + if( canChooseFilesCheckBox.isSelected() ) + options |= FC_canChooseFiles; + if( canChooseDirectoriesCheckBox.isSelected() ) + options |= FC_canChooseDirectories; + if( !resolvesAliasesCheckBox.isSelected() ) + options |= FC_resolvesAliases_NO; + if( allowsMultipleSelectionCheckBox.isSelected() ) + options |= FC_allowsMultipleSelection; + + // NSSavePanel + if( showsTagFieldCheckBox.getState() == State.SELECTED ) + options |= FC_showsTagField_YES; + else if( showsTagFieldCheckBox.getState() == State.UNSELECTED ) + options |= FC_showsTagField_NO; + if( canCreateDirectoriesCheckBox.getState() == State.SELECTED ) + options |= FC_canCreateDirectories_YES; + else if( canCreateDirectoriesCheckBox.getState() == State.UNSELECTED ) + options |= FC_canCreateDirectories_NO; + if( canSelectHiddenExtensionCheckBox.isSelected() ) + options |= FC_canSelectHiddenExtension; + if( showsHiddenFilesCheckBox.isSelected() ) + options |= FC_showsHiddenFiles; + if( extensionHiddenCheckBox.isSelected() ) + options |= FC_extensionHidden; + if( allowsOtherFileTypesCheckBox.isSelected() ) + options |= FC_allowsOtherFileTypes; + if( treatsFilePackagesAsDirectoriesCheckBox.isSelected() ) + options |= FC_treatsFilePackagesAsDirectories; + + String allowedFileTypesStr = n( allowedFileTypesField.getText() ); + String[] allowedFileTypes = {}; + if( allowedFileTypesStr != null ) + allowedFileTypes = allowedFileTypesStr.trim().split( "[ ,]+" ); + + if( direct ) { + String[] files = FlatNativeMacLibrary.showFileChooser( open, title, prompt, message, + nameFieldLabel, nameFieldStringValue, directoryURL, options, allowedFileTypes ); + + filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); + } else { + SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); + + int options2 = options; + String[] allowedFileTypes2 = allowedFileTypes; + new Thread( () -> { + String[] files = FlatNativeMacLibrary.showFileChooser( open, title, prompt, message, + nameFieldLabel, nameFieldStringValue, directoryURL, options2, allowedFileTypes2 ); + + System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); + + EventQueue.invokeLater( () -> { + filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); + } ); + } ).start(); + + System.out.println( "---- enter secondary loop ----" ); + System.out.println( "---- secondary loop exited (secondaryLoop.enter() returned " + secondaryLoop.enter() + ") ----" ); + } + } + + private static String n( String s ) { + return !s.isEmpty() ? s : null; + } + + private static void addListeners( Window w ) { + w.addWindowListener( new WindowListener() { + @Override + public void windowOpened( WindowEvent e ) { + System.out.println( e ); + } + + @Override + public void windowIconified( WindowEvent e ) { + System.out.println( e ); + } + + @Override + public void windowDeiconified( WindowEvent e ) { + System.out.println( e ); + } + + @Override + public void windowDeactivated( WindowEvent e ) { + System.out.println( e ); + } + + @Override + public void windowClosing( WindowEvent e ) { + System.out.println( e ); + } + + @Override + public void windowClosed( WindowEvent e ) { + System.out.println( e ); + } + + @Override + public void windowActivated( WindowEvent e ) { + System.out.println( e ); + } + } ); + w.addWindowStateListener( new WindowStateListener() { + @Override + public void windowStateChanged( WindowEvent e ) { + System.out.println( e ); + } + } ); + w.addWindowFocusListener( new WindowFocusListener() { + @Override + public void windowLostFocus( WindowEvent e ) { + System.out.println( e ); + } + + @Override + public void windowGainedFocus( WindowEvent e ) { + System.out.println( e ); + } + } ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + titleLabel = new JLabel(); + titleField = new JTextField(); + panel1 = new JPanel(); + options1Label = new JLabel(); + canChooseFilesCheckBox = new JCheckBox(); + canChooseDirectoriesCheckBox = new JCheckBox(); + resolvesAliasesCheckBox = new JCheckBox(); + allowsMultipleSelectionCheckBox = new JCheckBox(); + options2Label = new JLabel(); + showsTagFieldCheckBox = new FlatTriStateCheckBox(); + canCreateDirectoriesCheckBox = new FlatTriStateCheckBox(); + canSelectHiddenExtensionCheckBox = new JCheckBox(); + showsHiddenFilesCheckBox = new JCheckBox(); + extensionHiddenCheckBox = new JCheckBox(); + allowsOtherFileTypesCheckBox = new JCheckBox(); + treatsFilePackagesAsDirectoriesCheckBox = new JCheckBox(); + promptLabel = new JLabel(); + promptField = new JTextField(); + messageLabel = new JLabel(); + messageField = new JTextField(); + nameFieldLabelLabel = new JLabel(); + nameFieldLabelField = new JTextField(); + nameFieldStringValueLabel = new JLabel(); + nameFieldStringValueField = new JTextField(); + directoryURLLabel = new JLabel(); + directoryURLField = new JTextField(); + allowedFileTypesLabel = new JLabel(); + allowedFileTypesField = new JTextField(); + openButton = new JButton(); + saveButton = new JButton(); + openDirectButton = new JButton(); + saveDirectButton = new JButton(); + filesScrollPane = new JScrollPane(); + filesField = new JTextArea(); + + //======== this ======== + setLayout(new MigLayout( + "ltr,insets dialog,hidemode 3", + // columns + "[left]" + + "[grow,fill]" + + "[fill]", + // rows + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[grow,fill]")); + + //---- titleLabel ---- + titleLabel.setText("title"); + add(titleLabel, "cell 0 0"); + add(titleField, "cell 1 0"); + + //======== panel1 ======== + { + panel1.setLayout(new MigLayout( + "insets 2,hidemode 3", + // columns + "[left]", + // rows + "[]" + + "[]0" + + "[]0" + + "[]0" + + "[]para" + + "[]" + + "[]0" + + "[]0" + + "[]0" + + "[]0" + + "[]0" + + "[]0" + + "[]")); + + //---- options1Label ---- + options1Label.setText("NSOpenPanel options:"); + panel1.add(options1Label, "cell 0 0"); + + //---- canChooseFilesCheckBox ---- + canChooseFilesCheckBox.setText("canChooseFiles"); + canChooseFilesCheckBox.setSelected(true); + panel1.add(canChooseFilesCheckBox, "cell 0 1"); + + //---- canChooseDirectoriesCheckBox ---- + canChooseDirectoriesCheckBox.setText("canChooseDirectories"); + panel1.add(canChooseDirectoriesCheckBox, "cell 0 2"); + + //---- resolvesAliasesCheckBox ---- + resolvesAliasesCheckBox.setText("resolvesAliases"); + resolvesAliasesCheckBox.setSelected(true); + panel1.add(resolvesAliasesCheckBox, "cell 0 3"); + + //---- allowsMultipleSelectionCheckBox ---- + allowsMultipleSelectionCheckBox.setText("allowsMultipleSelection"); + panel1.add(allowsMultipleSelectionCheckBox, "cell 0 4"); + + //---- options2Label ---- + options2Label.setText("NSOpenPanel and NSSavePanel options:"); + panel1.add(options2Label, "cell 0 5"); + + //---- showsTagFieldCheckBox ---- + showsTagFieldCheckBox.setText("showsTagField"); + panel1.add(showsTagFieldCheckBox, "cell 0 6"); + + //---- canCreateDirectoriesCheckBox ---- + canCreateDirectoriesCheckBox.setText("canCreateDirectories"); + panel1.add(canCreateDirectoriesCheckBox, "cell 0 7"); + + //---- canSelectHiddenExtensionCheckBox ---- + canSelectHiddenExtensionCheckBox.setText("canSelectHiddenExtension"); + panel1.add(canSelectHiddenExtensionCheckBox, "cell 0 8"); + + //---- showsHiddenFilesCheckBox ---- + showsHiddenFilesCheckBox.setText("showsHiddenFiles"); + panel1.add(showsHiddenFilesCheckBox, "cell 0 9"); + + //---- extensionHiddenCheckBox ---- + extensionHiddenCheckBox.setText("extensionHidden"); + panel1.add(extensionHiddenCheckBox, "cell 0 10"); + + //---- allowsOtherFileTypesCheckBox ---- + allowsOtherFileTypesCheckBox.setText("allowsOtherFileTypes"); + panel1.add(allowsOtherFileTypesCheckBox, "cell 0 11"); + + //---- treatsFilePackagesAsDirectoriesCheckBox ---- + treatsFilePackagesAsDirectoriesCheckBox.setText("treatsFilePackagesAsDirectories"); + panel1.add(treatsFilePackagesAsDirectoriesCheckBox, "cell 0 12"); + } + add(panel1, "cell 2 0 1 8,aligny top,growy 0"); + + //---- promptLabel ---- + promptLabel.setText("prompt"); + add(promptLabel, "cell 0 1"); + add(promptField, "cell 1 1"); + + //---- messageLabel ---- + messageLabel.setText("message"); + add(messageLabel, "cell 0 2"); + add(messageField, "cell 1 2"); + + //---- nameFieldLabelLabel ---- + nameFieldLabelLabel.setText("nameFieldLabel"); + add(nameFieldLabelLabel, "cell 0 3"); + add(nameFieldLabelField, "cell 1 3"); + + //---- nameFieldStringValueLabel ---- + nameFieldStringValueLabel.setText("nameFieldStringValue"); + add(nameFieldStringValueLabel, "cell 0 4"); + add(nameFieldStringValueField, "cell 1 4"); + + //---- directoryURLLabel ---- + directoryURLLabel.setText("directoryURL"); + add(directoryURLLabel, "cell 0 5"); + add(directoryURLField, "cell 1 5"); + + //---- allowedFileTypesLabel ---- + allowedFileTypesLabel.setText("allowedFileTypes"); + add(allowedFileTypesLabel, "cell 0 6"); + add(allowedFileTypesField, "cell 1 6"); + + //---- openButton ---- + openButton.setText("Open..."); + openButton.addActionListener(e -> open()); + add(openButton, "cell 0 8 3 1"); + + //---- saveButton ---- + saveButton.setText("Save..."); + saveButton.addActionListener(e -> save()); + add(saveButton, "cell 0 8 3 1"); + + //---- openDirectButton ---- + openDirectButton.setText("Open (no-thread)..."); + openDirectButton.addActionListener(e -> openDirect()); + add(openDirectButton, "cell 0 8 3 1"); + + //---- saveDirectButton ---- + saveDirectButton.setText("Save (no-thread)..."); + saveDirectButton.addActionListener(e -> saveDirect()); + add(saveDirectButton, "cell 0 8 3 1"); + + //======== filesScrollPane ======== + { + + //---- filesField ---- + filesField.setRows(8); + filesScrollPane.setViewportView(filesField); + } + add(filesScrollPane, "cell 0 9 3 1,growx"); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JLabel titleLabel; + private JTextField titleField; + private JPanel panel1; + private JLabel options1Label; + private JCheckBox canChooseFilesCheckBox; + private JCheckBox canChooseDirectoriesCheckBox; + private JCheckBox resolvesAliasesCheckBox; + private JCheckBox allowsMultipleSelectionCheckBox; + private JLabel options2Label; + private FlatTriStateCheckBox showsTagFieldCheckBox; + private FlatTriStateCheckBox canCreateDirectoriesCheckBox; + private JCheckBox canSelectHiddenExtensionCheckBox; + private JCheckBox showsHiddenFilesCheckBox; + private JCheckBox extensionHiddenCheckBox; + private JCheckBox allowsOtherFileTypesCheckBox; + private JCheckBox treatsFilePackagesAsDirectoriesCheckBox; + private JLabel promptLabel; + private JTextField promptField; + private JLabel messageLabel; + private JTextField messageField; + private JLabel nameFieldLabelLabel; + private JTextField nameFieldLabelField; + private JLabel nameFieldStringValueLabel; + private JTextField nameFieldStringValueField; + private JLabel directoryURLLabel; + private JTextField directoryURLField; + private JLabel allowedFileTypesLabel; + private JTextField allowedFileTypesField; + private JButton openButton; + private JButton saveButton; + private JButton openDirectButton; + private JButton saveDirectButton; + 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/FlatMacOSFileChooserTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.jfd new file mode 100644 index 00000000..0412f9c3 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.jfd @@ -0,0 +1,220 @@ +JFDML JFormDesigner: "8.2.2.0.9999" Java: "21.0.1" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "ltr,insets dialog,hidemode 3" + "$columnConstraints": "[left][grow,fill][fill]" + "$rowConstraints": "[][][][][][][][][][grow,fill]" + } ) { + name: "this" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "titleLabel" + "text": "title" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "titleField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "insets 2,hidemode 3" + "$columnConstraints": "[left]" + "$rowConstraints": "[][]0[]0[]0[]para[][]0[]0[]0[]0[]0[]0[]" + } ) { + name: "panel1" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "options1Label" + "text": "NSOpenPanel options:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "canChooseFilesCheckBox" + "text": "canChooseFiles" + "selected": true + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "canChooseDirectoriesCheckBox" + "text": "canChooseDirectories" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "resolvesAliasesCheckBox" + "text": "resolvesAliases" + "selected": true + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "allowsMultipleSelectionCheckBox" + "text": "allowsMultipleSelection" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "options2Label" + "text": "NSOpenPanel and NSSavePanel options:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "showsTagFieldCheckBox" + "text": "showsTagField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "canCreateDirectoriesCheckBox" + "text": "canCreateDirectories" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "canSelectHiddenExtensionCheckBox" + "text": "canSelectHiddenExtension" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showsHiddenFilesCheckBox" + "text": "showsHiddenFiles" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 9" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "extensionHiddenCheckBox" + "text": "extensionHidden" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 10" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "allowsOtherFileTypesCheckBox" + "text": "allowsOtherFileTypes" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "treatsFilePackagesAsDirectoriesCheckBox" + "text": "treatsFilePackagesAsDirectories" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 12" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 1 8,aligny top,growy 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "promptLabel" + "text": "prompt" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "promptField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "messageLabel" + "text": "message" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "messageField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "nameFieldLabelLabel" + "text": "nameFieldLabel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "nameFieldLabelField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "nameFieldStringValueLabel" + "text": "nameFieldStringValue" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "nameFieldStringValueField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "directoryURLLabel" + "text": "directoryURL" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "directoryURLField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "allowedFileTypesLabel" + "text": "allowedFileTypes" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "allowedFileTypesField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "openButton" + "text": "Open..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "open", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "saveButton" + "text": "Save..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "save", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "openDirectButton" + "text": "Open (no-thread)..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openDirect", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "saveDirectButton" + "text": "Save (no-thread)..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "saveDirect", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "filesScrollPane" + add( new FormComponent( "javax.swing.JTextArea" ) { + name: "filesField" + "rows": 8 + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 9 3 1,growx" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 535, 465 ) + } ) + } +}