+ * Instead, use {@link ApproveContext#showMessageDialog(int, String, String, int, String...)},
+ * which shows a modal system message dialog as child of the file dialog.
+ *
+ *
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;
+ }
+
+ /**
+ * 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
+ : (parent != null) ? SwingUtilities.windowForComponent( parent ) : null;
+ if( owner == null )
+ owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
+
+ approveResult = APPROVE_OPTION;
+ File[] files = getProvider().showDialog( owner, this );
+ setSelectedFiles( files );
+ 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() {
+ 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.isMacOS && FlatNativeMacLibrary.isLoaded() )
+ return new MacFileChooserProvider();
+ else if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isLoaded() && FlatNativeLinuxLibrary.isGtk3Available() )
+ return new LinuxFileChooserProvider();
+ else // unknown platform or FlatLaf native library not loaded
+ return new SwingFileChooserProvider();
+ }
+
+ //---- interface FileChooserProvider --------------------------------------
+
+ private interface FileChooserProvider {
+ File[] showDialog( Window owner, SystemFileChooser fc );
+ }
+
+ //---- class SystemFileChooserProvider ------------------------------------
+
+ private static abstract class SystemFileChooserProvider
+ implements FileChooserProvider
+ {
+ @Override
+ public File[] showDialog( Window owner, SystemFileChooser fc ) {
+ 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();
+ }, "FlatLaf SystemFileChooser" ).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( owner, fc );
+
+ // canceled?
+ if( filenames.length == 0 )
+ 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;
+ }
+ }
+
+ //---- 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 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() )
+ optionsSet |= FlatNativeWindowsLibrary.FOS_ALLOWMULTISELECT;
+ if( !fc.isFileHidingEnabled() )
+ optionsSet |= FlatNativeWindowsLibrary.FOS_FORCESHOWHIDDEN;
+
+ // filter
+ int fileTypeIndex = 0;
+ ArrayList fileTypes = new ArrayList<>();
+ 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( fc.getAcceptAllFileFilter().getDescription() );
+ fileTypes.add( "*.*" );
+ }
+ }
+
+ // 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,
+ 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()] ) );
+ }
+
+ //---- 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 ---------------------------------------
+
+ private static class MacFileChooserProvider
+ extends SystemFileChooserProvider
+ {
+ @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;
+
+ // 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 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 | FlatNativeMacLibrary.FC_canCreateDirectories;
+ optionsClear |= FlatNativeMacLibrary.FC_canChooseFiles;
+ open = true;
+ }
+ if( fc.isMultiSelectionEnabled() )
+ 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;
+ 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 );
+ }
+ }
+ }
+
+ // callback
+ FlatNativeMacLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null)
+ ? (files, hwndFileDialog) -> {
+ return invokeApproveCallback( fc, files, new MacApproveContext( hwndFileDialog ) );
+ } : null;
+
+ // show system file dialog
+ return FlatNativeMacLibrary.showFileChooser( owner, dark, open,
+ 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()] ) );
+ }
+
+ //---- 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 -------------------------------------
+
+ 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 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() )
+ 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;
+
+ // 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 );
+ }
+ }
+ }
+
+ // 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, callback,
+ 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();
+ }
+
+ //---- 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 -------------------------------------
+
+ private static class SwingFileChooserProvider
+ implements FileChooserProvider
+ {
+ @Override
+ public File[] showDialog( Window owner, SystemFileChooser fc ) {
+ JFileChooser chooser = new JFileChooser() {
+ @Override
+ public void approveSelection() {
+ 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 ) )
+ return;
+ } else {
+ 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();
+ }
+ };
+
+ 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 );
+
+ // 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() );
+
+ if( chooser.showDialog( owner, null ) != JFileChooser.APPROVE_OPTION )
+ return null;
+
+ return chooser.isMultiSelectionEnabled()
+ ? 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;
+ }
+
+ //---- 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 ---------------------------------------------------
+
+ /** @see javax.swing.filechooser.FileFilter */
+ public static abstract class FileFilter {
+ /** @see javax.swing.filechooser.FileFilter#getDescription() */
+ public abstract String getDescription();
+ }
+
+ //---- 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" );
+ }
+ }
+
+ //---- 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 dialog.
+ *
+ * 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 );
+ }
+
+ //---- 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-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 716527e57..9fbd790ac 100644
Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-arm64.dll and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-arm64.dll differ
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 fb0f8325d..96e41e992 100644
Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86.dll and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86.dll differ
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 e810d4a4e..6b3d15c51 100644
Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86_64.dll and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86_64.dll differ
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 bcbafedca..39168077f 100755
Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-arm64.so and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-arm64.so differ
diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-x86_64.so b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-x86_64.so
index 3d0ddc5a2..a405e10c2 100644
Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-x86_64.so and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-x86_64.so differ
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 5d683a244..5dc29eb0c 100755
Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-arm64.dylib and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-arm64.dylib differ
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 b356acc91..88c461426 100755
Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib differ
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 54b0e33ec..e37112540 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 @@
import java.awt.*;
import java.awt.event.*;
+import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -49,6 +50,7 @@
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 +174,48 @@ private void saveAsActionPerformed() {
chooser.showSaveDialog( this );
}
+ 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" ) );
+
+ if( chooser.showOpenDialog( this ) != SystemFileChooser.APPROVE_OPTION )
+ return;
+
+ File[] files = chooser.getSelectedFiles();
+ System.out.println( Arrays.toString( files ).replace( ",", "\n" ) );
+ }
+
+ 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" ) );
+
+ 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() {
dispose();
}
@@ -508,6 +552,9 @@ private void initComponents() {
JMenuItem newMenuItem = new JMenuItem();
JMenuItem openMenuItem = new JMenuItem();
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();
@@ -608,6 +655,25 @@ private void initComponents() {
fileMenu.add(saveAsMenuItem);
fileMenu.addSeparator();
+ //---- 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);
+
+ //---- 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 ----
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 08248c1d6..70bb49ac2 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,27 @@ 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)..."
+ "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.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 09768e3e6..18ce55e52 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.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 static void main( String[] args ) {
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-natives/flatlaf-natives-linux/README.md b/flatlaf-natives/flatlaf-natives-linux/README.md
index 4be74776e..0054eefae 100644
--- a/flatlaf-natives/flatlaf-natives-linux/README.md
+++ b/flatlaf-natives/flatlaf-natives-linux/README.md
@@ -25,6 +25,7 @@ To build the library on Linux, some packages needs to be installed:
- `build-essential` - GCC and development tools
- `libxt-dev` - X11 toolkit development headers
+- `libgtk-3-dev` - GTK 3 toolkit development headers
- `g++-aarch64-linux-gnu` - GNU C++ compiler for the arm64 architecture (only on
x86_64 Linux for cross-compiling for arm64 architecture)
@@ -32,19 +33,39 @@ To build the library on Linux, some packages needs to be installed:
### Ubuntu
~~~
-sudo apt update
-sudo apt install build-essential libxt-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-get 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
~~~
-sudo apt install g++-aarch64-linux-gnu
+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 dc9de1e59..d73ab05f2 100644
--- a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts
+++ b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts
@@ -65,15 +65,37 @@ tasks {
includes.from(
"${javaHome}/include",
- "${javaHome}/include/linux"
+ "${javaHome}/include/linux",
+
+ // for GTK
+ "/usr/include/gtk-3.0",
+ "/usr/include/glib-2.0",
+ 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",
+ "/usr/include/pango-1.0",
+ "/usr/include/harfbuzz",
)
compilerArgs.addAll( toolChain.map {
when( it ) {
- is Gcc, is Clang -> listOf()
+ is Gcc, is Clang -> listOf( "-fvisibility=hidden" )
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 {
@@ -88,7 +110,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()
}
} )
@@ -128,7 +150,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",
)
}
@@ -152,10 +187,15 @@ tasks {
"-o", "$outDir/$libraryName",
"$objDir/ApiVersion.o",
+ "$objDir/GtkFileChooser.o",
+ "$objDir/GtkMessageDialog.o",
+ "$objDir/JNIUtils.o",
"$objDir/X11WmUtils.o",
+ "-lstdc++",
"-L${layout.projectDirectory}/lib/aarch64",
"-ljawt",
+ "-lgtk-3",
)
doLast {
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 02454fb3c..cdd701a90 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 000000000..5603e4ae1
--- /dev/null
+++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp
@@ -0,0 +1,277 @@
+/*
+ * 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.7
+ */
+
+// 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 );
+
+//---- 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 )
+
+static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) {
+ jclass stringClass = env->FindClass( "java/lang/String" );
+ return env->NewObjectArray( count, stringClass, NULL );
+}
+
+static 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 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 );
+}
+
+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 ) {
+ 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 );
+ 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, jobject owner, jboolean open,
+ jstring title, jstring okButtonLabel, jstring currentName, jstring currentFolder,
+ jint optionsSet, jint optionsClear, jobject callback, 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
+ : (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
+ 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
+ : (selectFolder ? _("_Select") : (open ? _("_Open") : _("_Save"))), GTK_RESPONSE_ACCEPT,
+ 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 ) )
+ 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 ) );
+
+ // initialize filter
+ initFilters( chooser, env, fileTypeIndex, fileTypes );
+
+ // 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)
+ 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)
+ // 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?
+ 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;
+ 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/GtkMessageDialog.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp
new file mode 100644
index 000000000..3f67fd5fa
--- /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.7
+ */
+
+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 000000000..4edea7e47
--- /dev/null
+++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp
@@ -0,0 +1,54 @@
+/*
+ * 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"
+
+/**
+ * @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 );
+}
+
+//---- 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/cpp/X11WmUtils.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp
index 8cbc57a97..3fc604f18 100644
--- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp
+++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp
@@ -25,18 +25,21 @@
*/
-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 ------------------------------------------------------------
/**
* 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"
@@ -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-linux/src/main/headers/JNIUtils.h b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/JNIUtils.h
new file mode 100644
index 000000000..6d7d8c22c
--- /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-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 c3b8c4b25..0c5816409 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,30 @@ 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
+ * 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, 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
}
#endif
diff --git a/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts b/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts
index 961121d8c..22e913182 100644
--- a/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts
+++ b/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts
@@ -75,7 +75,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/headers/JNIUtils.h b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h
index f0a8604c9..b2dfe6743 100644
--- a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h
+++ b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h
@@ -46,7 +46,33 @@
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(); \
+ }
+
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 f99549e5a..301039b21 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
+#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
+#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 1024L
+#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsHiddenFiles
+#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 4096L
+#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsOtherFileTypes
+#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
@@ -53,6 +79,22 @@ 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: (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, jobject, jint, 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
}
#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 ef0ff0f3c..b9b9a9b4d 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 c000a9e2e..a5fb10e75 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 000000000..52c2b28a8
--- /dev/null
+++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm
@@ -0,0 +1,406 @@
+/*
+ * 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.7
+ */
+
+// 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 {
+ NSArray* _filters;
+
+ JavaVM* _jvm;
+ jobject _callback;
+ NSMutableSet* _urlsSet;
+ }
+
+ @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:NSZeroRect];
+ 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: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: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
+ [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;
+ }
+
+ //---- NSOpenSavePanelDelegate ----
+
+ - (void) initCallback: (JavaVM*)jvm :(jobject)callback {
+ _jvm = jvm;
+ _callback = callback;
+ }
+
+ - (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
+ // 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;
+ }
+
+ //---- 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 -----------------------------------------------------------------
+
+#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 )
+
+static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) {
+ jclass stringClass = env->FindClass( "java/lang/String" );
+ 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, 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 )
+{
+ 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 );
+ 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 );
+ NSMutableArray* filters = initFilters( env, fileTypes );
+
+ NSArray* urls = NULL;
+ NSArray** purls = &urls;
+
+ // show file dialog on macOS thread
+ [FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){
+ JNI_COCOA_TRY()
+
+ // create open/save panel
+ 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 )
+ 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];
+
+ // set open options
+ 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( isOptionSetOrClear( FC_resolvesAliases ) )
+ openDialog.resolvesAliases = isOptionSet( FC_resolvesAliases );
+ if( isOptionSetOrClear( FC_allowsMultipleSelection ) )
+ openDialog.allowsMultipleSelection = isOptionSet( FC_allowsMultipleSelection );
+ }
+
+ // set options
+ 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 );
+
+ FileChooserDelegate* delegate = [FileChooserDelegate new];
+ delegate.dialog = dialog;
+
+ // 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 );
+ }
+
+ // initialize callback
+ if( callback != NULL )
+ [delegate initCallback :jvm :callback];
+
+ // set file dialog delegate
+ dialog.delegate = delegate;
+
+ // show dialog
+ NSModalResponse response = [dialog runModal];
+ [delegate release];
+ if( response != NSModalResponseOK ) {
+ *purls = @[];
+ return;
+ }
+
+ *purls = getDialogURLs( dialog );
+
+ JNI_COCOA_CATCH()
+ }];
+
+ if( urls == NULL )
+ return NULL;
+
+ // convert URLs to Java string array
+ return urlsToStringArray( env, urls );
+
+ JNI_COCOA_EXIT()
+}
+
+static NSArray* getDialogURLs( NSSavePanel* dialog ) {
+ if( [dialog isKindOfClass:[NSOpenPanel class]] )
+ 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
+ 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;
+}
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 000000000..def00f929
--- /dev/null
+++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm
@@ -0,0 +1,90 @@
+/*
+ * 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.7
+ */
+
+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 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 )
+ 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/MacWindow.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm
index 6676a4b25..c45f0bb85 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 @@ @interface WindowData : NSObject
@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 @@ @implementation WindowData
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 ) {
@@ -252,7 +254,7 @@ @implementation WindowData
return FALSE;
}
-void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ) {
+static void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ) {
// get buttons
NSView* buttons[3] = {
[nsWindow standardWindowButton:NSWindowCloseButton],
@@ -312,7 +314,7 @@ void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ) {
return NULL;
}
-int getWindowButtonAreaWidth( NSWindow* nsWindow ) {
+static int getWindowButtonAreaWidth( NSWindow* nsWindow ) {
// get buttons
NSView* buttons[3] = {
[nsWindow standardWindowButton:NSWindowCloseButton],
@@ -344,7 +346,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;
@@ -369,7 +371,7 @@ int getWindowTitleBarHeight( NSWindow* nsWindow ) {
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 0a7eee0d4..3892d870f 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()
}
} )
@@ -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" )
else -> emptyList()
}
} )
@@ -93,6 +93,15 @@ tasks {
into( nativesDir )
rename( linkedFile.get().asFile.name, 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/ApiVersion.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/ApiVersion.cpp
index a32f0d6dc..0f2e7a971 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/FlatWndProc.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp
index d45f758a8..d2e1ab2be 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/JNIUtils.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/JNIUtils.cpp
new file mode 100644
index 000000000..bc24a10e5
--- /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/Runtime.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/Runtime.cpp
index cbd5163d8..cea43d132 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/WinFileChooser.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp
new file mode 100644
index 000000000..002c7cdb6
--- /dev/null
+++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp
@@ -0,0 +1,333 @@
+/*
+ * 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 "JNIUtils.h"
+#include "com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h"
+
+/**
+ * @author Karl Tauber
+ * @since 3.7
+ */
+
+// 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 {
+ T* ptr;
+
+public:
+ AutoReleasePtr() {
+ ptr = NULL;
+ }
+ AutoReleasePtr( T* p ) {
+ ptr = p;
+ ptr->AddRef();
+ }
+ ~AutoReleasePtr() {
+ if( ptr != NULL )
+ ptr->Release();
+ }
+ T** operator&() { return &ptr; }
+ T* operator->() { return ptr; }
+ operator T*() { return ptr; }
+};
+
+//---- 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 {
+ AutoReleaseStringArray fileTypes;
+
+public:
+ UINT count = 0;
+ COMDLG_FILTERSPEC* specs = NULL;
+
+public:
+ FilterSpec( JNIEnv* _env, jobjectArray _fileTypes )
+ : fileTypes( _env, _fileTypes )
+ {
+ if( fileTypes.count == 0 )
+ return;
+
+ count = fileTypes.count / 2;
+ specs = new COMDLG_FILTERSPEC[fileTypes.count];
+
+ for( int i = 0; i < count; i++ ) {
+ specs[i].pszName = fileTypes[i * 2];
+ specs[i].pszSpec = fileTypes[(i * 2) + 1];
+ }
+ }
+ ~FilterSpec() {
+ if( specs != NULL )
+ delete[] specs;
+ }
+};
+
+//---- 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 {
+public:
+ bool initialized;
+
+ CoInitializer() {
+ HRESULT result = ::CoInitializeEx( NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE );
+ initialized = SUCCEEDED( result );
+ }
+ ~CoInitializer() {
+ if( initialized )
+ ::CoUninitialize();
+ }
+};
+
+//---- helper -----------------------------------------------------------------
+
+#define isOptionSet( option ) ((optionsSet & com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_ ## option) != 0)
+#define CHECK_HRESULT( code ) { if( (code) != S_OK ) return NULL; }
+
+static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) {
+ jclass stringClass = env->FindClass( "java/lang/String" );
+ return env->NewObjectArray( count, stringClass, NULL );
+}
+
+static 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, jobject callback, jint fileTypeIndex, jobjectArray fileTypes )
+{
+ // initialize COM library
+ CoInitializer coInitializer;
+ if( !coInitializer.initialized )
+ return NULL;
+
+ // handle limitations (without this, some Win32 method fails and this method returns NULL)
+ 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 );
+ 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 ) ) );
+
+ // set title, etc.
+ 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 ) );
+
+ // set options
+ FILEOPENDIALOGOPTIONS existingOptions;
+ CHECK_HRESULT( dialog->GetOptions( &existingOptions ) );
+ CHECK_HRESULT( dialog->SetOptions ( (existingOptions & ~optionsClear) | optionsSet ) );
+
+ // initialize filter
+ if( specs.count > 0 ) {
+ CHECK_HRESULT( dialog->SetFileTypes( specs.count, specs.specs ) );
+ if( fileTypeIndex > 0 )
+ 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 );
+
+ // 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;
+ 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, i, jpath );
+ env->DeleteLocalRef( jpath );
+ }
+ return array;
+ } else {
+ AutoReleasePtr shellItem;
+ LPWSTR path;
+ 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 );
+
+ 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/cpp/WinMessageDialog.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp
new file mode 100644
index 000000000..1445c4318
--- /dev/null
+++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp
@@ -0,0 +1,419 @@
+/*
+ * 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.
+ */
+
+// avoid inlining of printf()
+#define _NO_CRT_STDIO_INLINE
+
+#include
+#include
+#include "JNIUtils.h"
+#include "com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h"
+
+/**
+ * @author Karl Tauber
+ * @since 3.7
+ */
+
+#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_showMessageBox
+ ( 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 );
+}
+
+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 12
+#define INSETS_LEFT 12
+#define INSETS_RIGHT 12
+#define INSETS_BOTTOM 6
+
+#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 12
+#define BUTTON_GAP 5
+#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 )
+{
+ // get font info needed for DS_SETFONT
+ NONCLIENTMETRICS ncMetrics;
+ ncMetrics.cbSize = sizeof( NONCLIENTMETRICS );
+ if( !::SystemParametersInfo( SPI_GETNONCLIENTMETRICS, 0, &ncMetrics, 0 ) )
+ return NULL;
+
+ // 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;
+ 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;
+
+ // 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 ) + 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] ) + 1) * 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 | DS_SETFONT;
+ 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
+
+ // for DS_SETFONT
+ *lpw++ = fontPointSize;
+ wcscpy( (LPWSTR) lpw, fontFaceName );
+ lpw += wcslen( fontFaceName ) + 1;
+
+ //---- 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 ) {
+ switch( uMsg ) {
+ case WM_INITDIALOG:
+ ::EnumChildWindows( hwnd, focusDefaultButtonProc, 0 );
+ break;
+
+ case 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/cpp/WinWrapper.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinWrapper.cpp
index c1276973f..b77ef1d5d 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 ----------------------------------------------------------------
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 000000000..5eaad97cd
--- /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; }
+};
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 1701cb3bc..b5b332458 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,30 @@ 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;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, jobject, jint, jobjectArray);
+
+/*
+ * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary
+ * Method: showMessageDialog
+ * 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
}
#endif
diff --git a/flatlaf-testing/build.gradle.kts b/flatlaf-testing/build.gradle.kts
index 6fd1b64f8..7215de8b4 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/FlatSystemFileChooserLinuxTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java
new file mode 100644
index 000000000..65c68b498
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java
@@ -0,0 +1,382 @@
+/*
+ * 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.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.*;
+
+/**
+ * @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" );
+ FlatSystemFileChooserTest.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 ) {
+ 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 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();
+
+ 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;
+ };
+
+ System.out.println( FlatNativeLinuxLibrary.isGtk3Available() );
+
+ if( direct ) {
+ String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, open,
+ title, okButtonLabel, currentName, currentFolder,
+ optionsSet.get(), optionsClear.get(), callback, 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( owner, open,
+ title, okButtonLabel, currentName, currentFolder,
+ optionsSet.get(), optionsClear.get(), callback, 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 void initComponents() {
+ // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
+ 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();
+ 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();
+ JLabel okButtonLabelLabel = new JLabel();
+ okButtonLabelField = new JTextField();
+ JLabel currentNameLabel = new JLabel();
+ currentNameField = new JTextField();
+ JLabel currentFolderLabel = new JLabel();
+ currentFolderField = new JTextField();
+ JLabel fileTypesLabel = new JLabel();
+ fileTypesField = new JComboBox<>();
+ JLabel fileTypeIndexLabel = new JLabel();
+ fileTypeIndexSlider = new JSlider();
+ JButton openButton = new JButton();
+ JButton saveButton = new JButton();
+ JButton openDirectButton = new JButton();
+ JButton saveDirectButton = new JButton();
+ showMessageDialogOnOKCheckBox = new JCheckBox();
+ JScrollPane filesScrollPane = new JScrollPane();
+ filesField = 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");
+
+ //---- titleLabel ----
+ titleLabel.setText("title");
+ add(titleLabel, "cell 0 1");
+ add(titleField, "cell 1 1");
+
+ //======== 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 1 1 6,aligny top,growy 0");
+
+ //---- okButtonLabelLabel ----
+ okButtonLabelLabel.setText("okButtonLabel");
+ add(okButtonLabelLabel, "cell 0 2");
+ add(okButtonLabelField, "cell 1 2");
+
+ //---- currentNameLabel ----
+ currentNameLabel.setText("currentName");
+ add(currentNameLabel, "cell 0 3");
+ add(currentNameField, "cell 1 3");
+
+ //---- currentFolderLabel ----
+ currentFolderLabel.setText("currentFolder");
+ add(currentFolderLabel, "cell 0 4");
+ add(currentFolderField, "cell 1 4");
+
+ //---- fileTypesLabel ----
+ fileTypesLabel.setText("fileTypes");
+ add(fileTypesLabel, "cell 0 5");
+
+ //---- 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 5");
+
+ //---- fileTypeIndexLabel ----
+ fileTypeIndexLabel.setText("fileTypeIndex");
+ add(fileTypeIndexLabel, "cell 0 6");
+
+ //---- fileTypeIndexSlider ----
+ fileTypeIndexSlider.setMaximum(10);
+ fileTypeIndexSlider.setMajorTickSpacing(1);
+ fileTypeIndexSlider.setValue(0);
+ fileTypeIndexSlider.setPaintLabels(true);
+ fileTypeIndexSlider.setSnapToTicks(true);
+ add(fileTypeIndexSlider, "cell 1 6");
+
+ //---- openButton ----
+ openButton.setText("Open...");
+ openButton.addActionListener(e -> open());
+ add(openButton, "cell 0 7 3 1");
+
+ //---- saveButton ----
+ saveButton.setText("Save...");
+ saveButton.addActionListener(e -> save());
+ add(saveButton, "cell 0 7 3 1");
+
+ //---- openDirectButton ----
+ openDirectButton.setText("Open (no-thread)...");
+ openDirectButton.addActionListener(e -> openDirect());
+ add(openDirectButton, "cell 0 7 3 1");
+
+ //---- saveDirectButton ----
+ saveDirectButton.setText("Save (no-thread)...");
+ 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 ========
+ {
+
+ //---- filesField ----
+ filesField.setRows(8);
+ filesScrollPane.setViewportView(filesField);
+ }
+ 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 JRadioButton ownerFrameRadioButton;
+ private JRadioButton ownerDialogRadioButton;
+ private JRadioButton ownerNullRadioButton;
+ private JTextField titleField;
+ 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 JTextField okButtonLabelField;
+ private JTextField currentNameField;
+ private JTextField currentFolderField;
+ private JComboBox fileTypesField;
+ private JSlider fileTypeIndexSlider;
+ private JCheckBox showMessageDialogOnOKCheckBox;
+ 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 000000000..f3ebc9968
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd
@@ -0,0 +1,280 @@
+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]"
+ } ) {
+ 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 1"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "titleField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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[]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
+ 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: "select_multipleCheckBox"
+ "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"
+ } )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "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 2"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "okButtonLabelField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 2"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "currentNameLabel"
+ "text": "currentName"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 3"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "currentNameField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 3"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "currentFolderLabel"
+ "text": "currentFolder"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 4"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "currentFolderField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 4"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "fileTypesLabel"
+ "text": "fileTypes"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 5"
+ } )
+ 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" )
+ }
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 5"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "fileTypeIndexLabel"
+ "text": "fileTypeIndex"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 6"
+ } )
+ add( new FormComponent( "javax.swing.JSlider" ) {
+ name: "fileTypeIndexSlider"
+ "maximum": 10
+ "majorTickSpacing": 1
+ "value": 0
+ "paintLabels": true
+ "snapToTicks": true
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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 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 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 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 7 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 7 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 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 )
+ } )
+ }
+}
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
new file mode 100644
index 000000000..5c6c2a994
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.java
@@ -0,0 +1,564 @@
+/*
+ * 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.SecondaryLoop;
+import java.awt.Toolkit;
+import java.awt.Window;
+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.*;
+
+/**
+ * @author Karl Tauber
+ */
+public class FlatSystemFileChooserMacTest
+ extends FlatTestPanel
+{
+ 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)
+ // - "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( () -> {
+ if( !FlatNativeMacLibrary.isLoaded() ) {
+ JOptionPane.showMessageDialog( null, "FlatLaf native library not loaded" );
+ return;
+ }
+
+ FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserMacTest" );
+ FlatSystemFileChooserTest.addListeners( frame );
+ frame.showFrame( FlatSystemFileChooserMacTest::new );
+ frame.setJMenuBar( menuBar1 );
+ } );
+ }
+
+ FlatSystemFileChooserMacTest() {
+ 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 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() );
+ String filterFieldLabel = n( filterFieldLabelField.getText() );
+ String nameFieldLabel = n( nameFieldLabelField.getText() );
+ String nameFieldStringValue = n( nameFieldStringValueField.getText() );
+ String directoryURL = n( directoryURLField.getText() );
+ AtomicInteger optionsSet = new AtomicInteger();
+ AtomicInteger optionsClear = new AtomicInteger();
+
+ // NSOpenPanel
+ if( canChooseFilesCheckBox.isSelected() )
+ optionsSet.set( optionsSet.get() | FC_canChooseFiles );
+ if( canChooseDirectoriesCheckBox.isSelected() )
+ 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 );
+ 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 );
+
+ // 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();
+
+ 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;
+ };
+
+ int dark = FlatLaf.isLafDark() ? 1 : 0;
+ if( direct ) {
+ String[] files = FlatNativeMacLibrary.showFileChooser( owner, dark, open,
+ title, prompt, message, filterFieldLabel,
+ nameFieldLabel, nameFieldStringValue, directoryURL,
+ optionsSet.get(), optionsClear.get(), callback, 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 = FlatNativeMacLibrary.showFileChooser( owner, dark, open,
+ title, prompt, message, filterFieldLabel,
+ nameFieldLabel, nameFieldStringValue, directoryURL,
+ optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 );
+
+ System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() );
+
+ SwingUtilities.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 void menuItemAction() {
+ System.out.println( "menu item action" );
+ }
+
+ private void initComponents() {
+ // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
+ 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();
+ JPanel panel1 = new JPanel();
+ JLabel options1Label = new JLabel();
+ canChooseFilesCheckBox = new JCheckBox();
+ canChooseDirectoriesCheckBox = new JCheckBox();
+ resolvesAliasesCheckBox = new FlatTriStateCheckBox();
+ allowsMultipleSelectionCheckBox = new FlatTriStateCheckBox();
+ accessoryViewDisclosedCheckBox = new JCheckBox();
+ JLabel options2Label = new JLabel();
+ showsTagFieldCheckBox = new FlatTriStateCheckBox();
+ canCreateDirectoriesCheckBox = new FlatTriStateCheckBox();
+ canSelectHiddenExtensionCheckBox = new FlatTriStateCheckBox();
+ showsHiddenFilesCheckBox = new FlatTriStateCheckBox();
+ extensionHiddenCheckBox = new FlatTriStateCheckBox();
+ allowsOtherFileTypesCheckBox = new FlatTriStateCheckBox();
+ treatsFilePackagesAsDirectoriesCheckBox = new FlatTriStateCheckBox();
+ JLabel options3Label = new JLabel();
+ showSingleFilterFieldCheckBox = new JCheckBox();
+ JLabel promptLabel = new JLabel();
+ promptField = new JTextField();
+ JLabel messageLabel = new JLabel();
+ messageField = new JTextField();
+ JLabel filterFieldLabelLabel = new JLabel();
+ filterFieldLabelField = new JTextField();
+ JLabel nameFieldLabelLabel = new JLabel();
+ nameFieldLabelField = new JTextField();
+ JLabel nameFieldStringValueLabel = new JLabel();
+ nameFieldStringValueField = new JTextField();
+ JLabel directoryURLLabel = new JLabel();
+ directoryURLField = new JTextField();
+ JLabel fileTypesLabel = new JLabel();
+ fileTypesField = new JComboBox<>();
+ JLabel fileTypeIndexLabel = new JLabel();
+ fileTypeIndexSlider = new JSlider();
+ JButton openButton = new JButton();
+ JButton saveButton = new JButton();
+ JButton openDirectButton = new JButton();
+ JButton saveDirectButton = new JButton();
+ showMessageDialogOnOKCheckBox = new JCheckBox();
+ 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(
+ "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");
+
+ //---- titleLabel ----
+ titleLabel.setText("title");
+ add(titleLabel, "cell 0 1");
+ add(titleField, "cell 1 1");
+
+ //======== panel1 ========
+ {
+ panel1.setLayout(new MigLayout(
+ "insets 2,hidemode 3",
+ // columns
+ "[left]",
+ // rows
+ "[]" +
+ "[]0" +
+ "[]0" +
+ "[]0" +
+ "[]0" +
+ "[]para" +
+ "[]" +
+ "[]0" +
+ "[]0" +
+ "[]0" +
+ "[]0" +
+ "[]0" +
+ "[]0" +
+ "[]para" +
+ "[]" +
+ "[]"));
+
+ //---- 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.setState(FlatTriStateCheckBox.State.SELECTED);
+ panel1.add(resolvesAliasesCheckBox, "cell 0 3");
+
+ //---- allowsMultipleSelectionCheckBox ----
+ 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 6");
+
+ //---- showsTagFieldCheckBox ----
+ showsTagFieldCheckBox.setText("showsTagField");
+ panel1.add(showsTagFieldCheckBox, "cell 0 7");
+
+ //---- canCreateDirectoriesCheckBox ----
+ canCreateDirectoriesCheckBox.setText("canCreateDirectories");
+ panel1.add(canCreateDirectoriesCheckBox, "cell 0 8");
+
+ //---- canSelectHiddenExtensionCheckBox ----
+ canSelectHiddenExtensionCheckBox.setText("canSelectHiddenExtension");
+ panel1.add(canSelectHiddenExtensionCheckBox, "cell 0 9");
+
+ //---- showsHiddenFilesCheckBox ----
+ showsHiddenFilesCheckBox.setText("showsHiddenFiles");
+ panel1.add(showsHiddenFilesCheckBox, "cell 0 10");
+
+ //---- extensionHiddenCheckBox ----
+ extensionHiddenCheckBox.setText("extensionHidden");
+ panel1.add(extensionHiddenCheckBox, "cell 0 11");
+
+ //---- allowsOtherFileTypesCheckBox ----
+ allowsOtherFileTypesCheckBox.setText("allowsOtherFileTypes");
+ panel1.add(allowsOtherFileTypesCheckBox, "cell 0 12");
+
+ //---- treatsFilePackagesAsDirectoriesCheckBox ----
+ treatsFilePackagesAsDirectoriesCheckBox.setText("treatsFilePackagesAsDirectories");
+ 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 1 1 10,aligny top,growy 0");
+
+ //---- promptLabel ----
+ promptLabel.setText("prompt");
+ add(promptLabel, "cell 0 2");
+ add(promptField, "cell 1 2");
+
+ //---- messageLabel ----
+ messageLabel.setText("message");
+ add(messageLabel, "cell 0 3");
+ add(messageField, "cell 1 3");
+
+ //---- filterFieldLabelLabel ----
+ filterFieldLabelLabel.setText("filterFieldLabel");
+ add(filterFieldLabelLabel, "cell 0 4");
+ add(filterFieldLabelField, "cell 1 4");
+
+ //---- nameFieldLabelLabel ----
+ nameFieldLabelLabel.setText("nameFieldLabel");
+ add(nameFieldLabelLabel, "cell 0 5");
+ add(nameFieldLabelField, "cell 1 5");
+
+ //---- nameFieldStringValueLabel ----
+ nameFieldStringValueLabel.setText("nameFieldStringValue");
+ add(nameFieldStringValueLabel, "cell 0 6");
+ add(nameFieldStringValueField, "cell 1 6");
+
+ //---- directoryURLLabel ----
+ directoryURLLabel.setText("directoryURL");
+ add(directoryURLLabel, "cell 0 7");
+ add(directoryURLField, "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,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 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 11 3 1");
+
+ //---- saveButton ----
+ saveButton.setText("Save...");
+ saveButton.addActionListener(e -> save());
+ add(saveButton, "cell 0 11 3 1");
+
+ //---- openDirectButton ----
+ openDirectButton.setText("Open (no-thread)...");
+ openDirectButton.addActionListener(e -> openDirect());
+ add(openDirectButton, "cell 0 11 3 1");
+
+ //---- saveDirectButton ----
+ saveDirectButton.setText("Save (no-thread)...");
+ 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 ========
+ {
+
+ //---- filesField ----
+ filesField.setRows(8);
+ filesScrollPane.setViewportView(filesField);
+ }
+ 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 JRadioButton ownerFrameRadioButton;
+ private JRadioButton ownerDialogRadioButton;
+ private JRadioButton ownerNullRadioButton;
+ private JTextField titleField;
+ private JCheckBox canChooseFilesCheckBox;
+ private JCheckBox canChooseDirectoriesCheckBox;
+ private FlatTriStateCheckBox resolvesAliasesCheckBox;
+ private FlatTriStateCheckBox allowsMultipleSelectionCheckBox;
+ private JCheckBox accessoryViewDisclosedCheckBox;
+ private FlatTriStateCheckBox showsTagFieldCheckBox;
+ private FlatTriStateCheckBox canCreateDirectoriesCheckBox;
+ private FlatTriStateCheckBox canSelectHiddenExtensionCheckBox;
+ private FlatTriStateCheckBox showsHiddenFilesCheckBox;
+ private FlatTriStateCheckBox extensionHiddenCheckBox;
+ private FlatTriStateCheckBox allowsOtherFileTypesCheckBox;
+ private FlatTriStateCheckBox treatsFilePackagesAsDirectoriesCheckBox;
+ private JCheckBox showSingleFilterFieldCheckBox;
+ private JTextField promptField;
+ private JTextField messageField;
+ private JTextField filterFieldLabelField;
+ private JTextField nameFieldLabelField;
+ private JTextField nameFieldStringValueField;
+ private JTextField directoryURLField;
+ private JComboBox fileTypesField;
+ private JSlider fileTypeIndexSlider;
+ private JCheckBox showMessageDialogOnOKCheckBox;
+ 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
new file mode 100644
index 000000000..33ffd50af
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.jfd
@@ -0,0 +1,440 @@
+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]"
+ } ) {
+ 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 1"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "titleField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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[]0[]0[]para[][]0[]0[]0[]0[]0[]0[]para[][]"
+ } ) {
+ 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
+ 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"
+ } )
+ add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) {
+ 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"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "options2Label"
+ "text": "NSOpenPanel and NSSavePanel options:"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 6"
+ } )
+ 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"
+ } )
+ 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"
+ 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 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 2"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "promptField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 2"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "messageLabel"
+ "text": "message"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "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 3"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "filterFieldLabelLabel"
+ "text": "filterFieldLabel"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "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 4"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "nameFieldLabelLabel"
+ "text": "nameFieldLabel"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "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 5"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "nameFieldStringValueLabel"
+ "text": "nameFieldStringValue"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "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 6"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "directoryURLLabel"
+ "text": "directoryURL"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "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 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,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" )
+ }
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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 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 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 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 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 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 12 3 1,growx"
+ } )
+ }, new FormLayoutConstraints( null ) {
+ "location": new java.awt.Point( 0, 0 )
+ "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
new file mode 100644
index 000000000..adfd8ef2f
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java
@@ -0,0 +1,1024 @@
+/*
+ * 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.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;
+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 ) {
+ // 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)
+ // - "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 );
+ frame.setJMenuBar( menuBar1 );
+ } );
+ }
+
+ 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 ) );
+ 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() {
+ 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" );
+
+ // 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 )] );
+
+// 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 );
+
+ String id = (String) stateStoreIDField.getSelectedItem();
+ fc.setStateStoreID( id != null && !id.isEmpty() ? id : null );
+ }
+
+ 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() )
+ new DummyModalDialog( frame, showConsumer ).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 void menuItemAction() {
+ System.out.println( "menu item action" );
+ }
+
+ static void addListeners( Window w ) {
+ w.addWindowListener( new WindowListener() {
+ @Override
+ public void windowOpened( WindowEvent e ) {
+ printWindowEvent( e );
+ }
+
+ @Override
+ public void windowIconified( WindowEvent e ) {
+ printWindowEvent( e );
+ }
+
+ @Override
+ public void windowDeiconified( WindowEvent e ) {
+ printWindowEvent( e );
+ }
+
+ @Override
+ public void windowDeactivated( WindowEvent e ) {
+ printWindowEvent( e );
+ }
+
+ @Override
+ public void windowClosing( WindowEvent e ) {
+ printWindowEvent( e );
+ }
+
+ @Override
+ public void windowClosed( WindowEvent e ) {
+ printWindowEvent( e );
+ }
+
+ @Override
+ public void windowActivated( WindowEvent e ) {
+ printWindowEvent( e );
+ }
+ } );
+ w.addWindowStateListener( new WindowStateListener() {
+ @Override
+ public void windowStateChanged( WindowEvent e ) {
+ printWindowEvent( e );
+ }
+ } );
+ w.addWindowFocusListener( new WindowFocusListener() {
+ @Override
+ public void windowLostFocus( WindowEvent e ) {
+ printWindowEvent( e );
+ }
+
+ @Override
+ public void windowGainedFocus( WindowEvent 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
+ JLabel ownerLabel = new JLabel();
+ ownerFrameRadioButton = new JRadioButton();
+ ownerDialogRadioButton = new JRadioButton();
+ ownerNullRadioButton = new JRadioButton();
+ JPanel ownerSpacer = new JPanel(null);
+ JLabel dialogTitleLabel = new JLabel();
+ dialogTitleField = new JTextField();
+ JPanel panel1 = new JPanel();
+ directorySelectionCheckBox = new JCheckBox();
+ 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();
+ 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();
+ JLabel fileTypesLabel = new JLabel();
+ fileTypesField = new JComboBox<>();
+ JLabel fileTypeIndexLabel = new JLabel();
+ fileTypeIndexSlider = new JSlider();
+ useAcceptAllFileFilterCheckBox = new JCheckBox();
+ 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();
+ 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(
+ "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" +
+ "[]" +
+ "[]para" +
+ "[]" +
+ "[]"));
+
+ //---- 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");
+
+ //---- 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");
+
+ //---- 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");
+
+ //======== 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 JRadioButton ownerFrameRadioButton;
+ private JRadioButton ownerDialogRadioButton;
+ private JRadioButton ownerNullRadioButton;
+ private JTextField dialogTitleField;
+ private JCheckBox directorySelectionCheckBox;
+ private JCheckBox multiSelectionEnabledCheckBox;
+ private JCheckBox useFileHidingCheckBox;
+ private JCheckBox useSystemFileChooserCheckBox;
+ private JCheckBox persistStateCheckBox;
+ private JComboBox stateStoreIDField;
+ private JTextField approveButtonTextField;
+ 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 JComboBox fileTypesField;
+ private JSlider fileTypeIndexSlider;
+ private JCheckBox useAcceptAllFileFilterCheckBox;
+ private JButton javafxOpenButton;
+ private JButton javafxSaveButton;
+ 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
new file mode 100644
index 000000000..9a37ee61c
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd
@@ -0,0 +1,521 @@
+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]"
+ "$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" )
+ 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: "dialogTitleLabel"
+ "text": "dialogTitle"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "dialogTitleField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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[][]para[][]"
+ } ) {
+ name: "panel1"
+ 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"
+ } )
+ add( new FormComponent( "javax.swing.JCheckBox" ) {
+ name: "useFileHidingCheckBox"
+ "text": "useFileHiding"
+ "selected": true
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 2"
+ } )
+ add( new FormComponent( "javax.swing.JCheckBox" ) {
+ name: "useSystemFileChooserCheckBox"
+ "text": "use SystemFileChooser"
+ "selected": true
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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"
+ } )
+ 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"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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" )
+ }
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 7,growx"
+ } )
+ add( new FormComponent( "javax.swing.JCheckBox" ) {
+ name: "useAcceptAllFileFilterCheckBox"
+ "text": "useAcceptAllFileFilter"
+ "selected": true
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ } )
+ }, 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 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
new file mode 100644
index 000000000..b6ddbc4ec
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java
@@ -0,0 +1,622 @@
+/*
+ * 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.Font;
+import java.awt.SecondaryLoop;
+import java.awt.Toolkit;
+import java.awt.Window;
+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.testing.FlatSystemFileChooserTest.DummyModalDialog;
+import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary;
+import net.miginfocom.swing.*;
+
+/**
+ * @author Karl Tauber
+ */
+public class FlatSystemFileChooserWindowsTest
+ 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, "FlatSystemFileChooserWindowsTest" );
+ FlatSystemFileChooserTest.addListeners( frame );
+ frame.showFrame( FlatSystemFileChooserWindowsTest::new );
+ } );
+ }
+
+ 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() {
+ 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 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 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();
+
+ FlatNativeWindowsLibrary.FileChooserCallback callback = (files, hwndFileDialog) -> {
+ System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) );
+ if( showMessageDialogOnOKCheckBox.isSelected() ) {
+ System.out.println( FlatNativeWindowsLibrary.showMessageDialog( hwndFileDialog,
+ JOptionPane.INFORMATION_MESSAGE,
+ null, "some text", 1, "Yes", "No" ) );
+ }
+ return true;
+ };
+
+ if( direct ) {
+ String[] files = FlatNativeWindowsLibrary.showFileChooser( owner, open,
+ title, okButtonLabel, fileNameLabel, fileName,
+ folder, saveAsItem, defaultFolder, defaultExtension,
+ optionsSet.get(), optionsClear.get(), callback, 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(), callback, 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 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 ) );
+ }
+
+ private void initComponents() {
+ // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
+ 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();
+ JPanel 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();
+ JPanel messageDialogPanel = new JPanel();
+ JLabel messageLabel = new JLabel();
+ JScrollPane messageScrollPane = new JScrollPane();
+ messageField = new JTextArea();
+ JLabel buttonsLabel = new JLabel();
+ buttonsField = new JTextField();
+ JLabel okButtonLabelLabel = new JLabel();
+ okButtonLabelField = new JTextField();
+ JLabel fileNameLabelLabel = new JLabel();
+ fileNameLabelField = new JTextField();
+ JLabel fileNameLabel = new JLabel();
+ fileNameField = new JTextField();
+ JLabel folderLabel = new JLabel();
+ folderField = new JTextField();
+ JLabel saveAsItemLabel = new JLabel();
+ saveAsItemField = new JTextField();
+ JLabel defaultFolderLabel = new JLabel();
+ defaultFolderField = new JTextField();
+ JLabel defaultExtensionLabel = new JLabel();
+ defaultExtensionField = new JTextField();
+ JLabel fileTypesLabel = new JLabel();
+ fileTypesField = new JComboBox<>();
+ JLabel fileTypeIndexLabel = new JLabel();
+ fileTypeIndexSlider = new JSlider();
+ JButton openButton = new JButton();
+ JButton saveButton = new JButton();
+ JButton openDirectButton = new JButton();
+ JButton saveDirectButton = new JButton();
+ showMessageDialogOnOKCheckBox = new JCheckBox();
+ JPanel hSpacer1 = new JPanel(null);
+ JButton messageDialogButton = new JButton();
+ JButton messageBoxButton = new JButton();
+ JScrollPane filesScrollPane = new JScrollPane();
+ filesField = 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");
+
+ //---- titleLabel ----
+ titleLabel.setText("title");
+ add(titleLabel, "cell 0 1");
+ add(titleField, "cell 1 1");
+
+ //======== panel1 ========
+ {
+ panel1.setLayout(new MigLayout(
+ "insets 2,hidemode 3",
+ // columns
+ "[left]para" +
+ "[left]para" +
+ "[left]",
+ // rows
+ "[]0" +
+ "[]0" +
+ "[]0" +
+ "[]" +
+ "[]0" +
+ "[]0" +
+ "[]0" +
+ "[]0" +
+ "[grow]"));
+
+ //---- 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");
+ pickFoldersCheckBox.setFont(pickFoldersCheckBox.getFont().deriveFont(pickFoldersCheckBox.getFont().getStyle() | Font.BOLD));
+ panel1.add(pickFoldersCheckBox, "cell 0 3");
+
+ //---- shareAwareCheckBox ----
+ shareAwareCheckBox.setText("shareAware");
+ panel1.add(shareAwareCheckBox, "cell 1 3");
+
+ //---- forceShowHiddenCheckBox ----
+ forceShowHiddenCheckBox.setText("forceShowHidden");
+ forceShowHiddenCheckBox.setFont(forceShowHiddenCheckBox.getFont().deriveFont(forceShowHiddenCheckBox.getFont().getStyle() | Font.BOLD));
+ 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");
+ allowMultiSelectCheckBox.setFont(allowMultiSelectCheckBox.getFont().deriveFont(allowMultiSelectCheckBox.getFont().getStyle() | Font.BOLD));
+ panel1.add(allowMultiSelectCheckBox, "cell 0 7");
+
+ //---- 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,growy");
+
+ //---- okButtonLabelLabel ----
+ okButtonLabelLabel.setText("okButtonLabel");
+ add(okButtonLabelLabel, "cell 0 2");
+ add(okButtonLabelField, "cell 1 2");
+
+ //---- fileNameLabelLabel ----
+ fileNameLabelLabel.setText("fileNameLabel");
+ add(fileNameLabelLabel, "cell 0 3");
+ add(fileNameLabelField, "cell 1 3");
+
+ //---- fileNameLabel ----
+ fileNameLabel.setText("fileName");
+ add(fileNameLabel, "cell 0 4");
+ add(fileNameField, "cell 1 4");
+
+ //---- folderLabel ----
+ folderLabel.setText("folder");
+ add(folderLabel, "cell 0 5");
+ add(folderField, "cell 1 5");
+
+ //---- saveAsItemLabel ----
+ saveAsItemLabel.setText("saveAsItem");
+ add(saveAsItemLabel, "cell 0 6");
+ add(saveAsItemField, "cell 1 6");
+
+ //---- defaultFolderLabel ----
+ defaultFolderLabel.setText("defaultFolder");
+ add(defaultFolderLabel, "cell 0 7");
+ add(defaultFolderField, "cell 1 7");
+
+ //---- defaultExtensionLabel ----
+ defaultExtensionLabel.setText("defaultExtension");
+ add(defaultExtensionLabel, "cell 0 8");
+ add(defaultExtensionField, "cell 1 8");
+
+ //---- fileTypesLabel ----
+ fileTypesLabel.setText("fileTypes");
+ add(fileTypesLabel, "cell 0 9");
+
+ //---- 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 9");
+
+ //---- fileTypeIndexLabel ----
+ fileTypeIndexLabel.setText("fileTypeIndex");
+ add(fileTypeIndexLabel, "cell 0 10");
+
+ //---- fileTypeIndexSlider ----
+ fileTypeIndexSlider.setMaximum(10);
+ fileTypeIndexSlider.setMajorTickSpacing(1);
+ fileTypeIndexSlider.setValue(0);
+ fileTypeIndexSlider.setPaintLabels(true);
+ fileTypeIndexSlider.setSnapToTicks(true);
+ add(fileTypeIndexSlider, "cell 1 10");
+
+ //---- openButton ----
+ openButton.setText("Open...");
+ openButton.addActionListener(e -> open());
+ add(openButton, "cell 0 11 3 1");
+
+ //---- saveButton ----
+ saveButton.setText("Save...");
+ saveButton.addActionListener(e -> save());
+ add(saveButton, "cell 0 11 3 1");
+
+ //---- openDirectButton ----
+ openDirectButton.setText("Open (no-thread)...");
+ openDirectButton.addActionListener(e -> openDirect());
+ add(openDirectButton, "cell 0 11 3 1");
+
+ //---- saveDirectButton ----
+ saveDirectButton.setText("Save (no-thread)...");
+ 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");
+ 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 ========
+ {
+
+ //---- filesField ----
+ filesField.setRows(8);
+ filesScrollPane.setViewportView(filesField);
+ }
+ 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 JRadioButton ownerFrameRadioButton;
+ private JRadioButton ownerDialogRadioButton;
+ private JRadioButton ownerNullRadioButton;
+ private JTextField titleField;
+ 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 JTextArea messageField;
+ private JTextField buttonsField;
+ private JTextField okButtonLabelField;
+ private JTextField fileNameLabelField;
+ private JTextField fileNameField;
+ private JTextField folderField;
+ private JTextField saveAsItemField;
+ private JTextField defaultFolderField;
+ private JTextField defaultExtensionField;
+ private JComboBox fileTypesField;
+ private JSlider fileTypeIndexSlider;
+ private JCheckBox showMessageDialogOnOKCheckBox;
+ 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
new file mode 100644
index 000000000..86f6219ee
--- /dev/null
+++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd
@@ -0,0 +1,550 @@
+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]"
+ "$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" )
+ 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 1"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "titleField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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]para[left]para[left]"
+ "$rowConstraints": "[]0[]0[]0[][]0[]0[]0[]0[grow]"
+ } ) {
+ name: "panel1"
+ 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"
+ } )
+ add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) {
+ 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"
+ } )
+ add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) {
+ 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"
+ } )
+ add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) {
+ 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"
+ } )
+ 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
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ } )
+ }, 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"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, 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,growy"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "okButtonLabelLabel"
+ "text": "okButtonLabel"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 2"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "okButtonLabelField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 2"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "fileNameLabelLabel"
+ "text": "fileNameLabel"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 3"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "fileNameLabelField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 3"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "fileNameLabel"
+ "text": "fileName"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 4"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "fileNameField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 4"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "folderLabel"
+ "text": "folder"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 5"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "folderField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 5"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "saveAsItemLabel"
+ "text": "saveAsItem"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 6"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "saveAsItemField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 6"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "defaultFolderLabel"
+ "text": "defaultFolder"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 7"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "defaultFolderField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 7"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "defaultExtensionLabel"
+ "text": "defaultExtension"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 8"
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "defaultExtensionField"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 8"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "fileTypesLabel"
+ "text": "fileTypes"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 9"
+ } )
+ 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" )
+ }
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 9"
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "fileTypeIndexLabel"
+ "text": "fileTypeIndex"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 10"
+ } )
+ add( new FormComponent( "javax.swing.JSlider" ) {
+ name: "fileTypeIndexSlider"
+ "maximum": 10
+ "majorTickSpacing": 1
+ "value": 0
+ "paintLabels": true
+ "snapToTicks": true
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "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 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 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 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 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 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" ) {
+ name: "filesField"
+ "rows": 8
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": false
+ }
+ } )
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 12 3 1,growx"
+ } )
+ }, new FormLayoutConstraints( null ) {
+ "location": new java.awt.Point( 0, 0 )
+ "size": new java.awt.Dimension( 890, 630 )
+ } )
+ add( new FormNonVisual( "javax.swing.ButtonGroup" ) {
+ name: "ownerButtonGroup"
+ }, new FormLayoutConstraints( null ) {
+ "location": new java.awt.Point( 0, 640 )
+ } )
+ }
+}
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 fbb936f54..ce86d8689 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
@@ -245,6 +245,9 @@ public void dispose() {
super.dispose();
FlatUIDefaultsInspector.hide();
+
+ if( getDefaultCloseOperation() == JFrame.EXIT_ON_CLOSE )
+ System.exit( 0 );
}
private void updateTitle() {
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 9340627ac..ab4566711 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.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 @@ private void openDirectory() {
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 @@ else if( result == 1 ) {
"\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 @@ private void directoryChanged() {
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 @@ private File[] getPropertiesFiles( File dir ) {
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() );
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 91bcfee89..ee8ec1ab8 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"