System File Chooser: added "approve" callback to SystemFileChooser

This commit is contained in:
Karl Tauber
2025-01-20 16:14:45 +01:00
parent d524536575
commit f3ca3a001a
2 changed files with 333 additions and 34 deletions

View File

@@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import javax.swing.JDialog;
import javax.swing.JFileChooser; import javax.swing.JFileChooser;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
@@ -81,6 +82,7 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary;
* as first item and selects it by default. * as first item and selects it by default.
* Use {@code chooser.addChoosableFileFilter( chooser.getAcceptAllFileFilter() )} * Use {@code chooser.addChoosableFileFilter( chooser.getAcceptAllFileFilter() )}
* to place <b>All Files</b> filter somewhere else. * to place <b>All Files</b> filter somewhere else.
* <li>Accessory components are not supported.
* </ul> * </ul>
* *
* @author Karl Tauber * @author Karl Tauber
@@ -131,6 +133,9 @@ public class SystemFileChooser
*/ */
private boolean keepAcceptAllAtEnd = true; private boolean keepAcceptAllAtEnd = true;
private ApproveCallback approveCallback;
private int approveResult = APPROVE_OPTION;
/** @see JFileChooser#JFileChooser() */ /** @see JFileChooser#JFileChooser() */
public SystemFileChooser() { public SystemFileChooser() {
this( (File) null ); this( (File) null );
@@ -407,10 +412,71 @@ public class SystemFileChooser
return filters.size() == 1 && filters.get( 0 ) == getAcceptAllFileFilter(); return filters.size() == 1 && filters.get( 0 ) == getAcceptAllFileFilter();
} }
public ApproveCallback getApproveCallback() {
return approveCallback;
}
/**
* Sets a callback that is invoked when user presses "OK" button (or double-clicks a file).
* The file dialog is still open.
* If the callback returns {@link #CANCEL_OPTION}, then the file dialog stays open.
* If it returns {@link #APPROVE_OPTION} (or any other value other than {@link #CANCEL_OPTION}),
* the file dialog is closed and the {@code show...Dialog()} methods return that value.
* <p>
* The callback has two parameters:
* <ul>
* <li>{@code File[] selectedFiles} - one or more selected files
* <li>{@code ApproveContext context} - context object that provides additional methods
* </ul>
*
* <pre>{@code
* chooser.setApproveCallback( (selectedFiles, context) -> {
* // do something
* return SystemFileChooser.APPROVE_OPTION; // or SystemFileChooser.CANCEL_OPTION
* } );
* }</pre>
*
* or
*
* <pre>{@code
* chooser.setApproveCallback( this::approveCallback );
*
* ...
*
* private boolean approveCallback( File[] selectedFiles, ApproveContext context ) {
* // do something
* return SystemFileChooser.APPROVE_OPTION; // or SystemFileChooser.CANCEL_OPTION
* }
* }</pre>
*
* <b>WARNING:</b> Do not show a Swing dialog for the callback. This will not work!
* <p>
* Instead use {@link ApproveContext#showMessageDialog(int, String, String, int, String...)},
* which shows a modal system message dialog as child of the file dialog.
*
* <pre>{@code
* chooser.setApproveCallback( (selectedFiles, context) -> {
* if( !selectedFiles[0].getName().startsWith( "blabla" ) ) {
* context.showMessageDialog( JOptionPane.WARNING_MESSAGE,
* "File name must start with 'blabla' :)", null, 0 );
* return SystemFileChooser.CANCEL_OPTION;
* }
* return SystemFileChooser.APPROVE_OPTION;
* } );
* }</pre>
*
* @see ApproveContext
* @see JFileChooser#approveSelection()
*/
public void setApproveCallback( ApproveCallback approveCallback ) {
this.approveCallback = approveCallback;
}
private int showDialogImpl( Component parent ) { private int showDialogImpl( Component parent ) {
approveResult = APPROVE_OPTION;
File[] files = getProvider().showDialog( parent, this ); File[] files = getProvider().showDialog( parent, this );
setSelectedFiles( files ); setSelectedFiles( files );
return (files != null) ? APPROVE_OPTION : CANCEL_OPTION; return (files != null) ? approveResult : CANCEL_OPTION;
} }
private FileChooserProvider getProvider() { private FileChooserProvider getProvider() {
@@ -464,14 +530,31 @@ public class SystemFileChooser
return null; return null;
// convert file names to file objects // 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(); FileSystemView fsv = FileSystemView.getFileSystemView();
File[] files = new File[filenames.length]; File[] files = new File[filenames.length];
for( int i = 0; i < filenames.length; i++ ) for( int i = 0; i < filenames.length; i++ )
files[i] = fsv.createFileObject( filenames[i] ); files[i] = fsv.createFileObject( filenames[i] );
return files; return files;
} }
abstract String[] showSystemDialog( Window owner, SystemFileChooser fc );
} }
//---- class WindowsFileChooserProvider ----------------------------------- //---- class WindowsFileChooserProvider -----------------------------------
@@ -549,12 +632,50 @@ public class SystemFileChooser
} }
} }
// callback
FlatNativeWindowsLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null)
? (files, hwndFileDialog) -> {
return invokeApproveCallback( fc, files, new WindowsApproveContext( hwndFileDialog ) );
} : null;
// show system file dialog // show system file dialog
return FlatNativeWindowsLibrary.showFileChooser( owner, open, return FlatNativeWindowsLibrary.showFileChooser( owner, open,
fc.getDialogTitle(), approveButtonText, null, fileName, fc.getDialogTitle(), approveButtonText, null, fileName,
folder, saveAsItem, null, null, optionsSet, optionsClear, null, folder, saveAsItem, null, null, optionsSet, optionsClear, callback,
fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); 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 --------------------------------------- //---- class MacFileChooserProvider ---------------------------------------
@@ -613,12 +734,42 @@ public class SystemFileChooser
} }
} }
// callback
FlatNativeMacLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null)
? (files, hwndFileDialog) -> {
return invokeApproveCallback( fc, files, new MacApproveContext( hwndFileDialog ) );
} : null;
// show system file dialog // show system file dialog
return FlatNativeMacLibrary.showFileChooser( open, return FlatNativeMacLibrary.showFileChooser( open,
fc.getDialogTitle(), fc.getApproveButtonText(), null, null, null, fc.getDialogTitle(), fc.getApproveButtonText(), null, null, null,
nameFieldStringValue, directoryURL, optionsSet, optionsClear, null, nameFieldStringValue, directoryURL, optionsSet, optionsClear, callback,
fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); 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 ------------------------------------- //---- class LinuxFileChooserProvider -------------------------------------
@@ -690,10 +841,17 @@ public class SystemFileChooser
} }
} }
// callback
FlatNativeLinuxLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null)
? (files, hwndFileDialog) -> {
return invokeApproveCallback( fc, files, new LinuxApproveContext( hwndFileDialog ) );
} : null;
// show system file dialog // show system file dialog
return FlatNativeLinuxLibrary.showFileChooser( owner, open, return FlatNativeLinuxLibrary.showFileChooser( owner, open,
fc.getDialogTitle(), approveButtonText, currentName, currentFolder, fc.getDialogTitle(), approveButtonText, currentName, currentFolder,
optionsSet, optionsClear, null, fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); optionsSet, optionsClear, callback,
fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) );
} }
private String caseInsensitiveGlobPattern( String ext ) { private String caseInsensitiveGlobPattern( String ext ) {
@@ -712,6 +870,26 @@ public class SystemFileChooser
} }
return buf.toString(); 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 ------------------------------------- //---- class SwingFileChooserProvider -------------------------------------
@@ -727,6 +905,8 @@ public class SystemFileChooser
File[] files = isMultiSelectionEnabled() File[] files = isMultiSelectionEnabled()
? getSelectedFiles() ? getSelectedFiles()
: new File[] { getSelectedFile() }; : new File[] { getSelectedFile() };
if( files == null || files.length == 0 )
return; // should never happen
if( getDialogType() == OPEN_DIALOG || isDirectorySelectionEnabled() ) { if( getDialogType() == OPEN_DIALOG || isDirectorySelectionEnabled() ) {
if( !checkMustExist( this, files ) ) if( !checkMustExist( this, files ) )
@@ -735,6 +915,17 @@ public class SystemFileChooser
if( !checkOverwrite( this, files ) ) if( !checkOverwrite( this, files ) )
return; 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(); super.approveSelection();
} }
}; };
@@ -831,6 +1022,47 @@ public class SystemFileChooser
} }
return true; 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 --------------------------------------------------- //---- class FileFilter ---------------------------------------------------
@@ -892,4 +1124,41 @@ public class SystemFileChooser
return UIManager.getString( "FileChooser.acceptAllFileFilterText" ); return UIManager.getString( "FileChooser.acceptAllFileFilterText" );
} }
} }
//---- class ApproveCallback ----------------------------------------------
public interface ApproveCallback {
/**
* @param selectedFiles one or more selected files
* @param context context object that provides additional methods
* @return If the callback returns {@link #CANCEL_OPTION}, then the file dialog stays open.
* If it returns {@link #APPROVE_OPTION} (or any other value other than {@link #CANCEL_OPTION}),
* the file dialog is closed and the {@code show...Dialog()} methods return that value.
*/
int approve( File[] selectedFiles, ApproveContext context );
}
//---- class ApproveContext -----------------------------------------------
public static abstract class ApproveContext {
/**
* Shows a modal (operating system) message dialog as child of the system file chooser.
* <p>
* 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 );
}
} }

View File

@@ -73,6 +73,7 @@ import com.formdev.flatlaf.themes.FlatMacDarkLaf;
import com.formdev.flatlaf.themes.FlatMacLightLaf; import com.formdev.flatlaf.themes.FlatMacLightLaf;
import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.ui.FlatUIUtils;
import com.formdev.flatlaf.util.StringUtils; import com.formdev.flatlaf.util.StringUtils;
import com.formdev.flatlaf.util.SystemFileChooser;
import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.SystemInfo;
import com.formdev.flatlaf.util.UIScale; 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_RGB_COLORS = "showRgbColors";
private static final String KEY_SHOW_COLOR_LUMA = "showColorLuma"; private static final String KEY_SHOW_COLOR_LUMA = "showColorLuma";
private static final int NEW_PROPERTIES_FILE_OPTION = 100;
private File dir; private File dir;
private Preferences state; private Preferences state;
private boolean inLoadDirectory; private boolean inLoadDirectory;
@@ -227,48 +230,45 @@ class FlatThemeFileEditor
return; return;
// choose directory // choose directory
JFileChooser chooser = new JFileChooser( dir ) { SystemFileChooser chooser = new SystemFileChooser( dir );
@Override chooser.setFileSelectionMode( SystemFileChooser.DIRECTORIES_ONLY );
public void approveSelection() { chooser.setApproveCallback( this::checkDirectory );
if( !checkDirectory( this, getSelectedFile() ) ) int result = chooser.showOpenDialog( this );
return; if( result == SystemFileChooser.CANCEL_OPTION )
super.approveSelection();
}
};
chooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY );
if( chooser.showOpenDialog( this ) != JFileChooser.APPROVE_OPTION )
return; return;
File selectedFile = chooser.getSelectedFile(); File selectedFile = chooser.getSelectedFile();
if( selectedFile == null || selectedFile.equals( dir ) ) if( selectedFile == null || selectedFile.equals( dir ) )
return; return;
if( result == NEW_PROPERTIES_FILE_OPTION ) {
if( !newPropertiesFile( selectedFile ) )
return;
}
// open new directory // open new directory
loadDirectory( selectedFile ); loadDirectory( selectedFile );
} }
private boolean checkDirectory( Component parentComponent, File dir ) { private int checkDirectory( File[] selectedFiles, SystemFileChooser.ApproveContext context ) {
File dir = selectedFiles[0];
if( !dir.isDirectory() ) { if( !dir.isDirectory() ) {
JOptionPane.showMessageDialog( parentComponent, showMessageDialog( context, "Directory '" + dir + "' does not exist.", null );
"Directory '" + dir + "' does not exist.", return SystemFileChooser.CANCEL_OPTION;
getTitle(), JOptionPane.INFORMATION_MESSAGE );
return false;
} }
if( getPropertiesFiles( dir ).length == 0 ) { if( getPropertiesFiles( dir ).length == 0 ) {
UIManager.put( "OptionPane.sameSizeButtons", false ); UIManager.put( "OptionPane.sameSizeButtons", false );
int result = JOptionPane.showOptionDialog( parentComponent, int result = showMessageDialog( context,
"Directory '" + dir + "' does not contain properties files.\n\n" "Directory '" + dir + "' does not contain properties files.",
+ "Do you want create a new theme in this directory?\n\n" "Do you want create a new theme in this directory?\n\n"
+ "Or do you want modify/extend core themes and create empty" + "Or do you want modify/extend core themes and create empty"
+ " 'FlatLightLaf.properties' and 'FlatDarkLaf.properties' files in this directory?", + " 'FlatLightLaf.properties' and 'FlatDarkLaf.properties' files in this directory?",
getTitle(), JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, null, "_New Theme", "_Modify Core Themes", "_Cancel" );
new Object[] { "New Theme", "Modify Core Themes", "Cancel" }, null );
UIManager.put( "OptionPane.sameSizeButtons", null ); UIManager.put( "OptionPane.sameSizeButtons", null );
if( result == 0 ) if( result == 0 )
return newPropertiesFile( dir ); return NEW_PROPERTIES_FILE_OPTION;
else if( result == 1 ) { else if( result == 1 ) {
try { try {
String content = String content =
@@ -280,18 +280,37 @@ class FlatThemeFileEditor
"\n"; "\n";
writeFile( new File( dir, "FlatLightLaf.properties" ), content ); writeFile( new File( dir, "FlatLightLaf.properties" ), content );
writeFile( new File( dir, "FlatDarkLaf.properties" ), content ); writeFile( new File( dir, "FlatDarkLaf.properties" ), content );
return true; return SystemFileChooser.APPROVE_OPTION;
} catch( IOException ex ) { } catch( IOException ex ) {
ex.printStackTrace(); ex.printStackTrace();
JOptionPane.showMessageDialog( parentComponent, showMessageDialog( context,
"Failed to create 'FlatLightLaf.properties' or 'FlatDarkLaf.properties'." ); "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() { private void directoryChanged() {
@@ -302,7 +321,15 @@ class FlatThemeFileEditor
if( dir == null ) if( dir == null )
return; 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 ); loadDirectory( dir );
else { else {
// remove from directories history // remove from directories history
@@ -390,6 +417,9 @@ class FlatThemeFileEditor
File[] propertiesFiles = dir.listFiles( (d, name) -> { File[] propertiesFiles = dir.listFiles( (d, name) -> {
return name.endsWith( ".properties" ); return name.endsWith( ".properties" );
} ); } );
if( propertiesFiles == null )
propertiesFiles = new File[0];
Arrays.sort( propertiesFiles, (f1, f2) -> { Arrays.sort( propertiesFiles, (f1, f2) -> {
String n1 = toSortName( f1.getName() ); String n1 = toSortName( f1.getName() );
String n2 = toSortName( f2.getName() ); String n2 = toSortName( f2.getName() );