System File Chooser: added PatternFilter to support glob file filter on Windows and on Linux, but not on macOS (issue #1076)

This commit is contained in:
Karl Tauber
2026-01-13 17:22:50 +01:00
parent d0b01e4937
commit a30105c9ae
4 changed files with 185 additions and 5 deletions

View File

@@ -6,6 +6,8 @@ FlatLaf Change Log
- System File Chooser: - System File Chooser:
- Update current filter before invoking approve callback and after closing - Update current filter before invoking approve callback and after closing
dialog. (issue #1065) dialog. (issue #1065)
- Added `PatternFilter` to support glob file filter (e.g. `*.tar.gz`) on
Windows and on Linux, but not on macOS. (issue #1076)
- Fixed: System and Swing file dialogs were shown at the same time if - Fixed: System and Swing file dialogs were shown at the same time if
application has no other displayable window. (issue #1078) application has no other displayable window. (issue #1078)
- On Linux: Check whether required GSettings schemas are installed to avoid - On Linux: Check whether required GSettings schemas are installed to avoid

View File

@@ -115,7 +115,7 @@ public class FlatNativeMacLibrary
* @param fileTypes file types that the dialog can open or save. * @param fileTypes file types that the dialog can open or save.
* Two or more strings and {@code null} are required for each filter. * Two or more strings and {@code null} are required for each filter.
* First string is the display name of the filter shown in the combobox (e.g. "Text Files"). * First string is the display name of the filter shown in the combobox (e.g. "Text Files").
* Subsequent strings are the filter patterns (e.g. "txt" or "*"). * Subsequent strings are the file name extensions (e.g. "txt" or "*"; '.' is not supported).
* {@code null} is required to mark end of filter. * {@code null} is required to mark end of filter.
* @param retFileTypeIndex returns selected file type (zero-based); array must be have one element * @param retFileTypeIndex returns selected file type (zero-based); array must be have one element
* @return file path(s) that the user selected; an empty array if canceled; * @return file path(s) that the user selected; an empty array if canceled;

View File

@@ -36,6 +36,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import javax.swing.JDialog; import javax.swing.JDialog;
import javax.swing.JFileChooser; import javax.swing.JFileChooser;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
@@ -90,7 +91,8 @@ import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary;
* <li>{@link JFileChooser#FILES_AND_DIRECTORIES} is not supported. * <li>{@link JFileChooser#FILES_AND_DIRECTORIES} is not supported.
* <li>{@link #getSelectedFiles()} returns selected file also in single selection mode. * <li>{@link #getSelectedFiles()} returns selected file also in single selection mode.
* {@link JFileChooser#getSelectedFiles()} only in multi selection mode. * {@link JFileChooser#getSelectedFiles()} only in multi selection mode.
* <li>Only file name extension filters (see {@link FileNameExtensionFilter}) are supported. * <li>Only file name extension filters (see {@link FileNameExtensionFilter}) are supported on all platforms.
* <li>Pattern filters (see {@link PatternFilter}) are only supported on Windows and Linux, but not on macOS.
* <li>If adding choosable file filters and {@link #isAcceptAllFileFilterUsed()} is {@code true}, * <li>If adding choosable file filters and {@link #isAcceptAllFileFilterUsed()} is {@code true},
* then the <b>All Files</b> filter is placed at the end of the combobox list * then the <b>All Files</b> filter is placed at the end of the combobox list
* (as usual in current operating systems) and the first choosable filter is selected by default. * (as usual in current operating systems) and the first choosable filter is selected by default.
@@ -587,6 +589,7 @@ public class SystemFileChooser
private void checkSupportedFileFilter( FileFilter filter ) throws IllegalArgumentException { private void checkSupportedFileFilter( FileFilter filter ) throws IllegalArgumentException {
if( filter == null || if( filter == null ||
filter instanceof FileNameExtensionFilter || filter instanceof FileNameExtensionFilter ||
filter instanceof PatternFilter ||
filter instanceof AcceptAllFileFilter ) filter instanceof AcceptAllFileFilter )
return; return;
@@ -936,6 +939,10 @@ public class SystemFileChooser
fileTypes.add( filter.getDescription() ); fileTypes.add( filter.getDescription() );
fileTypes.add( "*." + String.join( ";*.", ((FileNameExtensionFilter)filter).getExtensions() ) ); fileTypes.add( "*." + String.join( ";*.", ((FileNameExtensionFilter)filter).getExtensions() ) );
fileTypeFilters.add( filter ); fileTypeFilters.add( filter );
} else if( filter instanceof PatternFilter ) {
fileTypes.add( filter.getDescription() );
fileTypes.add( String.join( ";", ((PatternFilter)filter).getPatterns() ) );
fileTypeFilters.add( filter );
} else if( filter instanceof AcceptAllFileFilter ) { } else if( filter instanceof AcceptAllFileFilter ) {
fileTypes.add( filter.getDescription() ); fileTypes.add( filter.getDescription() );
fileTypes.add( "*.*" ); fileTypes.add( "*.*" );
@@ -1013,6 +1020,26 @@ public class SystemFileChooser
private static class MacFileChooserProvider private static class MacFileChooserProvider
extends SystemFileChooserProvider extends SystemFileChooserProvider
{ {
@Override
public File[] showDialog( Window owner, SystemFileChooser fc ) {
// fallback to Swing file chooser if PatternFilter is used
boolean usesPatternFilter = (fc.getFileFilter() instanceof PatternFilter);
if( !usesPatternFilter ) {
for( FileFilter filter : fc.getChoosableFileFilters() ) {
if( filter instanceof PatternFilter ) {
usesPatternFilter = true;
break;
}
}
}
if( usesPatternFilter ) {
LoggingFacade.INSTANCE.logSevere( "FlatLaf: SystemFileChooser.PatternFilter is not supported on macOS. Using Swing JFileChooser.", null );
return new SwingFileChooserProvider().showDialog( owner, fc );
}
return super.showDialog( owner, fc );
}
@Override @Override
String[] showSystemDialog( Window owner, SystemFileChooser fc ) { String[] showSystemDialog( Window owner, SystemFileChooser fc ) {
int dark = FlatLaf.isLafDark() ? 1 : 0; int dark = FlatLaf.isLafDark() ? 1 : 0;
@@ -1202,9 +1229,14 @@ public class SystemFileChooser
if( filter instanceof FileNameExtensionFilter ) { if( filter instanceof FileNameExtensionFilter ) {
fileTypes.add( filter.getDescription() ); fileTypes.add( filter.getDescription() );
for( String ext : ((FileNameExtensionFilter)filter).getExtensions() ) for( String ext : ((FileNameExtensionFilter)filter).getExtensions() )
fileTypes.add( caseInsensitiveGlobPattern( ext ) ); fileTypes.add( "*." + caseInsensitiveGlobPattern( ext ) );
fileTypes.add( null ); fileTypes.add( null );
fileTypeFilters.add( filter ); fileTypeFilters.add( filter );
} else if( filter instanceof PatternFilter ) {
fileTypes.add( filter.getDescription() );
for( String pattern : ((PatternFilter)filter).getPatterns() )
fileTypes.add( caseInsensitiveGlobPattern( pattern ) );
fileTypeFilters.add( filter );
} else if( filter instanceof AcceptAllFileFilter ) { } else if( filter instanceof AcceptAllFileFilter ) {
fileTypes.add( filter.getDescription() ); fileTypes.add( filter.getDescription() );
fileTypes.add( "*" ); fileTypes.add( "*" );
@@ -1235,7 +1267,6 @@ public class SystemFileChooser
private String caseInsensitiveGlobPattern( String ext ) { private String caseInsensitiveGlobPattern( String ext ) {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
buf.append( "*." );
int len = ext.length(); int len = ext.length();
for( int i = 0; i < len; i++ ) { for( int i = 0; i < len; i++ ) {
char ch = ext.charAt( i ); char ch = ext.charAt( i );
@@ -1426,6 +1457,10 @@ public class SystemFileChooser
return new javax.swing.filechooser.FileNameExtensionFilter( return new javax.swing.filechooser.FileNameExtensionFilter(
((FileNameExtensionFilter)filter).getDescription(), ((FileNameExtensionFilter)filter).getDescription(),
((FileNameExtensionFilter)filter).getExtensions() ); ((FileNameExtensionFilter)filter).getExtensions() );
} else if( filter instanceof PatternFilter ) {
return new SwingGlobFilter(
((PatternFilter)filter).getDescription(),
((PatternFilter)filter).getPatterns() );
} else if( filter instanceof AcceptAllFileFilter ) } else if( filter instanceof AcceptAllFileFilter )
return chooser.getAcceptAllFileFilter(); return chooser.getAcceptAllFileFilter();
else else
@@ -1515,6 +1550,86 @@ public class SystemFileChooser
null, buttons, buttons[Math.min( Math.max( defaultButton, 0 ), buttons.length - 1 )] ); null, buttons, buttons[Math.min( Math.max( defaultButton, 0 ), buttons.length - 1 )] );
} }
} }
//---- class SwingGlobFilter ------------------------------------------
private static class SwingGlobFilter
extends javax.swing.filechooser.FileFilter
{
private final String description;
private final String[] patterns;
private Pattern regexPattern;
SwingGlobFilter( String description, String... patterns ) {
this.description = description;
this.patterns = patterns;
}
@Override
public String getDescription() {
return description;
}
@Override
public boolean accept( File f ) {
if( f == null )
return false;
if( f.isDirectory() )
return true;
initRegexPattern();
return regexPattern.matcher( f.getName() ).matches();
}
private void initRegexPattern() {
if( regexPattern != null )
return;
StringBuilder buf = new StringBuilder();
for( String pattern : patterns ) {
if( buf.length() > 0 )
buf.append( '|' );
glob2regexPattern( pattern, buf );
}
regexPattern = Pattern.compile( buf.toString(), Pattern.CASE_INSENSITIVE );
}
private static void glob2regexPattern( String globPattern, StringBuilder buf ) {
int globLength = globPattern.length();
// on windows, a pattern ending with "*.*" is equal to ending with "*"
if( SystemInfo.isWindows && globPattern.endsWith( "*.*" ) )
globLength -= 2;
for( int i = 0; i < globLength; i++ ) {
char ch = globPattern.charAt( i );
switch( ch ) {
// glob pattern
case '*': buf.append( ".*" ); break;
case '?': buf.append( '.' ); break;
// escape special regex characters
case '\\':
case '.':
case '+':
case '^':
case '$':
case '(':
case ')':
case '{':
case '}':
case '[':
case ']':
case '|':
buf.append( '\\' ).append( ch );
break;
default: buf.append( ch ); break;
}
}
}
}
} }
//---- class FileFilter --------------------------------------------------- //---- class FileFilter ---------------------------------------------------
@@ -1566,6 +1681,67 @@ public class SystemFileChooser
} }
} }
//---- class PatternFilter ------------------------------------------------
/**
* A case-insensitive file filter which accepts file patterns containing
* the wildcard characters {@code *?} on Windows and Linux.
* <ul>
* <li>{@code '*'} matches any sequence of characters.
* <li>{@code '?'} matches any single character.
* </ul>
* Sample filters: {@code *.tar.gz} or {@code *_copy.txt}
* <p>
* <b>Warning</b>: This filter is <b>not supported on macOS</b>.
* If used on macOS, the Swing file chooser {@link JFileChooser} is shown
* (instead of macOS file dialog) and a warning is logged.
* To avoid this, do not use this filter on macOS.
* <p>
* E.g.:
* <pre>{@code
* if( SystemInfo.isMacOS )
* chooser.addChoosableFileFilter( new FileNameExtensionFilter( "Compressed TAR", "tgz" ) );
* else
* chooser.addChoosableFileFilter( new PatternFilter( "Compressed TAR", "*.tar.gz" ) );
* } );
* }</pre>
*
* @see FileNameExtensionFilter
* @since 3.7.1
*/
public static final class PatternFilter
extends FileFilter
{
private final String description;
private final String[] patterns;
public PatternFilter( String description, String... patterns ) {
if( patterns == null || patterns.length == 0 )
throw new IllegalArgumentException( "Missing patterns" );
for( String extension : patterns ) {
if( extension == null || extension.isEmpty() )
throw new IllegalArgumentException( "Pattern is null or empty string" );
}
this.description = description;
this.patterns = patterns.clone();
}
@Override
public String getDescription() {
return description;
}
public String[] getPatterns() {
return patterns.clone();
}
@Override
public String toString() {
return super.toString() + "[description=" + description + " patterns=" + Arrays.toString( patterns ) + "]";
}
}
//---- class AcceptAllFileFilter ------------------------------------------ //---- class AcceptAllFileFilter ------------------------------------------
private static final class AcceptAllFileFilter private static final class AcceptAllFileFilter

View File

@@ -234,7 +234,9 @@ public class FlatSystemFileChooserTest
for( int i = 0; i < fileTypes.length; i += 2 ) { for( int i = 0; i < fileTypes.length; i += 2 ) {
fc.addChoosableFileFilter( "*".equals( fileTypes[i+1] ) fc.addChoosableFileFilter( "*".equals( fileTypes[i+1] )
? fc.getAcceptAllFileFilter() ? fc.getAcceptAllFileFilter()
: new SystemFileChooser.FileNameExtensionFilter( fileTypes[i], fileTypes[i+1].split( ";" ) ) ); : ((fileTypes[i+1].indexOf( '*' ) >= 0 || fileTypes[i+1].indexOf( '?' ) >= 0)
? new SystemFileChooser.PatternFilter( fileTypes[i], fileTypes[i+1].split( ";" ) )
: new SystemFileChooser.FileNameExtensionFilter( fileTypes[i], fileTypes[i+1].split( ";" ) )) );
} }
SystemFileChooser.FileFilter[] filters = fc.getChoosableFileFilters(); SystemFileChooser.FileFilter[] filters = fc.getChoosableFileFilters();
if( filters.length > 0 ) if( filters.length > 0 )