From 516bd80702dd47cf5d00df4b85fa8a3308ed5113 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 30 Dec 2024 12:46:28 +0100 Subject: [PATCH 01/34] 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 ) + } ) + } +} From 49a0a83ecaa4439f4b63972ace63bdb5a7c6efdf Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 31 Dec 2024 17:15:37 +0100 Subject: [PATCH 02/34] System File Chooser: implemented native bindings for IFileOpenDialog and IFileSaveDialog on Windows --- .github/workflows/ci.yml | 2 +- .github/workflows/natives.yml | 2 +- .../flatlaf/ui/FlatNativeWindowsLibrary.java | 75 ++- .../flatlaf-natives-windows/build.gradle.kts | 4 +- .../src/main/cpp/ApiVersion.cpp | 2 +- .../src/main/cpp/WinFileChooser.cpp | 257 ++++++++ ...mdev_flatlaf_ui_FlatNativeWindowsLibrary.h | 54 ++ .../testing/FlatWindowsFileChooserTest.java | 551 ++++++++++++++++++ .../testing/FlatWindowsFileChooserTest.jfd | 324 ++++++++++ 9 files changed, 1265 insertions(+), 6 deletions(-) create mode 100644 flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.java create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.jfd diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1f0e1e9..57acad88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: gradle/actions/wrapper-validation@v3 + - uses: gradle/actions/wrapper-validation@v4 if: matrix.java == '8' - name: Setup Java ${{ matrix.java }} diff --git a/.github/workflows/natives.yml b/.github/workflows/natives.yml index bae5cb2a..cf314dcc 100644 --- a/.github/workflows/natives.yml +++ b/.github/workflows/natives.yml @@ -30,7 +30,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: gradle/actions/wrapper-validation@v3 + - uses: gradle/actions/wrapper-validation@v4 - name: Setup Java 11 uses: actions/setup-java@v4 diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java index 8a4ce3cf..991a9878 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java @@ -30,7 +30,7 @@ import com.formdev.flatlaf.util.SystemInfo; */ public class FlatNativeWindowsLibrary { - private static int API_VERSION_WINDOWS = 1001; + private static int API_VERSION_WINDOWS = 1002; private static long osBuildNumber = Long.MIN_VALUE; @@ -158,4 +158,77 @@ public class FlatNativeWindowsLibrary // DwmSetWindowAttribute() expects COLORREF as attribute value, which is defined as DWORD return dwmSetWindowAttributeDWORD( hwnd, attribute, rgb ); } + + + /** + * FILEOPENDIALOGOPTIONS + * see https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/ne-shobjidl_core-_fileopendialogoptions + * + * @since 3.6 + */ + public static final int + FOS_OVERWRITEPROMPT = 0x2, // default for Save + FOS_STRICTFILETYPES = 0x4, + FOS_NOCHANGEDIR = 0x8, // default + FOS_PICKFOLDERS = 0x20, + FOS_FORCEFILESYSTEM = 0x40, + FOS_ALLNONSTORAGEITEMS = 0x80, + FOS_NOVALIDATE = 0x100, + FOS_ALLOWMULTISELECT = 0x200, + FOS_PATHMUSTEXIST = 0x800, // default + FOS_FILEMUSTEXIST = 0x1000, // default for Open + FOS_CREATEPROMPT = 0x2000, + FOS_SHAREAWARE = 0x4000, + FOS_NOREADONLYRETURN = 0x8000, // default for Save + FOS_NOTESTFILECREATE = 0x10000, + FOS_HIDEMRUPLACES = 0x20000, + FOS_HIDEPINNEDPLACES = 0x40000, + FOS_NODEREFERENCELINKS = 0x100000, + FOS_OKBUTTONNEEDSINTERACTION = 0x200000, + FOS_DONTADDTORECENT = 0x2000000, + FOS_FORCESHOWHIDDEN = 0x10000000, + FOS_DEFAULTNOMINIMODE = 0x20000000, + FOS_FORCEPREVIEWPANEON = 0x40000000, + FOS_SUPPORTSTREAMABLEITEMS = 0x80000000; + + /** + * Shows the Windows system + * file dialogs + * IFileOpenDialog or + * IFileSaveDialog. + *

+ * 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 owner the owner of the file dialog + * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog + * @param title text displayed in dialog title; or {@code null} + * @param okButtonLabel text displayed in default button; or {@code null} + * @param fileNameLabel text displayed in front of the filename text field; or {@code null} + * @param fileName user-editable filename currently shown in the filename field; or {@code null} + * @param folder current directory shown in the dialog; or {@code null} + * @param saveAsItem file to be used as the initial entry in a Save As dialog; or {@code null}. + * File name is shown in filename text field, folder is selected in view. + * To be used for saving files that already exist. For new files use {@code fileName}. + * @param defaultFolder folder used as a default if there is not a recently used folder value available; or {@code null}. + * Windows somewhere stores default folder on a per-app basis. + * So this is probably used only once when the app opens a file dialog for first time. + * @param defaultExtension default extension to be added to file name in save dialog; or {@code null} + * @param optionsSet options to set; see {@code FOS_*} constants + * @param optionsClear options to clear; see {@code FOS_*} constants + * @param fileTypeIndex the file type that appears as selected (zero-based) + * @param fileTypes file types that the dialog can open or save. + * Pairs of strings are required. + * First string is the display name of the filter shown in the combobox (e.g. "Text Files"). + * Second string is the filter pattern (e.g. "*.txt", "*.exe;*.dll" or "*.*"). + * @return file path(s) that the user selected; an empty array if canceled; + * or {@code null} on failures (no dialog shown) + * + * @since 3.6 + */ + public native static String[] showFileChooser( Window owner, boolean open, + String title, String okButtonLabel, String fileNameLabel, String fileName, + String folder, String saveAsItem, String defaultFolder, String defaultExtension, + int optionsSet, int optionsClear, int fileTypeIndex, String... fileTypes ); } diff --git a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts index e297703e..550179d1 100644 --- a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts @@ -80,8 +80,8 @@ tasks { linkerArgs.addAll( toolChain.map { when( it ) { - is Gcc, is Clang -> listOf( "-lUser32", "-lGdi32", "-lshell32", "-lAdvAPI32", "-lKernel32", "-lDwmapi" ) - is VisualCpp -> listOf( "User32.lib", "Gdi32.lib", "shell32.lib", "AdvAPI32.lib", "Kernel32.lib", "Dwmapi.lib", "/NODEFAULTLIB" ) + is Gcc, is Clang -> listOf( "-lUser32", "-lGdi32", "-lshell32", "-lAdvAPI32", "-lKernel32", "-lDwmapi", "-lOle32", "-luuid" ) + is VisualCpp -> listOf( "User32.lib", "Gdi32.lib", "shell32.lib", "AdvAPI32.lib", "Kernel32.lib", "Dwmapi.lib", "Ole32.lib", "uuid.lib", "/NODEFAULTLIB" ) else -> emptyList() } } ) diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/ApiVersion.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/ApiVersion.cpp index a32f0d6d..0f2e7a97 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/ApiVersion.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/ApiVersion.cpp @@ -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.FlatNativeWindowsLibrary -#define API_VERSION_WINDOWS 1001 +#define API_VERSION_WINDOWS 1002 //---- JNI methods ------------------------------------------------------------ diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp new file mode 100644 index 00000000..b51b0d7f --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp @@ -0,0 +1,257 @@ +/* + * 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. + */ + +// avoid inlining of printf() +#define _NO_CRT_STDIO_INLINE + +#include +#include +#include "com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h" + +/** + * @author Karl Tauber + */ + +// see FlatWndProc.cpp +HWND getWindowHandle( JNIEnv* env, jobject window ); + +//---- class AutoReleasePtr --------------------------------------------------- + +template class AutoReleasePtr { + T* ptr; + +public: + AutoReleasePtr() { + ptr = NULL; + } + ~AutoReleasePtr() { + if( ptr != NULL ) + ptr->Release(); + } + T** operator&() { return &ptr; } + T* operator->() { return ptr; } + operator T*() { return ptr; } +}; + +//---- class AutoReleaseString ------------------------------------------------ + +class AutoReleaseString { + JNIEnv* env; + jstring javaString; + const jchar* chars; + +public: + AutoReleaseString( JNIEnv* _env, jstring _javaString ) { + env = _env; + javaString = _javaString; + chars = (javaString != NULL) ? env->GetStringChars( javaString, NULL ) : NULL; + } + ~AutoReleaseString() { + if( chars != NULL ) + env->ReleaseStringChars( javaString, chars ); + } + operator LPCWSTR() { return (LPCWSTR) chars; } +}; + +//---- class AutoReleaseIShellItem -------------------------------------------- + +class AutoReleaseIShellItem : public AutoReleasePtr { +public: + AutoReleaseIShellItem( JNIEnv* env, jstring path ) { + AutoReleaseString cpath( env, path ); + ::SHCreateItemFromParsingName( cpath, NULL, IID_IShellItem, reinterpret_cast( &*this ) ); + } +}; + +//---- class FilterSpec ------------------------------------------------------- + +class FilterSpec { + JNIEnv* env; + jstring* jnames = NULL; + jstring* jspecs = NULL; + +public: + UINT count = 0; + COMDLG_FILTERSPEC* specs = NULL; + +public: + FilterSpec( JNIEnv* _env, jobjectArray fileTypes ) { + env = _env; + count = env->GetArrayLength( fileTypes ) / 2; + if( count <= 0 ) + return; + + specs = new COMDLG_FILTERSPEC[count]; + jnames = new jstring[count]; + jspecs = new jstring[count]; + + for( int i = 0; i < count; i++ ) { + jnames[i] = (jstring) env->GetObjectArrayElement( fileTypes, i * 2 ); + jspecs[i] = (jstring) env->GetObjectArrayElement( fileTypes, (i * 2) + 1 ); + specs[i].pszName = (LPCWSTR) env->GetStringChars( jnames[i] , NULL ); + specs[i].pszSpec = (LPCWSTR) env->GetStringChars( jspecs[i], NULL ); + } + } + ~FilterSpec() { + if( specs == NULL ) + return; + + for( int i = 0; i < count; i++ ) { + env->ReleaseStringChars( jnames[i], (jchar *) specs[i].pszName ); + env->ReleaseStringChars( jspecs[i], (jchar *) specs[i].pszSpec ); + env->DeleteLocalRef( jnames[i] ); + env->DeleteLocalRef( jspecs[i] ); + } + + delete[] jnames; + delete[] jspecs; + delete[] specs; + } +}; + +//---- class CoInitializer ---------------------------------------------------- + +class CoInitializer { +public: + bool initialized; + + CoInitializer() { + HRESULT result = ::CoInitializeEx( NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE ); + initialized = SUCCEEDED( result ); + } + ~CoInitializer() { + if( initialized ) + ::CoUninitialize(); + } +}; + +//---- helper ----------------------------------------------------------------- + +#define CHECK_HRESULT( code ) { if( (code) != S_OK ) return NULL; } + +jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { + jclass stringClass = env->FindClass( "java/lang/String" ); + return env->NewObjectArray( count, stringClass, NULL ); +} + +jstring newJavaString( JNIEnv* env, LPWSTR str ) { + return env->NewString( reinterpret_cast( str ), static_cast( wcslen( str ) ) ); +} + +//---- JNI methods ------------------------------------------------------------ + +extern "C" +JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showFileChooser + ( JNIEnv* env, jclass cls, jobject owner, jboolean open, + jstring title, jstring okButtonLabel, jstring fileNameLabel, jstring fileName, + jstring folder, jstring saveAsItem, jstring defaultFolder, jstring defaultExtension, + jint optionsSet, jint optionsClear, jint fileTypeIndex, jobjectArray fileTypes ) +{ + // initialize COM library + CoInitializer coInitializer; + if( !coInitializer.initialized ) + return NULL; + + HWND hwndOwner = getWindowHandle( env, owner ); + + // convert Java strings to C strings + AutoReleaseString ctitle( env, title ); + AutoReleaseString cokButtonLabel( env, okButtonLabel ); + AutoReleaseString cfileNameLabel( env, fileNameLabel ); + AutoReleaseString cfileName( env, fileName ); + AutoReleaseIShellItem cfolder( env, folder ); + AutoReleaseIShellItem csaveAsItem( env, saveAsItem ); + AutoReleaseIShellItem cdefaultFolder( env, defaultFolder ); + AutoReleaseString cdefaultExtension( env, defaultExtension ); + FilterSpec specs( env, fileTypes ); + + // create IFileOpenDialog or IFileSaveDialog + // https://learn.microsoft.com/en-us/windows/win32/shell/common-file-dialog + AutoReleasePtr dialog; + CHECK_HRESULT( ::CoCreateInstance( open ? CLSID_FileOpenDialog : CLSID_FileSaveDialog, + NULL, CLSCTX_INPROC_SERVER, open ? IID_IFileOpenDialog : IID_IFileSaveDialog, + reinterpret_cast( &dialog ) ) ); + + if( ctitle != NULL ) + CHECK_HRESULT( dialog->SetTitle( ctitle ) ); + if( cokButtonLabel != NULL ) + CHECK_HRESULT( dialog->SetOkButtonLabel( cokButtonLabel ) ); + if( cfileNameLabel != NULL ) + CHECK_HRESULT( dialog->SetFileNameLabel( cfileNameLabel ) ); + if( cfileName != NULL ) + CHECK_HRESULT( dialog->SetFileName( cfileName ) ); + if( cfolder != NULL ) + CHECK_HRESULT( dialog->SetFolder( cfolder ) ); + if( !open && csaveAsItem != NULL ) + CHECK_HRESULT( ((IFileSaveDialog*)(IFileDialog*)dialog)->SetSaveAsItem( csaveAsItem ) ); + if( cdefaultFolder != NULL ) + CHECK_HRESULT( dialog->SetDefaultFolder( cdefaultFolder ) ); + if( cdefaultExtension != NULL ) + CHECK_HRESULT( dialog->SetDefaultExtension( cdefaultExtension ) ); + + FILEOPENDIALOGOPTIONS existingOptions; + CHECK_HRESULT( dialog->GetOptions( &existingOptions ) ); + CHECK_HRESULT( dialog->SetOptions ( (existingOptions & ~optionsClear) | optionsSet ) ); + + if( specs.count > 0 ) { + CHECK_HRESULT( dialog->SetFileTypes( specs.count, specs.specs ) ); + if( fileTypeIndex > 0 ) + CHECK_HRESULT( dialog->SetFileTypeIndex( min( fileTypeIndex + 1, specs.count ) ) ); + } + + // show dialog + HRESULT hr = dialog->Show( hwndOwner ); + if( hr == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) + return newJavaStringArray( env, 0 ); + CHECK_HRESULT( hr ); + + // convert URLs to Java string array + if( open ) { + AutoReleasePtr shellItems; + DWORD count; + CHECK_HRESULT( ((IFileOpenDialog*)(IFileDialog*)dialog)->GetResults( &shellItems ) ); + CHECK_HRESULT( shellItems->GetCount( &count ) ); + + jobjectArray array = newJavaStringArray( env, count ); + for( int i = 0; i < count; i++ ) { + AutoReleasePtr shellItem; + LPWSTR path; + CHECK_HRESULT( shellItems->GetItemAt( i, &shellItem ) ); + CHECK_HRESULT( shellItem->GetDisplayName( SIGDN_FILESYSPATH, &path ) ); + + jstring jpath = newJavaString( env, path ); + CoTaskMemFree( path ); + + env->SetObjectArrayElement( array, 0, jpath ); + env->DeleteLocalRef( jpath ); + } + return array; + } else { + AutoReleasePtr shellItem; + LPWSTR path; + CHECK_HRESULT( dialog->GetResult( &shellItem ) ); + CHECK_HRESULT( shellItem->GetDisplayName( SIGDN_FILESYSPATH, &path ) ); + + jstring jpath = newJavaString( env, path ); + CoTaskMemFree( path ); + + jobjectArray array = newJavaStringArray( env, 1 ); + env->SetObjectArrayElement( array, 0, jpath ); + env->DeleteLocalRef( jpath ); + + return array; + } +} diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h index 1701cb3b..895b7267 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h @@ -27,6 +27,52 @@ extern "C" { #define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_DWMWA_COLOR_DEFAULT -1L #undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_DWMWA_COLOR_NONE #define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_DWMWA_COLOR_NONE -2L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_OVERWRITEPROMPT +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_OVERWRITEPROMPT 2L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_STRICTFILETYPES +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_STRICTFILETYPES 4L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOCHANGEDIR +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOCHANGEDIR 8L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_PICKFOLDERS +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_PICKFOLDERS 32L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCEFILESYSTEM +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCEFILESYSTEM 64L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_ALLNONSTORAGEITEMS +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_ALLNONSTORAGEITEMS 128L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOVALIDATE +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOVALIDATE 256L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_ALLOWMULTISELECT +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_ALLOWMULTISELECT 512L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_PATHMUSTEXIST +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_PATHMUSTEXIST 2048L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FILEMUSTEXIST +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FILEMUSTEXIST 4096L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_CREATEPROMPT +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_CREATEPROMPT 8192L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_SHAREAWARE +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_SHAREAWARE 16384L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOREADONLYRETURN +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOREADONLYRETURN 32768L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOTESTFILECREATE +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOTESTFILECREATE 65536L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_HIDEMRUPLACES +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_HIDEMRUPLACES 131072L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_HIDEPINNEDPLACES +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_HIDEPINNEDPLACES 262144L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NODEREFERENCELINKS +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NODEREFERENCELINKS 1048576L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_OKBUTTONNEEDSINTERACTION +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_OKBUTTONNEEDSINTERACTION 2097152L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_DONTADDTORECENT +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_DONTADDTORECENT 33554432L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCESHOWHIDDEN +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCESHOWHIDDEN 268435456L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_DEFAULTNOMINIMODE +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_DEFAULTNOMINIMODE 536870912L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCEPREVIEWPANEON +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCEPREVIEWPANEON 1073741824L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_SUPPORTSTREAMABLEITEMS +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_SUPPORTSTREAMABLEITEMS -2147483648L /* * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary * Method: getOSBuildNumberImpl @@ -67,6 +113,14 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_dwmSetWindowAttributeDWORD (JNIEnv *, jclass, jlong, jint, jint); +/* + * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary + * Method: showFileChooser + * Signature: (Ljava/awt/Window;ZLjava/lang/String;Ljava/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; + */ +JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showFileChooser + (JNIEnv *, jclass, jobject, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jint, jint, jint, jobjectArray); + #ifdef __cplusplus } #endif diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.java new file mode 100644 index 00000000..c15f1e63 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.java @@ -0,0 +1,551 @@ +/* + * 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.FlatNativeWindowsLibrary.*; +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 java.util.concurrent.atomic.AtomicInteger; +import javax.swing.*; +import com.formdev.flatlaf.extras.components.*; +import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; +import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatWindowsFileChooserTest + extends FlatTestPanel +{ + public static void main( String[] args ) { + SwingUtilities.invokeLater( () -> { + if( !FlatNativeWindowsLibrary.isLoaded() ) { + JOptionPane.showMessageDialog( null, "FlatLaf native library not loaded" ); + return; + } + + FlatTestFrame frame = FlatTestFrame.create( args, "FlatWindowsFileChooserTest" ); + addListeners( frame ); + frame.showFrame( FlatWindowsFileChooserTest::new ); + } ); + } + + FlatWindowsFileChooserTest() { + initComponents(); + + fileTypesField.setSelectedItem( null ); + } + + 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 ) { + Window owner = SwingUtilities.windowForComponent( this ); + String title = n( titleField.getText() ); + String okButtonLabel = n( okButtonLabelField.getText() ); + String fileNameLabel = n( fileNameLabelField.getText() ); + String fileName = n( fileNameField.getText() ); + String folder = n( folderField.getText() ); + String saveAsItem = n( saveAsItemField.getText() ); + String defaultFolder = n( defaultFolderField.getText() ); + String defaultExtension = n( defaultExtensionField.getText() ); + AtomicInteger optionsSet = new AtomicInteger(); + AtomicInteger optionsClear = new AtomicInteger(); + + o( FOS_OVERWRITEPROMPT, overwritePromptCheckBox, optionsSet, optionsClear ); + o( FOS_STRICTFILETYPES, strictFileTypesCheckBox, optionsSet, optionsClear ); + o( FOS_NOCHANGEDIR, noChangeDirCheckBox, optionsSet, optionsClear ); + o( FOS_PICKFOLDERS, pickFoldersCheckBox, optionsSet, optionsClear ); + o( FOS_FORCEFILESYSTEM, forceFileSystemCheckBox, optionsSet, optionsClear ); + o( FOS_ALLNONSTORAGEITEMS, allNonStorageItemsCheckBox, optionsSet, optionsClear ); + o( FOS_NOVALIDATE, noValidateCheckBox, optionsSet, optionsClear ); + o( FOS_ALLOWMULTISELECT, allowMultiSelectCheckBox, optionsSet, optionsClear ); + o( FOS_PATHMUSTEXIST, pathMustExistCheckBox, optionsSet, optionsClear ); + o( FOS_FILEMUSTEXIST, fileMustExistCheckBox, optionsSet, optionsClear ); + o( FOS_CREATEPROMPT, createPromptCheckBox, optionsSet, optionsClear ); + o( FOS_SHAREAWARE, shareAwareCheckBox, optionsSet, optionsClear ); + o( FOS_NOREADONLYRETURN, noReadOnlyReturnCheckBox, optionsSet, optionsClear ); + o( FOS_NOTESTFILECREATE, noTestFileCreateCheckBox, optionsSet, optionsClear ); + o( FOS_HIDEMRUPLACES, hideMruPlacesCheckBox, optionsSet, optionsClear ); + o( FOS_HIDEPINNEDPLACES, hidePinnedPlacesCheckBox, optionsSet, optionsClear ); + o( FOS_NODEREFERENCELINKS, noDereferenceLinksCheckBox, optionsSet, optionsClear ); + o( FOS_OKBUTTONNEEDSINTERACTION, okButtonNeedsInteractionCheckBox, optionsSet, optionsClear ); + o( FOS_DONTADDTORECENT, dontAddToRecentCheckBox, optionsSet, optionsClear ); + o( FOS_FORCESHOWHIDDEN, forceShowHiddenCheckBox, optionsSet, optionsClear ); + o( FOS_DEFAULTNOMINIMODE, defaultNoMiniModeCheckBox, optionsSet, optionsClear ); + o( FOS_FORCEPREVIEWPANEON, forcePreviewPaneonCheckBox, optionsSet, optionsClear ); + o( FOS_SUPPORTSTREAMABLEITEMS, supportStreamableItemsCheckBox, optionsSet, optionsClear ); + + String fileTypesStr = n( (String) fileTypesField.getSelectedItem() ); + String[] fileTypes = {}; + if( fileTypesStr != null ) + fileTypes = fileTypesStr.trim().split( "[,]+" ); + int fileTypeIndex = fileTypeIndexSlider.getValue(); + + if( direct ) { + String[] files = FlatNativeWindowsLibrary.showFileChooser( owner, open, + title, okButtonLabel, fileNameLabel, fileName, + folder, saveAsItem, defaultFolder, defaultExtension, + optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes ); + + filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); + } else { + SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); + + String[] fileTypes2 = fileTypes; + new Thread( () -> { + String[] files = FlatNativeWindowsLibrary.showFileChooser( owner, open, + title, okButtonLabel, fileNameLabel, fileName, + folder, saveAsItem, defaultFolder, defaultExtension, + optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes2 ); + + 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 != null && !s.isEmpty() ? s : null; + } + + private static void o( int option, FlatTriStateCheckBox checkBox, AtomicInteger optionsSet, AtomicInteger optionsClear ) { + if( checkBox.getState() == State.SELECTED ) + optionsSet.set( optionsSet.get() | option ); + else if( checkBox.getState() == State.UNSELECTED ) + optionsClear.set( optionsClear.get() | option ); + } + + 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(); + overwritePromptCheckBox = new FlatTriStateCheckBox(); + pathMustExistCheckBox = new FlatTriStateCheckBox(); + noDereferenceLinksCheckBox = new FlatTriStateCheckBox(); + strictFileTypesCheckBox = new FlatTriStateCheckBox(); + fileMustExistCheckBox = new FlatTriStateCheckBox(); + okButtonNeedsInteractionCheckBox = new FlatTriStateCheckBox(); + noChangeDirCheckBox = new FlatTriStateCheckBox(); + createPromptCheckBox = new FlatTriStateCheckBox(); + dontAddToRecentCheckBox = new FlatTriStateCheckBox(); + pickFoldersCheckBox = new FlatTriStateCheckBox(); + shareAwareCheckBox = new FlatTriStateCheckBox(); + forceShowHiddenCheckBox = new FlatTriStateCheckBox(); + forceFileSystemCheckBox = new FlatTriStateCheckBox(); + noReadOnlyReturnCheckBox = new FlatTriStateCheckBox(); + defaultNoMiniModeCheckBox = new FlatTriStateCheckBox(); + allNonStorageItemsCheckBox = new FlatTriStateCheckBox(); + noTestFileCreateCheckBox = new FlatTriStateCheckBox(); + forcePreviewPaneonCheckBox = new FlatTriStateCheckBox(); + noValidateCheckBox = new FlatTriStateCheckBox(); + hideMruPlacesCheckBox = new FlatTriStateCheckBox(); + supportStreamableItemsCheckBox = new FlatTriStateCheckBox(); + allowMultiSelectCheckBox = new FlatTriStateCheckBox(); + hidePinnedPlacesCheckBox = new FlatTriStateCheckBox(); + okButtonLabelLabel = new JLabel(); + okButtonLabelField = new JTextField(); + fileNameLabelLabel = new JLabel(); + fileNameLabelField = new JTextField(); + fileNameLabel = new JLabel(); + fileNameField = new JTextField(); + folderLabel = new JLabel(); + folderField = new JTextField(); + saveAsItemLabel = new JLabel(); + saveAsItemField = new JTextField(); + defaultFolderLabel = new JLabel(); + defaultFolderField = new JTextField(); + defaultExtensionLabel = new JLabel(); + defaultExtensionField = new JTextField(); + fileTypesLabel = new JLabel(); + fileTypesField = new JComboBox<>(); + fileTypeIndexLabel = new JLabel(); + fileTypeIndexSlider = new JSlider(); + 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]para" + + "[left]para" + + "[left]", + // rows + "[]0" + + "[]0" + + "[]0" + + "[]" + + "[]0" + + "[]0" + + "[]0" + + "[]0")); + + //---- overwritePromptCheckBox ---- + overwritePromptCheckBox.setText("overwritePrompt"); + panel1.add(overwritePromptCheckBox, "cell 0 0"); + + //---- pathMustExistCheckBox ---- + pathMustExistCheckBox.setText("pathMustExist"); + panel1.add(pathMustExistCheckBox, "cell 1 0"); + + //---- noDereferenceLinksCheckBox ---- + noDereferenceLinksCheckBox.setText("noDereferenceLinks"); + panel1.add(noDereferenceLinksCheckBox, "cell 2 0"); + + //---- strictFileTypesCheckBox ---- + strictFileTypesCheckBox.setText("strictFileTypes"); + panel1.add(strictFileTypesCheckBox, "cell 0 1"); + + //---- fileMustExistCheckBox ---- + fileMustExistCheckBox.setText("fileMustExist"); + panel1.add(fileMustExistCheckBox, "cell 1 1"); + + //---- okButtonNeedsInteractionCheckBox ---- + okButtonNeedsInteractionCheckBox.setText("okButtonNeedsInteraction"); + panel1.add(okButtonNeedsInteractionCheckBox, "cell 2 1"); + + //---- noChangeDirCheckBox ---- + noChangeDirCheckBox.setText("noChangeDir"); + panel1.add(noChangeDirCheckBox, "cell 0 2"); + + //---- createPromptCheckBox ---- + createPromptCheckBox.setText("createPrompt"); + panel1.add(createPromptCheckBox, "cell 1 2"); + + //---- dontAddToRecentCheckBox ---- + dontAddToRecentCheckBox.setText("dontAddToRecent"); + panel1.add(dontAddToRecentCheckBox, "cell 2 2"); + + //---- pickFoldersCheckBox ---- + pickFoldersCheckBox.setText("pickFolders"); + panel1.add(pickFoldersCheckBox, "cell 0 3"); + + //---- shareAwareCheckBox ---- + shareAwareCheckBox.setText("shareAware"); + panel1.add(shareAwareCheckBox, "cell 1 3"); + + //---- forceShowHiddenCheckBox ---- + forceShowHiddenCheckBox.setText("forceShowHidden"); + panel1.add(forceShowHiddenCheckBox, "cell 2 3"); + + //---- forceFileSystemCheckBox ---- + forceFileSystemCheckBox.setText("forceFileSystem"); + panel1.add(forceFileSystemCheckBox, "cell 0 4"); + + //---- noReadOnlyReturnCheckBox ---- + noReadOnlyReturnCheckBox.setText("noReadOnlyReturn"); + panel1.add(noReadOnlyReturnCheckBox, "cell 1 4"); + + //---- defaultNoMiniModeCheckBox ---- + defaultNoMiniModeCheckBox.setText("defaultNoMiniMode"); + panel1.add(defaultNoMiniModeCheckBox, "cell 2 4"); + + //---- allNonStorageItemsCheckBox ---- + allNonStorageItemsCheckBox.setText("allNonStorageItems"); + panel1.add(allNonStorageItemsCheckBox, "cell 0 5"); + + //---- noTestFileCreateCheckBox ---- + noTestFileCreateCheckBox.setText("noTestFileCreate"); + panel1.add(noTestFileCreateCheckBox, "cell 1 5"); + + //---- forcePreviewPaneonCheckBox ---- + forcePreviewPaneonCheckBox.setText("forcePreviewPaneon"); + panel1.add(forcePreviewPaneonCheckBox, "cell 2 5"); + + //---- noValidateCheckBox ---- + noValidateCheckBox.setText("noValidate"); + panel1.add(noValidateCheckBox, "cell 0 6"); + + //---- hideMruPlacesCheckBox ---- + hideMruPlacesCheckBox.setText("hideMruPlaces"); + panel1.add(hideMruPlacesCheckBox, "cell 1 6"); + + //---- supportStreamableItemsCheckBox ---- + supportStreamableItemsCheckBox.setText("supportStreamableItems"); + panel1.add(supportStreamableItemsCheckBox, "cell 2 6"); + + //---- allowMultiSelectCheckBox ---- + allowMultiSelectCheckBox.setText("allowMultiSelect"); + panel1.add(allowMultiSelectCheckBox, "cell 0 7"); + + //---- hidePinnedPlacesCheckBox ---- + hidePinnedPlacesCheckBox.setText("hidePinnedPlaces"); + panel1.add(hidePinnedPlacesCheckBox, "cell 1 7"); + } + add(panel1, "cell 2 0 1 10,aligny top,growy 0"); + + //---- okButtonLabelLabel ---- + okButtonLabelLabel.setText("okButtonLabel"); + add(okButtonLabelLabel, "cell 0 1"); + add(okButtonLabelField, "cell 1 1"); + + //---- fileNameLabelLabel ---- + fileNameLabelLabel.setText("fileNameLabel"); + add(fileNameLabelLabel, "cell 0 2"); + add(fileNameLabelField, "cell 1 2"); + + //---- fileNameLabel ---- + fileNameLabel.setText("fileName"); + add(fileNameLabel, "cell 0 3"); + add(fileNameField, "cell 1 3"); + + //---- folderLabel ---- + folderLabel.setText("folder"); + add(folderLabel, "cell 0 4"); + add(folderField, "cell 1 4"); + + //---- saveAsItemLabel ---- + saveAsItemLabel.setText("saveAsItem"); + add(saveAsItemLabel, "cell 0 5"); + add(saveAsItemField, "cell 1 5"); + + //---- defaultFolderLabel ---- + defaultFolderLabel.setText("defaultFolder"); + add(defaultFolderLabel, "cell 0 6"); + add(defaultFolderField, "cell 1 6"); + + //---- defaultExtensionLabel ---- + defaultExtensionLabel.setText("defaultExtension"); + add(defaultExtensionLabel, "cell 0 7"); + add(defaultExtensionField, "cell 1 7"); + + //---- fileTypesLabel ---- + fileTypesLabel.setText("fileTypes"); + add(fileTypesLabel, "cell 0 8"); + + //---- fileTypesField ---- + fileTypesField.setEditable(true); + fileTypesField.setModel(new DefaultComboBoxModel<>(new String[] { + "Text Files,*.txt", + "All Files,*.*", + "Text Files,*.txt,PDF Files,*.pdf,All Files,*.*", + "Text and PDF Files,*.txt;*.pdf" + })); + add(fileTypesField, "cell 1 8"); + + //---- fileTypeIndexLabel ---- + fileTypeIndexLabel.setText("fileTypeIndex"); + add(fileTypeIndexLabel, "cell 0 9"); + + //---- fileTypeIndexSlider ---- + fileTypeIndexSlider.setMaximum(10); + fileTypeIndexSlider.setMajorTickSpacing(1); + fileTypeIndexSlider.setValue(0); + fileTypeIndexSlider.setPaintLabels(true); + fileTypeIndexSlider.setSnapToTicks(true); + add(fileTypeIndexSlider, "cell 1 9"); + + //---- openButton ---- + openButton.setText("Open..."); + openButton.addActionListener(e -> open()); + add(openButton, "cell 0 10 3 1"); + + //---- saveButton ---- + saveButton.setText("Save..."); + saveButton.addActionListener(e -> save()); + add(saveButton, "cell 0 10 3 1"); + + //---- openDirectButton ---- + openDirectButton.setText("Open (no-thread)..."); + openDirectButton.addActionListener(e -> openDirect()); + add(openDirectButton, "cell 0 10 3 1"); + + //---- saveDirectButton ---- + saveDirectButton.setText("Save (no-thread)..."); + saveDirectButton.addActionListener(e -> saveDirect()); + add(saveDirectButton, "cell 0 10 3 1"); + + //======== filesScrollPane ======== + { + + //---- filesField ---- + filesField.setRows(8); + filesScrollPane.setViewportView(filesField); + } + add(filesScrollPane, "cell 0 11 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 FlatTriStateCheckBox overwritePromptCheckBox; + private FlatTriStateCheckBox pathMustExistCheckBox; + private FlatTriStateCheckBox noDereferenceLinksCheckBox; + private FlatTriStateCheckBox strictFileTypesCheckBox; + private FlatTriStateCheckBox fileMustExistCheckBox; + private FlatTriStateCheckBox okButtonNeedsInteractionCheckBox; + private FlatTriStateCheckBox noChangeDirCheckBox; + private FlatTriStateCheckBox createPromptCheckBox; + private FlatTriStateCheckBox dontAddToRecentCheckBox; + private FlatTriStateCheckBox pickFoldersCheckBox; + private FlatTriStateCheckBox shareAwareCheckBox; + private FlatTriStateCheckBox forceShowHiddenCheckBox; + private FlatTriStateCheckBox forceFileSystemCheckBox; + private FlatTriStateCheckBox noReadOnlyReturnCheckBox; + private FlatTriStateCheckBox defaultNoMiniModeCheckBox; + private FlatTriStateCheckBox allNonStorageItemsCheckBox; + private FlatTriStateCheckBox noTestFileCreateCheckBox; + private FlatTriStateCheckBox forcePreviewPaneonCheckBox; + private FlatTriStateCheckBox noValidateCheckBox; + private FlatTriStateCheckBox hideMruPlacesCheckBox; + private FlatTriStateCheckBox supportStreamableItemsCheckBox; + private FlatTriStateCheckBox allowMultiSelectCheckBox; + private FlatTriStateCheckBox hidePinnedPlacesCheckBox; + private JLabel okButtonLabelLabel; + private JTextField okButtonLabelField; + private JLabel fileNameLabelLabel; + private JTextField fileNameLabelField; + private JLabel fileNameLabel; + private JTextField fileNameField; + private JLabel folderLabel; + private JTextField folderField; + private JLabel saveAsItemLabel; + private JTextField saveAsItemField; + private JLabel defaultFolderLabel; + private JTextField defaultFolderField; + private JLabel defaultExtensionLabel; + private JTextField defaultExtensionField; + private JLabel fileTypesLabel; + private JComboBox fileTypesField; + private JLabel fileTypeIndexLabel; + private JSlider fileTypeIndexSlider; + 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/FlatWindowsFileChooserTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.jfd new file mode 100644 index 00000000..f26815de --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.jfd @@ -0,0 +1,324 @@ +JFDML JFormDesigner: "8.3" 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]para[left]para[left]" + "$rowConstraints": "[]0[]0[]0[][]0[]0[]0[]0" + } ) { + name: "panel1" + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "overwritePromptCheckBox" + "text": "overwritePrompt" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "pathMustExistCheckBox" + "text": "pathMustExist" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "noDereferenceLinksCheckBox" + "text": "noDereferenceLinks" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "strictFileTypesCheckBox" + "text": "strictFileTypes" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "fileMustExistCheckBox" + "text": "fileMustExist" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "okButtonNeedsInteractionCheckBox" + "text": "okButtonNeedsInteraction" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "noChangeDirCheckBox" + "text": "noChangeDir" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "createPromptCheckBox" + "text": "createPrompt" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "dontAddToRecentCheckBox" + "text": "dontAddToRecent" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "pickFoldersCheckBox" + "text": "pickFolders" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "shareAwareCheckBox" + "text": "shareAware" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "forceShowHiddenCheckBox" + "text": "forceShowHidden" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 3" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "forceFileSystemCheckBox" + "text": "forceFileSystem" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "noReadOnlyReturnCheckBox" + "text": "noReadOnlyReturn" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "defaultNoMiniModeCheckBox" + "text": "defaultNoMiniMode" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 4" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "allNonStorageItemsCheckBox" + "text": "allNonStorageItems" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "noTestFileCreateCheckBox" + "text": "noTestFileCreate" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "forcePreviewPaneonCheckBox" + "text": "forcePreviewPaneon" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 5" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "noValidateCheckBox" + "text": "noValidate" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "hideMruPlacesCheckBox" + "text": "hideMruPlaces" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "supportStreamableItemsCheckBox" + "text": "supportStreamableItems" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 6" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "allowMultiSelectCheckBox" + "text": "allowMultiSelect" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "hidePinnedPlacesCheckBox" + "text": "hidePinnedPlaces" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 1 10,aligny top,growy 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "okButtonLabelLabel" + "text": "okButtonLabel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "okButtonLabelField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileNameLabelLabel" + "text": "fileNameLabel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "fileNameLabelField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileNameLabel" + "text": "fileName" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "fileNameField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "folderLabel" + "text": "folder" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "folderField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "saveAsItemLabel" + "text": "saveAsItem" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "saveAsItemField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "defaultFolderLabel" + "text": "defaultFolder" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "defaultFolderField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "defaultExtensionLabel" + "text": "defaultExtension" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "defaultExtensionField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypesLabel" + "text": "fileTypes" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "fileTypesField" + "editable": true + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "Text Files,*.txt" + addElement( "Text Files,*.txt" ) + addElement( "All Files,*.*" ) + addElement( "Text Files,*.txt,PDF Files,*.pdf,All Files,*.*" ) + addElement( "Text and PDF Files,*.txt;*.pdf" ) + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 8" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypeIndexLabel" + "text": "fileTypeIndex" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 9" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "fileTypeIndexSlider" + "maximum": 10 + "majorTickSpacing": 1 + "value": 0 + "paintLabels": true + "snapToTicks": true + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 9" + } ) + 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 10 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 10 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 10 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 10 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 11 3 1,growx" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 690, 630 ) + } ) + } +} From 63272a03cf13f596a595af1d85e2543cabfd2296 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 31 Dec 2024 18:36:29 +0100 Subject: [PATCH 03/34] System File Chooser: macOS: - use `optionsSet` and `optionsClear` (as on Windows) - delete local reference after getting Java array item - added "or null" to javadoc --- .../flatlaf/ui/FlatNativeMacLibrary.java | 37 ++++---- ..._formdev_flatlaf_ui_FlatNativeMacLibrary.h | 30 +++---- .../src/main/objcpp/MacFileChooser.mm | 48 +++++----- .../testing/FlatMacOSFileChooserTest.java | 88 ++++++++++--------- .../testing/FlatMacOSFileChooserTest.jfd | 16 ++-- 5 files changed, 109 insertions(+), 110 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 60343778..5ceb2f61 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 @@ -75,18 +75,16 @@ public class FlatNativeMacLibrary // NSOpenPanel FC_canChooseFiles = 1 << 0, // default FC_canChooseDirectories = 1 << 1, - FC_resolvesAliases_NO = 1 << 2, // default + FC_resolvesAliases = 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; + FC_showsTagField = 1 << 8, // default for Save + FC_canCreateDirectories = 1 << 9, // default for Save + FC_canSelectHiddenExtension = 1 << 10, + FC_showsHiddenFiles = 1 << 11, + FC_extensionHidden = 1 << 12, + FC_allowsOtherFileTypes = 1 << 13, + FC_treatsFilePackagesAsDirectories = 1 << 14; /** * Shows the macOS system file dialogs @@ -98,13 +96,14 @@ public class FlatNativeMacLibrary * 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 title text displayed at top of save dialog (not used in open dialog); or {@code null} + * @param prompt text displayed in default button; or {@code null} + * @param message text displayed at top of open/save dialogs; or {@code null} + * @param nameFieldLabel text displayed in front of the filename text field in save dialog (not used in open dialog); or {@code null} + * @param nameFieldStringValue user-editable filename currently shown in the name field in save dialog (not used in open dialog); or {@code null} + * @param directoryURL current directory shown in the dialog; or {@code null} + * @param optionsSet options to set; see {@code FC_*} constants + * @param optionsClear options to clear; 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 * @@ -112,6 +111,6 @@ public class FlatNativeMacLibrary */ public native static String[] showFileChooser( boolean open, String title, String prompt, String message, String nameFieldLabel, - String nameFieldStringValue, String directoryURL, int options, - String... allowedFileTypes ); + String nameFieldStringValue, String directoryURL, + int optionsSet, int optionsClear, String... allowedFileTypes ); } 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 3c991a3c..7d163d7f 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 @@ -17,28 +17,24 @@ extern "C" { #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_resolvesAliases +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_resolvesAliases 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_showsTagField +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsTagField 256L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canCreateDirectories +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canCreateDirectories 512L #undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canSelectHiddenExtension -#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canSelectHiddenExtension 4096L +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canSelectHiddenExtension 1024L #undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsHiddenFiles -#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsHiddenFiles 16384L +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsHiddenFiles 2048L #undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_extensionHidden -#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_extensionHidden 65536L +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_extensionHidden 4096L #undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsOtherFileTypes -#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsOtherFileTypes 262144L +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsOtherFileTypes 8192L #undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_treatsFilePackagesAsDirectories -#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_treatsFilePackagesAsDirectories 1048576L +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_treatsFilePackagesAsDirectories 16384L /* * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary * Method: setWindowRoundedBorder @@ -82,10 +78,10 @@ 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;I[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;II[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); + (JNIEnv *, jclass, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jint, 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 d38ac371..05fd3d67 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -25,8 +25,9 @@ * @author Karl Tauber */ -#define isOptionSet( option ) ((options & com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ## option) != 0) -#define isYesOrNoOptionSet( option ) isOptionSet( option ## _YES ) || isOptionSet( option ## _NO ) +#define isOptionSet( option ) ((optionsSet & 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 ) // declare internal methods NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window ); @@ -38,7 +39,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ( JNIEnv* env, jclass cls, jboolean open, jstring title, jstring prompt, jstring message, jstring nameFieldLabel, jstring nameFieldStringValue, jstring directoryURL, - jint options, jobjectArray allowedFileTypes ) + jint optionsSet, jint optionsClear, jobjectArray allowedFileTypes ) { JNI_COCOA_ENTER() @@ -56,8 +57,8 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ 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; + nsArray[i] = JavaToNSString( env, str ); + env->DeleteLocalRef( str ); } nsAllowedFileTypes = nsArray; } @@ -93,26 +94,27 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ canChooseFiles = true; openDialog.canChooseFiles = canChooseFiles; openDialog.canChooseDirectories = canChooseDirectories; - if( isOptionSet( FC_resolvesAliases_NO ) ) - openDialog.resolvesAliases = NO; - if( isOptionSet( FC_allowsMultipleSelection ) ) - openDialog.allowsMultipleSelection = YES; + + if( isOptionSetOrClear( FC_resolvesAliases ) ) + openDialog.resolvesAliases = isOptionSet( FC_resolvesAliases ); + if( isOptionSetOrClear( FC_allowsMultipleSelection ) ) + openDialog.allowsMultipleSelection = isOptionSet( FC_allowsMultipleSelection ); } - 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; + if( isOptionSetOrClear( FC_showsTagField ) ) + dialog.showsTagField = isOptionSet( FC_showsTagField ); + if( isOptionSetOrClear( FC_canCreateDirectories ) ) + dialog.canCreateDirectories = isOptionSet( FC_canCreateDirectories ); + if( isOptionSetOrClear( FC_canSelectHiddenExtension ) ) + dialog.canSelectHiddenExtension = isOptionSet( FC_canSelectHiddenExtension ); + if( isOptionSetOrClear( FC_showsHiddenFiles) ) + dialog.showsHiddenFiles = isOptionSet( FC_showsHiddenFiles); + if( isOptionSetOrClear( FC_extensionHidden ) ) + dialog.extensionHidden = isOptionSet( FC_extensionHidden ); + if( isOptionSetOrClear( FC_allowsOtherFileTypes ) ) + dialog.allowsOtherFileTypes = isOptionSet( FC_allowsOtherFileTypes ); + if( isOptionSetOrClear( FC_treatsFilePackagesAsDirectories ) ) + dialog.treatsFilePackagesAsDirectories = isOptionSet( FC_treatsFilePackagesAsDirectories ); // use deprecated allowedFileTypes instead of newer allowedContentTypes (since macOS 11+) // to support older macOS versions 10.14+ and because of some problems with allowedContentTypes: 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 index 9f15a9da..5d4efd5c 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.java @@ -26,6 +26,7 @@ import java.awt.event.WindowFocusListener; import java.awt.event.WindowListener; import java.awt.event.WindowStateListener; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; import javax.swing.*; import com.formdev.flatlaf.extras.components.*; import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; @@ -52,6 +53,11 @@ public class FlatMacOSFileChooserTest } SwingUtilities.invokeLater( () -> { + if( !FlatNativeMacLibrary.isLoaded() ) { + JOptionPane.showMessageDialog( null, "FlatLaf native library not loaded" ); + return; + } + FlatTestFrame frame = FlatTestFrame.create( args, "FlatMacOSFileChooserTest" ); addListeners( frame ); frame.showFrame( FlatMacOSFileChooserTest::new ); @@ -85,37 +91,25 @@ public class FlatMacOSFileChooserTest String nameFieldLabel = n( nameFieldLabelField.getText() ); String nameFieldStringValue = n( nameFieldStringValueField.getText() ); String directoryURL = n( directoryURLField.getText() ); - int options = 0; + AtomicInteger optionsSet = new AtomicInteger(); + AtomicInteger optionsClear = new AtomicInteger(); // NSOpenPanel if( canChooseFilesCheckBox.isSelected() ) - options |= FC_canChooseFiles; + optionsSet.set( optionsSet.get() | FC_canChooseFiles ); if( canChooseDirectoriesCheckBox.isSelected() ) - options |= FC_canChooseDirectories; - if( !resolvesAliasesCheckBox.isSelected() ) - options |= FC_resolvesAliases_NO; - if( allowsMultipleSelectionCheckBox.isSelected() ) - options |= FC_allowsMultipleSelection; + optionsSet.set( optionsSet.get() | FC_canChooseDirectories ); + o( FC_resolvesAliases, resolvesAliasesCheckBox, optionsSet, optionsClear ); + o( FC_allowsMultipleSelection, allowsMultipleSelectionCheckBox, optionsSet, optionsClear ); // 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; + o( FC_showsTagField, showsTagFieldCheckBox, optionsSet, optionsClear ); + o( FC_canCreateDirectories, canCreateDirectoriesCheckBox, optionsSet, optionsClear ); + o( FC_canSelectHiddenExtension, canSelectHiddenExtensionCheckBox, optionsSet, optionsClear ); + o( FC_showsHiddenFiles, showsHiddenFilesCheckBox, optionsSet, optionsClear ); + o( FC_extensionHidden, extensionHiddenCheckBox, optionsSet, optionsClear ); + o( FC_allowsOtherFileTypes, allowsOtherFileTypesCheckBox, optionsSet, optionsClear ); + o( FC_treatsFilePackagesAsDirectories, treatsFilePackagesAsDirectoriesCheckBox, optionsSet, optionsClear ); String allowedFileTypesStr = n( allowedFileTypesField.getText() ); String[] allowedFileTypes = {}; @@ -124,17 +118,18 @@ public class FlatMacOSFileChooserTest if( direct ) { String[] files = FlatNativeMacLibrary.showFileChooser( open, title, prompt, message, - nameFieldLabel, nameFieldStringValue, directoryURL, options, allowedFileTypes ); + nameFieldLabel, nameFieldStringValue, directoryURL, + optionsSet.get(), optionsClear.get(), 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 ); + nameFieldLabel, nameFieldStringValue, directoryURL, + optionsSet.get(), optionsClear.get(), allowedFileTypes2 ); System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); @@ -152,6 +147,13 @@ public class FlatMacOSFileChooserTest return !s.isEmpty() ? s : null; } + private static void o( int option, FlatTriStateCheckBox checkBox, AtomicInteger optionsSet, AtomicInteger optionsClear ) { + if( checkBox.getState() == State.SELECTED ) + optionsSet.set( optionsSet.get() | option ); + else if( checkBox.getState() == State.UNSELECTED ) + optionsClear.set( optionsClear.get() | option ); + } + private static void addListeners( Window w ) { w.addWindowListener( new WindowListener() { @Override @@ -216,16 +218,16 @@ public class FlatMacOSFileChooserTest options1Label = new JLabel(); canChooseFilesCheckBox = new JCheckBox(); canChooseDirectoriesCheckBox = new JCheckBox(); - resolvesAliasesCheckBox = new JCheckBox(); - allowsMultipleSelectionCheckBox = new JCheckBox(); + resolvesAliasesCheckBox = new FlatTriStateCheckBox(); + allowsMultipleSelectionCheckBox = new FlatTriStateCheckBox(); options2Label = new JLabel(); showsTagFieldCheckBox = new FlatTriStateCheckBox(); canCreateDirectoriesCheckBox = new FlatTriStateCheckBox(); - canSelectHiddenExtensionCheckBox = new JCheckBox(); - showsHiddenFilesCheckBox = new JCheckBox(); - extensionHiddenCheckBox = new JCheckBox(); - allowsOtherFileTypesCheckBox = new JCheckBox(); - treatsFilePackagesAsDirectoriesCheckBox = new JCheckBox(); + canSelectHiddenExtensionCheckBox = new FlatTriStateCheckBox(); + showsHiddenFilesCheckBox = new FlatTriStateCheckBox(); + extensionHiddenCheckBox = new FlatTriStateCheckBox(); + allowsOtherFileTypesCheckBox = new FlatTriStateCheckBox(); + treatsFilePackagesAsDirectoriesCheckBox = new FlatTriStateCheckBox(); promptLabel = new JLabel(); promptField = new JTextField(); messageLabel = new JLabel(); @@ -305,7 +307,7 @@ public class FlatMacOSFileChooserTest //---- resolvesAliasesCheckBox ---- resolvesAliasesCheckBox.setText("resolvesAliases"); - resolvesAliasesCheckBox.setSelected(true); + resolvesAliasesCheckBox.setState(FlatTriStateCheckBox.State.SELECTED); panel1.add(resolvesAliasesCheckBox, "cell 0 3"); //---- allowsMultipleSelectionCheckBox ---- @@ -414,16 +416,16 @@ public class FlatMacOSFileChooserTest private JLabel options1Label; private JCheckBox canChooseFilesCheckBox; private JCheckBox canChooseDirectoriesCheckBox; - private JCheckBox resolvesAliasesCheckBox; - private JCheckBox allowsMultipleSelectionCheckBox; + private FlatTriStateCheckBox resolvesAliasesCheckBox; + private FlatTriStateCheckBox 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 FlatTriStateCheckBox canSelectHiddenExtensionCheckBox; + private FlatTriStateCheckBox showsHiddenFilesCheckBox; + private FlatTriStateCheckBox extensionHiddenCheckBox; + private FlatTriStateCheckBox allowsOtherFileTypesCheckBox; + private FlatTriStateCheckBox treatsFilePackagesAsDirectoriesCheckBox; private JLabel promptLabel; private JTextField promptField; private JLabel messageLabel; 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 index 0412f9c3..65c30bc1 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.jfd @@ -45,14 +45,14 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "resolvesAliasesCheckBox" "text": "resolvesAliases" - "selected": true + "state": enum com.formdev.flatlaf.extras.components.FlatTriStateCheckBox$State SELECTED }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "allowsMultipleSelectionCheckBox" "text": "allowsMultipleSelection" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { @@ -76,31 +76,31 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 7" } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "canSelectHiddenExtensionCheckBox" "text": "canSelectHiddenExtension" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 8" } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "showsHiddenFilesCheckBox" "text": "showsHiddenFiles" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 9" } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "extensionHiddenCheckBox" "text": "extensionHidden" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 10" } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "allowsOtherFileTypesCheckBox" "text": "allowsOtherFileTypes" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 11" } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "treatsFilePackagesAsDirectoriesCheckBox" "text": "treatsFilePackagesAsDirectories" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { From 2b810addd8bd3124f84c25c15eff41bafcca6185 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 3 Jan 2025 16:22:07 +0100 Subject: [PATCH 04/34] System File Chooser: implemented native bindings for GtkFileChooserDialog on Linux --- .github/workflows/natives.yml | 4 + .../flatlaf/ui/FlatNativeLinuxLibrary.java | 50 ++- .../flatlaf-natives-linux/README.md | 15 +- .../flatlaf-natives-linux/build.gradle.kts | 14 +- .../src/main/cpp/ApiVersion.cpp | 2 +- .../src/main/cpp/GtkFileChooser.cpp | 177 ++++++++ .../src/main/cpp/X11WmUtils.cpp | 2 +- ...ormdev_flatlaf_ui_FlatNativeLinuxLibrary.h | 20 + .../FlatSystemFileChooserLinuxTest.java | 392 ++++++++++++++++++ .../FlatSystemFileChooserLinuxTest.jfd | 182 ++++++++ 10 files changed, 848 insertions(+), 10 deletions(-) create mode 100644 flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd diff --git a/.github/workflows/natives.yml b/.github/workflows/natives.yml index cf314dcc..f908574b 100644 --- a/.github/workflows/natives.yml +++ b/.github/workflows/natives.yml @@ -32,6 +32,10 @@ jobs: - uses: gradle/actions/wrapper-validation@v4 + - name: install libgtk-3-dev + if: matrix.os == 'ubuntu' + run: sudo apt install libgtk-3-dev + - name: Setup Java 11 uses: actions/setup-java@v4 with: diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java index 952d0ece..353b10b0 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java @@ -34,9 +34,9 @@ import com.formdev.flatlaf.util.SystemInfo; * @author Karl Tauber * @since 2.5 */ -class FlatNativeLinuxLibrary +public class FlatNativeLinuxLibrary { - private static int API_VERSION_LINUX = 3001; + private static int API_VERSION_LINUX = 3002; /** * Checks whether native library is loaded/available. @@ -44,7 +44,7 @@ class FlatNativeLinuxLibrary * Note: It is required to invoke this method before invoking any other * method of this class. Otherwise, the native library may not be loaded. */ - static boolean isLoaded() { + public static boolean isLoaded() { return SystemInfo.isLinux && FlatNativeLibrary.isLoaded( API_VERSION_LINUX ); } @@ -115,4 +115,48 @@ class FlatNativeLinuxLibrary return (window instanceof JFrame && JFrame.isDefaultLookAndFeelDecorated() && ((JFrame)window).isUndecorated()) || (window instanceof JDialog && JDialog.isDefaultLookAndFeelDecorated() && ((JDialog)window).isUndecorated()); } + + + /** + * https://docs.gtk.org/gtk3/iface.FileChooser.html#properties + * + * @since 3.6 + */ + public static final int + FC_select_folder = 1 << 0, + FC_select_multiple = 1 << 1, + FC_show_hidden = 1 << 2, + FC_local_only = 1 << 3, // default + FC_do_overwrite_confirmation = 1 << 4, // GTK 3 only; removed and always-on in GTK 4 + FC_create_folders = 1 << 5; // default for Save + + /** + * Shows the Linux system file dialog + * GtkFileChooserDialog. + *

+ * 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 in dialog title; or {@code null} + * @param okButtonLabel text displayed in default button; or {@code null} + * @param currentName user-editable filename currently shown in the filename field in save dialog; or {@code null} + * @param currentFolder current directory shown in the dialog; or {@code null} + * @param optionsSet options to set; see {@code FOS_*} constants + * @param optionsClear options to clear; see {@code FOS_*} constants + * @param fileTypeIndex the file type that appears as selected (zero-based) + * @param fileTypes file types that the dialog can open or save. + * Two or more strings and {@code null} are required for each filter. + * First string is the display name of the filter shown in the combobox (e.g. "Text Files"). + * Subsequent strings are the filter patterns (e.g. "*.txt" or "*"). + * {@code null} is required to mark end of filter. + * @return file path(s) that the user selected; an empty array if canceled; + * or {@code null} on failures (no dialog shown) + * + * @since 3.6 + */ + public native static String[] showFileChooser( boolean open, + String title, String okButtonLabel, String currentName, String currentFolder, + int optionsSet, int optionsClear, int fileTypeIndex, String... fileTypes ); } diff --git a/flatlaf-natives/flatlaf-natives-linux/README.md b/flatlaf-natives/flatlaf-natives-linux/README.md index 3dd00c68..0b1cf98b 100644 --- a/flatlaf-natives/flatlaf-natives-linux/README.md +++ b/flatlaf-natives/flatlaf-natives-linux/README.md @@ -24,16 +24,25 @@ To build the library on Linux, some packages needs to be installed. ### Ubuntu `build-essential` contains GCC and development tools. `libxt-dev` contains the -X11 toolkit development headers. +X11 toolkit development headers. `libgtk-3-dev` contains the GTK toolkit +development headers. ~~~ sudo apt update -sudo apt install build-essential libxt-dev +sudo apt install build-essential libxt-dev libgtk-3-dev +~~~ + + +### Fedora + +~~~ +sudo dnf group install c-development +sudo dnf install libXt-devel gtk3-devel ~~~ ### CentOS ~~~ -sudo yum install libXt-devel +sudo yum install libXt-devel gtk3-devel ~~~ diff --git a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts index a81d6643..1ae7abbc 100644 --- a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts @@ -52,7 +52,17 @@ tasks { includes.from( "${javaHome}/include", - "${javaHome}/include/linux" + "${javaHome}/include/linux", + + // for GTK + "/usr/include/gtk-3.0", + "/usr/include/glib-2.0", + "/usr/lib/x86_64-linux-gnu/glib-2.0/include", + "/usr/include/gdk-pixbuf-2.0", + "/usr/include/atk-1.0", + "/usr/include/cairo", + "/usr/include/pango-1.0", + "/usr/include/harfbuzz", ) compilerArgs.addAll( toolChain.map { @@ -75,7 +85,7 @@ tasks { linkerArgs.addAll( toolChain.map { when( it ) { - is Gcc, is Clang -> listOf( "-L${jawtPath}", "-l${jawt}" ) + is Gcc, is Clang -> listOf( "-L${jawtPath}", "-l${jawt}", "-lgtk-3" ) else -> emptyList() } } ) diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/ApiVersion.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/ApiVersion.cpp index 02454fb3..cdd701a9 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/ApiVersion.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/ApiVersion.cpp @@ -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.FlatNativeLinuxLibrary -#define API_VERSION_LINUX 3001 +#define API_VERSION_LINUX 3002 //---- JNI methods ------------------------------------------------------------ diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp new file mode 100644 index 00000000..ca048af5 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp @@ -0,0 +1,177 @@ +/* + * Copyright 2025 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. + */ + +#include +#include +#include +#include +#include +#include "com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h" + +/** + * @author Karl Tauber + * @since 3.6 + */ + +//---- class AutoReleaseStringUTF8 -------------------------------------------- + +class AutoReleaseStringUTF8 { + JNIEnv* env; + jstring javaString; + const char* chars; + +public: + AutoReleaseStringUTF8( JNIEnv* _env, jstring _javaString ) { + env = _env; + javaString = _javaString; + chars = (javaString != NULL) ? env->GetStringUTFChars( javaString, NULL ) : NULL; + } + ~AutoReleaseStringUTF8() { + if( chars != NULL ) + env->ReleaseStringUTFChars( javaString, chars ); + } + operator const gchar*() { return chars; } +}; + +//---- helper ----------------------------------------------------------------- + +#define isOptionSet( option ) ((optionsSet & com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_ ## option) != 0) +#define isOptionClear( option ) ((optionsClear & com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_ ## option) != 0) +#define isOptionSetOrClear( option ) isOptionSet( option ) || isOptionClear( option ) + +jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { + jclass stringClass = env->FindClass( "java/lang/String" ); + return env->NewObjectArray( count, stringClass, NULL ); +} + +void initFilters( GtkFileChooser* chooser, JNIEnv* env, jint fileTypeIndex, jobjectArray fileTypes ) { + jint length = env->GetArrayLength( fileTypes ); + if( length <= 0 ) + return; + + GtkFileFilter* filter = NULL; + int filterIndex = 0; + for( int i = 0; i < length; i++ ) { + jstring jstr = (jstring) env->GetObjectArrayElement( fileTypes, i ); + if( jstr == NULL ) { + if( filter != NULL ) { + gtk_file_chooser_add_filter( chooser, filter ); + if( fileTypeIndex == filterIndex ) + gtk_file_chooser_set_filter( chooser, filter ); + filter = NULL; + filterIndex++; + } + continue; + } + + AutoReleaseStringUTF8 str( env, jstr ); + if( filter == NULL ) { + filter = gtk_file_filter_new(); + gtk_file_filter_set_name( filter, str ); + } else + gtk_file_filter_add_pattern( filter, str ); + } +} + +static void handle_response( GtkWidget* dialog, gint responseId, gpointer data ) { + if( responseId == GTK_RESPONSE_ACCEPT ) + *((GSList**)data) = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER( dialog ) ); + + gtk_widget_hide( dialog ); + gtk_widget_destroy( dialog ); + gtk_main_quit(); +} + +//---- JNI methods ------------------------------------------------------------ + +extern "C" +JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser + ( JNIEnv* env, jclass cls, jboolean open, + jstring title, jstring okButtonLabel, jstring currentName, jstring currentFolder, + jint optionsSet, jint optionsClear, jint fileTypeIndex, jobjectArray fileTypes ) +{ + // initialize GTK + if( !gtk_init_check( NULL, NULL ) ) + return NULL; + + // convert Java strings to C strings + AutoReleaseStringUTF8 ctitle( env, title ); + AutoReleaseStringUTF8 cokButtonLabel( env, okButtonLabel ); + AutoReleaseStringUTF8 ccurrentName( env, currentName ); + AutoReleaseStringUTF8 ccurrentFolder( env, currentFolder ); + + // create GTK file chooser dialog + // https://docs.gtk.org/gtk3/class.FileChooserDialog.html + bool selectFolder = isOptionSet( FC_select_folder ); + bool multiSelect = isOptionSet( FC_select_multiple ); + GtkWidget* dialog = gtk_file_chooser_dialog_new( + (ctitle != NULL) ? ctitle + : (open ? (selectFolder ? (multiSelect ? _("Select Folders") : _("Select Folder")) + : (multiSelect ? _("Open Files") : _("Open File"))) : _("Save File")), + NULL, // can not use AWT X11 window as parent because GtkWindow is required + open ? (selectFolder ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER : GTK_FILE_CHOOSER_ACTION_OPEN) + : GTK_FILE_CHOOSER_ACTION_SAVE, + _("_Cancel"), GTK_RESPONSE_CANCEL, + (cokButtonLabel != NULL) ? cokButtonLabel : (open ? _("_Open") : _("_Save")), GTK_RESPONSE_ACCEPT, + NULL ); // marks end of buttons + GtkFileChooser* chooser = GTK_FILE_CHOOSER( dialog ); + + if( !open && ccurrentName != NULL ) + gtk_file_chooser_set_current_name( chooser, ccurrentName ); + if( ccurrentFolder != NULL ) + gtk_file_chooser_set_current_folder( chooser, ccurrentFolder ); + + if( isOptionSetOrClear( FC_select_multiple ) ) + gtk_file_chooser_set_select_multiple( chooser, isOptionSet( FC_select_multiple ) ); + if( isOptionSetOrClear( FC_show_hidden ) ) + gtk_file_chooser_set_show_hidden( chooser, isOptionSet( FC_show_hidden ) ); + if( isOptionSetOrClear( FC_local_only ) ) + gtk_file_chooser_set_local_only( chooser, isOptionSet( FC_local_only ) ); + if( isOptionSetOrClear( FC_do_overwrite_confirmation ) ) + gtk_file_chooser_set_do_overwrite_confirmation( chooser, isOptionSet( FC_do_overwrite_confirmation ) ); + if( isOptionSetOrClear( FC_create_folders ) ) + gtk_file_chooser_set_create_folders( chooser, isOptionSet( FC_create_folders ) ); + + initFilters( chooser, env, fileTypeIndex, fileTypes ); + + gtk_window_set_modal( GTK_WINDOW( dialog ), true ); + + // show dialog + // (similar to what's done in sun_awt_X11_GtkFileDialogPeer.c) + GSList* fileList = NULL; + g_signal_connect( dialog, "response", G_CALLBACK( handle_response ), &fileList ); + gtk_widget_show( dialog ); + gtk_main(); + + // canceled? + if( fileList == NULL ) + return newJavaStringArray( env, 0 ); + + // convert GSList to Java string array + guint count = g_slist_length( fileList ); + jobjectArray array = newJavaStringArray( env, count ); + GSList* it = fileList; + for( int i = 0; i < count; i++, it = it->next ) { + gchar* path = (gchar*) it->data; + jstring jpath = env->NewStringUTF( path ); + g_free( path ); + + env->SetObjectArrayElement( array, i, jpath ); + env->DeleteLocalRef( jpath ); + } + g_slist_free( fileList ); + return array; +} diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp index 8cbc57a9..0f01b693 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp @@ -36,7 +36,7 @@ Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display** displa /** * Send _NET_WM_MOVERESIZE to window to initiate moving or resizing. * - * https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45446104441728 + * https://specifications.freedesktop.org/wm-spec/latest/ar01s04.html#id-1.5.4 * https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gdk/x11/gdksurface-x11.c#L3841-3881 */ extern "C" diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h index c3b8c4b2..dd0436a0 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h @@ -9,6 +9,18 @@ extern "C" { #endif #undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_MOVE #define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_MOVE 8L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_select_folder +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_select_folder 1L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_select_multiple +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_select_multiple 2L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_show_hidden +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_show_hidden 4L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_local_only +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_local_only 8L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_do_overwrite_confirmation +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_do_overwrite_confirmation 16L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_create_folders +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_create_folders 32L /* * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary * Method: xMoveOrResizeWindow @@ -25,6 +37,14 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xM JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xShowWindowMenu (JNIEnv *, jclass, jobject, jint, jint); +/* + * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary + * Method: showFileChooser + * Signature: (ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;III[Ljava/lang/String;)[Ljava/lang/String; + */ +JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser + (JNIEnv *, jclass, jboolean, jstring, jstring, jstring, jstring, jint, jint, jint, jobjectArray); + #ifdef __cplusplus } #endif diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java new file mode 100644 index 00000000..ecb0e439 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java @@ -0,0 +1,392 @@ +/* + * Copyright 2025 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.FlatNativeLinuxLibrary.*; +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 java.util.concurrent.atomic.AtomicInteger; +import javax.swing.*; +import com.formdev.flatlaf.extras.components.*; +import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; +import com.formdev.flatlaf.ui.FlatNativeLinuxLibrary; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatSystemFileChooserLinuxTest + extends FlatTestPanel +{ + public static void main( String[] args ) { + SwingUtilities.invokeLater( () -> { + if( !FlatNativeLinuxLibrary.isLoaded() ) { + JOptionPane.showMessageDialog( null, "FlatLaf native library not loaded" ); + return; + } + + FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserLinuxTest" ); + addListeners( frame ); + frame.showFrame( FlatSystemFileChooserLinuxTest::new ); + } ); + } + + FlatSystemFileChooserLinuxTest() { + initComponents(); + + fileTypesField.setSelectedItem( null ); + } + + 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 okButtonLabel = n( okButtonLabelField.getText() ); + String currentName = n( currentNameField.getText() ); + String currentFolder = n( currentFolderField.getText() ); + AtomicInteger optionsSet = new AtomicInteger(); + AtomicInteger optionsClear = new AtomicInteger(); + + o( FC_select_folder, select_folderCheckBox, optionsSet, optionsClear ); + o( FC_select_multiple, select_multipleCheckBox, optionsSet, optionsClear ); + o( FC_show_hidden, show_hiddenCheckBox, optionsSet, optionsClear ); + o( FC_local_only, local_onlyCheckBox, optionsSet, optionsClear ); + o( FC_do_overwrite_confirmation, do_overwrite_confirmationCheckBox, optionsSet, optionsClear ); + o( FC_create_folders, create_foldersCheckBox, optionsSet, optionsClear ); + + String fileTypesStr = n( (String) fileTypesField.getSelectedItem() ); + String[] fileTypes = {}; + if( fileTypesStr != null ) { + if( !fileTypesStr.endsWith( ",null" ) ) + fileTypesStr += ",null"; + fileTypes = fileTypesStr.trim().split( "[,]+" ); + for( int i = 0; i < fileTypes.length; i++ ) { + if( "null".equals( fileTypes[i] ) ) + fileTypes[i] = null; + } + } + int fileTypeIndex = fileTypeIndexSlider.getValue(); + + if( direct ) { + String[] files = FlatNativeLinuxLibrary.showFileChooser( open, + title, okButtonLabel, currentName, currentFolder, + optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes ); + + filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); + } else { + SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); + + String[] fileTypes2 = fileTypes; + new Thread( () -> { + String[] files = FlatNativeLinuxLibrary.showFileChooser( open, + title, okButtonLabel, currentName, currentFolder, + optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes2 ); + + 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 != null && !s.isEmpty() ? s : null; + } + + private static void o( int option, FlatTriStateCheckBox checkBox, AtomicInteger optionsSet, AtomicInteger optionsClear ) { + if( checkBox.getState() == State.SELECTED ) + optionsSet.set( optionsSet.get() | option ); + else if( checkBox.getState() == State.UNSELECTED ) + optionsClear.set( optionsClear.get() | option ); + } + + 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(); + select_folderCheckBox = new FlatTriStateCheckBox(); + select_multipleCheckBox = new FlatTriStateCheckBox(); + do_overwrite_confirmationCheckBox = new FlatTriStateCheckBox(); + create_foldersCheckBox = new FlatTriStateCheckBox(); + show_hiddenCheckBox = new FlatTriStateCheckBox(); + local_onlyCheckBox = new FlatTriStateCheckBox(); + okButtonLabelLabel = new JLabel(); + okButtonLabelField = new JTextField(); + currentNameLabel = new JLabel(); + currentNameField = new JTextField(); + currentFolderLabel = new JLabel(); + currentFolderField = new JTextField(); + fileTypesLabel = new JLabel(); + fileTypesField = new JComboBox<>(); + fileTypeIndexLabel = new JLabel(); + fileTypeIndexSlider = new JSlider(); + 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" + + "[]0" + + "[]0" + + "[]")); + + //---- select_folderCheckBox ---- + select_folderCheckBox.setText("select_folder"); + select_folderCheckBox.setAllowIndeterminate(false); + select_folderCheckBox.setState(FlatTriStateCheckBox.State.UNSELECTED); + panel1.add(select_folderCheckBox, "cell 0 0"); + + //---- select_multipleCheckBox ---- + select_multipleCheckBox.setText("select_multiple"); + select_multipleCheckBox.setState(FlatTriStateCheckBox.State.UNSELECTED); + select_multipleCheckBox.setAllowIndeterminate(false); + panel1.add(select_multipleCheckBox, "cell 0 1"); + + //---- do_overwrite_confirmationCheckBox ---- + do_overwrite_confirmationCheckBox.setText("do_overwrite_confirmation"); + panel1.add(do_overwrite_confirmationCheckBox, "cell 0 2"); + + //---- create_foldersCheckBox ---- + create_foldersCheckBox.setText("create_folders"); + panel1.add(create_foldersCheckBox, "cell 0 3"); + + //---- show_hiddenCheckBox ---- + show_hiddenCheckBox.setText("show_hidden"); + panel1.add(show_hiddenCheckBox, "cell 0 4"); + + //---- local_onlyCheckBox ---- + local_onlyCheckBox.setText("local_only"); + panel1.add(local_onlyCheckBox, "cell 0 5"); + } + add(panel1, "cell 2 0 1 6,aligny top,growy 0"); + + //---- okButtonLabelLabel ---- + okButtonLabelLabel.setText("okButtonLabel"); + add(okButtonLabelLabel, "cell 0 1"); + add(okButtonLabelField, "cell 1 1"); + + //---- currentNameLabel ---- + currentNameLabel.setText("currentName"); + add(currentNameLabel, "cell 0 2"); + add(currentNameField, "cell 1 2"); + + //---- currentFolderLabel ---- + currentFolderLabel.setText("currentFolder"); + add(currentFolderLabel, "cell 0 3"); + add(currentFolderField, "cell 1 3"); + + //---- fileTypesLabel ---- + fileTypesLabel.setText("fileTypes"); + add(fileTypesLabel, "cell 0 4"); + + //---- fileTypesField ---- + fileTypesField.setEditable(true); + fileTypesField.setModel(new DefaultComboBoxModel<>(new String[] { + "Text Files,*.txt,null", + "All Files,*,null", + "Text Files,*.txt,null,PDF Files,*.pdf,null,All Files,*,null", + "Text and PDF Files,*.txt,*.pdf,null" + })); + add(fileTypesField, "cell 1 4"); + + //---- fileTypeIndexLabel ---- + fileTypeIndexLabel.setText("fileTypeIndex"); + add(fileTypeIndexLabel, "cell 0 5"); + + //---- fileTypeIndexSlider ---- + fileTypeIndexSlider.setMaximum(10); + fileTypeIndexSlider.setMajorTickSpacing(1); + fileTypeIndexSlider.setValue(0); + fileTypeIndexSlider.setPaintLabels(true); + fileTypeIndexSlider.setSnapToTicks(true); + add(fileTypeIndexSlider, "cell 1 5"); + + //---- openButton ---- + openButton.setText("Open..."); + openButton.addActionListener(e -> open()); + add(openButton, "cell 0 6 3 1"); + + //---- saveButton ---- + saveButton.setText("Save..."); + saveButton.addActionListener(e -> save()); + add(saveButton, "cell 0 6 3 1"); + + //---- openDirectButton ---- + openDirectButton.setText("Open (no-thread)..."); + openDirectButton.addActionListener(e -> openDirect()); + add(openDirectButton, "cell 0 6 3 1"); + + //---- saveDirectButton ---- + saveDirectButton.setText("Save (no-thread)..."); + saveDirectButton.addActionListener(e -> saveDirect()); + add(saveDirectButton, "cell 0 6 3 1"); + + //======== filesScrollPane ======== + { + + //---- filesField ---- + filesField.setRows(8); + filesScrollPane.setViewportView(filesField); + } + add(filesScrollPane, "cell 0 7 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 FlatTriStateCheckBox select_folderCheckBox; + private FlatTriStateCheckBox select_multipleCheckBox; + private FlatTriStateCheckBox do_overwrite_confirmationCheckBox; + private FlatTriStateCheckBox create_foldersCheckBox; + private FlatTriStateCheckBox show_hiddenCheckBox; + private FlatTriStateCheckBox local_onlyCheckBox; + private JLabel okButtonLabelLabel; + private JTextField okButtonLabelField; + private JLabel currentNameLabel; + private JTextField currentNameField; + private JLabel currentFolderLabel; + private JTextField currentFolderField; + private JLabel fileTypesLabel; + private JComboBox fileTypesField; + private JLabel fileTypeIndexLabel; + private JSlider fileTypeIndexSlider; + 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/FlatSystemFileChooserLinuxTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd new file mode 100644 index 00000000..2c463484 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd @@ -0,0 +1,182 @@ +JFDML JFormDesigner: "8.3" 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[]0[]0[]" + } ) { + name: "panel1" + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "select_folderCheckBox" + "text": "select_folder" + "allowIndeterminate": false + "state": enum com.formdev.flatlaf.extras.components.FlatTriStateCheckBox$State UNSELECTED + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "select_multipleCheckBox" + "text": "select_multiple" + "state": enum com.formdev.flatlaf.extras.components.FlatTriStateCheckBox$State UNSELECTED + "allowIndeterminate": false + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "do_overwrite_confirmationCheckBox" + "text": "do_overwrite_confirmation" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "create_foldersCheckBox" + "text": "create_folders" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "show_hiddenCheckBox" + "text": "show_hidden" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "local_onlyCheckBox" + "text": "local_only" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 1 6,aligny top,growy 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "okButtonLabelLabel" + "text": "okButtonLabel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "okButtonLabelField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "currentNameLabel" + "text": "currentName" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "currentNameField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "currentFolderLabel" + "text": "currentFolder" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "currentFolderField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypesLabel" + "text": "fileTypes" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "fileTypesField" + "editable": true + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "Text Files,*.txt,null" + addElement( "Text Files,*.txt,null" ) + addElement( "All Files,*,null" ) + addElement( "Text Files,*.txt,null,PDF Files,*.pdf,null,All Files,*,null" ) + addElement( "Text and PDF Files,*.txt,*.pdf,null" ) + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypeIndexLabel" + "text": "fileTypeIndex" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "fileTypeIndexSlider" + "maximum": 10 + "majorTickSpacing": 1 + "value": 0 + "paintLabels": true + "snapToTicks": true + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + 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 6 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 6 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 6 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 6 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 7 3 1,growx" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 690, 630 ) + } ) + } +} From a303cd2dec85877ce3cec29094286a2127cdf570 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 3 Jan 2025 17:56:02 +0100 Subject: [PATCH 05/34] System File Chooser: renamed Windows and macOS test apps --- ...ChooserTest.java => FlatSystemFileChooserMacTest.java} | 8 ++++---- ...leChooserTest.jfd => FlatSystemFileChooserMacTest.jfd} | 0 ...serTest.java => FlatSystemFileChooserWindowsTest.java} | 8 ++++---- ...ooserTest.jfd => FlatSystemFileChooserWindowsTest.jfd} | 0 4 files changed, 8 insertions(+), 8 deletions(-) rename flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/{FlatMacOSFileChooserTest.java => FlatSystemFileChooserMacTest.java} (98%) rename flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/{FlatMacOSFileChooserTest.jfd => FlatSystemFileChooserMacTest.jfd} (100%) rename flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/{FlatWindowsFileChooserTest.java => FlatSystemFileChooserWindowsTest.java} (98%) rename flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/{FlatWindowsFileChooserTest.jfd => FlatSystemFileChooserWindowsTest.jfd} (100%) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.java similarity index 98% rename from flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.java rename to flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.java index 5d4efd5c..ae52327d 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.java @@ -37,7 +37,7 @@ import net.miginfocom.swing.*; /** * @author Karl Tauber */ -public class FlatMacOSFileChooserTest +public class FlatSystemFileChooserMacTest extends FlatTestPanel { public static void main( String[] args ) { @@ -58,13 +58,13 @@ public class FlatMacOSFileChooserTest return; } - FlatTestFrame frame = FlatTestFrame.create( args, "FlatMacOSFileChooserTest" ); + FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserMacTest" ); addListeners( frame ); - frame.showFrame( FlatMacOSFileChooserTest::new ); + frame.showFrame( FlatSystemFileChooserMacTest::new ); } ); } - FlatMacOSFileChooserTest() { + FlatSystemFileChooserMacTest() { initComponents(); } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.jfd similarity index 100% rename from flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatMacOSFileChooserTest.jfd rename to flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.jfd diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java similarity index 98% rename from flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.java rename to flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java index c15f1e63..ab1f0865 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java @@ -36,7 +36,7 @@ import net.miginfocom.swing.*; /** * @author Karl Tauber */ -public class FlatWindowsFileChooserTest +public class FlatSystemFileChooserWindowsTest extends FlatTestPanel { public static void main( String[] args ) { @@ -46,13 +46,13 @@ public class FlatWindowsFileChooserTest return; } - FlatTestFrame frame = FlatTestFrame.create( args, "FlatWindowsFileChooserTest" ); + FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserWindowsTest" ); addListeners( frame ); - frame.showFrame( FlatWindowsFileChooserTest::new ); + frame.showFrame( FlatSystemFileChooserWindowsTest::new ); } ); } - FlatWindowsFileChooserTest() { + FlatSystemFileChooserWindowsTest() { initComponents(); fileTypesField.setSelectedItem( null ); diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd similarity index 100% rename from flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowsFileChooserTest.jfd rename to flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd From 641fada6c41f05603aa96308872a03ed73e36375 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 4 Jan 2025 12:22:14 +0100 Subject: [PATCH 06/34] System File Chooser: implemented modality for GtkFileChooserDialog on Linux --- .../flatlaf/ui/FlatNativeLinuxLibrary.java | 5 +- .../src/main/cpp/GtkFileChooser.cpp | 90 ++++++++++++++++- ...ormdev_flatlaf_ui_FlatNativeLinuxLibrary.h | 4 +- .../FlatSystemFileChooserLinuxTest.java | 97 +++++++++++++++---- .../FlatSystemFileChooserLinuxTest.jfd | 76 +++++++++++---- 5 files changed, 226 insertions(+), 46 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java index 353b10b0..9ebdda20 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java @@ -138,9 +138,10 @@ public class FlatNativeLinuxLibrary * the file dialog. It is highly recommended to invoke it from a new thread * to avoid blocking the AWT event dispatching thread. * + * @param owner the owner of the file dialog; or {@code null} * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog * @param title text displayed in dialog title; or {@code null} - * @param okButtonLabel text displayed in default button; or {@code null} + * @param okButtonLabel text displayed in default button; or {@code null}. Use '_' for mnemonics (e.g. "_Choose") * @param currentName user-editable filename currently shown in the filename field in save dialog; or {@code null} * @param currentFolder current directory shown in the dialog; or {@code null} * @param optionsSet options to set; see {@code FOS_*} constants @@ -156,7 +157,7 @@ public class FlatNativeLinuxLibrary * * @since 3.6 */ - public native static String[] showFileChooser( boolean open, + public native static String[] showFileChooser( Window owner, boolean open, String title, String okButtonLabel, String currentName, String currentFolder, int optionsSet, int optionsClear, int fileTypeIndex, String... fileTypes ); } diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp index ca048af5..01017446 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp @@ -26,6 +26,9 @@ * @since 3.6 */ +// see X11WmUtils.cpp +Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display** display_return ); + //---- class AutoReleaseStringUTF8 -------------------------------------------- class AutoReleaseStringUTF8 { @@ -86,10 +89,65 @@ void initFilters( GtkFileChooser* chooser, JNIEnv* env, jint fileTypeIndex, jobj } } +GdkWindow* getGdkWindow( JNIEnv* env, jobject window ) { + // get the AWT + JAWT awt; + awt.version = JAWT_VERSION_1_4; + if( !JAWT_GetAWT( env, &awt ) ) + return NULL; + + // get Xlib window and display from AWT window + Display* display; + Window w = getWindowHandle( env, &awt, window, &display ); + if( w == 0 ) + return NULL; + + // based on GetAllocNativeWindowHandle() from https://github.com/btzy/nativefiledialog-extended + // https://github.com/btzy/nativefiledialog-extended/blob/29e3bcb578345b9fa345d1d7683f00c150565ca3/src/nfd_gtk.cpp#L384-L437 + GdkDisplay* gdkDisplay = gdk_x11_lookup_xdisplay( display ); + if( gdkDisplay == NULL ) { + // search for existing X11 display (there should only be one, even if multiple screens are connected) + GdkDisplayManager* displayManager = gdk_display_manager_get(); + GSList* displays = gdk_display_manager_list_displays( displayManager ); + for( GSList* l = displays; l; l = l->next ) { + if( GDK_IS_X11_DISPLAY( l->data ) ) { + gdkDisplay = GDK_DISPLAY( l->data ); + break; + } + } + g_slist_free( displays ); + + // create our own X11 display + if( gdkDisplay == NULL ) { + gdk_set_allowed_backends( "x11" ); + gdkDisplay = gdk_display_manager_open_display( displayManager, NULL ); + gdk_set_allowed_backends( NULL ); + + if( gdkDisplay == NULL ) + return NULL; + } + } + + return gdk_x11_window_foreign_new_for_display( gdkDisplay, w ); +} + +static void handle_realize( GtkWidget* dialog, gpointer data ) { + GdkWindow* gdkOwner = static_cast( data ); + + // make file dialog a transient of owner window, + // which centers file dialog on owner and keeps file dialog above owner + gdk_window_set_transient_for( gtk_widget_get_window( dialog ), gdkOwner ); + + // necessary because gdk_x11_window_foreign_new_for_display() increases the reference counter + g_object_unref( gdkOwner ); +} + static void handle_response( GtkWidget* dialog, gint responseId, gpointer data ) { + // get filenames if user pressed OK if( responseId == GTK_RESPONSE_ACCEPT ) *((GSList**)data) = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER( dialog ) ); + // hide/destroy file dialog and quit loop gtk_widget_hide( dialog ); gtk_widget_destroy( dialog ); gtk_main_quit(); @@ -99,7 +157,7 @@ static void handle_response( GtkWidget* dialog, gint responseId, gpointer data ) extern "C" JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser - ( JNIEnv* env, jclass cls, jboolean open, + ( JNIEnv* env, jclass cls, jobject owner, jboolean open, jstring title, jstring okButtonLabel, jstring currentName, jstring currentFolder, jint optionsSet, jint optionsClear, jint fileTypeIndex, jobjectArray fileTypes ) { @@ -129,11 +187,13 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrar NULL ); // marks end of buttons GtkFileChooser* chooser = GTK_FILE_CHOOSER( dialog ); + // set current name and folder if( !open && ccurrentName != NULL ) gtk_file_chooser_set_current_name( chooser, ccurrentName ); if( ccurrentFolder != NULL ) gtk_file_chooser_set_current_folder( chooser, ccurrentFolder ); + // set options if( isOptionSetOrClear( FC_select_multiple ) ) gtk_file_chooser_set_select_multiple( chooser, isOptionSet( FC_select_multiple ) ); if( isOptionSetOrClear( FC_show_hidden ) ) @@ -145,15 +205,39 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrar if( isOptionSetOrClear( FC_create_folders ) ) gtk_file_chooser_set_create_folders( chooser, isOptionSet( FC_create_folders ) ); + // initialize filter initFilters( chooser, env, fileTypeIndex, fileTypes ); - gtk_window_set_modal( GTK_WINDOW( dialog ), true ); + // setup modality + GdkWindow* gdkOwner = (owner != NULL) ? getGdkWindow( env, owner ) : NULL; + if( gdkOwner != NULL ) { + gtk_window_set_modal( GTK_WINDOW( dialog ), true ); + + // file dialog should use same screen as owner + gtk_window_set_screen( GTK_WINDOW( dialog ), gdk_window_get_screen( gdkOwner ) ); + + // set the transient when the file dialog is realized + g_signal_connect( dialog, "realize", G_CALLBACK( handle_realize ), gdkOwner ); + } // show dialog // (similar to what's done in sun_awt_X11_GtkFileDialogPeer.c) GSList* fileList = NULL; - g_signal_connect( dialog, "response", G_CALLBACK( handle_response ), &fileList ); + g_signal_connect( dialog, "response", G_CALLBACK( handle_response ), &fileList ); gtk_widget_show( dialog ); + + // necessary to bring file dialog to the front (and make it active) + // see issues: + // https://github.com/btzy/nativefiledialog-extended/issues/31 + // https://github.com/mlabbe/nativefiledialog/pull/92 + // https://github.com/guillaumechereau/noc/pull/11 + if( GDK_IS_X11_DISPLAY( gtk_widget_get_display( GTK_WIDGET( dialog ) ) ) ) { + GdkWindow* gdkWindow = gtk_widget_get_window( GTK_WIDGET( dialog ) ); + gdk_window_set_events( gdkWindow, static_cast( gdk_window_get_events( gdkWindow ) | GDK_PROPERTY_CHANGE_MASK ) ); + gtk_window_present_with_time( GTK_WINDOW( dialog ), gdk_x11_get_server_time( gdkWindow ) ); + } + + // start event loop (will be quit in respone handler) gtk_main(); // canceled? diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h index dd0436a0..4ca717bb 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h @@ -40,10 +40,10 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xS /* * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary * Method: showFileChooser - * Signature: (ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;III[Ljava/lang/String;)[Ljava/lang/String; + * Signature: (Ljava/awt/Window;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;III[Ljava/lang/String;)[Ljava/lang/String; */ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser - (JNIEnv *, jclass, jboolean, jstring, jstring, jstring, jstring, jint, jint, jint, jobjectArray); + (JNIEnv *, jclass, jobject, jboolean, jstring, jstring, jstring, jstring, jint, jint, jint, jobjectArray); #ifdef __cplusplus } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java index ecb0e439..05289d16 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java @@ -17,10 +17,12 @@ package com.formdev.flatlaf.testing; import static com.formdev.flatlaf.ui.FlatNativeLinuxLibrary.*; +import java.awt.Dialog; import java.awt.EventQueue; import java.awt.SecondaryLoop; import java.awt.Toolkit; import java.awt.Window; +import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; import java.awt.event.WindowListener; @@ -75,6 +77,26 @@ public class FlatSystemFileChooserLinuxTest } private void openOrSave( boolean open, boolean direct ) { + Window frame = SwingUtilities.windowForComponent( this ); + if( ownerFrameRadioButton.isSelected() ) + openOrSave( open, direct, frame ); + else if( ownerDialogRadioButton.isSelected() ) { + JDialog dialog = new JDialog( frame, "Dummy Modal Dialog", Dialog.DEFAULT_MODALITY_TYPE ); + dialog.setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE ); + dialog.addWindowListener( new WindowAdapter() { + @Override + public void windowOpened( WindowEvent e ) { + openOrSave( open, direct, dialog ); + } + } ); + dialog.setSize( 1200, 1000 ); + dialog.setLocationRelativeTo( this ); + dialog.setVisible( true ); + } else + openOrSave( open, direct, null ); + } + + private void openOrSave( boolean open, boolean direct, Window owner ) { String title = n( titleField.getText() ); String okButtonLabel = n( okButtonLabelField.getText() ); String currentName = n( currentNameField.getText() ); @@ -103,7 +125,7 @@ public class FlatSystemFileChooserLinuxTest int fileTypeIndex = fileTypeIndexSlider.getValue(); if( direct ) { - String[] files = FlatNativeLinuxLibrary.showFileChooser( open, + String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, open, title, okButtonLabel, currentName, currentFolder, optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes ); @@ -113,7 +135,7 @@ public class FlatSystemFileChooserLinuxTest String[] fileTypes2 = fileTypes; new Thread( () -> { - String[] files = FlatNativeLinuxLibrary.showFileChooser( open, + String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, open, title, okButtonLabel, currentName, currentFolder, optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes2 ); @@ -198,6 +220,11 @@ public class FlatSystemFileChooserLinuxTest private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + ownerLabel = new JLabel(); + ownerFrameRadioButton = new JRadioButton(); + ownerDialogRadioButton = new JRadioButton(); + ownerNullRadioButton = new JRadioButton(); + ownerSpacer = new JPanel(null); titleLabel = new JLabel(); titleField = new JTextField(); panel1 = new JPanel(); @@ -239,12 +266,31 @@ public class FlatSystemFileChooserLinuxTest "[]" + "[]" + "[]" + + "[]" + "[grow,fill]")); + //---- ownerLabel ---- + ownerLabel.setText("owner"); + add(ownerLabel, "cell 0 0"); + + //---- ownerFrameRadioButton ---- + ownerFrameRadioButton.setText("JFrame"); + ownerFrameRadioButton.setSelected(true); + add(ownerFrameRadioButton, "cell 1 0"); + + //---- ownerDialogRadioButton ---- + ownerDialogRadioButton.setText("JDialog"); + add(ownerDialogRadioButton, "cell 1 0"); + + //---- ownerNullRadioButton ---- + ownerNullRadioButton.setText("null"); + add(ownerNullRadioButton, "cell 1 0"); + add(ownerSpacer, "cell 1 0,growx"); + //---- titleLabel ---- titleLabel.setText("title"); - add(titleLabel, "cell 0 0"); - add(titleField, "cell 1 0"); + add(titleLabel, "cell 0 1"); + add(titleField, "cell 1 1"); //======== panel1 ======== { @@ -288,26 +334,26 @@ public class FlatSystemFileChooserLinuxTest local_onlyCheckBox.setText("local_only"); panel1.add(local_onlyCheckBox, "cell 0 5"); } - add(panel1, "cell 2 0 1 6,aligny top,growy 0"); + add(panel1, "cell 2 1 1 6,aligny top,growy 0"); //---- okButtonLabelLabel ---- okButtonLabelLabel.setText("okButtonLabel"); - add(okButtonLabelLabel, "cell 0 1"); - add(okButtonLabelField, "cell 1 1"); + add(okButtonLabelLabel, "cell 0 2"); + add(okButtonLabelField, "cell 1 2"); //---- currentNameLabel ---- currentNameLabel.setText("currentName"); - add(currentNameLabel, "cell 0 2"); - add(currentNameField, "cell 1 2"); + add(currentNameLabel, "cell 0 3"); + add(currentNameField, "cell 1 3"); //---- currentFolderLabel ---- currentFolderLabel.setText("currentFolder"); - add(currentFolderLabel, "cell 0 3"); - add(currentFolderField, "cell 1 3"); + add(currentFolderLabel, "cell 0 4"); + add(currentFolderField, "cell 1 4"); //---- fileTypesLabel ---- fileTypesLabel.setText("fileTypes"); - add(fileTypesLabel, "cell 0 4"); + add(fileTypesLabel, "cell 0 5"); //---- fileTypesField ---- fileTypesField.setEditable(true); @@ -317,11 +363,11 @@ public class FlatSystemFileChooserLinuxTest "Text Files,*.txt,null,PDF Files,*.pdf,null,All Files,*,null", "Text and PDF Files,*.txt,*.pdf,null" })); - add(fileTypesField, "cell 1 4"); + add(fileTypesField, "cell 1 5"); //---- fileTypeIndexLabel ---- fileTypeIndexLabel.setText("fileTypeIndex"); - add(fileTypeIndexLabel, "cell 0 5"); + add(fileTypeIndexLabel, "cell 0 6"); //---- fileTypeIndexSlider ---- fileTypeIndexSlider.setMaximum(10); @@ -329,27 +375,27 @@ public class FlatSystemFileChooserLinuxTest fileTypeIndexSlider.setValue(0); fileTypeIndexSlider.setPaintLabels(true); fileTypeIndexSlider.setSnapToTicks(true); - add(fileTypeIndexSlider, "cell 1 5"); + add(fileTypeIndexSlider, "cell 1 6"); //---- openButton ---- openButton.setText("Open..."); openButton.addActionListener(e -> open()); - add(openButton, "cell 0 6 3 1"); + add(openButton, "cell 0 7 3 1"); //---- saveButton ---- saveButton.setText("Save..."); saveButton.addActionListener(e -> save()); - add(saveButton, "cell 0 6 3 1"); + add(saveButton, "cell 0 7 3 1"); //---- openDirectButton ---- openDirectButton.setText("Open (no-thread)..."); openDirectButton.addActionListener(e -> openDirect()); - add(openDirectButton, "cell 0 6 3 1"); + add(openDirectButton, "cell 0 7 3 1"); //---- saveDirectButton ---- saveDirectButton.setText("Save (no-thread)..."); saveDirectButton.addActionListener(e -> saveDirect()); - add(saveDirectButton, "cell 0 6 3 1"); + add(saveDirectButton, "cell 0 7 3 1"); //======== filesScrollPane ======== { @@ -358,11 +404,22 @@ public class FlatSystemFileChooserLinuxTest filesField.setRows(8); filesScrollPane.setViewportView(filesField); } - add(filesScrollPane, "cell 0 7 3 1,growx"); + add(filesScrollPane, "cell 0 8 3 1,growx"); + + //---- ownerButtonGroup ---- + ButtonGroup ownerButtonGroup = new ButtonGroup(); + ownerButtonGroup.add(ownerFrameRadioButton); + ownerButtonGroup.add(ownerDialogRadioButton); + ownerButtonGroup.add(ownerNullRadioButton); // JFormDesigner - End of component initialization //GEN-END:initComponents } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JLabel ownerLabel; + private JRadioButton ownerFrameRadioButton; + private JRadioButton ownerDialogRadioButton; + private JRadioButton ownerNullRadioButton; + private JPanel ownerSpacer; private JLabel titleLabel; private JTextField titleField; private JPanel panel1; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd index 2c463484..d172fc00 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd @@ -6,19 +6,52 @@ new FormModel { 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]" + "$rowConstraints": "[][][][][][][][][grow,fill]" } ) { name: "this" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "ownerLabel" + "text": "owner" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerFrameRadioButton" + "text": "JFrame" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + "selected": true + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerDialogRadioButton" + "text": "JDialog" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerNullRadioButton" + "text": "null" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) { + name: "ownerSpacer" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0,growx" + } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "titleLabel" "text": "title" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 0" + "value": "cell 0 1" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "titleField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 0" + "value": "cell 1 1" } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets 2,hidemode 3" @@ -67,46 +100,46 @@ new FormModel { "value": "cell 0 5" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 0 1 6,aligny top,growy 0" + "value": "cell 2 1 1 6,aligny top,growy 0" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "okButtonLabelLabel" "text": "okButtonLabel" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 1" + "value": "cell 0 2" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "okButtonLabelField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 1" + "value": "cell 1 2" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "currentNameLabel" "text": "currentName" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2" + "value": "cell 0 3" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "currentNameField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 2" + "value": "cell 1 3" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "currentFolderLabel" "text": "currentFolder" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 3" + "value": "cell 0 4" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "currentFolderField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 3" + "value": "cell 1 4" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "fileTypesLabel" "text": "fileTypes" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" + "value": "cell 0 5" } ) add( new FormComponent( "javax.swing.JComboBox" ) { name: "fileTypesField" @@ -119,13 +152,13 @@ new FormModel { addElement( "Text and PDF Files,*.txt,*.pdf,null" ) } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 4" + "value": "cell 1 5" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "fileTypeIndexLabel" "text": "fileTypeIndex" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5" + "value": "cell 0 6" } ) add( new FormComponent( "javax.swing.JSlider" ) { name: "fileTypeIndexSlider" @@ -135,35 +168,35 @@ new FormModel { "paintLabels": true "snapToTicks": true }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 5" + "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 6 3 1" + "value": "cell 0 7 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 6 3 1" + "value": "cell 0 7 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 6 3 1" + "value": "cell 0 7 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 6 3 1" + "value": "cell 0 7 3 1" } ) add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "filesScrollPane" @@ -172,11 +205,16 @@ new FormModel { "rows": 8 } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 7 3 1,growx" + "value": "cell 0 8 3 1,growx" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) "size": new java.awt.Dimension( 690, 630 ) } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "ownerButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 640 ) + } ) } } From 9453d55abd83230f4947bcd16df950da89f996fd Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 4 Jan 2025 12:33:18 +0100 Subject: [PATCH 07/34] System File Chooser: fixes for Windows --- .../flatlaf/ui/FlatNativeWindowsLibrary.java | 6 +- .../src/main/cpp/WinFileChooser.cpp | 24 +++- .../FlatSystemFileChooserWindowsTest.java | 110 +++++++++++++----- .../FlatSystemFileChooserWindowsTest.jfd | 94 ++++++++++----- 4 files changed, 171 insertions(+), 63 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java index 991a9878..3e7c7b0c 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java @@ -201,10 +201,10 @@ public class FlatNativeWindowsLibrary * the file dialog. It is highly recommended to invoke it from a new thread * to avoid blocking the AWT event dispatching thread. * - * @param owner the owner of the file dialog + * @param owner the owner of the file dialog; or {@code null} * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog * @param title text displayed in dialog title; or {@code null} - * @param okButtonLabel text displayed in default button; or {@code null} + * @param okButtonLabel text displayed in default button; or {@code null}. Use '&' for mnemonics (e.g. "&Choose") * @param fileNameLabel text displayed in front of the filename text field; or {@code null} * @param fileName user-editable filename currently shown in the filename field; or {@code null} * @param folder current directory shown in the dialog; or {@code null} @@ -219,7 +219,7 @@ public class FlatNativeWindowsLibrary * @param optionsClear options to clear; see {@code FOS_*} constants * @param fileTypeIndex the file type that appears as selected (zero-based) * @param fileTypes file types that the dialog can open or save. - * Pairs of strings are required. + * Pairs of strings are required for each filter. * First string is the display name of the filter shown in the combobox (e.g. "Text Files"). * Second string is the filter pattern (e.g. "*.txt", "*.exe;*.dll" or "*.*"). * @return file path(s) that the user selected; an empty array if canceled; diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp index b51b0d7f..03bc2b5b 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp @@ -23,6 +23,7 @@ /** * @author Karl Tauber + * @since 3.6 */ // see FlatWndProc.cpp @@ -79,7 +80,7 @@ public: //---- class FilterSpec ------------------------------------------------------- class FilterSpec { - JNIEnv* env; + JNIEnv* env = NULL; jstring* jnames = NULL; jstring* jspecs = NULL; @@ -89,6 +90,9 @@ public: public: FilterSpec( JNIEnv* _env, jobjectArray fileTypes ) { + if( fileTypes == NULL ) + return; + env = _env; count = env->GetArrayLength( fileTypes ) / 2; if( count <= 0 ) @@ -165,7 +169,13 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr if( !coInitializer.initialized ) return NULL; - HWND hwndOwner = getWindowHandle( env, owner ); + // handle limitations (without this, some Win32 method fails and this method returns NULL) + if( (optionsSet & FOS_PICKFOLDERS) != 0 ) { + if( open ) + fileTypes = NULL; // no filter allowed for picking folders + else + optionsSet &= ~FOS_PICKFOLDERS; // not allowed for save dialog + } // convert Java strings to C strings AutoReleaseString ctitle( env, title ); @@ -185,6 +195,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr NULL, CLSCTX_INPROC_SERVER, open ? IID_IFileOpenDialog : IID_IFileSaveDialog, reinterpret_cast( &dialog ) ) ); + // set title, etc. if( ctitle != NULL ) CHECK_HRESULT( dialog->SetTitle( ctitle ) ); if( cokButtonLabel != NULL ) @@ -202,23 +213,26 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr if( cdefaultExtension != NULL ) CHECK_HRESULT( dialog->SetDefaultExtension( cdefaultExtension ) ); + // set options FILEOPENDIALOGOPTIONS existingOptions; CHECK_HRESULT( dialog->GetOptions( &existingOptions ) ); CHECK_HRESULT( dialog->SetOptions ( (existingOptions & ~optionsClear) | optionsSet ) ); - if( specs.count > 0 ) { + // initialize filter + if( specs.count > 0 && (optionsSet & FOS_PICKFOLDERS) == 0 ) { CHECK_HRESULT( dialog->SetFileTypes( specs.count, specs.specs ) ); if( fileTypeIndex > 0 ) CHECK_HRESULT( dialog->SetFileTypeIndex( min( fileTypeIndex + 1, specs.count ) ) ); } // show dialog + HWND hwndOwner = (owner != NULL) ? getWindowHandle( env, owner ) : NULL; HRESULT hr = dialog->Show( hwndOwner ); if( hr == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) return newJavaStringArray( env, 0 ); CHECK_HRESULT( hr ); - // convert URLs to Java string array + // convert shell items to Java string array if( open ) { AutoReleasePtr shellItems; DWORD count; @@ -235,7 +249,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr jstring jpath = newJavaString( env, path ); CoTaskMemFree( path ); - env->SetObjectArrayElement( array, 0, jpath ); + env->SetObjectArrayElement( array, i, jpath ); env->DeleteLocalRef( jpath ); } return array; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java index ab1f0865..31a55aea 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java @@ -17,10 +17,12 @@ package com.formdev.flatlaf.testing; import static com.formdev.flatlaf.ui.FlatNativeWindowsLibrary.*; +import java.awt.Dialog; import java.awt.EventQueue; import java.awt.SecondaryLoop; import java.awt.Toolkit; import java.awt.Window; +import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; import java.awt.event.WindowListener; @@ -75,7 +77,26 @@ public class FlatSystemFileChooserWindowsTest } private void openOrSave( boolean open, boolean direct ) { - Window owner = SwingUtilities.windowForComponent( this ); + Window frame = SwingUtilities.windowForComponent( this ); + if( ownerFrameRadioButton.isSelected() ) + openOrSave( open, direct, frame ); + else if( ownerDialogRadioButton.isSelected() ) { + JDialog dialog = new JDialog( frame, "Dummy Modal Dialog", Dialog.DEFAULT_MODALITY_TYPE ); + dialog.setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE ); + dialog.addWindowListener( new WindowAdapter() { + @Override + public void windowOpened( WindowEvent e ) { + openOrSave( open, direct, dialog ); + } + } ); + dialog.setSize( 1200, 1000 ); + dialog.setLocationRelativeTo( this ); + dialog.setVisible( true ); + } else + openOrSave( open, direct, null ); + } + + private void openOrSave( boolean open, boolean direct, Window owner ) { String title = n( titleField.getText() ); String okButtonLabel = n( okButtonLabelField.getText() ); String fileNameLabel = n( fileNameLabelField.getText() ); @@ -215,6 +236,11 @@ public class FlatSystemFileChooserWindowsTest private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + ownerLabel = new JLabel(); + ownerFrameRadioButton = new JRadioButton(); + ownerDialogRadioButton = new JRadioButton(); + ownerNullRadioButton = new JRadioButton(); + ownerSpacer = new JPanel(null); titleLabel = new JLabel(); titleField = new JTextField(); panel1 = new JPanel(); @@ -285,12 +311,31 @@ public class FlatSystemFileChooserWindowsTest "[]" + "[]" + "[]" + + "[]" + "[grow,fill]")); + //---- ownerLabel ---- + ownerLabel.setText("owner"); + add(ownerLabel, "cell 0 0"); + + //---- ownerFrameRadioButton ---- + ownerFrameRadioButton.setText("JFrame"); + ownerFrameRadioButton.setSelected(true); + add(ownerFrameRadioButton, "cell 1 0"); + + //---- ownerDialogRadioButton ---- + ownerDialogRadioButton.setText("JDialog"); + add(ownerDialogRadioButton, "cell 1 0"); + + //---- ownerNullRadioButton ---- + ownerNullRadioButton.setText("null"); + add(ownerNullRadioButton, "cell 1 0"); + add(ownerSpacer, "cell 1 0,growx"); + //---- titleLabel ---- titleLabel.setText("title"); - add(titleLabel, "cell 0 0"); - add(titleField, "cell 1 0"); + add(titleLabel, "cell 0 1"); + add(titleField, "cell 1 1"); //======== panel1 ======== { @@ -402,46 +447,46 @@ public class FlatSystemFileChooserWindowsTest hidePinnedPlacesCheckBox.setText("hidePinnedPlaces"); panel1.add(hidePinnedPlacesCheckBox, "cell 1 7"); } - add(panel1, "cell 2 0 1 10,aligny top,growy 0"); + add(panel1, "cell 2 1 1 10,aligny top,growy 0"); //---- okButtonLabelLabel ---- okButtonLabelLabel.setText("okButtonLabel"); - add(okButtonLabelLabel, "cell 0 1"); - add(okButtonLabelField, "cell 1 1"); + add(okButtonLabelLabel, "cell 0 2"); + add(okButtonLabelField, "cell 1 2"); //---- fileNameLabelLabel ---- fileNameLabelLabel.setText("fileNameLabel"); - add(fileNameLabelLabel, "cell 0 2"); - add(fileNameLabelField, "cell 1 2"); + add(fileNameLabelLabel, "cell 0 3"); + add(fileNameLabelField, "cell 1 3"); //---- fileNameLabel ---- fileNameLabel.setText("fileName"); - add(fileNameLabel, "cell 0 3"); - add(fileNameField, "cell 1 3"); + add(fileNameLabel, "cell 0 4"); + add(fileNameField, "cell 1 4"); //---- folderLabel ---- folderLabel.setText("folder"); - add(folderLabel, "cell 0 4"); - add(folderField, "cell 1 4"); + add(folderLabel, "cell 0 5"); + add(folderField, "cell 1 5"); //---- saveAsItemLabel ---- saveAsItemLabel.setText("saveAsItem"); - add(saveAsItemLabel, "cell 0 5"); - add(saveAsItemField, "cell 1 5"); + add(saveAsItemLabel, "cell 0 6"); + add(saveAsItemField, "cell 1 6"); //---- defaultFolderLabel ---- defaultFolderLabel.setText("defaultFolder"); - add(defaultFolderLabel, "cell 0 6"); - add(defaultFolderField, "cell 1 6"); + add(defaultFolderLabel, "cell 0 7"); + add(defaultFolderField, "cell 1 7"); //---- defaultExtensionLabel ---- defaultExtensionLabel.setText("defaultExtension"); - add(defaultExtensionLabel, "cell 0 7"); - add(defaultExtensionField, "cell 1 7"); + add(defaultExtensionLabel, "cell 0 8"); + add(defaultExtensionField, "cell 1 8"); //---- fileTypesLabel ---- fileTypesLabel.setText("fileTypes"); - add(fileTypesLabel, "cell 0 8"); + add(fileTypesLabel, "cell 0 9"); //---- fileTypesField ---- fileTypesField.setEditable(true); @@ -451,11 +496,11 @@ public class FlatSystemFileChooserWindowsTest "Text Files,*.txt,PDF Files,*.pdf,All Files,*.*", "Text and PDF Files,*.txt;*.pdf" })); - add(fileTypesField, "cell 1 8"); + add(fileTypesField, "cell 1 9"); //---- fileTypeIndexLabel ---- fileTypeIndexLabel.setText("fileTypeIndex"); - add(fileTypeIndexLabel, "cell 0 9"); + add(fileTypeIndexLabel, "cell 0 10"); //---- fileTypeIndexSlider ---- fileTypeIndexSlider.setMaximum(10); @@ -463,27 +508,27 @@ public class FlatSystemFileChooserWindowsTest fileTypeIndexSlider.setValue(0); fileTypeIndexSlider.setPaintLabels(true); fileTypeIndexSlider.setSnapToTicks(true); - add(fileTypeIndexSlider, "cell 1 9"); + add(fileTypeIndexSlider, "cell 1 10"); //---- openButton ---- openButton.setText("Open..."); openButton.addActionListener(e -> open()); - add(openButton, "cell 0 10 3 1"); + add(openButton, "cell 0 11 3 1"); //---- saveButton ---- saveButton.setText("Save..."); saveButton.addActionListener(e -> save()); - add(saveButton, "cell 0 10 3 1"); + add(saveButton, "cell 0 11 3 1"); //---- openDirectButton ---- openDirectButton.setText("Open (no-thread)..."); openDirectButton.addActionListener(e -> openDirect()); - add(openDirectButton, "cell 0 10 3 1"); + add(openDirectButton, "cell 0 11 3 1"); //---- saveDirectButton ---- saveDirectButton.setText("Save (no-thread)..."); saveDirectButton.addActionListener(e -> saveDirect()); - add(saveDirectButton, "cell 0 10 3 1"); + add(saveDirectButton, "cell 0 11 3 1"); //======== filesScrollPane ======== { @@ -492,11 +537,22 @@ public class FlatSystemFileChooserWindowsTest filesField.setRows(8); filesScrollPane.setViewportView(filesField); } - add(filesScrollPane, "cell 0 11 3 1,growx"); + add(filesScrollPane, "cell 0 12 3 1,growx"); + + //---- ownerButtonGroup ---- + ButtonGroup ownerButtonGroup = new ButtonGroup(); + ownerButtonGroup.add(ownerFrameRadioButton); + ownerButtonGroup.add(ownerDialogRadioButton); + ownerButtonGroup.add(ownerNullRadioButton); // JFormDesigner - End of component initialization //GEN-END:initComponents } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JLabel ownerLabel; + private JRadioButton ownerFrameRadioButton; + private JRadioButton ownerDialogRadioButton; + private JRadioButton ownerNullRadioButton; + private JPanel ownerSpacer; private JLabel titleLabel; private JTextField titleField; private JPanel panel1; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd index f26815de..66590a21 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd @@ -6,19 +6,52 @@ new FormModel { 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]" + "$rowConstraints": "[][][][][][][][][][][][][grow,fill]" } ) { name: "this" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "ownerLabel" + "text": "owner" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerFrameRadioButton" + "text": "JFrame" + "selected": true + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerDialogRadioButton" + "text": "JDialog" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerNullRadioButton" + "text": "null" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) { + name: "ownerSpacer" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0,growx" + } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "titleLabel" "text": "title" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 0" + "value": "cell 0 1" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "titleField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 0" + "value": "cell 1 1" } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets 2,hidemode 3" @@ -165,90 +198,90 @@ new FormModel { "value": "cell 1 7" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 0 1 10,aligny top,growy 0" + "value": "cell 2 1 1 10,aligny top,growy 0" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "okButtonLabelLabel" "text": "okButtonLabel" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 1" + "value": "cell 0 2" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "okButtonLabelField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 1" + "value": "cell 1 2" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "fileNameLabelLabel" "text": "fileNameLabel" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2" + "value": "cell 0 3" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "fileNameLabelField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 2" + "value": "cell 1 3" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "fileNameLabel" "text": "fileName" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 3" + "value": "cell 0 4" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "fileNameField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 3" + "value": "cell 1 4" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "folderLabel" "text": "folder" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" + "value": "cell 0 5" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "folderField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 4" + "value": "cell 1 5" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "saveAsItemLabel" "text": "saveAsItem" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5" + "value": "cell 0 6" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "saveAsItemField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 5" + "value": "cell 1 6" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "defaultFolderLabel" "text": "defaultFolder" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6" + "value": "cell 0 7" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "defaultFolderField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 6" + "value": "cell 1 7" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "defaultExtensionLabel" "text": "defaultExtension" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 7" + "value": "cell 0 8" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "defaultExtensionField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 7" + "value": "cell 1 8" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "fileTypesLabel" "text": "fileTypes" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 8" + "value": "cell 0 9" } ) add( new FormComponent( "javax.swing.JComboBox" ) { name: "fileTypesField" @@ -261,13 +294,13 @@ new FormModel { addElement( "Text and PDF Files,*.txt;*.pdf" ) } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 8" + "value": "cell 1 9" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "fileTypeIndexLabel" "text": "fileTypeIndex" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 9" + "value": "cell 0 10" } ) add( new FormComponent( "javax.swing.JSlider" ) { name: "fileTypeIndexSlider" @@ -277,35 +310,35 @@ new FormModel { "paintLabels": true "snapToTicks": true }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 9" + "value": "cell 1 10" } ) 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 10 3 1" + "value": "cell 0 11 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 10 3 1" + "value": "cell 0 11 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 10 3 1" + "value": "cell 0 11 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 10 3 1" + "value": "cell 0 11 3 1" } ) add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "filesScrollPane" @@ -314,11 +347,16 @@ new FormModel { "rows": 8 } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 11 3 1,growx" + "value": "cell 0 12 3 1,growx" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 690, 630 ) + "size": new java.awt.Dimension( 845, 630 ) + } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "ownerButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 640 ) } ) } } From 91e8d04a9f4fe7215213aadd1241d1903504e1d4 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 6 Jan 2025 18:01:50 +0100 Subject: [PATCH 08/34] System File Chooser: introduced class `SystemFileChooser` as replacement for `JFileChooser` --- .../formdev/flatlaf/FlatSystemProperties.java | 13 + .../flatlaf/ui/FlatNativeLinuxLibrary.java | 5 + .../flatlaf/ui/FlatNativeWindowsLibrary.java | 3 +- .../flatlaf/util/SystemFileChooser.java | 559 +++++++++++++ .../com/formdev/flatlaf/demo/DemoFrame.java | 24 + .../com/formdev/flatlaf/demo/DemoFrame.jfd | 15 +- .../flatlaf-natives-linux/build.gradle.kts | 11 + .../src/main/cpp/GtkFileChooser.cpp | 8 +- .../src/main/cpp/WinFileChooser.cpp | 13 +- flatlaf-testing/build.gradle.kts | 1 + .../testing/FlatSystemFileChooserTest.java | 749 ++++++++++++++++++ .../testing/FlatSystemFileChooserTest.jfd | 321 ++++++++ .../FlatSystemFileChooserWindowsTest.java | 4 + .../FlatSystemFileChooserWindowsTest.jfd | 3 + .../flatlaf/testing/FlatTestFrame.java | 3 + gradle/libs.versions.toml | 1 + 16 files changed, 1721 insertions(+), 12 deletions(-) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemFileChooser.java create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java index db7ccb13..1d333eaa 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java @@ -16,7 +16,9 @@ package com.formdev.flatlaf; +import javax.swing.JFileChooser; import javax.swing.SwingUtilities; +import com.formdev.flatlaf.util.SystemFileChooser; import com.formdev.flatlaf.util.UIScale; /** @@ -226,6 +228,17 @@ public interface FlatSystemProperties */ String USE_SUB_MENU_SAFE_TRIANGLE = "flatlaf.useSubMenuSafeTriangle"; + /** + * Specifies whether {@link SystemFileChooser} uses operating system file dialogs. + * If set to {@code false}, the {@link JFileChooser} is used instead. + *

+ * Allowed Values {@code false} and {@code true}
+ * Default {@code true} + * + * @since 3.6 + */ + String USE_SYSTEM_FILE_CHOOSER = "flatlaf.useSystemFileChooser"; + /** * Checks whether a system property is set and returns {@code true} if its value * is {@code "true"} (case-insensitive), otherwise it returns {@code false}. diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java index 9ebdda20..f1bed502 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java @@ -134,6 +134,10 @@ public class FlatNativeLinuxLibrary * Shows the Linux system file dialog * GtkFileChooserDialog. *

+ * Uses {@code GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER} if {@link #FC_select_folder} is set in parameter {@code optionsSet}. + * Otherwise uses {@code GTK_FILE_CHOOSER_ACTION_OPEN} if parameter {@code open} is {@code true}, + * or {@code GTK_FILE_CHOOSER_ACTION_SAVE} if {@code false}. + *

* 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. @@ -142,6 +146,7 @@ public class FlatNativeLinuxLibrary * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog * @param title text displayed in dialog title; or {@code null} * @param okButtonLabel text displayed in default button; or {@code null}. Use '_' for mnemonics (e.g. "_Choose") + * Use '__' for '_' character (e.g. "Choose__and__Quit"). * @param currentName user-editable filename currently shown in the filename field in save dialog; or {@code null} * @param currentFolder current directory shown in the dialog; or {@code null} * @param optionsSet options to set; see {@code FOS_*} constants diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java index 3e7c7b0c..f0a769aa 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java @@ -204,7 +204,8 @@ public class FlatNativeWindowsLibrary * @param owner the owner of the file dialog; or {@code null} * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog * @param title text displayed in dialog title; or {@code null} - * @param okButtonLabel text displayed in default button; or {@code null}. Use '&' for mnemonics (e.g. "&Choose") + * @param okButtonLabel text displayed in default button; or {@code null}. Use '&' for mnemonics (e.g. "&Choose"). + * Use '&&' for '&' character (e.g. "Choose && Quit"). * @param fileNameLabel text displayed in front of the filename text field; or {@code null} * @param fileName user-editable filename currently shown in the filename field; or {@code null} * @param folder current directory shown in the dialog; or {@code null} 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 new file mode 100644 index 00000000..d5d6d773 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemFileChooser.java @@ -0,0 +1,559 @@ +/* + * Copyright 2025 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.util; + +import java.awt.Component; +import java.awt.SecondaryLoop; +import java.awt.Toolkit; +import java.awt.Window; +import java.io.File; +import java.util.ArrayList; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicReference; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.filechooser.FileSystemView; +import com.formdev.flatlaf.FlatSystemProperties; +import com.formdev.flatlaf.ui.FlatNativeLinuxLibrary; +import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; + +/** + * Gives access to operating system file dialogs. + *

+ * The API is (mostly) compatible with {@link JFileChooser}. + * To use this class in existing code, do a string replace from {@code JFileChooser} to {@code SystemFileChooser}. + * If there are no compile errors, then there is a good chance that it works without further changes. + * If there are compile errors, then you're using a feature that {@code SystemFileChooser} does not support. + *

+ * Supported platforms are Windows 10+, macOS 10.14+ and Linux GTK 3. + * {@code JFileChooser} is used on unsupported platforms. + *

+ * {@code SystemFileChooser} requires FlatLaf native libraries (usually contained in flatlaf.jar). + * If not available or disabled (via {@link FlatSystemProperties#USE_NATIVE_LIBRARY} + * or {@link FlatSystemProperties#USE_SYSTEM_FILE_CHOOSER}), then {@code JFileChooser} is used. + *

+ *

+ *

Limitations/incompatibilities compared to JFileChooser

+ * + * + * + * @author Karl Tauber + * @since 3.6 + */ +public class SystemFileChooser +{ + /** @see JFileChooser#OPEN_DIALOG */ + public static final int OPEN_DIALOG = JFileChooser.OPEN_DIALOG; + + /** @see JFileChooser#SAVE_DIALOG */ + public static final int SAVE_DIALOG = JFileChooser.SAVE_DIALOG; + + /** @see JFileChooser#CANCEL_OPTION */ + public static final int CANCEL_OPTION = JFileChooser.CANCEL_OPTION; + + /** @see JFileChooser#APPROVE_OPTION */ + public static final int APPROVE_OPTION = JFileChooser.APPROVE_OPTION; + + /** @see JFileChooser#FILES_ONLY */ + public static final int FILES_ONLY = JFileChooser.FILES_ONLY; + + /** @see JFileChooser#DIRECTORIES_ONLY */ + public static final int DIRECTORIES_ONLY = JFileChooser.DIRECTORIES_ONLY; + + private int dialogType = OPEN_DIALOG; + private String dialogTitle; + private String approveButtonText; + private int approveButtonMnemonic = 0; + private int fileSelectionMode = FILES_ONLY; + private boolean multiSelection; + private boolean useFileHiding = true; + + private File currentDirectory; + private File selectedFile; + private File[] selectedFiles; + + /** @see JFileChooser#JFileChooser() */ + public SystemFileChooser() { + this( (File) null ); + } + + /** @see JFileChooser#JFileChooser(String) */ + public SystemFileChooser( String currentDirectoryPath ) { + setCurrentDirectory( (currentDirectoryPath != null) + ? FileSystemView.getFileSystemView().createFileObject( currentDirectoryPath ) + : null ); + } + + /** @see JFileChooser#JFileChooser(File) */ + public SystemFileChooser( File currentDirectory ) { + setCurrentDirectory( currentDirectory ); + } + + /** @see JFileChooser#showOpenDialog(Component) */ + public int showOpenDialog( Component parent ) { + setDialogType( OPEN_DIALOG ); + return showDialogImpl( parent ); + } + + /** @see JFileChooser#showSaveDialog(Component) */ + public int showSaveDialog( Component parent ) { + setDialogType( SAVE_DIALOG ); + return showDialogImpl( parent ); + } + + /** @see JFileChooser#showDialog(Component, String) */ + public int showDialog( Component parent, String approveButtonText ) { + if( approveButtonText != null ) + setApproveButtonText( approveButtonText ); + return showDialogImpl( parent ); + } + + /** @see JFileChooser#getDialogType() */ + public int getDialogType() { + return dialogType; + } + + /** @see JFileChooser#setDialogType(int) */ + public void setDialogType( int dialogType ) { + if( dialogType != OPEN_DIALOG && dialogType != SAVE_DIALOG ) + throw new IllegalArgumentException( "Invalid dialog type " + dialogType ); + + this.dialogType = dialogType; + } + + /** @see JFileChooser#getDialogTitle() */ + public String getDialogTitle() { + return dialogTitle; + } + + /** @see JFileChooser#setDialogTitle(String) */ + public void setDialogTitle( String dialogTitle ) { + this.dialogTitle = dialogTitle; + } + + /** @see JFileChooser#getApproveButtonText() */ + public String getApproveButtonText() { + return approveButtonText; + } + + /** @see JFileChooser#setApproveButtonText(String) */ + public void setApproveButtonText( String approveButtonText ) { + this.approveButtonText = approveButtonText; + } + + /** @see JFileChooser#getApproveButtonMnemonic() */ + public int getApproveButtonMnemonic() { + return approveButtonMnemonic; + } + + /** @see JFileChooser#setApproveButtonMnemonic(int) */ + public void setApproveButtonMnemonic( int mnemonic ) { + approveButtonMnemonic = mnemonic; + } + + /** @see JFileChooser#setApproveButtonMnemonic(char) */ + public void setApproveButtonMnemonic( char mnemonic ) { + int vk = mnemonic; + if( vk >= 'a' && vk <= 'z' ) + vk -= 'a' - 'A'; + setApproveButtonMnemonic( vk ); + } + + /** @see JFileChooser#getFileSelectionMode() */ + public int getFileSelectionMode() { + return fileSelectionMode; + } + + /** @see JFileChooser#setFileSelectionMode(int) */ + public void setFileSelectionMode( int fileSelectionMode ) { + if( fileSelectionMode != FILES_ONLY && fileSelectionMode != DIRECTORIES_ONLY ) + throw new IllegalArgumentException( "Invalid file selection mode " + fileSelectionMode ); + + this.fileSelectionMode = fileSelectionMode; + } + + /** @see JFileChooser#isFileSelectionEnabled() */ + public boolean isFileSelectionEnabled() { + return fileSelectionMode == FILES_ONLY; + } + + /** @see JFileChooser#isDirectorySelectionEnabled() */ + public boolean isDirectorySelectionEnabled() { + return fileSelectionMode == DIRECTORIES_ONLY; + } + + /** @see JFileChooser#isMultiSelectionEnabled() */ + public boolean isMultiSelectionEnabled() { + return multiSelection; + } + + /** @see JFileChooser#setMultiSelectionEnabled(boolean) */ + public void setMultiSelectionEnabled( boolean multiSelection ) { + this.multiSelection = multiSelection; + } + + /** @see JFileChooser#isFileHidingEnabled() */ + public boolean isFileHidingEnabled() { + return useFileHiding; + } + + /** @see JFileChooser#setFileHidingEnabled(boolean) */ + public void setFileHidingEnabled( boolean useFileHiding ) { + this.useFileHiding = useFileHiding; + } + + /** @see JFileChooser#getCurrentDirectory() */ + public File getCurrentDirectory() { + return currentDirectory; + } + + /** @see JFileChooser#setCurrentDirectory(File) */ + public void setCurrentDirectory( File dir ) { + // for compatibility with JFileChooser + if( dir != null && !dir.exists() ) + return; + if( dir == null ) + dir = FileSystemView.getFileSystemView().getDefaultDirectory(); + + currentDirectory = dir; + } + + /** @see JFileChooser#getSelectedFile() */ + public File getSelectedFile() { + return selectedFile; + } + + /** @see JFileChooser#setSelectedFile(File) */ + public void setSelectedFile( File file ) { + selectedFile = file; + + // for compatibility with JFileChooser + if( file != null && + file.isAbsolute() && + !FileSystemView.getFileSystemView().isParent( getCurrentDirectory(), file ) ) + setCurrentDirectory( file.getParentFile() ); + } + + /** @see JFileChooser#getSelectedFiles() */ + public File[] getSelectedFiles() { + return (selectedFiles != null) ? selectedFiles.clone() : new File[0]; + } + + /** @see JFileChooser#setSelectedFiles(File[]) */ + public void setSelectedFiles( File[] selectedFiles ) { + if( selectedFiles != null && selectedFiles.length > 0 ) { + this.selectedFiles = selectedFiles.clone(); + setSelectedFile( selectedFiles[0] ); + } else { + this.selectedFiles = null; + setSelectedFile( null ); + } + } + + private int showDialogImpl( Component parent ) { + File[] files = getProvider().showDialog( parent, this ); + setSelectedFiles( files ); + return (files != null) ? APPROVE_OPTION : CANCEL_OPTION; + } + + private FileChooserProvider getProvider() { + if( !FlatSystemProperties.getBoolean( FlatSystemProperties.USE_SYSTEM_FILE_CHOOSER, true ) ) + return new SwingFileChooserProvider(); + + if( SystemInfo.isWindows_10_orLater && FlatNativeWindowsLibrary.isLoaded() ) + return new WindowsFileChooserProvider(); + else if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isLoaded() ) + return new LinuxFileChooserProvider(); + else // unknown platform or FlatLaf native library not loaded + return new SwingFileChooserProvider(); + } + + //---- interface FileChooserProvider -------------------------------------- + + private interface FileChooserProvider { + File[] showDialog( Component parent, SystemFileChooser fc ); + } + + //---- class SystemFileChooserProvider ------------------------------------ + + private static abstract class SystemFileChooserProvider + implements FileChooserProvider + { + @Override + public File[] showDialog( Component parent, SystemFileChooser fc ) { + Window owner = (parent instanceof Window) + ? (Window) parent + : (parent != null) ? SwingUtilities.windowForComponent( parent ) : null; + AtomicReference filenamesRef = new AtomicReference<>(); + + // create secondary event look and invoke system file dialog on a new thread + SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); + new Thread( () -> { + filenamesRef.set( showSystemDialog( owner, fc ) ); + secondaryLoop.exit(); + } ).start(); + secondaryLoop.enter(); + + String[] filenames = filenamesRef.get(); + + // fallback to Swing file chooser if system file dialog failed or is not available + if( filenames == null ) + return new SwingFileChooserProvider().showDialog( parent, fc ); + + // canceled? + if( filenames.length == 0 ) + return null; + + // convert file names to file objects + FileSystemView fsv = FileSystemView.getFileSystemView(); + File[] files = new File[filenames.length]; + for( int i = 0; i < filenames.length; i++ ) + files[i] = fsv.createFileObject( filenames[i] ); + return files; + } + + abstract String[] showSystemDialog( Window owner, SystemFileChooser fc ); + } + + //---- class WindowsFileChooserProvider ----------------------------------- + + private static class WindowsFileChooserProvider + extends SystemFileChooserProvider + { + @Override + String[] showSystemDialog( Window owner, SystemFileChooser fc ) { + boolean open = (fc.getDialogType() == OPEN_DIALOG); + String approveButtonText = fc.getApproveButtonText(); + int approveButtonMnemonic = fc.getApproveButtonMnemonic(); + String fileName = null; + String folder = null; + String saveAsItem = null; + + // approve button text and mnemonic + if( approveButtonText != null ) { + approveButtonText = approveButtonText.replace( "&", "&&" ); + if( approveButtonMnemonic > 0 ) { + int mnemonicIndex = approveButtonText.toUpperCase( Locale.ENGLISH ).indexOf( approveButtonMnemonic ); + if( mnemonicIndex >= 0 ) { + approveButtonText = approveButtonText.substring( 0, mnemonicIndex ) + + '&' + approveButtonText.substring( mnemonicIndex ); + } + } + } + + // paths + File currentDirectory = fc.getCurrentDirectory(); + File selectedFile = fc.getSelectedFile(); + if( selectedFile != null ) { + if( selectedFile.exists() && !open ) + saveAsItem = selectedFile.getAbsolutePath(); + else { + fileName = selectedFile.getName(); + folder = selectedFile.getParent(); + } + } else if( currentDirectory != null ) + folder = currentDirectory.getAbsolutePath(); + + // options + int optionsSet = FlatNativeWindowsLibrary.FOS_OVERWRITEPROMPT; + int optionsClear = 0; + if( fc.isDirectorySelectionEnabled() ) + optionsSet |= FlatNativeWindowsLibrary.FOS_PICKFOLDERS; + if( fc.isMultiSelectionEnabled() ) + optionsSet |= FlatNativeWindowsLibrary.FOS_ALLOWMULTISELECT; + if( !fc.isFileHidingEnabled() ) + optionsSet |= FlatNativeWindowsLibrary.FOS_FORCESHOWHIDDEN; + + // filter + int fileTypeIndex = 0; + ArrayList fileTypes = new ArrayList<>(); + // FOS_PICKFOLDERS does not support file types + if( !fc.isDirectorySelectionEnabled() ) { + // if there are no file types + // - for Save dialog add "All Files", otherwise Windows would show an empty "Save as type" combobox + // - for Open dialog, Windows hides the combobox + if( !open && fileTypes.isEmpty() ) { + fileTypes.add( UIManager.getString( "FileChooser.acceptAllFileFilterText" ) ); + fileTypes.add( "*.*" ); + } + } + + // show system file dialog + return FlatNativeWindowsLibrary.showFileChooser( owner, open, + fc.getDialogTitle(), approveButtonText, null, fileName, + folder, saveAsItem, null, null, optionsSet, optionsClear, + fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); + } + } + + //---- class LinuxFileChooserProvider -----------------------------------.. + + private static class LinuxFileChooserProvider + extends SystemFileChooserProvider + { + @Override + String[] showSystemDialog( Window owner, SystemFileChooser fc ) { + boolean open = (fc.getDialogType() == OPEN_DIALOG); + String approveButtonText = fc.getApproveButtonText(); + int approveButtonMnemonic = fc.getApproveButtonMnemonic(); + String currentName = null; + String currentFolder = null; + + // approve button text and mnemonic + if( approveButtonText != null ) { + approveButtonText = approveButtonText.replace( "_", "__" ); + if( approveButtonMnemonic > 0 ) { + int mnemonicIndex = approveButtonText.toUpperCase( Locale.ENGLISH ).indexOf( approveButtonMnemonic ); + if( mnemonicIndex >= 0 ) { + approveButtonText = approveButtonText.substring( 0, mnemonicIndex ) + + '_' + approveButtonText.substring( mnemonicIndex ); + } + } + } + + // paths + File currentDirectory = fc.getCurrentDirectory(); + File selectedFile = fc.getSelectedFile(); + if( selectedFile != null ) { + if( selectedFile.isDirectory() ) + currentFolder = selectedFile.getAbsolutePath(); + else { + currentName = selectedFile.getName(); + currentFolder = selectedFile.getParent(); + } + } else if( currentDirectory != null ) + currentFolder = currentDirectory.getAbsolutePath(); + + // options + int optionsSet = FlatNativeLinuxLibrary.FC_do_overwrite_confirmation; + int optionsClear = 0; + if( fc.isDirectorySelectionEnabled() ) + optionsSet |= FlatNativeLinuxLibrary.FC_select_folder; + if( fc.isMultiSelectionEnabled() ) + optionsSet |= FlatNativeLinuxLibrary.FC_select_multiple; + if( !fc.isFileHidingEnabled() ) + optionsSet |= FlatNativeLinuxLibrary.FC_show_hidden; + else // necessary because GTK seems to be remember last state and re-use it for new file dialogs + optionsClear |= FlatNativeLinuxLibrary.FC_show_hidden; + + // show system file dialog + return FlatNativeLinuxLibrary.showFileChooser( owner, open, + fc.getDialogTitle(), approveButtonText, currentName, currentFolder, + optionsSet, optionsClear, 0 ); + } + } + + //---- class SwingFileChooserProvider ------------------------------------- + + private static class SwingFileChooserProvider + implements FileChooserProvider + { + @Override + public File[] showDialog( Component parent, SystemFileChooser fc ) { + JFileChooser chooser = new JFileChooser() { + @Override + public void approveSelection() { + File[] files = isMultiSelectionEnabled() + ? getSelectedFiles() + : new File[] { getSelectedFile() }; + + if( getDialogType() == OPEN_DIALOG || isDirectorySelectionEnabled() ) { + if( !checkMustExist( this, files ) ) + return; + } else { + if( !checkOverwrite( this, files ) ) + return; + } + super.approveSelection(); + } + }; + + chooser.setDialogType( fc.getDialogType() ); + chooser.setDialogTitle( fc.getDialogTitle() ); + chooser.setApproveButtonText( fc.getApproveButtonText() ); + chooser.setApproveButtonMnemonic( fc.getApproveButtonMnemonic() ); + chooser.setFileSelectionMode( fc.getFileSelectionMode() ); + chooser.setMultiSelectionEnabled( fc.isMultiSelectionEnabled() ); + chooser.setFileHidingEnabled( fc.isFileHidingEnabled() ); + + // system file dialogs do not support multi-selection for Save File dialogs + if( chooser.isMultiSelectionEnabled() && + chooser.getDialogType() == JFileChooser.SAVE_DIALOG && + !chooser.isDirectorySelectionEnabled() ) + chooser.setMultiSelectionEnabled( false ); + + // paths + chooser.setCurrentDirectory( fc.getCurrentDirectory() ); + chooser.setSelectedFile( fc.getSelectedFile() ); + + if( chooser.showDialog( parent, null ) != JFileChooser.APPROVE_OPTION ) + return null; + + return chooser.isMultiSelectionEnabled() + ? chooser.getSelectedFiles() + : new File[] { chooser.getSelectedFile() }; + } + } + + private static boolean checkMustExist( JFileChooser chooser, File[] files ) { + for( File file : files ) { + if( !file.exists() ) { + String title = chooser.getDialogTitle(); + JOptionPane.showMessageDialog( chooser, + file.getName() + (chooser.isDirectorySelectionEnabled() + ? "\nPath does not exist.\nCheck the path and try again." + : "\nFile not found.\nCheck the file name and try again."), + (title != null) ? title : "Open", + JOptionPane.WARNING_MESSAGE ); + return false; + } + } + return true; + } + + private static boolean checkOverwrite( JFileChooser chooser, File[] files ) { + for( File file : files ) { + if( file.exists() ) { + String title = chooser.getDialogTitle(); + Locale l = chooser.getLocale(); + Object[] options = { + UIManager.getString( "OptionPane.yesButtonText", l ), + UIManager.getString( "OptionPane.noButtonText", l ), }; + int result = JOptionPane.showOptionDialog( chooser, + file.getName() + " already exists.\nDo you want to replace it?", + "Confirm " + (title != null ? title : "Save"), + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, + null, options, options[1] ); + return (result == 0); + } + } + return true; + } +} diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java index e9a9171a..9ec5e815 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java @@ -49,6 +49,7 @@ import com.formdev.flatlaf.extras.FlatSVGUtils; import com.formdev.flatlaf.util.ColorFunctions; import com.formdev.flatlaf.util.FontUtils; import com.formdev.flatlaf.util.LoggingFacade; +import com.formdev.flatlaf.util.SystemFileChooser; import com.formdev.flatlaf.util.SystemInfo; import net.miginfocom.layout.ConstraintParser; import net.miginfocom.layout.LC; @@ -172,6 +173,16 @@ class DemoFrame chooser.showSaveDialog( this ); } + private void openSystemActionPerformed() { + SystemFileChooser chooser = new SystemFileChooser(); + chooser.showOpenDialog( this ); + } + + private void saveAsSystemActionPerformed() { + SystemFileChooser chooser = new SystemFileChooser(); + chooser.showSaveDialog( this ); + } + private void exitActionPerformed() { dispose(); } @@ -496,6 +507,8 @@ class DemoFrame JMenuItem newMenuItem = new JMenuItem(); JMenuItem openMenuItem = new JMenuItem(); JMenuItem saveAsMenuItem = new JMenuItem(); + JMenuItem openSystemMenuItem = new JMenuItem(); + JMenuItem saveAsSystemMenuItem = new JMenuItem(); JMenuItem closeMenuItem = new JMenuItem(); exitMenuItem = new JMenuItem(); JMenu editMenu = new JMenu(); @@ -596,6 +609,17 @@ class DemoFrame fileMenu.add(saveAsMenuItem); fileMenu.addSeparator(); + //---- openSystemMenuItem ---- + openSystemMenuItem.setText("Open (System)..."); + openSystemMenuItem.addActionListener(e -> openSystemActionPerformed()); + fileMenu.add(openSystemMenuItem); + + //---- saveAsSystemMenuItem ---- + saveAsSystemMenuItem.setText("Save As (System)..."); + saveAsSystemMenuItem.addActionListener(e -> saveAsSystemActionPerformed()); + fileMenu.add(saveAsSystemMenuItem); + fileMenu.addSeparator(); + //---- closeMenuItem ---- closeMenuItem.setText("Close"); closeMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd index 08248c1d..61243782 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "8.2.1.0.348" Java: "21.0.1" encoding: "UTF-8" +JFDML JFormDesigner: "8.3" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -182,6 +182,19 @@ new FormModel { "mnemonic": 83 addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "saveAsActionPerformed", false ) ) } ) + add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { + name: "separator9" + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "openSystemMenuItem" + "text": "Open (System)..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openSystemActionPerformed", false ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "saveAsSystemMenuItem" + "text": "Save As (System)..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "saveAsSystemActionPerformed", false ) ) + } ) add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { name: "separator2" } ) diff --git a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts index 1ae7abbc..e541d58e 100644 --- a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts @@ -71,6 +71,17 @@ tasks { else -> emptyList() } } ) + + doFirst { + // check required Java version + if( JavaVersion.current() < JavaVersion.VERSION_11 ) { + println() + println( "WARNING: Java 11 or later required to build Linux native library (running ${System.getProperty( "java.version" )})" ) + println( " Native library built with older Java versions throw following exception when running in Java 17+:" ) + println( " java.lang.UnsatisfiedLinkError: .../libjawt.so: version `SUNWprivate_1.1' not found" ) + println() + } + } } withType().configureEach { diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp index 01017446..858ef1b0 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp @@ -177,11 +177,11 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrar bool multiSelect = isOptionSet( FC_select_multiple ); GtkWidget* dialog = gtk_file_chooser_dialog_new( (ctitle != NULL) ? ctitle - : (open ? (selectFolder ? (multiSelect ? _("Select Folders") : _("Select Folder")) - : (multiSelect ? _("Open Files") : _("Open File"))) : _("Save File")), + : (selectFolder ? (multiSelect ? _("Select Folders") : _("Select Folder")) + : (open ? ((multiSelect ? _("Open Files") : _("Open File"))) : _("Save File"))), NULL, // can not use AWT X11 window as parent because GtkWindow is required - open ? (selectFolder ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER : GTK_FILE_CHOOSER_ACTION_OPEN) - : GTK_FILE_CHOOSER_ACTION_SAVE, + selectFolder ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER + : (open ? GTK_FILE_CHOOSER_ACTION_OPEN : GTK_FILE_CHOOSER_ACTION_SAVE), _("_Cancel"), GTK_RESPONSE_CANCEL, (cokButtonLabel != NULL) ? cokButtonLabel : (open ? _("_Open") : _("_Save")), GTK_RESPONSE_ACCEPT, NULL ); // marks end of buttons diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp index 03bc2b5b..613cb072 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp @@ -144,6 +144,7 @@ public: //---- helper ----------------------------------------------------------------- +#define isOptionSet( option ) ((optionsSet & com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_ ## option) != 0) #define CHECK_HRESULT( code ) { if( (code) != S_OK ) return NULL; } jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { @@ -170,12 +171,12 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr return NULL; // handle limitations (without this, some Win32 method fails and this method returns NULL) - if( (optionsSet & FOS_PICKFOLDERS) != 0 ) { - if( open ) - fileTypes = NULL; // no filter allowed for picking folders - else - optionsSet &= ~FOS_PICKFOLDERS; // not allowed for save dialog + if( isOptionSet( FOS_PICKFOLDERS ) ) { + open = true; // always use IFileOpenDialog for picking folders + fileTypes = NULL; // no filter allowed for picking folders } + if( !open && isOptionSet( FOS_ALLOWMULTISELECT ) ) + optionsSet &= ~FOS_ALLOWMULTISELECT; // convert Java strings to C strings AutoReleaseString ctitle( env, title ); @@ -219,7 +220,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr CHECK_HRESULT( dialog->SetOptions ( (existingOptions & ~optionsClear) | optionsSet ) ); // initialize filter - if( specs.count > 0 && (optionsSet & FOS_PICKFOLDERS) == 0 ) { + if( specs.count > 0 ) { CHECK_HRESULT( dialog->SetFileTypes( specs.count, specs.specs ) ); if( fileTypeIndex > 0 ) CHECK_HRESULT( dialog->SetFileTypeIndex( min( fileTypeIndex + 1, specs.count ) ) ); diff --git a/flatlaf-testing/build.gradle.kts b/flatlaf-testing/build.gradle.kts index 6fd1b64f..7215de8b 100644 --- a/flatlaf-testing/build.gradle.kts +++ b/flatlaf-testing/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation( libs.jide.oss ) implementation( libs.glazedlists ) implementation( libs.netbeans.api.awt ) + implementation( libs.nativejfilechooser ) components.all() } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java new file mode 100644 index 00000000..cb66fa12 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java @@ -0,0 +1,749 @@ +/* + * Copyright 2025 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 java.awt.Dialog; +import java.awt.FileDialog; +import java.awt.Frame; +import java.awt.Window; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.event.WindowFocusListener; +import java.awt.event.WindowListener; +import java.awt.event.WindowStateListener; +import java.io.File; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.prefs.Preferences; +import java.util.stream.Stream; +import javax.swing.*; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; +import com.formdev.flatlaf.FlatSystemProperties; +import com.formdev.flatlaf.demo.DemoPrefs; +import com.formdev.flatlaf.util.SystemFileChooser; +import com.formdev.flatlaf.util.SystemInfo; +import li.flor.nativejfilechooser.NativeJFileChooser; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatSystemFileChooserTest + 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, "FlatSystemFileChooserTest" ); + frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); // necessary because of JavaFX + addListeners( frame ); + frame.showFrame( FlatSystemFileChooserTest::new ); + } ); + } + + FlatSystemFileChooserTest() { + initComponents(); + + if( !NativeJFileChooser.FX_AVAILABLE ) { + javafxOpenButton.setEnabled( false ); + javafxSaveButton.setEnabled( false ); + } + + Preferences state = DemoPrefs.getState(); + currentDirField.setText( state.get( "systemfilechooser.currentdir", "" ) ); + selectedFileField.setText( state.get( "systemfilechooser.selectedfile", "" ) ); + selectedFilesField.setText( state.get( "systemfilechooser.selectedfiles", "" ) ); + currentDirCheckBox.setSelected( state.getBoolean( "systemfilechooser.currentdir.enabled", false ) ); + selectedFileCheckBox.setSelected( state.getBoolean( "systemfilechooser.selectedfile.enabled", false ) ); + selectedFilesCheckBox.setSelected( state.getBoolean( "systemfilechooser.selectedfiles.enabled", false ) ); + + currentDirChanged(); + selectedFileChanged(); + selectedFilesChanged(); + } + + private void open() { + SystemFileChooser fc = new SystemFileChooser(); + configureSystemFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showOpenDialog( owner ); + outputSystemFileChooser( fc, result ); + } ); + } + + private void save() { + SystemFileChooser fc = new SystemFileChooser(); + configureSystemFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showSaveDialog( owner ); + outputSystemFileChooser( fc, result ); + } ); + } + + private void swingOpen() { + JFileChooser fc = new JFileChooser(); + configureSwingFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showOpenDialog( owner ); + outputSwingFileChooser( "Swing", fc, result ); + } ); + } + + private void swingSave() { + JFileChooser fc = new JFileChooser(); + configureSwingFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showSaveDialog( owner ); + outputSwingFileChooser( "Swing", fc, result ); + } ); + } + + private void awtOpen() { + showWithOwner( owner -> { + FileDialog fc = (owner instanceof Frame) + ? new FileDialog( (Frame) owner ) + : new FileDialog( (Dialog) owner ); + configureAWTFileChooser( fc, true ); + fc.setVisible( true ); + outputAWTFileChooser( fc ); + } ); + } + + private void awtSave() { + showWithOwner( owner -> { + FileDialog fc = (owner instanceof Frame) + ? new FileDialog( (Frame) owner ) + : new FileDialog( (Dialog) owner ); + configureAWTFileChooser( fc, false ); + fc.setVisible( true ); + outputAWTFileChooser( fc ); + } ); + } + + private void javafxOpen() { + JFileChooser fc = new NativeJFileChooser(); + configureSwingFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showOpenDialog( owner ); + outputSwingFileChooser( "JavaFX", fc, result ); + } ); + } + + private void javafxSave() { + JFileChooser fc = new NativeJFileChooser(); + configureSwingFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showSaveDialog( owner ); + outputSwingFileChooser( "JavaFX", fc, result ); + } ); + } + + private void configureSystemFileChooser( SystemFileChooser fc ) { + fc.setDialogTitle( n( dialogTitleField.getText() ) ); + fc.setApproveButtonText( n( approveButtonTextField.getText() ) ); + fc.setApproveButtonMnemonic( mnemonic( approveButtonMnemonicField.getText() ) ); + + // paths + if( currentDirCheckBox.isSelected() ) + fc.setCurrentDirectory( toFile( currentDirField.getText() ) ); + if( selectedFileCheckBox.isSelected() ) + fc.setSelectedFile( toFile( selectedFileField.getText() ) ); + if( selectedFilesCheckBox.isSelected() ) + fc.setSelectedFiles( toFiles( selectedFilesField.getText() ) ); + + // options + if( directorySelectionCheckBox.isSelected() ) + fc.setFileSelectionMode( SystemFileChooser.DIRECTORIES_ONLY ); + fc.setMultiSelectionEnabled( multiSelectionEnabledCheckBox.isSelected() ); + fc.setFileHidingEnabled( useFileHidingCheckBox.isSelected() ); + if( useSystemFileChooserCheckBox.isSelected() ) + System.clearProperty( FlatSystemProperties.USE_SYSTEM_FILE_CHOOSER ); + else + System.setProperty( FlatSystemProperties.USE_SYSTEM_FILE_CHOOSER, "false" ); + + //TODO filter + } + + private void configureSwingFileChooser( JFileChooser fc ) { + fc.setDialogTitle( n( dialogTitleField.getText() ) ); + fc.setApproveButtonText( n( approveButtonTextField.getText() ) ); + fc.setApproveButtonMnemonic( mnemonic( approveButtonMnemonicField.getText() ) ); + + // paths + if( currentDirCheckBox.isSelected() ) + fc.setCurrentDirectory( toFile( currentDirField.getText() ) ); + if( selectedFileCheckBox.isSelected() ) + fc.setSelectedFile( toFile( selectedFileField.getText() ) ); + if( selectedFilesCheckBox.isSelected() ) + fc.setSelectedFiles( toFiles( selectedFilesField.getText() ) ); + + // options + if( directorySelectionCheckBox.isSelected() ) + fc.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY ); + fc.setMultiSelectionEnabled( multiSelectionEnabledCheckBox.isSelected() ); + fc.setFileHidingEnabled( useFileHidingCheckBox.isSelected() ); + + // filter + String fileTypesStr = n( (String) fileTypesField.getSelectedItem() ); + String[] fileTypes = {}; + if( fileTypesStr != null ) + fileTypes = fileTypesStr.trim().split( "[,]+" ); + int fileTypeIndex = fileTypeIndexSlider.getValue(); + if( !useAcceptAllFileFilterCheckBox.isSelected() ) + fc.setAcceptAllFileFilterUsed( false ); + for( int i = 0; i < fileTypes.length; i += 2 ) { + fc.addChoosableFileFilter( "*".equals( fileTypes[i+1] ) + ? fc.getAcceptAllFileFilter() + : new FileNameExtensionFilter( fileTypes[i], fileTypes[i+1].split( ";" ) ) ); + } + FileFilter[] filters = fc.getChoosableFileFilters(); + if( filters.length > 0 ) + fc.setFileFilter( filters[Math.min( Math.max( fileTypeIndex, 0 ), filters.length - 1 )] ); + } + + private void configureAWTFileChooser( FileDialog fc, boolean open ) { + fc.setMode( open ? FileDialog.LOAD : FileDialog.SAVE ); + fc.setTitle( n( dialogTitleField.getText() ) ); + + // paths + if( currentDirCheckBox.isSelected() ) + fc.setDirectory( n( currentDirField.getText() ) ); + + // options + fc.setMultipleMode( multiSelectionEnabledCheckBox.isSelected() ); + } + + private void outputSystemFileChooser( SystemFileChooser fc, int result ) { + output( "System", fc.getDialogType() == SystemFileChooser.OPEN_DIALOG, + fc.isDirectorySelectionEnabled(), fc.isMultiSelectionEnabled(), + "result", result, + "currentDirectory", fc.getCurrentDirectory(), + "selectedFile", fc.getSelectedFile(), + "selectedFiles", fc.getSelectedFiles() ); + } + + private void outputSwingFileChooser( String type, JFileChooser fc, int result ) { + output( type, fc.getDialogType() == JFileChooser.OPEN_DIALOG, + fc.isDirectorySelectionEnabled(), fc.isMultiSelectionEnabled(), + "result", result, + "currentDirectory", fc.getCurrentDirectory(), + "selectedFile", fc.getSelectedFile(), + "selectedFiles", fc.getSelectedFiles() ); + } + + private void outputAWTFileChooser( FileDialog fc ) { + output( "AWT", fc.getMode() == FileDialog.LOAD, false, fc.isMultipleMode(), + "files", fc.getFiles(), + "directory", fc.getDirectory(), + "file", fc.getFile() ); + } + + private void output( String type, boolean open, boolean directorySelection, + boolean multiSelection, Object... values ) + { + outputField.append( "---- " + type + " " + (open ? "Open " : "Save ") + + (directorySelection ? " directory-sel " : "") + + (multiSelection ? " multi-sel " : "") + + "----\n" ); + + for( int i = 0; i < values.length; i += 2 ) { + outputField.append( values[i] + " = " ); + Object value = values[i+1]; + if( value instanceof File[] ) + outputField.append( Arrays.toString( (File[]) value ).replace( ",", "\n " ) ); + else + outputField.append( String.valueOf( value ) ); + outputField.append( "\n" ); + } + outputField.append( "\n" ); + outputField.setCaretPosition( outputField.getDocument().getLength() ); + } + + private static String n( String s ) { + return !s.isEmpty() ? s : null; + } + + private static char mnemonic( String s ) { + return !s.isEmpty() ? s.charAt( 0 ) : 0; + } + + private void showWithOwner( Consumer showConsumer ) { + Window frame = SwingUtilities.windowForComponent( this ); + if( ownerFrameRadioButton.isSelected() ) + showConsumer.accept( frame ); + else if( ownerDialogRadioButton.isSelected() ) { + JDialog dialog = new JDialog( frame, "Dummy Modal Dialog", Dialog.DEFAULT_MODALITY_TYPE ); + dialog.setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE ); + dialog.addWindowListener( new WindowAdapter() { + @Override + public void windowOpened( WindowEvent e ) { + showConsumer.accept( dialog ); + } + } ); + dialog.setSize( 1200, 1000 ); + dialog.setLocationRelativeTo( this ); + dialog.setVisible( true ); + } else + showConsumer.accept( null ); + } + + private void currentDirChanged() { + boolean b = currentDirCheckBox.isSelected(); + currentDirField.setEditable( b ); + currentDirChooseButton.setEnabled( b ); + + DemoPrefs.getState().putBoolean( "systemfilechooser.currentdir.enabled", b ); + } + + private void selectedFileChanged() { + boolean b = selectedFileCheckBox.isSelected(); + selectedFileField.setEditable( b ); + selectedFileChooseButton.setEnabled( b ); + + DemoPrefs.getState().putBoolean( "systemfilechooser.selectedfile.enabled", b ); + } + + private void selectedFilesChanged() { + boolean b = selectedFilesCheckBox.isSelected(); + selectedFilesField.setEditable( b ); + selectedFilesChooseButton.setEnabled( b ); + + DemoPrefs.getState().putBoolean( "systemfilechooser.selectedfiles.enabled", b ); + } + + private void chooseCurrentDir() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle( "Current Directory" ); + chooser.setSelectedFile( toFile( currentDirField.getText() ) ); + chooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY ); + if( chooser.showOpenDialog( this ) == JFileChooser.APPROVE_OPTION ) { + currentDirField.setText( toString( chooser.getSelectedFile() ) ); + putState( "systemfilechooser.currentdir", currentDirField.getText() ); + } + } + + private void chooseSelectedFile() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle( "Selected File" ); + chooser.setSelectedFile( toFile( selectedFileField.getText() ) ); + chooser.setFileSelectionMode( JFileChooser.FILES_ONLY ); + if( chooser.showOpenDialog( this ) == JFileChooser.APPROVE_OPTION ) { + selectedFileField.setText( toString( chooser.getSelectedFile() ) ); + putState( "systemfilechooser.selectedfile", selectedFileField.getText() ); + } + } + + private void chooseSelectedFiles() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle( "Selected Files" ); + chooser.setSelectedFiles( toFiles( selectedFilesField.getText() ) ); + chooser.setFileSelectionMode( JFileChooser.FILES_ONLY ); + chooser.setMultiSelectionEnabled( true ); + if( chooser.showOpenDialog( this ) == JFileChooser.APPROVE_OPTION ) { + selectedFilesField.setText( toString( chooser.getSelectedFiles() ) ); + putState( "systemfilechooser.selectedfiles", selectedFilesField.getText() ); + } + } + + private static File toFile( String s ) { + return !s.isEmpty() ? new File( s ) : null; + } + + private static String toString( File file ) { + return (file != null) ? file.getAbsolutePath() : null; + } + + private static File[] toFiles( String s ) { + return !s.isEmpty() + ? Stream.of( s.split( "," ) ).map( name -> new File( name ) ).toArray( File[]::new ) + : new File[0]; + } + + private static String toString( File[] files ) { + return (files != null && files.length > 0) + ? String.join( ",", Stream.of( files ).map( file -> file.getAbsolutePath() ).toArray( String[]::new ) ) + : ""; + } + + private static void putState( String key, String value ) { + if( value.isEmpty() ) + DemoPrefs.getState().remove( key ); + else + DemoPrefs.getState().put( key, value ); + } + + 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 + ownerLabel = new JLabel(); + ownerFrameRadioButton = new JRadioButton(); + ownerDialogRadioButton = new JRadioButton(); + ownerNullRadioButton = new JRadioButton(); + ownerSpacer = new JPanel(null); + dialogTitleLabel = new JLabel(); + dialogTitleField = new JTextField(); + panel1 = new JPanel(); + directorySelectionCheckBox = new JCheckBox(); + multiSelectionEnabledCheckBox = new JCheckBox(); + useFileHidingCheckBox = new JCheckBox(); + useSystemFileChooserCheckBox = new JCheckBox(); + approveButtonTextLabel = new JLabel(); + approveButtonTextField = new JTextField(); + approveButtonMnemonicLabel = new JLabel(); + approveButtonMnemonicField = new JTextField(); + currentDirCheckBox = new JCheckBox(); + currentDirField = new JTextField(); + currentDirChooseButton = new JButton(); + selectedFileCheckBox = new JCheckBox(); + selectedFileField = new JTextField(); + selectedFileChooseButton = new JButton(); + selectedFilesCheckBox = new JCheckBox(); + selectedFilesField = new JTextField(); + selectedFilesChooseButton = new JButton(); + fileTypesLabel = new JLabel(); + fileTypesField = new JComboBox<>(); + fileTypeIndexLabel = new JLabel(); + fileTypeIndexSlider = new JSlider(); + useAcceptAllFileFilterCheckBox = new JCheckBox(); + openButton = new JButton(); + saveButton = new JButton(); + swingOpenButton = new JButton(); + swingSaveButton = new JButton(); + awtOpenButton = new JButton(); + awtSaveButton = new JButton(); + javafxOpenButton = new JButton(); + javafxSaveButton = new JButton(); + outputScrollPane = new JScrollPane(); + outputField = new JTextArea(); + + //======== this ======== + setLayout(new MigLayout( + "ltr,insets dialog,hidemode 3", + // columns + "[left]" + + "[grow,fill]" + + "[fill]", + // rows + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[grow,fill]")); + + //---- ownerLabel ---- + ownerLabel.setText("owner"); + add(ownerLabel, "cell 0 0"); + + //---- ownerFrameRadioButton ---- + ownerFrameRadioButton.setText("JFrame"); + ownerFrameRadioButton.setSelected(true); + add(ownerFrameRadioButton, "cell 1 0"); + + //---- ownerDialogRadioButton ---- + ownerDialogRadioButton.setText("JDialog"); + add(ownerDialogRadioButton, "cell 1 0"); + + //---- ownerNullRadioButton ---- + ownerNullRadioButton.setText("null"); + add(ownerNullRadioButton, "cell 1 0"); + add(ownerSpacer, "cell 1 0,growx"); + + //---- dialogTitleLabel ---- + dialogTitleLabel.setText("dialogTitle"); + add(dialogTitleLabel, "cell 0 1"); + add(dialogTitleField, "cell 1 1"); + + //======== panel1 ======== + { + panel1.setLayout(new MigLayout( + "insets 2,hidemode 3", + // columns + "[left]", + // rows + "[]0" + + "[]0" + + "[]" + + "[]")); + + //---- directorySelectionCheckBox ---- + directorySelectionCheckBox.setText("directorySelection"); + panel1.add(directorySelectionCheckBox, "cell 0 0"); + + //---- multiSelectionEnabledCheckBox ---- + multiSelectionEnabledCheckBox.setText("multiSelectionEnabled"); + panel1.add(multiSelectionEnabledCheckBox, "cell 0 1"); + + //---- useFileHidingCheckBox ---- + useFileHidingCheckBox.setText("useFileHiding"); + useFileHidingCheckBox.setSelected(true); + panel1.add(useFileHidingCheckBox, "cell 0 2"); + + //---- useSystemFileChooserCheckBox ---- + useSystemFileChooserCheckBox.setText("use SystemFileChooser"); + useSystemFileChooserCheckBox.setSelected(true); + panel1.add(useSystemFileChooserCheckBox, "cell 0 3"); + } + add(panel1, "cell 2 1 1 7,aligny top,growy 0"); + + //---- approveButtonTextLabel ---- + approveButtonTextLabel.setText("approveButtonText"); + add(approveButtonTextLabel, "cell 0 2"); + add(approveButtonTextField, "cell 1 2,growx"); + + //---- approveButtonMnemonicLabel ---- + approveButtonMnemonicLabel.setText("approveButtonMnemonic"); + add(approveButtonMnemonicLabel, "cell 1 2"); + + //---- approveButtonMnemonicField ---- + approveButtonMnemonicField.setColumns(3); + add(approveButtonMnemonicField, "cell 1 2"); + + //---- currentDirCheckBox ---- + currentDirCheckBox.setText("current directory"); + currentDirCheckBox.addActionListener(e -> currentDirChanged()); + add(currentDirCheckBox, "cell 0 3"); + add(currentDirField, "cell 1 3,growx"); + + //---- currentDirChooseButton ---- + currentDirChooseButton.setText("..."); + currentDirChooseButton.addActionListener(e -> chooseCurrentDir()); + add(currentDirChooseButton, "cell 1 3"); + + //---- selectedFileCheckBox ---- + selectedFileCheckBox.setText("selected file"); + selectedFileCheckBox.addActionListener(e -> selectedFileChanged()); + add(selectedFileCheckBox, "cell 0 4"); + add(selectedFileField, "cell 1 4,growx"); + + //---- selectedFileChooseButton ---- + selectedFileChooseButton.setText("..."); + selectedFileChooseButton.addActionListener(e -> chooseSelectedFile()); + add(selectedFileChooseButton, "cell 1 4"); + + //---- selectedFilesCheckBox ---- + selectedFilesCheckBox.setText("selected files"); + selectedFilesCheckBox.addActionListener(e -> selectedFilesChanged()); + add(selectedFilesCheckBox, "cell 0 5"); + add(selectedFilesField, "cell 1 5,growx"); + + //---- selectedFilesChooseButton ---- + selectedFilesChooseButton.setText("..."); + selectedFilesChooseButton.addActionListener(e -> chooseSelectedFiles()); + add(selectedFilesChooseButton, "cell 1 5"); + + //---- fileTypesLabel ---- + fileTypesLabel.setText("fileTypes"); + add(fileTypesLabel, "cell 0 6"); + + //---- fileTypesField ---- + fileTypesField.setEditable(true); + fileTypesField.setModel(new DefaultComboBoxModel<>(new String[] { + "Text Files,txt", + "All Files,*", + "Text Files,txt,PDF Files,pdf,All Files,*", + "Text and PDF Files,txt;pdf" + })); + add(fileTypesField, "cell 1 6"); + + //---- fileTypeIndexLabel ---- + fileTypeIndexLabel.setText("fileTypeIndex"); + add(fileTypeIndexLabel, "cell 0 7"); + + //---- fileTypeIndexSlider ---- + fileTypeIndexSlider.setMaximum(10); + fileTypeIndexSlider.setMajorTickSpacing(1); + fileTypeIndexSlider.setValue(0); + fileTypeIndexSlider.setPaintLabels(true); + fileTypeIndexSlider.setSnapToTicks(true); + add(fileTypeIndexSlider, "cell 1 7,growx"); + + //---- useAcceptAllFileFilterCheckBox ---- + useAcceptAllFileFilterCheckBox.setText("useAcceptAllFileFilter"); + useAcceptAllFileFilterCheckBox.setSelected(true); + add(useAcceptAllFileFilterCheckBox, "cell 1 7"); + + //---- 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"); + + //---- swingOpenButton ---- + swingOpenButton.setText("Swing Open..."); + swingOpenButton.addActionListener(e -> swingOpen()); + add(swingOpenButton, "cell 0 8 3 1"); + + //---- swingSaveButton ---- + swingSaveButton.setText("Swing Save..."); + swingSaveButton.addActionListener(e -> swingSave()); + add(swingSaveButton, "cell 0 8 3 1"); + + //---- awtOpenButton ---- + awtOpenButton.setText("AWT Open..."); + awtOpenButton.addActionListener(e -> awtOpen()); + add(awtOpenButton, "cell 0 8 3 1"); + + //---- awtSaveButton ---- + awtSaveButton.setText("AWT Save..."); + awtSaveButton.addActionListener(e -> awtSave()); + add(awtSaveButton, "cell 0 8 3 1"); + + //---- javafxOpenButton ---- + javafxOpenButton.setText("JavaFX Open..."); + javafxOpenButton.addActionListener(e -> javafxOpen()); + add(javafxOpenButton, "cell 0 8 3 1"); + + //---- javafxSaveButton ---- + javafxSaveButton.setText("JavaFX Save..."); + javafxSaveButton.addActionListener(e -> javafxSave()); + add(javafxSaveButton, "cell 0 8 3 1"); + + //======== outputScrollPane ======== + { + + //---- outputField ---- + outputField.setRows(20); + outputScrollPane.setViewportView(outputField); + } + add(outputScrollPane, "cell 0 9 3 1,growx"); + + //---- ownerButtonGroup ---- + ButtonGroup ownerButtonGroup = new ButtonGroup(); + ownerButtonGroup.add(ownerFrameRadioButton); + ownerButtonGroup.add(ownerDialogRadioButton); + ownerButtonGroup.add(ownerNullRadioButton); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JLabel ownerLabel; + private JRadioButton ownerFrameRadioButton; + private JRadioButton ownerDialogRadioButton; + private JRadioButton ownerNullRadioButton; + private JPanel ownerSpacer; + private JLabel dialogTitleLabel; + private JTextField dialogTitleField; + private JPanel panel1; + private JCheckBox directorySelectionCheckBox; + private JCheckBox multiSelectionEnabledCheckBox; + private JCheckBox useFileHidingCheckBox; + private JCheckBox useSystemFileChooserCheckBox; + private JLabel approveButtonTextLabel; + private JTextField approveButtonTextField; + private JLabel approveButtonMnemonicLabel; + private JTextField approveButtonMnemonicField; + private JCheckBox currentDirCheckBox; + private JTextField currentDirField; + private JButton currentDirChooseButton; + private JCheckBox selectedFileCheckBox; + private JTextField selectedFileField; + private JButton selectedFileChooseButton; + private JCheckBox selectedFilesCheckBox; + private JTextField selectedFilesField; + private JButton selectedFilesChooseButton; + private JLabel fileTypesLabel; + private JComboBox fileTypesField; + private JLabel fileTypeIndexLabel; + private JSlider fileTypeIndexSlider; + private JCheckBox useAcceptAllFileFilterCheckBox; + private JButton openButton; + private JButton saveButton; + private JButton swingOpenButton; + private JButton swingSaveButton; + private JButton awtOpenButton; + private JButton awtSaveButton; + private JButton javafxOpenButton; + private JButton javafxSaveButton; + private JScrollPane outputScrollPane; + private JTextArea outputField; + // JFormDesigner - End of variables declaration //GEN-END:variables +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd new file mode 100644 index 00000000..2aa7c66b --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd @@ -0,0 +1,321 @@ +JFDML JFormDesigner: "8.3" 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: "ownerLabel" + "text": "owner" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerFrameRadioButton" + "text": "JFrame" + "selected": true + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerDialogRadioButton" + "text": "JDialog" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerNullRadioButton" + "text": "null" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) { + name: "ownerSpacer" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0,growx" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "dialogTitleLabel" + "text": "dialogTitle" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "dialogTitleField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "insets 2,hidemode 3" + "$columnConstraints": "[left]" + "$rowConstraints": "[]0[]0[][]" + } ) { + name: "panel1" + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "directorySelectionCheckBox" + "text": "directorySelection" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "multiSelectionEnabledCheckBox" + "text": "multiSelectionEnabled" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "useFileHidingCheckBox" + "text": "useFileHiding" + "selected": true + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "useSystemFileChooserCheckBox" + "text": "use SystemFileChooser" + "selected": true + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1 1 7,aligny top,growy 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "approveButtonTextLabel" + "text": "approveButtonText" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "approveButtonTextField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2,growx" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "approveButtonMnemonicLabel" + "text": "approveButtonMnemonic" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "approveButtonMnemonicField" + "columns": 3 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "currentDirCheckBox" + "text": "current directory" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "currentDirChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "currentDirField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3,growx" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "currentDirChooseButton" + "text": "..." + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "chooseCurrentDir", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "selectedFileCheckBox" + "text": "selected file" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "selectedFileChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "selectedFileField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4,growx" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "selectedFileChooseButton" + "text": "..." + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "chooseSelectedFile", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "selectedFilesCheckBox" + "text": "selected files" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "selectedFilesChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "selectedFilesField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5,growx" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "selectedFilesChooseButton" + "text": "..." + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "chooseSelectedFiles", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypesLabel" + "text": "fileTypes" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "fileTypesField" + "editable": true + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "Text Files,txt" + addElement( "Text Files,txt" ) + addElement( "All Files,*" ) + addElement( "Text Files,txt,PDF Files,pdf,All Files,*" ) + addElement( "Text and PDF Files,txt;pdf" ) + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypeIndexLabel" + "text": "fileTypeIndex" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "fileTypeIndexSlider" + "maximum": 10 + "majorTickSpacing": 1 + "value": 0 + "paintLabels": true + "snapToTicks": true + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7,growx" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "useAcceptAllFileFilterCheckBox" + "text": "useAcceptAllFileFilter" + "selected": true + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7" + } ) + 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: "swingOpenButton" + "text": "Swing Open..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "swingOpen", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "swingSaveButton" + "text": "Swing Save..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "swingSave", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "awtOpenButton" + "text": "AWT Open..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "awtOpen", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "awtSaveButton" + "text": "AWT Save..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "awtSave", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "javafxOpenButton" + "text": "JavaFX Open..." + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "javafxOpen", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "javafxSaveButton" + "text": "JavaFX Save..." + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "javafxSave", 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: "outputScrollPane" + add( new FormComponent( "javax.swing.JTextArea" ) { + name: "outputField" + "rows": 20 + } ) + }, 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( 825, 465 ) + } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "ownerButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 475 ) + } ) + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java index 31a55aea..2aa9af1e 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java @@ -19,6 +19,7 @@ package com.formdev.flatlaf.testing; import static com.formdev.flatlaf.ui.FlatNativeWindowsLibrary.*; import java.awt.Dialog; import java.awt.EventQueue; +import java.awt.Font; import java.awt.SecondaryLoop; import java.awt.Toolkit; import java.awt.Window; @@ -393,6 +394,7 @@ public class FlatSystemFileChooserWindowsTest //---- pickFoldersCheckBox ---- pickFoldersCheckBox.setText("pickFolders"); + pickFoldersCheckBox.setFont(pickFoldersCheckBox.getFont().deriveFont(pickFoldersCheckBox.getFont().getStyle() | Font.BOLD)); panel1.add(pickFoldersCheckBox, "cell 0 3"); //---- shareAwareCheckBox ---- @@ -401,6 +403,7 @@ public class FlatSystemFileChooserWindowsTest //---- forceShowHiddenCheckBox ---- forceShowHiddenCheckBox.setText("forceShowHidden"); + forceShowHiddenCheckBox.setFont(forceShowHiddenCheckBox.getFont().deriveFont(forceShowHiddenCheckBox.getFont().getStyle() | Font.BOLD)); panel1.add(forceShowHiddenCheckBox, "cell 2 3"); //---- forceFileSystemCheckBox ---- @@ -441,6 +444,7 @@ public class FlatSystemFileChooserWindowsTest //---- allowMultiSelectCheckBox ---- allowMultiSelectCheckBox.setText("allowMultiSelect"); + allowMultiSelectCheckBox.setFont(allowMultiSelectCheckBox.getFont().deriveFont(allowMultiSelectCheckBox.getFont().getStyle() | Font.BOLD)); panel1.add(allowMultiSelectCheckBox, "cell 0 7"); //---- hidePinnedPlacesCheckBox ---- diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd index 66590a21..82828444 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd @@ -116,6 +116,7 @@ new FormModel { add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "pickFoldersCheckBox" "text": "pickFolders" + "font": new com.jformdesigner.model.SwingDerivedFont( null, 1, 0, false ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) @@ -128,6 +129,7 @@ new FormModel { add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "forceShowHiddenCheckBox" "text": "forceShowHidden" + "font": new com.jformdesigner.model.SwingDerivedFont( null, 1, 0, false ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 3" } ) @@ -188,6 +190,7 @@ new FormModel { add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "allowMultiSelectCheckBox" "text": "allowMultiSelect" + "font": new com.jformdesigner.model.SwingDerivedFont( null, 1, 0, false ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 7" } ) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java index 520220a9..61b01c1c 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java @@ -242,6 +242,9 @@ public class FlatTestFrame super.dispose(); FlatUIDefaultsInspector.hide(); + + if( getDefaultCloseOperation() == JFrame.EXIT_ON_CLOSE ) + System.exit( 0 ); } private void updateTitle() { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d9d326c..6bd75ca2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ fifesoft-autocomplete = "com.fifesoft:autocomplete:3.3.1" # flatlaf-testing glazedlists = "com.glazedlists:glazedlists:1.11.0" netbeans-api-awt = "org.netbeans.api:org-openide-awt:RELEASE112" +nativejfilechooser = "li.flor:native-j-file-chooser:1.6.4" # flatlaf-natives-jna jna = "net.java.dev.jna:jna:5.15.0" From 2e16ded5d447820701d26e27e0820c137ae1efb9 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 6 Jan 2025 19:22:15 +0100 Subject: [PATCH 09/34] System File Chooser: support macOS in class `SystemFileChooser` --- .../flatlaf/ui/FlatNativeMacLibrary.java | 5 +- .../flatlaf/util/SystemFileChooser.java | 54 ++++++++++++++++++- .../src/main/objcpp/MacFileChooser.mm | 19 ++++--- 3 files changed, 69 insertions(+), 9 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 5ceb2f61..73bef9b7 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 @@ -72,7 +72,7 @@ public class FlatNativeMacLibrary /** @since 3.6 */ public static final int - // NSOpenPanel + // NSOpenPanel (extends NSSavePanel) FC_canChooseFiles = 1 << 0, // default FC_canChooseDirectories = 1 << 1, FC_resolvesAliases = 1 << 2, // default @@ -105,7 +105,8 @@ public class FlatNativeMacLibrary * @param optionsSet options to set; see {@code FC_*} constants * @param optionsClear options to clear; 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 + * @return file path(s) that the user selected; an empty array if canceled; + * or {@code null} on failures (no dialog shown) * * @since 3.6 */ 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 d5d6d773..23f6a2d6 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 @@ -31,6 +31,7 @@ import javax.swing.UIManager; import javax.swing.filechooser.FileSystemView; import com.formdev.flatlaf.FlatSystemProperties; import com.formdev.flatlaf.ui.FlatNativeLinuxLibrary; +import com.formdev.flatlaf.ui.FlatNativeMacLibrary; import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; /** @@ -61,7 +62,11 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; * If user selects "Yes", the file chooser closes. If user selects "No", the file chooser stays open. * It is not possible to customize that question dialog. *
  • Save File dialog does not support multi-selection. + *
  • For selection mode {@link #DIRECTORIES_ONLY}, dialog type {@link #SAVE_DIALOG} is ignored. + * Operating system file dialogs support folder selection only in "Open" dialogs. *
  • {@link JFileChooser#FILES_AND_DIRECTORIES} is not supported. + *
  • {@link #getSelectedFiles()} returns selected file also in single selection mode. + * {@link JFileChooser#getSelectedFiles()} only in multi selection mode. * * * @author Karl Tauber @@ -289,6 +294,8 @@ public class SystemFileChooser if( SystemInfo.isWindows_10_orLater && FlatNativeWindowsLibrary.isLoaded() ) return new WindowsFileChooserProvider(); + else if( SystemInfo.isMacOS && FlatNativeMacLibrary.isLoaded() ) + return new MacFileChooserProvider(); else if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isLoaded() ) return new LinuxFileChooserProvider(); else // unknown platform or FlatLaf native library not loaded @@ -413,7 +420,52 @@ public class SystemFileChooser } } - //---- class LinuxFileChooserProvider -----------------------------------.. + //---- class MacFileChooserProvider --------------------------------------- + + private static class MacFileChooserProvider + extends SystemFileChooserProvider + { + @Override + String[] showSystemDialog( Window owner, SystemFileChooser fc ) { + boolean open = (fc.getDialogType() == OPEN_DIALOG); + String nameFieldStringValue = null; + String directoryURL = null; + + // paths + File currentDirectory = fc.getCurrentDirectory(); + File selectedFile = fc.getSelectedFile(); + if( selectedFile != null ) { + if( selectedFile.isDirectory() ) + directoryURL = selectedFile.getAbsolutePath(); + else { + nameFieldStringValue = selectedFile.getName(); + directoryURL = selectedFile.getParent(); + } + } else if( currentDirectory != null ) + directoryURL = currentDirectory.getAbsolutePath(); + + // options + int optionsSet = 0; + int optionsClear = 0; + if( fc.isDirectorySelectionEnabled() ) { + optionsSet |= FlatNativeMacLibrary.FC_canChooseDirectories; + optionsClear |= FlatNativeMacLibrary.FC_canChooseFiles; + open = true; + } + if( fc.isMultiSelectionEnabled() ) + optionsSet |= FlatNativeMacLibrary.FC_allowsMultipleSelection; + if( !fc.isFileHidingEnabled() ) + optionsSet |= FlatNativeMacLibrary.FC_showsHiddenFiles; + + // show system file dialog + return FlatNativeMacLibrary.showFileChooser( open, + fc.getDialogTitle(), fc.getApproveButtonText(), null, null, + nameFieldStringValue, directoryURL, + optionsSet, optionsClear ); + } + } + + //---- class LinuxFileChooserProvider ------------------------------------- private static class LinuxFileChooserProvider extends SystemFileChooserProvider 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 05fd3d67..9755c5ab 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -23,15 +23,23 @@ /** * @author Karl Tauber + * @since 3.6 */ +//---- helper ----------------------------------------------------------------- + #define isOptionSet( option ) ((optionsSet & 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 ) -// declare internal methods +// declare methods NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window ); +jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { + jclass stringClass = env->FindClass( "java/lang/String" ); + return env->NewObjectArray( count, stringClass, NULL ); +} + //---- JNI methods ------------------------------------------------------------ extern "C" @@ -135,19 +143,18 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ urls = @[url]; if( urls == NULL ) - return NULL; + return newJavaStringArray( env, 0 ); // convert URLs to Java string array jsize count = urls.count; - jclass stringClass = env->FindClass( "java/lang/String" ); - jobjectArray result = env->NewObjectArray( count, stringClass, NULL ); + jobjectArray array = newJavaStringArray( env, count ); for( int i = 0; i < count; i++ ) { jstring filename = NormalizedPathJavaFromNSString( env, [urls[i] path] ); - env->SetObjectArrayElement( result, i, filename ); + env->SetObjectArrayElement( array, i, filename ); env->DeleteLocalRef( filename ); } - return result; + return array; JNI_COCOA_EXIT() } From 9af7f95197fada8b994a239a89711dd7897121ae Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 7 Jan 2025 14:37:58 +0100 Subject: [PATCH 10/34] System File Chooser: added "Format" combobox on macOS (if using more than one filter) --- .../flatlaf/ui/FlatNativeMacLibrary.java | 19 ++- .../flatlaf/util/SystemFileChooser.java | 6 +- ..._formdev_flatlaf_ui_FlatNativeMacLibrary.h | 8 +- .../src/main/objcpp/MacFileChooser.mm | 157 +++++++++++++++--- .../testing/FlatSystemFileChooserMacTest.java | 153 ++++++++++++----- .../testing/FlatSystemFileChooserMacTest.jfd | 114 +++++++++---- 6 files changed, 358 insertions(+), 99 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 73bef9b7..3615ef20 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 @@ -77,6 +77,7 @@ public class FlatNativeMacLibrary FC_canChooseDirectories = 1 << 1, FC_resolvesAliases = 1 << 2, // default FC_allowsMultipleSelection = 1 << 3, + FC_accessoryViewDisclosed = 1 << 4, // NSSavePanel FC_showsTagField = 1 << 8, // default for Save FC_canCreateDirectories = 1 << 9, // default for Save @@ -84,7 +85,9 @@ public class FlatNativeMacLibrary FC_showsHiddenFiles = 1 << 11, FC_extensionHidden = 1 << 12, FC_allowsOtherFileTypes = 1 << 13, - FC_treatsFilePackagesAsDirectories = 1 << 14; + FC_treatsFilePackagesAsDirectories = 1 << 14, + // custom + FC_showSingleFilterField = 1 << 24; /** * Shows the macOS system file dialogs @@ -99,19 +102,25 @@ public class FlatNativeMacLibrary * @param title text displayed at top of save dialog (not used in open dialog); or {@code null} * @param prompt text displayed in default button; or {@code null} * @param message text displayed at top of open/save dialogs; or {@code null} + * @param filterFieldLabel text displayed in front of the filter combobox; or {@code null} * @param nameFieldLabel text displayed in front of the filename text field in save dialog (not used in open dialog); or {@code null} * @param nameFieldStringValue user-editable filename currently shown in the name field in save dialog (not used in open dialog); or {@code null} * @param directoryURL current directory shown in the dialog; or {@code null} * @param optionsSet options to set; see {@code FC_*} constants * @param optionsClear options to clear; see {@code FC_*} constants - * @param allowedFileTypes allowed filename extensions (e.g. "txt") + * @param fileTypeIndex the file type that appears as selected (zero-based) + * @param fileTypes file types that the dialog can open or save. + * Two or more strings and {@code null} are required for each filter. + * First string is the display name of the filter shown in the combobox (e.g. "Text Files"). + * Subsequent strings are the filter patterns (e.g. "*.txt" or "*"). + * {@code null} is required to mark end of filter. * @return file path(s) that the user selected; an empty array if canceled; * or {@code null} on failures (no dialog shown) * * @since 3.6 */ public native static String[] showFileChooser( boolean open, - String title, String prompt, String message, String nameFieldLabel, - String nameFieldStringValue, String directoryURL, - int optionsSet, int optionsClear, String... allowedFileTypes ); + String title, String prompt, String message, String filterFieldLabel, + String nameFieldLabel, String nameFieldStringValue, String directoryURL, + int optionsSet, int optionsClear, int fileTypeIndex, String... fileTypes ); } 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 23f6a2d6..dea48265 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 @@ -445,7 +445,7 @@ public class SystemFileChooser directoryURL = currentDirectory.getAbsolutePath(); // options - int optionsSet = 0; + int optionsSet = FlatNativeMacLibrary.FC_accessoryViewDisclosed; int optionsClear = 0; if( fc.isDirectorySelectionEnabled() ) { optionsSet |= FlatNativeMacLibrary.FC_canChooseDirectories; @@ -459,9 +459,9 @@ public class SystemFileChooser // show system file dialog return FlatNativeMacLibrary.showFileChooser( open, - fc.getDialogTitle(), fc.getApproveButtonText(), null, null, + fc.getDialogTitle(), fc.getApproveButtonText(), null, null, null, nameFieldStringValue, directoryURL, - optionsSet, optionsClear ); + optionsSet, optionsClear, 0 ); } } 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 7d163d7f..1e581622 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 @@ -21,6 +21,8 @@ extern "C" { #define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_resolvesAliases 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_accessoryViewDisclosed +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_accessoryViewDisclosed 16L #undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsTagField #define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsTagField 256L #undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canCreateDirectories @@ -35,6 +37,8 @@ extern "C" { #define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsOtherFileTypes 8192L #undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_treatsFilePackagesAsDirectories #define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_treatsFilePackagesAsDirectories 16384L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showSingleFilterField +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showSingleFilterField 16777216L /* * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary * Method: setWindowRoundedBorder @@ -78,10 +82,10 @@ 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;II[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;III[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, jint, jobjectArray); + (JNIEnv *, jclass, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jint, jint, 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 9755c5ab..8a6615ec 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -26,6 +26,80 @@ * @since 3.6 */ +//---- class FileChooserDelegate ---------------------------------------------- + +@interface FileChooserDelegate : NSObject { + NSArray* _filters; + } + + @property (nonatomic, assign) NSSavePanel* dialog; + + - (void)initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex + :(NSString*)filterFieldLabel :(bool)showSingleFilterField; + - (void)selectFormat: (id)sender; + - (void)selectFormatAtIndex: (int)index; +@end + +@implementation FileChooserDelegate + + - (void)initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex + :(NSString*)filterFieldLabel :(bool)showSingleFilterField + { + _filters = filters; + + // get filter names + NSArray* filterNames = filters.lastObject; + [filters removeLastObject]; + + // do not add filter/format combobox if there is only one filter + if( filters.count <= 1 && !showSingleFilterField ) { + [self selectFormatAtIndex:0]; + return; + } + + // create label + NSTextField* label = [[NSTextField alloc] initWithFrame:NSMakeRect( 0, 0, 60, 22 )]; + label.stringValue = (filterFieldLabel != NULL) ? filterFieldLabel : @"Format:"; + label.editable = NO; + label.bordered = NO; + label.bezeled = NO; + label.drawsBackground = NO; + + // create combobox + NSPopUpButton* popupButton = [[NSPopUpButton alloc] initWithFrame:NSMakeRect( 50, 2, 140, 22 ) pullsDown:NO]; + [popupButton addItemsWithTitles:filterNames]; + [popupButton selectItemAtIndex:MIN( MAX( filterIndex, 0 ), filterNames.count - 1 )]; + [popupButton setTarget:self]; + [popupButton setAction:@selector(selectFormat:)]; + + // create view + NSView* accessoryView = [[NSView alloc] initWithFrame:NSMakeRect( 0, 0, 200, 32 )]; + [accessoryView addSubview:label]; + [accessoryView addSubview:popupButton]; + + [_dialog setAccessoryView:accessoryView]; + + // initial filter + [self selectFormatAtIndex:filterIndex]; + } + + - (void)selectFormat:(id)sender { + NSPopUpButton* popupButton = (NSPopUpButton*) sender; + [self selectFormatAtIndex:popupButton.indexOfSelectedItem]; + } + + - (void)selectFormatAtIndex: (int)index { + index = MIN( MAX( index, 0 ), _filters.count - 1 ); + NSArray* fileTypes = [_filters objectAtIndex:index]; + + // 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 + _dialog.allowedFileTypes = [fileTypes containsObject:@"*"] ? nil : fileTypes; + } + +@end + //---- helper ----------------------------------------------------------------- #define isOptionSet( option ) ((optionsSet & com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ## option) != 0) @@ -40,14 +114,55 @@ jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { return env->NewObjectArray( count, stringClass, NULL ); } +static NSMutableArray* initFilters( JNIEnv* env, jobjectArray fileTypes ) { + jint length = env->GetArrayLength( fileTypes ); + if( length <= 0 ) + return NULL; + + NSMutableArray* filterNames = [NSMutableArray array]; + NSMutableArray* filters = [NSMutableArray array]; + NSString* filterName = NULL; + NSMutableArray* filter = NULL; + for( int i = 0; i < length; i++ ) { + jstring jstr = (jstring) env->GetObjectArrayElement( fileTypes, i ); + if( jstr == NULL ) { + if( filter != NULL ) { + if( filter.count > 0 ) { + [filterNames addObject:filterName]; + [filters addObject:filter]; + } + filterName = NULL; + filter = NULL; + } + continue; + } + + NSString* str = JavaToNSString( env, jstr ); + env->DeleteLocalRef( jstr ); + if( filter == NULL ) { + filterName = str; + filter = [NSMutableArray array]; + } else + [filter addObject:str]; + } + + if( filters.count == 0 ) + return NULL; + + // add filter names to array (removed again after creating combobox) + [filters addObject:filterNames]; + + return filters; +} + //---- 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 optionsSet, jint optionsClear, jobjectArray allowedFileTypes ) + jstring title, jstring prompt, jstring message, jstring filterFieldLabel, + jstring nameFieldLabel, jstring nameFieldStringValue, jstring directoryURL, + jint optionsSet, jint optionsClear, jint fileTypeIndex, jobjectArray fileTypes ) { JNI_COCOA_ENTER() @@ -55,21 +170,11 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ NSString* nsTitle = JavaToNSString( env, title ); NSString* nsPrompt = JavaToNSString( env, prompt ); NSString* nsMessage = JavaToNSString( env, message ); + NSString* nsFilterFieldLabel = JavaToNSString( env, filterFieldLabel ); 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 ); - nsArray[i] = JavaToNSString( env, str ); - env->DeleteLocalRef( str ); - } - nsAllowedFileTypes = nsArray; - } + NSMutableArray* filters = initFilters( env, fileTypes ); NSArray* urls = NULL; NSArray** purls = &urls; @@ -93,6 +198,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ if( nsDirectoryURL != NULL ) dialog.directoryURL = [NSURL fileURLWithPath:nsDirectoryURL isDirectory:YES]; + // set open options if( open ) { NSOpenPanel* openDialog = (NSOpenPanel*) dialog; @@ -109,6 +215,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ openDialog.allowsMultipleSelection = isOptionSet( FC_allowsMultipleSelection ); } + // set options if( isOptionSetOrClear( FC_showsTagField ) ) dialog.showsTagField = isOptionSet( FC_showsTagField ); if( isOptionSetOrClear( FC_canCreateDirectories ) ) @@ -124,13 +231,21 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ if( isOptionSetOrClear( FC_treatsFilePackagesAsDirectories ) ) dialog.treatsFilePackagesAsDirectories = isOptionSet( FC_treatsFilePackagesAsDirectories ); - // 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; + FileChooserDelegate* delegate = [FileChooserDelegate new]; + delegate.dialog = dialog; - if( [dialog runModal] != NSModalResponseOK ) + // initialize filter accessory view + if( filters != NULL ) { + [delegate initFilterAccessoryView:filters :fileTypeIndex :nsFilterFieldLabel :isOptionSet( FC_showSingleFilterField )]; + + if( open && isOptionSetOrClear( FC_accessoryViewDisclosed ) ) + ((NSOpenPanel*)dialog).accessoryViewDisclosed = isOptionSet( FC_accessoryViewDisclosed ); + } + + // show dialog + NSModalResponse response = [dialog runModal]; + [delegate release]; + if( response != NSModalResponseOK ) return; if( open ) 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 ae52327d..99d2297b 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 @@ -66,6 +66,8 @@ public class FlatSystemFileChooserMacTest FlatSystemFileChooserMacTest() { initComponents(); + + fileTypesField.setSelectedItem( null ); } private void open() { @@ -88,6 +90,7 @@ public class FlatSystemFileChooserMacTest String title = n( titleField.getText() ); String prompt = n( promptField.getText() ); String message = n( messageField.getText() ); + String filterFieldLabel = n( filterFieldLabelField.getText() ); String nameFieldLabel = n( nameFieldLabelField.getText() ); String nameFieldStringValue = n( nameFieldStringValueField.getText() ); String directoryURL = n( directoryURLField.getText() ); @@ -101,6 +104,8 @@ public class FlatSystemFileChooserMacTest optionsSet.set( optionsSet.get() | FC_canChooseDirectories ); o( FC_resolvesAliases, resolvesAliasesCheckBox, optionsSet, optionsClear ); o( FC_allowsMultipleSelection, allowsMultipleSelectionCheckBox, optionsSet, optionsClear ); + if( accessoryViewDisclosedCheckBox.isSelected() ) + optionsSet.set( optionsSet.get() | FC_accessoryViewDisclosed ); // NSSavePanel o( FC_showsTagField, showsTagFieldCheckBox, optionsSet, optionsClear ); @@ -111,25 +116,39 @@ public class FlatSystemFileChooserMacTest o( FC_allowsOtherFileTypes, allowsOtherFileTypesCheckBox, optionsSet, optionsClear ); o( FC_treatsFilePackagesAsDirectories, treatsFilePackagesAsDirectoriesCheckBox, optionsSet, optionsClear ); - String allowedFileTypesStr = n( allowedFileTypesField.getText() ); - String[] allowedFileTypes = {}; - if( allowedFileTypesStr != null ) - allowedFileTypes = allowedFileTypesStr.trim().split( "[ ,]+" ); + // custom + if( showSingleFilterFieldCheckBox.isSelected() ) + optionsSet.set( optionsSet.get() | FC_showSingleFilterField ); + + String fileTypesStr = n( (String) fileTypesField.getSelectedItem() ); + String[] fileTypes = {}; + if( fileTypesStr != null ) { + if( !fileTypesStr.endsWith( ",null" ) ) + fileTypesStr += ",null"; + fileTypes = fileTypesStr.trim().split( "[,]+" ); + for( int i = 0; i < fileTypes.length; i++ ) { + if( "null".equals( fileTypes[i] ) ) + fileTypes[i] = null; + } + } + int fileTypeIndex = fileTypeIndexSlider.getValue(); if( direct ) { - String[] files = FlatNativeMacLibrary.showFileChooser( open, title, prompt, message, + String[] files = FlatNativeMacLibrary.showFileChooser( open, + title, prompt, message, filterFieldLabel, nameFieldLabel, nameFieldStringValue, directoryURL, - optionsSet.get(), optionsClear.get(), allowedFileTypes ); + optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes ); filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); } else { SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); - String[] allowedFileTypes2 = allowedFileTypes; + String[] fileTypes2 = fileTypes; new Thread( () -> { - String[] files = FlatNativeMacLibrary.showFileChooser( open, title, prompt, message, + String[] files = FlatNativeMacLibrary.showFileChooser( open, + title, prompt, message, filterFieldLabel, nameFieldLabel, nameFieldStringValue, directoryURL, - optionsSet.get(), optionsClear.get(), allowedFileTypes2 ); + optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes2 ); System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); @@ -144,7 +163,7 @@ public class FlatSystemFileChooserMacTest } private static String n( String s ) { - return !s.isEmpty() ? s : null; + return s != null && !s.isEmpty() ? s : null; } private static void o( int option, FlatTriStateCheckBox checkBox, AtomicInteger optionsSet, AtomicInteger optionsClear ) { @@ -220,6 +239,7 @@ public class FlatSystemFileChooserMacTest canChooseDirectoriesCheckBox = new JCheckBox(); resolvesAliasesCheckBox = new FlatTriStateCheckBox(); allowsMultipleSelectionCheckBox = new FlatTriStateCheckBox(); + accessoryViewDisclosedCheckBox = new JCheckBox(); options2Label = new JLabel(); showsTagFieldCheckBox = new FlatTriStateCheckBox(); canCreateDirectoriesCheckBox = new FlatTriStateCheckBox(); @@ -228,18 +248,24 @@ public class FlatSystemFileChooserMacTest extensionHiddenCheckBox = new FlatTriStateCheckBox(); allowsOtherFileTypesCheckBox = new FlatTriStateCheckBox(); treatsFilePackagesAsDirectoriesCheckBox = new FlatTriStateCheckBox(); + options3Label = new JLabel(); + showSingleFilterFieldCheckBox = new JCheckBox(); promptLabel = new JLabel(); promptField = new JTextField(); messageLabel = new JLabel(); messageField = new JTextField(); + filterFieldLabelLabel = new JLabel(); + filterFieldLabelField = 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(); + fileTypesLabel = new JLabel(); + fileTypesField = new JComboBox<>(); + fileTypeIndexLabel = new JLabel(); + fileTypeIndexSlider = new JSlider(); openButton = new JButton(); saveButton = new JButton(); openDirectButton = new JButton(); @@ -264,6 +290,8 @@ public class FlatSystemFileChooserMacTest "[]" + "[]" + "[]" + + "[]" + + "[]" + "[grow,fill]")); //---- titleLabel ---- @@ -282,6 +310,7 @@ public class FlatSystemFileChooserMacTest "[]0" + "[]0" + "[]0" + + "[]0" + "[]para" + "[]" + "[]0" + @@ -290,6 +319,8 @@ public class FlatSystemFileChooserMacTest "[]0" + "[]0" + "[]0" + + "[]para" + + "[]" + "[]")); //---- options1Label ---- @@ -314,39 +345,51 @@ public class FlatSystemFileChooserMacTest allowsMultipleSelectionCheckBox.setText("allowsMultipleSelection"); panel1.add(allowsMultipleSelectionCheckBox, "cell 0 4"); + //---- accessoryViewDisclosedCheckBox ---- + accessoryViewDisclosedCheckBox.setText("accessoryViewDisclosed"); + panel1.add(accessoryViewDisclosedCheckBox, "cell 0 5"); + //---- options2Label ---- options2Label.setText("NSOpenPanel and NSSavePanel options:"); - panel1.add(options2Label, "cell 0 5"); + panel1.add(options2Label, "cell 0 6"); //---- showsTagFieldCheckBox ---- showsTagFieldCheckBox.setText("showsTagField"); - panel1.add(showsTagFieldCheckBox, "cell 0 6"); + panel1.add(showsTagFieldCheckBox, "cell 0 7"); //---- canCreateDirectoriesCheckBox ---- canCreateDirectoriesCheckBox.setText("canCreateDirectories"); - panel1.add(canCreateDirectoriesCheckBox, "cell 0 7"); + panel1.add(canCreateDirectoriesCheckBox, "cell 0 8"); //---- canSelectHiddenExtensionCheckBox ---- canSelectHiddenExtensionCheckBox.setText("canSelectHiddenExtension"); - panel1.add(canSelectHiddenExtensionCheckBox, "cell 0 8"); + panel1.add(canSelectHiddenExtensionCheckBox, "cell 0 9"); //---- showsHiddenFilesCheckBox ---- showsHiddenFilesCheckBox.setText("showsHiddenFiles"); - panel1.add(showsHiddenFilesCheckBox, "cell 0 9"); + panel1.add(showsHiddenFilesCheckBox, "cell 0 10"); //---- extensionHiddenCheckBox ---- extensionHiddenCheckBox.setText("extensionHidden"); - panel1.add(extensionHiddenCheckBox, "cell 0 10"); + panel1.add(extensionHiddenCheckBox, "cell 0 11"); //---- allowsOtherFileTypesCheckBox ---- allowsOtherFileTypesCheckBox.setText("allowsOtherFileTypes"); - panel1.add(allowsOtherFileTypesCheckBox, "cell 0 11"); + panel1.add(allowsOtherFileTypesCheckBox, "cell 0 12"); //---- treatsFilePackagesAsDirectoriesCheckBox ---- treatsFilePackagesAsDirectoriesCheckBox.setText("treatsFilePackagesAsDirectories"); - panel1.add(treatsFilePackagesAsDirectoriesCheckBox, "cell 0 12"); + panel1.add(treatsFilePackagesAsDirectoriesCheckBox, "cell 0 13"); + + //---- options3Label ---- + options3Label.setText("Custom options:"); + panel1.add(options3Label, "cell 0 14"); + + //---- showSingleFilterFieldCheckBox ---- + showSingleFilterFieldCheckBox.setText("showSingleFilterField"); + panel1.add(showSingleFilterFieldCheckBox, "cell 0 15"); } - add(panel1, "cell 2 0 1 8,aligny top,growy 0"); + add(panel1, "cell 2 0 1 10,aligny top,growy 0"); //---- promptLabel ---- promptLabel.setText("prompt"); @@ -358,45 +401,72 @@ public class FlatSystemFileChooserMacTest add(messageLabel, "cell 0 2"); add(messageField, "cell 1 2"); + //---- filterFieldLabelLabel ---- + filterFieldLabelLabel.setText("filterFieldLabel"); + add(filterFieldLabelLabel, "cell 0 3"); + add(filterFieldLabelField, "cell 1 3"); + //---- nameFieldLabelLabel ---- nameFieldLabelLabel.setText("nameFieldLabel"); - add(nameFieldLabelLabel, "cell 0 3"); - add(nameFieldLabelField, "cell 1 3"); + add(nameFieldLabelLabel, "cell 0 4"); + add(nameFieldLabelField, "cell 1 4"); //---- nameFieldStringValueLabel ---- nameFieldStringValueLabel.setText("nameFieldStringValue"); - add(nameFieldStringValueLabel, "cell 0 4"); - add(nameFieldStringValueField, "cell 1 4"); + add(nameFieldStringValueLabel, "cell 0 5"); + add(nameFieldStringValueField, "cell 1 5"); //---- directoryURLLabel ---- directoryURLLabel.setText("directoryURL"); - add(directoryURLLabel, "cell 0 5"); - add(directoryURLField, "cell 1 5"); + add(directoryURLLabel, "cell 0 6"); + add(directoryURLField, "cell 1 6"); - //---- allowedFileTypesLabel ---- - allowedFileTypesLabel.setText("allowedFileTypes"); - add(allowedFileTypesLabel, "cell 0 6"); - add(allowedFileTypesField, "cell 1 6"); + //---- fileTypesLabel ---- + fileTypesLabel.setText("fileTypes"); + add(fileTypesLabel, "cell 0 7"); + + //---- fileTypesField ---- + fileTypesField.setEditable(true); + fileTypesField.setModel(new DefaultComboBoxModel<>(new String[] { + "Text Files,txt,null", + "All Files,*,null", + "Text Files,txt,null,PDF Files,pdf,null,All Files,*,null", + "Text and PDF Files,txt,pdf,null", + "Compressed,zip,gz,null,Disk Images,dmg,null" + })); + add(fileTypesField, "cell 1 7"); + + //---- fileTypeIndexLabel ---- + fileTypeIndexLabel.setText("fileTypeIndex"); + add(fileTypeIndexLabel, "cell 0 8"); + + //---- fileTypeIndexSlider ---- + fileTypeIndexSlider.setMaximum(10); + fileTypeIndexSlider.setMajorTickSpacing(1); + fileTypeIndexSlider.setValue(0); + fileTypeIndexSlider.setPaintLabels(true); + fileTypeIndexSlider.setSnapToTicks(true); + add(fileTypeIndexSlider, "cell 1 8"); //---- openButton ---- openButton.setText("Open..."); openButton.addActionListener(e -> open()); - add(openButton, "cell 0 8 3 1"); + add(openButton, "cell 0 10 3 1"); //---- saveButton ---- saveButton.setText("Save..."); saveButton.addActionListener(e -> save()); - add(saveButton, "cell 0 8 3 1"); + add(saveButton, "cell 0 10 3 1"); //---- openDirectButton ---- openDirectButton.setText("Open (no-thread)..."); openDirectButton.addActionListener(e -> openDirect()); - add(openDirectButton, "cell 0 8 3 1"); + add(openDirectButton, "cell 0 10 3 1"); //---- saveDirectButton ---- saveDirectButton.setText("Save (no-thread)..."); saveDirectButton.addActionListener(e -> saveDirect()); - add(saveDirectButton, "cell 0 8 3 1"); + add(saveDirectButton, "cell 0 10 3 1"); //======== filesScrollPane ======== { @@ -405,7 +475,7 @@ public class FlatSystemFileChooserMacTest filesField.setRows(8); filesScrollPane.setViewportView(filesField); } - add(filesScrollPane, "cell 0 9 3 1,growx"); + add(filesScrollPane, "cell 0 11 3 1,growx"); // JFormDesigner - End of component initialization //GEN-END:initComponents } @@ -418,6 +488,7 @@ public class FlatSystemFileChooserMacTest private JCheckBox canChooseDirectoriesCheckBox; private FlatTriStateCheckBox resolvesAliasesCheckBox; private FlatTriStateCheckBox allowsMultipleSelectionCheckBox; + private JCheckBox accessoryViewDisclosedCheckBox; private JLabel options2Label; private FlatTriStateCheckBox showsTagFieldCheckBox; private FlatTriStateCheckBox canCreateDirectoriesCheckBox; @@ -426,18 +497,24 @@ public class FlatSystemFileChooserMacTest private FlatTriStateCheckBox extensionHiddenCheckBox; private FlatTriStateCheckBox allowsOtherFileTypesCheckBox; private FlatTriStateCheckBox treatsFilePackagesAsDirectoriesCheckBox; + private JLabel options3Label; + private JCheckBox showSingleFilterFieldCheckBox; private JLabel promptLabel; private JTextField promptField; private JLabel messageLabel; private JTextField messageField; + private JLabel filterFieldLabelLabel; + private JTextField filterFieldLabelField; 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 JLabel fileTypesLabel; + private JComboBox fileTypesField; + private JLabel fileTypeIndexLabel; + private JSlider fileTypeIndexSlider; private JButton openButton; private JButton saveButton; private JButton openDirectButton; 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 65c30bc1..02b47359 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 @@ -6,7 +6,7 @@ new FormModel { 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]" + "$rowConstraints": "[][][][][][][][][][][][grow,fill]" } ) { name: "this" add( new FormComponent( "javax.swing.JLabel" ) { @@ -23,7 +23,7 @@ new FormModel { 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[]" + "$rowConstraints": "[][]0[]0[]0[]0[]para[][]0[]0[]0[]0[]0[]0[]para[][]" } ) { name: "panel1" add( new FormComponent( "javax.swing.JLabel" ) { @@ -58,56 +58,74 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 4" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "accessoryViewDisclosedCheckBox" + "text": "accessoryViewDisclosed" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "options2Label" "text": "NSOpenPanel and NSSavePanel options:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5" + "value": "cell 0 6" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "showsTagFieldCheckBox" "text": "showsTagField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6" + "value": "cell 0 7" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "canCreateDirectoriesCheckBox" "text": "canCreateDirectories" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 7" + "value": "cell 0 8" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "canSelectHiddenExtensionCheckBox" "text": "canSelectHiddenExtension" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 8" + "value": "cell 0 9" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "showsHiddenFilesCheckBox" "text": "showsHiddenFiles" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 9" + "value": "cell 0 10" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "extensionHiddenCheckBox" "text": "extensionHidden" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 10" + "value": "cell 0 11" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "allowsOtherFileTypesCheckBox" "text": "allowsOtherFileTypes" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 11" + "value": "cell 0 12" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "treatsFilePackagesAsDirectoriesCheckBox" "text": "treatsFilePackagesAsDirectories" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 12" + "value": "cell 0 13" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "options3Label" + "text": "Custom options:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 14" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showSingleFilterFieldCheckBox" + "text": "showSingleFilterField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 15" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 0 1 8,aligny top,growy 0" + "value": "cell 2 0 1 10,aligny top,growy 0" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "promptLabel" @@ -132,76 +150,112 @@ new FormModel { "value": "cell 1 2" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "nameFieldLabelLabel" - "text": "nameFieldLabel" + name: "filterFieldLabelLabel" + "text": "filterFieldLabel" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) add( new FormComponent( "javax.swing.JTextField" ) { - name: "nameFieldLabelField" + name: "filterFieldLabelField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 3" } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "nameFieldLabelLabel" + "text": "nameFieldLabel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "nameFieldLabelField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "nameFieldStringValueLabel" "text": "nameFieldStringValue" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" + "value": "cell 0 5" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "nameFieldStringValueField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 4" + "value": "cell 1 5" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "directoryURLLabel" "text": "directoryURL" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5" + "value": "cell 0 6" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "directoryURLField" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 5" + "value": "cell 1 6" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "allowedFileTypesLabel" - "text": "allowedFileTypes" + name: "fileTypesLabel" + "text": "fileTypes" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6" + "value": "cell 0 7" } ) - add( new FormComponent( "javax.swing.JTextField" ) { - name: "allowedFileTypesField" + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "fileTypesField" + "editable": true + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "Text Files,txt,null" + addElement( "Text Files,txt,null" ) + addElement( "All Files,*,null" ) + addElement( "Text Files,txt,null,PDF Files,pdf,null,All Files,*,null" ) + addElement( "Text and PDF Files,txt,pdf,null" ) + addElement( "Compressed,zip,gz,null,Disk Images,dmg,null" ) + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 6" + "value": "cell 1 7" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypeIndexLabel" + "text": "fileTypeIndex" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "fileTypeIndexSlider" + "maximum": 10 + "majorTickSpacing": 1 + "value": 0 + "paintLabels": true + "snapToTicks": true + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 8" } ) 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" + "value": "cell 0 10 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" + "value": "cell 0 10 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" + "value": "cell 0 10 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" + "value": "cell 0 10 3 1" } ) add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "filesScrollPane" @@ -210,11 +264,11 @@ new FormModel { "rows": 8 } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 9 3 1,growx" + "value": "cell 0 11 3 1,growx" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 535, 465 ) + "size": new java.awt.Dimension( 750, 475 ) } ) } } From d7462bd42460b8129f4bbb9378f384e9dcf5046d Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 7 Jan 2025 19:20:40 +0100 Subject: [PATCH 11/34] System File Chooser: use Cocoa autolayout for "Format" label and combobox on macOS --- .../src/main/objcpp/MacFileChooser.mm | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) 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 8a6615ec..13099a4c 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -58,7 +58,7 @@ } // create label - NSTextField* label = [[NSTextField alloc] initWithFrame:NSMakeRect( 0, 0, 60, 22 )]; + NSTextField* label = [[NSTextField alloc] initWithFrame:NSZeroRect]; label.stringValue = (filterFieldLabel != NULL) ? filterFieldLabel : @"Format:"; label.editable = NO; label.bordered = NO; @@ -66,17 +66,37 @@ label.drawsBackground = NO; // create combobox - NSPopUpButton* popupButton = [[NSPopUpButton alloc] initWithFrame:NSMakeRect( 50, 2, 140, 22 ) pullsDown:NO]; + NSPopUpButton* popupButton = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO]; [popupButton addItemsWithTitles:filterNames]; [popupButton selectItemAtIndex:MIN( MAX( filterIndex, 0 ), filterNames.count - 1 )]; [popupButton setTarget:self]; [popupButton setAction:@selector(selectFormat:)]; // create view - NSView* accessoryView = [[NSView alloc] initWithFrame:NSMakeRect( 0, 0, 200, 32 )]; + NSView* accessoryView = [[NSView alloc] initWithFrame:NSZeroRect]; [accessoryView addSubview:label]; [accessoryView addSubview:popupButton]; + // autolayout + label.translatesAutoresizingMaskIntoConstraints = NO; + popupButton.translatesAutoresizingMaskIntoConstraints = NO; + int labelWidth = label.intrinsicContentSize.width; + int gap = 12; + int popupButtonWidth = popupButton.intrinsicContentSize.width; + int popupButtonMinimumWidth = 140; + int totalWidth = labelWidth + gap + MAX( popupButtonWidth, popupButtonMinimumWidth ); + [accessoryView addConstraints:@[ + // horizontal layout + [label.leadingAnchor constraintEqualToAnchor:accessoryView.centerXAnchor constant:-(totalWidth / 2)], + [popupButton.leadingAnchor constraintEqualToAnchor:label.trailingAnchor constant:gap], + [popupButton.widthAnchor constraintGreaterThanOrEqualToConstant:popupButtonMinimumWidth], + + // vertical layout + [popupButton.topAnchor constraintEqualToAnchor:accessoryView.topAnchor constant:8], + [popupButton.bottomAnchor constraintEqualToAnchor:accessoryView.bottomAnchor constant:-8], + [label.firstBaselineAnchor constraintEqualToAnchor:popupButton.firstBaselineAnchor], + ]]; + [_dialog setAccessoryView:accessoryView]; // initial filter From 251198c66dca307e94a9a53a3ba9e30ee66a176b Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 7 Jan 2025 19:49:04 +0100 Subject: [PATCH 12/34] Native Libraries: - made C methods `static` (similar to `private` in Java) to avoid that they are added (exported) to shared library symbol table - macOS and Linux: added `-fvisibility=hidden` to compiler options to mark C methods hidden by default --- .../flatlaf-natives-linux/build.gradle.kts | 27 ++++++++++++++++++- .../src/main/cpp/GtkFileChooser.cpp | 10 +++---- .../src/main/cpp/X11WmUtils.cpp | 13 +++++---- .../flatlaf-natives-macos/build.gradle.kts | 2 +- .../src/main/objcpp/MacFileChooser.mm | 6 ++--- .../src/main/objcpp/MacWindow.mm | 24 +++++++++-------- .../flatlaf-natives-windows/build.gradle.kts | 9 +++++++ .../src/main/cpp/FlatWndProc.cpp | 1 + .../src/main/cpp/WinFileChooser.cpp | 8 +++--- .../src/main/cpp/WinWrapper.cpp | 4 +-- 10 files changed, 72 insertions(+), 32 deletions(-) diff --git a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts index e541d58e..f14fb260 100644 --- a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts @@ -14,6 +14,8 @@ * limitations under the License. */ +import java.io.FileOutputStream + plugins { `cpp-library` `flatlaf-cpp-library` @@ -67,7 +69,7 @@ tasks { compilerArgs.addAll( toolChain.map { when( it ) { - is Gcc, is Clang -> listOf() + is Gcc, is Clang -> listOf( "-fvisibility=hidden" ) else -> emptyList() } } ) @@ -108,6 +110,29 @@ tasks { into( nativesDir ) rename( "libflatlaf-natives-linux.so", libraryName ) } + +/*dump + val dylib = linkedFile.asFile.get() + val dylibDir = dylib.parent + exec { commandLine( "size", dylib ) } + exec { + commandLine( "objdump", + // commands + "--archive-headers", + "--section-headers", + "--private-headers", + "--reloc", + "--dynamic-reloc", + "--syms", + // options +// "--private-header", + // files + dylib ) + standardOutput = FileOutputStream( "$dylibDir/objdump.txt" ) + } + exec { commandLine( "objdump", "--disassemble-all", dylib ); standardOutput = FileOutputStream( "$dylibDir/disassemble.txt" ) } + exec { commandLine( "objdump", "--full-contents", dylib ); standardOutput = FileOutputStream( "$dylibDir/full-contents.txt" ) } +dump*/ } } } diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp index 858ef1b0..567c485c 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp @@ -26,8 +26,8 @@ * @since 3.6 */ -// see X11WmUtils.cpp -Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display** display_return ); +// declare external methods +extern Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display** display_return ); //---- class AutoReleaseStringUTF8 -------------------------------------------- @@ -55,12 +55,12 @@ public: #define isOptionClear( option ) ((optionsClear & com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_ ## option) != 0) #define isOptionSetOrClear( option ) isOptionSet( option ) || isOptionClear( option ) -jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { +static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { jclass stringClass = env->FindClass( "java/lang/String" ); return env->NewObjectArray( count, stringClass, NULL ); } -void initFilters( GtkFileChooser* chooser, JNIEnv* env, jint fileTypeIndex, jobjectArray fileTypes ) { +static void initFilters( GtkFileChooser* chooser, JNIEnv* env, jint fileTypeIndex, jobjectArray fileTypes ) { jint length = env->GetArrayLength( fileTypes ); if( length <= 0 ) return; @@ -89,7 +89,7 @@ void initFilters( GtkFileChooser* chooser, JNIEnv* env, jint fileTypeIndex, jobj } } -GdkWindow* getGdkWindow( JNIEnv* env, jobject window ) { +static GdkWindow* getGdkWindow( JNIEnv* env, jobject window ) { // get the AWT JAWT awt; awt.version = JAWT_VERSION_1_4; diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp index 0f01b693..3fc604f1 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp @@ -25,11 +25,14 @@ */ -bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, - long data0, long data1, long data2, long data3, long data4 ); -bool isWMHintSupported( Display* display, Window rootWindow, Atom atom ); +// declare exported methods Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display** display_return ); +// declare internal methods +static bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, + long data0, long data1, long data2, long data3, long data4 ); +static bool isWMHintSupported( Display* display, Window rootWindow, Atom atom ); + //---- JNI methods ------------------------------------------------------------ @@ -79,7 +82,7 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xS 0 ); } -bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, +static bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, long data0, long data1, long data2, long data3, long data4 ) { // get the AWT @@ -131,7 +134,7 @@ bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, } -bool isWMHintSupported( Display* display, Window rootWindow, Atom atom ) { +static bool isWMHintSupported( Display* display, Window rootWindow, Atom atom ) { Atom type; int format; unsigned long n_atoms; diff --git a/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts b/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts index 5b542d3a..3f943a57 100644 --- a/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts @@ -72,7 +72,7 @@ tasks { compilerArgs.addAll( toolChain.map { when( it ) { - is Gcc, is Clang -> listOf( "-x", "objective-c++", "-mmacosx-version-min=$minOs" ) + is Gcc, is Clang -> listOf( "-x", "objective-c++", "-mmacosx-version-min=$minOs", "-fvisibility=hidden" ) else -> emptyList() } } ) 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 13099a4c..d07caec2 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -126,10 +126,10 @@ #define isOptionClear( option ) ((optionsClear & com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ## option) != 0) #define isOptionSetOrClear( option ) isOptionSet( option ) || isOptionClear( option ) -// declare methods -NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window ); +// declare external methods +extern NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window ); -jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { +static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { jclass stringClass = env->FindClass( "java/lang/String" ); return env->NewObjectArray( count, stringClass, NULL ); } diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm index bddb9326..c309260a 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm @@ -39,13 +39,15 @@ @implementation WindowData @end -// declare internal methods +// declare exported methods NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window ); -WindowData* getWindowData( NSWindow* nsWindow, bool allocate ); -void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ); -int getWindowButtonAreaWidth( NSWindow* nsWindow ); -int getWindowTitleBarHeight( NSWindow* nsWindow ); -bool isWindowFullScreen( NSWindow* nsWindow ); + +// declare internal methods +static WindowData* getWindowData( NSWindow* nsWindow, bool allocate ); +static void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ); +static int getWindowButtonAreaWidth( NSWindow* nsWindow ); +static int getWindowTitleBarHeight( NSWindow* nsWindow ); +static bool isWindowFullScreen( NSWindow* nsWindow ); NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window ) { @@ -79,7 +81,7 @@ NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window ) { return (NSWindow *) jlong_to_ptr( env->GetLongField( platformWindow, ptrID ) ); } -WindowData* getWindowData( NSWindow* nsWindow, bool allocate ) { +static WindowData* getWindowData( NSWindow* nsWindow, bool allocate ) { static char key; WindowData* windowData = objc_getAssociatedObject( nsWindow, &key ); if( windowData == NULL && allocate ) { @@ -243,7 +245,7 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_setW return FALSE; } -void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ) { +static void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ) { // get buttons NSView* buttons[3] = { [nsWindow standardWindowButton:NSWindowCloseButton], @@ -303,7 +305,7 @@ JNIEXPORT jobject JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_getWi return NULL; } -int getWindowButtonAreaWidth( NSWindow* nsWindow ) { +static int getWindowButtonAreaWidth( NSWindow* nsWindow ) { // get buttons NSView* buttons[3] = { [nsWindow standardWindowButton:NSWindowCloseButton], @@ -335,7 +337,7 @@ int getWindowButtonAreaWidth( NSWindow* nsWindow ) { return right + left; } -int getWindowTitleBarHeight( NSWindow* nsWindow ) { +static int getWindowTitleBarHeight( NSWindow* nsWindow ) { NSView* closeButton = [nsWindow standardWindowButton:NSWindowCloseButton]; if( closeButton == NULL ) return -1; @@ -360,7 +362,7 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_isWi return FALSE; } -bool isWindowFullScreen( NSWindow* nsWindow ) { +static bool isWindowFullScreen( NSWindow* nsWindow ) { return ((nsWindow.styleMask & NSWindowStyleMaskFullScreen) != 0); } diff --git a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts index 550179d1..3dc0a1dc 100644 --- a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts @@ -93,6 +93,15 @@ tasks { into( nativesDir ) rename( "flatlaf-natives-windows.dll", libraryName ) } + +/*dump + val dumpbin = "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.42.34433/bin/Hostx64/x64/dumpbin.exe" + val dll = linkedFile.asFile.get() + val dllDir = dll.parent + exec { commandLine( dumpbin, "/all", "/rawdata:none", "/out:$dllDir/objdump.txt", dll ) } + exec { commandLine( dumpbin, "/all", "/out:$dllDir/full-contents.txt", dll ) } + exec { commandLine( dumpbin, "/disasm", "/out:$dllDir/disassemble.txt", dll ) } +dump*/ } } } diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp index d45f758a..d2e1ab2b 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp @@ -30,6 +30,7 @@ * @author Karl Tauber */ +// declare exported methods HWND getWindowHandle( JNIEnv* env, jobject window ); //---- JNI methods ------------------------------------------------------------ diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp index 613cb072..8b300d34 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp @@ -26,8 +26,8 @@ * @since 3.6 */ -// see FlatWndProc.cpp -HWND getWindowHandle( JNIEnv* env, jobject window ); +// declare external methods +extern HWND getWindowHandle( JNIEnv* env, jobject window ); //---- class AutoReleasePtr --------------------------------------------------- @@ -147,12 +147,12 @@ public: #define isOptionSet( option ) ((optionsSet & com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_ ## option) != 0) #define CHECK_HRESULT( code ) { if( (code) != S_OK ) return NULL; } -jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { +static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { jclass stringClass = env->FindClass( "java/lang/String" ); return env->NewObjectArray( count, stringClass, NULL ); } -jstring newJavaString( JNIEnv* env, LPWSTR str ) { +static jstring newJavaString( JNIEnv* env, LPWSTR str ) { return env->NewString( reinterpret_cast( str ), static_cast( wcslen( str ) ) ); } diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinWrapper.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinWrapper.cpp index c1276973..b77ef1d5 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinWrapper.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinWrapper.cpp @@ -25,8 +25,8 @@ * @author Karl Tauber */ -// see FlatWndProc.cpp -HWND getWindowHandle( JNIEnv* env, jobject window ); +// declare external methods +extern HWND getWindowHandle( JNIEnv* env, jobject window ); //---- Utility ---------------------------------------------------------------- From c73fd5170490950c75a11c565c4dc778520d6751 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 8 Jan 2025 15:14:50 +0100 Subject: [PATCH 13/34] System File Chooser: support filename extension filters --- .../flatlaf/ui/FlatNativeMacLibrary.java | 2 +- .../flatlaf/ui/FlatNativeWindowsLibrary.java | 4 +- .../flatlaf/util/SystemFileChooser.java | 376 +++++++++++++++--- .../com/formdev/flatlaf/demo/DemoFrame.java | 12 + .../com/formdev/flatlaf/demo/DemoFrame.jfd | 2 + .../testing/FlatSystemFileChooserTest.java | 17 +- 6 files changed, 363 insertions(+), 50 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 3615ef20..9308a680 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 @@ -112,7 +112,7 @@ public class FlatNativeMacLibrary * @param fileTypes file types that the dialog can open or save. * Two or more strings and {@code null} are required for each filter. * First string is the display name of the filter shown in the combobox (e.g. "Text Files"). - * Subsequent strings are the filter patterns (e.g. "*.txt" or "*"). + * Subsequent strings are the filter patterns (e.g. "txt" or "*"). * {@code null} is required to mark end of filter. * @return file path(s) that the user selected; an empty array if canceled; * or {@code null} on failures (no dialog shown) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java index f0a769aa..8dfa52a4 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java @@ -204,8 +204,8 @@ public class FlatNativeWindowsLibrary * @param owner the owner of the file dialog; or {@code null} * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog * @param title text displayed in dialog title; or {@code null} - * @param okButtonLabel text displayed in default button; or {@code null}. Use '&' for mnemonics (e.g. "&Choose"). - * Use '&&' for '&' character (e.g. "Choose && Quit"). + * @param okButtonLabel text displayed in default button; or {@code null}. Use '&' for mnemonics (e.g. "&Choose"). + * Use '&&' for '&' character (e.g. "Choose && Quit"). * @param fileNameLabel text displayed in front of the filename text field; or {@code null} * @param fileName user-editable filename currently shown in the filename field; or {@code null} * @param folder current directory shown in the dialog; or {@code null} 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 dea48265..6801014e 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 @@ -22,6 +22,7 @@ import java.awt.Toolkit; import java.awt.Window; import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.Locale; import java.util.concurrent.atomic.AtomicReference; import javax.swing.JFileChooser; @@ -37,6 +38,12 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; /** * Gives access to operating system file dialogs. *

    + * There are some limitations and incompatibilities to {@link JFileChooser} because + * operating system file dialogs do not offer all features that {@code JFileChooser} provides. + * On the other hand, operating system file dialogs offer features out of the box + * that {@code JFileChooser} do not offer (e.g. ask for overwrite on save). + * So this class offers only features that are available on all platforms. + *

    * The API is (mostly) compatible with {@link JFileChooser}. * To use this class in existing code, do a string replace from {@code JFileChooser} to {@code SystemFileChooser}. * If there are no compile errors, then there is a good chance that it works without further changes. @@ -48,8 +55,7 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; * {@code SystemFileChooser} requires FlatLaf native libraries (usually contained in flatlaf.jar). * If not available or disabled (via {@link FlatSystemProperties#USE_NATIVE_LIBRARY} * or {@link FlatSystemProperties#USE_SYSTEM_FILE_CHOOSER}), then {@code JFileChooser} is used. - *

    - *

    + * *

    Limitations/incompatibilities compared to JFileChooser

    * *
      @@ -67,6 +73,14 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; *
    • {@link JFileChooser#FILES_AND_DIRECTORIES} is not supported. *
    • {@link #getSelectedFiles()} returns selected file also in single selection mode. * {@link JFileChooser#getSelectedFiles()} only in multi selection mode. + *
    • Only file name extension filters (see {@link FileNameExtensionFilter}) are supported. + *
    • If adding choosable file filters and {@link #isAcceptAllFileFilterUsed()} is {@code true}, + * then the All Files filter is placed at the end of the combobox list + * (as usual in current operating systems) and the first choosable filter is selected by default. + * {@code JFileChooser}, on the other hand, adds All Files filter + * as first item and selects it by default. + * Use {@code chooser.addChoosableFileFilter( chooser.getAcceptAllFileFilter() )} + * to place All Files filter somewhere else. *
    * * @author Karl Tauber @@ -92,17 +106,30 @@ public class SystemFileChooser /** @see JFileChooser#DIRECTORIES_ONLY */ public static final int DIRECTORIES_ONLY = JFileChooser.DIRECTORIES_ONLY; - private int dialogType = OPEN_DIALOG; - private String dialogTitle; - private String approveButtonText; - private int approveButtonMnemonic = 0; - private int fileSelectionMode = FILES_ONLY; - private boolean multiSelection; - private boolean useFileHiding = true; + private int dialogType = OPEN_DIALOG; + private String dialogTitle; + private String approveButtonText; + private int approveButtonMnemonic = 0; + private int fileSelectionMode = FILES_ONLY; + private boolean multiSelection; + private boolean useFileHiding = true; - private File currentDirectory; - private File selectedFile; - private File[] selectedFiles; + private File currentDirectory; + private File selectedFile; + private File[] selectedFiles; + + private final ArrayList filters = new ArrayList<>(); + private FileFilter fileFilter; + private AcceptAllFileFilter acceptAllFileFilter; + private boolean useAcceptAllFileFilter = true; + + /** + * If {@code fc.addChoosableFileFilter(fc.getAcceptAllFileFilter())} is invoked from user code, + * then this flag is set to {@code false} and subsequent invocations of {@code fc.addChoosableFileFilter(...)} + * no longer insert added filters before the "All Files" filter. + * This allows custom ordering the "All Files" filter. + */ + private boolean keepAcceptAllAtEnd = true; /** @see JFileChooser#JFileChooser() */ public SystemFileChooser() { @@ -111,7 +138,7 @@ public class SystemFileChooser /** @see JFileChooser#JFileChooser(String) */ public SystemFileChooser( String currentDirectoryPath ) { - setCurrentDirectory( (currentDirectoryPath != null) + this( (currentDirectoryPath != null) ? FileSystemView.getFileSystemView().createFileObject( currentDirectoryPath ) : null ); } @@ -119,6 +146,9 @@ public class SystemFileChooser /** @see JFileChooser#JFileChooser(File) */ public SystemFileChooser( File currentDirectory ) { setCurrentDirectory( currentDirectory ); + + addChoosableFileFilter( getAcceptAllFileFilter() ); + keepAcceptAllAtEnd = true; } /** @see JFileChooser#showOpenDialog(Component) */ @@ -282,6 +312,101 @@ public class SystemFileChooser } } + /** @see JFileChooser#getChoosableFileFilters() */ + public FileFilter[] getChoosableFileFilters() { + return filters.toArray( new FileFilter[filters.size()] ); + } + + /** @see JFileChooser#addChoosableFileFilter(javax.swing.filechooser.FileFilter) */ + public void addChoosableFileFilter( FileFilter filter ) { + if( filter == getAcceptAllFileFilter() ) + keepAcceptAllAtEnd = false; + + if( filter == null || filters.contains( filter ) ) + return; + + if( !(filter instanceof FileNameExtensionFilter) && !(filter instanceof AcceptAllFileFilter) ) + throw new IllegalArgumentException( "Filter class not supported: " + filter.getClass().getName() ); + + // either insert filter before "All Files" filter, or append to the end + int size = filters.size(); + if( keepAcceptAllAtEnd && size > 0 && (filters.get( size - 1 ) == getAcceptAllFileFilter()) ) + filters.add( size - 1, filter ); + else + filters.add( filter ); + + // initialize current filter + if( fileFilter == null || (filters.size() == 2 && filters.get( 1 ) == getAcceptAllFileFilter()) ) + setFileFilter( filter ); + } + + /** @see JFileChooser#removeChoosableFileFilter(javax.swing.filechooser.FileFilter) */ + public boolean removeChoosableFileFilter( FileFilter filter ) { + if( !filters.remove( filter ) ) + return false; + + // update current filter if necessary + if( filter == getFileFilter() ) { + if( isAcceptAllFileFilterUsed() && filter != getAcceptAllFileFilter() ) + setFileFilter( getAcceptAllFileFilter() ); + else + setFileFilter( !filters.isEmpty() ? filters.get( 0 ) : null ); + } + + return true; + } + + /** @see JFileChooser#resetChoosableFileFilters() */ + public void resetChoosableFileFilters() { + filters.clear(); + setFileFilter( null ); + if( isAcceptAllFileFilterUsed() ) { + addChoosableFileFilter( getAcceptAllFileFilter() ); + keepAcceptAllAtEnd = true; + } + } + + /** @see JFileChooser#getAcceptAllFileFilter() */ + public FileFilter getAcceptAllFileFilter() { + if( acceptAllFileFilter == null ) + acceptAllFileFilter = new AcceptAllFileFilter(); + return acceptAllFileFilter; + } + + /** @see JFileChooser#isAcceptAllFileFilterUsed() */ + public boolean isAcceptAllFileFilterUsed() { + return useAcceptAllFileFilter; + } + + /** @see JFileChooser#setAcceptAllFileFilterUsed(boolean) */ + public void setAcceptAllFileFilterUsed( boolean acceptAll ) { + useAcceptAllFileFilter = acceptAll; + + removeChoosableFileFilter( getAcceptAllFileFilter() ); + if( acceptAll ) { + addChoosableFileFilter( getAcceptAllFileFilter() ); + keepAcceptAllAtEnd = true; + } + } + + /** @see JFileChooser#getFileFilter() */ + public FileFilter getFileFilter() { + return fileFilter; + } + + /** @see JFileChooser#setFileFilter(javax.swing.filechooser.FileFilter) */ + public void setFileFilter( FileFilter filter ) { + this.fileFilter = filter; + } + + private int indexOfCurrentFilter() { + return filters.indexOf( fileFilter ); + } + + private boolean hasOnlyAcceptAll() { + return filters.size() == 1 && filters.get( 0 ) == getAcceptAllFileFilter(); + } + private int showDialogImpl( Component parent ) { File[] files = getProvider().showDialog( parent, this ); setSelectedFiles( files ); @@ -401,13 +526,25 @@ public class SystemFileChooser // filter int fileTypeIndex = 0; ArrayList fileTypes = new ArrayList<>(); - // FOS_PICKFOLDERS does not support file types if( !fc.isDirectorySelectionEnabled() ) { + if( !fc.hasOnlyAcceptAll() ) { + fileTypeIndex = fc.indexOfCurrentFilter(); + for( FileFilter filter : fc.getChoosableFileFilters() ) { + if( filter instanceof FileNameExtensionFilter ) { + fileTypes.add( filter.getDescription() ); + fileTypes.add( "*." + String.join( ";*.", ((FileNameExtensionFilter)filter).getExtensions() ) ); + } else if( filter instanceof AcceptAllFileFilter ) { + fileTypes.add( filter.getDescription() ); + fileTypes.add( "*.*" ); + } + } + } + // if there are no file types // - for Save dialog add "All Files", otherwise Windows would show an empty "Save as type" combobox // - for Open dialog, Windows hides the combobox if( !open && fileTypes.isEmpty() ) { - fileTypes.add( UIManager.getString( "FileChooser.acceptAllFileFilterText" ) ); + fileTypes.add( fc.getAcceptAllFileFilter().getDescription() ); fileTypes.add( "*.*" ); } } @@ -457,11 +594,30 @@ public class SystemFileChooser if( !fc.isFileHidingEnabled() ) optionsSet |= FlatNativeMacLibrary.FC_showsHiddenFiles; + // filter + int fileTypeIndex = 0; + ArrayList fileTypes = new ArrayList<>(); + if( !fc.isDirectorySelectionEnabled() && !fc.hasOnlyAcceptAll() ) { + fileTypeIndex = fc.indexOfCurrentFilter(); + for( FileFilter filter : fc.getChoosableFileFilters() ) { + if( filter instanceof FileNameExtensionFilter ) { + fileTypes.add( filter.getDescription() ); + for( String ext : ((FileNameExtensionFilter)filter).getExtensions() ) + fileTypes.add( ext ); + fileTypes.add( null ); + } else if( filter instanceof AcceptAllFileFilter ) { + fileTypes.add( filter.getDescription() ); + fileTypes.add( "*" ); + fileTypes.add( null ); + } + } + } + // show system file dialog return FlatNativeMacLibrary.showFileChooser( open, fc.getDialogTitle(), fc.getApproveButtonText(), null, null, null, nameFieldStringValue, directoryURL, - optionsSet, optionsClear, 0 ); + optionsSet, optionsClear, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); } } @@ -515,10 +671,46 @@ public class SystemFileChooser else // necessary because GTK seems to be remember last state and re-use it for new file dialogs optionsClear |= FlatNativeLinuxLibrary.FC_show_hidden; + // filter + int fileTypeIndex = 0; + ArrayList fileTypes = new ArrayList<>(); + if( !fc.isDirectorySelectionEnabled() && !fc.hasOnlyAcceptAll() ) { + fileTypeIndex = fc.indexOfCurrentFilter(); + for( FileFilter filter : fc.getChoosableFileFilters() ) { + if( filter instanceof FileNameExtensionFilter ) { + fileTypes.add( filter.getDescription() ); + for( String ext : ((FileNameExtensionFilter)filter).getExtensions() ) + fileTypes.add( caseInsensitiveGlobPattern( ext ) ); + fileTypes.add( null ); + } else if( filter instanceof AcceptAllFileFilter ) { + fileTypes.add( filter.getDescription() ); + fileTypes.add( "*" ); + fileTypes.add( null ); + } + } + } + // show system file dialog return FlatNativeLinuxLibrary.showFileChooser( owner, open, fc.getDialogTitle(), approveButtonText, currentName, currentFolder, - optionsSet, optionsClear, 0 ); + optionsSet, optionsClear, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); + } + + private String caseInsensitiveGlobPattern( String ext ) { + StringBuilder buf = new StringBuilder(); + buf.append( "*." ); + int len = ext.length(); + for( int i = 0; i < len; i++ ) { + char ch = ext.charAt( i ); + if( Character.isLetter( ch ) ) { + buf.append( '[' ) + .append( Character.toLowerCase( ch ) ) + .append( Character.toUpperCase( ch ) ) + .append( ']' ); + } else + buf.append( ch ); + } + return buf.toString(); } } @@ -561,6 +753,27 @@ public class SystemFileChooser !chooser.isDirectorySelectionEnabled() ) chooser.setMultiSelectionEnabled( false ); + // filter + if( !fc.isDirectorySelectionEnabled() && !fc.hasOnlyAcceptAll() ) { + FileFilter currentFilter = fc.getFileFilter(); + for( FileFilter filter : fc.getChoosableFileFilters() ) { + javax.swing.filechooser.FileFilter jfilter = convertFilter( filter, chooser ); + if( jfilter == null ) + continue; + + chooser.addChoosableFileFilter( jfilter ); + if( filter == currentFilter ) { + chooser.setFileFilter( jfilter ); + currentFilter = null; + } + } + if( currentFilter != null ) { + javax.swing.filechooser.FileFilter jfilter = convertFilter( currentFilter, chooser ); + if( jfilter != null ) + chooser.setFileFilter( jfilter ); + } + } + // paths chooser.setCurrentDirectory( fc.getCurrentDirectory() ); chooser.setSelectedFile( fc.getSelectedFile() ); @@ -572,40 +785,111 @@ public class SystemFileChooser ? chooser.getSelectedFiles() : new File[] { chooser.getSelectedFile() }; } + + private javax.swing.filechooser.FileFilter convertFilter( FileFilter filter, JFileChooser chooser ) { + if( filter instanceof FileNameExtensionFilter ) { + return new javax.swing.filechooser.FileNameExtensionFilter( + ((FileNameExtensionFilter)filter).getDescription(), + ((FileNameExtensionFilter)filter).getExtensions() ); + } else if( filter instanceof AcceptAllFileFilter ) + return chooser.getAcceptAllFileFilter(); + else + return null; + } + + private static boolean checkMustExist( JFileChooser chooser, File[] files ) { + for( File file : files ) { + if( !file.exists() ) { + String title = chooser.getDialogTitle(); + JOptionPane.showMessageDialog( chooser, + file.getName() + (chooser.isDirectorySelectionEnabled() + ? "\nPath does not exist.\nCheck the path and try again." + : "\nFile not found.\nCheck the file name and try again."), + (title != null) ? title : "Open", + JOptionPane.WARNING_MESSAGE ); + return false; + } + } + return true; + } + + private static boolean checkOverwrite( JFileChooser chooser, File[] files ) { + for( File file : files ) { + if( file.exists() ) { + String title = chooser.getDialogTitle(); + Locale l = chooser.getLocale(); + Object[] options = { + UIManager.getString( "OptionPane.yesButtonText", l ), + UIManager.getString( "OptionPane.noButtonText", l ), }; + int result = JOptionPane.showOptionDialog( chooser, + file.getName() + " already exists.\nDo you want to replace it?", + "Confirm " + (title != null ? title : "Save"), + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, + null, options, options[1] ); + return (result == 0); + } + } + return true; + } } - private static boolean checkMustExist( JFileChooser chooser, File[] files ) { - for( File file : files ) { - if( !file.exists() ) { - String title = chooser.getDialogTitle(); - JOptionPane.showMessageDialog( chooser, - file.getName() + (chooser.isDirectorySelectionEnabled() - ? "\nPath does not exist.\nCheck the path and try again." - : "\nFile not found.\nCheck the file name and try again."), - (title != null) ? title : "Open", - JOptionPane.WARNING_MESSAGE ); - return false; - } - } - return true; + //---- class FileFilter --------------------------------------------------- + + /** @see javax.swing.filechooser.FileFilter */ + public static abstract class FileFilter { + /** @see javax.swing.filechooser.FileFilter#getDescription() */ + public abstract String getDescription(); } - private static boolean checkOverwrite( JFileChooser chooser, File[] files ) { - for( File file : files ) { - if( file.exists() ) { - String title = chooser.getDialogTitle(); - Locale l = chooser.getLocale(); - Object[] options = { - UIManager.getString( "OptionPane.yesButtonText", l ), - UIManager.getString( "OptionPane.noButtonText", l ), }; - int result = JOptionPane.showOptionDialog( chooser, - file.getName() + " already exists.\nDo you want to replace it?", - "Confirm " + (title != null ? title : "Save"), - JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, - null, options, options[1] ); - return (result == 0); + //---- class FileNameExtensionFilter -------------------------------------- + + /** @see javax.swing.filechooser.FileNameExtensionFilter */ + public static final class FileNameExtensionFilter + extends FileFilter + { + private final String description; + private final String[] extensions; + + /** @see javax.swing.filechooser.FileNameExtensionFilter#FileNameExtensionFilter(String, String...) */ + public FileNameExtensionFilter( String description, String... extensions ) { + if( extensions == null || extensions.length == 0 ) + throw new IllegalArgumentException( "Missing extensions" ); + for( String extension : extensions ) { + if( extension == null || extension.isEmpty() ) + throw new IllegalArgumentException( "Extension is null or empty string" ); + if( extension.indexOf( '.' ) >= 0 || extension.indexOf( '*' ) >= 0 ) + throw new IllegalArgumentException( "Extension must not contain '.' or '*'" ); } + + this.description = description; + this.extensions = extensions.clone(); + } + + /** @see javax.swing.filechooser.FileNameExtensionFilter#getDescription() */ + @Override + public String getDescription() { + return description; + } + + /** @see javax.swing.filechooser.FileNameExtensionFilter#getExtensions() */ + public String[] getExtensions() { + return extensions.clone(); + } + + @Override + public String toString() { + return super.toString() + "[description=" + description + " extensions=" + Arrays.toString( extensions ) + "]"; + } + } + + //---- class AcceptAllFileFilter ------------------------------------------ + + private static final class AcceptAllFileFilter + extends FileFilter + { + @Override + public String getDescription() { + return UIManager.getString( "FileChooser.acceptAllFileFilterText" ); } - return true; } } diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java index 9ec5e815..088d3083 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java @@ -175,11 +175,21 @@ class DemoFrame private void openSystemActionPerformed() { SystemFileChooser chooser = new SystemFileChooser(); + chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( + "Text Files", "txt", "md" ) ); + chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( + "PDF Files", "pdf" ) ); + chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( + "Archives", "zip", "tar", "jar", "7z" ) ); chooser.showOpenDialog( this ); } private void saveAsSystemActionPerformed() { SystemFileChooser chooser = new SystemFileChooser(); + chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( + "Text Files", "txt", "md" ) ); + chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( + "Images", "png", "gif", "jpg" ) ); chooser.showSaveDialog( this ); } @@ -611,11 +621,13 @@ class DemoFrame //---- openSystemMenuItem ---- openSystemMenuItem.setText("Open (System)..."); + openSystemMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()|KeyEvent.SHIFT_DOWN_MASK)); openSystemMenuItem.addActionListener(e -> openSystemActionPerformed()); fileMenu.add(openSystemMenuItem); //---- saveAsSystemMenuItem ---- saveAsSystemMenuItem.setText("Save As (System)..."); + saveAsSystemMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()|KeyEvent.SHIFT_DOWN_MASK)); saveAsSystemMenuItem.addActionListener(e -> saveAsSystemActionPerformed()); fileMenu.add(saveAsSystemMenuItem); fileMenu.addSeparator(); diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd index 61243782..5cce720d 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd @@ -188,11 +188,13 @@ new FormModel { add( new FormComponent( "javax.swing.JMenuItem" ) { name: "openSystemMenuItem" "text": "Open (System)..." + "accelerator": static javax.swing.KeyStroke getKeyStroke( 79, 4291, false ) addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openSystemActionPerformed", false ) ) } ) add( new FormComponent( "javax.swing.JMenuItem" ) { name: "saveAsSystemMenuItem" "text": "Save As (System)..." + "accelerator": static javax.swing.KeyStroke getKeyStroke( 83, 4291, false ) addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "saveAsSystemActionPerformed", false ) ) } ) add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java index cb66fa12..6d2102a6 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java @@ -186,7 +186,22 @@ public class FlatSystemFileChooserTest else System.setProperty( FlatSystemProperties.USE_SYSTEM_FILE_CHOOSER, "false" ); - //TODO filter + // filter + String fileTypesStr = n( (String) fileTypesField.getSelectedItem() ); + String[] fileTypes = {}; + if( fileTypesStr != null ) + fileTypes = fileTypesStr.trim().split( "[,]+" ); + int fileTypeIndex = fileTypeIndexSlider.getValue(); + if( !useAcceptAllFileFilterCheckBox.isSelected() ) + fc.setAcceptAllFileFilterUsed( false ); + for( int i = 0; i < fileTypes.length; i += 2 ) { + fc.addChoosableFileFilter( "*".equals( fileTypes[i+1] ) + ? fc.getAcceptAllFileFilter() + : new SystemFileChooser.FileNameExtensionFilter( fileTypes[i], fileTypes[i+1].split( ";" ) ) ); + } + SystemFileChooser.FileFilter[] filters = fc.getChoosableFileFilters(); + if( filters.length > 0 ) + fc.setFileFilter( filters[Math.min( Math.max( fileTypeIndex, 0 ), filters.length - 1 )] ); } private void configureSwingFileChooser( JFileChooser fc ) { From d49282dfe8207996a2b4c7c1b0df45df0a53fdad Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 11 Jan 2025 17:50:46 +0100 Subject: [PATCH 14/34] System File Chooser: support "approve" callback and system message dialog on Windows and Linux (not yet used in `SystemFileChooser` --- .../flatlaf/ui/FlatNativeLinuxLibrary.java | 35 +++++- .../flatlaf/ui/FlatNativeWindowsLibrary.java | 27 +++- .../flatlaf/util/SystemFileChooser.java | 6 +- .../src/main/cpp/GtkFileChooser.cpp | 95 ++++++++++++++- ...ormdev_flatlaf_ui_FlatNativeLinuxLibrary.h | 12 +- .../flatlaf-natives-windows/build.gradle.kts | 4 +- .../src/main/cpp/WinFileChooser.cpp | 115 +++++++++++++++++- ...mdev_flatlaf_ui_FlatNativeWindowsLibrary.h | 12 +- .../FlatSystemFileChooserLinuxTest.java | 20 ++- .../FlatSystemFileChooserLinuxTest.jfd | 8 +- .../FlatSystemFileChooserWindowsTest.java | 20 ++- .../FlatSystemFileChooserWindowsTest.jfd | 6 + 12 files changed, 335 insertions(+), 25 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java index f1bed502..5ea7d178 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java @@ -24,6 +24,7 @@ import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import javax.swing.JDialog; import javax.swing.JFrame; +import javax.swing.JOptionPane; import com.formdev.flatlaf.util.SystemInfo; /** @@ -131,7 +132,7 @@ public class FlatNativeLinuxLibrary FC_create_folders = 1 << 5; // default for Save /** - * Shows the Linux system file dialog + * Shows the Linux/GTK system file dialog * GtkFileChooserDialog. *

    * Uses {@code GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER} if {@link #FC_select_folder} is set in parameter {@code optionsSet}. @@ -151,6 +152,7 @@ public class FlatNativeLinuxLibrary * @param currentFolder current directory shown in the dialog; or {@code null} * @param optionsSet options to set; see {@code FOS_*} constants * @param optionsClear options to clear; see {@code FOS_*} constants + * @param callback approve callback; or {@code null} * @param fileTypeIndex the file type that appears as selected (zero-based) * @param fileTypes file types that the dialog can open or save. * Two or more strings and {@code null} are required for each filter. @@ -164,5 +166,34 @@ public class FlatNativeLinuxLibrary */ public native static String[] showFileChooser( Window owner, boolean open, String title, String okButtonLabel, String currentName, String currentFolder, - 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 GTK message box + * GtkMessageDialog. + *

    + * For use in {@link FileChooserCallback} only. + * + * @param hwndParent the parent of the message box + * @param messageType type of message being displayed: + * {@link JOptionPane#ERROR_MESSAGE}, {@link JOptionPane#INFORMATION_MESSAGE}, + * {@link JOptionPane#WARNING_MESSAGE}, {@link JOptionPane#QUESTION_MESSAGE} or + * {@link JOptionPane#PLAIN_MESSAGE} + * @param primaryText primary text; if the dialog has a secondary text, + * this will appear as title in a larger bold font + * @param secondaryText secondary text; shown below of primary text; 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; or -1 for ESC key + * + * @since 3.6 + */ + public native static int showMessageDialog( long hwndParent, int messageType, + String primaryText, String secondaryText, int defaultButton, String... buttons ); } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java index 8dfa52a4..ec2bed8f 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java @@ -218,6 +218,7 @@ public class FlatNativeWindowsLibrary * @param defaultExtension default extension to be added to file name in save dialog; or {@code null} * @param optionsSet options to set; see {@code FOS_*} constants * @param optionsClear options to clear; see {@code FOS_*} constants + * @param callback approve callback; or {@code null} * @param fileTypeIndex the file type that appears as selected (zero-based) * @param fileTypes file types that the dialog can open or save. * Pairs of strings are required for each filter. @@ -231,5 +232,29 @@ public class FlatNativeWindowsLibrary public native static String[] showFileChooser( Window owner, boolean open, String title, String okButtonLabel, String fileNameLabel, String fileName, String folder, String saveAsItem, String defaultFolder, String defaultExtension, - 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 Windows message box + * MessageBox. + *

    + * For use in {@link FileChooserCallback} only. + * + * @param hwndParent the parent of the message box + * @param text message to be displayed + * @param caption dialog box title + * @param type see MessageBox parameter uType + * @return see MessageBox Return value + * @return index of pressed button; or -1 for ESC key + * + * @since 3.6 + */ + public native static int showMessageDialog( long hwndParent, + String text, String caption, int type ); } 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 6801014e..b4528f62 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 @@ -450,7 +450,7 @@ public class SystemFileChooser new Thread( () -> { filenamesRef.set( showSystemDialog( owner, fc ) ); secondaryLoop.exit(); - } ).start(); + }, "FlatLaf SystemFileChooser" ).start(); secondaryLoop.enter(); String[] filenames = filenamesRef.get(); @@ -552,7 +552,7 @@ public class SystemFileChooser // show system file dialog return FlatNativeWindowsLibrary.showFileChooser( owner, open, fc.getDialogTitle(), approveButtonText, null, fileName, - folder, saveAsItem, null, null, optionsSet, optionsClear, + folder, saveAsItem, null, null, optionsSet, optionsClear, null, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); } } @@ -693,7 +693,7 @@ public class SystemFileChooser // show system file dialog return FlatNativeLinuxLibrary.showFileChooser( owner, open, fc.getDialogTitle(), approveButtonText, currentName, currentFolder, - optionsSet, optionsClear, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); + optionsSet, optionsClear, null, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); } private String caseInsensitiveGlobPattern( String ext ) { diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp index 567c485c..7e427a85 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp @@ -29,6 +29,9 @@ // declare external methods extern Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display** display_return ); +// declare internal methods +static jobjectArray fileListToStringArray( JNIEnv* env, GSList* fileList ); + //---- class AutoReleaseStringUTF8 -------------------------------------------- class AutoReleaseStringUTF8 { @@ -142,10 +145,37 @@ static void handle_realize( GtkWidget* dialog, gpointer data ) { g_object_unref( gdkOwner ); } +struct ResponseData { + JNIEnv* env; + jobject callback; + GSList* fileList; + + ResponseData( JNIEnv* _env, jobject _callback ) { + env = _env; + callback = _callback; + fileList = NULL; + } +}; + static void handle_response( GtkWidget* dialog, gint responseId, gpointer data ) { // get filenames if user pressed OK - if( responseId == GTK_RESPONSE_ACCEPT ) - *((GSList**)data) = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER( dialog ) ); + if( responseId == GTK_RESPONSE_ACCEPT ) { + ResponseData *response = static_cast( data ); + if( response->callback != NULL ) { + GSList* fileList = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER( dialog ) ); + jobjectArray files = fileListToStringArray( response->env, fileList ); + + GtkWindow* window = GTK_WINDOW( dialog ); + + // invoke callback: boolean approve( String[] files, long hwnd ); + jclass cls = response->env->GetObjectClass( response->callback ); + jmethodID approveID = response->env->GetMethodID( cls, "approve", "([Ljava/lang/String;J)Z" ); + if( approveID != NULL && !response->env->CallBooleanMethod( response->callback, approveID, files, window ) ) + return; // keep dialog open + } + + response->fileList = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER( dialog ) ); + } // hide/destroy file dialog and quit loop gtk_widget_hide( dialog ); @@ -159,7 +189,7 @@ extern "C" JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser ( JNIEnv* env, jclass cls, jobject owner, jboolean open, jstring title, jstring okButtonLabel, jstring currentName, jstring currentFolder, - jint optionsSet, jint optionsClear, jint fileTypeIndex, jobjectArray fileTypes ) + jint optionsSet, jint optionsClear, jobject callback, jint fileTypeIndex, jobjectArray fileTypes ) { // initialize GTK if( !gtk_init_check( NULL, NULL ) ) @@ -222,8 +252,8 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrar // show dialog // (similar to what's done in sun_awt_X11_GtkFileDialogPeer.c) - GSList* fileList = NULL; - g_signal_connect( dialog, "response", G_CALLBACK( handle_response ), &fileList ); + ResponseData responseData( env, callback ); + g_signal_connect( dialog, "response", G_CALLBACK( handle_response ), &responseData ); gtk_widget_show( dialog ); // necessary to bring file dialog to the front (and make it active) @@ -241,10 +271,14 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrar gtk_main(); // canceled? - if( fileList == NULL ) + if( responseData.fileList == NULL ) return newJavaStringArray( env, 0 ); // convert GSList to Java string array + return fileListToStringArray( env, responseData.fileList ); +} + +static jobjectArray fileListToStringArray( JNIEnv* env, GSList* fileList ) { guint count = g_slist_length( fileList ); jobjectArray array = newJavaStringArray( env, count ); GSList* it = fileList; @@ -259,3 +293,52 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrar g_slist_free( fileList ); return array; } + + +extern "C" +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showMessageDialog + ( JNIEnv* env, jclass cls, jlong hwndParent, jint messageType, jstring primaryText, jstring secondaryText, + jint defaultButton, jobjectArray buttons ) +{ + GtkWindow* window = (GtkWindow*) hwndParent; + + // convert message type + GtkMessageType gmessageType; + switch( messageType ) { + case /* JOptionPane.ERROR_MESSAGE */ 0: gmessageType = GTK_MESSAGE_ERROR; break; + case /* JOptionPane.INFORMATION_MESSAGE */ 1: gmessageType = GTK_MESSAGE_INFO; break; + case /* JOptionPane.WARNING_MESSAGE */ 2: gmessageType = GTK_MESSAGE_WARNING; break; + case /* JOptionPane.QUESTION_MESSAGE */ 3: gmessageType = GTK_MESSAGE_QUESTION; break; + default: + case /* JOptionPane.PLAIN_MESSAGE */ -1: gmessageType = GTK_MESSAGE_OTHER; break; + } + + // convert Java strings to C strings + AutoReleaseStringUTF8 cprimaryText( env, primaryText ); + AutoReleaseStringUTF8 csecondaryText( env, secondaryText ); + + // create GTK file chooser dialog + // https://docs.gtk.org/gtk3/class.MessageDialog.html + jint buttonCount = env->GetArrayLength( buttons ); + GtkWidget* dialog = gtk_message_dialog_new( window, GTK_DIALOG_MODAL, gmessageType, + (buttonCount > 0) ? GTK_BUTTONS_NONE : GTK_BUTTONS_OK, + "%s", (const gchar*) cprimaryText ); + if( csecondaryText != NULL ) + gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( dialog ), "%s", (const gchar*) csecondaryText ); + + // add buttons + for( int i = 0; i < buttonCount; i++ ) { + AutoReleaseStringUTF8 str( env, (jstring) env->GetObjectArrayElement( buttons, i ) ); + gtk_dialog_add_button( GTK_DIALOG( dialog ), str, i ); + } + + // set default button + gtk_dialog_set_default_response( GTK_DIALOG( dialog ), MIN( MAX( defaultButton, 0 ), buttonCount - 1 ) ); + + // show message dialog + gint responseID = gtk_dialog_run( GTK_DIALOG( dialog ) ); + gtk_widget_destroy( dialog ); + + // return -1 if closed with ESC key + return (responseID >= 0) ? responseID : -1; +} diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h index 4ca717bb..5e9573b4 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h @@ -40,10 +40,18 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xS /* * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary * Method: showFileChooser - * Signature: (Ljava/awt/Window;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;III[Ljava/lang/String;)[Ljava/lang/String; + * Signature: (Ljava/awt/Window;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILcom/formdev/flatlaf/ui/FlatNativeLinuxLibrary/FileChooserCallback;I[Ljava/lang/String;)[Ljava/lang/String; */ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser - (JNIEnv *, jclass, jobject, jboolean, jstring, jstring, jstring, jstring, jint, jint, jint, jobjectArray); + (JNIEnv *, jclass, jobject, jboolean, jstring, jstring, jstring, jstring, jint, jint, jobject, jint, jobjectArray); + +/* + * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary + * Method: showMessageDialog + * Signature: (JILjava/lang/String;Ljava/lang/String;I[Ljava/lang/String;)I + */ +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showMessageDialog + (JNIEnv *, jclass, jlong, jint, jstring, jstring, jint, jobjectArray); #ifdef __cplusplus } diff --git a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts index 3dc0a1dc..a0893c8c 100644 --- a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts @@ -64,7 +64,7 @@ tasks { compilerArgs.addAll( toolChain.map { when( it ) { is Gcc, is Clang -> listOf( "-O2", "-DUNICODE" ) - is VisualCpp -> listOf( "/O2", "/Zl", "/GS-", "/DUNICODE" ) + is VisualCpp -> listOf( "/O2", "/GS-", "/DUNICODE" ) else -> emptyList() } } ) @@ -81,7 +81,7 @@ tasks { linkerArgs.addAll( toolChain.map { when( it ) { is Gcc, is Clang -> listOf( "-lUser32", "-lGdi32", "-lshell32", "-lAdvAPI32", "-lKernel32", "-lDwmapi", "-lOle32", "-luuid" ) - is VisualCpp -> listOf( "User32.lib", "Gdi32.lib", "shell32.lib", "AdvAPI32.lib", "Kernel32.lib", "Dwmapi.lib", "Ole32.lib", "uuid.lib", "/NODEFAULTLIB" ) + is VisualCpp -> listOf( "User32.lib", "Gdi32.lib", "shell32.lib", "AdvAPI32.lib", "Kernel32.lib", "Dwmapi.lib", "Ole32.lib", "uuid.lib" ) else -> emptyList() } } ) diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp index 8b300d34..2a643643 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp @@ -29,6 +29,9 @@ // declare external methods extern HWND getWindowHandle( JNIEnv* env, jobject window ); +// declare internal methods +static jobjectArray getFiles( JNIEnv* env, jboolean open, IFileDialog* dialog ); + //---- class AutoReleasePtr --------------------------------------------------- template class AutoReleasePtr { @@ -38,6 +41,10 @@ public: AutoReleasePtr() { ptr = NULL; } + AutoReleasePtr( T* p ) { + ptr = p; + ptr->AddRef(); + } ~AutoReleasePtr() { if( ptr != NULL ) ptr->Release(); @@ -126,6 +133,86 @@ public: } }; +//---- class DialogEventHandler ----------------------------------------------- + +// see https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appplatform/commonfiledialog/CommonFileDialogApp.cpp + +class DialogEventHandler : public IFileDialogEvents { + JNIEnv* env; + jboolean open; + jobject callback; + LONG refCount = 1; + +public: + DialogEventHandler( JNIEnv* _env, jboolean _open, jobject _callback ) { + env = _env; + open = _open; + callback = _callback; + } + + //---- IFileDialogEvents methods ---- + + IFACEMETHODIMP OnFileOk( IFileDialog* dialog ) { + if( callback == NULL ) + return S_OK; + + // get files + jobjectArray files; + if( open ) { + AutoReleasePtr openDialog; + HRESULT hr = dialog->QueryInterface( &openDialog ); + files = SUCCEEDED( hr ) ? getFiles( env, true, openDialog ) : getFiles( env, false, dialog ); + } else + files = getFiles( env, false, dialog ); + + // get hwnd of file dialog + HWND hwndFileDialog = 0; + AutoReleasePtr window; + if( SUCCEEDED( dialog->QueryInterface( &window ) ) ) + window->GetWindow( &hwndFileDialog ); + + // 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 ) + return S_OK; + return env->CallBooleanMethod( callback, approveID, files, hwndFileDialog ) ? S_OK : S_FALSE; + } + + IFACEMETHODIMP OnFolderChange( IFileDialog* ) { return S_OK; } + IFACEMETHODIMP OnFolderChanging( IFileDialog*, IShellItem* ) { return S_OK; } + IFACEMETHODIMP OnHelp( IFileDialog* ) { return S_OK; } + IFACEMETHODIMP OnSelectionChange( IFileDialog* ) { return S_OK; } + IFACEMETHODIMP OnShareViolation( IFileDialog*, IShellItem*, FDE_SHAREVIOLATION_RESPONSE* ) { return S_OK; } + IFACEMETHODIMP OnTypeChange( IFileDialog*pfd ) { return S_OK; } + IFACEMETHODIMP OnOverwrite( IFileDialog*, IShellItem*, FDE_OVERWRITE_RESPONSE* ) { return S_OK; } + + //---- IUnknown methods ---- + + IFACEMETHODIMP QueryInterface( REFIID riid, void** ppv ) { + if( riid != IID_IFileDialogEvents && riid != IID_IUnknown ) + return E_NOINTERFACE; + + *ppv = static_cast( this ); + AddRef(); + return S_OK; + } + + IFACEMETHODIMP_(ULONG) AddRef() { + return InterlockedIncrement( &refCount ); + } + + IFACEMETHODIMP_(ULONG) Release() { + LONG newRefCount = InterlockedDecrement( &refCount ); + if( newRefCount == 0 ) + delete this; + return newRefCount; + } + +private: + ~DialogEventHandler() {} +}; + //---- class CoInitializer ---------------------------------------------------- class CoInitializer { @@ -163,7 +250,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr ( JNIEnv* env, jclass cls, jobject owner, jboolean open, jstring title, jstring okButtonLabel, jstring fileNameLabel, jstring fileName, jstring folder, jstring saveAsItem, jstring defaultFolder, jstring defaultExtension, - jint optionsSet, jint optionsClear, jint fileTypeIndex, jobjectArray fileTypes ) + jint optionsSet, jint optionsClear, jobject callback, jint fileTypeIndex, jobjectArray fileTypes ) { // initialize COM library CoInitializer coInitializer; @@ -226,20 +313,31 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr CHECK_HRESULT( dialog->SetFileTypeIndex( min( fileTypeIndex + 1, specs.count ) ) ); } + // add event handler + AutoReleasePtr handler( new DialogEventHandler( env, open, callback ) ); + DWORD dwCookie = 0; + CHECK_HRESULT( dialog->Advise( handler, &dwCookie ) ); + // show dialog HWND hwndOwner = (owner != NULL) ? getWindowHandle( env, owner ) : NULL; HRESULT hr = dialog->Show( hwndOwner ); + dialog->Unadvise( dwCookie ); if( hr == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) return newJavaStringArray( env, 0 ); CHECK_HRESULT( hr ); - // convert shell items to Java string array + // get selected files as Java string array + return getFiles( env, open, dialog ); +} + +static jobjectArray getFiles( JNIEnv* env, jboolean open, IFileDialog* dialog ) { if( open ) { AutoReleasePtr shellItems; DWORD count; CHECK_HRESULT( ((IFileOpenDialog*)(IFileDialog*)dialog)->GetResults( &shellItems ) ); CHECK_HRESULT( shellItems->GetCount( &count ) ); + // convert shell items to Java string array jobjectArray array = newJavaStringArray( env, count ); for( int i = 0; i < count; i++ ) { AutoReleasePtr shellItem; @@ -260,6 +358,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr CHECK_HRESULT( dialog->GetResult( &shellItem ) ); CHECK_HRESULT( shellItem->GetDisplayName( SIGDN_FILESYSPATH, &path ) ); + // convert shell item to Java string array jstring jpath = newJavaString( env, path ); CoTaskMemFree( path ); @@ -270,3 +369,15 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr return array; } } + + +extern "C" +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageDialog + ( JNIEnv* env, jclass cls, jlong hwndParent, jstring text, jstring caption, jint type ) +{ + // convert Java strings to C strings + AutoReleaseString ctext( env, text ); + AutoReleaseString ccaption( env, caption ); + + return ::MessageBox( reinterpret_cast( hwndParent ), ctext, ccaption, type ); +} diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h index 895b7267..18b9f377 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h @@ -116,10 +116,18 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_ /* * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary * Method: showFileChooser - * Signature: (Ljava/awt/Window;ZLjava/lang/String;Ljava/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: (Ljava/awt/Window;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILcom/formdev/flatlaf/ui/FlatNativeWindowsLibrary/FileChooserCallback;I[Ljava/lang/String;)[Ljava/lang/String; */ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showFileChooser - (JNIEnv *, jclass, jobject, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jint, jint, jint, jobjectArray); + (JNIEnv *, jclass, jobject, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jint, jint, jobject, jint, jobjectArray); + +/* + * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary + * Method: showMessageDialog + * Signature: (JLjava/lang/String;Ljava/lang/String;I)I + */ +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageDialog + (JNIEnv *, jclass, jlong, jstring, jstring, jint); #ifdef __cplusplus } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java index 05289d16..0c4f59f6 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java @@ -124,10 +124,20 @@ public class FlatSystemFileChooserLinuxTest } int fileTypeIndex = fileTypeIndexSlider.getValue(); + FlatNativeLinuxLibrary.FileChooserCallback callback = (files, hwndFileDialog) -> { + System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) ); + if( showMessageDialogOnOKCheckBox.isSelected() ) { + System.out.println( FlatNativeLinuxLibrary.showMessageDialog( hwndFileDialog, + JOptionPane.INFORMATION_MESSAGE, + "primary text", "secondary text", 1, "Yes", "No" ) ); + } + return true; + }; + if( direct ) { String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, open, title, okButtonLabel, currentName, currentFolder, - optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes ); + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes ); filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); } else { @@ -137,7 +147,7 @@ public class FlatSystemFileChooserLinuxTest new Thread( () -> { String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, open, title, okButtonLabel, currentName, currentFolder, - optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes2 ); + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 ); System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); @@ -248,6 +258,7 @@ public class FlatSystemFileChooserLinuxTest saveButton = new JButton(); openDirectButton = new JButton(); saveDirectButton = new JButton(); + showMessageDialogOnOKCheckBox = new JCheckBox(); filesScrollPane = new JScrollPane(); filesField = new JTextArea(); @@ -397,6 +408,10 @@ public class FlatSystemFileChooserLinuxTest saveDirectButton.addActionListener(e -> saveDirect()); add(saveDirectButton, "cell 0 7 3 1"); + //---- showMessageDialogOnOKCheckBox ---- + showMessageDialogOnOKCheckBox.setText("show message dialog on OK"); + add(showMessageDialogOnOKCheckBox, "cell 0 7 3 1"); + //======== filesScrollPane ======== { @@ -443,6 +458,7 @@ public class FlatSystemFileChooserLinuxTest 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/FlatSystemFileChooserLinuxTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd index d172fc00..3506768d 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "8.3" encoding: "UTF-8" +JFDML JFormDesigner: "8.2.2.0.9999" Java: "21.0.1" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -198,6 +198,12 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 7 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 7 3 1" + } ) add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "filesScrollPane" add( new FormComponent( "javax.swing.JTextArea" ) { diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java index 2aa9af1e..621d00d8 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java @@ -139,11 +139,21 @@ public class FlatSystemFileChooserWindowsTest fileTypes = fileTypesStr.trim().split( "[,]+" ); int fileTypeIndex = fileTypeIndexSlider.getValue(); + FlatNativeWindowsLibrary.FileChooserCallback callback = (files, hwndFileDialog) -> { + System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) ); + if( showMessageDialogOnOKCheckBox.isSelected() ) { + System.out.println( FlatNativeWindowsLibrary.showMessageDialog( hwndFileDialog, + "some text", "title", + /* MB_ICONINFORMATION */ 0x00000040 | /* MB_YESNO */ 0x00000004 ) ); + } + return true; + }; + if( direct ) { String[] files = FlatNativeWindowsLibrary.showFileChooser( owner, open, title, okButtonLabel, fileNameLabel, fileName, folder, saveAsItem, defaultFolder, defaultExtension, - optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes ); + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes ); filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); } else { @@ -154,7 +164,7 @@ public class FlatSystemFileChooserWindowsTest String[] files = FlatNativeWindowsLibrary.showFileChooser( owner, open, title, okButtonLabel, fileNameLabel, fileName, folder, saveAsItem, defaultFolder, defaultExtension, - optionsSet.get(), optionsClear.get(), fileTypeIndex, fileTypes2 ); + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 ); System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); @@ -290,6 +300,7 @@ public class FlatSystemFileChooserWindowsTest saveButton = new JButton(); openDirectButton = new JButton(); saveDirectButton = new JButton(); + showMessageDialogOnOKCheckBox = new JCheckBox(); filesScrollPane = new JScrollPane(); filesField = new JTextArea(); @@ -534,6 +545,10 @@ public class FlatSystemFileChooserWindowsTest saveDirectButton.addActionListener(e -> saveDirect()); add(saveDirectButton, "cell 0 11 3 1"); + //---- showMessageDialogOnOKCheckBox ---- + showMessageDialogOnOKCheckBox.setText("show message dialog on OK"); + add(showMessageDialogOnOKCheckBox, "cell 0 11 3 1"); + //======== filesScrollPane ======== { @@ -605,6 +620,7 @@ public class FlatSystemFileChooserWindowsTest 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/FlatSystemFileChooserWindowsTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd index 82828444..74e22930 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd @@ -343,6 +343,12 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 11 3 1" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showMessageDialogsOnOKCheckBox" + "text": "show message dialog on OK" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "filesScrollPane" add( new FormComponent( "javax.swing.JTextArea" ) { From 078e59a44387d60638a904d1919e638f365869f9 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 12 Jan 2025 18:16:40 +0100 Subject: [PATCH 15/34] 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" ) { From 07fc190b5fbcae9ed892fb16670a7c958960c75e Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 14 Jan 2025 16:29:27 +0100 Subject: [PATCH 16/34] Native Libraries: moved code to JNIUtils.cpp and *MessageDialog.cpp --- .../src/main/cpp/GtkFileChooser.cpp | 70 +-------------- .../src/main/cpp/GtkMessageDialog.cpp | 76 +++++++++++++++++ .../src/main/cpp/JNIUtils.cpp | 37 ++++++++ .../src/main/headers/JNIUtils.h | 36 ++++++++ .../src/main/objcpp/MacFileChooser.mm | 58 ------------- .../src/main/objcpp/MacMessageDialog.mm | 85 +++++++++++++++++++ .../src/main/cpp/JNIUtils.cpp | 67 +++++++++++++++ .../src/main/cpp/WinFileChooser.cpp | 74 +++------------- .../src/main/cpp/WinMessageDialog.cpp | 38 +++++++++ .../src/main/headers/JNIUtils.h | 53 ++++++++++++ 10 files changed, 405 insertions(+), 189 deletions(-) create mode 100644 flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp create mode 100644 flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp create mode 100644 flatlaf-natives/flatlaf-natives-linux/src/main/headers/JNIUtils.h create mode 100644 flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm create mode 100644 flatlaf-natives/flatlaf-natives-windows/src/main/cpp/JNIUtils.cpp create mode 100644 flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp create mode 100644 flatlaf-natives/flatlaf-natives-windows/src/main/headers/JNIUtils.h diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp index 7e427a85..1aacde3a 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp @@ -19,6 +19,7 @@ #include #include #include +#include "JNIUtils.h" #include "com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h" /** @@ -32,26 +33,6 @@ extern Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display** // declare internal methods static jobjectArray fileListToStringArray( JNIEnv* env, GSList* fileList ); -//---- class AutoReleaseStringUTF8 -------------------------------------------- - -class AutoReleaseStringUTF8 { - JNIEnv* env; - jstring javaString; - const char* chars; - -public: - AutoReleaseStringUTF8( JNIEnv* _env, jstring _javaString ) { - env = _env; - javaString = _javaString; - chars = (javaString != NULL) ? env->GetStringUTFChars( javaString, NULL ) : NULL; - } - ~AutoReleaseStringUTF8() { - if( chars != NULL ) - env->ReleaseStringUTFChars( javaString, chars ); - } - operator const gchar*() { return chars; } -}; - //---- helper ----------------------------------------------------------------- #define isOptionSet( option ) ((optionsSet & com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_ ## option) != 0) @@ -293,52 +274,3 @@ static jobjectArray fileListToStringArray( JNIEnv* env, GSList* fileList ) { g_slist_free( fileList ); return array; } - - -extern "C" -JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showMessageDialog - ( JNIEnv* env, jclass cls, jlong hwndParent, jint messageType, jstring primaryText, jstring secondaryText, - jint defaultButton, jobjectArray buttons ) -{ - GtkWindow* window = (GtkWindow*) hwndParent; - - // convert message type - GtkMessageType gmessageType; - switch( messageType ) { - case /* JOptionPane.ERROR_MESSAGE */ 0: gmessageType = GTK_MESSAGE_ERROR; break; - case /* JOptionPane.INFORMATION_MESSAGE */ 1: gmessageType = GTK_MESSAGE_INFO; break; - case /* JOptionPane.WARNING_MESSAGE */ 2: gmessageType = GTK_MESSAGE_WARNING; break; - case /* JOptionPane.QUESTION_MESSAGE */ 3: gmessageType = GTK_MESSAGE_QUESTION; break; - default: - case /* JOptionPane.PLAIN_MESSAGE */ -1: gmessageType = GTK_MESSAGE_OTHER; break; - } - - // convert Java strings to C strings - AutoReleaseStringUTF8 cprimaryText( env, primaryText ); - AutoReleaseStringUTF8 csecondaryText( env, secondaryText ); - - // create GTK file chooser dialog - // https://docs.gtk.org/gtk3/class.MessageDialog.html - jint buttonCount = env->GetArrayLength( buttons ); - GtkWidget* dialog = gtk_message_dialog_new( window, GTK_DIALOG_MODAL, gmessageType, - (buttonCount > 0) ? GTK_BUTTONS_NONE : GTK_BUTTONS_OK, - "%s", (const gchar*) cprimaryText ); - if( csecondaryText != NULL ) - gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( dialog ), "%s", (const gchar*) csecondaryText ); - - // add buttons - for( int i = 0; i < buttonCount; i++ ) { - AutoReleaseStringUTF8 str( env, (jstring) env->GetObjectArrayElement( buttons, i ) ); - gtk_dialog_add_button( GTK_DIALOG( dialog ), str, i ); - } - - // set default button - gtk_dialog_set_default_response( GTK_DIALOG( dialog ), MIN( MAX( defaultButton, 0 ), buttonCount - 1 ) ); - - // show message dialog - gint responseID = gtk_dialog_run( GTK_DIALOG( dialog ) ); - gtk_widget_destroy( dialog ); - - // return -1 if closed with ESC key - return (responseID >= 0) ? responseID : -1; -} diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp new file mode 100644 index 00000000..34f26c1b --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp @@ -0,0 +1,76 @@ +/* + * Copyright 2025 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. + */ + +#include +#include +#include +#include +#include +#include "JNIUtils.h" +#include "com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h" + +/** + * @author Karl Tauber + * @since 3.6 + */ + +extern "C" +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showMessageDialog + ( JNIEnv* env, jclass cls, jlong hwndParent, jint messageType, jstring primaryText, jstring secondaryText, + jint defaultButton, jobjectArray buttons ) +{ + GtkWindow* window = (GtkWindow*) hwndParent; + + // convert message type + GtkMessageType gmessageType; + switch( messageType ) { + case /* JOptionPane.ERROR_MESSAGE */ 0: gmessageType = GTK_MESSAGE_ERROR; break; + case /* JOptionPane.INFORMATION_MESSAGE */ 1: gmessageType = GTK_MESSAGE_INFO; break; + case /* JOptionPane.WARNING_MESSAGE */ 2: gmessageType = GTK_MESSAGE_WARNING; break; + case /* JOptionPane.QUESTION_MESSAGE */ 3: gmessageType = GTK_MESSAGE_QUESTION; break; + default: + case /* JOptionPane.PLAIN_MESSAGE */ -1: gmessageType = GTK_MESSAGE_OTHER; break; + } + + // convert Java strings to C strings + AutoReleaseStringUTF8 cprimaryText( env, primaryText ); + AutoReleaseStringUTF8 csecondaryText( env, secondaryText ); + + // create GTK file chooser dialog + // https://docs.gtk.org/gtk3/class.MessageDialog.html + jint buttonCount = env->GetArrayLength( buttons ); + GtkWidget* dialog = gtk_message_dialog_new( window, GTK_DIALOG_MODAL, gmessageType, + (buttonCount > 0) ? GTK_BUTTONS_NONE : GTK_BUTTONS_OK, + "%s", (const gchar*) cprimaryText ); + if( csecondaryText != NULL ) + gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( dialog ), "%s", (const gchar*) csecondaryText ); + + // add buttons + for( int i = 0; i < buttonCount; i++ ) { + AutoReleaseStringUTF8 str( env, (jstring) env->GetObjectArrayElement( buttons, i ) ); + gtk_dialog_add_button( GTK_DIALOG( dialog ), str, i ); + } + + // set default button + gtk_dialog_set_default_response( GTK_DIALOG( dialog ), MIN( MAX( defaultButton, 0 ), buttonCount - 1 ) ); + + // show message dialog + gint responseID = gtk_dialog_run( GTK_DIALOG( dialog ) ); + gtk_widget_destroy( dialog ); + + // return -1 if closed with ESC key + return (responseID >= 0) ? responseID : -1; +} diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp new file mode 100644 index 00000000..bd587070 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp @@ -0,0 +1,37 @@ +/* + * 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. + */ + +// avoid inlining of printf() +#define _NO_CRT_STDIO_INLINE + +#include "JNIUtils.h" + +/** + * @author Karl Tauber + */ + +//---- class AutoReleaseStringUTF8 -------------------------------------------- + +AutoReleaseStringUTF8::AutoReleaseStringUTF8( JNIEnv* _env, jstring _javaString ) { + env = _env; + javaString = _javaString; + chars = (javaString != NULL) ? env->GetStringUTFChars( javaString, NULL ) : NULL; +} + +AutoReleaseStringUTF8::~AutoReleaseStringUTF8() { + if( chars != NULL ) + env->ReleaseStringUTFChars( javaString, chars ); +} diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/JNIUtils.h b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/JNIUtils.h new file mode 100644 index 00000000..6d7d8c22 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/JNIUtils.h @@ -0,0 +1,36 @@ +/* + * Copyright 2025 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. + */ + +#include +#include + +/** + * @author Karl Tauber + */ + +//---- class AutoReleaseStringUTF8 -------------------------------------------- + +class AutoReleaseStringUTF8 { + JNIEnv* env; + jstring javaString; + const char* chars; + +public: + AutoReleaseStringUTF8( JNIEnv* _env, jstring _javaString ); + ~AutoReleaseStringUTF8(); + + operator const gchar*() { return chars; } +}; 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 a782ca74..891d97c2 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -366,61 +366,3 @@ static jobjectArray urlsToStringArray( JNIEnv* env, NSArray* urls ) { } 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-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm new file mode 100644 index 00000000..d186c8e6 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm @@ -0,0 +1,85 @@ +/* + * 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 + * @since 3.6 + */ + +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-natives/flatlaf-natives-windows/src/main/cpp/JNIUtils.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/JNIUtils.cpp new file mode 100644 index 00000000..bc24a10e --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/JNIUtils.cpp @@ -0,0 +1,67 @@ +/* + * 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. + */ + +// avoid inlining of printf() +#define _NO_CRT_STDIO_INLINE + +#include "JNIUtils.h" + +/** + * @author Karl Tauber + */ + +//---- class AutoReleaseString ------------------------------------------------ + +AutoReleaseString::AutoReleaseString( JNIEnv* _env, jstring _javaString ) { + env = _env; + javaString = _javaString; + chars = (javaString != NULL) ? env->GetStringChars( javaString, NULL ) : NULL; +} + +AutoReleaseString::~AutoReleaseString() { + if( chars != NULL ) + env->ReleaseStringChars( javaString, chars ); +} + +//---- class AutoReleaseStringArray ------------------------------------------- + +AutoReleaseStringArray::AutoReleaseStringArray( JNIEnv* _env, jobjectArray _javaStringArray ) { + env = _env; + count = (_javaStringArray != NULL) ? env->GetArrayLength( _javaStringArray ) : 0; + if( count <= 0 ) + return; + + javaStringArray = new jstring[count]; + charsArray = new const jchar*[count]; + + for( int i = 0; i < count; i++ ) { + javaStringArray[i] = (jstring) env->GetObjectArrayElement( _javaStringArray, i ); + charsArray[i] = env->GetStringChars( javaStringArray[i] , NULL ); + } +} + +AutoReleaseStringArray::~AutoReleaseStringArray() { + if( count == 0 ) + return; + + for( int i = 0; i < count; i++ ) { + env->ReleaseStringChars( javaStringArray[i], charsArray[i] ); + env->DeleteLocalRef( javaStringArray[i] ); + } + + delete[] javaStringArray; + delete[] charsArray; +} diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp index 2a643643..1eebe21e 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp @@ -19,6 +19,7 @@ #include #include +#include "JNIUtils.h" #include "com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h" /** @@ -54,26 +55,6 @@ public: operator T*() { return ptr; } }; -//---- class AutoReleaseString ------------------------------------------------ - -class AutoReleaseString { - JNIEnv* env; - jstring javaString; - const jchar* chars; - -public: - AutoReleaseString( JNIEnv* _env, jstring _javaString ) { - env = _env; - javaString = _javaString; - chars = (javaString != NULL) ? env->GetStringChars( javaString, NULL ) : NULL; - } - ~AutoReleaseString() { - if( chars != NULL ) - env->ReleaseStringChars( javaString, chars ); - } - operator LPCWSTR() { return (LPCWSTR) chars; } -}; - //---- class AutoReleaseIShellItem -------------------------------------------- class AutoReleaseIShellItem : public AutoReleasePtr { @@ -87,49 +68,30 @@ public: //---- class FilterSpec ------------------------------------------------------- class FilterSpec { - JNIEnv* env = NULL; - jstring* jnames = NULL; - jstring* jspecs = NULL; + AutoReleaseStringArray fileTypes; public: UINT count = 0; COMDLG_FILTERSPEC* specs = NULL; public: - FilterSpec( JNIEnv* _env, jobjectArray fileTypes ) { - if( fileTypes == NULL ) + FilterSpec( JNIEnv* _env, jobjectArray _fileTypes ) + : fileTypes( _env, _fileTypes ) + { + if( fileTypes.count == 0 ) return; - env = _env; - count = env->GetArrayLength( fileTypes ) / 2; - if( count <= 0 ) - return; - - specs = new COMDLG_FILTERSPEC[count]; - jnames = new jstring[count]; - jspecs = new jstring[count]; + count = fileTypes.count / 2; + specs = new COMDLG_FILTERSPEC[fileTypes.count]; for( int i = 0; i < count; i++ ) { - jnames[i] = (jstring) env->GetObjectArrayElement( fileTypes, i * 2 ); - jspecs[i] = (jstring) env->GetObjectArrayElement( fileTypes, (i * 2) + 1 ); - specs[i].pszName = (LPCWSTR) env->GetStringChars( jnames[i] , NULL ); - specs[i].pszSpec = (LPCWSTR) env->GetStringChars( jspecs[i], NULL ); + specs[i].pszName = fileTypes[i * 2]; + specs[i].pszSpec = fileTypes[(i * 2) + 1]; } } ~FilterSpec() { - if( specs == NULL ) - return; - - for( int i = 0; i < count; i++ ) { - env->ReleaseStringChars( jnames[i], (jchar *) specs[i].pszName ); - env->ReleaseStringChars( jspecs[i], (jchar *) specs[i].pszSpec ); - env->DeleteLocalRef( jnames[i] ); - env->DeleteLocalRef( jspecs[i] ); - } - - delete[] jnames; - delete[] jspecs; - delete[] specs; + if( specs != NULL ) + delete[] specs; } }; @@ -369,15 +331,3 @@ static jobjectArray getFiles( JNIEnv* env, jboolean open, IFileDialog* dialog ) return array; } } - - -extern "C" -JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageDialog - ( JNIEnv* env, jclass cls, jlong hwndParent, jstring text, jstring caption, jint type ) -{ - // convert Java strings to C strings - AutoReleaseString ctext( env, text ); - AutoReleaseString ccaption( env, caption ); - - return ::MessageBox( reinterpret_cast( hwndParent ), ctext, ccaption, type ); -} diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp new file mode 100644 index 00000000..38b9dd3a --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp @@ -0,0 +1,38 @@ +/* + * 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. + */ + +// avoid inlining of printf() +#define _NO_CRT_STDIO_INLINE + +#include +#include "JNIUtils.h" +#include "com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h" + +/** + * @author Karl Tauber + * @since 3.6 + */ + +extern "C" +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageDialog + ( JNIEnv* env, jclass cls, jlong hwndParent, jstring text, jstring caption, jint type ) +{ + // convert Java strings to C strings + AutoReleaseString ctext( env, text ); + AutoReleaseString ccaption( env, caption ); + + return ::MessageBox( reinterpret_cast( hwndParent ), ctext, ccaption, type ); +} diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/JNIUtils.h b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/JNIUtils.h new file mode 100644 index 00000000..5eaad97c --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/JNIUtils.h @@ -0,0 +1,53 @@ +/* + * Copyright 2025 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. + */ + +#include +#include + +/** + * @author Karl Tauber + */ + +//---- class AutoReleaseString ------------------------------------------------ + +class AutoReleaseString { + JNIEnv* env; + jstring javaString; + const jchar* chars; + +public: + AutoReleaseString( JNIEnv* _env, jstring _javaString ); + ~AutoReleaseString(); + + operator LPCWSTR() { return (LPCWSTR) chars; } +}; + +//---- class AutoReleaseStringArray ------------------------------------------- + +class AutoReleaseStringArray { + JNIEnv* env; + jstring* javaStringArray; + const jchar** charsArray; + +public: + UINT count; + +public: + AutoReleaseStringArray( JNIEnv* _env, jobjectArray _javaStringArray ); + ~AutoReleaseStringArray(); + + operator LPCWSTR*() { return (LPCWSTR*) charsArray; } +}; From d513ec497b80e07c36d7b825af8814b443b51845 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 15 Jan 2025 18:51:37 +0100 Subject: [PATCH 17/34] System File Chooser: support system message dialog with custom buttons on Windows (not yet used in `SystemFileChooser` --- .../flatlaf/ui/FlatNativeLinuxLibrary.java | 7 +- .../flatlaf/ui/FlatNativeWindowsLibrary.java | 31 +- .../src/main/cpp/Runtime.cpp | 3 + .../src/main/cpp/WinMessageDialog.cpp | 344 +++++++++++++++++- ...mdev_flatlaf_ui_FlatNativeWindowsLibrary.h | 10 +- .../FlatSystemFileChooserWindowsTest.java | 102 +++++- .../FlatSystemFileChooserWindowsTest.jfd | 64 +++- 7 files changed, 543 insertions(+), 18 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java index 5ea7d178..1cbefc97 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java @@ -146,7 +146,8 @@ public class FlatNativeLinuxLibrary * @param owner the owner of the file dialog; or {@code null} * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog * @param title text displayed in dialog title; or {@code null} - * @param okButtonLabel text displayed in default button; or {@code null}. Use '_' for mnemonics (e.g. "_Choose") + * @param okButtonLabel text displayed in default button; or {@code null}. + * Use '_' for mnemonics (e.g. "_Choose") * Use '__' for '_' character (e.g. "Choose__and__Quit"). * @param currentName user-editable filename currently shown in the filename field in save dialog; or {@code null} * @param currentFolder current directory shown in the dialog; or {@code null} @@ -189,7 +190,9 @@ public class FlatNativeLinuxLibrary * this will appear as title in a larger bold font * @param secondaryText secondary text; shown below of primary text; 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 + * @param buttons texts of the buttons; if no buttons given the a default "OK" button is shown. + * Use '_' for mnemonics (e.g. "_Choose") + * Use '__' for '_' character (e.g. "Choose__and__Quit"). * @return index of pressed button; or -1 for ESC key * * @since 3.6 diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java index ec2bed8f..65326049 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java @@ -18,6 +18,7 @@ package com.formdev.flatlaf.ui; import java.awt.Color; import java.awt.Window; +import javax.swing.JOptionPane; import com.formdev.flatlaf.util.SystemInfo; /** @@ -204,7 +205,8 @@ public class FlatNativeWindowsLibrary * @param owner the owner of the file dialog; or {@code null} * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog * @param title text displayed in dialog title; or {@code null} - * @param okButtonLabel text displayed in default button; or {@code null}. Use '&' for mnemonics (e.g. "&Choose"). + * @param okButtonLabel text displayed in default button; or {@code null}. + * Use '&' for mnemonics (e.g. "&Choose"). * Use '&&' for '&' character (e.g. "Choose && Quit"). * @param fileNameLabel text displayed in front of the filename text field; or {@code null} * @param fileName user-editable filename currently shown in the filename field; or {@code null} @@ -240,6 +242,29 @@ public class FlatNativeWindowsLibrary boolean approve( String[] files, long hwndFileDialog ); } + /** + * Shows a modal Windows message dialog. + *

    + * For use in {@link FileChooserCallback} only. + * + * @param hwndParent the parent of the message box + * @param messageType type of message being displayed: + * {@link JOptionPane#ERROR_MESSAGE}, {@link JOptionPane#INFORMATION_MESSAGE}, + * {@link JOptionPane#WARNING_MESSAGE}, {@link JOptionPane#QUESTION_MESSAGE} or + * {@link JOptionPane#PLAIN_MESSAGE} + * @param title dialog box title; or {@code null} to use title from parent window + * @param text message to be displayed + * @param defaultButton index of the default button, which can be pressed using ENTER key + * @param buttons texts of the buttons. + * Use '&' for mnemonics (e.g. "&Choose"). + * Use '&&' for '&' character (e.g. "Choose && Quit"). + * @return index of pressed button; or -1 for ESC key + * + * @since 3.6 + */ + public native static int showMessageDialog( long hwndParent, int messageType, + String title, String text, int defaultButton, String... buttons ); + /** * Shows a Windows message box * MessageBox. @@ -251,10 +276,8 @@ public class FlatNativeWindowsLibrary * @param caption dialog box title * @param type see MessageBox parameter uType * @return see MessageBox Return value - * @return index of pressed button; or -1 for ESC key * * @since 3.6 */ - public native static int showMessageDialog( long hwndParent, - String text, String caption, int type ); + public native static int showMessageBox( long hwndParent, String text, String caption, int type ); } diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/Runtime.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/Runtime.cpp index cbd5163d..cea43d13 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/Runtime.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/Runtime.cpp @@ -36,8 +36,11 @@ * @author Karl Tauber */ +HINSTANCE _instance; + extern "C" BOOL WINAPI _DllMainCRTStartup( HINSTANCE instance, DWORD reason, LPVOID reserved ) { + _instance = instance; return TRUE; } diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp index 38b9dd3a..7a087fc8 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 FormDev Software GmbH + * Copyright 2025 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. @@ -18,6 +18,7 @@ #define _NO_CRT_STDIO_INLINE #include +#include #include "JNIUtils.h" #include "com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h" @@ -26,8 +27,24 @@ * @since 3.6 */ +#define ID_BUTTON1 101 + +// declare external fields +extern HINSTANCE _instance; + +// declare internal methods +static byte* createInMemoryTemplate( HWND owner, int messageType, LPCWSTR title, LPCWSTR text, + int defaultButton, int buttonCount, LPCWSTR* buttons ); +static INT_PTR CALLBACK messageDialogProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ); +static int textLengthAsDLUs( HDC hdc, LPCWSTR str, int strLen ); +static LONG pixel2dluX( LONG px ); +static LONG pixel2dluY( LONG px ); +static LONG dluX2pixel( LONG dluX ); +static LPWORD lpwAlign( LPWORD lpIn ); + + extern "C" -JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageDialog +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageBox ( JNIEnv* env, jclass cls, jlong hwndParent, jstring text, jstring caption, jint type ) { // convert Java strings to C strings @@ -36,3 +53,326 @@ JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_show return ::MessageBox( reinterpret_cast( hwndParent ), ctext, ccaption, type ); } + +extern "C" +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageDialog + ( JNIEnv* env, jclass cls, jlong hwndParent, jint messageType, jstring title, + jstring text, jint defaultButton, jobjectArray buttons ) +{ + HWND owner = reinterpret_cast( hwndParent ); + + // convert Java strings to C strings + AutoReleaseString ctitle( env, title ); + AutoReleaseString ctext( env, text ); + AutoReleaseStringArray cbuttons( env, buttons ); + + // get title from parent window if necessary + WCHAR parentTitle[100]; + if( ctitle == NULL ) + ::GetWindowText( owner, parentTitle, 100 ); + + byte* templ = createInMemoryTemplate( owner, messageType, (ctitle != NULL) ? ctitle : parentTitle, + ctext, defaultButton, cbuttons.count, cbuttons ); + if( templ == NULL ) + return -1; + + LRESULT ret = ::DialogBoxIndirect( _instance, (LPDLGTEMPLATE) templ, owner, messageDialogProc ); + delete templ; + return (ret >= ID_BUTTON1) ? ret - ID_BUTTON1 : -1; +} + + +// all values in DLUs + +#define INSETS_TOP 16 +#define INSETS_LEFT 12 +#define INSETS_RIGHT 12 +#define INSETS_BOTTOM 8 + +#define ICON_TEXT_GAP 8 + +#define LABEL_MIN_WIDTH 100 +#define LABEL_MAX_WIDTH 250 +#define LABEL_HEIGHT 8 + +#define BUTTON_WIDTH 50 +#define BUTTON_HEIGHT 14 +#define BUTTON_GAP 5 +#define BUTTON_TOP_GAP 16 +#define BUTTON_LEFT_RIGHT_GAP 8 + +// based on https://learn.microsoft.com/en-us/windows/win32/dlgbox/using-dialog-boxes#creating-a-template-in-memory +static byte* createInMemoryTemplate( HWND owner, int messageType, LPCWSTR title, LPCWSTR text, + int defaultButton, int buttonCount, LPCWSTR* buttons ) +{ + //---- calculate layout (in DLUs) ---- + + HDC hdc = GetDC( owner ); + + // layout icon + LPWSTR icon; + switch( messageType ) { + case /* JOptionPane.ERROR_MESSAGE */ 0: icon = IDI_ERROR; break; + case /* JOptionPane.INFORMATION_MESSAGE */ 1: icon = IDI_INFORMATION; break; + case /* JOptionPane.WARNING_MESSAGE */ 2: icon = IDI_WARNING; break; + case /* JOptionPane.QUESTION_MESSAGE */ 3: icon = IDI_QUESTION; break; + default: + case /* JOptionPane.PLAIN_MESSAGE */ -1: icon = NULL; break; + } + int ix = INSETS_LEFT; + int iy = INSETS_TOP; + int iw = pixel2dluX( ::GetSystemMetrics( SM_CXICON ) ); + int ih = pixel2dluY( ::GetSystemMetrics( SM_CYICON ) ); + + // layout text + int tx = ix + (icon != NULL ? iw + ICON_TEXT_GAP : 0); + int ty = iy; + int tw = 0; + int th = 0; + if( text == NULL ) + text = L""; + LPWSTR wrappedText = new WCHAR[wcslen( text ) + 1]; + wcscpy( wrappedText, text ); + LPWSTR lineStart = wrappedText; + for( LPWSTR t = wrappedText; ; t++ ) { + if( *t != '\n' && *t != 0 ) + continue; + + // calculate line width (in pixels) and number of charaters that fit into LABEL_MAX_WIDTH + int lineLen = t - lineStart; + int fit = 0; + SIZE size{ 0 }; + if( !::GetTextExtentExPoint( hdc, lineStart, lineLen, dluX2pixel( LABEL_MAX_WIDTH ), &fit, NULL, &size ) ) + break; + + if( fit < lineLen ) { + // wrap too long line --> try to wrap at space character + bool wrapped = false; + for( LPWSTR t2 = lineStart + fit - 1; t2 > lineStart; t2-- ) { + if( *t2 == ' ' || *t2 == '\t' ) { + *t2 = '\n'; + int w = textLengthAsDLUs( hdc, lineStart, t2 - lineStart ); + tw = max( tw, w ); + th += LABEL_HEIGHT; + + // continue wrapping after inserted line break + t = t2; + lineStart = t + 1; + wrapped = true; + break; + } + } + if( !wrapped ) { + // not able to wrap at word --> break long word + int breakIndex = (lineStart + fit) - wrappedText; + int w = textLengthAsDLUs( hdc, lineStart, breakIndex ); + tw = max( tw, w ); + th += LABEL_HEIGHT; + + // duplicate string + LPWSTR wrappedText2 = new WCHAR[wcslen( wrappedText ) + 1 + 1]; + // use wcscpy(), instead of wcsncpy(), because this method is inlined and does not require linking to runtime lib + wcscpy( wrappedText2, wrappedText ); + wrappedText2[breakIndex] = '\n'; + wcscpy( wrappedText2 + breakIndex + 1, wrappedText + breakIndex ); + + // delete old text + delete[] wrappedText; + wrappedText = wrappedText2; + + // continue wrapping after inserted line break + t = wrappedText + breakIndex; + lineStart = t + 1; + } + } else { + // line fits into LABEL_MAX_WIDTH + int w = pixel2dluX( size.cx ); + tw = max( tw, w ); + th += LABEL_HEIGHT; + lineStart = t + 1; + } + + if( *t == 0 ) + break; + } + tw = min( max( tw, LABEL_MIN_WIDTH ), LABEL_MAX_WIDTH ); + th = max( th, LABEL_HEIGHT ); + if( icon != NULL && th < ih ) + ty += (ih - th) / 2; // vertically center text + + // layout buttons + int* bw = new int[buttonCount]; + int buttonTotalWidth = BUTTON_GAP * (buttonCount - 1); + for( int i = 0; i < buttonCount; i++ ) { + int w = textLengthAsDLUs( hdc, buttons[i], -1 ) + 16; + bw[i] = max( BUTTON_WIDTH, w ); + buttonTotalWidth += bw[i]; + } + + // layout dialog + int dx = 0; + int dy = 0; + int dw = max( tx + tw + INSETS_RIGHT, BUTTON_LEFT_RIGHT_GAP + buttonTotalWidth + BUTTON_LEFT_RIGHT_GAP ); + int dh = max( iy + ih, ty + th ) + BUTTON_TOP_GAP + BUTTON_HEIGHT + INSETS_BOTTOM; + + // center dialog in owner + RECT ownerRect{ 0 }; + if( ::GetClientRect( owner, &ownerRect ) ) { + dx = (pixel2dluX( ownerRect.right - ownerRect.left ) - dw) / 2; + dy = (pixel2dluY( ownerRect.bottom - ownerRect.top ) - dh) / 2; + } + + // layout button area + int bx = dw - buttonTotalWidth - BUTTON_LEFT_RIGHT_GAP; + int by = dh - BUTTON_HEIGHT - INSETS_BOTTOM; + + // (approximately) calculate memory size needed for in-memory template + int templSize = (sizeof(DLGTEMPLATE) + /*menu*/ 2 + /*class*/ 2 + /*title*/ 2) + + ((sizeof(DLGITEMTEMPLATE) + /*class*/ 4 + /*title/icon*/ 4 + /*creation data*/ 2) * (/*icon+text*/2 + buttonCount)) + + (title != NULL ? wcslen( title ) * sizeof(wchar_t) : 0) + + (wcslen( wrappedText ) * sizeof(wchar_t)); + for( int i = 0; i < buttonCount; i++ ) + templSize += (wcslen( buttons[i] ) * sizeof(wchar_t)); + + templSize += (2 * (1 + 1 + buttonCount)); // necessary for DWORD alignment + templSize += 100; // some reserve + + // allocate memory for in-memory template + byte* templ = new byte[templSize]; + if( templ == NULL ) + return NULL; + + + //---- define dialog box ---- + + LPDLGTEMPLATE lpdt = (LPDLGTEMPLATE) templ; + lpdt->style = WS_POPUP | WS_BORDER | WS_SYSMENU | DS_MODALFRAME | WS_CAPTION; + lpdt->cdit = /*text*/ 1 + buttonCount; // number of controls + lpdt->x = dx; + lpdt->y = dy; + lpdt->cx = dw; + lpdt->cy = dh; + + LPWORD lpw = (LPWORD) (lpdt + 1); + *lpw++ = 0; // no menu + *lpw++ = 0; // predefined dialog box class (by default) + if( title != NULL ) { + wcscpy( (LPWSTR) lpw, title ); + lpw += wcslen( title ) + 1; + } else + *lpw++ = 0; // no title + + + //---- define icon ---- + + if( icon != NULL ) { + lpdt->cdit++; + + lpw = lpwAlign( lpw ); + LPDLGITEMTEMPLATE lpdit = (LPDLGITEMTEMPLATE) lpw; + lpdit->x = ix; + lpdit->y = iy; + lpdit->cx = iw; + lpdit->cy = ih; + lpdit->id = ID_BUTTON1 - 1; + lpdit->style = WS_CHILD | WS_VISIBLE | SS_ICON; + + lpw = (LPWORD) (lpdit + 1); + *lpw++ = 0xffff; *lpw++ = 0x0082; // Static class + *lpw++ = 0xffff; *lpw++ = (WORD) icon; // icon + *lpw++ = 0; // creation data + } + + + //---- define text ---- + + lpw = lpwAlign( lpw ); + LPDLGITEMTEMPLATE lpdit = (LPDLGITEMTEMPLATE) lpw; + lpdit->x = tx; + lpdit->y = ty; + lpdit->cx = tw; + lpdit->cy = th; + lpdit->id = ID_BUTTON1 - 2; + lpdit->style = WS_CHILD | WS_VISIBLE | SS_LEFT | SS_NOPREFIX | SS_EDITCONTROL; + + lpw = (LPWORD) (lpdit + 1); + *lpw++ = 0xffff; *lpw++ = 0x0082; // Static class + wcscpy( (LPWSTR) lpw, wrappedText ); lpw += wcslen( wrappedText ) + 1; // text + *lpw++ = 0; // creation data + + + //---- define buttons ---- + + defaultButton = min( max( defaultButton, 0 ), buttonCount - 1 ); + int buttonId = ID_BUTTON1; + for( int i = 0; i < buttonCount; i++ ) { + lpw = lpwAlign( lpw ); + LPDLGITEMTEMPLATE lpdit = (LPDLGITEMTEMPLATE) lpw; + lpdit->x = bx; + lpdit->y = by; + lpdit->cx = bw[i]; + lpdit->cy = BUTTON_HEIGHT; + lpdit->id = buttonId++; + lpdit->style = WS_CHILD | WS_VISIBLE | WS_TABSTOP | (i == 0 ? WS_GROUP : 0) + | BS_TEXT | (i == defaultButton ? BS_DEFPUSHBUTTON : BS_PUSHBUTTON); + + lpw = (LPWORD) (lpdit + 1); + *lpw++ = 0xffff; *lpw++ = 0x0080; // Button class + wcscpy( (LPWSTR) lpw, buttons[i] ); lpw += wcslen( buttons[i] ) + 1; // text + *lpw++ = 0; // creation data + + bx += bw[i] + BUTTON_GAP; + } + + delete[] wrappedText; + delete[] bw; + + return templ; +} + +static BOOL CALLBACK focusDefaultButtonProc( HWND hwnd, LPARAM lParam ) { + if( ::GetWindowLong( hwnd, GWL_ID ) >= ID_BUTTON1 ) { + LONG style = ::GetWindowLong( hwnd, GWL_STYLE ); + if( (style & BS_DEFPUSHBUTTON) != 0 ) { + ::SetFocus( hwnd ); + return FALSE; + } + } + return TRUE; +} + +static INT_PTR CALLBACK messageDialogProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { + if( uMsg == WM_INITDIALOG ) + ::EnumChildWindows( hwnd, focusDefaultButtonProc, 0 ); + else if( uMsg == WM_COMMAND ) { + ::EndDialog( hwnd, wParam ); + return TRUE; + } + return FALSE; +} + +static int textLengthAsDLUs( HDC hdc, LPCWSTR str, int strLen ) { + SIZE size{ 0 }; + ::GetTextExtentPoint32( hdc, str, (strLen >= 0) ? strLen : wcslen( str ), &size ); + return pixel2dluX( size.cx ); +} + +static LONG pixel2dluX( LONG px ) { + return MulDiv( px, 4, LOWORD( ::GetDialogBaseUnits() ) ); +} + +static LONG pixel2dluY( LONG py ) { + return MulDiv( py, 8, HIWORD( ::GetDialogBaseUnits() ) ); +} + +static LONG dluX2pixel( LONG dluX ) { + return MulDiv( dluX, LOWORD( ::GetDialogBaseUnits() ), 4 ); +} + +static LPWORD lpwAlign( LPWORD lpIn ) { + ULONG_PTR ul = (ULONG_PTR) lpIn; + ul += 3; + ul >>= 2; + ul <<= 2; + return (LPWORD) ul; +} diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h index 18b9f377..b5b33245 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h @@ -124,9 +124,17 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr /* * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary * Method: showMessageDialog - * Signature: (JLjava/lang/String;Ljava/lang/String;I)I + * Signature: (JILjava/lang/String;Ljava/lang/String;I[Ljava/lang/String;)I */ JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageDialog + (JNIEnv *, jclass, jlong, jint, jstring, jstring, jint, jobjectArray); + +/* + * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary + * Method: showMessageBox + * Signature: (JLjava/lang/String;Ljava/lang/String;I)I + */ +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageBox (JNIEnv *, jclass, jlong, jstring, jstring, jint); #ifdef __cplusplus diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java index 621d00d8..cc36e77b 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java @@ -30,7 +30,10 @@ import java.awt.event.WindowListener; import java.awt.event.WindowStateListener; import java.util.Arrays; import java.util.concurrent.atomic.AtomicInteger; +import java.util.prefs.Preferences; import javax.swing.*; +import javax.swing.border.TitledBorder; +import com.formdev.flatlaf.demo.DemoPrefs; import com.formdev.flatlaf.extras.components.*; import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; @@ -50,7 +53,7 @@ public class FlatSystemFileChooserWindowsTest } FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserWindowsTest" ); - addListeners( frame ); +// addListeners( frame ); frame.showFrame( FlatSystemFileChooserWindowsTest::new ); } ); } @@ -59,6 +62,10 @@ public class FlatSystemFileChooserWindowsTest initComponents(); fileTypesField.setSelectedItem( null ); + + Preferences state = DemoPrefs.getState(); + messageField.setText( state.get( "systemfilechooser.windows.message", "some message" ) ); + buttonsField.setText( state.get( "systemfilechooser.windows.buttons", "OK" ) ); } private void open() { @@ -143,8 +150,8 @@ public class FlatSystemFileChooserWindowsTest System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) ); if( showMessageDialogOnOKCheckBox.isSelected() ) { System.out.println( FlatNativeWindowsLibrary.showMessageDialog( hwndFileDialog, - "some text", "title", - /* MB_ICONINFORMATION */ 0x00000040 | /* MB_YESNO */ 0x00000004 ) ); + JOptionPane.INFORMATION_MESSAGE, + null, "some text", 1, "Yes", "No" ) ); } return true; }; @@ -189,6 +196,28 @@ public class FlatSystemFileChooserWindowsTest optionsClear.set( optionsClear.get() | option ); } + private void messageDialog() { + long hwnd = getHWND( SwingUtilities.windowForComponent( this ) ); + String message = messageField.getText(); + String[] buttons = buttonsField.getText().trim().split( "[,]+" ); + + Preferences state = DemoPrefs.getState(); + state.put( "systemfilechooser.windows.message", message ); + state.put( "systemfilechooser.windows.buttons", buttonsField.getText() ); + + System.out.println( FlatNativeWindowsLibrary.showMessageDialog( hwnd, + JOptionPane.WARNING_MESSAGE, null, message, 1, buttons ) ); + } + + private void messageBox() { + long hwnd = getHWND( SwingUtilities.windowForComponent( this ) ); + String message = messageField.getText(); + + System.out.println( FlatNativeWindowsLibrary.showMessageBox( hwnd, message, null, + /* MB_ICONINFORMATION */ 0x00000040 | /* MB_YESNO */ 0x00000004 ) ); + } + + @SuppressWarnings( "unused" ) private static void addListeners( Window w ) { w.addWindowListener( new WindowListener() { @Override @@ -278,6 +307,12 @@ public class FlatSystemFileChooserWindowsTest supportStreamableItemsCheckBox = new FlatTriStateCheckBox(); allowMultiSelectCheckBox = new FlatTriStateCheckBox(); hidePinnedPlacesCheckBox = new FlatTriStateCheckBox(); + messageDialogPanel = new JPanel(); + messageLabel = new JLabel(); + messageScrollPane = new JScrollPane(); + messageField = new JTextArea(); + buttonsLabel = new JLabel(); + buttonsField = new JTextField(); okButtonLabelLabel = new JLabel(); okButtonLabelField = new JTextField(); fileNameLabelLabel = new JLabel(); @@ -301,6 +336,9 @@ public class FlatSystemFileChooserWindowsTest openDirectButton = new JButton(); saveDirectButton = new JButton(); showMessageDialogOnOKCheckBox = new JCheckBox(); + hSpacer1 = new JPanel(null); + messageDialogButton = new JButton(); + messageBoxButton = new JButton(); filesScrollPane = new JScrollPane(); filesField = new JTextArea(); @@ -365,7 +403,8 @@ public class FlatSystemFileChooserWindowsTest "[]0" + "[]0" + "[]0" + - "[]0")); + "[]0" + + "[grow]")); //---- overwritePromptCheckBox ---- overwritePromptCheckBox.setText("overwritePrompt"); @@ -461,8 +500,41 @@ public class FlatSystemFileChooserWindowsTest //---- hidePinnedPlacesCheckBox ---- hidePinnedPlacesCheckBox.setText("hidePinnedPlaces"); panel1.add(hidePinnedPlacesCheckBox, "cell 1 7"); + + //======== messageDialogPanel ======== + { + messageDialogPanel.setBorder(new TitledBorder("MessageDialog")); + messageDialogPanel.setLayout(new MigLayout( + "hidemode 3", + // columns + "[fill]" + + "[grow,fill]", + // rows + "[grow,fill]" + + "[]")); + + //---- messageLabel ---- + messageLabel.setText("Message"); + messageDialogPanel.add(messageLabel, "cell 0 0,aligny top,growy 0"); + + //======== messageScrollPane ======== + { + + //---- messageField ---- + messageField.setColumns(40); + messageField.setRows(4); + messageScrollPane.setViewportView(messageField); + } + messageDialogPanel.add(messageScrollPane, "cell 1 0"); + + //---- buttonsLabel ---- + buttonsLabel.setText("Buttons:"); + messageDialogPanel.add(buttonsLabel, "cell 0 1"); + messageDialogPanel.add(buttonsField, "cell 1 1"); + } + panel1.add(messageDialogPanel, "cell 0 8 3 1,grow"); } - add(panel1, "cell 2 1 1 10,aligny top,growy 0"); + add(panel1, "cell 2 1 1 10,growy"); //---- okButtonLabelLabel ---- okButtonLabelLabel.setText("okButtonLabel"); @@ -548,6 +620,17 @@ public class FlatSystemFileChooserWindowsTest //---- showMessageDialogOnOKCheckBox ---- showMessageDialogOnOKCheckBox.setText("show message dialog on OK"); add(showMessageDialogOnOKCheckBox, "cell 0 11 3 1"); + add(hSpacer1, "cell 0 11 3 1,growx"); + + //---- messageDialogButton ---- + messageDialogButton.setText("MessageDialog..."); + messageDialogButton.addActionListener(e -> messageDialog()); + add(messageDialogButton, "cell 0 11 3 1,alignx right,growx 0"); + + //---- messageBoxButton ---- + messageBoxButton.setText("MessageBox..."); + messageBoxButton.addActionListener(e -> messageBox()); + add(messageBoxButton, "cell 0 11 3 1"); //======== filesScrollPane ======== { @@ -598,6 +681,12 @@ public class FlatSystemFileChooserWindowsTest private FlatTriStateCheckBox supportStreamableItemsCheckBox; private FlatTriStateCheckBox allowMultiSelectCheckBox; private FlatTriStateCheckBox hidePinnedPlacesCheckBox; + private JPanel messageDialogPanel; + private JLabel messageLabel; + private JScrollPane messageScrollPane; + private JTextArea messageField; + private JLabel buttonsLabel; + private JTextField buttonsField; private JLabel okButtonLabelLabel; private JTextField okButtonLabelField; private JLabel fileNameLabelLabel; @@ -621,6 +710,9 @@ public class FlatSystemFileChooserWindowsTest private JButton openDirectButton; private JButton saveDirectButton; private JCheckBox showMessageDialogOnOKCheckBox; + private JPanel hSpacer1; + private JButton messageDialogButton; + private JButton messageBoxButton; 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/FlatSystemFileChooserWindowsTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd index 74e22930..040c5c82 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd @@ -56,7 +56,7 @@ new FormModel { add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets 2,hidemode 3" "$columnConstraints": "[left]para[left]para[left]" - "$rowConstraints": "[]0[]0[]0[][]0[]0[]0[]0" + "$rowConstraints": "[]0[]0[]0[][]0[]0[]0[]0[grow]" } ) { name: "panel1" add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { @@ -200,8 +200,45 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 7" } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "hidemode 3" + "$columnConstraints": "[fill][grow,fill]" + "$rowConstraints": "[grow,fill][]" + } ) { + name: "messageDialogPanel" + "border": new javax.swing.border.TitledBorder( "MessageDialog" ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "messageLabel" + "text": "Message" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0,aligny top,growy 0" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "messageScrollPane" + add( new FormComponent( "javax.swing.JTextArea" ) { + name: "messageField" + "columns": 40 + "rows": 4 + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "buttonsLabel" + "text": "Buttons:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "buttonsField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1,grow" + } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 1 1 10,aligny top,growy 0" + "value": "cell 2 1 1 10,growy" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "okButtonLabelLabel" @@ -344,11 +381,30 @@ new FormModel { "value": "cell 0 11 3 1" } ) add( new FormComponent( "javax.swing.JCheckBox" ) { - name: "showMessageDialogsOnOKCheckBox" + name: "showMessageDialogOnOKCheckBox" "text": "show message dialog on OK" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 11 3 1" } ) + add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) { + name: "hSpacer1" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1,growx" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "messageDialogButton" + "text": "MessageDialog..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "messageDialog", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1,alignx right,growx 0" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "messageBoxButton" + "text": "MessageBox..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "messageBox", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "filesScrollPane" add( new FormComponent( "javax.swing.JTextArea" ) { @@ -360,7 +416,7 @@ new FormModel { } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 845, 630 ) + "size": new java.awt.Dimension( 890, 630 ) } ) add( new FormNonVisual( "javax.swing.ButtonGroup" ) { name: "ownerButtonGroup" From d5245365759ce1546862a7f028d85dc54c60bf42 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 19 Jan 2025 16:11:42 +0100 Subject: [PATCH 18/34] System File Chooser: Linux: cross-compile native library for ARM64 on x86_64 Linux --- .github/workflows/natives.yml | 13 ++++++++++ .../flatlaf/ui/FlatNativeLinuxLibrary.java | 25 +++++++++++++++++++ .../flatlaf/util/SystemFileChooser.java | 6 ++--- .../flatlaf-natives-linux/README.md | 10 ++++++++ .../flatlaf-natives-linux/build.gradle.kts | 17 +++++++++++++ .../src/main/cpp/JNIUtils.cpp | 17 +++++++++++++ ...ormdev_flatlaf_ui_FlatNativeLinuxLibrary.h | 8 ++++++ .../FlatSystemFileChooserLinuxTest.java | 4 ++- 8 files changed, 96 insertions(+), 4 deletions(-) diff --git a/.github/workflows/natives.yml b/.github/workflows/natives.yml index 676b17ba..dfe1ef56 100644 --- a/.github/workflows/natives.yml +++ b/.github/workflows/natives.yml @@ -40,6 +40,19 @@ jobs: if: matrix.os == 'ubuntu' run: sudo apt install libgtk-3-dev + - name: Download libgtk-3.so for arm64 + if: matrix.os == 'ubuntu' + working-directory: flatlaf-natives/flatlaf-natives-linux/lib/aarch64 + run: | + pwd + ls -l /usr/lib/x86_64-linux-gnu/libgtk* + wget --no-verbose https://ports.ubuntu.com/pool/main/g/gtk%2b3.0/libgtk-3-0_3.24.18-1ubuntu1_arm64.deb + ls -l + ar -x libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz + tar -xvf data.tar.xz --wildcards --to-stdout "./usr/lib/aarch64-linux-gnu/libgtk-3.so.0.*" > libgtk-3.so + rm libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz + ls -l + - name: install g++-aarch64-linux-gnu if: matrix.os == 'ubuntu' run: sudo apt install g++-aarch64-linux-gnu diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java index 1cbefc97..51770be4 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java @@ -49,6 +49,9 @@ public class FlatNativeLinuxLibrary return SystemInfo.isLinux && FlatNativeLibrary.isLoaded( API_VERSION_LINUX ); } + + //---- X Window System ---------------------------------------------------- + // direction for _NET_WM_MOVERESIZE message // see https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html static final int MOVE = 8; @@ -118,6 +121,28 @@ public class FlatNativeLinuxLibrary } + //---- GTK ---------------------------------------------------------------- + + private static Boolean isGtk3Available; + + /** + * Checks whether GTK 3 is available. + * Use this before invoking any native method that uses GTK. + * Otherwise the app may terminate immediately if GTK is not installed. + *

    + * This works because Java uses {@code dlopen(RTLD_LAZY)} to load JNI libraries, + * which only resolves symbols as the code that references them is executed. + * + * @since 3.6 + */ + public static boolean isGtk3Available() { + if( isGtk3Available == null ) + isGtk3Available = isLibAvailable( "libgtk-3.so.0" ) || isLibAvailable( "libgtk-3.so" ); + return isGtk3Available; + } + + private native static boolean isLibAvailable( String libname ); + /** * https://docs.gtk.org/gtk3/iface.FileChooser.html#properties * 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 f3740102..cd25e182 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 @@ -49,8 +49,8 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; * If there are no compile errors, then there is a good chance that it works without further changes. * If there are compile errors, then you're using a feature that {@code SystemFileChooser} does not support. *

    - * Supported platforms are Windows 10+, macOS 10.14+ and Linux GTK 3. - * {@code JFileChooser} is used on unsupported platforms. + * Supported platforms are Windows 10+, macOS 10.14+ and Linux with GTK 3. + * {@code JFileChooser} is used on unsupported platforms or if GTK 3 is not installed. *

    * {@code SystemFileChooser} requires FlatLaf native libraries (usually contained in flatlaf.jar). * If not available or disabled (via {@link FlatSystemProperties#USE_NATIVE_LIBRARY} @@ -421,7 +421,7 @@ public class SystemFileChooser return new WindowsFileChooserProvider(); else if( SystemInfo.isMacOS && FlatNativeMacLibrary.isLoaded() ) return new MacFileChooserProvider(); - else if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isLoaded() ) + else if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isLoaded() && FlatNativeLinuxLibrary.isGtk3Available() ) return new LinuxFileChooserProvider(); else // unknown platform or FlatLaf native library not loaded return new SwingFileChooserProvider(); diff --git a/flatlaf-natives/flatlaf-natives-linux/README.md b/flatlaf-natives/flatlaf-natives-linux/README.md index 0fcc9877..3d4c2051 100644 --- a/flatlaf-natives/flatlaf-natives-linux/README.md +++ b/flatlaf-natives/flatlaf-natives-linux/README.md @@ -43,6 +43,16 @@ Only on x86_64 Linux for cross-compiling for arm64 architecture: sudo apt install g++-aarch64-linux-gnu ~~~ +Download `libgtk-3.so` for arm64 architecture: + +~~~ +cd flatlaf-natives/flatlaf-natives-linux/lib/aarch64 +wget --no-verbose https://ports.ubuntu.com/pool/main/g/gtk%2b3.0/libgtk-3-0_3.24.18-1ubuntu1_arm64.deb +ar -x libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz +tar -xvf data.tar.xz --wildcards --to-stdout "./usr/lib/aarch64-linux-gnu/libgtk-3.so.0.*" > libgtk-3.so +rm libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz +~~~ + ### Fedora diff --git a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts index 9ddee01b..6b286266 100644 --- a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts @@ -149,7 +149,20 @@ tasks { "-I", "${javaHome}/include/linux", "-I", "$include", + // for GTK + "-I", "/usr/include/gtk-3.0", + "-I", "/usr/include/glib-2.0", + "-I", "/usr/lib/x86_64-linux-gnu/glib-2.0/include", + "-I", "/usr/include/gdk-pixbuf-2.0", + "-I", "/usr/include/atk-1.0", + "-I", "/usr/include/cairo", + "-I", "/usr/include/pango-1.0", + "-I", "/usr/include/harfbuzz", + "$src/ApiVersion.cpp", + "$src/GtkFileChooser.cpp", + "$src/GtkMessageDialog.cpp", + "$src/JNIUtils.cpp", "$src/X11WmUtils.cpp", ) } @@ -173,10 +186,14 @@ tasks { "-o", "$outDir/$libraryName", "$objDir/ApiVersion.o", + "$objDir/GtkFileChooser.o", + "$objDir/GtkMessageDialog.o", + "$objDir/JNIUtils.o", "$objDir/X11WmUtils.o", "-L${layout.projectDirectory}/lib/aarch64", "-ljawt", + "-lgtk-3", ) doLast { diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp index bd587070..4edea7e4 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp @@ -17,6 +17,7 @@ // avoid inlining of printf() #define _NO_CRT_STDIO_INLINE +#include #include "JNIUtils.h" /** @@ -35,3 +36,19 @@ AutoReleaseStringUTF8::~AutoReleaseStringUTF8() { if( chars != NULL ) env->ReleaseStringUTFChars( javaString, chars ); } + +//---- JNI methods ------------------------------------------------------------ + +extern "C" +JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_isLibAvailable + ( JNIEnv* env, jclass cls, jstring libname ) +{ + AutoReleaseStringUTF8 clibname( env, libname ); + + void* lib = dlopen( clibname, RTLD_LAZY ); + if( lib == NULL ) + return false; + + dlclose( lib ); + return true; +} diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h index 5e9573b4..0c581640 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h @@ -37,6 +37,14 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xM JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xShowWindowMenu (JNIEnv *, jclass, jobject, jint, jint); +/* + * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary + * Method: isLibAvailable + * Signature: (Ljava/lang/String;)Z + */ +JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_isLibAvailable + (JNIEnv *, jclass, jstring); + /* * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary * Method: showFileChooser diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java index 0c4f59f6..df89e063 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java @@ -49,7 +49,7 @@ public class FlatSystemFileChooserLinuxTest } FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserLinuxTest" ); - addListeners( frame ); +// addListeners( frame ); frame.showFrame( FlatSystemFileChooserLinuxTest::new ); } ); } @@ -134,6 +134,8 @@ public class FlatSystemFileChooserLinuxTest return true; }; + System.out.println( FlatNativeLinuxLibrary.isGtk3Available() ); + if( direct ) { String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, open, title, okButtonLabel, currentName, currentFolder, From f3ca3a001a730f42f6b56229e2646ea61a0d79bb Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 20 Jan 2025 16:14:45 +0100 Subject: [PATCH 19/34] System File Chooser: added "approve" callback to `SystemFileChooser` --- .../flatlaf/util/SystemFileChooser.java | 281 +++++++++++++++++- .../themeeditor/FlatThemeFileEditor.java | 86 ++++-- 2 files changed, 333 insertions(+), 34 deletions(-) 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 cd25e182..ae08f63f 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 @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; import java.util.concurrent.atomic.AtomicReference; +import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; @@ -81,6 +82,7 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; * as first item and selects it by default. * Use {@code chooser.addChoosableFileFilter( chooser.getAcceptAllFileFilter() )} * to place All Files filter somewhere else. + *

  • Accessory components are not supported. * * * @author Karl Tauber @@ -131,6 +133,9 @@ public class SystemFileChooser */ private boolean keepAcceptAllAtEnd = true; + private ApproveCallback approveCallback; + private int approveResult = APPROVE_OPTION; + /** @see JFileChooser#JFileChooser() */ public SystemFileChooser() { this( (File) null ); @@ -407,10 +412,71 @@ public class SystemFileChooser return filters.size() == 1 && filters.get( 0 ) == getAcceptAllFileFilter(); } + public ApproveCallback getApproveCallback() { + return approveCallback; + } + + /** + * Sets a callback that is invoked when user presses "OK" button (or double-clicks a file). + * The file dialog is still open. + * If the callback returns {@link #CANCEL_OPTION}, then the file dialog stays open. + * If it returns {@link #APPROVE_OPTION} (or any other value other than {@link #CANCEL_OPTION}), + * the file dialog is closed and the {@code show...Dialog()} methods return that value. + *

    + * The callback has two parameters: + *

      + *
    • {@code File[] selectedFiles} - one or more selected files + *
    • {@code ApproveContext context} - context object that provides additional methods + *
    + * + *
    {@code
    +	 * chooser.setApproveCallback( (selectedFiles, context) -> {
    +	 *     // do something
    +	 *     return SystemFileChooser.APPROVE_OPTION; // or SystemFileChooser.CANCEL_OPTION
    +	 * } );
    +	 * }
    + * + * or + * + *
    {@code
    +	 * chooser.setApproveCallback( this::approveCallback );
    +	 *
    +	 * ...
    +	 *
    +	 * private boolean approveCallback( File[] selectedFiles, ApproveContext context ) {
    +	 *     // do something
    +	 *     return SystemFileChooser.APPROVE_OPTION; // or SystemFileChooser.CANCEL_OPTION
    +	 * }
    +	 * }
    + * + * WARNING: Do not show a Swing dialog for the callback. This will not work! + *

    + * Instead use {@link ApproveContext#showMessageDialog(int, String, String, int, String...)}, + * which shows a modal system message dialog as child of the file dialog. + * + *

    {@code
    +	 * chooser.setApproveCallback( (selectedFiles, context) -> {
    +	 *     if( !selectedFiles[0].getName().startsWith( "blabla" ) ) {
    +	 *         context.showMessageDialog( JOptionPane.WARNING_MESSAGE,
    +	 *             "File name must start with 'blabla' :)", null, 0 );
    +	 *         return SystemFileChooser.CANCEL_OPTION;
    +	 *     }
    +	 *     return SystemFileChooser.APPROVE_OPTION;
    +	 * } );
    +	 * }
    + * + * @see ApproveContext + * @see JFileChooser#approveSelection() + */ + public void setApproveCallback( ApproveCallback approveCallback ) { + this.approveCallback = approveCallback; + } + private int showDialogImpl( Component parent ) { + approveResult = APPROVE_OPTION; File[] files = getProvider().showDialog( parent, this ); setSelectedFiles( files ); - return (files != null) ? APPROVE_OPTION : CANCEL_OPTION; + return (files != null) ? approveResult : CANCEL_OPTION; } private FileChooserProvider getProvider() { @@ -464,14 +530,31 @@ public class SystemFileChooser return null; // convert file names to file objects + return filenames2files( filenames ); + } + + abstract String[] showSystemDialog( Window owner, SystemFileChooser fc ); + + boolean invokeApproveCallback( SystemFileChooser fc, String[] files, ApproveContext context ) { + if( files == null || files.length == 0 ) + return false; // should never happen + + ApproveCallback approveCallback = fc.getApproveCallback(); + int result = approveCallback.approve( filenames2files( files ), context ); + if( result == CANCEL_OPTION ) + return false; + + fc.approveResult = result; + return true; + } + + private static File[] filenames2files( String[] filenames ) { FileSystemView fsv = FileSystemView.getFileSystemView(); File[] files = new File[filenames.length]; for( int i = 0; i < filenames.length; i++ ) files[i] = fsv.createFileObject( filenames[i] ); return files; } - - abstract String[] showSystemDialog( Window owner, SystemFileChooser fc ); } //---- class WindowsFileChooserProvider ----------------------------------- @@ -549,12 +632,50 @@ public class SystemFileChooser } } + // callback + FlatNativeWindowsLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null) + ? (files, hwndFileDialog) -> { + return invokeApproveCallback( fc, files, new WindowsApproveContext( hwndFileDialog ) ); + } : null; + // show system file dialog return FlatNativeWindowsLibrary.showFileChooser( owner, open, fc.getDialogTitle(), approveButtonText, null, fileName, - folder, saveAsItem, null, null, optionsSet, optionsClear, null, + folder, saveAsItem, null, null, optionsSet, optionsClear, callback, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); } + + //---- class WindowsApproveContext ---- + + private static class WindowsApproveContext + extends ApproveContext + { + private final long hwndFileDialog; + + WindowsApproveContext( long hwndFileDialog ) { + this.hwndFileDialog = hwndFileDialog; + } + + @Override + public int showMessageDialog( int messageType, String primaryText, + String secondaryText, int defaultButton, String... buttons ) + { + // concat primary and secondary texts + if( secondaryText != null ) + primaryText = primaryText + "\n\n" + secondaryText; + + // button menmonics ("&" -> "&&", "__" -> "_", "_" -> "&") + for( int i = 0; i < buttons.length; i++ ) + buttons[i] = buttons[i].replace( "&", "&&" ).replace( "__", "\u0001" ).replace( '_', '&' ).replace( '\u0001', '_' ); + + // use "OK" button if no buttons given + if( buttons.length == 0 ) + buttons = new String[] { UIManager.getString( "OptionPane.okButtonText", Locale.getDefault() ) }; + + return FlatNativeWindowsLibrary.showMessageDialog( hwndFileDialog, + messageType, null, primaryText, defaultButton, buttons ); + } + } } //---- class MacFileChooserProvider --------------------------------------- @@ -613,12 +734,42 @@ public class SystemFileChooser } } + // callback + FlatNativeMacLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null) + ? (files, hwndFileDialog) -> { + return invokeApproveCallback( fc, files, new MacApproveContext( hwndFileDialog ) ); + } : null; + // show system file dialog return FlatNativeMacLibrary.showFileChooser( open, fc.getDialogTitle(), fc.getApproveButtonText(), null, null, null, - nameFieldStringValue, directoryURL, optionsSet, optionsClear, null, + nameFieldStringValue, directoryURL, optionsSet, optionsClear, callback, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); } + + //---- class MacApproveContext ---- + + private static class MacApproveContext + extends ApproveContext + { + private final long hwndFileDialog; + + MacApproveContext( long hwndFileDialog ) { + this.hwndFileDialog = hwndFileDialog; + } + + @Override + public int showMessageDialog( int messageType, String primaryText, + String secondaryText, int defaultButton, String... buttons ) + { + // remove button menmonics ("__" -> "_", "_" -> "") + for( int i = 0; i < buttons.length; i++ ) + buttons[i] = buttons[i].replace( "__", "\u0001" ).replace( "_", "" ).replace( "\u0001", "_" ); + + return FlatNativeMacLibrary.showMessageDialog( hwndFileDialog, + messageType, primaryText, secondaryText, defaultButton, buttons ); + } + } } //---- class LinuxFileChooserProvider ------------------------------------- @@ -690,10 +841,17 @@ public class SystemFileChooser } } + // callback + FlatNativeLinuxLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null) + ? (files, hwndFileDialog) -> { + return invokeApproveCallback( fc, files, new LinuxApproveContext( hwndFileDialog ) ); + } : null; + // show system file dialog return FlatNativeLinuxLibrary.showFileChooser( owner, open, fc.getDialogTitle(), approveButtonText, currentName, currentFolder, - optionsSet, optionsClear, null, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); + optionsSet, optionsClear, callback, + fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); } private String caseInsensitiveGlobPattern( String ext ) { @@ -712,6 +870,26 @@ public class SystemFileChooser } return buf.toString(); } + + //---- class LinuxApproveContext ---- + + private static class LinuxApproveContext + extends ApproveContext + { + private final long hwndFileDialog; + + LinuxApproveContext( long hwndFileDialog ) { + this.hwndFileDialog = hwndFileDialog; + } + + @Override + public int showMessageDialog( int messageType, String primaryText, + String secondaryText, int defaultButton, String... buttons ) + { + return FlatNativeLinuxLibrary.showMessageDialog( hwndFileDialog, + messageType, primaryText, secondaryText, defaultButton, buttons ); + } + } } //---- class SwingFileChooserProvider ------------------------------------- @@ -727,6 +905,8 @@ public class SystemFileChooser File[] files = isMultiSelectionEnabled() ? getSelectedFiles() : new File[] { getSelectedFile() }; + if( files == null || files.length == 0 ) + return; // should never happen if( getDialogType() == OPEN_DIALOG || isDirectorySelectionEnabled() ) { if( !checkMustExist( this, files ) ) @@ -735,6 +915,17 @@ public class SystemFileChooser if( !checkOverwrite( this, files ) ) return; } + + // callback + ApproveCallback approveCallback = fc.getApproveCallback(); + if( approveCallback != null ) { + int result = approveCallback.approve( files, new SwingApproveContext( this ) ); + if( result == CANCEL_OPTION ) + return; + + fc.approveResult = result; + } + super.approveSelection(); } }; @@ -831,6 +1022,47 @@ public class SystemFileChooser } return true; } + + //---- class SwingApproveContext ---- + + private static class SwingApproveContext + extends ApproveContext + { + private final JFileChooser chooser; + + SwingApproveContext( JFileChooser chooser ) { + this.chooser = chooser; + } + + @Override + public int showMessageDialog( int messageType, String primaryText, + String secondaryText, int defaultButton, String... buttons ) + { + // title + String title = chooser.getDialogTitle(); + if( title == null ) { + Window window = SwingUtilities.windowForComponent( chooser ); + if( window instanceof JDialog ) + title = ((JDialog)window).getTitle(); + } + + // concat primary and secondary texts + if( secondaryText != null ) + primaryText = primaryText + "\n\n" + secondaryText; + + // remove button menmonics ("__" -> "_", "_" -> "") + for( int i = 0; i < buttons.length; i++ ) + buttons[i] = buttons[i].replace( "__", "\u0001" ).replace( "_", "" ).replace( "\u0001", "_" ); + + // use "OK" button if no buttons given + if( buttons.length == 0 ) + buttons = new String[] { UIManager.getString( "OptionPane.okButtonText", Locale.getDefault() ) }; + + return JOptionPane.showOptionDialog( chooser, + primaryText, title, JOptionPane.YES_NO_OPTION, messageType, + null, buttons, buttons[Math.min( Math.max( defaultButton, 0 ), buttons.length - 1 )] ); + } + } } //---- class FileFilter --------------------------------------------------- @@ -892,4 +1124,41 @@ public class SystemFileChooser return UIManager.getString( "FileChooser.acceptAllFileFilterText" ); } } + + //---- class ApproveCallback ---------------------------------------------- + + public interface ApproveCallback { + /** + * @param selectedFiles one or more selected files + * @param context context object that provides additional methods + * @return If the callback returns {@link #CANCEL_OPTION}, then the file dialog stays open. + * If it returns {@link #APPROVE_OPTION} (or any other value other than {@link #CANCEL_OPTION}), + * the file dialog is closed and the {@code show...Dialog()} methods return that value. + */ + int approve( File[] selectedFiles, ApproveContext context ); + } + + //---- class ApproveContext ----------------------------------------------- + + public static abstract class ApproveContext { + /** + * Shows a modal (operating system) message dialog as child of the system file chooser. + *

    + * Use this instead of {@link JOptionPane} in approve callbacks. + * + * @param messageType type of message being displayed: + * {@link JOptionPane#ERROR_MESSAGE}, {@link JOptionPane#INFORMATION_MESSAGE}, + * {@link JOptionPane#WARNING_MESSAGE}, {@link JOptionPane#QUESTION_MESSAGE} or + * {@link JOptionPane#PLAIN_MESSAGE} + * @param primaryText primary text + * @param secondaryText secondary text; shown below of primary text; 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. + * Use '_' for mnemonics (e.g. "_Choose") + * Use '__' for '_' character (e.g. "Choose__and__Quit"). + * @return index of pressed button; or -1 for ESC key + */ + public abstract int showMessageDialog( int messageType, String primaryText, + String secondaryText, int defaultButton, String... buttons ); + } } diff --git a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java index 9340627a..ab456671 100644 --- a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java +++ b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java @@ -73,6 +73,7 @@ import com.formdev.flatlaf.themes.FlatMacDarkLaf; import com.formdev.flatlaf.themes.FlatMacLightLaf; import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.util.StringUtils; +import com.formdev.flatlaf.util.SystemFileChooser; import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; @@ -96,6 +97,8 @@ class FlatThemeFileEditor private static final String KEY_SHOW_RGB_COLORS = "showRgbColors"; private static final String KEY_SHOW_COLOR_LUMA = "showColorLuma"; + private static final int NEW_PROPERTIES_FILE_OPTION = 100; + private File dir; private Preferences state; private boolean inLoadDirectory; @@ -227,48 +230,45 @@ class FlatThemeFileEditor return; // choose directory - JFileChooser chooser = new JFileChooser( dir ) { - @Override - public void approveSelection() { - if( !checkDirectory( this, getSelectedFile() ) ) - return; - - super.approveSelection(); - } - }; - chooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY ); - if( chooser.showOpenDialog( this ) != JFileChooser.APPROVE_OPTION ) + SystemFileChooser chooser = new SystemFileChooser( dir ); + chooser.setFileSelectionMode( SystemFileChooser.DIRECTORIES_ONLY ); + chooser.setApproveCallback( this::checkDirectory ); + int result = chooser.showOpenDialog( this ); + if( result == SystemFileChooser.CANCEL_OPTION ) return; File selectedFile = chooser.getSelectedFile(); if( selectedFile == null || selectedFile.equals( dir ) ) return; + if( result == NEW_PROPERTIES_FILE_OPTION ) { + if( !newPropertiesFile( selectedFile ) ) + return; + } + // open new directory loadDirectory( selectedFile ); } - private boolean checkDirectory( Component parentComponent, File dir ) { + private int checkDirectory( File[] selectedFiles, SystemFileChooser.ApproveContext context ) { + File dir = selectedFiles[0]; if( !dir.isDirectory() ) { - JOptionPane.showMessageDialog( parentComponent, - "Directory '" + dir + "' does not exist.", - getTitle(), JOptionPane.INFORMATION_MESSAGE ); - return false; + showMessageDialog( context, "Directory '" + dir + "' does not exist.", null ); + return SystemFileChooser.CANCEL_OPTION; } if( getPropertiesFiles( dir ).length == 0 ) { UIManager.put( "OptionPane.sameSizeButtons", false ); - int result = JOptionPane.showOptionDialog( parentComponent, - "Directory '" + dir + "' does not contain properties files.\n\n" - + "Do you want create a new theme in this directory?\n\n" + int result = showMessageDialog( context, + "Directory '" + dir + "' does not contain properties files.", + "Do you want create a new theme in this directory?\n\n" + "Or do you want modify/extend core themes and create empty" + " 'FlatLightLaf.properties' and 'FlatDarkLaf.properties' files in this directory?", - getTitle(), JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, null, - new Object[] { "New Theme", "Modify Core Themes", "Cancel" }, null ); + "_New Theme", "_Modify Core Themes", "_Cancel" ); UIManager.put( "OptionPane.sameSizeButtons", null ); if( result == 0 ) - return newPropertiesFile( dir ); + return NEW_PROPERTIES_FILE_OPTION; else if( result == 1 ) { try { String content = @@ -280,18 +280,37 @@ class FlatThemeFileEditor "\n"; writeFile( new File( dir, "FlatLightLaf.properties" ), content ); writeFile( new File( dir, "FlatDarkLaf.properties" ), content ); - return true; + return SystemFileChooser.APPROVE_OPTION; } catch( IOException ex ) { ex.printStackTrace(); - JOptionPane.showMessageDialog( parentComponent, - "Failed to create 'FlatLightLaf.properties' or 'FlatDarkLaf.properties'." ); + showMessageDialog( context, + "Failed to create 'FlatLightLaf.properties' or 'FlatDarkLaf.properties'.", null ); } } - return false; + return SystemFileChooser.CANCEL_OPTION; } - return true; + return SystemFileChooser.APPROVE_OPTION; + } + + private int showMessageDialog( SystemFileChooser.ApproveContext context, + String primaryText, String secondaryText, String... buttons ) + { + if( context != null ) { + // invoked from SystemFileChooser + return context.showMessageDialog( JOptionPane.INFORMATION_MESSAGE, + primaryText, secondaryText, 0, buttons ); + } else { + // invoked from directoryChanged() + if( secondaryText != null ) + primaryText = primaryText + "\n\n" + secondaryText; + for( int i = 0; i < buttons.length; i++ ) + buttons[i] = buttons[i].replace( "_", "" ); + return JOptionPane.showOptionDialog( this, primaryText, getTitle(), + JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, null, + (buttons.length > 0) ? buttons : null, null ); + } } private void directoryChanged() { @@ -302,7 +321,15 @@ class FlatThemeFileEditor if( dir == null ) return; - if( checkDirectory( this, dir ) ) + directoryField.hidePopup(); + + int result = checkDirectory( new File[] { dir }, null ); + if( result == NEW_PROPERTIES_FILE_OPTION ) { + if( !newPropertiesFile( dir ) ) + return; + } + + if( result != SystemFileChooser.CANCEL_OPTION ) loadDirectory( dir ); else { // remove from directories history @@ -390,6 +417,9 @@ class FlatThemeFileEditor File[] propertiesFiles = dir.listFiles( (d, name) -> { return name.endsWith( ".properties" ); } ); + if( propertiesFiles == null ) + propertiesFiles = new File[0]; + Arrays.sort( propertiesFiles, (f1, f2) -> { String n1 = toSortName( f1.getName() ); String n2 = toSortName( f2.getName() ); From b808f6e803d6bdc2dfcca473e037698114174900 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 20 Jan 2025 18:47:54 +0100 Subject: [PATCH 20/34] System File Chooser: support platform specific features --- .../flatlaf/util/SystemFileChooser.java | 192 ++++++++++++++++-- .../testing/FlatSystemFileChooserTest.java | 11 + 2 files changed, 191 insertions(+), 12 deletions(-) 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 ae08f63f..39fa5a39 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 @@ -23,7 +23,9 @@ import java.awt.Window; import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import javax.swing.JDialog; import javax.swing.JFileChooser; @@ -63,10 +65,10 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; *

  • Open File and Select Folder dialogs always warn about not existing files/folders. * The operating system shows a warning dialog to inform the user. * It is not possible to customize that warning dialog. - * The file chooser stays open. + * The file dialog stays open. *
  • Save File dialog always asks whether an existing file should be overwritten. * The operating system shows a question dialog to ask the user whether he wants to overwrite the file or not. - * If user selects "Yes", the file chooser closes. If user selects "No", the file chooser stays open. + * If user selects "Yes", the file dialog closes. If user selects "No", the file dialog stays open. * It is not possible to customize that question dialog. *
  • Save File dialog does not support multi-selection. *
  • For selection mode {@link #DIRECTORIES_ONLY}, dialog type {@link #SAVE_DIALOG} is ignored. @@ -83,6 +85,9 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; * Use {@code chooser.addChoosableFileFilter( chooser.getAcceptAllFileFilter() )} * to place All Files filter somewhere else. *
  • Accessory components are not supported. + *
  • macOS: By default, the user can not navigate into file packages (e.g. applications). + * If needed, this can be enabled by setting platform property + * {@link #MAC_TREATS_FILE_PACKAGES_AS_DIRECTORIES} to {@code true}. * * * @author Karl Tauber @@ -136,6 +141,112 @@ public class SystemFileChooser private ApproveCallback approveCallback; private int approveResult = APPROVE_OPTION; + + /** + * Windows: Text displayed in front of the filename text field. + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String WINDOWS_FILE_NAME_LABEL = "windows.fileNameLabel"; + + /** + * Windows: Folder used as a default if there is not a recently used folder value available. + * Windows somewhere stores default folder on a per-app basis. + * So this is probably used only once when the app opens a file dialog for first time. + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String WINDOWS_DEFAULT_FOLDER = "windows.defaultFolder"; + + /** + * Windows: Default extension to be added to file name in save dialog. + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String WINDOWS_DEFAULT_EXTENSION = "windows.defaultExtension"; + + /** + * macOS: Text displayed at top of open/save dialogs. + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_MESSAGE = "mac.message"; + + /** + * macOS: Text displayed in front of the filter combobox. + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_FILTER_FIELD_LABEL = "mac.filterFieldLabel"; + + /** + * macOS: Text displayed in front of the filename text field in save dialog (not used in open dialog). + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_NAME_FIELD_LABEL = "mac.nameFieldLabel"; + + /** + * macOS: If {@code true}, displays file packages (e.g. applications) as directories + * and allows the user to navigate into the file package. + * Value type must be {@link Boolean}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_TREATS_FILE_PACKAGES_AS_DIRECTORIES = "mac.treatsFilePackagesAsDirectories"; + + /** + * Windows: Low-level options to set. See {@code FOS_*} constants in {@link FlatNativeWindowsLibrary}. + * Options {@code FOS_PICKFOLDERS}, {@code FOS_ALLOWMULTISELECT} and {@code FOS_FORCESHOWHIDDEN} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String WINDOWS_OPTIONS_SET = "windows.optionsSet"; + + /** + * Windows: Low-level options to clear. See {@code FOS_*} constants in {@link FlatNativeWindowsLibrary}. + * Options {@code FOS_PICKFOLDERS}, {@code FOS_ALLOWMULTISELECT} and {@code FOS_FORCESHOWHIDDEN} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String WINDOWS_OPTIONS_CLEAR = "windows.optionsClear"; + + /** + * macOS: Low-level options to set. See {@code FC_*} constants in {@link FlatNativeMacLibrary}. + * Options {@code FC_canChooseFiles}, {@code FC_canChooseDirectories}, + * {@code FC_allowsMultipleSelection} and {@code FC_showsHiddenFiles} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_OPTIONS_SET = "mac.optionsSet"; + + /** + * macOS: Low-level options to clear. See {@code FC_*} constants in {@link FlatNativeMacLibrary}. + * Options {@code FC_canChooseFiles}, {@code FC_canChooseDirectories}, + * {@code FC_allowsMultipleSelection} and {@code FC_showsHiddenFiles} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_OPTIONS_CLEAR = "mac.optionsClear"; + + /** + * Linux: Low-level options to set. See {@code FC_*} constants in {@link FlatNativeLinuxLibrary}. + * Options {@code FC_select_folder}, {@code FC_select_multiple} and {@code FC_show_hidden} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String LINUX_OPTIONS_SET = "linux.optionsSet"; + + /** + * Linux: Low-level options to clear. See {@code FC_*} constants in {@link FlatNativeLinuxLibrary}. + * Options {@code FC_select_folder}, {@code FC_select_multiple} and {@code FC_show_hidden} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String LINUX_OPTIONS_CLEAR = "linux.optionsClear"; + + private Map platformProperties; + + /** @see JFileChooser#JFileChooser() */ public SystemFileChooser() { this( (File) null ); @@ -472,6 +583,38 @@ public class SystemFileChooser this.approveCallback = approveCallback; } + @SuppressWarnings( "unchecked" ) + public T getPlatformProperty( String key ) { + return (platformProperties != null) ? (T) platformProperties.get( key ) : null; + } + + /** + * Set a platform specific file dialog property. + *

    + * For supported properties see {@code WINDOWS_}, {@code MAC_} and {@code LINUX_} constants in this class. + * + *

    {@code
    +	 * chooser.putPlatformProperty( SystemFileChooser.WINDOWS_FILE_NAME_LABEL, "My filename label:" );
    +	 * chooser.putPlatformProperty( SystemFileChooser.MAC_TREATS_FILE_PACKAGES_AS_DIRECTORIES, true );
    +	 * chooser.putPlatformProperty( SystemFileChooser.LINUX_OPTIONS_CLEAR,
    +	 *     FlatNativeLinuxLibrary.FC_create_folders | FlatNativeLinuxLibrary.FC_do_overwrite_confirmation );
    +	 * }
    + */ + public void putPlatformProperty( String key, Object value ) { + if( platformProperties == null ) + platformProperties = new HashMap<>(); + + if( value != null ) + platformProperties.put( key, value ); + else + platformProperties.remove( key ); + } + + private int getPlatformOptions( String key, int optionsBlocked ) { + Object value = getPlatformProperty( key ); + return (value instanceof Integer) ? (Integer) value & ~optionsBlocked : 0; + } + private int showDialogImpl( Component parent ) { approveResult = APPROVE_OPTION; File[] files = getProvider().showDialog( parent, this ); @@ -597,8 +740,13 @@ public class SystemFileChooser folder = currentDirectory.getAbsolutePath(); // options - int optionsSet = FlatNativeWindowsLibrary.FOS_OVERWRITEPROMPT; - int optionsClear = 0; + int optionsBlocked = FlatNativeWindowsLibrary.FOS_PICKFOLDERS + | FlatNativeWindowsLibrary.FOS_ALLOWMULTISELECT + | FlatNativeWindowsLibrary.FOS_FORCESHOWHIDDEN; + int optionsSet = fc.getPlatformOptions( WINDOWS_OPTIONS_SET, optionsBlocked ); + int optionsClear = fc.getPlatformOptions( WINDOWS_OPTIONS_CLEAR, optionsBlocked ); + if( (optionsClear & FlatNativeWindowsLibrary.FOS_OVERWRITEPROMPT) == 0 ) + optionsSet |= FlatNativeWindowsLibrary.FOS_OVERWRITEPROMPT; if( fc.isDirectorySelectionEnabled() ) optionsSet |= FlatNativeWindowsLibrary.FOS_PICKFOLDERS; if( fc.isMultiSelectionEnabled() ) @@ -640,8 +788,12 @@ public class SystemFileChooser // show system file dialog return FlatNativeWindowsLibrary.showFileChooser( owner, open, - fc.getDialogTitle(), approveButtonText, null, fileName, - folder, saveAsItem, null, null, optionsSet, optionsClear, callback, + fc.getDialogTitle(), approveButtonText, + fc.getPlatformProperty( WINDOWS_FILE_NAME_LABEL ), + fileName, folder, saveAsItem, + fc.getPlatformProperty( WINDOWS_DEFAULT_FOLDER ), + fc.getPlatformProperty( WINDOWS_DEFAULT_EXTENSION ), + optionsSet, optionsClear, callback, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); } @@ -703,8 +855,14 @@ public class SystemFileChooser directoryURL = currentDirectory.getAbsolutePath(); // options - int optionsSet = FlatNativeMacLibrary.FC_accessoryViewDisclosed; - int optionsClear = 0; + int optionsBlocked = FlatNativeMacLibrary.FC_canChooseFiles + | FlatNativeMacLibrary.FC_canChooseDirectories + | FlatNativeMacLibrary.FC_allowsMultipleSelection + | FlatNativeMacLibrary.FC_showsHiddenFiles; + int optionsSet = fc.getPlatformOptions( MAC_OPTIONS_SET, optionsBlocked ); + int optionsClear = fc.getPlatformOptions( MAC_OPTIONS_CLEAR, optionsBlocked ); + if( (optionsClear & FlatNativeMacLibrary.FC_accessoryViewDisclosed) == 0 ) + optionsSet |= FlatNativeMacLibrary.FC_accessoryViewDisclosed; if( fc.isDirectorySelectionEnabled() ) { optionsSet |= FlatNativeMacLibrary.FC_canChooseDirectories; optionsClear |= FlatNativeMacLibrary.FC_canChooseFiles; @@ -714,6 +872,8 @@ public class SystemFileChooser optionsSet |= FlatNativeMacLibrary.FC_allowsMultipleSelection; if( !fc.isFileHidingEnabled() ) optionsSet |= FlatNativeMacLibrary.FC_showsHiddenFiles; + if( Boolean.TRUE.equals( fc.getPlatformProperty( MAC_TREATS_FILE_PACKAGES_AS_DIRECTORIES ) ) ) + optionsSet |= FlatNativeMacLibrary.FC_treatsFilePackagesAsDirectories; // filter int fileTypeIndex = 0; @@ -742,7 +902,10 @@ public class SystemFileChooser // show system file dialog return FlatNativeMacLibrary.showFileChooser( open, - fc.getDialogTitle(), fc.getApproveButtonText(), null, null, null, + fc.getDialogTitle(), fc.getApproveButtonText(), + fc.getPlatformProperty( MAC_MESSAGE ), + fc.getPlatformProperty( MAC_FILTER_FIELD_LABEL ), + fc.getPlatformProperty( MAC_NAME_FIELD_LABEL ), nameFieldStringValue, directoryURL, optionsSet, optionsClear, callback, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); } @@ -811,8 +974,13 @@ public class SystemFileChooser currentFolder = currentDirectory.getAbsolutePath(); // options - int optionsSet = FlatNativeLinuxLibrary.FC_do_overwrite_confirmation; - int optionsClear = 0; + int optionsBlocked = FlatNativeLinuxLibrary.FC_select_folder + | FlatNativeLinuxLibrary.FC_select_multiple + | FlatNativeLinuxLibrary.FC_show_hidden; + int optionsSet = fc.getPlatformOptions( LINUX_OPTIONS_SET, optionsBlocked ); + int optionsClear = fc.getPlatformOptions( LINUX_OPTIONS_CLEAR, optionsBlocked ); + if( (optionsClear & FlatNativeLinuxLibrary.FC_do_overwrite_confirmation) == 0 ) + optionsSet |= FlatNativeLinuxLibrary.FC_do_overwrite_confirmation; if( fc.isDirectorySelectionEnabled() ) optionsSet |= FlatNativeLinuxLibrary.FC_select_folder; if( fc.isMultiSelectionEnabled() ) @@ -1142,7 +1310,7 @@ public class SystemFileChooser public static abstract class ApproveContext { /** - * Shows a modal (operating system) message dialog as child of the system file chooser. + * Shows a modal (operating system) message dialog as child of the system file dialog. *

    * Use this instead of {@link JOptionPane} in approve callbacks. * diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java index 6d2102a6..1a2d629e 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java @@ -202,6 +202,17 @@ public class FlatSystemFileChooserTest SystemFileChooser.FileFilter[] filters = fc.getChoosableFileFilters(); if( filters.length > 0 ) fc.setFileFilter( filters[Math.min( Math.max( fileTypeIndex, 0 ), filters.length - 1 )] ); + +// fc.putPlatformProperty( SystemFileChooser.WINDOWS_FILE_NAME_LABEL, "My filename label:" ); +// fc.putPlatformProperty( SystemFileChooser.WINDOWS_OPTIONS_SET, FlatNativeWindowsLibrary.FOS_HIDEMRUPLACES ); + +// fc.putPlatformProperty( SystemFileChooser.MAC_MESSAGE, "some message" ); +// fc.putPlatformProperty( SystemFileChooser.MAC_NAME_FIELD_LABEL, "My name label:" ); +// fc.putPlatformProperty( SystemFileChooser.MAC_FILTER_FIELD_LABEL, "My filter label" ); +// fc.putPlatformProperty( SystemFileChooser.MAC_TREATS_FILE_PACKAGES_AS_DIRECTORIES, true ); +// fc.putPlatformProperty( SystemFileChooser.MAC_OPTIONS_CLEAR, FlatNativeMacLibrary.FC_showsTagField ); + +// fc.putPlatformProperty( SystemFileChooser.LINUX_OPTIONS_CLEAR, FlatNativeLinuxLibrary.FC_create_folders | FlatNativeLinuxLibrary.FC_do_overwrite_confirmation ); } private void configureSwingFileChooser( JFileChooser fc ) { From 1e3e4d7c6114d828323f483c8db141fc168d394c Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 21 Jan 2025 02:42:34 +0100 Subject: [PATCH 21/34] System File Chooser: fixed (cross-)compile native library for ARM64 Linux --- flatlaf-natives/flatlaf-natives-linux/build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts index 6b286266..d73ab05f 100644 --- a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts @@ -70,7 +70,8 @@ tasks { // for GTK "/usr/include/gtk-3.0", "/usr/include/glib-2.0", - "/usr/lib/x86_64-linux-gnu/glib-2.0/include", + if( name.contains( "X86-64" ) ) "/usr/lib/x86_64-linux-gnu/glib-2.0/include" + else "/usr/lib/aarch64-linux-gnu/glib-2.0/include", "/usr/include/gdk-pixbuf-2.0", "/usr/include/atk-1.0", "/usr/include/cairo", @@ -191,6 +192,7 @@ tasks { "$objDir/JNIUtils.o", "$objDir/X11WmUtils.o", + "-lstdc++", "-L${layout.projectDirectory}/lib/aarch64", "-ljawt", "-lgtk-3", From aecb496142a411211c5dbebed0a10973fead612c Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 21 Jan 2025 14:33:03 +0100 Subject: [PATCH 22/34] System File Chooser: macOS: show file dialog in dark if current FlatLaf theme is dark --- .../java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java | 4 +++- .../java/com/formdev/flatlaf/util/SystemFileChooser.java | 4 +++- .../headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h | 4 ++-- .../src/main/objcpp/MacFileChooser.mm | 8 +++++++- .../src/main/objcpp/MacMessageDialog.mm | 5 +++++ .../flatlaf/testing/FlatSystemFileChooserMacTest.java | 7 +++++-- 6 files changed, 25 insertions(+), 7 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 362f8ee3..f4d17c01 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 @@ -99,6 +99,8 @@ public class FlatNativeMacLibrary * the file dialog. It is highly recommended to invoke it from a new thread * to avoid blocking the AWT event dispatching thread. * + * @param owner the owner of the file dialog; or {@code null} + * @param dark appearance of the file dialog: {@code 1} = dark, {@code 0} = light, {@code -1} = default * @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); or {@code null} * @param prompt text displayed in default button; or {@code null} @@ -120,7 +122,7 @@ public class FlatNativeMacLibrary * * @since 3.6 */ - public native static String[] showFileChooser( boolean open, + public native static String[] showFileChooser( Window owner, int dark, boolean open, String title, String prompt, String message, String filterFieldLabel, String nameFieldLabel, String nameFieldStringValue, String directoryURL, int optionsSet, int optionsClear, FileChooserCallback callback, 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 39fa5a39..79c97f9d 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 @@ -33,6 +33,7 @@ import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.filechooser.FileSystemView; +import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.FlatSystemProperties; import com.formdev.flatlaf.ui.FlatNativeLinuxLibrary; import com.formdev.flatlaf.ui.FlatNativeMacLibrary; @@ -837,6 +838,7 @@ public class SystemFileChooser { @Override String[] showSystemDialog( Window owner, SystemFileChooser fc ) { + int dark = FlatLaf.isLafDark() ? 1 : 0; boolean open = (fc.getDialogType() == OPEN_DIALOG); String nameFieldStringValue = null; String directoryURL = null; @@ -901,7 +903,7 @@ public class SystemFileChooser } : null; // show system file dialog - return FlatNativeMacLibrary.showFileChooser( open, + return FlatNativeMacLibrary.showFileChooser( owner, dark, open, fc.getDialogTitle(), fc.getApproveButtonText(), fc.getPlatformProperty( MAC_MESSAGE ), fc.getPlatformProperty( MAC_FILTER_FIELD_LABEL ), 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 329085b0..301039b2 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,10 @@ 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;IILcom/formdev/flatlaf/ui/FlatNativeMacLibrary/FileChooserCallback;I[Ljava/lang/String;)[Ljava/lang/String; + * Signature: (Ljava/awt/Window;IZLjava/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, jobject, jint, jobjectArray); + (JNIEnv *, jclass, jobject, jint, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jint, jint, jobject, jint, jobjectArray); /* * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary 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 891d97c2..44d83fce 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -232,7 +232,7 @@ static NSMutableArray* initFilters( JNIEnv* env, jobjectArray fileTypes ) { extern "C" JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_showFileChooser - ( JNIEnv* env, jclass cls, jboolean open, + ( JNIEnv* env, jclass cls, jobject owner, jint dark, jboolean open, jstring title, jstring prompt, jstring message, jstring filterFieldLabel, jstring nameFieldLabel, jstring nameFieldStringValue, jstring directoryURL, jint optionsSet, jint optionsClear, jobject callback, jint fileTypeIndex, jobjectArray fileTypes ) @@ -262,6 +262,12 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ NSSavePanel* dialog = open ? [NSOpenPanel openPanel] : [NSSavePanel savePanel]; + // set appearance + if( dark == 1 ) + dialog.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + else if( dark == 0 ) + dialog.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + if( nsTitle != NULL ) dialog.title = nsTitle; if( nsPrompt != NULL ) diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm index d186c8e6..b8d3ee9b 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm @@ -51,6 +51,11 @@ JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_showMess [FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){ NSAlert* alert = [[NSAlert alloc] init]; + // use appearance from parent window + NSWindow* parent = (NSWindow*) hwndParent; + if( parent != NULL ) + alert.window.appearance = parent.appearance; + // use empty string because if alert.messageText is not set it displays "Alert" alert.messageText = (nsMessageText != NULL) ? nsMessageText : @""; if( nsInformativeText != NULL ) 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 de339d50..306e90b4 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 @@ -28,6 +28,7 @@ import java.awt.event.WindowStateListener; import java.util.Arrays; import java.util.concurrent.atomic.AtomicInteger; import javax.swing.*; +import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.extras.components.*; import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; import com.formdev.flatlaf.ui.FlatNativeMacLibrary; @@ -87,6 +88,7 @@ public class FlatSystemFileChooserMacTest } private void openOrSave( boolean open, boolean direct ) { + Window owner = SwingUtilities.windowForComponent( this ); String title = n( titleField.getText() ); String prompt = n( promptField.getText() ); String message = n( messageField.getText() ); @@ -146,8 +148,9 @@ public class FlatSystemFileChooserMacTest return true; }; + int dark = FlatLaf.isLafDark() ? 1 : 0; if( direct ) { - String[] files = FlatNativeMacLibrary.showFileChooser( open, + String[] files = FlatNativeMacLibrary.showFileChooser( owner, dark, open, title, prompt, message, filterFieldLabel, nameFieldLabel, nameFieldStringValue, directoryURL, optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes ); @@ -158,7 +161,7 @@ public class FlatSystemFileChooserMacTest String[] fileTypes2 = fileTypes; new Thread( () -> { - String[] files = FlatNativeMacLibrary.showFileChooser( open, + String[] files = FlatNativeMacLibrary.showFileChooser( owner, dark, open, title, prompt, message, filterFieldLabel, nameFieldLabel, nameFieldStringValue, directoryURL, optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 ); From 3283cfe22f11f5b2fdeb7de3ec72b1ade770b0f8 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 22 Jan 2025 13:36:11 +0100 Subject: [PATCH 23/34] System File Chooser: macOS: disable screen menu bar when file dialog is shown Testing: reduced duplicate code --- .../src/main/objcpp/MacFileChooser.mm | 56 +++- .../FlatSystemFileChooserLinuxTest.java | 123 ++----- .../FlatSystemFileChooserLinuxTest.jfd | 54 ++++ .../testing/FlatSystemFileChooserMacTest.java | 264 +++++++-------- .../testing/FlatSystemFileChooserMacTest.jfd | 214 +++++++++++-- .../testing/FlatSystemFileChooserTest.java | 300 ++++++++++++++---- .../testing/FlatSystemFileChooserTest.jfd | 169 ++++++++++ .../FlatSystemFileChooserWindowsTest.java | 157 ++------- .../FlatSystemFileChooserWindowsTest.jfd | 123 +++++++ 9 files changed, 1012 insertions(+), 448 deletions(-) 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 44d83fce..77ecd67e 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -33,7 +33,7 @@ static NSArray* getDialogURLs( NSSavePanel* dialog ); //---- class FileChooserDelegate ---------------------------------------------- -@interface FileChooserDelegate : NSObject { +@interface FileChooserDelegate : NSObject { NSArray* _filters; JavaVM* _jvm; @@ -43,15 +43,15 @@ static NSArray* getDialogURLs( NSSavePanel* dialog ); @property (nonatomic, assign) NSSavePanel* dialog; - - (void)initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex + - (void) initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex :(NSString*)filterFieldLabel :(bool)showSingleFilterField; - - (void)selectFormat: (id)sender; - - (void)selectFormatAtIndex: (int)index; + - (void) selectFormat: (id)sender; + - (void) selectFormatAtIndex: (int)index; @end @implementation FileChooserDelegate - - (void)initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex + - (void) initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex :(NSString*)filterFieldLabel :(bool)showSingleFilterField { _filters = filters; @@ -112,12 +112,12 @@ static NSArray* getDialogURLs( NSSavePanel* dialog ); [self selectFormatAtIndex:filterIndex]; } - - (void)selectFormat:(id)sender { + - (void) selectFormat: (id)sender { NSPopUpButton* popupButton = (NSPopUpButton*) sender; [self selectFormatAtIndex:popupButton.indexOfSelectedItem]; } - - (void)selectFormatAtIndex: (int)index { + - (void) selectFormatAtIndex: (int)index { index = MIN( MAX( index, 0 ), _filters.count - 1 ); NSArray* fileTypes = [_filters objectAtIndex:index]; @@ -129,14 +129,17 @@ static NSArray* getDialogURLs( NSSavePanel* dialog ); //---- NSOpenSavePanelDelegate ---- - - (void)initCallback: (JavaVM*)jvm :(jobject)callback { + - (void) initCallback: (JavaVM*)jvm :(jobject)callback { _jvm = jvm; _callback = callback; } - - (BOOL) panel:(id) sender validateURL:(NSURL*) url error:(NSError**) outError { + - (BOOL) panel: (id) sender validateURL:(NSURL*) url error:(NSError**) outError { JNI_COCOA_TRY() + if( _callback == NULL ) + return true; + NSArray* urls = getDialogURLs( sender ); // if multiple files are selected for opening, then the validateURL method @@ -174,6 +177,33 @@ static NSArray* getDialogURLs( NSSavePanel* dialog ); return true; } + //---- NSWindowDelegate ---- + + - (void) windowDidBecomeMain:(NSNotification *) notification { + JNI_COCOA_TRY() + + // Disable main menu bar because the file dialog is modal and it should be not possible + // to select any menu item. Otherwiese an action could show a Swing dialog, which would + // be shown under the file dialog. + // + // NOTE: It is not necessary to re-enable the main menu bar because Swing does this itself. + // When the file dialog is closed and a Swing window becomes active, + // macOS sends windowDidBecomeMain (and windowDidBecomeKey) message to AWTWindow, + // which invokes [self activateWindowMenuBar], + // which invokes [CMenuBar activate:menuBar modallyDisabled:isDisabled], + // which updates main menu bar. + NSMenu* mainMenu = [NSApp mainMenu]; + int count = [mainMenu numberOfItems]; + for( int i = 0; i < count; i++ ) { + NSMenuItem* menuItem = [mainMenu itemAtIndex:i]; + NSMenu *subenu = [menuItem submenu]; + if( [subenu isJavaMenu] ) + [menuItem setEnabled:NO]; + } + + JNI_COCOA_CATCH() + } + @end //---- helper ----------------------------------------------------------------- @@ -260,6 +290,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ [FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){ JNI_COCOA_TRY() + // create open/save panel NSSavePanel* dialog = open ? [NSOpenPanel openPanel] : [NSSavePanel savePanel]; // set appearance @@ -326,10 +357,11 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ } // initialize callback - if( callback != NULL ) { + if( callback != NULL ) [delegate initCallback :jvm :callback]; - dialog.delegate = delegate; - } + + // set file dialog delegate + dialog.delegate = delegate; // show dialog NSModalResponse response = [dialog runModal]; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java index df89e063..65c68b49 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java @@ -17,21 +17,16 @@ package com.formdev.flatlaf.testing; import static com.formdev.flatlaf.ui.FlatNativeLinuxLibrary.*; -import java.awt.Dialog; import java.awt.EventQueue; import java.awt.SecondaryLoop; import java.awt.Toolkit; import java.awt.Window; -import java.awt.event.WindowAdapter; -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 java.util.concurrent.atomic.AtomicInteger; import javax.swing.*; import com.formdev.flatlaf.extras.components.*; import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; +import com.formdev.flatlaf.testing.FlatSystemFileChooserTest.DummyModalDialog; import com.formdev.flatlaf.ui.FlatNativeLinuxLibrary; import net.miginfocom.swing.*; @@ -49,7 +44,7 @@ public class FlatSystemFileChooserLinuxTest } FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserLinuxTest" ); -// addListeners( frame ); + FlatSystemFileChooserTest.addListeners( frame ); frame.showFrame( FlatSystemFileChooserLinuxTest::new ); } ); } @@ -80,19 +75,9 @@ public class FlatSystemFileChooserLinuxTest Window frame = SwingUtilities.windowForComponent( this ); if( ownerFrameRadioButton.isSelected() ) openOrSave( open, direct, frame ); - else if( ownerDialogRadioButton.isSelected() ) { - JDialog dialog = new JDialog( frame, "Dummy Modal Dialog", Dialog.DEFAULT_MODALITY_TYPE ); - dialog.setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE ); - dialog.addWindowListener( new WindowAdapter() { - @Override - public void windowOpened( WindowEvent e ) { - openOrSave( open, direct, dialog ); - } - } ); - dialog.setSize( 1200, 1000 ); - dialog.setLocationRelativeTo( this ); - dialog.setVisible( true ); - } else + else if( ownerDialogRadioButton.isSelected() ) + new DummyModalDialog( frame, owner -> openOrSave( open, direct, owner ) ).setVisible( true ); + else openOrSave( open, direct, null ); } @@ -174,94 +159,38 @@ public class FlatSystemFileChooserLinuxTest optionsClear.set( optionsClear.get() | option ); } - 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 - ownerLabel = new JLabel(); + JLabel ownerLabel = new JLabel(); ownerFrameRadioButton = new JRadioButton(); ownerDialogRadioButton = new JRadioButton(); ownerNullRadioButton = new JRadioButton(); - ownerSpacer = new JPanel(null); - titleLabel = new JLabel(); + JPanel ownerSpacer = new JPanel(null); + JLabel titleLabel = new JLabel(); titleField = new JTextField(); - panel1 = new JPanel(); + JPanel panel1 = new JPanel(); select_folderCheckBox = new FlatTriStateCheckBox(); select_multipleCheckBox = new FlatTriStateCheckBox(); do_overwrite_confirmationCheckBox = new FlatTriStateCheckBox(); create_foldersCheckBox = new FlatTriStateCheckBox(); show_hiddenCheckBox = new FlatTriStateCheckBox(); local_onlyCheckBox = new FlatTriStateCheckBox(); - okButtonLabelLabel = new JLabel(); + JLabel okButtonLabelLabel = new JLabel(); okButtonLabelField = new JTextField(); - currentNameLabel = new JLabel(); + JLabel currentNameLabel = new JLabel(); currentNameField = new JTextField(); - currentFolderLabel = new JLabel(); + JLabel currentFolderLabel = new JLabel(); currentFolderField = new JTextField(); - fileTypesLabel = new JLabel(); + JLabel fileTypesLabel = new JLabel(); fileTypesField = new JComboBox<>(); - fileTypeIndexLabel = new JLabel(); + JLabel fileTypeIndexLabel = new JLabel(); fileTypeIndexSlider = new JSlider(); - openButton = new JButton(); - saveButton = new JButton(); - openDirectButton = new JButton(); - saveDirectButton = new JButton(); + JButton openButton = new JButton(); + JButton saveButton = new JButton(); + JButton openDirectButton = new JButton(); + JButton saveDirectButton = new JButton(); showMessageDialogOnOKCheckBox = new JCheckBox(); - filesScrollPane = new JScrollPane(); + JScrollPane filesScrollPane = new JScrollPane(); filesField = new JTextArea(); //======== this ======== @@ -432,36 +361,22 @@ public class FlatSystemFileChooserLinuxTest } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables - private JLabel ownerLabel; private JRadioButton ownerFrameRadioButton; private JRadioButton ownerDialogRadioButton; private JRadioButton ownerNullRadioButton; - private JPanel ownerSpacer; - private JLabel titleLabel; private JTextField titleField; - private JPanel panel1; private FlatTriStateCheckBox select_folderCheckBox; private FlatTriStateCheckBox select_multipleCheckBox; private FlatTriStateCheckBox do_overwrite_confirmationCheckBox; private FlatTriStateCheckBox create_foldersCheckBox; private FlatTriStateCheckBox show_hiddenCheckBox; private FlatTriStateCheckBox local_onlyCheckBox; - private JLabel okButtonLabelLabel; private JTextField okButtonLabelField; - private JLabel currentNameLabel; private JTextField currentNameField; - private JLabel currentFolderLabel; private JTextField currentFolderField; - private JLabel fileTypesLabel; private JComboBox fileTypesField; - private JLabel fileTypeIndexLabel; private JSlider fileTypeIndexSlider; - private JButton openButton; - 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/FlatSystemFileChooserLinuxTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd index 3506768d..f3ebc996 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd @@ -3,6 +3,9 @@ JFDML JFormDesigner: "8.2.2.0.9999" Java: "21.0.1" encoding: "UTF-8" new FormModel { contentType: "form/swing" root: new FormRoot { + auxiliary() { + "JavaCodeGenerator.defaultVariableLocal": true + } 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]" @@ -20,6 +23,9 @@ new FormModel { "text": "JFrame" "$buttonGroup": new FormReference( "ownerButtonGroup" ) "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" } ) @@ -27,6 +33,9 @@ new FormModel { name: "ownerDialogRadioButton" "text": "JDialog" "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" } ) @@ -34,6 +43,9 @@ new FormModel { name: "ownerNullRadioButton" "text": "null" "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" } ) @@ -50,6 +62,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "titleField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 1" } ) @@ -64,6 +79,9 @@ new FormModel { "text": "select_folder" "allowIndeterminate": false "state": enum com.formdev.flatlaf.extras.components.FlatTriStateCheckBox$State UNSELECTED + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0" } ) @@ -72,30 +90,45 @@ new FormModel { "text": "select_multiple" "state": enum com.formdev.flatlaf.extras.components.FlatTriStateCheckBox$State UNSELECTED "allowIndeterminate": false + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "do_overwrite_confirmationCheckBox" "text": "do_overwrite_confirmation" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "create_foldersCheckBox" "text": "create_folders" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "show_hiddenCheckBox" "text": "show_hidden" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 4" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "local_onlyCheckBox" "text": "local_only" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 5" } ) @@ -110,6 +143,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "okButtonLabelField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 2" } ) @@ -121,6 +157,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "currentNameField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 3" } ) @@ -132,6 +171,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "currentFolderField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 4" } ) @@ -151,6 +193,9 @@ new FormModel { addElement( "Text Files,*.txt,null,PDF Files,*.pdf,null,All Files,*,null" ) addElement( "Text and PDF Files,*.txt,*.pdf,null" ) } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 5" } ) @@ -167,6 +212,9 @@ new FormModel { "value": 0 "paintLabels": true "snapToTicks": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 6" } ) @@ -201,6 +249,9 @@ new FormModel { add( new FormComponent( "javax.swing.JCheckBox" ) { name: "showMessageDialogOnOKCheckBox" "text": "show message dialog on OK" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 7 3 1" } ) @@ -209,6 +260,9 @@ new FormModel { add( new FormComponent( "javax.swing.JTextArea" ) { name: "filesField" "rows": 8 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 8 3 1,growx" 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 306e90b4..5c6c2a99 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 @@ -17,20 +17,16 @@ 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 java.util.concurrent.atomic.AtomicInteger; import javax.swing.*; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.extras.components.*; import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; +import com.formdev.flatlaf.testing.FlatSystemFileChooserTest.DummyModalDialog; import com.formdev.flatlaf.ui.FlatNativeMacLibrary; import com.formdev.flatlaf.util.SystemInfo; import net.miginfocom.swing.*; @@ -44,6 +40,10 @@ public class FlatSystemFileChooserMacTest public static void main( String[] args ) { // macOS (see https://www.formdev.com/flatlaf/macos/) if( SystemInfo.isMacOS ) { + // enable screen menu bar + // (moves menu bar from JFrame window to top of screen) + System.setProperty( "apple.laf.useScreenMenuBar", "true" ); + // appearance of window title bars // possible values: // - "system": use current macOS appearance (light or dark) @@ -60,8 +60,9 @@ public class FlatSystemFileChooserMacTest } FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserMacTest" ); -// addListeners( frame ); + FlatSystemFileChooserTest.addListeners( frame ); frame.showFrame( FlatSystemFileChooserMacTest::new ); + frame.setJMenuBar( menuBar1 ); } ); } @@ -88,7 +89,16 @@ public class FlatSystemFileChooserMacTest } private void openOrSave( boolean open, boolean direct ) { - Window owner = SwingUtilities.windowForComponent( this ); + Window frame = SwingUtilities.windowForComponent( this ); + if( ownerFrameRadioButton.isSelected() ) + openOrSave( open, direct, frame ); + else if( ownerDialogRadioButton.isSelected() ) + new DummyModalDialog( frame, owner -> openOrSave( open, direct, owner ) ).setVisible( true ); + else + openOrSave( open, direct, null ); + } + + private void openOrSave( boolean open, boolean direct, Window owner ) { String title = n( titleField.getText() ); String prompt = n( promptField.getText() ); String message = n( messageField.getText() ); @@ -168,7 +178,7 @@ public class FlatSystemFileChooserMacTest System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); - EventQueue.invokeLater( () -> { + SwingUtilities.invokeLater( () -> { filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); } ); } ).start(); @@ -189,75 +199,27 @@ public class FlatSystemFileChooserMacTest optionsClear.set( optionsClear.get() | option ); } - @SuppressWarnings( "unused" ) - 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 menuItemAction() { + System.out.println( "menu item action" ); } private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents - titleLabel = new JLabel(); + JLabel ownerLabel = new JLabel(); + ownerFrameRadioButton = new JRadioButton(); + ownerDialogRadioButton = new JRadioButton(); + ownerNullRadioButton = new JRadioButton(); + JPanel ownerSpacer = new JPanel(null); + JLabel titleLabel = new JLabel(); titleField = new JTextField(); - panel1 = new JPanel(); - options1Label = new JLabel(); + JPanel panel1 = new JPanel(); + JLabel options1Label = new JLabel(); canChooseFilesCheckBox = new JCheckBox(); canChooseDirectoriesCheckBox = new JCheckBox(); resolvesAliasesCheckBox = new FlatTriStateCheckBox(); allowsMultipleSelectionCheckBox = new FlatTriStateCheckBox(); accessoryViewDisclosedCheckBox = new JCheckBox(); - options2Label = new JLabel(); + JLabel options2Label = new JLabel(); showsTagFieldCheckBox = new FlatTriStateCheckBox(); canCreateDirectoriesCheckBox = new FlatTriStateCheckBox(); canSelectHiddenExtensionCheckBox = new FlatTriStateCheckBox(); @@ -265,31 +227,38 @@ public class FlatSystemFileChooserMacTest extensionHiddenCheckBox = new FlatTriStateCheckBox(); allowsOtherFileTypesCheckBox = new FlatTriStateCheckBox(); treatsFilePackagesAsDirectoriesCheckBox = new FlatTriStateCheckBox(); - options3Label = new JLabel(); + JLabel options3Label = new JLabel(); showSingleFilterFieldCheckBox = new JCheckBox(); - promptLabel = new JLabel(); + JLabel promptLabel = new JLabel(); promptField = new JTextField(); - messageLabel = new JLabel(); + JLabel messageLabel = new JLabel(); messageField = new JTextField(); - filterFieldLabelLabel = new JLabel(); + JLabel filterFieldLabelLabel = new JLabel(); filterFieldLabelField = new JTextField(); - nameFieldLabelLabel = new JLabel(); + JLabel nameFieldLabelLabel = new JLabel(); nameFieldLabelField = new JTextField(); - nameFieldStringValueLabel = new JLabel(); + JLabel nameFieldStringValueLabel = new JLabel(); nameFieldStringValueField = new JTextField(); - directoryURLLabel = new JLabel(); + JLabel directoryURLLabel = new JLabel(); directoryURLField = new JTextField(); - fileTypesLabel = new JLabel(); + JLabel fileTypesLabel = new JLabel(); fileTypesField = new JComboBox<>(); - fileTypeIndexLabel = new JLabel(); + JLabel fileTypeIndexLabel = new JLabel(); fileTypeIndexSlider = new JSlider(); - openButton = new JButton(); - saveButton = new JButton(); - openDirectButton = new JButton(); - saveDirectButton = new JButton(); + JButton openButton = new JButton(); + JButton saveButton = new JButton(); + JButton openDirectButton = new JButton(); + JButton saveDirectButton = new JButton(); showMessageDialogOnOKCheckBox = new JCheckBox(); - filesScrollPane = new JScrollPane(); + JScrollPane filesScrollPane = new JScrollPane(); filesField = new JTextArea(); + menuBar1 = new JMenuBar(); + JMenu menu1 = new JMenu(); + JMenuItem menuItem1 = new JMenuItem(); + JMenuItem menuItem2 = new JMenuItem(); + JMenu menu2 = new JMenu(); + JMenuItem menuItem3 = new JMenuItem(); + JMenuItem menuItem4 = new JMenuItem(); //======== this ======== setLayout(new MigLayout( @@ -310,12 +279,31 @@ public class FlatSystemFileChooserMacTest "[]" + "[]" + "[]" + + "[]" + "[grow,fill]")); + //---- ownerLabel ---- + ownerLabel.setText("owner"); + add(ownerLabel, "cell 0 0"); + + //---- ownerFrameRadioButton ---- + ownerFrameRadioButton.setText("JFrame"); + ownerFrameRadioButton.setSelected(true); + add(ownerFrameRadioButton, "cell 1 0"); + + //---- ownerDialogRadioButton ---- + ownerDialogRadioButton.setText("JDialog"); + add(ownerDialogRadioButton, "cell 1 0"); + + //---- ownerNullRadioButton ---- + ownerNullRadioButton.setText("null"); + add(ownerNullRadioButton, "cell 1 0"); + add(ownerSpacer, "cell 1 0,growx"); + //---- titleLabel ---- titleLabel.setText("title"); - add(titleLabel, "cell 0 0"); - add(titleField, "cell 1 0"); + add(titleLabel, "cell 0 1"); + add(titleField, "cell 1 1"); //======== panel1 ======== { @@ -407,41 +395,41 @@ public class FlatSystemFileChooserMacTest showSingleFilterFieldCheckBox.setText("showSingleFilterField"); panel1.add(showSingleFilterFieldCheckBox, "cell 0 15"); } - add(panel1, "cell 2 0 1 10,aligny top,growy 0"); + add(panel1, "cell 2 1 1 10,aligny top,growy 0"); //---- promptLabel ---- promptLabel.setText("prompt"); - add(promptLabel, "cell 0 1"); - add(promptField, "cell 1 1"); + add(promptLabel, "cell 0 2"); + add(promptField, "cell 1 2"); //---- messageLabel ---- messageLabel.setText("message"); - add(messageLabel, "cell 0 2"); - add(messageField, "cell 1 2"); + add(messageLabel, "cell 0 3"); + add(messageField, "cell 1 3"); //---- filterFieldLabelLabel ---- filterFieldLabelLabel.setText("filterFieldLabel"); - add(filterFieldLabelLabel, "cell 0 3"); - add(filterFieldLabelField, "cell 1 3"); + add(filterFieldLabelLabel, "cell 0 4"); + add(filterFieldLabelField, "cell 1 4"); //---- nameFieldLabelLabel ---- nameFieldLabelLabel.setText("nameFieldLabel"); - add(nameFieldLabelLabel, "cell 0 4"); - add(nameFieldLabelField, "cell 1 4"); + add(nameFieldLabelLabel, "cell 0 5"); + add(nameFieldLabelField, "cell 1 5"); //---- nameFieldStringValueLabel ---- nameFieldStringValueLabel.setText("nameFieldStringValue"); - add(nameFieldStringValueLabel, "cell 0 5"); - add(nameFieldStringValueField, "cell 1 5"); + add(nameFieldStringValueLabel, "cell 0 6"); + add(nameFieldStringValueField, "cell 1 6"); //---- directoryURLLabel ---- directoryURLLabel.setText("directoryURL"); - add(directoryURLLabel, "cell 0 6"); - add(directoryURLField, "cell 1 6"); + add(directoryURLLabel, "cell 0 7"); + add(directoryURLField, "cell 1 7"); //---- fileTypesLabel ---- fileTypesLabel.setText("fileTypes"); - add(fileTypesLabel, "cell 0 7"); + add(fileTypesLabel, "cell 0 8"); //---- fileTypesField ---- fileTypesField.setEditable(true); @@ -452,11 +440,11 @@ public class FlatSystemFileChooserMacTest "Text and PDF Files,txt,pdf,null", "Compressed,zip,gz,null,Disk Images,dmg,null" })); - add(fileTypesField, "cell 1 7"); + add(fileTypesField, "cell 1 8"); //---- fileTypeIndexLabel ---- fileTypeIndexLabel.setText("fileTypeIndex"); - add(fileTypeIndexLabel, "cell 0 8"); + add(fileTypeIndexLabel, "cell 0 9"); //---- fileTypeIndexSlider ---- fileTypeIndexSlider.setMaximum(10); @@ -464,31 +452,31 @@ public class FlatSystemFileChooserMacTest fileTypeIndexSlider.setValue(0); fileTypeIndexSlider.setPaintLabels(true); fileTypeIndexSlider.setSnapToTicks(true); - add(fileTypeIndexSlider, "cell 1 8"); + add(fileTypeIndexSlider, "cell 1 9"); //---- openButton ---- openButton.setText("Open..."); openButton.addActionListener(e -> open()); - add(openButton, "cell 0 10 3 1"); + add(openButton, "cell 0 11 3 1"); //---- saveButton ---- saveButton.setText("Save..."); saveButton.addActionListener(e -> save()); - add(saveButton, "cell 0 10 3 1"); + add(saveButton, "cell 0 11 3 1"); //---- openDirectButton ---- openDirectButton.setText("Open (no-thread)..."); openDirectButton.addActionListener(e -> openDirect()); - add(openDirectButton, "cell 0 10 3 1"); + add(openDirectButton, "cell 0 11 3 1"); //---- saveDirectButton ---- saveDirectButton.setText("Save (no-thread)..."); saveDirectButton.addActionListener(e -> saveDirect()); - add(saveDirectButton, "cell 0 10 3 1"); + add(saveDirectButton, "cell 0 11 3 1"); //---- showMessageDialogOnOKCheckBox ---- showMessageDialogOnOKCheckBox.setText("show message dialog on OK"); - add(showMessageDialogOnOKCheckBox, "cell 0 10 3 1"); + add(showMessageDialogOnOKCheckBox, "cell 0 11 3 1"); //======== filesScrollPane ======== { @@ -497,21 +485,62 @@ public class FlatSystemFileChooserMacTest filesField.setRows(8); filesScrollPane.setViewportView(filesField); } - add(filesScrollPane, "cell 0 11 3 1,growx"); + add(filesScrollPane, "cell 0 12 3 1,growx"); + + //======== menuBar1 ======== + { + + //======== menu1 ======== + { + menu1.setText("text"); + + //---- menuItem1 ---- + menuItem1.setText("text"); + menuItem1.addActionListener(e -> menuItemAction()); + menu1.add(menuItem1); + + //---- menuItem2 ---- + menuItem2.setText("text"); + menuItem2.addActionListener(e -> menuItemAction()); + menu1.add(menuItem2); + } + menuBar1.add(menu1); + + //======== menu2 ======== + { + menu2.setText("text"); + + //---- menuItem3 ---- + menuItem3.setText("text"); + menuItem3.addActionListener(e -> menuItemAction()); + menu2.add(menuItem3); + + //---- menuItem4 ---- + menuItem4.setText("text"); + menuItem4.addActionListener(e -> menuItemAction()); + menu2.add(menuItem4); + } + menuBar1.add(menu2); + } + + //---- ownerButtonGroup ---- + ButtonGroup ownerButtonGroup = new ButtonGroup(); + ownerButtonGroup.add(ownerFrameRadioButton); + ownerButtonGroup.add(ownerDialogRadioButton); + ownerButtonGroup.add(ownerNullRadioButton); // JFormDesigner - End of component initialization //GEN-END:initComponents } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables - private JLabel titleLabel; + private JRadioButton ownerFrameRadioButton; + private JRadioButton ownerDialogRadioButton; + private JRadioButton ownerNullRadioButton; private JTextField titleField; - private JPanel panel1; - private JLabel options1Label; private JCheckBox canChooseFilesCheckBox; private JCheckBox canChooseDirectoriesCheckBox; private FlatTriStateCheckBox resolvesAliasesCheckBox; private FlatTriStateCheckBox allowsMultipleSelectionCheckBox; private JCheckBox accessoryViewDisclosedCheckBox; - private JLabel options2Label; private FlatTriStateCheckBox showsTagFieldCheckBox; private FlatTriStateCheckBox canCreateDirectoriesCheckBox; private FlatTriStateCheckBox canSelectHiddenExtensionCheckBox; @@ -519,30 +548,17 @@ public class FlatSystemFileChooserMacTest private FlatTriStateCheckBox extensionHiddenCheckBox; private FlatTriStateCheckBox allowsOtherFileTypesCheckBox; private FlatTriStateCheckBox treatsFilePackagesAsDirectoriesCheckBox; - private JLabel options3Label; private JCheckBox showSingleFilterFieldCheckBox; - private JLabel promptLabel; private JTextField promptField; - private JLabel messageLabel; private JTextField messageField; - private JLabel filterFieldLabelLabel; private JTextField filterFieldLabelField; - private JLabel nameFieldLabelLabel; private JTextField nameFieldLabelField; - private JLabel nameFieldStringValueLabel; private JTextField nameFieldStringValueField; - private JLabel directoryURLLabel; private JTextField directoryURLField; - private JLabel fileTypesLabel; private JComboBox fileTypesField; - private JLabel fileTypeIndexLabel; private JSlider fileTypeIndexSlider; - private JButton openButton; - private JButton saveButton; - private JButton openDirectButton; - private JButton saveDirectButton; private JCheckBox showMessageDialogOnOKCheckBox; - private JScrollPane filesScrollPane; private JTextArea filesField; + private static JMenuBar menuBar1; // 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 83d73f63..33ffd50a 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 @@ -3,22 +3,70 @@ JFDML JFormDesigner: "8.2.2.0.9999" Java: "21.0.1" encoding: "UTF-8" new FormModel { contentType: "form/swing" root: new FormRoot { + auxiliary() { + "JavaCodeGenerator.defaultVariableLocal": true + } 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]" + "$rowConstraints": "[][][][][][][][][][][][][grow,fill]" } ) { name: "this" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "ownerLabel" + "text": "owner" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerFrameRadioButton" + "text": "JFrame" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerDialogRadioButton" + "text": "JDialog" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerNullRadioButton" + "text": "null" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) { + name: "ownerSpacer" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0,growx" + } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "titleLabel" "text": "title" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 0" + "value": "cell 0 1" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "titleField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 0" + "value": "cell 1 1" } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets 2,hidemode 3" @@ -36,12 +84,18 @@ new FormModel { name: "canChooseFilesCheckBox" "text": "canChooseFiles" "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "canChooseDirectoriesCheckBox" "text": "canChooseDirectories" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) @@ -49,18 +103,27 @@ new FormModel { name: "resolvesAliasesCheckBox" "text": "resolvesAliases" "state": enum com.formdev.flatlaf.extras.components.FlatTriStateCheckBox$State SELECTED + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "allowsMultipleSelectionCheckBox" "text": "allowsMultipleSelection" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 4" } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "accessoryViewDisclosedCheckBox" "text": "accessoryViewDisclosed" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 5" } ) @@ -73,42 +136,63 @@ new FormModel { add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "showsTagFieldCheckBox" "text": "showsTagField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 7" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "canCreateDirectoriesCheckBox" "text": "canCreateDirectories" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 8" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "canSelectHiddenExtensionCheckBox" "text": "canSelectHiddenExtension" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 9" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "showsHiddenFilesCheckBox" "text": "showsHiddenFiles" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 10" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "extensionHiddenCheckBox" "text": "extensionHidden" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 11" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "allowsOtherFileTypesCheckBox" "text": "allowsOtherFileTypes" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 12" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "treatsFilePackagesAsDirectoriesCheckBox" "text": "treatsFilePackagesAsDirectories" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 13" } ) @@ -121,83 +205,104 @@ new FormModel { add( new FormComponent( "javax.swing.JCheckBox" ) { name: "showSingleFilterFieldCheckBox" "text": "showSingleFilterField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 15" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 0 1 10,aligny top,growy 0" + "value": "cell 2 1 1 10,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" + "value": "cell 0 2" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "promptField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 1" + "value": "cell 1 2" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "messageLabel" "text": "message" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2" + "value": "cell 0 3" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "messageField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 2" + "value": "cell 1 3" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "filterFieldLabelLabel" "text": "filterFieldLabel" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 3" + "value": "cell 0 4" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "filterFieldLabelField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 3" + "value": "cell 1 4" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "nameFieldLabelLabel" "text": "nameFieldLabel" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" + "value": "cell 0 5" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "nameFieldLabelField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 4" + "value": "cell 1 5" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "nameFieldStringValueLabel" "text": "nameFieldStringValue" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5" + "value": "cell 0 6" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "nameFieldStringValueField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 5" + "value": "cell 1 6" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "directoryURLLabel" "text": "directoryURL" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6" + "value": "cell 0 7" } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "directoryURLField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 6" + "value": "cell 1 7" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "fileTypesLabel" "text": "fileTypes" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 7" + "value": "cell 0 8" } ) add( new FormComponent( "javax.swing.JComboBox" ) { name: "fileTypesField" @@ -210,14 +315,17 @@ new FormModel { addElement( "Text and PDF Files,txt,pdf,null" ) addElement( "Compressed,zip,gz,null,Disk Images,dmg,null" ) } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 7" + "value": "cell 1 8" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "fileTypeIndexLabel" "text": "fileTypeIndex" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 8" + "value": "cell 0 9" } ) add( new FormComponent( "javax.swing.JSlider" ) { name: "fileTypeIndexSlider" @@ -226,55 +334,107 @@ new FormModel { "value": 0 "paintLabels": true "snapToTicks": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 8" + "value": "cell 1 9" } ) 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 10 3 1" + "value": "cell 0 11 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 10 3 1" + "value": "cell 0 11 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 10 3 1" + "value": "cell 0 11 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 10 3 1" + "value": "cell 0 11 3 1" } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "showMessageDialogOnOKCheckBox" "text": "show message dialog on OK" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 10 3 1" + "value": "cell 0 11 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 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 11 3 1,growx" + "value": "cell 0 12 3 1,growx" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 750, 475 ) + "size": new java.awt.Dimension( 750, 565 ) + } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "ownerButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 575 ) + } ) + add( new FormContainer( "javax.swing.JMenuBar", new FormLayoutManager( class javax.swing.JMenuBar ) ) { + name: "menuBar1" + auxiliary() { + "JavaCodeGenerator.variableModifiers": 10 + "JavaCodeGenerator.variableLocal": false + } + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "menu1" + "text": "text" + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem1" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem2" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + } ) + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "menu2" + "text": "text" + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem3" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem4" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 630 ) + "size": new java.awt.Dimension( 76, 24 ) } ) } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java index 1a2d629e..d7fda240 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java @@ -16,9 +16,12 @@ package com.formdev.flatlaf.testing; +import java.awt.Component; +import java.awt.Container; import java.awt.Dialog; import java.awt.FileDialog; import java.awt.Frame; +import java.awt.Point; import java.awt.Window; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; @@ -49,6 +52,10 @@ public class FlatSystemFileChooserTest public static void main( String[] args ) { // macOS (see https://www.formdev.com/flatlaf/macos/) if( SystemInfo.isMacOS ) { + // enable screen menu bar + // (moves menu bar from JFrame window to top of screen) + System.setProperty( "apple.laf.useScreenMenuBar", "true" ); + // appearance of window title bars // possible values: // - "system": use current macOS appearance (light or dark) @@ -63,6 +70,7 @@ public class FlatSystemFileChooserTest frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); // necessary because of JavaFX addListeners( frame ); frame.showFrame( FlatSystemFileChooserTest::new ); + frame.setJMenuBar( menuBar1 ); } ); } @@ -322,19 +330,9 @@ public class FlatSystemFileChooserTest Window frame = SwingUtilities.windowForComponent( this ); if( ownerFrameRadioButton.isSelected() ) showConsumer.accept( frame ); - else if( ownerDialogRadioButton.isSelected() ) { - JDialog dialog = new JDialog( frame, "Dummy Modal Dialog", Dialog.DEFAULT_MODALITY_TYPE ); - dialog.setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE ); - dialog.addWindowListener( new WindowAdapter() { - @Override - public void windowOpened( WindowEvent e ) { - showConsumer.accept( dialog ); - } - } ); - dialog.setSize( 1200, 1000 ); - dialog.setLocationRelativeTo( this ); - dialog.setVisible( true ); - } else + else if( ownerDialogRadioButton.isSelected() ) + new DummyModalDialog( frame, showConsumer ).setVisible( true ); + else showConsumer.accept( null ); } @@ -423,79 +421,105 @@ public class FlatSystemFileChooserTest DemoPrefs.getState().put( key, value ); } - private static void addListeners( Window w ) { + private void menuItemAction() { + System.out.println( "menu item action" ); + } + + static void addListeners( Window w ) { w.addWindowListener( new WindowListener() { @Override public void windowOpened( WindowEvent e ) { - System.out.println( e ); + printWindowEvent( e ); } @Override public void windowIconified( WindowEvent e ) { - System.out.println( e ); + printWindowEvent( e ); } @Override public void windowDeiconified( WindowEvent e ) { - System.out.println( e ); + printWindowEvent( e ); } @Override public void windowDeactivated( WindowEvent e ) { - System.out.println( e ); + printWindowEvent( e ); } @Override public void windowClosing( WindowEvent e ) { - System.out.println( e ); + printWindowEvent( e ); } @Override public void windowClosed( WindowEvent e ) { - System.out.println( e ); + printWindowEvent( e ); } @Override public void windowActivated( WindowEvent e ) { - System.out.println( e ); + printWindowEvent( e ); } } ); w.addWindowStateListener( new WindowStateListener() { @Override public void windowStateChanged( WindowEvent e ) { - System.out.println( e ); + printWindowEvent( e ); } } ); w.addWindowFocusListener( new WindowFocusListener() { @Override public void windowLostFocus( WindowEvent e ) { - System.out.println( e ); + printWindowEvent( e ); } @Override public void windowGainedFocus( WindowEvent e ) { - System.out.println( e ); + printWindowEvent( e ); } } ); } + static void printWindowEvent( WindowEvent e ) { + String typeStr; + switch( e.getID() ) { + case WindowEvent.WINDOW_OPENED: typeStr = "WINDOW_OPENED "; break; + case WindowEvent.WINDOW_CLOSING: typeStr = "WINDOW_CLOSING "; break; + case WindowEvent.WINDOW_CLOSED: typeStr = "WINDOW_CLOSED "; break; + case WindowEvent.WINDOW_ICONIFIED: typeStr = "WINDOW_ICONIFIED "; break; + case WindowEvent.WINDOW_DEICONIFIED: typeStr = "WINDOW_DEICONIFIED "; break; + case WindowEvent.WINDOW_ACTIVATED: typeStr = "WINDOW_ACTIVATED "; break; + case WindowEvent.WINDOW_DEACTIVATED: typeStr = "WINDOW_DEACTIVATED "; break; + case WindowEvent.WINDOW_GAINED_FOCUS: typeStr = "WINDOW_GAINED_FOCUS "; break; + case WindowEvent.WINDOW_LOST_FOCUS: typeStr = "WINDOW_LOST_FOCUS "; break; + case WindowEvent.WINDOW_STATE_CHANGED: typeStr = "WINDOW_STATE_CHANGED"; break; + default: typeStr = "unknown type "; break; + } + Object source = e.getSource(); + Window opposite = e.getOppositeWindow(); + String sourceStr = (source instanceof Component) ? ((Component)source).getName() : String.valueOf( source ); + String oppositeStr = (opposite != null) ? opposite.getName() : null; + System.out.println( typeStr + " source " + sourceStr + " opposite " + oppositeStr ); + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents - ownerLabel = new JLabel(); + JLabel ownerLabel = new JLabel(); ownerFrameRadioButton = new JRadioButton(); ownerDialogRadioButton = new JRadioButton(); ownerNullRadioButton = new JRadioButton(); - ownerSpacer = new JPanel(null); - dialogTitleLabel = new JLabel(); + JPanel ownerSpacer = new JPanel(null); + JLabel dialogTitleLabel = new JLabel(); dialogTitleField = new JTextField(); - panel1 = new JPanel(); + JPanel panel1 = new JPanel(); directorySelectionCheckBox = new JCheckBox(); multiSelectionEnabledCheckBox = new JCheckBox(); useFileHidingCheckBox = new JCheckBox(); useSystemFileChooserCheckBox = new JCheckBox(); - approveButtonTextLabel = new JLabel(); + JLabel approveButtonTextLabel = new JLabel(); approveButtonTextField = new JTextField(); - approveButtonMnemonicLabel = new JLabel(); + JLabel approveButtonMnemonicLabel = new JLabel(); approveButtonMnemonicField = new JTextField(); currentDirCheckBox = new JCheckBox(); currentDirField = new JTextField(); @@ -506,21 +530,28 @@ public class FlatSystemFileChooserTest selectedFilesCheckBox = new JCheckBox(); selectedFilesField = new JTextField(); selectedFilesChooseButton = new JButton(); - fileTypesLabel = new JLabel(); + JLabel fileTypesLabel = new JLabel(); fileTypesField = new JComboBox<>(); - fileTypeIndexLabel = new JLabel(); + JLabel fileTypeIndexLabel = new JLabel(); fileTypeIndexSlider = new JSlider(); useAcceptAllFileFilterCheckBox = new JCheckBox(); - openButton = new JButton(); - saveButton = new JButton(); - swingOpenButton = new JButton(); - swingSaveButton = new JButton(); - awtOpenButton = new JButton(); - awtSaveButton = new JButton(); + JButton openButton = new JButton(); + JButton saveButton = new JButton(); + JButton swingOpenButton = new JButton(); + JButton swingSaveButton = new JButton(); + JButton awtOpenButton = new JButton(); + JButton awtSaveButton = new JButton(); javafxOpenButton = new JButton(); javafxSaveButton = new JButton(); - outputScrollPane = new JScrollPane(); + JScrollPane outputScrollPane = new JScrollPane(); outputField = new JTextArea(); + menuBar1 = new JMenuBar(); + JMenu menu1 = new JMenu(); + JMenuItem menuItem1 = new JMenuItem(); + JMenuItem menuItem2 = new JMenuItem(); + JMenu menu2 = new JMenu(); + JMenuItem menuItem3 = new JMenuItem(); + JMenuItem menuItem4 = new JMenuItem(); //======== this ======== setLayout(new MigLayout( @@ -722,6 +753,42 @@ public class FlatSystemFileChooserTest } add(outputScrollPane, "cell 0 9 3 1,growx"); + //======== menuBar1 ======== + { + + //======== menu1 ======== + { + menu1.setText("text"); + + //---- menuItem1 ---- + menuItem1.setText("text"); + menuItem1.addActionListener(e -> menuItemAction()); + menu1.add(menuItem1); + + //---- menuItem2 ---- + menuItem2.setText("text"); + menuItem2.addActionListener(e -> menuItemAction()); + menu1.add(menuItem2); + } + menuBar1.add(menu1); + + //======== menu2 ======== + { + menu2.setText("text"); + + //---- menuItem3 ---- + menuItem3.setText("text"); + menuItem3.addActionListener(e -> menuItemAction()); + menu2.add(menuItem3); + + //---- menuItem4 ---- + menuItem4.setText("text"); + menuItem4.addActionListener(e -> menuItemAction()); + menu2.add(menuItem4); + } + menuBar1.add(menu2); + } + //---- ownerButtonGroup ---- ButtonGroup ownerButtonGroup = new ButtonGroup(); ownerButtonGroup.add(ownerFrameRadioButton); @@ -731,21 +798,15 @@ public class FlatSystemFileChooserTest } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables - private JLabel ownerLabel; private JRadioButton ownerFrameRadioButton; private JRadioButton ownerDialogRadioButton; private JRadioButton ownerNullRadioButton; - private JPanel ownerSpacer; - private JLabel dialogTitleLabel; private JTextField dialogTitleField; - private JPanel panel1; private JCheckBox directorySelectionCheckBox; private JCheckBox multiSelectionEnabledCheckBox; private JCheckBox useFileHidingCheckBox; private JCheckBox useSystemFileChooserCheckBox; - private JLabel approveButtonTextLabel; private JTextField approveButtonTextField; - private JLabel approveButtonMnemonicLabel; private JTextField approveButtonMnemonicField; private JCheckBox currentDirCheckBox; private JTextField currentDirField; @@ -756,20 +817,151 @@ public class FlatSystemFileChooserTest private JCheckBox selectedFilesCheckBox; private JTextField selectedFilesField; private JButton selectedFilesChooseButton; - private JLabel fileTypesLabel; private JComboBox fileTypesField; - private JLabel fileTypeIndexLabel; private JSlider fileTypeIndexSlider; private JCheckBox useAcceptAllFileFilterCheckBox; - private JButton openButton; - private JButton saveButton; - private JButton swingOpenButton; - private JButton swingSaveButton; - private JButton awtOpenButton; - private JButton awtSaveButton; private JButton javafxOpenButton; private JButton javafxSaveButton; - private JScrollPane outputScrollPane; private JTextArea outputField; + private static JMenuBar menuBar1; // JFormDesigner - End of variables declaration //GEN-END:variables + + //---- class DummyModalDialog --------------------------------------------- + + static class DummyModalDialog + extends JDialog + { + private final Consumer showConsumer; + + DummyModalDialog( Window owner, Consumer showConsumer ) { + super( owner ); + this.showConsumer = showConsumer; + initComponents(); + addListeners( this ); + ((JComponent)getContentPane()).registerKeyboardAction( + e -> dispose(), + KeyStroke.getKeyStroke( "ESCAPE" ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + + if( owner != null ) { + Point pt = owner.getLocationOnScreen(); + setLocation( pt.x + (getWidth() / 2), pt.y + 40 ); + } else + setLocationRelativeTo( null ); + } + + private void modalityTypeChanged() { + if( applicationRadioButton.isSelected() ) + setModalityType( ModalityType.APPLICATION_MODAL ); + else if( documentRadioButton.isSelected() ) + setModalityType( ModalityType.DOCUMENT_MODAL ); + else if( toolkitRadioButton.isSelected() ) + setModalityType( ModalityType.TOOLKIT_MODAL ); + else + setModalityType( ModalityType.MODELESS ); + + setVisible( false ); + setVisible( true ); + } + + private void showModalDialog() { + new DummyModalDialog( this, showConsumer ).setVisible( true ); + } + + private void showFileDialog() { + showConsumer.accept( this ); + } + + private void windowOpened() { + showConsumer.accept( this ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off + JLabel label1 = new JLabel(); + applicationRadioButton = new JRadioButton(); + documentRadioButton = new JRadioButton(); + toolkitRadioButton = new JRadioButton(); + modelessRadioButton = new JRadioButton(); + JButton showModalDialogButton = new JButton(); + JButton showFileDialogButton = new JButton(); + + //======== this ======== + setTitle("Dummy Modal Dialog"); + setModalityType(Dialog.ModalityType.APPLICATION_MODAL); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + addWindowListener(new WindowAdapter() { + @Override + public void windowOpened(WindowEvent e) { + DummyModalDialog.this.windowOpened(); + } + }); + Container contentPane = getContentPane(); + contentPane.setLayout(new MigLayout( + "hidemode 3", + // columns + "[fill]" + + "[fill]", + // rows + "[]0" + + "[]0" + + "[]0" + + "[]" + + "[]para" + + "[]" + + "[198]")); + + //---- label1 ---- + label1.setText("Modality type:"); + contentPane.add(label1, "cell 0 0"); + + //---- applicationRadioButton ---- + applicationRadioButton.setText("Application"); + applicationRadioButton.setSelected(true); + applicationRadioButton.addActionListener(e -> modalityTypeChanged()); + contentPane.add(applicationRadioButton, "cell 1 0"); + + //---- documentRadioButton ---- + documentRadioButton.setText("Document"); + documentRadioButton.addActionListener(e -> modalityTypeChanged()); + contentPane.add(documentRadioButton, "cell 1 1"); + + //---- toolkitRadioButton ---- + toolkitRadioButton.setText("Toolkit"); + toolkitRadioButton.addActionListener(e -> modalityTypeChanged()); + contentPane.add(toolkitRadioButton, "cell 1 2"); + + //---- modelessRadioButton ---- + modelessRadioButton.setText("modeless"); + modelessRadioButton.addActionListener(e -> modalityTypeChanged()); + contentPane.add(modelessRadioButton, "cell 1 3"); + + //---- showModalDialogButton ---- + showModalDialogButton.setText("Show Modal Dialog..."); + showModalDialogButton.addActionListener(e -> showModalDialog()); + contentPane.add(showModalDialogButton, "cell 0 4 2 1"); + + //---- showFileDialogButton ---- + showFileDialogButton.setText("Show File Dialog..."); + showFileDialogButton.addActionListener(e -> showFileDialog()); + contentPane.add(showFileDialogButton, "cell 0 5 2 1"); + pack(); + setLocationRelativeTo(getOwner()); + + //---- modalityTypeButtonGroup ---- + ButtonGroup modalityTypeButtonGroup = new ButtonGroup(); + modalityTypeButtonGroup.add(applicationRadioButton); + modalityTypeButtonGroup.add(documentRadioButton); + modalityTypeButtonGroup.add(toolkitRadioButton); + modalityTypeButtonGroup.add(modelessRadioButton); + // JFormDesigner - End of component initialization //GEN-END:initComponents @formatter:on + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables @formatter:off + private JRadioButton applicationRadioButton; + private JRadioButton documentRadioButton; + private JRadioButton toolkitRadioButton; + private JRadioButton modelessRadioButton; + // JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on + } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd index 2aa7c66b..9031aa7c 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd @@ -3,6 +3,9 @@ JFDML JFormDesigner: "8.3" encoding: "UTF-8" new FormModel { contentType: "form/swing" root: new FormRoot { + auxiliary() { + "JavaCodeGenerator.defaultVariableLocal": true + } 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]" @@ -20,6 +23,9 @@ new FormModel { "text": "JFrame" "selected": true "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" } ) @@ -27,6 +33,9 @@ new FormModel { name: "ownerDialogRadioButton" "text": "JDialog" "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" } ) @@ -34,6 +43,9 @@ new FormModel { name: "ownerNullRadioButton" "text": "null" "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" } ) @@ -50,6 +62,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "dialogTitleField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 1" } ) @@ -62,12 +77,18 @@ new FormModel { add( new FormComponent( "javax.swing.JCheckBox" ) { name: "directorySelectionCheckBox" "text": "directorySelection" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0" } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "multiSelectionEnabledCheckBox" "text": "multiSelectionEnabled" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" } ) @@ -75,6 +96,9 @@ new FormModel { name: "useFileHidingCheckBox" "text": "useFileHiding" "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) @@ -82,6 +106,9 @@ new FormModel { name: "useSystemFileChooserCheckBox" "text": "use SystemFileChooser" "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) @@ -96,6 +123,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "approveButtonTextField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 2,growx" } ) @@ -108,6 +138,9 @@ new FormModel { add( new FormComponent( "javax.swing.JTextField" ) { name: "approveButtonMnemonicField" "columns": 3 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 2" } ) @@ -211,6 +244,9 @@ new FormModel { addElement( "Text Files,txt,PDF Files,pdf,All Files,*" ) addElement( "Text and PDF Files,txt;pdf" ) } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 6" } ) @@ -227,6 +263,9 @@ new FormModel { "value": 0 "paintLabels": true "snapToTicks": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 7,growx" } ) @@ -234,6 +273,9 @@ new FormModel { name: "useAcceptAllFileFilterCheckBox" "text": "useAcceptAllFileFilter" "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 7" } ) @@ -304,6 +346,9 @@ new FormModel { add( new FormComponent( "javax.swing.JTextArea" ) { name: "outputField" "rows": 20 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 9 3 1,growx" @@ -312,10 +357,134 @@ new FormModel { "location": new java.awt.Point( 0, 0 ) "size": new java.awt.Dimension( 825, 465 ) } ) + add( new FormContainer( "javax.swing.JMenuBar", new FormLayoutManager( class javax.swing.JMenuBar ) ) { + name: "menuBar1" + auxiliary() { + "JavaCodeGenerator.variableModifiers": 10 + "JavaCodeGenerator.variableLocal": false + } + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "menu1" + "text": "text" + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem1" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem2" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + } ) + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "menu2" + "text": "text" + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem3" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem4" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 10, 570 ) + } ) + add( new FormWindow( "javax.swing.JDialog", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "hidemode 3" + "$columnConstraints": "[fill][fill]" + "$rowConstraints": "[]0[]0[]0[][]para[][198]" + } ) { + name: "dialog1" + "title": "Dummy Modal Dialog" + "modalityType": enum java.awt.Dialog$ModalityType APPLICATION_MODAL + "defaultCloseOperation": 2 + auxiliary() { + "JavaCodeGenerator.className": "DummyModalDialog" + } + addEvent( new FormEvent( "java.awt.event.WindowListener", "windowOpened", "windowOpened", false ) ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label1" + "text": "Modality type:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "applicationRadioButton" + "text": "Application" + "selected": true + "$buttonGroup": new FormReference( "modalityTypeButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "modalityTypeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "documentRadioButton" + "text": "Document" + "$buttonGroup": new FormReference( "modalityTypeButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "modalityTypeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "toolkitRadioButton" + "text": "Toolkit" + "$buttonGroup": new FormReference( "modalityTypeButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "modalityTypeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "modelessRadioButton" + "text": "modeless" + "$buttonGroup": new FormReference( "modalityTypeButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "modalityTypeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "showModalDialogButton" + "text": "Show Modal Dialog..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showModalDialog", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4 2 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "showFileDialogButton" + "text": "Show File Dialog..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showFileDialog", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5 2 1" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 20, 635 ) + "size": new java.awt.Dimension( 290, 465 ) + } ) add( new FormNonVisual( "javax.swing.ButtonGroup" ) { name: "ownerButtonGroup" }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 475 ) } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "modalityTypeButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 115, 575 ) + } ) } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java index cc36e77b..b6ddbc4e 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java @@ -17,17 +17,11 @@ package com.formdev.flatlaf.testing; import static com.formdev.flatlaf.ui.FlatNativeWindowsLibrary.*; -import java.awt.Dialog; import java.awt.EventQueue; import java.awt.Font; import java.awt.SecondaryLoop; import java.awt.Toolkit; import java.awt.Window; -import java.awt.event.WindowAdapter; -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 java.util.concurrent.atomic.AtomicInteger; import java.util.prefs.Preferences; @@ -36,6 +30,7 @@ import javax.swing.border.TitledBorder; import com.formdev.flatlaf.demo.DemoPrefs; import com.formdev.flatlaf.extras.components.*; import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; +import com.formdev.flatlaf.testing.FlatSystemFileChooserTest.DummyModalDialog; import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; import net.miginfocom.swing.*; @@ -53,7 +48,7 @@ public class FlatSystemFileChooserWindowsTest } FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserWindowsTest" ); -// addListeners( frame ); + FlatSystemFileChooserTest.addListeners( frame ); frame.showFrame( FlatSystemFileChooserWindowsTest::new ); } ); } @@ -88,19 +83,9 @@ public class FlatSystemFileChooserWindowsTest Window frame = SwingUtilities.windowForComponent( this ); if( ownerFrameRadioButton.isSelected() ) openOrSave( open, direct, frame ); - else if( ownerDialogRadioButton.isSelected() ) { - JDialog dialog = new JDialog( frame, "Dummy Modal Dialog", Dialog.DEFAULT_MODALITY_TYPE ); - dialog.setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE ); - dialog.addWindowListener( new WindowAdapter() { - @Override - public void windowOpened( WindowEvent e ) { - openOrSave( open, direct, dialog ); - } - } ); - dialog.setSize( 1200, 1000 ); - dialog.setLocationRelativeTo( this ); - dialog.setVisible( true ); - } else + else if( ownerDialogRadioButton.isSelected() ) + new DummyModalDialog( frame, owner -> openOrSave( open, direct, owner ) ).setVisible( true ); + else openOrSave( open, direct, null ); } @@ -217,73 +202,16 @@ public class FlatSystemFileChooserWindowsTest /* MB_ICONINFORMATION */ 0x00000040 | /* MB_YESNO */ 0x00000004 ) ); } - @SuppressWarnings( "unused" ) - 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 - ownerLabel = new JLabel(); + JLabel ownerLabel = new JLabel(); ownerFrameRadioButton = new JRadioButton(); ownerDialogRadioButton = new JRadioButton(); ownerNullRadioButton = new JRadioButton(); - ownerSpacer = new JPanel(null); - titleLabel = new JLabel(); + JPanel ownerSpacer = new JPanel(null); + JLabel titleLabel = new JLabel(); titleField = new JTextField(); - panel1 = new JPanel(); + JPanel panel1 = new JPanel(); overwritePromptCheckBox = new FlatTriStateCheckBox(); pathMustExistCheckBox = new FlatTriStateCheckBox(); noDereferenceLinksCheckBox = new FlatTriStateCheckBox(); @@ -307,39 +235,39 @@ public class FlatSystemFileChooserWindowsTest supportStreamableItemsCheckBox = new FlatTriStateCheckBox(); allowMultiSelectCheckBox = new FlatTriStateCheckBox(); hidePinnedPlacesCheckBox = new FlatTriStateCheckBox(); - messageDialogPanel = new JPanel(); - messageLabel = new JLabel(); - messageScrollPane = new JScrollPane(); + JPanel messageDialogPanel = new JPanel(); + JLabel messageLabel = new JLabel(); + JScrollPane messageScrollPane = new JScrollPane(); messageField = new JTextArea(); - buttonsLabel = new JLabel(); + JLabel buttonsLabel = new JLabel(); buttonsField = new JTextField(); - okButtonLabelLabel = new JLabel(); + JLabel okButtonLabelLabel = new JLabel(); okButtonLabelField = new JTextField(); - fileNameLabelLabel = new JLabel(); + JLabel fileNameLabelLabel = new JLabel(); fileNameLabelField = new JTextField(); - fileNameLabel = new JLabel(); + JLabel fileNameLabel = new JLabel(); fileNameField = new JTextField(); - folderLabel = new JLabel(); + JLabel folderLabel = new JLabel(); folderField = new JTextField(); - saveAsItemLabel = new JLabel(); + JLabel saveAsItemLabel = new JLabel(); saveAsItemField = new JTextField(); - defaultFolderLabel = new JLabel(); + JLabel defaultFolderLabel = new JLabel(); defaultFolderField = new JTextField(); - defaultExtensionLabel = new JLabel(); + JLabel defaultExtensionLabel = new JLabel(); defaultExtensionField = new JTextField(); - fileTypesLabel = new JLabel(); + JLabel fileTypesLabel = new JLabel(); fileTypesField = new JComboBox<>(); - fileTypeIndexLabel = new JLabel(); + JLabel fileTypeIndexLabel = new JLabel(); fileTypeIndexSlider = new JSlider(); - openButton = new JButton(); - saveButton = new JButton(); - openDirectButton = new JButton(); - saveDirectButton = new JButton(); + JButton openButton = new JButton(); + JButton saveButton = new JButton(); + JButton openDirectButton = new JButton(); + JButton saveDirectButton = new JButton(); showMessageDialogOnOKCheckBox = new JCheckBox(); - hSpacer1 = new JPanel(null); - messageDialogButton = new JButton(); - messageBoxButton = new JButton(); - filesScrollPane = new JScrollPane(); + JPanel hSpacer1 = new JPanel(null); + JButton messageDialogButton = new JButton(); + JButton messageBoxButton = new JButton(); + JScrollPane filesScrollPane = new JScrollPane(); filesField = new JTextArea(); //======== this ======== @@ -650,14 +578,10 @@ public class FlatSystemFileChooserWindowsTest } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables - private JLabel ownerLabel; private JRadioButton ownerFrameRadioButton; private JRadioButton ownerDialogRadioButton; private JRadioButton ownerNullRadioButton; - private JPanel ownerSpacer; - private JLabel titleLabel; private JTextField titleField; - private JPanel panel1; private FlatTriStateCheckBox overwritePromptCheckBox; private FlatTriStateCheckBox pathMustExistCheckBox; private FlatTriStateCheckBox noDereferenceLinksCheckBox; @@ -681,39 +605,18 @@ public class FlatSystemFileChooserWindowsTest private FlatTriStateCheckBox supportStreamableItemsCheckBox; private FlatTriStateCheckBox allowMultiSelectCheckBox; private FlatTriStateCheckBox hidePinnedPlacesCheckBox; - private JPanel messageDialogPanel; - private JLabel messageLabel; - private JScrollPane messageScrollPane; private JTextArea messageField; - private JLabel buttonsLabel; private JTextField buttonsField; - private JLabel okButtonLabelLabel; private JTextField okButtonLabelField; - private JLabel fileNameLabelLabel; private JTextField fileNameLabelField; - private JLabel fileNameLabel; private JTextField fileNameField; - private JLabel folderLabel; private JTextField folderField; - private JLabel saveAsItemLabel; private JTextField saveAsItemField; - private JLabel defaultFolderLabel; private JTextField defaultFolderField; - private JLabel defaultExtensionLabel; private JTextField defaultExtensionField; - private JLabel fileTypesLabel; private JComboBox fileTypesField; - private JLabel fileTypeIndexLabel; private JSlider fileTypeIndexSlider; - private JButton openButton; - private JButton saveButton; - private JButton openDirectButton; - private JButton saveDirectButton; private JCheckBox showMessageDialogOnOKCheckBox; - private JPanel hSpacer1; - private JButton messageDialogButton; - private JButton messageBoxButton; - 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/FlatSystemFileChooserWindowsTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd index 040c5c82..86f6219e 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd @@ -3,6 +3,9 @@ JFDML JFormDesigner: "8.3" encoding: "UTF-8" new FormModel { contentType: "form/swing" root: new FormRoot { + auxiliary() { + "JavaCodeGenerator.defaultVariableLocal": true + } 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]" @@ -20,6 +23,9 @@ new FormModel { "text": "JFrame" "selected": true "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" } ) @@ -27,6 +33,9 @@ new FormModel { name: "ownerDialogRadioButton" "text": "JDialog" "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" } ) @@ -34,6 +43,9 @@ new FormModel { name: "ownerNullRadioButton" "text": "null" "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" } ) @@ -50,6 +62,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "titleField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 1" } ) @@ -62,54 +77,81 @@ new FormModel { add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "overwritePromptCheckBox" "text": "overwritePrompt" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "pathMustExistCheckBox" "text": "pathMustExist" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "noDereferenceLinksCheckBox" "text": "noDereferenceLinks" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 0" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "strictFileTypesCheckBox" "text": "strictFileTypes" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "fileMustExistCheckBox" "text": "fileMustExist" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 1" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "okButtonNeedsInteractionCheckBox" "text": "okButtonNeedsInteraction" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 1" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "noChangeDirCheckBox" "text": "noChangeDir" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "createPromptCheckBox" "text": "createPrompt" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 2" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "dontAddToRecentCheckBox" "text": "dontAddToRecent" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 2" } ) @@ -117,12 +159,18 @@ new FormModel { name: "pickFoldersCheckBox" "text": "pickFolders" "font": new com.jformdesigner.model.SwingDerivedFont( null, 1, 0, false ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "shareAwareCheckBox" "text": "shareAware" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 3" } ) @@ -130,60 +178,90 @@ new FormModel { name: "forceShowHiddenCheckBox" "text": "forceShowHidden" "font": new com.jformdesigner.model.SwingDerivedFont( null, 1, 0, false ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 3" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "forceFileSystemCheckBox" "text": "forceFileSystem" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 4" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "noReadOnlyReturnCheckBox" "text": "noReadOnlyReturn" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 4" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "defaultNoMiniModeCheckBox" "text": "defaultNoMiniMode" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 4" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "allNonStorageItemsCheckBox" "text": "allNonStorageItems" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 5" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "noTestFileCreateCheckBox" "text": "noTestFileCreate" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 5" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "forcePreviewPaneonCheckBox" "text": "forcePreviewPaneon" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 5" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "noValidateCheckBox" "text": "noValidate" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 6" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "hideMruPlacesCheckBox" "text": "hideMruPlaces" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 6" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "supportStreamableItemsCheckBox" "text": "supportStreamableItems" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 6" } ) @@ -191,12 +269,18 @@ new FormModel { name: "allowMultiSelectCheckBox" "text": "allowMultiSelect" "font": new com.jformdesigner.model.SwingDerivedFont( null, 1, 0, false ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 7" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { name: "hidePinnedPlacesCheckBox" "text": "hidePinnedPlaces" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 7" } ) @@ -219,6 +303,9 @@ new FormModel { name: "messageField" "columns": 40 "rows": 4 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 0" @@ -231,6 +318,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "buttonsField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 1" } ) @@ -248,6 +338,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "okButtonLabelField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 2" } ) @@ -259,6 +352,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "fileNameLabelField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 3" } ) @@ -270,6 +366,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "fileNameField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 4" } ) @@ -281,6 +380,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "folderField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 5" } ) @@ -292,6 +394,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "saveAsItemField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 6" } ) @@ -303,6 +408,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "defaultFolderField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 7" } ) @@ -314,6 +422,9 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JTextField" ) { name: "defaultExtensionField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 8" } ) @@ -333,6 +444,9 @@ new FormModel { addElement( "Text Files,*.txt,PDF Files,*.pdf,All Files,*.*" ) addElement( "Text and PDF Files,*.txt;*.pdf" ) } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 9" } ) @@ -349,6 +463,9 @@ new FormModel { "value": 0 "paintLabels": true "snapToTicks": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 10" } ) @@ -383,6 +500,9 @@ new FormModel { add( new FormComponent( "javax.swing.JCheckBox" ) { name: "showMessageDialogOnOKCheckBox" "text": "show message dialog on OK" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 11 3 1" } ) @@ -410,6 +530,9 @@ new FormModel { add( new FormComponent( "javax.swing.JTextArea" ) { name: "filesField" "rows": 8 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 12 3 1,growx" From 54d6959533ec78d5a462300ff30e12e652bef13c Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 23 Jan 2025 19:52:39 +0100 Subject: [PATCH 24/34] System File Chooser: - always use some window as owner (similar to `JFileChooser`) - Linux: use "Select" for approve button in directory selection --- .../flatlaf/util/SystemFileChooser.java | 22 +++++++++++-------- .../src/main/cpp/GtkFileChooser.cpp | 3 ++- 2 files changed, 15 insertions(+), 10 deletions(-) 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 79c97f9d..53c52861 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 @@ -17,6 +17,7 @@ package com.formdev.flatlaf.util; import java.awt.Component; +import java.awt.KeyboardFocusManager; import java.awt.SecondaryLoop; import java.awt.Toolkit; import java.awt.Window; @@ -617,8 +618,14 @@ public class SystemFileChooser } private int showDialogImpl( Component parent ) { + Window owner = (parent instanceof Window) + ? (Window) parent + : (parent != null) ? SwingUtilities.windowForComponent( parent ) : null; + if( owner == null ) + owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow(); + approveResult = APPROVE_OPTION; - File[] files = getProvider().showDialog( parent, this ); + File[] files = getProvider().showDialog( owner, this ); setSelectedFiles( files ); return (files != null) ? approveResult : CANCEL_OPTION; } @@ -640,7 +647,7 @@ public class SystemFileChooser //---- interface FileChooserProvider -------------------------------------- private interface FileChooserProvider { - File[] showDialog( Component parent, SystemFileChooser fc ); + File[] showDialog( Window owner, SystemFileChooser fc ); } //---- class SystemFileChooserProvider ------------------------------------ @@ -649,10 +656,7 @@ public class SystemFileChooser implements FileChooserProvider { @Override - public File[] showDialog( Component parent, SystemFileChooser fc ) { - Window owner = (parent instanceof Window) - ? (Window) parent - : (parent != null) ? SwingUtilities.windowForComponent( parent ) : null; + public File[] showDialog( Window owner, SystemFileChooser fc ) { AtomicReference filenamesRef = new AtomicReference<>(); // create secondary event look and invoke system file dialog on a new thread @@ -667,7 +671,7 @@ public class SystemFileChooser // fallback to Swing file chooser if system file dialog failed or is not available if( filenames == null ) - return new SwingFileChooserProvider().showDialog( parent, fc ); + return new SwingFileChooserProvider().showDialog( owner, fc ); // canceled? if( filenames.length == 0 ) @@ -1068,7 +1072,7 @@ public class SystemFileChooser implements FileChooserProvider { @Override - public File[] showDialog( Component parent, SystemFileChooser fc ) { + public File[] showDialog( Window owner, SystemFileChooser fc ) { JFileChooser chooser = new JFileChooser() { @Override public void approveSelection() { @@ -1139,7 +1143,7 @@ public class SystemFileChooser chooser.setCurrentDirectory( fc.getCurrentDirectory() ); chooser.setSelectedFile( fc.getSelectedFile() ); - if( chooser.showDialog( parent, null ) != JFileChooser.APPROVE_OPTION ) + if( chooser.showDialog( owner, null ) != JFileChooser.APPROVE_OPTION ) return null; return chooser.isMultiSelectionEnabled() diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp index 1aacde3a..47a64a55 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp @@ -194,7 +194,8 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrar selectFolder ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER : (open ? GTK_FILE_CHOOSER_ACTION_OPEN : GTK_FILE_CHOOSER_ACTION_SAVE), _("_Cancel"), GTK_RESPONSE_CANCEL, - (cokButtonLabel != NULL) ? cokButtonLabel : (open ? _("_Open") : _("_Save")), GTK_RESPONSE_ACCEPT, + (cokButtonLabel != NULL) ? cokButtonLabel + : (selectFolder ? _("_Select") : (open ? _("_Open") : _("_Save"))), GTK_RESPONSE_ACCEPT, NULL ); // marks end of buttons GtkFileChooser* chooser = GTK_FILE_CHOOSER( dialog ); From 03e5f8623e552975c96d80220822741dcbece7bf Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 27 Jan 2025 18:45:50 +0100 Subject: [PATCH 25/34] update to Gradle 8.12.1 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a793..e18bc253 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 5d247f62695e82919282f1384e68510da22d4096 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 17 Mar 2025 19:27:47 +0100 Subject: [PATCH 26/34] GitHub Actions: natives.yml: include only the core natives that have been built in artefacts --- .github/workflows/natives.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/natives.yml b/.github/workflows/natives.yml index 6ff056e4..7bbe9654 100644 --- a/.github/workflows/natives.yml +++ b/.github/workflows/natives.yml @@ -66,10 +66,20 @@ jobs: # tar.exe: Couldn't open ~/.gradle/caches/modules-2/modules-2.lock: Permission denied run: ./gradlew build-natives --no-daemon + - name: Set artifacts pattern + shell: bash + run: | + case ${{ matrix.os }} in + windows-latest) echo "artifactPattern=flatlaf-windows-*.dll" >> $GITHUB_ENV ;; + macos-latest) echo "artifactPattern=libflatlaf-macos-*.dylib" >> $GITHUB_ENV ;; + ubuntu-latest) echo "artifactPattern=libflatlaf-linux-x86_64.so" >> $GITHUB_ENV ;; + ubuntu-24.04-arm) echo "artifactPattern=libflatlaf-linux-arm64.so" >> $GITHUB_ENV ;; + esac + - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: FlatLaf-natives-build-artifacts-${{ matrix.os }} path: | - flatlaf-core/src/main/resources/com/formdev/flatlaf/natives + flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/${{ env.artifactPattern }} flatlaf-natives/flatlaf-natives-*/build From 202a0d159b999f9c1a1d3b5d5064d2067e46f8c7 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 18 Mar 2025 18:46:53 +0100 Subject: [PATCH 27/34] GitHub Actions: natives.yml: sign Windows and macOS native libraries --- .github/workflows/natives.yml | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.github/workflows/natives.yml b/.github/workflows/natives.yml index 7bbe9654..0b856b3a 100644 --- a/.github/workflows/natives.yml +++ b/.github/workflows/natives.yml @@ -66,6 +66,44 @@ jobs: # tar.exe: Couldn't open ~/.gradle/caches/modules-2/modules-2.lock: Permission denied run: ./gradlew build-natives --no-daemon + - name: Sign Windows DLLs + if: matrix.os == 'windows-latest' + uses: skymatic/code-sign-action@v3 + with: + certificate: '${{ secrets.CODE_SIGN_CERT_BASE64 }}' + password: '${{ secrets.CODE_SIGN_CERT_PASSWORD }}' + certificatesha1: '${{ secrets.CODE_SIGN_CERT_SHA1 }}' + folder: 'flatlaf-core/src/main/resources/com/formdev/flatlaf/natives' + + - name: Sign macOS natives + if: matrix.os == 'macos-latest' + env: + CERT_BASE64: ${{ secrets.CODE_SIGN_CERT_BASE64 }} + CERT_PASSWORD: ${{ secrets.CODE_SIGN_CERT_PASSWORD }} + CERT_IDENTITY: ${{ secrets.CODE_SIGN_CERT_IDENTITY }} + run: | + # https://docs.github.com/en/actions/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development + # create variables + CERTIFICATE_PATH=$RUNNER_TEMP/cert.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/signing.keychain-db + KEYCHAIN_PASSWORD=$CERT_PASSWORD + # decode certificate + printenv CERT_BASE64 | base64 --decode > $CERTIFICATE_PATH + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$CERT_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security list-keychains -d user -s $KEYCHAIN_PATH + # sign code + codesign -s "$CERT_IDENTITY" -fv --timestamp \ + flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-*.dylib + codesign -d --verbose=4 flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-*.dylib + # cleanup + security delete-keychain $KEYCHAIN_PATH + - name: Set artifacts pattern shell: bash run: | From 3e8b213367c38b8f30e0f46dcdd5981930375d38 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 20 Mar 2025 18:41:12 +0100 Subject: [PATCH 28/34] System File Chooser: fixed font in message dialog on Windows --- .../src/main/cpp/WinMessageDialog.cpp | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp index 7a087fc8..15926115 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp @@ -84,10 +84,10 @@ JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_show // all values in DLUs -#define INSETS_TOP 16 +#define INSETS_TOP 12 #define INSETS_LEFT 12 #define INSETS_RIGHT 12 -#define INSETS_BOTTOM 8 +#define INSETS_BOTTOM 6 #define ICON_TEXT_GAP 8 @@ -96,18 +96,41 @@ JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_show #define LABEL_HEIGHT 8 #define BUTTON_WIDTH 50 -#define BUTTON_HEIGHT 14 +#define BUTTON_HEIGHT 12 #define BUTTON_GAP 5 -#define BUTTON_TOP_GAP 16 +#define BUTTON_TOP_GAP 14 #define BUTTON_LEFT_RIGHT_GAP 8 // based on https://learn.microsoft.com/en-us/windows/win32/dlgbox/using-dialog-boxes#creating-a-template-in-memory static byte* createInMemoryTemplate( HWND owner, int messageType, LPCWSTR title, LPCWSTR text, int defaultButton, int buttonCount, LPCWSTR* buttons ) { - //---- calculate layout (in DLUs) ---- + // get font info needed for DS_SETFONT + NONCLIENTMETRICS ncMetrics; + ncMetrics.cbSize = sizeof( NONCLIENTMETRICS ); + if( !::SystemParametersInfo( SPI_GETNONCLIENTMETRICS, 0, &ncMetrics, 0 ) ) + return NULL; - HDC hdc = GetDC( owner ); + // create DC to use message font + HDC hdcOwner = ::GetDC( owner ); + HDC hdc = ::CreateCompatibleDC( hdcOwner ); + ::ReleaseDC( owner, hdcOwner ); + if( hdc == NULL ) + return NULL; + + HFONT hfont = ::CreateFontIndirect( &ncMetrics.lfMessageFont ); + if( hfont == NULL ) { + ::DeleteDC( hdc ); + return NULL; + } + + if( ::SelectObject( hdc, hfont ) == NULL ) { + ::DeleteDC( hdc ); + ::DeleteObject( hfont ); + return NULL; + } + + //---- calculate layout (in DLUs) ---- // layout icon LPWSTR icon; @@ -226,13 +249,24 @@ static byte* createInMemoryTemplate( HWND owner, int messageType, LPCWSTR title, int bx = dw - buttonTotalWidth - BUTTON_LEFT_RIGHT_GAP; int by = dh - BUTTON_HEIGHT - INSETS_BOTTOM; + // get font info needed for DS_SETFONT + int fontPointSize = (ncMetrics.lfMessageFont.lfHeight < 0) + ? -MulDiv( ncMetrics.lfMessageFont.lfHeight, 72, ::GetDeviceCaps( hdc, LOGPIXELSY ) ) + : ncMetrics.lfMessageFont.lfHeight; + LPCWSTR fontFaceName = ncMetrics.lfMessageFont.lfFaceName; + + // delete DC and font + ::DeleteDC( hdc ); + ::DeleteObject( hfont ); + // (approximately) calculate memory size needed for in-memory template int templSize = (sizeof(DLGTEMPLATE) + /*menu*/ 2 + /*class*/ 2 + /*title*/ 2) + ((sizeof(DLGITEMTEMPLATE) + /*class*/ 4 + /*title/icon*/ 4 + /*creation data*/ 2) * (/*icon+text*/2 + buttonCount)) - + (title != NULL ? wcslen( title ) * sizeof(wchar_t) : 0) - + (wcslen( wrappedText ) * sizeof(wchar_t)); + + (title != NULL ? (wcslen( title ) + 1) * sizeof(wchar_t) : 0) + + /*fontPointSize*/ 2 + ((wcslen( fontFaceName ) + 1) * sizeof(wchar_t)) + + ((wcslen( wrappedText ) + 1) * sizeof(wchar_t)); for( int i = 0; i < buttonCount; i++ ) - templSize += (wcslen( buttons[i] ) * sizeof(wchar_t)); + templSize += ((wcslen( buttons[i] ) + 1) * sizeof(wchar_t)); templSize += (2 * (1 + 1 + buttonCount)); // necessary for DWORD alignment templSize += 100; // some reserve @@ -246,7 +280,7 @@ static byte* createInMemoryTemplate( HWND owner, int messageType, LPCWSTR title, //---- define dialog box ---- LPDLGTEMPLATE lpdt = (LPDLGTEMPLATE) templ; - lpdt->style = WS_POPUP | WS_BORDER | WS_SYSMENU | DS_MODALFRAME | WS_CAPTION; + lpdt->style = WS_POPUP | WS_BORDER | WS_SYSMENU | DS_MODALFRAME | WS_CAPTION | DS_SETFONT; lpdt->cdit = /*text*/ 1 + buttonCount; // number of controls lpdt->x = dx; lpdt->y = dy; @@ -262,6 +296,10 @@ static byte* createInMemoryTemplate( HWND owner, int messageType, LPCWSTR title, } else *lpw++ = 0; // no title + // for DS_SETFONT + *lpw++ = fontPointSize; + wcscpy( (LPWSTR) lpw, fontFaceName ); + lpw += wcslen( fontFaceName ) + 1; //---- define icon ---- @@ -342,11 +380,14 @@ static BOOL CALLBACK focusDefaultButtonProc( HWND hwnd, LPARAM lParam ) { } static INT_PTR CALLBACK messageDialogProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { - if( uMsg == WM_INITDIALOG ) - ::EnumChildWindows( hwnd, focusDefaultButtonProc, 0 ); - else if( uMsg == WM_COMMAND ) { - ::EndDialog( hwnd, wParam ); - return TRUE; + switch( uMsg ) { + case WM_INITDIALOG: + ::EnumChildWindows( hwnd, focusDefaultButtonProc, 0 ); + break; + + case WM_COMMAND: + ::EndDialog( hwnd, wParam ); + return TRUE; } return FALSE; } From dade1cba5a5998285124a5c87cd41dd454180386 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 22 Mar 2025 13:51:16 +0100 Subject: [PATCH 29/34] System File Chooser: - introduced state storage - added "New Folder" to macOS select folder dialog - Demo: added "Select Folder (System)" menu item - javadoc fixes --- .../flatlaf/util/SystemFileChooser.java | 135 ++++++++++++++++-- .../com/formdev/flatlaf/demo/DemoFrame.java | 34 ++++- .../com/formdev/flatlaf/demo/DemoFrame.jfd | 6 + .../com/formdev/flatlaf/demo/FlatLafDemo.java | 24 ++++ .../testing/FlatSystemFileChooserTest.java | 57 ++++++++ .../testing/FlatSystemFileChooserTest.jfd | 33 ++++- 6 files changed, 275 insertions(+), 14 deletions(-) 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 53c52861..be09b6f3 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 @@ -60,6 +60,10 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; * {@code SystemFileChooser} requires FlatLaf native libraries (usually contained in flatlaf.jar). * If not available or disabled (via {@link FlatSystemProperties#USE_NATIVE_LIBRARY} * or {@link FlatSystemProperties#USE_SYSTEM_FILE_CHOOSER}), then {@code JFileChooser} is used. + *

    + * To improve user experience, it is recommended to use a state storage + * (see {@link #setStateStore(StateStore)}), so that file dialogs open at previously + * visited folder. * *

    Limitations/incompatibilities compared to JFileChooser

    * @@ -90,6 +94,11 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; *
  • macOS: By default, the user can not navigate into file packages (e.g. applications). * If needed, this can be enabled by setting platform property * {@link #MAC_TREATS_FILE_PACKAGES_AS_DIRECTORIES} to {@code true}. + *
  • If no "current directory" is specified, then {@code JFileChooser} always opens + * the users "Documents" folder (on Windows) or "Home" folder (on macOS and Linux).
    + * {@code SystemFileChooser} does the same when first shown, but then remembers + * last visited folder (either in memory or in a {@link StateStore}) and + * re-uses that folder when {@code SystemFileChooser} is shown again. * * * @author Karl Tauber @@ -248,6 +257,25 @@ public class SystemFileChooser private Map platformProperties; + private static final StateStore inMemoryStateStore = new StateStore() { + private final Map state = new HashMap<>(); + + @Override + public String get( String key, String def ) { + return state.getOrDefault( key, def ); + } + + @Override + public void put( String key, String value ) { + if( value != null ) + state.put( key, value ); + else + state.remove( key ); + } + }; + + private static StateStore stateStore; + private String stateStoreID; /** @see JFileChooser#JFileChooser() */ public SystemFileChooser() { @@ -384,18 +412,35 @@ public class SystemFileChooser /** @see JFileChooser#getCurrentDirectory() */ public File getCurrentDirectory() { + if( currentDirectory == null ) { + // get current directory from state store + StateStore store = (stateStore != null) ? stateStore : inMemoryStateStore; + String path = store.get( buildStateKey( StateStore.KEY_CURRENT_DIRECTORY ), null ); + if( path != null ) + currentDirectory = getTraversableDirectory( FileSystemView.getFileSystemView().createFileObject( path ) ); + + // for compatibility with JFileChooser + if( currentDirectory == null ) + currentDirectory = getTraversableDirectory( FileSystemView.getFileSystemView().getDefaultDirectory() ); + } + return currentDirectory; } /** @see JFileChooser#setCurrentDirectory(File) */ public void setCurrentDirectory( File dir ) { - // for compatibility with JFileChooser - if( dir != null && !dir.exists() ) - return; - if( dir == null ) - dir = FileSystemView.getFileSystemView().getDefaultDirectory(); + currentDirectory = getTraversableDirectory( dir ); + } - currentDirectory = dir; + private File getTraversableDirectory( File dir ) { + if( dir == null ) + return null; + + // make sure to use existing (traversable) directory + FileSystemView fsv = FileSystemView.getFileSystemView(); + while( dir != null && !fsv.isTraversable( dir ) ) + dir = fsv.getParentDirectory( dir ); + return dir; } /** @see JFileChooser#getSelectedFile() */ @@ -533,7 +578,7 @@ public class SystemFileChooser * Sets a callback that is invoked when user presses "OK" button (or double-clicks a file). * The file dialog is still open. * If the callback returns {@link #CANCEL_OPTION}, then the file dialog stays open. - * If it returns {@link #APPROVE_OPTION} (or any other value other than {@link #CANCEL_OPTION}), + * If it returns {@link #APPROVE_OPTION} (or any value other than {@link #CANCEL_OPTION}), * the file dialog is closed and the {@code show...Dialog()} methods return that value. *

    * The callback has two parameters: @@ -562,9 +607,9 @@ public class SystemFileChooser * } * } * - * WARNING: Do not show a Swing dialog for the callback. This will not work! + * WARNING: Do not show a Swing dialog from within the callback. This will not work! *

    - * Instead use {@link ApproveContext#showMessageDialog(int, String, String, int, String...)}, + * Instead, use {@link ApproveContext#showMessageDialog(int, String, String, int, String...)}, * which shows a modal system message dialog as child of the file dialog. * *

    {@code
    @@ -617,6 +662,43 @@ public class SystemFileChooser
     		return (value instanceof Integer) ? (Integer) value & ~optionsBlocked : 0;
     	}
     
    +	/**
    +	 * Returns state storage used to persist file chooser state (e.g. last used directory).
    +	 * Or {@code null} if there is no state storage (the default).
    +	 */
    +	public static StateStore getStateStore() {
    +		return stateStore;
    +	}
    +
    +	/**
    +	 * Sets state storage used to persist file chooser state (e.g. last used directory).
    +	 */
    +	public static void setStateStore( StateStore stateStore ) {
    +		SystemFileChooser.stateStore = stateStore;
    +	}
    +
    +	/**
    +	 * Returns the ID used to prefix keys in state storage. Or {@code null} (the default).
    +	 */
    +	public String getStateStoreID() {
    +		return stateStoreID;
    +	}
    +
    +	/**
    +	 * Sets the ID used to prefix keys in state storage. Or {@code null} (the default).
    +	 * 

    + * By specifying an ID, an application can have different persisted states + * for different kinds of file dialogs within the application. E.g. Import/Export + * file dialogs could use a different ID then Open/Save file dialogs. + */ + public void setStateStoreID( String stateStoreID ) { + this.stateStoreID = stateStoreID; + } + + private String buildStateKey( String key ) { + return (stateStoreID != null) ? stateStoreID + '.' + key : key; + } + private int showDialogImpl( Component parent ) { Window owner = (parent instanceof Window) ? (Window) parent @@ -627,7 +709,16 @@ public class SystemFileChooser approveResult = APPROVE_OPTION; File[] files = getProvider().showDialog( owner, this ); setSelectedFiles( files ); - return (files != null) ? approveResult : CANCEL_OPTION; + if( files == null ) + return CANCEL_OPTION; + + // remember current directory in state store + File currentDirectory = getCurrentDirectory(); + StateStore store = (stateStore != null) ? stateStore : inMemoryStateStore; + store.put( buildStateKey( StateStore.KEY_CURRENT_DIRECTORY ), + (currentDirectory != null) ? currentDirectory.getAbsolutePath() : null ); + + return approveResult; } private FileChooserProvider getProvider() { @@ -870,7 +961,7 @@ public class SystemFileChooser if( (optionsClear & FlatNativeMacLibrary.FC_accessoryViewDisclosed) == 0 ) optionsSet |= FlatNativeMacLibrary.FC_accessoryViewDisclosed; if( fc.isDirectorySelectionEnabled() ) { - optionsSet |= FlatNativeMacLibrary.FC_canChooseDirectories; + optionsSet |= FlatNativeMacLibrary.FC_canChooseDirectories | FlatNativeMacLibrary.FC_canCreateDirectories; optionsClear |= FlatNativeMacLibrary.FC_canChooseFiles; open = true; } @@ -1335,4 +1426,26 @@ public class SystemFileChooser public abstract int showMessageDialog( int messageType, String primaryText, String secondaryText, int defaultButton, String... buttons ); } + + //---- class StateStore --------------------------------------------------- + + /** + * Simple state storage used to persist file chooser state (e.g. last used directory). + * + * @see SystemFileChooser#setStateStore(StateStore) + * @see SystemFileChooser#setStateStoreID(String) + */ + public interface StateStore { + String KEY_CURRENT_DIRECTORY = "currentDirectory"; + + /** + * Returns the value for the given key, or the default value if there is no value stored. + */ + String get( String key, String def ); + + /** + * Stores the given key and value. If value is {@code null}, it is removed from the store. + */ + void put( String key, String value ); + } } diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java index 088d3083..08b0a202 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java @@ -18,6 +18,7 @@ package com.formdev.flatlaf.demo; import java.awt.*; import java.awt.event.*; +import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -175,13 +176,19 @@ class DemoFrame private void openSystemActionPerformed() { SystemFileChooser chooser = new SystemFileChooser(); + chooser.setMultiSelectionEnabled( true ); chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( "Text Files", "txt", "md" ) ); chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( "PDF Files", "pdf" ) ); chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( "Archives", "zip", "tar", "jar", "7z" ) ); - chooser.showOpenDialog( this ); + + if( chooser.showOpenDialog( this ) != SystemFileChooser.APPROVE_OPTION ) + return; + + File[] files = chooser.getSelectedFiles(); + System.out.println( Arrays.toString( files ).replace( ",", "\n" ) ); } private void saveAsSystemActionPerformed() { @@ -190,7 +197,23 @@ class DemoFrame "Text Files", "txt", "md" ) ); chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( "Images", "png", "gif", "jpg" ) ); - chooser.showSaveDialog( this ); + + if( chooser.showSaveDialog( this ) != SystemFileChooser.APPROVE_OPTION ) + return; + + File file = chooser.getSelectedFile(); + System.out.println( file ); + } + + private void selectFolderSystemActionPerformed() { + SystemFileChooser chooser = new SystemFileChooser(); + chooser.setFileSelectionMode( SystemFileChooser.DIRECTORIES_ONLY ); + + if( chooser.showOpenDialog( this ) != SystemFileChooser.APPROVE_OPTION ) + return; + + File directory = chooser.getSelectedFile(); + System.out.println( directory ); } private void exitActionPerformed() { @@ -519,6 +542,7 @@ class DemoFrame JMenuItem saveAsMenuItem = new JMenuItem(); JMenuItem openSystemMenuItem = new JMenuItem(); JMenuItem saveAsSystemMenuItem = new JMenuItem(); + JMenuItem selectFolderSystemMenuItem = new JMenuItem(); JMenuItem closeMenuItem = new JMenuItem(); exitMenuItem = new JMenuItem(); JMenu editMenu = new JMenu(); @@ -630,6 +654,12 @@ class DemoFrame saveAsSystemMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()|KeyEvent.SHIFT_DOWN_MASK)); saveAsSystemMenuItem.addActionListener(e -> saveAsSystemActionPerformed()); fileMenu.add(saveAsSystemMenuItem); + + //---- selectFolderSystemMenuItem ---- + selectFolderSystemMenuItem.setText("Select Folder (System)..."); + selectFolderSystemMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()|KeyEvent.SHIFT_DOWN_MASK)); + selectFolderSystemMenuItem.addActionListener(e -> selectFolderSystemActionPerformed()); + fileMenu.add(selectFolderSystemMenuItem); fileMenu.addSeparator(); //---- closeMenuItem ---- diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd index 5cce720d..70bb49ac 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd @@ -197,6 +197,12 @@ new FormModel { "accelerator": static javax.swing.KeyStroke getKeyStroke( 83, 4291, false ) addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "saveAsSystemActionPerformed", false ) ) } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "selectFolderSystemMenuItem" + "text": "Select Folder (System)..." + "accelerator": static javax.swing.KeyStroke getKeyStroke( 70, 4291, false ) + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "selectFolderSystemActionPerformed", false ) ) + } ) add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { name: "separator2" } ) diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java index 09768e3e..18ce55e5 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java @@ -17,6 +17,7 @@ package com.formdev.flatlaf.demo; import java.awt.Dimension; +import java.util.prefs.Preferences; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.SwingUtilities; @@ -27,6 +28,7 @@ import com.formdev.flatlaf.fonts.inter.FlatInterFont; import com.formdev.flatlaf.fonts.jetbrains_mono.FlatJetBrainsMonoFont; import com.formdev.flatlaf.fonts.roboto.FlatRobotoFont; import com.formdev.flatlaf.fonts.roboto_mono.FlatRobotoMonoFont; +import com.formdev.flatlaf.util.SystemFileChooser; import com.formdev.flatlaf.util.SystemInfo; /** @@ -73,6 +75,28 @@ public class FlatLafDemo DemoPrefs.init( PREFS_ROOT_PATH ); DemoPrefs.initSystemScale(); + // SystemFileChooser state storage + SystemFileChooser.setStateStore( new SystemFileChooser.StateStore() { + private static final String KEY_PREFIX = "fileChooser."; + private final Preferences state = Preferences.userRoot().node( PREFS_ROOT_PATH ); + + @Override + public String get( String key, String def ) { + String value = state.get( KEY_PREFIX + key, def ); + System.out.println( "SystemFileChooser State GET " + key + " = " + value ); + return value; + } + + @Override + public void put( String key, String value ) { + System.out.println( "SystemFileChooser State PUT " + key + " = " + value ); + if( value != null ) + state.put( KEY_PREFIX + key, value ); + else + state.remove( KEY_PREFIX + key ); + } + } ); + SwingUtilities.invokeLater( () -> { // install fonts for lazy loading FlatInterFont.installLazy(); diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java index d7fda240..adfd8ef2 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java @@ -89,10 +89,39 @@ public class FlatSystemFileChooserTest currentDirCheckBox.setSelected( state.getBoolean( "systemfilechooser.currentdir.enabled", false ) ); selectedFileCheckBox.setSelected( state.getBoolean( "systemfilechooser.selectedfile.enabled", false ) ); selectedFilesCheckBox.setSelected( state.getBoolean( "systemfilechooser.selectedfiles.enabled", false ) ); + persistStateCheckBox.setSelected( state.getBoolean( "systemfilechooser.persistState.enabled", false ) ); currentDirChanged(); selectedFileChanged(); selectedFilesChanged(); + persistStateChanged(); + } + + private void persistStateChanged() { + boolean b = persistStateCheckBox.isSelected(); + + SystemFileChooser.setStateStore( b + ? new SystemFileChooser.StateStore() { + private static final String KEY_PREFIX = "fileChooser."; + + @Override + public String get( String key, String def ) { + String value = DemoPrefs.getState().get( KEY_PREFIX + key, def ); + System.out.println( "GET " + key + " = " + value ); + return value; + } + + @Override + public void put( String key, String value ) { + System.out.println( "PUT " + key + " = " + value ); + if( value != null ) + DemoPrefs.getState().put( KEY_PREFIX + key, value ); + else + DemoPrefs.getState().remove( KEY_PREFIX + key ); + } + } : null ); + + DemoPrefs.getState().putBoolean( "systemfilechooser.persistState.enabled", b ); } private void open() { @@ -221,6 +250,9 @@ public class FlatSystemFileChooserTest // fc.putPlatformProperty( SystemFileChooser.MAC_OPTIONS_CLEAR, FlatNativeMacLibrary.FC_showsTagField ); // fc.putPlatformProperty( SystemFileChooser.LINUX_OPTIONS_CLEAR, FlatNativeLinuxLibrary.FC_create_folders | FlatNativeLinuxLibrary.FC_do_overwrite_confirmation ); + + String id = (String) stateStoreIDField.getSelectedItem(); + fc.setStateStoreID( id != null && !id.isEmpty() ? id : null ); } private void configureSwingFileChooser( JFileChooser fc ) { @@ -517,6 +549,9 @@ public class FlatSystemFileChooserTest multiSelectionEnabledCheckBox = new JCheckBox(); useFileHidingCheckBox = new JCheckBox(); useSystemFileChooserCheckBox = new JCheckBox(); + persistStateCheckBox = new JCheckBox(); + JLabel stateStoreIDLabel = new JLabel(); + stateStoreIDField = new JComboBox<>(); JLabel approveButtonTextLabel = new JLabel(); approveButtonTextField = new JTextField(); JLabel approveButtonMnemonicLabel = new JLabel(); @@ -605,6 +640,8 @@ public class FlatSystemFileChooserTest "[]0" + "[]0" + "[]" + + "[]para" + + "[]" + "[]")); //---- directorySelectionCheckBox ---- @@ -624,6 +661,24 @@ public class FlatSystemFileChooserTest useSystemFileChooserCheckBox.setText("use SystemFileChooser"); useSystemFileChooserCheckBox.setSelected(true); panel1.add(useSystemFileChooserCheckBox, "cell 0 3"); + + //---- persistStateCheckBox ---- + persistStateCheckBox.setText("persist state"); + persistStateCheckBox.addActionListener(e -> persistStateChanged()); + panel1.add(persistStateCheckBox, "cell 0 4"); + + //---- stateStoreIDLabel ---- + stateStoreIDLabel.setText("ID:"); + panel1.add(stateStoreIDLabel, "cell 0 5"); + + //---- stateStoreIDField ---- + stateStoreIDField.setModel(new DefaultComboBoxModel<>(new String[] { + "abc", + "def" + })); + stateStoreIDField.setEditable(true); + stateStoreIDField.setSelectedIndex(-1); + panel1.add(stateStoreIDField, "cell 0 5,growx"); } add(panel1, "cell 2 1 1 7,aligny top,growy 0"); @@ -806,6 +861,8 @@ public class FlatSystemFileChooserTest private JCheckBox multiSelectionEnabledCheckBox; private JCheckBox useFileHidingCheckBox; private JCheckBox useSystemFileChooserCheckBox; + private JCheckBox persistStateCheckBox; + private JComboBox stateStoreIDField; private JTextField approveButtonTextField; private JTextField approveButtonMnemonicField; private JCheckBox currentDirCheckBox; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd index 9031aa7c..9a37ee61 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd @@ -71,7 +71,7 @@ new FormModel { add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets 2,hidemode 3" "$columnConstraints": "[left]" - "$rowConstraints": "[]0[]0[][]" + "$rowConstraints": "[]0[]0[][]para[][]" } ) { name: "panel1" add( new FormComponent( "javax.swing.JCheckBox" ) { @@ -112,6 +112,37 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "persistStateCheckBox" + "text": "persist state" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "persistStateChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "stateStoreIDLabel" + "text": "ID:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "stateStoreIDField" + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "abc" + addElement( "abc" ) + addElement( "def" ) + } + "editable": true + "selectedIndex": -1 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5,growx" + } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 1 1 7,aligny top,growy 0" } ) From 35e86ba772a69e9507f53ddf7af75da1cfc0b9cd Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 22 Mar 2025 14:24:34 +0100 Subject: [PATCH 30/34] System File Chooser: updated all native libraries built and signed by GitHub Actions: https://github.com/JFormDesigner/FlatLaf/actions/runs/14008769165 --- .../flatlaf/natives/flatlaf-windows-arm64.dll | Bin 23272 -> 34016 bytes .../flatlaf/natives/flatlaf-windows-x86.dll | Bin 21728 -> 30440 bytes .../natives/flatlaf-windows-x86_64.dll | Bin 23784 -> 34536 bytes .../flatlaf/natives/libflatlaf-linux-arm64.so | Bin 70840 -> 75096 bytes .../natives/libflatlaf-linux-x86_64.so | Bin 16552 -> 28872 bytes .../natives/libflatlaf-macos-arm64.dylib | Bin 78496 -> 104032 bytes .../natives/libflatlaf-macos-x86_64.dylib | Bin 50016 -> 74992 bytes 7 files changed, 0 insertions(+), 0 deletions(-) diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-arm64.dll b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-arm64.dll index 716527e577ed3df9800e7d7b5cfe2d5708356ef2..9fbd790ac99960c06dbf47d06003c86347d5174b 100644 GIT binary patch delta 14710 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 delta 5402 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 JW-+D7{{gM5M+5)> diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86.dll b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86.dll index fb0f8325d25750a87bbb9149991968b4d8348eb2..96e41e992e0059b3e58a49a8eed56a47d9015d4b 100644 GIT binary patch delta 12839 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 delta 5237 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 TJ~i}J4GOEsOJoHtNLKw1j&dD! diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86_64.dll b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86_64.dll index e810d4a4e70466366b31c7e805141b6d955c6e95..6b3d15c51a021d77e8e7b9b2a592069f0b743a52 100644 GIT binary patch delta 17125 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 delta 7760 zcmd^Dd0Z67wys&&mth+m0b!6a7#A`uA};74i;WCI1jVQ^AdE_669)I#DxhGC0oz1l zG~#lvNp6frjUq7yuP834Xf$H3x#E@#ZgJlt&iksT3B0`f{(Jwu{{7CJQ(v7r=Tx1l zQ{7{#l$!QSs)kBhsw1Oyhu&Jbqil5Rs}4FHezm<^XkUF*11>*yv%N&%t?i`(zb5c8 z*_qSnm}-Ajb*r^TPIkP{Oe5lRpY&8aNS*r-F904Q_$F^J3A9T`OHRm_Bl*FSpaCSD zkSsK_1{1PE)Du`TAq(o)Pvt{y+%rU@NqV3H*=Mijw@;CeYnZ;@QjTw z;Hv)&tNt^*R`x%QD9ly&yK3_TZ2L>=`tn(}Gy64T(u&f3_>HR6y^h?12`HA8P$QK} zhudbeVsELmNks^^V#!usV&@T@hRn6ipbFe*HN#zCDi&zeX61cv>i4-sQI;eOPSt1` z?vze7EGbG~{3KXUmiwFAiZiXQpzTZB(t4NDzg@oUG;lW#?_jV-kAK%ApT$w)13X zw8PHYwp@Tr$sZnyWeJdK9c32m|Fna@s&}}>1<}% z3uDU^z(VWw+?lAwuQwrRi@xHc`k2Z^67-pRU}R{0n;FZDv9Fnu2rR|2OLWz_#u&v9 zjm#mF3r0aaMmM4_IJL?-iv*ReXZx>v`nS2jwj8^W+1iD zA#Ka>i#~PTE6YmhE3!1{X5|_v&?VAyQ`K~ty8VY`5|)sYFx)s7bJb2^9;3BTHd>pN z`;cUYbE@wR=Ehwx_5QAG#CVHa==_~ zzuz6>)Z%`#7p8lPp}Xl<6|Z$1Dy>)Xd5(e7H7b6MV~<|aZiIXc+UH5gNsyhA5MPiF zou)F zqW05lFx>iYmZ+*o9&3&lERM1{EdSgS)=r3I* zAuJSK(n$p=KeWyGZOlB4#*dc(X1To$$#V=Ag@F(AEZ$8 zmz|v)4`9hK6Qj%=EvKNWG4?PHI5pO2R+jwPo51&1^mRxS+fuWPDzi|g)-B3Pv2&m~ z!S;5@;>cnRA@|1;Qus!c5Nb@t2eb8+v9s$iLrXxPVnkVeu{9Sv8`IKrmPHJ$XE;j^ z^qjczeiOy#;nW&%2Zg5?Mb!Hn4X|eGMtQ#r#1Sd zxhh$R9%1M?qlTTX=Q@JySaB;aVXP%lSn%TROr_IncO*?oEXH5>!zebj0q5xn zVTszHgviZGlPiP@t4rw9bM;hpuuzH{V>DX1aO)sv&879mX#w%vc^X!#CIYpHYPwPj z$u0mtmlzf3&|$-tG!#TlnmqBaIYmWv92w4*;jrY#%qeO*Jf*kAh>pQTJCw!u0Lu7& z?mbu;!imR@;BGP81A6Yi!J;)-505dgW2VjVz^cUKR?c<3z0hoOYN&@~R zeR(K}l%Y+E{h3O=MvZi+Jz(6doZ><)8|}FmO}`4OR=ktpAkmBV0j@F2DKh5bdb=j{ zG@EYy4j5T~XTJ{SObNF9Hy#63tI%tTgx}Ko;$ZReJ89}?yGp4ZH35CMFEgAqR)JQ_ zsupYN1Wo$tV6OTccCdyik7(5MRhHYiA{zSun|eE5Oqq&kHC_g8uj3aeBm8y(5#Dv; zX<~31k^n%Cq4E|5|D!TV8lvDcx+(cVo*~jUXFk_6S-QqqK+by(l*T*rPTj^xpEv;- zp123^A2_jf>9}PCkDmfyaA>u$ToXOKy<@o}bu5e8zafCiS#L!Vh68+yFX4Sn!$ zR(|2c-|ZGFwRhsfR70d29Qm25H?1Gy4b9Zf*QipY6^{H9)gb9;M_$|AU-t)Yp)ULM z&(aGYPJS2^&oxJJFH9P|N{Z(LyPoNI>9}jpS9kZ3UbN?TboXMvL&1P8oJK|ILoBCo z)klp(A=l}-2h?;jtZu-2A*|vcm^R<)u&OAfX92w|dTHt_ax~-V{L@Xp+%cQ=Ty)3# z5@vo|s3}gV7FE+E!HS9D8LuFzwF5uL%V%iE>rsN1L6u1t-;vJQA!$0n(MeO8lzI|k zT7cSE(x8i)EL=Z)v)5qOTt!P{BIk*$#B&#gEZ@h;qG;%GY+N0^osszpy{3SvL_2D~ ziXy0zT0PfDp#p~ciMD$)P*O9EhmfL0W~B|HNaURLLP>;SdaSEyRTRKR!J92kv4HWP zdB4RsdxuEt?0C6PBs(92vA(NmNy3)~7P5_b+?hhW1u^h~f;k3@)N#JRh3_%5a;W8$ z8r7(lI$;@6E&W8$K{%A=X6!1IT!d^^#-Izw%pN6haRV&#^*%$`Ljw3`43_Ov+lCvm zL_jB)m-@#&_jQ_D&=$cX{@R?V7*f%ZFK%=7@Z%$aji-n8@D{xxL66MBXg&bs`^U z;Vii<0v$1&{)=N~sHG=d#2JxK68RF5_Yt|5$n8b`SRCIf^3x(eC~!R?TSZ{4$je1O zPvp}@K3?PnqT!B!J%(GPSHpI@4fc|-9e)g<#*kN_^&ucNA#{t&7ReTY3D0WaG_CZD zUHF*qA`2h+n%q1=QAA>=YJylHh{uz+`0U|nJg zqZm!peQG;9%Z~;M(|B~2zZ`<56cq{WWco)3KXrKB7-xJ6XfV z2Lam({Dv?LaRR4$otR}@*LGK15<5AuJ7-moV6;LgMt7q2AtV75gQN&ps#}@>6ah@8 zpvs!49y=138&1Ubya%Cpcn|odeVF%O+tk>fW6|H~fd4G2FP|9g$JSt7k$K`MQg5`! zgUUfwz2P1$%+4%|%P)*6$R^}i*Pgs$vPYI$n2|XnIlrK| zAkhGTBvHFmj5iv1AH6M`&;i64re+l97=h>Fg@YQ3&dE098OInhjpQM1Tf!0AE|R$H zoE$+Uw>$gv`FYbYTedTJiXUixST!`rCdei816D5V3Z&#wmq~)$2n;g0gaQ$Sj^6-9u8wZy4>}Jzf% z7iJr?Gc$5h5O;Py?t)hS?C5Un2pJ5IGZYp9i=C53PD#G`mIG3uB8(onoLfqtuSfT6@bB%`7{Iu*WL-h2FLb8flO3cqH&M`0A2>b6pYoVqE+zgt7A`Z?H8(%_Jqd~bh zXe(sO&w}Iqle*pWZ1+ih&hUbeAl))n0cAIHm{e5*V;IP>>Q582Z8_ml>$leJx+rOeW2P7;a4 zipZBp$%+$$hD>)3{OS9zf;WE39q+b7x%AtQk_;~wXqNOKpUC=t7O}B;Wx@yZE{+IV zbNT3?D?hFJ?zn!|t#{m13-&7FOkW*Q4yYKhCH1MhHrV=nGulDnI8@Plr9Ny%2Zg)~0~?C4TZDw-gsV--whe zya%REoSEOA=(F_P(((xQ)|Pp$<=6XFZ||P)p3CE=5$fANFI@Lw?X>B3vdEuiwOmjQ z&G4Q23;RybkF27ftYqyUtR8mj2zz2&&CtNjBa+I-+?zVP^3vc$Ls>)d_rqF??ryt2 zV32;2`{6%kZFO97;L{N~L)Yy~s&I7uyH(%tMEh4Rfl~Jy>)f<`M=8U#2LmRua|d3Y z`L9oO6_2u)bWeI%-RJthm$iT4lsRC}_|KO(G&n_fpStFl@6xySzdd%!zN<|qdj6KW zc3|Jh(Szokv~#bRXDs#z4+ze%GRW8JcW-K+61S|$qyH+0l3P1&m_ib~3eNr9x-o2b z$Q`FXW1a;%UU|B5O3QYevCN&^vMUafZSd0uCsd4y!7JF*FI_+So}E7ugLeH)Ri1P=e)Eg*sv>MfbX(5)`W(t6d~G>u;Ad3 zh~Q9dc<`Xz?-ew;J9Gi~@Y3Pxwd>y=yX5WE!5@#e-#%r_jO$sus-{HF3@-j**Jk^Z zUITaWAC>CaNX`5yx?hj&Jyw0*VSD_w9qX;fG}v#S_U56RZrfUqTptqla&GrS0pI1? z3|#*9ke4=B2dtC~4~TztYV*Nadq!0~Ke+n**as`Vn7qiLhbCuuW8_p1c~a=r<e;nm-_>$Yak#oKFURa*~UN5=JvE^f>A9u@*c==!j zxBmPM_u=wqTc3RBUS;?5<++nvKl!p`aZr!ZzyE&qQEfj>Tk4KT*_)x(p^t|*eeZg^ z|Duua-)gxy|HoAe8aC$s+k1OLz^^-e*&o)qw~m@Su%}z8^kv#V+#`oCdg#0%m z{{0)>w)WhRYiif}TsaZ(c2V`6H*PHh#w55*@)okPcId9X%>&Mws zXMy1 z^`AeGT@hUN$SG%I)hG9FF?l&R6VrNLZ#twh^}X`ewwov1Kl76h{#w=IAJ%Q!kgWB0 zXKnMAj~jKc_HN;Zdrt8;s*hIsWR@pN5B6AdWG%leY|`Of16%_dMl{V@95d6tIQ5`S vL)!K`w>+~{ySC|5AKlk(i#yua_V>^&J2i>ViVkTW{21nN_8a{ovEKa`X={i@ diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-arm64.so b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-arm64.so index bcbafedca9dca241be813721b18c69493a3b398c..39168077f877f8a629ed761b98d1069fa7455a1c 100755 GIT binary patch literal 75096 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 iIjHSM{oPA6E@CRAdPx>NNyEJF`}_ZYpZ_P( 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 delta 16387 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 T1&;;vLTfy#?2MVW)#d*HrbK9N diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib index b356acc91bcdc28543e5af9c65447bd8c91e2f9e..f1938f77bdcb1292933b776ecd0b7169689b3817 100755 GIT binary patch literal 74992 zcmeFa3tUvy+6TOcVGu7km{M4&lWA#r0R;t;WQ5Tn2Sve4rVRrOGKvf_Gm6JF5-F#N z6P2gz;)zZ-D^G_yWp*=AQ#@IbS=m*xi-UO5w9Kya{hzhgp38>8I`4UZ-}`;PHtbpJ zxvXbB>sj~9-utZ||NUzZNm8jLNs7Wf0Cz4W=QC93Pjx6L z30MO(-Mt0bY<60!oC1KOl&7PJa8+b5M8nn^dLZt_*Vszn%0eayTn(<%Lm9d70H&T5efp?PR}dGiUszKX+wV7SmbY;-Vt6wW`Qk;WRrf zg-ZRZg=zZ;{RmddkY$r=HW%5;9f1s1%Bx?_xkrdV1Pjpu6Y7Zc7L{2X4oa|6-Zmkx zKm?OvS&CBEAp1F2R&=&s{0biOfG7|$9A2KQe1)yN*la5=v4cse-?7y^WTPlA1S@rt zWs_?*TT9I)m6WyYQp#(%f#c_MMnyZ723*|YmHN#W@}flm zg1axrJiT&bpp;i2#v4V<$+lDh;4%R?X3mHk+L zkVK`tihG1Sp#Z_b@__D2o^82>dX3KdN%u0el*=87V1+!H!d#KwVvExv;L!4%E99LJ zp$P6?o?ld#@-pt@f)YgiAh>&Zg$~g*bSbamelBk_ci3;RJQ0nbO6`;px7lpUo-y5! zm7On04Z>{-g-?j`Lvn&;#Q<)=P4y@kKX#78TIm>D=_)U`R*qd^uUtBIYMI4pvXuDU z62N7alF{WBr){~_F;)SOUS=t>JH{5eY-Pn`%WQ>XmsxD(V=Jv?R*S$K;^jZ8|v@s|AlFP^@;VH|Ke z+)x#rK`#vE`Z7Pu%RYl5F9SN3XyU?aoHwoyH zMB(P+4um5g!sV@WIIYXZnrwxYmdcf5r&8BQ+2y>vunq0aSUo2p zJeN!MmgQ=E52Gsz)u-t4%f}=Nb(H!?RO!)lgz|Hh#!LxLfl`0lr007|(&+B=*H=_b zw>i5?K;~FZ6Z)41=`X4FK<-`L$<4G^TBq7w<*4v5M_2O6K*2mBo`O#Cj8b)D|L)_+ z92=lR?j^XXoMfOJQ+h@Un94O>gK!hQ;Uh`P?XCb5^B>nA7Wl&ge^}rT3;bb$KP>Qv z1^%$W9~St-0)JTGf6)SSjP5VD?Z>L)qMa-jg7x$CbM^UodiQ~B_xIWEUyYs~zT5<3 zZIg4T(Y@2K?hB_O+Y@E)|)L56azZDu#+rhD z+X+m;OxE-=Sul zZ9z&%XN!M576|m)qOokyb^He~2@H<^oc~$;W~2L*$bottChU6mE^a~CJaNCTegtro zF%z@hdyJm+HEUquqJ2hBawL34_YPm&Dl1Syp-k={ef0?lW^%W6L?kx(6JMeok|inA zUzF`W&5WOX#VVH9zPQVnz&+VTzU{OS;$3$oS0ROR!IQk|PmGVn?JJgzZ*qT?MG?o7 zzV4kS_YrReg>xSa)uCQt|5Yq<&0s`sq=L5F7xx4SMUo4$-MbOrLx|7l-s6kA19(`$ zuh+43&L{Nxd}Tnd#og(3kiseSLOR7o`gx{BFG7K>CKSQ$d7-dpS|0^H6gGOM#V*{msiY*6!D?y9fm}+x;G_sNJe5#h%K#ChfX`KmxQ0 zFb7~_ld&d!G7yq>-EsT^(sSrD!fn-<1alD&nT3qR-{fO$4#JX`)j0FJJu`lqkyEWh`&w{y~LUX7JTxxeEF_bB&(3*Fq_B zxMUvg=1{5#7msk8LMbCqo2dXAYj#Cn!y2%irn>YDFqzyw?;TfQpBVOP{nh6))+JwX zCA4uLdJ$Sk-UB}Z=hnoKyt?$haJ)9Kw?7PCh#)+Pm?=cu&qW+(WlM;-os0NzH52g> z9PcG0qJoPcJc;NdMBKzhv~m%N`@OR$d|ljrLe#|wJgSHR&E9id_MagG>fHv%`>!;} z)^pi}C)tg_Q3_K@wkLTqV~=|%ok@I>9QPp-SHi{3Wc2i0INntxPQ}F$p2S&%xF3FD z;v~i%w3J8%2X(1ucZgq62+3t9pc(zF7S zoU`!-V0XaN03-fN9PXUW@UF5jZ==y|*mg=AYdAm=G?9-*wJXc*YSC|1!D`oGtJYYT zvk~oaTnvP^uZ0v8#F`&f+S+^IN9X6=lFC{qutyL(HCUgS85>a3fI?z@ac?pdHKVsy z3Dipr^%_w3I0b4mP{>txA;G6j?!zW`d$zj?`SfrKOgRlv84^*U4tXzwSFhc=8|B+b z4gDfs#-W_|B9KV2+YqFrWK5_2m+YI%THhOVh@qkac*=*lvfrYNxe~_>RlBS?n=%=na}i^>S7xB z;)cs%we$9B$24niqJ0WaW`eu9xjk09b+2|_(@E`R%_p_7!+YgZ6S)tJz~H`imv(Cl zyl=z1uh|<9??HGE;Qkn0`0J#jC;0%j+kA1`7ct;G0IbM71vuM{!T{@&DgR1zVUasH z?|rgakNV5n=@3T&W5pO@p1=vi82uokxPkx?#aKp2Zomc^Q)h@^ z^y}M~5Uefrmo-+CSygbuL{umroFJF`@8xv8w~$CXA!ia~(_k@fCs1rqlYAqL&QOgE zMYZG4*9p`n3RsuEhKis2Q`B~o`!ik@?nwqJM3we2h#0YWFJbN>?*_0?|81<<19L3b zy-1rsi`th0jm~);073WMpxe%AbBKnk>-xgmqqorEQY-XN(!gzKWzEn@Z{ZoAkGdyn ziS9xlqJM=tl2_?xJMCI0?oZt7T{MP`@xb$!od&JpxW2|0K`zp~0J+7?@c1=g-b-@+ z((Ke3YQJz^0CJ{GzJ-%(&ZxB;ntjdsXI1*tH?-?6K}6`p_MwJSwI2iJ$h;Ufk3p$h zMn%DsTn@YX;*5|$w7TTKg8K#15J51a^F~sVJn2Iix8}?OmcWhMz3EK?1QS?*;mCbw(b`bGOp?$u3*W-_9o7E@H)gOJlsg8^#y<%aWK9;?@pB$tD| z6(kxB=VtJeCDBf>a)5^OFAPORBaamzpw==JHJk-Fz`~kWJ5VSsxhSQIhI8q7G@N}A z6~^vH&+1n12_VpN?&_lDJOUD#a1PwSc$M`>Hy-2jnxC>fixVhk=D=Di2V(Ukp9A%M zamTKfbw5l-aPOg(=zSomPJ=6-Rnpx+JT7HOlJh*^qJcw%#QAp+XxegGu)o0!aMBmI z0fg#RNs@CETA{?fHD^@FePi=y5jAHbSh+T84R4D;4G83mE98-mKmxk7|TL8#ZH+l>*%H;lgDbYOBb!en@+J3baMD(Q3S_WcW z9SMO|5F@yMzX%$P5+2$Hj4c?4wBs?7|=lKKmZHf7Ua1PbO^0u5Qn8#~Fe zb6ID|fI`;7fUFmUtn7fSyUK*Djtq2-_ z1p0((5U5Y6OOUI@=ZKQk@*NbE^$8<*9KN`J0LA-+Wvoi0o4B8$sN(-=DNB_v&P@U9 z(wDIO4CxbU$D)>xKveClWT7j#0071B2Bp*dx8|6R&An*=H0UFw2VD&2za*X-@r}?u z+p|XJ-ON3d>Q_ir&+sVlxOeD%2Z#=IIRYCJeTn;lqU=o|eVITsXCKUA0-YCOe)9Jv z@YkV{#0c$n=yWnmJ8Jd8jwt=BDD6H-qeu!631-u>c=x9y)sy@g(5yirux6JoFtZQa zU)^|&5`j@3#&ZASs!xYDXdIcUur+Is03KWI;^sKHqKk#^l;5gi*59Q9^{3Hlm6 zu2x@MSvT>4-{_eGotwRJOvR%)?(Znt=j3RK!~Kr;`_WKGJIYXRNc}d;{h3}n^G$ut z&py32>&-0Hi5&OWS*d4`LE4$E=)Zm1tX94IeZ6~MPVH&!IuBWo*#aZ<54~F;is~X$ z?ybD;jDa?vD_6zBoK|S|p|_x0wYt_Rvo3NHDGA;8h&1d%g{5K7kM3cfWtfROEv4QO!u#u)*GA?E zKmJZZ{Y0bfop2e&MT2kjnIC$;JVt|WS|!%5tEZtxUGfgrw;k~g11FnPB_AYB+(&$I z)1V8QYc>LNrBJ$Qpfj+9$Vfi=>EYn0ZCZo;=5QP((!Hl67U8`cvAh9*!c9P{*p3BL z@UiCZ-!oabAu{|4V65q43GD)?v#!Zi)&43gs>xSyWvFxC1}5q?u7C}*fM(s}LjswK zUSkEVXV$I8TE%YfH6wXbX>Ebf-KYf+dU3}xUUz}|Cr8D0EWl6~@IwrCn&92cy{i#T zExd-re`5(=yN;Ip5XoAg*qUM<;4JsBVK8mtUW~mA?jNx9-~bk8Y)@v5Z@LNG-dQ6k zwa$lO?YAx`BYe-wp!e=0Xhz%`9Z9T9zlc~zfwjGgweIPJ_r|fX+I4o;gdI%99T8l zjaD#dz2;|7`A}mv8JDIVi8BaA(~i4=LUH%S4Q41BeSFLc2T;8kil!ayg@Wr$4ol|; zC?kq#$G4Y2k-HHU*_x&uE8#b}y(sf3LCr5>^pYg~nZHM50aZ zT0S(cqoFm{OE1xmWaDb>$T}K9Ye&wh){b0MuN`S^(2iWzs2%BQ(vDo+YV>F(lcFA~ zH)o7>wFzK`_k^*|5DV99+!l{m5?D-zHtfe>3fJbV_pT z)4PDF8>YVIvYE2a>jo1wuhfE^^rp5WUaF`MFv-b!)E|}sjV6`>+09?~)mML|>T;be z3!-?w-3#-xU@Lhrp7^*ZmhzfkNd?t8p2=@ ztU#cpKJC5#3$_YF^FZDPdv5`TS>ClR%T3k56`$=fK!M}F`uQ5xJYy8wgo%7E6rl8D z?avo?F_eJSF!NnA8JnG+iu*%feJKR6cy@b-A|6)4G48|M^z&>;*{M(c z*tO4Nh}~m2fZB$hIDID-TTjgWAfrOlz*FRQ9hNOoXA$NSt6RLs zhhhqI5DigPQ0{a@2QAODMZIcP{0@2dft{C_p(K_Hk%}dO7WEdh;(^)P*G9Rh@p1*! zM+`;f>IJqe15^u8C`6C5q=ii2c3p%*v=~vfvt}>>BiIsl`X&@( z@43W_c9|9_Nx=eHn^h^sT59TAslQHEkshApDWGCF!fVDLgr^3Y4`hb$B+_!DjLX!a zzM_K~KuT*3C`QR|vs9yLIWvs{=Z!$`>5K)TUM3s@!INpE<(ddA=G8Xw5hq)zNxm25 z32M9q9uylM@o8F8^Q=lBh?<8Dus+7JLl|09%@^FZ89kb11z>bzUBjdK7v#X(g6_WK zy!vb(OuZ@rT08-6$T6k1EzcFGn^_JVCM-3@U!$PH@0o7OPTl8v4K_ou-UbY_39Bki zfH)o%OtwbHy%2vNlZDj(I2G}yx2v&|=*5pwo6~Y*b3Ev}*MUal18FLNu4l;;YldfQ9yu*?cO?-D2{@oKHNS~!bck0u)h_5}ptqmWi38ZGROM?;-EX-UvCdJ7RTo2D5X z4aF`*yclx>20-hm0>A?3igd#|pDP6t7Ym737+STp`=}6llKYdwp4IhOuu`s1`G!wr z0n%5X413!KG5*(CI%B@ggxO+M+Su&aZ1=vviiLX*Qwa;|AE6Z}ry8#{yhHJ@{30rV zazsieW7tX%A%0;Coqq7WP;eqkoJ>tW0a4yrT%9baqu0hp4#N*yHIbK2XhoT+LQU$q z^PvZA-kb!=W4Iq}#>|7LDIh%w5-H_tF{U0gYNs3ot_O?$2qsKiT{AZ>)*b2@BJMq& z+2053>`5;qtzNp2j1=?U6smXJJj^yFY&nM-j^tZ-gp_S=LpA0_x6=wDNNnE*@<}i9 zqy2$t21W411H}@Ae3iWrwg|#l8}$?Vwe>irgfO%oasq6apb?}U)s$X`Quo5G_(7wi zuZ!%96=v!w4Rw?H>TCA&fuM!VLMY{@5QY~iJzLpaT@8f3`S=i{hxM*msvl~a`@H?b z_aBJ8Jp`T%PH4@}$e^Ws7YS?NdNP2zzL#NA6tnd}U^7soNue|!!bpU*trTK2xE|$( zr%)JQVDXT6Kby$x!{s)@BJ@nQUp#b0=Ti+Mz$`-g^9aFn_Yqp8q}H2aK!-5M1Vl*K z;BYtl!z~LA*Mx8n69W44NGMX44_woUOeif%^N3o|Dqg`7oP2;5HS6XyybuNJbM6~~ z%AqAaXv_k57S&Tbj3nR142cy@O8-g#u#soZ{S4MToOXK`Ects{%k(5ykpOBlXc6K9F5SHYOAFLj@CAj=`+0Q!4b#G36F;impQW7v zaW_yiN zkozjL_J=r$MPn>}evU0~gwB`fe2~#Slg(X@h%l_Px5lCdivrjCNm(Fbn>jm7!kwJg}`Bs*dI*ISbZcs)<1ykm;}_LepC{L zb?;=Oj(XS`vS&fIcSk(=lV3putS*1(i|Ywij1aIMrd_w0u@1TtWL{EzaYwHND-C$o zdKi($N>vy=`J5&Q$|w7}4`ctA*gS$rE5*+c0u5O0un0lflc9qIsl7i#Xs!c;$%Ew@ zpX=6+aZs?8vKr$(o+n5@6PbfvMXw_~*O|pOClFgKo7uWrUW~%IA)I@xIrFP?G{w6` zB}sb>yC_PN$J#>ceUl-k5H>>Ed~y9C0~MrWv`||XgvnQ~jsz@)0LrReBDkv=bD?Yx zt#>D1b^%E_g7|WPfG>+R5s7>CzWM?U(&08>!m_%-PlRk9&G-%uGdxzEA-QN2T()?!+#Yz~D3){0NP3|9j^%aOkplOM# zejC!6?c1G``km_uNJZVI%&3T`)1dWdKqRDzRsrI2&IeQ%GlLw=y?pg_KJuR`WK03xGPpP}D)cmMgdp0EIcvb5ssP4>Gh4Ks9q9s`kri%7-~m2>^=!04Stp zE@1uhs`fotke@^68U9=AL1RfNw7kD-J&2yP#!%o?8aaGDsE>AQQ(#GG8r1fmugLZ4 zD1BP|_{Xck^LZ3EO$rm00&{JLxv>Aw;N-mkpjs15fc3Nx9??=7BW44uh5*` zdpGe@WxsJ3$qKR@ja=NHdVkzcEfiLjT(Q7-I{-pH+kwNP3ZV{i6k8ARxO9YijicD2 zh{r|SOWtQVipG18Me{@N0}MrjPge@Tw{dtq-~#zzIxYD?-I~>{lJ?IFSy@h>$2K$u zouTzW>WVH+ArFn*(kHQ^1zdlQ+h%evtIu*|mCOUFI_`Uh6*D=;M*d~q{~2YGfessmEz zLY0`);)|O|Xc{pP2FqG;mjJ<2koZx1HnT9JLPT$nvOKFBvpm*kO`c`-Bo38W1oq6? z?2GeGAaOa{aPoF9jPf*r?%F1_UexDcVY9Xceq)`+P93|umY=`X^kfyL_8@3k>mQ+| z?kx8!DR6L7q>Tc2cqwt$dO5@jh`aU#hgd;y*PdYz%7D98Vu~9uwylkZ*JH?V*G^&{ zU^0m4u8U>xe0OaY!5%}dagSjmv_t~fYC2fr86~|JZOFK5bqr!l@PI}TRA<--ZvrDE zXHtQvtE1G4Rksh4N!p@~Z14IlwlI>+)=}Mud~q*>h&*RiD_Mg|jC^K;1j_I%_gCmr zgU1N1bQ0&!^(2I?S!($DAXb0eN9eHx&uDhKEik?m?OxqF&~Z*jY)9XYKG z45Dgx{=|~wy$AqxqPKyfSU+Ly9Zna!f2w|UJ>kclY&Os(CX%qkT8zF3BEp>NUDjz^Ev10E+lsF^t=#}f0b@#$>S)NtbXSvS|Q|V9dQDY3i zf@*QZ%9A_*;j~-dX4!Ud9NN9ssHDpFQ zFKlX$sQ!#t?+y}jr_32Gb4EJ{Hnpoc=OPG|qq7lb-u2(_pyvXJ`9_FUIR`c$jfTu? zAhYJMip4(J&xwXe=8PatTHm`GGG)#Pu>sCl;-rO(@j;w-`#E(1P91S#)SPTXrpbCP^K)hdI5V8*HXp@cI{EG(nVH~3 z#y<;lfHDUQTE4ik!0}pG3z=+CbkY|$ka1#>5oKk8$3ydQlP7b27V=Hs+#av5VR==9 zWneZ&^P~&lkD?J55y^r@~k{^a-*-$6O zA|T3xW)D0xuJ%l?FuGOp+<&M4!3Q>UtbrY=fvSWc67ETE;j(;jk5PEmK*n%gG5!UL zXU%M64Kvz}3`Gs3Syr^10@kI^L}^ttkV{Y+WkuRGq&o(UzW_-pEQK5Nyi=I<`SIWi zU7x>*V-je6p0C(zIR;~SM+(RM(u0M{049TDIylA%jFDqL;Fw%saye!XVQO~I2WCDJ z%aCm4|0xiZ<6C+Cf4*4%2MgAna>e>Thm0I@aVUdBi#e3Sp{qG0b`hp?NbDk{b0}mN zVI*+U92m>}j{7@39;2*DUxeD^b6w$yA@s@!*YSS7Z@zT(HJqyJ&Ku3-~3gyUi3v>27sNMQ6M(9Jg%&oV_pP+($`LNE0%3egxo+OwYAhnd1C5W=+qCix0%3meXJ*DEikF= z+S_O}O(na1JUpp!b|GpwAn2S65M|_QJoS}$+WopWgLMAD6PLl{K^yIDKVg{MZUejb zArxWQ|NhI!!D2A%@0tk7-_UrzE*i|fV2c~5~KOIx0plV~CvRixchBy150 z+oh-vBEQkCXs!?uAcG44?n7v5*L!BH!*b@wG$E-;9}FJ0{G^E^Znx&q5U#DnRhRs6 z4>)^hQO4(8O4?%$aNWH`^TdPvW)RsVlUIREjeTA6>p|qvAXDRBm%Kj+dmh8SW^$h~ z_4I1jW$;*AgE+n;6vfJxOfjdfOMW|u{GLqKuvmA2Ozowh|f^+!0=2E z!(D;_15PkBafXM27+it@Lsc+5z#0A$#E>r-bc|sQV@O{^40Xwy;dmz#0|hQ58cYCv z;uBA^eR6LS5k1!ri}xnLmxx&kRZ4KLA5PqiGGFk-de7maoY!mzQI>@fUp~TkOc3$` z1)_4Lt!)Db zA4EWd1BwUsiS`~PdCXF8GN0y(O9{aYQQ*>|p(zboqVambL^~Az{gT{7pLYb1+TQrV zm5aJ)M?Hp48<-Q*0Ov(I73jq%N%9SkKm@+wk?c2${YJCjUhKCI`|ZnqW7+RH>~{eB zJ(vB)v)}XC?}hAlF#FZ9-=XYx82i11{f=P2m$Bbb>~{?NO<=#{*l!a1oxpxm*l!y9 zoy2~xWWO2gcMAK>V!u<_uMxkdx$@;yPmE#zw;-!AgqMZOQncPsf$k#8;eVqjl(e6n;T z`N|1rAYU>0t|i|B@~tIbF8OXJ-&FEFPCja>OP?p-IP&c#-w5&@Am3o}9VXuZ@*O8% zFY^6DK3Yd9?TzZyej1Hk>G|Y4M!wPHJ50VD^3lpoX*v1cB;U>C+eN;=lJ7cwckO$k>pzspLp42ny$oRE3+2soOWHA-M-XSp<}N=)m=K= zaj6uZsl;Y2D`reOdx?(ToP4$r^oDACS3w-E^08$rEGwL2P4njA&B^u^xmLVtJAh{I z*{m9yId!(xiC1h|i)ULMc2{MQRa#EN@+wku90&k z>8cr1XS>Qxc6&vZtr&tUoxw6B{k(iZ&aoF;rKw1a#hE5uCRr-*0^sFVX~Z?$1Z5CE zHjlk;{EDkaF5poIA@O=un?p|Z%*wfzN}HwJDI>%SH1$ps((IJ%4)b#UN=^h1c3pYvYbjnJn)iJ}KX|GrbT5hEsuU>bqG~q4D0Uip`C2?w{ zecAjRlc4jo2ZCjCgkN};t%zQVi8qN(vpVNm%3M|mg*P81Iy(z~&fg_Q$O6-B{?gY@ zFX5!OYUWuhm)nZ)`dI#!P+`+C74(v3sZv7fid{ucftH3Bk755(=Cn+EnY~iVrvj8` zS#Hg>lv~S&j+!xQuDRG|DYGw@=FBz;GRxqhqh^oFlq|)?ne>k3O4JmmBMl5J$O>Co znW5Zit(*#j=M`02t>rTdQGAi15|uqN`^>^6$SMSxHEK31DZyn7kvSysa$5-+6dS5+ z&Mt&Gtj;O+%3^CJQ$I~IFIm1!0y4+yaNt$a`FPnska&ALqNKW>Z*!Jer&ubDR@>rI zCvnaTuPQ>KC{W$avQTv4|^G;?4maQ)g8{QIl?>#kF4!>E0a-wV7p(<9|t5$Z_YKqJ0w3q8qLFU|TN!fNLB`~H!9sP%ItM6tIL|^a^SlPLRawSxeAxttSca*qQYvaw3MUA2zV)Y zF=&;Qc&`Qf4b%ZgS`prti-Kx#p^XxiyU>nISf(#8Dn+#{r1$AMB+3KU?&q;L*8^ci zdsc*w$XYpHu(JMCD#B}_d7Y4osA5xOSypNpWk=9gN^XI2V1ti_P|oX~zQ>_#e} zd2muMmFBb)isZm9QjyC6Q}7B54Hfy0yx}CU|3*f8Id79XhX^S=L6VEDg|5X}R!32# zt%6!`sn}X#ag{kUne71uQFwD1zNVtSSkb;wmgHKTrD-+?Z(i(`E7PPBTcyJ}1+Vlj z!_Y%y>0+xhvlJx?9a|-eXsN|fidMAPS~asI52c0rJ1WPlWo18^qOPW;F>orYp!yAaHD?Wg6==B~S@sp>0RpcW(14n7q|){$E43WKN|s@68gbR=SCNhW<;gD5%HWrfzt00K$UJF_XBX%aP= zJbLEk8Ln(q!nUhrmX+vuP$h~{V}()6tmTW*w$WSEnL6xqCgm=!0&5l|UfcaTgn(A6 z?>y7fB#ao!S%)cAP`8htm)vQWy6v$hJ--wc zv^Whz%oQjJ+{9@sEH>wya;L2<3!_|w;+xL~@gYfz;!ypv}ssdd$jnf^-D%Pb^+0L|-+siRFF3UmFOrOO87q!~6t?)Qdeko7% z7^E$?Iajh$5Y!03QW%Si4iIBXeL22YL9GD|Rj5-m1xl&hOY$)eWMjfmjK<5KVg#Z7 zS~!`kp{sP2V}xxflN|C044o=fa45ls+`*8)Tyi8S8;mNU{y4PGcfw@Z97SbhpfpaR zv~X`3TH-wC$};3WiNW}J1uvB}QiXA4q$3B7psk_|<4)2u5FP;>Go7Uv0iq}e1@iZe zOvV%pC>B>z=O)H)kUPctC&=O|@$rNJkawA-cBm`YDVxy5PoTKCa)e?%PZzvgRimxA z*jjF=LZd~m?Puy>0FBQx%tkZe;$pIRE6*F6Omw!4EerZBM&rf-*4ztkm|?1*(ijr$ zFKY>q(5yKjLA@JV?~eytcBo~@GHvoW^|*)!P{33u>>(MO%kcV;5!n01dQ0I zYdr=8e#V4a6L_KcR`B)MuAFF+U8N=RoUxDqjxLAQz<= zlMRR7-;v8DbFR%{LxaMin}-pP!$dt12Wj3BGTaCZsUe*jD?JSs!HijZv8A#&IQIxk zpY>vGT3~FzJ2fJr>XPDE*Fd|>$ArvFjU8q%i?Iw4m`?fUG|UztSBOrM=5-i|$bBaD zP*k=s634ti@N_>53YSPRm=_N9m|1A9i!nUKSg(>=VGMMsimhN1sWjeaqRK4JvKP59 zFJKljEQcqp1VxE;2e~NY)4vTJgwaV@H3J-f5UQQyw!? z|CHrIab&HAtln=CL7DYiLxz-$E(3DAcV{!=$hAArQ)9qeYDHyWQx-PDmhDPBusKF; z=wrmPz?4eVJu90tJg+sX&nm8V>sHOmT%UK{V@g zNx_+HT#d@lEs8HZYEC6E! z7cKwrNS+fYLUA<2mY}K!#hPVZhH8d2V1XMJX7n{)Va-)Xgc_sw2hAfeUFoP1B z9l_L)(xD)96N&~yLEK^*9ZpzIm&xPH8Gaj4G8dPJW)jCKGO3e2=2XxGK~@LtH^#)7 zG#Uv-EwH0H_o)Z$(MX@FN57_#nj;79)JXN3 zfz2A}aShIn`R*I88Yp3debkE0D)mFEizR8dYT;`U(%&LL#Tl*DI62gZ_!3n^FT5?{ zok-2CYQx)+>O<-x_;*BNf90FVg>4$?4GqFl0Blc4l8J>$R*jT^y}0+=5$Z7+l2kt8 ztqAqsRrjghh)}oCrquC>g~!#>>uMa}<8cjO;YO+kjoGStE=qk;^>pO(QR;`HaC+w5 zsD&@}ke=>wzRc^z{+I8*bX9_+lO$d59g*sZ+NMZ#?kf?yBGr$m2W*K{A5#z96sfL_ zwBUXtKy|0k~G9z zqf)Oyf*w>YY>1G4@pDPiLdK_8CDGT2M)p1(p-wvI+X(g5Jx@lcS45qNP;XF;JQJaQ zHR7DFBh+t481UUC^}?Uk(n+;HIZ`~6VO7medobcml=`cPKBuG9cd2`R6Q%iwx+nba zsxO7VA#z52w5C=Q^;(qX4o&+0D9s7Y1S9Em~#?(MO#I$Ap2 zgLcl5FDMZFYV5`HbNJAxohtR%m}Zr_s^4oWwOe)R9+kRLh2yVZsumuIkeVYzmf}SSVD%6jollqo;*sDmiFEgtzLfaYtiakRsDBHs~=GHZ;n?_gg zcU7@3MXQgg^lwC~+ag|6y&bK7EHW9uyHVG?4pfgBZ$+!0iq@kjL@#`|r}TQyfIXsF zoaw5>$u~xB-Z}e%tIB92&vhYKwq%vfDI|jZWC=KkZ%xHc})S81m^#r95AdX5`JpN`ZfQJs8gx z7Qr9{iW!Dv!>&$3T8gxMTmrcOr3^nRiBJF~n}l5;8J)z?X#mG1WsDzJl!Uz@_=t5f zc$be$A%79*lgA}UMS=v%#X^A zBeQh4xppk85o-{~>SQ@pXJe$~{2peJ-%#|dI!us@9KxJU4VWOH2Iu#u3t5nqBF*yW z#4P9>$i!lT{8mEHV#o?IMR4L|qcVMD07YId*T@xKrC7-l5e6e6K3KMhHkczQX3qOt zvPmJJ+aQoCTk3ZPqpA`#CO}DMMLSr^Z_U%-FeFx3?YHG~S9XOAnq{%tC2}kv7Bv|F zo4$bo77i{Npaf=eLxT!RaCj;*B!f+oyG*!Tpn^lOqScj^0;0mCg--)0HFD7mmP^6? z998o%bF!luz_h@%9QcIae3FSqJ{UlIp%cu zW@KlVFSjqXVp#)gw3Z^Pc{N&OG|x-;8h85Y5aUW`4GfMzSe7k0F1C)9XLSzc&857e zIh2m`)98tP=bZ*|1gGGp!N%M3HM0n%!v955gbJ{#U++j&X*Gsx10{)$*DS;>e@C6* z;OhhgS34xB6+^Era1$<)qyumZhT;39PPjPqlL5C4Ly|JM^en~}a+45-@EFLPf!mrP zNt0YSk%&>(!*J=W<3YFyCX7>;^KaAk!S^+rBn&v9Z!Eq{nxK|ct!gQ%F$&+3j0U#1 zq{_hfEF%VRKS^_bB6NmYU2OT`5kj9e(GnxKE{U@5X* zC#l zc9OODmzVHR@hiBSdm(qtOS!8P_yU19DEKP`ezk(XTHs$=#^qct zNskruXA2lv$`IkHx=f# zf?u}JMFJ+dq+hu5vw0ntBmXwk%0BKIZ{jYM9}Ewqr>q>FBGR9c#$l;|yK=tMmw)Ig zTg=_71b>sj-(A4**9!azf%gf$Wjy+Sc5M=JmI!{Gz=x|JJ4OBQ3jMj1@V~O1$X3CZ zEGHP(ouBN~ANL_)r+_z4FpK0TG*?uzrPLb|g75oPT{%!^Sgup+dz~dAs zyS6Iur-Yu{75E5I@0Iv!p}#U+hf2Bpy$U&}1YVi0xB||9K*1j+EA!!efqzfIPaox=OR49l0{@+Ye~_pLN_}1s^3{Lh^5oy)qCb@F zEaR)f$T<~8&Q!s#wDWSo-%k;5naD3?ypIm~_1^+JAZcqMOF?&nxDA2?DPJ zS%Z7Ty`1kQG0wVE=%EIh(wQLW#sbdwR|P#q&>s}xn-ug6LBCM+b8jfJ{F+TjCjweYjOCL{`|5q*#t)d*rl)#1 z5bocF`<8I0hkS@|W7j(2b_ly{ z67UPceN(tz;mY%|{$d=g6Rx~2Ag>2pEAYj_trYG`;jR_#CXwEo1^gG`J}lhr!d)!J z(eivwa5o6|3E|4^i@d(nCh*?~ zS0mQJ&Jpfl;f@n-hH$3~S6*k6*U#=1_$|WC5&CZv@aw|;Lb%6;`=fCCigmJ)!o5Qgt-`%dxNC*mDBJ_WZ4>Sap^v;C*H?^(FBI-L;m#E908xL71zaxNEkaJU zfa`>Nw{Ra7?mvVpuXD-kTPFpc=dL8l>!>oUbmjF{CH}t!%j>t%LSP@^QoBnR^`ms@ z*e`F3$s3M7Lhzlj6 zWC@#VWFL2bRg7C=*N4*E+@Y{^3x^8?+9+TJmmmB}ULOp{=kc?KcnSoxF$`U1?jhD; zTEg(Pg~7}6Tk2&=f4F~G;13J@U$sELi#DnXhE2)K4F0Q*j<1NGn^loeFj$`_|4mtt zIa^;5T@hVyPUh@s6$!KTzu|An>?s8o>MLS{S?4O?VA_A%Uy!7qoduM8wr0Nsnn>0EEXOv}*#d+59VyU_{I20qv53$z|fS4J1co+olfWXth z^v)9YxTsW3Z`EO!imBru5?=JN)V$b*=Xa#)*m}L$j3Z07GD{(zK_Uf%-v%PpXB4k2 zD>gfvu0nGmPT}GKUxbOvKy> zQgz!nD5fO&pT?A`Q#zB`GnrC#eQ-`GB+z+gW&x=>x3d8D*cE?x5(;!9#(yMQs*dk0 zhMr+$&qtuBbt5ESyq6=iXekt?hurz)kP!^V{Ht>ftit=>3nWz^4A#+X&Ya4gO~mts{Ma;mqJB0# zwPmH3P*gW+nxe_9R27+m31%})dB&g6^T-$rtd({;rrLc@1s68)@`LK8E{Yb{1gZus zL4D748&$A;=F};8jU_=Aj{B1O$IS2|h5ZH(DmRAn(2(xXXeGMz=G#O@7HSlec{6xvZE zK{?ThQgodQubLs+86_=9-|7gm3fexo&7`+#QK0n^1J2TFO4rp>U_1ejm)TH|jTD4T zMCzK+P+QF9q$0ZDSCfD)45MTvT&3EF|GXSh@?Z$N<9`KZ7Dqs?~C2 z=)nS6wLXBB-@;`^$5o04B1v4mpNuDq^>~>^5uX3EiWkNZ!$v=Y=p2zp2v4L2KM7Cw zb`^HB8s@09QW+%vMgkpr-JS{0*Ih&s9#yfY|Ta={CNU7=1 zDhjX^yg|>fNH+0Gr7Zq>6T1{a4=K}2jwBd4j}ZK}4&jD$8-Lqv5{qpCWhF+x`#*`&_RsEAF7)m7k$pn6qoLv*7m z_C!Q%?1`QYy}*UP*!qar6Y8cYEIPFJkfgzgGhrZ)HxdgM1UCwJw15-N=k$3pUBGK( z`URZ+h)fso$AJ4nUhaiLeoqb;2slB&6#||o;A#P{6>z) z69n8U;Q0b>6Yx5~l-?5pep=A!oHboNdlS9de@=6_faUX`S%4{hbPk)YO#)81jKgOH z9D6y3FT#Ql$&=4tc77#_ZD9EYa^CjKUIK=dI2pP0z$ zrv<-!t~0(b4=<&0dOBbVFQ3<1AYl2N*7X9G&u6_VVEJ6uR|1yLW5sB>Joy}!LBR4p zQI&w@{h>_)PRQW;>=kg6fR78fRlq|~X32ghvN->I0XG;pTmhKUBkvF0FJO6J=tTj` z`#~Seu!)BsjSVxBFQ03@R>1Q4);kz1N!7DC|7O4xpOnYp{em7V;I9Q;m(S_F&msA! zZvsvMOyPBNIejjpS4#y~bNDtvuMqG~f};?>gVWy=Fg+VUSG$1e83DR}7BD?4K$o^Z z4^Phw&^1)R^y~m#$pWTl2ly*mGX-=l6EHnnK-Ucdre_T3x?RBZtN~pQ z3z(icpzC?SgZoxX+iv9WApv^@Ja_=7$FJq|1p+n-xK_Yb0{**zHw*Y90Us3bz&Ob3 zS1o0T_0lu}8wI>Xz_|jx6L4SXB(JYND`5G2hpMf@;c`b0n6*0(**3jh1+kr%*7ZQEbk-qM8DJ*Hq5w>>l+Vv zFkqvAQy7f)O2Bynmd^vM7O;GNochhauvx|ZT;5>;R|)u%L7ZMK;Bo=47w|R#*9-V- z0dJJU?1l%Uz zzX;eX;Kv1gLcp&J_>6!L3ixLMe<@&Tlf?9z)AL$%{Y}91To+wm37DSuqH8P^017<> zzp^>3k>OkgT%v%h72)qv(4XNjMveS>K|$ZIfaxX5(8nMDP<1GL6^H%u<|^oA3ixIY z`_s2cLI0AS&*+%K!&7{X*D_=4e$$u`2XGkQBO}^dAVqVp8zB(|;x*bg}C9%Lub7 z6@KNKZzW0G3EEL&yGfm}{(UA;y4ZvOHtgOLXtXvCr|az}5cpym-fI^6e!<|SdkW9l z&bIXw${2p*34Hz?giiY=YzwLrHn98DiNs!$t;F*EC?z&z=OqjguOX0>q+oN1oeU+* zO#W7$PH6w8my!~^;iaSn_P9Dx*voD@DHz!HQU;KBx|Dd@J5u7} zc3R{umf*b=;qSbu(lvp0Q#d7f8--&6`zHeKv~j}m)Nv|zOgKe8M-T#ujT4UZX8_wC z>T3T)-Z+tWOgfznK+#Gq!3IPoUJ~xeorqVU7|NHMbEg*-N+2aBKs~m_oShXQPBbqm zV(in4@{-J)F%gV8?AtPD<4mFrn|!&5i`QVKwy9+7#v3O4#0(GlR=B|4-dWM9Vrb%#Y#2p^0aco#4q75G`x z6ubeMp)UW_8`3+WpPp$fQz0#Y*mx19|B$fG9G&-39aSAIu;Fcu5Euo0$vgb0%gRdlL}JrTuktskID; zf&|T)Vje%vPBgqx-C1c5Oa5(plR?8^ze=G4&rUl1uLw4eBb%GXCBf+2Fp7ennrk#_%E!Y7c8X2=a?>I~}&J{mIQ`b%YEf_Vx5&Q^BTpZ+6V zx*lf#7fJyp5g}86P9nnpqb@j5@n~;9 z~k4uoxiNXnr43$NSsKLrr`96)q+ok(C6h0x&DSZ zw^L=n(4l-W8^)Ra$A`k0Y6UwPfj)x2@|wLlIb4$X*SNw_(XR3X8mwRW4;qKi+4t|P zHpvt06O!x`$JtX7>?yyo7IhKE=9rl$PJ757gQJ5lIH6?YM?tgw-=EP39T3f=uf$O^ z6!^xRQc9qLo+aSfK2C>7y4Qi{z6D3qxQ1m`3{lzPE(g+5i9=R&?uNZY9)orqNnK&D zR49KI9me1zO~qpprD>U&W-A+Xo7pS(5)(S3y5JhunJc+7YCdXYc8WUn+Qk2CY}irJcMUY??woKQxEp%Y(Hir_<2Z?=w0GUGM1 zU{>M%jKNC+LsP;8Pw+?cyTc}r501KfEd$YX%a@6TR+8Q=$Z{l%r%>`^YXh8!VlDkRfCgd9YTi$ zva*WQbj=hLPuC2?0y)ts?fxeM{O0lJS|{F_x%pVGf$Bi{`~Kh?cK-?s!V};(k*|W!gRwHT%x;Dl{E?3h%uywj|uui>53f2K>fl75Fq6X?6#_`dS*wM9_4-w|8l1o`9BJTLx(h`Sc?J9J3Kx5WR0 zcmu%}OicXUD@%&yGzUNWZx~-x<`Aa;?+g7e>X{<1W&PKc4UxRSlsMjRDf)6qV70N( zFn+0ZB}ES&M=oCu!ZxoJ_R9>O0OP}r?0KD;cwC2lIucJmnx~c73+cm_OY8x_RvrNG z!BRZLL~xhEgJ;BI-ppszbWY;OPy00CCF4)SE1yLg20u=#MQor0EpX3(hu{h+B9ahv zHR7I+8+ZvP-#=>5MMyZI|68tVKU0~lu!KMAGJ2V%$nF?jl`_FRAxZjY!c`w0&N$Y` zeC?_HPu@Q^{o+p}1~?y$`ty+qImXln(#45UznD&*PhYXmeK0d>_Lr|X`i&dsxPAXi z4?q3hhtD)V@kWemF-u8YMW$#C(Za%{RcNZK6w6& z1rL1xkJH;ezUlSKm$P5`c=9E6A5}bg)j40CTD?KKWy4Fy3!X{-;fFu#C-xk;D=Ph- zVPEUM%bwVC{>u@TbEItGGBT1oTGNh`q>eh(&-@NPmH?>ul^-137SUEK*XSM3pAF3ak@bHO$Jzk|+ zUG(CMZ(93Lc;w)Z$qg^*rat-9mZvhOpWeJZ<&NR`*G=zJ{_lVM({cA>AK1p4_8dPI zwdA{lmtS5xC$8qHcOSSj|EoDW){Tlx8hQKGr#AK3y|DSXPk(UV-InGHC%yU9##_G` zwb+(*xc=o|w@-Du z_p~2v{N&q1_V2&nviR83J#R{?_fEH%@4r3i($_U{2S;zdZ_B1Xoj3lQ=e$F_F^=Vy zzA<&HcJ%mq=xZ;3?RY!py>iW>yq+SYMu)8+5>ukPJ)_{8jO+tqzP$p84{;pT77 zweCo)f9TGk8;)*!{&N?~8HV*J@QcSc{rG8b$hrK5gzBiCm)&H%>H8Q}kBElqgz5;DDp8vd6Vsis3H)CLI^o34MMC+?48gvNuRi_II@pWt zx=g*!WLsv#GiiwzB@7{&x=*}buh(VLC-F+~;ajIwmuIb{4}m)p2A>y`kd%;!KXxrR zFJ@dy;<)knPfkrvUGV>|a%)vXgN+6As@JNtQo>qQuZXqyKE(EnP2ZM(c69m87v>nU z{=9u^OHI_K=F5JKAM)ofB7Qve4u9yLb86`PQ=fkP*72H~{&wqznGckn+*m(#-KBe$ zzxw&UJ3 zZ3jQyzjNLFG1q#>?u}668yOGX*pzVND+z;;(}Vg(o*Ox!p)&g6B?3k8dvIcR-}y` zTU1$wWkxDmMHcdmMOg|eN^(gW1HJ@=YZ!M^LZ^^p%B{}s!iCldWU@+%p8IpeiTk|1 zhktx;)$Xk7H)rm8?X)z1-=kY!9*|ucx!n8J*NcutXbaw}%Z;CT$qd!!Uo}4R^{X#C z^YyR(^7A>{_RL)Ht-C;TL&5W!t4^K%X2+|$UU1&Db@b|``}Uuj`pMYcZA*6UOj3_p zeb*-ocJzI$*FDV|&y#(|o?4jpR@uZkJ#YRrFQaV!@t?OoX)TXDeZj|*+7`s^{_6+d zTy0Fa$f_A1`hDTltRn%btEn``1@HN{`Gv`P9kJFHcG9dE;|SYv12K`rnh!fBu=G!`~H- zT6Lxt4_ZIhi8_o@`7k&N@ zG$P{?6OzXzhiOFShPH6=3FlI?)T_TX4-anJ7Tf&|*64)GiK6a5Os3?z3d?LoI{NaB zE)UOlA~2$fWKWWzZ_BZoQ1_|CmJ;*MLkJ~g3s z%d`U*-+uX(AFn=~*f7h!W!>$c&%Wik!*_k!dUMe*_btBT=iGV4>X)|O^2#fk8!o+X zsr8*l`q-X+IXm^%X>qUSsJJ7@lT z?wvaWGnvWSdzGD+wVvmB-*;st+h1Eg-?h-<0p@|+A=1S`(K8=v#3aXWF`0a0Jt7~B z=Q+8lXx$l)u-kBYAXZf8>pc`}uw>OxAOtOZVL$U8o|`-5+&i^1z*eYK`)GkI?NxtT z(&DQT)_hXW2RyKhp6YLbA0eHKv%!1`E?0LR)3k4QP)KA7>x*1>*Kp7jr}DxV)kg|f z360jGa-k0NeXbj=L`zOlNt9*9g9?HZ1@xt_2q7LeBTmyuZ*vzUzY zEw_PiU<&Vn(kn6wsn<4nMv)s;0BQr>@R6k~#NAGz=%uY}*`$qV@D~gzY*hC)0tkgqG$G3GMm0n;*eP!`! zw~aopDgMHB9p8_yzP#Dmc !K?ReDhR#$L5Nf27#mNv`C!M{(m-UD=2au9`qah)F zDYbgxMfW<1=Y~H%lTvf6_;j~#M@0ZmN&fYM`T$5OWf|>Ot26x&+HyO`Ah+sb)omKZ zZ5XLXBfBwG0>gEjX~yp7;h)E^&6$1q@Esq6AxA*QAm8Oe77i#l`vU(OBY#1t$ZTx# zj`&S2IIoh53ryn11-h`wKOi*xA0d;veT$2O6Sgl6SP$n*74lfz0jt13Kh^&DfkQj9vVu|de%YYlzw z%uCkKPDnhA!adorb#~JcXJI=rM>rva)QFE2-N%rlpSD&+JYy34>hit7+^0cWhM|Z7 zkvW=)936Q_Y0TS0VbZYoVT`t;xmr^B9THq#bypc^>mzln{ou&9#3{RXMRIv2x0{&H zm-H>e-P^1DS9?QlAHHdsT%t}j5HO^<`R;|`ok@aI)3>iI$ylCvGRH~5_x5aH=!E)3 z<31pmfDwdT+#ir@$>Kz62uYK@?n48MVq+%x!(_mJ3e`YK2BN8`h{#Vg6_o&sKPB18 ze-%vy0KV^N%Kl%WDX13>!2yCHnfpKBcdZqJ)mqV@)(XN&5x8x%A+C7MoIut7S^G-B zYNL@6ePdgfqyN@f!UQ@Wl<1+uM{zVX996xQ8gWZw`wj9=>(TEtZrtzWObYX1nEEP# zhdf0Nu?!riZ)Lg0PiOCEk&qEq8J4~J_3Qf={oOnM=4wPLL$%_B z0fTJm=V$NupJDiRLq5w$)*;p=|6AtZ=9OcPIhxI@8Q{-~UC0>l*NlEBbtS)^b;n#| z&0p!`u*Us!=<|D6W(EF7%zwkOzXYpaSVqC$0*m0SnnP=q2%OB}E1=7MP5KX5b_5GV zyK;~SAOPhcNS?sI4`;`}!`q|1fN>{Ei6?_9qtA%D!r@8pi<_8-)aWp&tKg7u* zs6S**BJh#a)eSF&8NGE^7NRfMRSt(7wR`k_YKONW3Ob{+iKuk~lVJI8wrF;Rx0nbiajy0|V&i8-k< zN$2Z@etaIC^!XF_Wz+}ji>aZ)Wb)j`a?pk+ouZXK^*$DilL3;=8R=- zD`EdjzJTF@N)J_+u`0>@BYoU zV#zi$uEoTq4XWD@-u4`uZgB<-46OX7>l|iIP+!7@vKUmr@6o@tC~JrrQHznNekbX0 zxb?K4_(r{*&h^>OzN5zxyQnjC$*gr1e|AG#pbk3Pw=t6$ z{m>~y=czU|ucCCYn}6kT?qd7F&%J4V0(~f#^@NOKTT;CMcsFl<=_7~0h<;{}9l*5* zZT-Ox)~X+Pilx|$zgl;>Y>gT)|1o8N{|riFxdCJX#4m0T5eJJuxxsWFH>j!aMkhc$ zO+-qdV&Q{wb4DBmn6Z>VLQ10n?lMzDdBTj)*6v;wZYY?hlcm<)eY}5i|8Eud2L`VX zfMdh0I&%67Rx0Ll2@=1>fZ~A#^o)79auJ@Uu6?xe-Qe@t7QXW87a`%LV;@=tsk@!E z?}_+NY$)EZAYVWGjPrT8-dy*FGucueZ$mk6%b>8#_4D6)yr&B}$I zBz$_>8NauLcPgWEM~=A`q-@NocCpcx>!x;_goGO_+`eVdYHqRDv>!SCh=v0%Y=sdEiwth zuU;CaE55_WW96l(coj`Xk*oWrypybu>EpI+|MK~0$rasay0KzR_-AFh*6Px1IxZ|lDI85C?p?AGqZ}hTn zKwK-5Lj>Mxz0D^4b=m|a`uJL+nVQmjO8$imBQMmMA%-*gIw&h$WvPoUA{(g?Y3Yww zp5bfeOUNwWqbm>HjATp2J?+^(5?YsK@N|`=9j^*4lCDU!8CPnDc=6adAN-kiY(yd$ zlC!HPu+gVI#r>Bw^oylXFD~U)$YivM<|9_W;oB#(vZLbWH*{@#zH8R`l)&1A!0hRtgrLtWIJE8-cw7` zR~lN=v9f1goiY;5D+$nho82CbXX)bUDVO>PDd#md%JQZ`H#sx_BXu{g+4N9_{**|_ zT$+mrnd+;ZpxBMXyN?VX+n!S^{cM4JI+f7WmQ!{?u#OZJ+0OP+ZiBMViFgjKHZ7HO zD%&ct!X$|4L*8gF-{85XrcfHa@ljJ{BE8;zf_CMN7WTyW!9)^A$&fd;sRokCRi^B@ zKs3>%>$;N$8Yv`YI(6-VX?_wFI_bvmNmoA_zsijCHSjSTmwOwnZSIDov*5U4`|V<@ zm~`7IHDS!R+DtN{dJgONhN{B8ud7Y+Q}|j{{fQW$tCx&6dnvka-qIuyoy!V?BU45fMzI1O(H_ zVI#%1{}~#=rTi=Q+84!64~D!21&7c+mp;{YUT(u^hNX)B^hkbM~CwNgHczqzl3TFg%U)z<8f^ zkkD2+W{j4{)`S13CSWWosYh^Esji_* z{WGku6sewu>KzaNk~{D6hGO9cWddE)Eg{uLMkD@ffy=#hF^$P*-fhW`xyth_2H3Rp zy*t*MFsjc>u5y4^a>tP30xsumhsc}MfsR(KyZOT zs|S;GB#9d3U9vaHtgZPL6YHPdGDiZmNA1?63DaYqKB->W%-IeU{k}el?JGp!K@zV{_Bolv&88|QqQ6baEBX}PpcwJ<@#IoyIW5eU^pb~q9>fGZR;z)xkOU)p0fR7FAewHhmMEp<@ZFgX9D5xya19uAqpQg+8h)kCx9K<5s6dKbE^WHvLd> z=-t@G!DD{x{d%85dh;Wgv%9!@#~$Hp#xVr7d)<`l7}8I;jQ~)e@ckJzwooEgC-Iur#b$eI@1 z;SvT39n6OD>@IECNjoPnsb=BivM)1chjxZ3dw~1;-N4g^5!hH7*y0c=%l8()(jLFp zqXG_N>yeO>e6M%R87aI^h@UdTrrE#`A|fIQfXW>qhE21H17hGKVT+>1fXY7Q{p-1~ zUNOoIW~iqxg82I}{w`aVr>9}MO<#vp`&sEZ zT3fitW2i$G6l<#ANpZuMYnAbp#mLX&%i3GVzdG++<{BLpU|{d*g0unh|Pa)x1t05iOn zjwl#7wZShJxLLul-#h^uI9tJxXlE-scW~}vU}$TY2L>hh+dwX8uq}2lu;$P4u`}HT zJpbAqOc!JObVoU%oZT_PyEz;dE-+T~TfmzL5XIVw6)Fhu1H9?n>73zvbcszQ?wVki zF5UN5->yFXcpY%ZS>Reh+xl%h z*83dkVFuj{@goin{<8d0)hW-22XGFzeKgQ``_MLq9}exJk{Er1D^^Z0uOsl}3Js^1 zZP861*%z;FmvOjqoGEt}n!Lqqqex!PUGpe$b86lRct74+A#;c;Be6kkmYV;L+;EIc z{qgzEo=dkTm))N<@voLvKR&TU;O0`TwZs`A@+sZ`vPJCmY%<&3aAGHr#KY|dL#If- z%?eZU+raw)gJ$y5D11ZHfQ!=o=fjQ)5&$MB06}5{S^iY{Ck$HthD$$Dg=#-??ZF<^-@qO%K#hexJSwv9u(z9+Q&q--9T=bx1Hi}x zAR@L4c9Q#w|I1MPcQD!IMW3;jL}_!OQ#h?EhQmf-OJ$7Mk0iQ^ehZ@nfBLe|pq{63 zyGqO@os3HEDUn`s99mj(APu!rj{I`H@tBhdtV{YEarGCOLJ&6h(GrNPd-|9Uac4L;Renk=#4-Y8`u3H5*k*^Fnmm@Wt~xS<@X^LH^r2 zlUI&S$das6=D3MI7JR8~>_#GO;6BXj>|^Gin@8P#`oU?(yVIXumaNafuF*H}x{nd* zMsch7n68?StDnu-JaNo4R$KSc;#*9`0QBOx;hmN&29qL(M8~wTId>s7h4&T;gzElc z9^8paDIRH!!2I!AubA&J8I%JAW$oKk@Ry+Y51R<~!>GU;JI#1c;_WXESA~^w)--hk zh)j1G7z76%tTUd@md+Z^vd^Xd1rBat9C#NOc0u>&wuNwjl7M7arXZ~daEYPdNnd}= z?_DU6=v*0bH6m={=<`lNJz4HPKV22T#LkuF=W&q}h_{HOe=h7gBIYS@fFaUT;jU zJy&q6tQwgTE1*d)xnPq=T9g7R#&HTjMQpmH3(}Q4tb*~lbV4;O*_?8ygs0QNrb$#C z-IRDVJvl)4aNMZf&fIbJWxZy0@-w~cgdf<;Bc@Zk)LWbtKU}NY(&)a>R9c8H<0Q$O zm8XG1pUSX}oH0~r`to*iLos`TJFr5=q36Yib&;S(kxZ@8YXn&g6AxNskH350N`C)d zVn+xF1&={Rl=U47Ah`Q&gxzrTKZF5*1Ry3NE`ksPL?uAMBMRytK>A1Vf8-hdZlzD$ z$R}pKF?dwdU>1Mg)F)JERwDC^&Z)V}3{fNN#~oJ)3)unODSCyk2E<(f%6W;(x)KkmgI8t$F$UTUtrcmX{Q|ZPa!hO$=@;Cgj}#W+_Wtc-5z3bWO-U zTZwbf9H%+Q@*sX~H8ri>pt|m?flUliIc%+^NDzj0d8h~hu2ibFQ{0wc7C6Ss*tp$R z@rb3k5k&?4dF?T4DEYIkG`Z+QKZ?SWTl-b@$-P558`WyS$#v(oy$k z)p0PmsT}pv-SQA+h;oEzbWj;^cFq_oa>e#R49f@#vtwh5dUCAK^NuJrD#I(jNoY?H z`GSJR^LLK@s~tkWIQlP__TR4kU9y80lMGa z>|KyRTEPEQQH=jLRn$Hu11`;7__`pGq$A{AIe#T(XxS@e$+Oc7(63_+3khRNlGl`6 z%1=?cCiu4O`jNhxFPM~tPAiHQAAPaDOoest9fwB3IX~%;%SNy+sh4H7$@(M?I|5th za`A~T(J7rieAKz%uc|-o};>}#${S-Y8 zZ!!aG-~Bh@xzTYMNt;7@dZ)E!w18 zw=fmz-`LYp=k98KKH-FCNRW2R|88KUu`*12+J3{nz`T)`bh!GRPx-s>=r_Sl*D_FL zIE*_sYYRP?k*O`s(?Q zOS|iPrD2Mvq?0*(=IOeZ?}DeTYeAz4lHnBfddO1Sm3|{ z2NpQ6z<~u0EO20f0}C8j;J^Y07C5lLfdvjMaA1K03mjPBzyb#rIIzHh1r99m-)n*Y E2aL)MEC2ui delta 17657 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 H$>V Date: Fri, 13 Jun 2025 14:33:54 +0200 Subject: [PATCH 31/34] GitHub Actions: natives.yml: - disabled cross-compile for arm64 architecture on x86_64 Linux - use `apt-get` instead of `apt` - use long command line options for `codesign` --- .github/workflows/natives.yml | 38 +++++++++---------- .../flatlaf-natives-linux/README.md | 10 +++-- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/.github/workflows/natives.yml b/.github/workflows/natives.yml index 0b856b3a..37c99b3e 100644 --- a/.github/workflows/natives.yml +++ b/.github/workflows/natives.yml @@ -33,26 +33,26 @@ jobs: - uses: gradle/actions/wrapper-validation@v4 - - name: install libxt-dev and libgtk-3-dev + - name: install libxt-dev and libgtk-3-dev (Linux) if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' - run: sudo apt install libxt-dev libgtk-3-dev + run: sudo apt-get install libxt-dev libgtk-3-dev - - name: Download libgtk-3.so for arm64 - if: matrix.os == 'ubuntu-latest' - working-directory: flatlaf-natives/flatlaf-natives-linux/lib/aarch64 - run: | - pwd - ls -l /usr/lib/x86_64-linux-gnu/libgtk* - wget --no-verbose https://ports.ubuntu.com/pool/main/g/gtk%2b3.0/libgtk-3-0_3.24.18-1ubuntu1_arm64.deb - ls -l - ar -x libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz - tar -xvf data.tar.xz --wildcards --to-stdout "./usr/lib/aarch64-linux-gnu/libgtk-3.so.0.*" > libgtk-3.so - rm libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz - ls -l +# - name: Download libgtk-3.so for arm64 (Linux) +# if: matrix.os == 'ubuntu-latest' +# working-directory: flatlaf-natives/flatlaf-natives-linux/lib/aarch64 +# run: | +# pwd +# ls -l /usr/lib/x86_64-linux-gnu/libgtk* +# wget --no-verbose https://ports.ubuntu.com/pool/main/g/gtk%2b3.0/libgtk-3-0_3.24.18-1ubuntu1_arm64.deb +# ls -l +# ar -x libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz +# tar -xvf data.tar.xz --wildcards --to-stdout "./usr/lib/aarch64-linux-gnu/libgtk-3.so.0.*" > libgtk-3.so +# rm libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz +# ls -l - - name: install g++-aarch64-linux-gnu - if: matrix.os == 'ubuntu-latest' - run: sudo apt install g++-aarch64-linux-gnu +# - name: install g++-aarch64-linux-gnu (Linux) +# if: matrix.os == 'ubuntu-latest' +# run: sudo apt-get install g++-aarch64-linux-gnu - name: Setup Java 11 uses: actions/setup-java@v4 @@ -98,9 +98,9 @@ jobs: security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security list-keychains -d user -s $KEYCHAIN_PATH # sign code - codesign -s "$CERT_IDENTITY" -fv --timestamp \ + codesign --sign "$CERT_IDENTITY" --force --verbose --timestamp \ flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-*.dylib - codesign -d --verbose=4 flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-*.dylib + codesign --display --verbose=4 flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-*.dylib # cleanup security delete-keychain $KEYCHAIN_PATH diff --git a/flatlaf-natives/flatlaf-natives-linux/README.md b/flatlaf-natives/flatlaf-natives-linux/README.md index 3d4c2051..0054eefa 100644 --- a/flatlaf-natives/flatlaf-natives-linux/README.md +++ b/flatlaf-natives/flatlaf-natives-linux/README.md @@ -33,14 +33,16 @@ To build the library on Linux, some packages needs to be installed: ### Ubuntu ~~~ -sudo apt update -sudo apt install build-essential libxt-dev libgtk-3-dev +sudo apt-get update +sudo apt-get install build-essential libxt-dev libgtk-3-dev ~~~ -Only on x86_64 Linux for cross-compiling for arm64 architecture: +#### Cross-compile for arm64 architecture on x86_64 Linux + +Only needed on x86_64 Linux if you want cross-compile for arm64 architecture: ~~~ -sudo apt install g++-aarch64-linux-gnu +sudo apt-get install g++-aarch64-linux-gnu ~~~ Download `libgtk-3.so` for arm64 architecture: From 0f2712510705cbeae71dbf17bdc5f0c23b10ea14 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 17 Jun 2025 11:32:20 +0200 Subject: [PATCH 32/34] GitHub Actions: natives.yml: - fixed build issue on ubuntu arm64 - disabled signing on macOS (because it no longer works and I have no idea how to fix it) --- .github/workflows/natives.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/natives.yml b/.github/workflows/natives.yml index 37c99b3e..3817c17f 100644 --- a/.github/workflows/natives.yml +++ b/.github/workflows/natives.yml @@ -33,6 +33,10 @@ jobs: - uses: gradle/actions/wrapper-validation@v4 + - name: apt update (Linux) + if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' + run: sudo apt-get update + - name: install libxt-dev and libgtk-3-dev (Linux) if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' run: sudo apt-get install libxt-dev libgtk-3-dev @@ -76,7 +80,7 @@ jobs: folder: 'flatlaf-core/src/main/resources/com/formdev/flatlaf/natives' - name: Sign macOS natives - if: matrix.os == 'macos-latest' + if: matrix.os == 'DISABLED--macos-latest' env: CERT_BASE64: ${{ secrets.CODE_SIGN_CERT_BASE64 }} CERT_PASSWORD: ${{ secrets.CODE_SIGN_CERT_PASSWORD }} @@ -95,10 +99,12 @@ jobs: security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH # import certificate to keychain security import $CERTIFICATE_PATH -P "$CERT_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + # set partition list (required for codesign) security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + # add keychain to keychain search list security list-keychains -d user -s $KEYCHAIN_PATH # sign code - codesign --sign "$CERT_IDENTITY" --force --verbose --timestamp \ + codesign --sign "$CERT_IDENTITY" --force --verbose=4 --timestamp \ flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-*.dylib codesign --display --verbose=4 flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-*.dylib # cleanup From 5c2d8ba5550c202593322d97ba70ded6eec696c0 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 17 Jun 2025 11:51:33 +0200 Subject: [PATCH 33/34] System File Chooser: fix crash on macOS 15.x --- .../natives/libflatlaf-macos-arm64.dylib | Bin 104032 -> 103920 bytes .../natives/libflatlaf-macos-x86_64.dylib | Bin 74992 -> 75328 bytes .../src/main/objcpp/MacFileChooser.mm | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-arm64.dylib b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-arm64.dylib index 1f6585b9d055605d57d560fb81db1ae50089ee03..5dc29eb0c8c26404c9f5aa7e63003b33a5a949e8 100755 GIT binary patch delta 28808 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 diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib index f1938f77bdcb1292933b776ecd0b7169689b3817..88c4614261435ddaf98d6ba4d1b394cfa7d033d5 100755 GIT binary patch delta 28519 zcmb__34Baf`~RIuCaai)42cLMgxHfXBnSx!6N1=x5`-WjB0+1J$f6n3RO-?y+D2`m zTS`l+)sCe?Esdr2+FDFRZEdZs`G22t&Lz^<`~Uxbpa1Q9=iKM}Jm-0y^PK(M8}HqN zj@HADh55D0Ye?)$isET`jki~It!3!PsWUv04E^4|*|nBOe@@O-@zkTg+)357R$e?O ziyU~G7cVaX95|ShW4(DAI9NWX>Z;34=A@61CR#1OUBgc{s`a{}X`CCx6NzXC#!b-F zkk_ldYaI|02}0t)H2Ft$*IGd-oNn&S)1VZ&o%H;k*AR>*OD|!4pG-$=H}9N@!z91RHnD{H=jP)LOQhUN+`tixLHAJx=E5o z%br<4HB04=ZeKV`U#2+Ax7=C*>m~cS8=N&=w2*8qf9_sip5<+b3UF!tS_?doiwS9wqY(%$mBi9NF8D{nXg3+iyAs8-e^7)Yd!@R zVsfotXQ5W2O7Efq;`D}N)H!@i>+rV@(H*GYZgY~x}5q67noY~H;E6@M3 z9%B8s1195tbRZvrk1BOR75!%iT&g-y^MB{S5Hc-)RWDrE!GaP@IBc{SmM^AY@3HF! z&6FMd45%H7(*&h{3~}JcnIGD94l|L(ToyaQ{v9<_+w0vZq}GfuC~3=upeNS6->y4@ z*1&57wH0lCVlVX3kd?hqWSd0!%x5aqFV#~a^KDp^i7EyT%ww%Xb19F6AEGJPbvdLJ z1`}WdHos)7GP`aX@T!ho?aJlIv3DT^ql#mZ|IM+Zl+Wv5p2@xHmzw-gFGlkT?RHJ$ zHzg_8s?Dtf1Y{FI(kMy=ig<#^O4{68_yzShr!EqO`h;k!JsR%c;q(TS*h_yH|E_M4{ zYeqacIWKdRdwN`R^0Ho2$4kEE7a`B3_kcFZjNRf$aih7|*=9(^|&bX$xxTd9C zQz2>c5t=4*O>dGWuIN$c=Pu3RioS-TkmEKZX(`ZsM@^x?<69)~+-NrKk3qNpgqw)q zCc2V|?OahsC^ymRg^71wm}o*KEQV`lQmR|bb-#s#ln&!cA2DXcVM!jI##MHl`eU$6Ff6tvi*Q` zQk4-?^vR)7^4W&qT*ZrA-VO`fT*Ygz;w=&kTPotS2H;P<8xM16WPAR1M5iy5>J&}apk#h@Pm z4N#y2Kxj7arBbd3aptRW=89Oe6;XXH93`#~Q!yc!XD-?7prf?g_ZwyBMz!1?fTET; zFSh1bRq{QU!bD(ee_S&8j4^43jQtDX7oH7)7wtgqZPiag=B;ZNPQibs?CjEDu*P??w zph-hhSd{rEtXN{rf7|kzGR&vwi}!6k9@*{Gfv7u{sL;}P;YXrrMf%PiKOx6M4%)tJ z1BQ7YWLCtSj?u>4iOR97DMiu7FAxDp#G*AO?>Fs=Js}54-3qIIPu3GvW)Or-F^W_d zNP(a8$U(b&w|#fi4pmh6k<8i^VX&AlNNsKlP#Em{WA#Oy?Y03VR#PEHOT-^pc(xe+ zo=^3EdlHjt2Dz`vWeBtRdY`QfS>nu>k>Mj`FkyH(L9*t)MOf`hd+<<HNMYjpmpGFgF|KFd7w!4mAh5Di$L%jf4na&J&e3?>*4u zZCjY;EkRGYXqYnfLIyrg>433usy&0Kb+{=|BL>kpWuGUcY63!o>4kQKPEvK8YStd( z)Q|Xy3BfdT$@VE8F;11C5rvJ4QX0`-ShcP929I2;h4gt#krSHwnoPkldG?GUh|_D- zFD-^;ux!`mCMo_d0gWg}Wq$fPwMM%(ml~E`x1KVfQRb=H3Q6-PQ{aiQZxx>6%p2n6cbe5Q zna@pby%zG*hKz%HYOH&BIqOKijV^hR1rp91>jkO8pVl;!8 zwYLFN)I$V>qJF?6F(3K=OP4#>m7(aGhynP~_~)+o70Rv&2+Bq(%8n8Cl4IX9DMOH= z47xr66mnby!Wj%q`Qzn9&6}EfhA1tx<6J5-df8aYPfJ%rD?q3fyRHu)-WMAwP#A-# z-_=o|K+0GUF_r@R@~GT37@h12ekxciFz>aU1cB~W!`2-SqA$s9TKJjv&SAOjx;=?_ zpz(Ai)q!Q9-d4;Xs9pXHDYUu|AYO8I7=l z))82+z=~~+ttULVAFXY*;7p}clzgB?4d1!Yj!l!I*L;!GSqxEgpwz@FAKCSU8>ccs=Vu8F7C%drWBi<$6Mqj|S2 z3i4E>B#h1iziDeFzy+l-svAbjm@H%7d2Q}+VTsk)O`9`>;HuG*S#H;P{3lBhq!Q9b z%Mdub+ja`G8hq?){);STGK<7EpRj!c)_rZOn9`W=Kcmg}qO@^`<>Xe)lk+GZtaMl! zp0lMf4G+1^+2dYWIt+u{WH*-KK~;O_Y%TtSF?*{H4mtbg%AH%+3t+{~E2eFNxw3EG z18rVA3_6wDU#zy}8LjK%bm`63KApaA1uw*@1uX^WtO92xrXaQ+ux$jJ#$-mZ<;Iwb zWh_gy+@?{z#!WJ)i!~h!DQdy*QF$$+Xid8n98|yiSlO?Qq0M|yee-_H@M6n|2Vz?M zZVYP;gu+(^EgmB;Y!jf)9V36#rbFXs95C8-Jt4^~l)yq9tpF{pkAs7*(Q=IZv`s71 z`=F>DV?k&MTnPdt|UTDD64Wi&y*6Ld!`Tz_N`24_QQI_4yB-~|&d0f&lBQ>ex8 z5vV9Tj&BSlkV6+^?lXi9fm?yK3h_ z_Oa4*XKm8Zn~h*MY!#4XD_nyWDiJEX?#?LI(b=q-8Ab&N!l0PQ*eC}E*&~q1vo1so zIlT*Xl)0?Z6Zvh$PwXZ;D9YFY-7th_Wc_Oe&)5(d8J)*6?!)21It&`Q*35Yo7g&87 z+_^V2%qKvI2Ar;lMh(Dh!098vE*Nm`;OquXzJvxGJ<4<6MBVcMwD#j#n^wF5tx?~i zP1=Eman!RiBPQfG&_RsOn1sjH>`|L5b1$4g;=D$?+zKLdIA+sN4^g;eabWo|E-RYAB>C-T2~>#IT;m0x}D6fupwd;POdp#SGY_ zsqNbl1kpZs$rNC00lArBG?AGU=p6>p0y0B^asa`LZ&9Vx`m1bo*EEAC-N@fUj4jY^ z3k5+A6oI2{m?d8b9iY~ZklVKF}`45G6C251tC$J1eylL~)U5wih8;Tw(oSK(DnQTX-v5xe|1@W^)gFKxlt z>Ukkp6qQ@@lJ*Un^#S#ws7^Lf)Sp;UBRWuTv3N`#CjZpFK|_j}xi2Q0nDVIYDmYe$ zid4|IgnC}RVeC-jRG?DcvJ?_WW#1mk!UFkk0kN{3XAqV53k5pBAS&)g1^NmQihCP& ztJU?>r``9`uGBA7c-UA1a?RG&USInunCc zn*XuggB%;4Fbl>Uuuc1oHJ@m662?Rtdu$_supniY${`*6nw_T>U@@$RLc8wh5LROB z6&H37IgfF61z>H%Xi1XOI@Alk|0_*&_#c#G1!#e^eQ#9wh0Md27|*hpljvLMl@WcI zN{{(NxSYoHSa42}%zp+_ggsi8M{r;RB`p3Tg0^q-YMdHPhjJWSVnGNg?b4L^&*uEwJACj`41JdWg# z2QVd6)-8;+BZ%{8i5mbMTH?Yl&yE*R-Klch@ZRo~IJIFd|D%C&Zg?;6m3q`i!9pyd zX9cli58GnBd?~!HDHRMX!_j)GE_Z_Ya%U)*G@XjD>$YA@>q zIkym)h>JKVw7sb(JszP0De=OS3wv_K!piVq0M(5XN^>h2>@r7m4j=^{25KDGIa}?z zC6K|aa|l(A`O0(5-scWu2`{Fi*f|Mb`Pl=H$5WKcYz|NW z>6To&{&Q&@O~ue@4PK?e{CY@Ny(mibK1^QO+0SGK(`cazFE^SO6dKLLTwwUnWC~8} z!NfG5jly1AYd$PC5^_=(uZe4^R zXp7}8bet@o>(WH+J9$}z@35hhsP+DAv>z~rpD}8?pMmE4z)&(Q47ur>g0>7PWPt7p z0LWeCIKv|R$-*{&R=Sz}<+O-;wTFNYCWQ+dF|72~PmqyGzb>|;K7F*v*vY~5`+HI0Nu4^N&J4vK;K|a{`wVs;faQW@7x}ak~pNIc22;xPD zlP(r9b4&>=ev>30?b^`49&ika@Hvf?(XRDp_=Fzr|3;GR*3Hj!Gfa^Pt14lI#4rM2 zAEP7pki~|cp|3lbq*+mU0QLQVho>#&D;QGrwG$c~mu|wOF(xtxEMp4As@!`hV_?%u zI-5--A1_&ktYNg~^Eh%qGIowB&+LhHOFr~fIUR#<(4#R|k|sRIlLBhsw5yOO$h9LQ`rjH)tiU6Es01g`O5(yF#c8`g!j_-9 zMj++ndv!LR`;+Gdx`q8n6RP24t~FL0eF@SS^bF1uc2dZ3W|!u z89*ATqF$bDVN9Bp=3<_*XT(9#Rf?GTs$CaLViv)qd|?FSS%+)$0@Q+47O95{sltl^ zns*@)XTgrjp7~~FC_L=IN@;k5>3m|>J&2`@eK9&k>0xUQaKxFJJ0B}I?;a4a1>1|V zn0=%*&XRNhsxrDkvNVDDZ`ZvWE2nmEsG1@dboWyokvDg5*?9&U&wZvTrZ~p@7d!Fv z_k|_2ss%;J+?r*+Z$ANBI43B!pI|*;zb#Jo>M_{mKAILb6@SR7Jw8&Ml0Bmvt3Hw2 zM+cb})S)=if`2+~m1&i-q8B|-1x^6ORw>zl_-bId0?l9$ty20b&}hn75V3*+@bW4p zQ-@mgfvJj?VwK_!0!=|@dkrj@wf%(@r}PzN*dxf7L;I>P^_08Cbn3XSC-`DRP(!=j z`eILT%bU%}xfDk>C)xG@v3)^spV!HhN>q+gh1495} z@*XT)h#>FcJWP(QchaLp^9@JJqZVC zk*hWDb`(oiDF@hb7n;Itdcdv`3te`Q#7@TKgr3bz-Qhg1j&`&+CI?5t6t9jTwztBf zu@@j-9sUZ`o0eijbMhoIRU!ck}>mf)Fs!X|KxPQ?M?g!!Q@oel3?sU=+`y$1?RT#WxQJ;ckB-&3RVztWx-jw*h$v@Qfv4s+w`iIaSs} zJ`(%BI)0R#(mT);#rCuXluK-KxKAf~);u^jrg4tC6!g=f(UYI3S(U*70&qNQJ{Mw5@$e?OftN?5$KnYG{gBzNi(s&! z4T{O4hIzOJg9c%4#0Jd-$tMUgn0B2P=RDuKRVa){T}kU8d=$s`bY~RCCCk~7K_ z#(vI-R2bV8#;2TNBt|QvrG3QjR7i{A0|>Ak1UBYO#IT%aVQYuEfU>@!C6=VkeMf%1 zZv)dru(jKdF~=jpjw-(sm34#8LI#3^BZ$+C6$qB4$NXZ)OxFiFS;+60;b*m}n~-1# zQ-Pxel>MIwF6W}ixn%fQZ4OM%tBTyuBsXaO!zO#^>mw3g1f{AB-`zK9?R5u%&bg{$ zDnE{pyY;JI^GFqFQG`6DU;VI;t3bnq!aelyq@vG-^x?BZgSm=i>j-%lGEc4oIXus- zf00>FnLUs>ri$d*F0xnu`eBW$K%aMEO-4fLAiNn{9J_8cFy7`>kVMbGv723&*F{e1 z-^BeiYxCIT5A7l^@88t)8EEZx9=`yltWSb5?_h>E+DbB1zN5%%7}M|;?=x(ayx*Bw zZ8=D^y{n*c*v)q47wI|YZya2%L~PNj|3OCR8;S&9x$v9=hL~Qo=?%cJByA7bEfBH| zcT>o)%T#0AdZ|93URt5(O$_5Dh3B6zEIJSP=0$ z#q8xp)e*E1nqBGJ(~4x6q%kD{X$=f2wxi(E?w{V-m{x+QcB;XVV4QOV^K*_2j$pMk zD~u!KIT8sZk|XgPF#<7iq$@|_fy8s96(KqMhXWaoauR5QpJT^V)CQ;OG+Ff^WjH@N zdEksK&STz!2-ZLmg=p9I z%n*wQJ(QvkOW}`Ajlv%lm}FAkl(Fw!Do$b66-PB z$!~#U$oWd0MUzc*xygP0-EfcB@vMCx4OkN-{Ok-rGuvvn@8wHLYKD8wNATe^qGHQ| zSwsXciOm%jlg7X_FWgjVwe^LMFTQvVu~!^K)SrJWGK3-$H6&{AAQ6>sI=&Q@r-RAY z`Iga5hFema#KZf0Krjug!>_qZlm8GP$LMf`)Fo>770=z*5W63uUGZA!kO-dn`uC^~ zyo{lBiN&KYGl%yN^x8PjUoIc4L-Y{NpUgi;ng5KzTLskk4o528ofz}oX!CXR&1m!E zDD&AWKJ{Zh<&D8ghJxGyZu&9DGc4?(L>2zfv}k^m#oz@v{FHX-Oo+}83HQ)|M*FHr z1RX`w`c$JSC*l&uMSEtF#e*J7RMFuFwM!!*vA{XRTG0^?7LSAAgjZ;nHYY_D;r_co zlOkWxqB+h#z=zfO&5NCcA=W5gD_F`rYCLv)Wv+EVTEs2TUOMxM)iiSLeU4xn32lnhL z>`BB%BgD!Gp3~pJKp?S-K#UNeKQz$hmT`fBT)@Q2-9$)WIR=T1Tq3%P3I`#eX99Ul zAfh!16c~mfvE3%mDf1NKAqa5DC*%P;aJT(ROw0TxKDJAEqA$H@6oPbqD{m5KEN3!- zCr{gM*?qX4f5t?^fHv<2Y7)6D7>bOR!N`guU>s7Bxx-+gCP?Rg`pl2cJG6MM8D&3R z>4yIQ1^75bL$$=AOLO;^0fXOfV3ZEA@02iPKp_s^FcW96GoJm}Yk0A30JfEPSVriO zw+=U$LTU6b(9NK!x4}753AQT^BxBUW7weI-Ag?aj3f0}FxbbB7#?I8~;^pL93RUlB& zpYgZi>u2z3)%Y_13(n8x`7cB3%lyrppU(4JU*^ZdfN4GTkOIR&mU#S(WTydUY)Oy> ze*jkyzzt-*tN{EF&R27O0{Hg8YcceDgZ);p-%9rTJNv!Ee($m0KVndRd*EM;c*uSq zv)`xe_aF9aXTPeRBM z(uRy^#C{vI-$3@;jQuuezb)|_SKz-KC#q3{?1A1P+cHPGxND-)(-U9YHY@3MRkMjx zCJstUNt%!`__eg8PEuOp^rVzd6Q|5dOG$jKUt(%f&l%~&*>-JGeRn?)gOU@|r%azb zFllCH(yWZ0=`&J?O_@G%#++@BXXdFeil~C5Gw57r)LvIK(pm?vOB(5>LyKQD(h0{p z=QUEXrVb|S?=&3{meqkOU#S)TglSH(O0!PYSd#XuM*pCeK2oJa)pfPzinrv)94+}(x=Y#xE<93Zzdb1GDvz!(z{=B)O6JT;%L&u?^9oK)GT*sgChtB?}LsS zv!m}$N6kl$iP$7*MptO0OPc3O%Iokb)RF2-QvD|Jnn+}uKKgf+^rPyzSFl(tXpu#e1{tEIBQBBT;FzfKIY(x z=Z6l>@N9MLSK{KlLF07I$$6_L;=Yq}p;JrKLnqBeC(TVKO}4Yf?Cku8Gm5d(d32GB z^o~mlC3a{Ybe+^u=?$u;0Zu1WnzkOtRGNI1*Uu`=d#YxqRT?W|EB&P!jcN3lTGX#< z0rIV3`DUsnN@xIWJOAf z?4miX^8C?7b6XX4!9{ady;F7BMf0_z0mKcbQRe_T_r2(%+2w+^^2}xQZ?4ig*B4O< zVn!oWA>9_K*Sl)&s53rr)hu$%C*obF&s86}YK}O6RbN%)s>ybZMCH4V{@6|Wz|EgZ zN-f#uy=}?tD=PQMcA=5Mox&nRIz-CJb5lYC`VAVCK|Lzq1tVa{VCr)Lq3xyF|3hX8+E+{1OffoARxNC3N>n+3i7$4uB#HhEmnN)< zxRpP?CA#WW9T|aB6tkDPK7E-Sn@Ot6D1p8eQVdB6Z6s9|T2)p% z$<-PrIi!WdK)B?V&`EMn>m=1k>nwR>b(U&IMo8MMNXfY{Qt~W`l)TC#rCN6*rP`iR zl5=pBKkV^^U_Zw!#%R@IU9 z*)FxJ=K!ly_P=vL(W`QRwF)^3TXP$iMGE*wf-zju6^S{OF2GN+8>sOI==E|MxNgDP zi>?s4QBGag3xWz|R0%#cpevXtb-+OUeQ+H)ILF1c@j%XKz{I)>swSfb2+EnL@DYMS zA7bSVb11ioP$#})ZPf$ITla3`0JW5t@>)hLqAoSk*)RGk>n&j}ZKL zk>3I07@wpp!M`c=SLf%Oguotbf$6F)Kn|0qN)e z{nhYI!qKDE@a?a_1BY|hsuyg}7>=K1dPHL<=#*eoFWF8}b-z_J{IQ5ob@^X}p5Lp< zFHGi!|Eh|U{bQo5R?pBybouI4FtJ+K=4$yr$3TN$%q#fRz^Ml-_KHw2R({3MV$tRl z1976{io%ZtUkRz=m=Xb{?v=V$s#d8=rRtQ_IaT7ViPy!8!Ac3WqF9NH5;}!e9H^hl zW1ytU6i_PPTT+|OZ=@ebafvoUl2hojqIq_yhR+xB{i@+t1U|YN-caaCt%iq;t~Q=4 zhD(H^dDSwkeuZJPko!QSJ4AX|q>5o>T)!u9ujyKD^5vKq0IDW)j#8b9YYzop-L;DX zZ#G?#EMl^!8Y6^O+qjNY!wnNSUcK`E5gnxd49-`6*9bk; z9i>0nqpS0bD&3xdJz}h=?$8=xIJTOBAH^7@1eu5>rw1i3d7YyJwjv{ zR?Tq#N!(C%!$U;LlB>yU1zx=wn=XjZRcDMAgIDzuToi_@B>`C8%Cv`4#f&r2ibDO)tqS@rP`*XrTbm z3~YXdJpDa0UDXQ~{x3bk{=1^Pd?M0)BE2ipo@rd+0+Fs2spCwJHxa3lZ=#^*i*%Jp zw^qkZ0w@#d{c3`B)4758YN?`lroapTm#OK0oALiszV6chR1s}I=z6hhdu>^>T35x| zmh*57_%_zK4dn7=E~aNT*krTw|RO-&_uBwo+WdJe}A4f6DhshpsTY;V?^3t zq$5N+L8Pf7og>mkBDIKgg-AaXDPGPn`_eW6>=J32NRNs1yhyK!^o~d$i_{^WJL)0Q zx*}~P(v~7Mh_stXW8=AfX^;TMh;*_@XNq*5NOfX8&{Qlx_lU;YLG%_~0(WGMSg`CB z#}4$BDP5Ds@pM4~Ps>c=VTx!}lE|q1D}=v@*7}#&MV%JiZ>cbtJ(4TxAPnRS+$5Kdxb)OkvfX>7m-00C2AxT z2Md0zNNWgOCFDOB^0L5{Nlck_V@C)9W%?`-ij;FD01{1ohC6yzS(q|0BD|AVIcSBQo{5o&>pYNRIGaIxHGmC=c$_)*h6bCWExEYhBLTP46 zkT+cIo-%_xossq$VWmNGn~i>IYmnS$V}0KPRV32!dOmSh5($+D$ypn9v1fv6Bmc-j z8Q~d8>8VquCuStgCiZ2PYh2>2SxM;`c|@-k7W&poj<_Y#fdIdm70JY%=7Go&B!=Q?tH zv2$ov3q`hCeDa?&(8^wft+0h`F7|g^39fu$cfID&SM*#`v`I9>^rQ@OtoS)=mi$F= z9j}rWa5Oz>4nCS7c4-UQdvgc>`zrJ)2dUf_Kh|FOx!VsvCBgV9Y9>$K91sEKY+??= zBE096l%yWXGZ4UZX-eOuS+f!+Cq++5OhN0KjwN~qe=jTd2%u*@;6{$R*Us+C<)ps>|eUW3i^JP`JoV+E# zWOXS~d1g6ydStXU)Ac^5O_II=2ObFl`Ywd7MhG=2jBmv0NI@40dOT=SXchGLg2teN7V-vNu3TVh8}OY=GwqTG{x%FKEy9+<}9FRyMkK1g&gv8=|UMm4tEm z;esv-=QLHli3}F7c_m2LRT+A(`PAkjTAI}ousjiUqAKsgDbqPG!ixBwm_gazOT2(8_^WKF+REzTO`PT(a^;{z4J2E;;Qa)iBC}`zSbiSbHn$~aw9|EX}dO0KL zD}uf(=s?Vp#J34LRnT_@y+hEC1pT|9rT4l1z=k}(i=Zb7+TwWd^hH7I1+CL_ z1wn#N7Id(ncL_R7k#EH1BLzJ}&_+Qo=d?+R7eKKP7%u1^1)U)1(}JET=-&jLEa*Q4 zohE2ktOF=wa|NwNAhaq>_gE*;)eW~+Wy09sn9))`12FwI;|NT`aQ+Ns*H-}c)& z9hp?Kuk_?%4J(e@N>Bg?+;la;jfzmOpt?c&7&1i9l+=y{(DeoG;kbd*hn*KL>(x>X zoEJ2eYhBQ_!IpK(?4(((Qxhl5m?f`$Lg*MHi0&1{#y^Vdy!-C!u0Fy!-wriz7Yx-PyS zHr4aG=BK6atonNN@ZKZW-+0!()mMqPwE@XnYP1Z9Y5ZlFXa2+%bB@1$uU*RWu1+ib zR<@5BqRqJ2W#5DHeTxRJyLEc}wgzY4)R?{dMLj$FelOE|$EF@~`&t#Y%s+ki&0QaS zS8thN(4fctn(g|v*77G`ZGXqIcg_yuf-P=!O=mq5rxkT+zxd9wjxD0b4j!GjC8KQW zEcb>h`bJM}dFgKDh_k=lNf~?5A!Ens1J}3o`|Xcm?(c8>=i;CxkEO*m)&KOCYvoR@ zQ8{YgP4l};os;t}{uy^6W3jGcZ{tlv+Yg-hK|ASK%eL1lBR&az_;#S?gR$ct@1AaJ zvo>nCZ+YYOYa)mCc+Y>`jgO8Q+sBS*`04L0{_Z{aL$AtdextVZjBWmA(zAPa8lArQ z_|S}{vD=)S^2}pnHhtsQa_7@$d4u~6esA1Sx1;B8EZHr$zu3~(CuPd`N%;S1NlBd4 z`lV(d&%M}8wPRb+#i?~PU4yfAon-A_pLOmK(lI1BG`L;+k-=@9Tw5$QE`I2ta&-&| zaMZW@$xtzWYuQ(4tQB8RTz9W^%D4tQzRjMtcl(vDk8ixTaE$!uuLl2jp$-o!{o?wD zg>)GH+G}$=4j(b6f2v_*pZK|P!{$yhrcaQ&JnUpjoxbP9$gh*uXs333d~I)d?Is^g zxf|5{(bbE48t&ZrN0*h!qf%?uoAA{L$KrWk#FjaJd}2r~ZLU|~1;5Coh0DIZ)~V%% zb=zwFGpeoc`QVik^S^F-<1cMlhe=nSPHuiV^m^B`0WH3paq?MQqxX*gJbZSKhS&28 zegD4iwb$GAv%sUzE~S2Es`%-xv)8uT3s;sEH*tMuQOmHaP7C&@t$XjiRu4P9H@NW0 ztbgh|wcPyR_Q63v^yoBlvD= z>+Bz=_WR-6+KZC!ZAsra`cztj?|O&bJJ)LS`Qkova#z-yFm%b8?-M`xwe{eBU;f>E z!-l1%%WE8Oc4>FM@#trRcYTvxf9mT~c52V;{Ao}9^f$Xp%}o9iUtU}`1L9|RgOi|9H#_NmJdDlPs&Ix%^i}kzm}h4 z)1q&NGFlCxkMiJbjXF5n;bsw7YPE|?BM(je`=|er`#o9SsY6J+kg#@!knoXpJ)oy! zsDY7TBZH$t{Db|R9ELbL)bJiWB^7_#lbD*OA2en1^rUos->4AT|7j1?pKTg$x$|g} z+mHHYA>#%;E$wn@)KTls5gWI)@JlZ7$v?S2)~W3wUu)mm5!)Is-M;iKYcJv8P;;N||`9shp+(xJGP@_CPl@spa3{e6o5sj2jfQ9Zlc76%m0xPQ8|@tvc;)-LVRu68+9Ul0{>i~p0Kvx2LrlRto}i+_pFI2Gq>3A z`pT^Xe_iDp+Gt*>Z(*o!_N`e{4`sAm_4Mz-=MRpWl+tE!Txd>L&ryEuAGs8DNgJt} z(`xapuKR9R&R(6-;q1Vvo6mS}`m5gY6RMJ%o*&TvE)>HXqn@rOg_&5Btv;Fn)h zBiDuhx}a>6w{=M3<#RiJys))dtD$Y(N*+`eM4EamEgFs`+e z@1~#YcjEA+ZmnQ>ij{wciml8zNT?yIXBX8crTj!S{?7K$6MQf@?>}YV2|h7RVt0-n2kRZ z30zQlyYZkW-8PRZ|6xIDmfy2KI{SaRC$r-CX!k>dP89`p|Fxv2Zt$1wkETx>_T~ol z!C?!|yI!01^S)`fY&V?>{)3vE*W((x{{IFVjYq&&s}MQaQfk-ecXx zrl#wE3~QRc?A*=b^A9^)t=t+VcK(N80Zg;HLVpo?KL;?*9QciLdkk delta 27567 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 KJU2y`n*Rr5!Hg;Z 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 77ecd67e..67b0f071 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -387,7 +387,7 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_ static NSArray* getDialogURLs( NSSavePanel* dialog ) { if( [dialog isKindOfClass:[NSOpenPanel class]] ) - return static_cast(dialog).URLs; + return [[NSArray alloc] initWithArray: static_cast(dialog).URLs]; NSURL* url = dialog.URL; // use '[[NSArray alloc] initWithObject:url]' here because '@[url]' crashes on macOS 10.14 From 299250a710671a8a70d878d9a4e91ac32c6d4b5d Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 27 Oct 2025 18:06:41 +0100 Subject: [PATCH 34/34] System File Chooser: change `@since 3.6` to `@since 3.7` --- .../java/com/formdev/flatlaf/FlatSystemProperties.java | 2 +- .../com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java | 10 +++++----- .../com/formdev/flatlaf/ui/FlatNativeMacLibrary.java | 8 ++++---- .../formdev/flatlaf/ui/FlatNativeWindowsLibrary.java | 10 +++++----- .../com/formdev/flatlaf/util/SystemFileChooser.java | 2 +- .../src/main/cpp/GtkFileChooser.cpp | 2 +- .../src/main/cpp/GtkMessageDialog.cpp | 2 +- .../src/main/objcpp/MacFileChooser.mm | 2 +- .../src/main/objcpp/MacMessageDialog.mm | 2 +- .../src/main/cpp/WinFileChooser.cpp | 2 +- .../src/main/cpp/WinMessageDialog.cpp | 2 +- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java index a344f220..e9ee613b 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java @@ -235,7 +235,7 @@ public interface FlatSystemProperties * Allowed Values {@code false} and {@code true}
    * Default {@code true} * - * @since 3.6 + * @since 3.7 */ String USE_SYSTEM_FILE_CHOOSER = "flatlaf.useSystemFileChooser"; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java index a9a45ec7..1aceddf4 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java @@ -142,7 +142,7 @@ public class FlatNativeLinuxLibrary * This works because Java uses {@code dlopen(RTLD_LAZY)} to load JNI libraries, * which only resolves symbols as the code that references them is executed. * - * @since 3.6 + * @since 3.7 */ public static boolean isGtk3Available() { if( isGtk3Available == null ) @@ -155,7 +155,7 @@ public class FlatNativeLinuxLibrary /** * https://docs.gtk.org/gtk3/iface.FileChooser.html#properties * - * @since 3.6 + * @since 3.7 */ public static final int FC_select_folder = 1 << 0, @@ -197,14 +197,14 @@ public class FlatNativeLinuxLibrary * @return file path(s) that the user selected; an empty array if canceled; * or {@code null} on failures (no dialog shown) * - * @since 3.6 + * @since 3.7 */ public native static String[] showFileChooser( Window owner, boolean open, String title, String okButtonLabel, String currentName, String currentFolder, int optionsSet, int optionsClear, FileChooserCallback callback, int fileTypeIndex, String... fileTypes ); - /** @since 3.6 */ + /** @since 3.7 */ public interface FileChooserCallback { boolean approve( String[] files, long hwndFileDialog ); } @@ -229,7 +229,7 @@ public class FlatNativeLinuxLibrary * Use '__' for '_' character (e.g. "Choose__and__Quit"). * @return index of pressed button; or -1 for ESC key * - * @since 3.6 + * @since 3.7 */ public native static int showMessageDialog( long hwndParent, int messageType, String primaryText, String secondaryText, int defaultButton, String... buttons ); 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 f4d17c01..460ab899 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 @@ -71,7 +71,7 @@ public class FlatNativeMacLibrary /** @since 3.4 */ public native static boolean toggleWindowFullScreen( Window window ); - /** @since 3.6 */ + /** @since 3.7 */ public static final int // NSOpenPanel (extends NSSavePanel) FC_canChooseFiles = 1 << 0, // default @@ -120,7 +120,7 @@ public class FlatNativeMacLibrary * @return file path(s) that the user selected; an empty array if canceled; * or {@code null} on failures (no dialog shown) * - * @since 3.6 + * @since 3.7 */ public native static String[] showFileChooser( Window owner, int dark, boolean open, String title, String prompt, String message, String filterFieldLabel, @@ -128,7 +128,7 @@ public class FlatNativeMacLibrary int optionsSet, int optionsClear, FileChooserCallback callback, int fileTypeIndex, String... fileTypes ); - /** @since 3.6 */ + /** @since 3.7 */ public interface FileChooserCallback { boolean approve( String[] files, long hwndFileDialog ); } @@ -149,7 +149,7 @@ public class FlatNativeMacLibrary * @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 + * @since 3.7 */ 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/ui/FlatNativeWindowsLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java index 65326049..a5caaef7 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java @@ -165,7 +165,7 @@ public class FlatNativeWindowsLibrary * FILEOPENDIALOGOPTIONS * see https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/ne-shobjidl_core-_fileopendialogoptions * - * @since 3.6 + * @since 3.7 */ public static final int FOS_OVERWRITEPROMPT = 0x2, // default for Save @@ -229,7 +229,7 @@ public class FlatNativeWindowsLibrary * @return file path(s) that the user selected; an empty array if canceled; * or {@code null} on failures (no dialog shown) * - * @since 3.6 + * @since 3.7 */ public native static String[] showFileChooser( Window owner, boolean open, String title, String okButtonLabel, String fileNameLabel, String fileName, @@ -237,7 +237,7 @@ public class FlatNativeWindowsLibrary int optionsSet, int optionsClear, FileChooserCallback callback, int fileTypeIndex, String... fileTypes ); - /** @since 3.6 */ + /** @since 3.7 */ public interface FileChooserCallback { boolean approve( String[] files, long hwndFileDialog ); } @@ -260,7 +260,7 @@ public class FlatNativeWindowsLibrary * Use '&&' for '&' character (e.g. "Choose && Quit"). * @return index of pressed button; or -1 for ESC key * - * @since 3.6 + * @since 3.7 */ public native static int showMessageDialog( long hwndParent, int messageType, String title, String text, int defaultButton, String... buttons ); @@ -277,7 +277,7 @@ public class FlatNativeWindowsLibrary * @param type see
    MessageBox parameter uType * @return see MessageBox Return value * - * @since 3.6 + * @since 3.7 */ public native static int showMessageBox( long hwndParent, String text, String caption, int type ); } 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 be09b6f3..56af434e 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 @@ -102,7 +102,7 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; * * * @author Karl Tauber - * @since 3.6 + * @since 3.7 */ public class SystemFileChooser { diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp index 47a64a55..5603e4ae 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp @@ -24,7 +24,7 @@ /** * @author Karl Tauber - * @since 3.6 + * @since 3.7 */ // declare external methods diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp index 34f26c1b..3f67fd5f 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp @@ -24,7 +24,7 @@ /** * @author Karl Tauber - * @since 3.6 + * @since 3.7 */ extern "C" 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 67b0f071..52c2b28a 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -23,7 +23,7 @@ /** * @author Karl Tauber - * @since 3.6 + * @since 3.7 */ // declare internal methods diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm index b8d3ee9b..def00f92 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm @@ -23,7 +23,7 @@ /** * @author Karl Tauber - * @since 3.6 + * @since 3.7 */ extern "C" diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp index 1eebe21e..002c7cdb 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp @@ -24,7 +24,7 @@ /** * @author Karl Tauber - * @since 3.6 + * @since 3.7 */ // declare external methods diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp index 15926115..1445c431 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp @@ -24,7 +24,7 @@ /** * @author Karl Tauber - * @since 3.6 + * @since 3.7 */ #define ID_BUTTON1 101