Compare commits

..

4 Commits

26 changed files with 551 additions and 244 deletions

View File

@@ -72,39 +72,19 @@ jobs:
# tar.exe: Couldn't open ~/.gradle/caches/modules-2/modules-2.lock: Permission denied # tar.exe: Couldn't open ~/.gradle/caches/modules-2/modules-2.lock: Permission denied
run: ./gradlew build-natives --no-daemon run: ./gradlew build-natives --no-daemon
- name: Upload unsigned Windows DLLs for signing by SignPath.org - name: Sign Windows DLLs
if: matrix.os == 'windows-latest' && github.repository == 'JFormDesigner/FlatLaf' if: false
id: windows-unsigned # if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4 uses: skymatic/code-sign-action@v3
with: with:
name: FlatLaf-natives-windows-unsigned certificate: '${{ secrets.CODE_SIGN_CERT_BASE64 }}'
path: flatlaf-natives/flatlaf-natives-windows/build/lib/main/release/**/*.dll password: '${{ secrets.CODE_SIGN_CERT_PASSWORD }}'
certificatesha1: '${{ secrets.CODE_SIGN_CERT_SHA1 }}'
- name: Sign Windows DLLs using SignPath.org folder: 'flatlaf-core/src/main/resources/com/formdev/flatlaf/natives'
if: matrix.os == 'windows-latest' && github.repository == 'JFormDesigner/FlatLaf'
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: FlatLaf
signing-policy-slug: release-signing
artifact-configuration-slug: windows-dlls
github-artifact-id: ${{ steps.windows-unsigned.outputs.artifact-id }}
wait-for-completion: true
output-artifact-directory: flatlaf-natives/flatlaf-natives-windows/build/lib/signed
- name: Copy signed Windows DLLs to flatlaf-core
if: matrix.os == 'windows-latest' && github.repository == 'JFormDesigner/FlatLaf'
shell: bash
run: |
SRC=flatlaf-natives/flatlaf-natives-windows/build/lib/signed
DEST=flatlaf-core/src/main/resources/com/formdev/flatlaf/natives
cp $SRC/aarch64/flatlaf-natives-windows.dll $DEST/flatlaf-windows-arm64.dll
cp $SRC/x86/flatlaf-natives-windows.dll $DEST/flatlaf-windows-x86.dll
cp $SRC/x86-64/flatlaf-natives-windows.dll $DEST/flatlaf-windows-x86_64.dll
- name: Sign macOS natives - name: Sign macOS natives
if: matrix.os == 'DISABLED--macos-latest' if: false
# if: matrix.os == 'DISABLED--macos-latest'
env: env:
CERT_BASE64: ${{ secrets.CODE_SIGN_CERT_BASE64 }} CERT_BASE64: ${{ secrets.CODE_SIGN_CERT_BASE64 }}
CERT_PASSWORD: ${{ secrets.CODE_SIGN_CERT_PASSWORD }} CERT_PASSWORD: ${{ secrets.CODE_SIGN_CERT_PASSWORD }}
@@ -134,7 +114,7 @@ jobs:
# cleanup # cleanup
security delete-keychain $KEYCHAIN_PATH security delete-keychain $KEYCHAIN_PATH
- name: Set artifacts pattern for upload step - name: Set artifacts pattern
shell: bash shell: bash
run: | run: |
case ${{ matrix.os }} in case ${{ matrix.os }} in

View File

@@ -3,19 +3,19 @@ FlatLaf Change Log
## 3.7.1-SNAPSHOT ## 3.7.1-SNAPSHOT
- System File Chooser:
- Update current filter before invoking approve callback and after closing
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
application has no other displayable window. (issue #1078)
- On Linux: Check whether required GSettings schemas are installed to avoid
application crash (occurred on NixOS with Plasma/KDE desktop). (issue #1069)
- ComboBox: Added UI property `ComboBox.buttonFocusedEditableBackground`. (issue - ComboBox: Added UI property `ComboBox.buttonFocusedEditableBackground`. (issue
#1068) #1068)
- Popup: Fixed scrolling popup painting issue on Windows 10 when a glass pane is - Popup: Fixed scrolling popup painting issue on Windows 10 when a glass pane is
visible and frame is maximized. (issue #1071) visible and frame is maximized. (issue #1071)
- Slider: Styling `thumbSize` or `focusWidth` did not update slider size/layout.
(PR #1074)
- ToolBar: Grip disappeared when switching between Look and Feels. (issue #1075)
- Extras:
- UI defaults inspector: Fixed NPE if color of `FlatLineBorder` is null. Also
use `FlatLineBorder` line color as cell background color in "Value" column.
(PR #1080)
- `FlatDesktop`: Avoid unnecessary logging if desktop is not supported (e.g.
on NixOS with Plasma/KDE desktop).
## 3.7 ## 3.7

View File

@@ -76,19 +76,10 @@ Otherwise, download `flatlaf-<version>.jar` here:
[![Maven Central](https://img.shields.io/maven-central/v/com.formdev/flatlaf?style=flat-square)](https://central.sonatype.com/artifact/com.formdev/flatlaf) [![Maven Central](https://img.shields.io/maven-central/v/com.formdev/flatlaf?style=flat-square)](https://central.sonatype.com/artifact/com.formdev/flatlaf)
- See See also
[Native Libraries distribution](https://www.formdev.com/flatlaf/native-libraries/) [Native Libraries distribution](https://www.formdev.com/flatlaf/native-libraries/)
for instructions on how to redistribute FlatLaf native libraries with your for instructions on how to redistribute FlatLaf native libraries with your
application. application.
- Windows DLLs: Free code signing provided by
[SignPath.io](https://about.signpath.io/), certificate by
[SignPath Foundation](https://signpath.org/).
- If repackaging FlatLaf (and other) JARs into a single fat/uber JAR:
- add `Multi-Release: true` to `META-INF/MANIFEST.MF`
- keep `META-INF/versions/` and `META-INF/services/` directories
- merge content of equally named files in `META-INF/services/`
- If using obfuscation/minimizing/shrinking tools (e.g. **ProGuard** or
**Shadow**), exclude package `com.formdev.flatlaf` and all sub-packages.
### Snapshots ### Snapshots

View File

@@ -37,7 +37,7 @@ import com.formdev.flatlaf.util.SystemInfo;
*/ */
public class FlatNativeLinuxLibrary public class FlatNativeLinuxLibrary
{ {
private static int API_VERSION_LINUX = 3003; private static int API_VERSION_LINUX = 3004;
/** /**
* Checks whether native library is loaded/available. * Checks whether native library is loaded/available.
@@ -186,8 +186,8 @@ public class FlatNativeLinuxLibrary
* Use '__' for '_' character (e.g. "Choose__and__Quit"). * Use '__' for '_' character (e.g. "Choose__and__Quit").
* @param currentName user-editable filename currently shown in the filename field in save dialog; or {@code null} * @param currentName user-editable filename currently shown in the filename field in save dialog; or {@code null}
* @param currentFolder current directory shown in the dialog; or {@code null} * @param currentFolder current directory shown in the dialog; or {@code null}
* @param optionsSet options to set; see {@code FOS_*} constants * @param optionsSet options to set; see {@code FC_*} constants
* @param optionsClear options to clear; see {@code FOS_*} constants * @param optionsClear options to clear; see {@code FC_*} constants
* @param callback approve callback; or {@code null} * @param callback approve callback; or {@code null}
* @param fileTypeIndex the file type that appears as selected (zero-based) * @param fileTypeIndex the file type that appears as selected (zero-based)
* @param fileTypes file types that the dialog can open or save. * @param fileTypes file types that the dialog can open or save.
@@ -195,19 +195,20 @@ public class FlatNativeLinuxLibrary
* 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 filter patterns (e.g. "*.txt" or "*").
* {@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
* @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;
* or {@code null} on failures (no dialog shown) * or {@code null} on failures (no dialog shown)
* *
* @since 3.7 * @since 3.7.1
*/ */
public native static String[] showFileChooser( Window owner, int dark, boolean open, public native static String[] showFileChooser( Window owner, int dark, boolean open,
String title, String okButtonLabel, String currentName, String currentFolder, String title, String okButtonLabel, String currentName, String currentFolder,
int optionsSet, int optionsClear, FileChooserCallback callback, int optionsSet, int optionsClear, FileChooserCallback callback,
int fileTypeIndex, String... fileTypes ); int fileTypeIndex, String[] fileTypes, int[] retFileTypeIndex );
/** @since 3.7 */ /** @since 3.7.1 */
public interface FileChooserCallback { public interface FileChooserCallback {
boolean approve( String[] files, long hwndFileDialog ); boolean approve( String[] files, int fileTypeIndex, long hwndFileDialog );
} }
/** /**

View File

@@ -45,7 +45,7 @@ import com.formdev.flatlaf.util.SystemInfo;
*/ */
public class FlatNativeMacLibrary public class FlatNativeMacLibrary
{ {
private static int API_VERSION_MACOS = 2002; private static int API_VERSION_MACOS = 2003;
/** /**
* Checks whether native library is loaded/available. * Checks whether native library is loaded/available.
@@ -115,22 +115,23 @@ 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
* @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;
* or {@code null} on failures (no dialog shown) * or {@code null} on failures (no dialog shown)
* *
* @since 3.7 * @since 3.7.1
*/ */
public native static String[] showFileChooser( Window owner, int dark, boolean open, public native static String[] showFileChooser( Window owner, int dark, boolean open,
String title, String prompt, String message, String filterFieldLabel, String title, String prompt, String message, String filterFieldLabel,
String nameFieldLabel, String nameFieldStringValue, String directoryURL, String nameFieldLabel, String nameFieldStringValue, String directoryURL,
int optionsSet, int optionsClear, FileChooserCallback callback, int optionsSet, int optionsClear, FileChooserCallback callback,
int fileTypeIndex, String... fileTypes ); int fileTypeIndex, String[] fileTypes, int[] retFileTypeIndex );
/** @since 3.7 */ /** @since 3.7.1 */
public interface FileChooserCallback { public interface FileChooserCallback {
boolean approve( String[] files, long hwndFileDialog ); boolean approve( String[] files, int fileTypeIndex, long hwndFileDialog );
} }
/** /**

View File

@@ -31,7 +31,7 @@ import com.formdev.flatlaf.util.SystemInfo;
*/ */
public class FlatNativeWindowsLibrary public class FlatNativeWindowsLibrary
{ {
private static int API_VERSION_WINDOWS = 1002; private static int API_VERSION_WINDOWS = 1003;
private static long osBuildNumber = Long.MIN_VALUE; private static long osBuildNumber = Long.MIN_VALUE;
@@ -226,20 +226,21 @@ public class FlatNativeWindowsLibrary
* Pairs of strings are required for each filter. * Pairs of strings are required for each filter.
* First string is the display name of the filter shown in the combobox (e.g. "Text Files"). * First string is the display name of the filter shown in the combobox (e.g. "Text Files").
* Second string is the filter pattern (e.g. "*.txt", "*.exe;*.dll" or "*.*"). * Second string is the filter pattern (e.g. "*.txt", "*.exe;*.dll" or "*.*").
* @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;
* or {@code null} on failures (no dialog shown) * or {@code null} on failures (no dialog shown)
* *
* @since 3.7 * @since 3.7.1
*/ */
public native static String[] showFileChooser( Window owner, boolean open, public native static String[] showFileChooser( Window owner, boolean open,
String title, String okButtonLabel, String fileNameLabel, String fileName, String title, String okButtonLabel, String fileNameLabel, String fileName,
String folder, String saveAsItem, String defaultFolder, String defaultExtension, String folder, String saveAsItem, String defaultFolder, String defaultExtension,
int optionsSet, int optionsClear, FileChooserCallback callback, int optionsSet, int optionsClear, FileChooserCallback callback,
int fileTypeIndex, String... fileTypes ); int fileTypeIndex, String[] fileTypes, int[] retFileTypeIndex );
/** @since 3.7 */ /** @since 3.7.1 */
public interface FileChooserCallback { public interface FileChooserCallback {
boolean approve( String[] files, long hwndFileDialog ); boolean approve( String[] files, int fileTypeIndex, long hwndFileDialog );
} }
/** /**

View File

@@ -227,13 +227,7 @@ public class FlatSliderUI
/** @since 2 */ /** @since 2 */
protected void applyStyle( Object style ) { protected void applyStyle( Object style ) {
Dimension oldThumbSize = thumbSize;
int oldFocusWidth = focusWidth;
oldStyleValues = FlatStylingSupport.parseAndApply( oldStyleValues, style, this::applyStyleProperty ); oldStyleValues = FlatStylingSupport.parseAndApply( oldStyleValues, style, this::applyStyleProperty );
if( !thumbSize.equals( oldThumbSize ) || focusWidth != oldFocusWidth )
calculateGeometry();
} }
/** @since 2 */ /** @since 2 */

View File

@@ -34,6 +34,7 @@ import java.awt.event.ComponentListener;
import java.awt.event.FocusEvent; import java.awt.event.FocusEvent;
import java.awt.event.FocusListener; import java.awt.event.FocusListener;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.util.Map; import java.util.Map;
import javax.swing.Action; import javax.swing.Action;
@@ -65,7 +66,6 @@ import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.icons.FlatCheckBoxIcon; import com.formdev.flatlaf.icons.FlatCheckBoxIcon;
import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable;
import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI;
import com.formdev.flatlaf.ui.FlatUIUtils.FlatPropertyWatcher;
import com.formdev.flatlaf.util.Graphics2DProxy; import com.formdev.flatlaf.util.Graphics2DProxy;
import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.HiDPIUtils;
import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.LoggingFacade;
@@ -189,26 +189,29 @@ public class FlatTableUI
if( rowHeight > 0 ) if( rowHeight > 0 )
LookAndFeel.installProperty( table, "rowHeight", UIScale.scale( rowHeight ) ); LookAndFeel.installProperty( table, "rowHeight", UIScale.scale( rowHeight ) );
if( !showHorizontalLines ) { FlatTablePropertyWatcher watcher = FlatTablePropertyWatcher.get( table );
FlatPropertyWatcher.runIfNotChanged( table, "showHorizontalLines", () -> { if( watcher != null )
oldShowHorizontalLines = table.getShowHorizontalLines(); watcher.enabled = false;
table.setShowHorizontalLines( false );
} ); if( !showHorizontalLines && (watcher == null || !watcher.showHorizontalLinesChanged) ) {
oldShowHorizontalLines = table.getShowHorizontalLines();
table.setShowHorizontalLines( false );
} }
if( !showVerticalLines ) { if( !showVerticalLines && (watcher == null || !watcher.showVerticalLinesChanged) ) {
FlatPropertyWatcher.runIfNotChanged( table, "showVerticalLines", () -> { oldShowVerticalLines = table.getShowVerticalLines();
oldShowVerticalLines = table.getShowVerticalLines(); table.setShowVerticalLines( false );
table.setShowVerticalLines( false );
} );
} }
if( intercellSpacing != null ) { if( intercellSpacing != null && (watcher == null || !watcher.intercellSpacingChanged) ) {
FlatPropertyWatcher.runIfNotChanged( table, "rowMargin", () -> { oldIntercellSpacing = table.getIntercellSpacing();
oldIntercellSpacing = table.getIntercellSpacing(); table.setIntercellSpacing( intercellSpacing );
table.setIntercellSpacing( intercellSpacing );
} );
} }
if( watcher != null )
watcher.enabled = true;
else
table.addPropertyChangeListener( new FlatTablePropertyWatcher() );
// install boolean renderer // install boolean renderer
oldBooleanRenderer = table.getDefaultRenderer( Boolean.class ); oldBooleanRenderer = table.getDefaultRenderer( Boolean.class );
if( oldBooleanRenderer instanceof UIResource ) if( oldBooleanRenderer instanceof UIResource )
@@ -228,24 +231,25 @@ public class FlatTableUI
oldStyleValues = null; oldStyleValues = null;
FlatTablePropertyWatcher watcher = FlatTablePropertyWatcher.get( table );
if( watcher != null )
watcher.enabled = false;
// restore old show horizontal/vertical lines (if not modified) // restore old show horizontal/vertical lines (if not modified)
if( !showHorizontalLines && oldShowHorizontalLines && !table.getShowHorizontalLines() ) { if( !showHorizontalLines && oldShowHorizontalLines && !table.getShowHorizontalLines() &&
FlatPropertyWatcher.runIfNotChanged( table, "showHorizontalLines", () -> { (watcher == null || !watcher.showHorizontalLinesChanged) )
table.setShowHorizontalLines( true ); table.setShowHorizontalLines( true );
} ); if( !showVerticalLines && oldShowVerticalLines && !table.getShowVerticalLines() &&
} (watcher == null || !watcher.showVerticalLinesChanged) )
if( !showVerticalLines && oldShowVerticalLines && !table.getShowVerticalLines() ) { table.setShowVerticalLines( true );
FlatPropertyWatcher.runIfNotChanged( table, "showVerticalLines", () -> {
table.setShowVerticalLines( true );
} );
}
// restore old intercell spacing (if not modified) // restore old intercell spacing (if not modified)
if( intercellSpacing != null && table.getIntercellSpacing().equals( intercellSpacing ) ) { if( intercellSpacing != null && table.getIntercellSpacing().equals( intercellSpacing ) &&
FlatPropertyWatcher.runIfNotChanged( table, "rowMargin", () -> { (watcher == null || !watcher.intercellSpacingChanged) )
table.setIntercellSpacing( oldIntercellSpacing ); table.setIntercellSpacing( oldIntercellSpacing );
} );
} if( watcher != null )
watcher.enabled = true;
// uninstall boolean renderer // uninstall boolean renderer
if( table.getDefaultRenderer( Boolean.class ) instanceof FlatBooleanRenderer ) { if( table.getDefaultRenderer( Boolean.class ) instanceof FlatBooleanRenderer ) {
@@ -934,6 +938,48 @@ public class FlatTableUI
} }
} }
//---- class FlatTablePropertyWatcher -------------------------------------
/**
* Listener that watches for change of some table properties from application code.
* This information is used in {@link FlatTableUI#installDefaults()} and
* {@link FlatTableUI#uninstallDefaults()} to decide whether FlatLaf modifies those properties.
* If they are modified in application code, FlatLaf no longer changes them.
*
* The listener is added once for each table, but never removed.
* So switching Laf/theme reuses existing listener.
*/
private static class FlatTablePropertyWatcher
implements PropertyChangeListener
{
boolean enabled = true;
boolean showHorizontalLinesChanged;
boolean showVerticalLinesChanged;
boolean intercellSpacingChanged;
static FlatTablePropertyWatcher get( JTable table ) {
for( PropertyChangeListener l : table.getPropertyChangeListeners() ) {
if( l instanceof FlatTablePropertyWatcher )
return (FlatTablePropertyWatcher) l;
}
return null;
}
//---- interface PropertyChangeListener ----
@Override
public void propertyChange( PropertyChangeEvent e ) {
if( !enabled )
return;
switch( e.getPropertyName() ) {
case "showHorizontalLines": showHorizontalLinesChanged = true; break;
case "showVerticalLines": showVerticalLinesChanged = true; break;
case "rowMargin": intercellSpacingChanged = true; break;
}
}
}
//---- class FlatBooleanRenderer ------------------------------------------ //---- class FlatBooleanRenderer ------------------------------------------
private static class FlatBooleanRenderer private static class FlatBooleanRenderer

View File

@@ -47,7 +47,6 @@ import javax.swing.plaf.basic.BasicToolBarUI;
import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable;
import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI;
import com.formdev.flatlaf.ui.FlatUIUtils.FlatPropertyWatcher;
import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.HiDPIUtils;
import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.LoggingFacade;
import com.formdev.flatlaf.util.UIScale; import com.formdev.flatlaf.util.UIScale;
@@ -145,13 +144,11 @@ public class FlatToolBarUI
hoverButtonGroupBackground = UIManager.getColor( "ToolBar.hoverButtonGroupBackground" ); hoverButtonGroupBackground = UIManager.getColor( "ToolBar.hoverButtonGroupBackground" );
// floatable // floatable
oldFloatable = null;
if( !UIManager.getBoolean( "ToolBar.floatable" ) ) { if( !UIManager.getBoolean( "ToolBar.floatable" ) ) {
FlatPropertyWatcher.runIfNotChanged( toolBar, "floatable", () -> { oldFloatable = toolBar.isFloatable();
oldFloatable = toolBar.isFloatable(); toolBar.setFloatable( false );
toolBar.setFloatable( false ); } else
} ); oldFloatable = null;
}
} }
@Override @Override
@@ -161,9 +158,7 @@ public class FlatToolBarUI
hoverButtonGroupBackground = null; hoverButtonGroupBackground = null;
if( oldFloatable != null ) { if( oldFloatable != null ) {
FlatPropertyWatcher.runIfNotChanged( toolBar, "floatable", () -> { toolBar.setFloatable( oldFloatable );
toolBar.setFloatable( oldFloatable );
} );
oldFloatable = null; oldFloatable = null;
} }
} }

View File

@@ -44,8 +44,6 @@ import java.awt.geom.Path2D;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D; import java.awt.geom.RoundRectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import java.util.function.Predicate; import java.util.function.Predicate;
@@ -1415,47 +1413,4 @@ debug*/
return delegate.isBorderOpaque(); return delegate.isBorderOpaque();
} }
} }
//---- class FlatPropertyWatcher ------------------------------------------
/**
* Listener that watches for change of a property from application code.
* This information can be used to decide whether FlatLaf modifies the property.
* If it is modified in application code, FlatLaf no longer changes it.
* <p>
* The listener is added once for a property, but never removed.
* So switching Laf/theme reuses existing listener.
*/
static class FlatPropertyWatcher
implements PropertyChangeListener
{
private boolean changed;
static void runIfNotChanged( JComponent c, String propName, Runnable runnable ) {
FlatPropertyWatcher watcher = getOrInstall( c, propName );
if( watcher.changed )
return;
runnable.run();
watcher.changed = false;
}
private static FlatPropertyWatcher getOrInstall( JComponent c, String propName ) {
for( PropertyChangeListener l : c.getPropertyChangeListeners( propName ) ) {
if( l instanceof FlatPropertyWatcher )
return (FlatPropertyWatcher) l;
}
FlatPropertyWatcher watcher = new FlatPropertyWatcher();
c.addPropertyChangeListener( propName, watcher );
return watcher;
}
//---- interface PropertyChangeListener ----
@Override
public void propertyChange( PropertyChangeEvent e ) {
changed = true;
}
}
} }

View File

@@ -18,6 +18,8 @@ package com.formdev.flatlaf.util;
import java.awt.Component; import java.awt.Component;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.KeyboardFocusManager; import java.awt.KeyboardFocusManager;
import java.awt.SecondaryLoop; import java.awt.SecondaryLoop;
import java.awt.Toolkit; import java.awt.Toolkit;
@@ -28,11 +30,13 @@ import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; 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;
@@ -87,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.
@@ -584,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;
@@ -607,6 +613,11 @@ public class SystemFileChooser
return filters2; return filters2;
} }
private void updateFileFilter( List<FileFilter> filters, int index ) {
if( index >= 0 && index < filters.size() )
setFileFilter( filters.get( index ) );
}
public ApproveCallback getApproveCallback() { public ApproveCallback getApproveCallback() {
return approveCallback; return approveCallback;
} }
@@ -737,6 +748,9 @@ public class SystemFileChooser
} }
private int showDialogImpl( Component parent ) { private int showDialogImpl( Component parent ) {
if( !EventQueue.isDispatchThread() )
throw new IllegalStateException( "Must be invoked from the AWT/Swing event dispatch thread" );
Window owner = (parent instanceof Window) Window owner = (parent instanceof Window)
? (Window) parent ? (Window) parent
: (parent != null) ? SwingUtilities.windowForComponent( parent ) : null; : (parent != null) ? SwingUtilities.windowForComponent( parent ) : null;
@@ -785,6 +799,16 @@ public class SystemFileChooser
{ {
@Override @Override
public File[] showDialog( Window owner, SystemFileChooser fc ) { public File[] showDialog( Window owner, SystemFileChooser fc ) {
// if there is no displayable window, then AWT's auto-shutdown feature
// quits our secondary event loop (see below) immediately
// https://docs.oracle.com/en/java/javase/25/docs/api/java.desktop/java/awt/doc-files/AWTThreadIssues.html#Autoshutdown
Window dummyWindow = null;
if( !hasDisplayableWindow( owner ) ) {
// create a (not visible) displayable window to avoid AWT auto-shutdown
dummyWindow = new Window( (Frame) null );
dummyWindow.addNotify();
}
AtomicReference<String[]> filenamesRef = new AtomicReference<>(); AtomicReference<String[]> filenamesRef = new AtomicReference<>();
// create secondary event look and invoke system file dialog on a new thread // create secondary event look and invoke system file dialog on a new thread
@@ -795,6 +819,10 @@ public class SystemFileChooser
}, "FlatLaf SystemFileChooser" ).start(); }, "FlatLaf SystemFileChooser" ).start();
secondaryLoop.enter(); secondaryLoop.enter();
// dispose dummy window to allow AWT to auto-shutdown
if( dummyWindow != null )
dummyWindow.dispose();
String[] filenames = filenamesRef.get(); String[] filenames = filenamesRef.get();
// fallback to Swing file chooser if system file dialog failed or is not available // fallback to Swing file chooser if system file dialog failed or is not available
@@ -831,6 +859,17 @@ public class SystemFileChooser
files[i] = fsv.createFileObject( filenames[i] ); files[i] = fsv.createFileObject( filenames[i] );
return files; return files;
} }
private static boolean hasDisplayableWindow( Window owner ) {
if( owner != null && owner.isDisplayable() )
return true;
for( Window window : Window.getWindows() ) {
if( window.isDisplayable() )
return true;
}
return false;
}
} }
//---- class WindowsFileChooserProvider ----------------------------------- //---- class WindowsFileChooserProvider -----------------------------------
@@ -890,6 +929,7 @@ public class SystemFileChooser
// filter // filter
int fileTypeIndex = 0; int fileTypeIndex = 0;
ArrayList<String> fileTypes = new ArrayList<>(); ArrayList<String> fileTypes = new ArrayList<>();
ArrayList<FileFilter> fileTypeFilters = new ArrayList<>();
if( !fc.isDirectorySelectionEnabled() ) { if( !fc.isDirectorySelectionEnabled() ) {
List<FileFilter> filters = fc.getFiltersForDialog(); List<FileFilter> filters = fc.getFiltersForDialog();
if( !filters.isEmpty() ) { if( !filters.isEmpty() ) {
@@ -898,9 +938,15 @@ public class SystemFileChooser
if( filter instanceof FileNameExtensionFilter ) { if( filter instanceof FileNameExtensionFilter ) {
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 );
} 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( "*.*" );
fileTypeFilters.add( filter );
} }
} }
} }
@@ -916,19 +962,24 @@ public class SystemFileChooser
// callback // callback
FlatNativeWindowsLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null) FlatNativeWindowsLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null)
? (files, hwndFileDialog) -> { ? (files, fileTypeIndex2, hwndFileDialog) -> {
fc.updateFileFilter( fileTypeFilters, fileTypeIndex2 );
return invokeApproveCallback( fc, files, new WindowsApproveContext( hwndFileDialog ) ); return invokeApproveCallback( fc, files, new WindowsApproveContext( hwndFileDialog ) );
} : null; } : null;
// show system file dialog // show system file dialog
return FlatNativeWindowsLibrary.showFileChooser( owner, open, int[] retFileTypeIndex = { -1 };
String[] result = FlatNativeWindowsLibrary.showFileChooser( owner, open,
fc.getDialogTitle(), approveButtonText, fc.getDialogTitle(), approveButtonText,
fc.getPlatformProperty( WINDOWS_FILE_NAME_LABEL ), fc.getPlatformProperty( WINDOWS_FILE_NAME_LABEL ),
fileName, folder, saveAsItem, fileName, folder, saveAsItem,
fc.getPlatformProperty( WINDOWS_DEFAULT_FOLDER ), fc.getPlatformProperty( WINDOWS_DEFAULT_FOLDER ),
fc.getPlatformProperty( WINDOWS_DEFAULT_EXTENSION ), fc.getPlatformProperty( WINDOWS_DEFAULT_EXTENSION ),
optionsSet, optionsClear, callback, optionsSet, optionsClear, callback,
fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ), retFileTypeIndex );
if( result != null )
fc.updateFileFilter( fileTypeFilters, retFileTypeIndex[0] );
return result;
} }
//---- class WindowsApproveContext ---- //---- class WindowsApproveContext ----
@@ -969,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;
@@ -1013,6 +1084,7 @@ public class SystemFileChooser
// filter // filter
int fileTypeIndex = 0; int fileTypeIndex = 0;
ArrayList<String> fileTypes = new ArrayList<>(); ArrayList<String> fileTypes = new ArrayList<>();
ArrayList<FileFilter> fileTypeFilters = new ArrayList<>();
if( !fc.isDirectorySelectionEnabled() ) { if( !fc.isDirectorySelectionEnabled() ) {
List<FileFilter> filters = fc.getFiltersForDialog(); List<FileFilter> filters = fc.getFiltersForDialog();
if( !filters.isEmpty() ) { if( !filters.isEmpty() ) {
@@ -1023,10 +1095,12 @@ public class SystemFileChooser
for( String ext : ((FileNameExtensionFilter)filter).getExtensions() ) for( String ext : ((FileNameExtensionFilter)filter).getExtensions() )
fileTypes.add( ext ); fileTypes.add( ext );
fileTypes.add( null ); fileTypes.add( null );
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( "*" );
fileTypes.add( null ); fileTypes.add( null );
fileTypeFilters.add( filter );
} }
} }
} }
@@ -1034,18 +1108,23 @@ public class SystemFileChooser
// callback // callback
FlatNativeMacLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null) FlatNativeMacLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null)
? (files, hwndFileDialog) -> { ? (files, fileTypeIndex2, hwndFileDialog) -> {
fc.updateFileFilter( fileTypeFilters, fileTypeIndex2 );
return invokeApproveCallback( fc, files, new MacApproveContext( hwndFileDialog ) ); return invokeApproveCallback( fc, files, new MacApproveContext( hwndFileDialog ) );
} : null; } : null;
// show system file dialog // show system file dialog
return FlatNativeMacLibrary.showFileChooser( owner, dark, open, int[] retFileTypeIndex = { -1 };
String[] result = FlatNativeMacLibrary.showFileChooser( owner, dark, open,
fc.getDialogTitle(), fc.getApproveButtonText(), fc.getDialogTitle(), fc.getApproveButtonText(),
fc.getPlatformProperty( MAC_MESSAGE ), fc.getPlatformProperty( MAC_MESSAGE ),
fc.getPlatformProperty( MAC_FILTER_FIELD_LABEL ), fc.getPlatformProperty( MAC_FILTER_FIELD_LABEL ),
fc.getPlatformProperty( MAC_NAME_FIELD_LABEL ), fc.getPlatformProperty( MAC_NAME_FIELD_LABEL ),
nameFieldStringValue, directoryURL, optionsSet, optionsClear, callback, nameFieldStringValue, directoryURL, optionsSet, optionsClear, callback,
fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ), retFileTypeIndex );
if( result != null )
fc.updateFileFilter( fileTypeFilters, retFileTypeIndex[0] );
return result;
} }
//---- class MacApproveContext ---- //---- class MacApproveContext ----
@@ -1141,6 +1220,7 @@ public class SystemFileChooser
// filter // filter
int fileTypeIndex = 0; int fileTypeIndex = 0;
ArrayList<String> fileTypes = new ArrayList<>(); ArrayList<String> fileTypes = new ArrayList<>();
ArrayList<FileFilter> fileTypeFilters = new ArrayList<>();
if( !fc.isDirectorySelectionEnabled() ) { if( !fc.isDirectorySelectionEnabled() ) {
List<FileFilter> filters = fc.getFiltersForDialog(); List<FileFilter> filters = fc.getFiltersForDialog();
if( !filters.isEmpty() ) { if( !filters.isEmpty() ) {
@@ -1149,12 +1229,19 @@ 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 );
} 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( "*" );
fileTypes.add( null ); fileTypes.add( null );
fileTypeFilters.add( filter );
} }
} }
} }
@@ -1162,20 +1249,24 @@ public class SystemFileChooser
// callback // callback
FlatNativeLinuxLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null) FlatNativeLinuxLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null)
? (files, hwndFileDialog) -> { ? (files, fileTypeIndex2, hwndFileDialog) -> {
fc.updateFileFilter( fileTypeFilters, fileTypeIndex2 );
return invokeApproveCallback( fc, files, new LinuxApproveContext( hwndFileDialog ) ); return invokeApproveCallback( fc, files, new LinuxApproveContext( hwndFileDialog ) );
} : null; } : null;
// show system file dialog // show system file dialog
return FlatNativeLinuxLibrary.showFileChooser( owner, dark, open, int[] retFileTypeIndex = { -1 };
String[] result = FlatNativeLinuxLibrary.showFileChooser( owner, dark, open,
fc.getDialogTitle(), approveButtonText, currentName, currentFolder, fc.getDialogTitle(), approveButtonText, currentName, currentFolder,
optionsSet, optionsClear, callback, optionsSet, optionsClear, callback,
fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ), retFileTypeIndex );
if( result != null )
fc.updateFileFilter( fileTypeFilters, retFileTypeIndex[0] );
return result;
} }
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 );
@@ -1253,6 +1344,8 @@ public class SystemFileChooser
{ {
@Override @Override
public File[] showDialog( Window owner, SystemFileChooser fc ) { public File[] showDialog( Window owner, SystemFileChooser fc ) {
IdentityHashMap<javax.swing.filechooser.FileFilter, FileFilter> filterMap = new IdentityHashMap<>();
JFileChooser chooser = new JFileChooser() { JFileChooser chooser = new JFileChooser() {
@Override @Override
public void approveSelection() { public void approveSelection() {
@@ -1273,6 +1366,7 @@ public class SystemFileChooser
// callback // callback
ApproveCallback approveCallback = fc.getApproveCallback(); ApproveCallback approveCallback = fc.getApproveCallback();
if( approveCallback != null ) { if( approveCallback != null ) {
updateFileFilter( fc, this, filterMap );
int result = approveCallback.approve( files, new SwingApproveContext( this ) ); int result = approveCallback.approve( files, new SwingApproveContext( this ) );
if( result == CANCEL_OPTION ) if( result == CANCEL_OPTION )
return; return;
@@ -1313,6 +1407,7 @@ public class SystemFileChooser
chooser.addChoosableFileFilter( jfilter ); chooser.addChoosableFileFilter( jfilter );
if( filter == currentFilter ) if( filter == currentFilter )
chooser.setFileFilter( jfilter ); chooser.setFileFilter( jfilter );
filterMap.put( jfilter, filter );
} }
} }
} }
@@ -1340,6 +1435,7 @@ public class SystemFileChooser
// show dialog // show dialog
int result = chooser.showDialog( owner, null ); int result = chooser.showDialog( owner, null );
updateFileFilter( fc, chooser, filterMap );
// save window size // save window size
Dimension windowSize = chooser.getSize(); Dimension windowSize = chooser.getSize();
@@ -1361,12 +1457,24 @@ 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
return null; return null;
} }
private void updateFileFilter( SystemFileChooser fc, JFileChooser chooser,
IdentityHashMap<javax.swing.filechooser.FileFilter, FileFilter> filterMap )
{
FileFilter fileFilter = filterMap.get( chooser.getFileFilter() );
if( fileFilter != null )
fc.setFileFilter( fileFilter );
}
private static boolean checkMustExist( JFileChooser chooser, File[] files ) { private static boolean checkMustExist( JFileChooser chooser, File[] files ) {
for( File file : files ) { for( File file : files ) {
if( !file.exists() ) { if( !file.exists() ) {
@@ -1442,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 ---------------------------------------------------
@@ -1493,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

@@ -43,8 +43,7 @@ public class FlatDesktop
public static boolean isSupported( Action action ) { public static boolean isSupported( Action action ) {
if( SystemInfo.isJava_9_orLater ) { if( SystemInfo.isJava_9_orLater ) {
try { try {
return Desktop.isDesktopSupported() && return Desktop.getDesktop().isSupported( Enum.valueOf( Desktop.Action.class, action.name() ) );
Desktop.getDesktop().isSupported( Enum.valueOf( Desktop.Action.class, action.name() ) );
} catch( Exception ex ) { } catch( Exception ex ) {
LoggingFacade.INSTANCE.logSevere( null, ex ); LoggingFacade.INSTANCE.logSevere( null, ex );
return false; return false;

View File

@@ -777,9 +777,6 @@ public class FlatUIDefaultsInspector
@SuppressWarnings( "FormatString" ) // Error Prone @SuppressWarnings( "FormatString" ) // Error Prone
private static String color2hex( Color color ) { private static String color2hex( Color color ) {
if( color == null )
return "";
int rgb = color.getRGB(); int rgb = color.getRGB();
boolean hasAlpha = color.getAlpha() != 255; boolean hasAlpha = color.getAlpha() != 255;
@@ -1021,36 +1018,28 @@ public class FlatUIDefaultsInspector
item = (Item) value; item = (Item) value;
init( table, item.key, isSelected, row ); init( table, item.key, isSelected, row );
// get color of value // reset background, foreground and icon
valueColor = null; if( !(item.value instanceof Color) ) {
if( item.value instanceof Color )
valueColor = (item.info instanceof Color[]) ? ((Color[])item.info)[0] : (Color) item.value;
else if( item.value instanceof FlatLineBorder )
valueColor = ((FlatLineBorder)item.value).getLineColor();
// reset background and foreground
if( valueColor == null ) {
setBackground( null ); setBackground( null );
setForeground( null ); setForeground( null );
} }
if( !(item.value instanceof Icon) )
setIcon( null );
// value to string // value to string
value = item.getValueAsString(); value = item.getValueAsString();
super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column );
// set foreground, if value has color if( item.value instanceof Color ) {
if( valueColor != null ) { Color color = (item.info instanceof Color[]) ? ((Color[])item.info)[0] : (Color) item.value;
boolean isDark = new HSLColor( valueColor ).getLuminance() < 70 && valueColor.getAlpha() >= 128; boolean isDark = new HSLColor( color ).getLuminance() < 70 && color.getAlpha() >= 128;
valueColor = color;
setForeground( isDark ? Color.white : Color.black ); setForeground( isDark ? Color.white : Color.black );
} } else if( item.value instanceof Icon ) {
// set icon
if( item.value instanceof Icon ) {
Icon icon = (Icon) item.value; Icon icon = (Icon) item.value;
setIcon( new SafeIcon( icon ) ); setIcon( new SafeIcon( icon ) );
} else }
setIcon( null );
// set tooltip // set tooltip
String toolTipText = (item.value instanceof Object[]) String toolTipText = (item.value instanceof Object[])
@@ -1067,7 +1056,7 @@ public class FlatUIDefaultsInspector
@Override @Override
protected void paintComponent( Graphics g ) { protected void paintComponent( Graphics g ) {
if( valueColor != null ) { if( item.value instanceof Color ) {
int width = getWidth(); int width = getWidth();
int height = getHeight(); int height = getHeight();
Color background = valueColor; Color background = valueColor;

View File

@@ -24,7 +24,7 @@
// increase this version if changing API or functionality of native library // increase this version if changing API or functionality of native library
// also update version in Java class com.formdev.flatlaf.ui.FlatNativeLinuxLibrary // also update version in Java class com.formdev.flatlaf.ui.FlatNativeLinuxLibrary
#define API_VERSION_LINUX 3003 #define API_VERSION_LINUX 3004
//---- JNI methods ------------------------------------------------------------ //---- JNI methods ------------------------------------------------------------

View File

@@ -33,6 +33,9 @@ extern Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display**
// declare internal methods // declare internal methods
static jobjectArray fileListToStringArray( JNIEnv* env, GSList* fileList ); static jobjectArray fileListToStringArray( JNIEnv* env, GSList* fileList );
// fields
static int settingsSchemaInstalled = -1;
//---- helper ----------------------------------------------------------------- //---- helper -----------------------------------------------------------------
#define isOptionSet( option ) ((optionsSet & com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_ ## option) != 0) #define isOptionSet( option ) ((optionsSet & com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_ ## option) != 0)
@@ -58,6 +61,7 @@ static void initFilters( GtkFileChooser* chooser, JNIEnv* env, jint fileTypeInde
gtk_file_chooser_add_filter( chooser, filter ); gtk_file_chooser_add_filter( chooser, filter );
if( fileTypeIndex == filterIndex ) if( fileTypeIndex == filterIndex )
gtk_file_chooser_set_filter( chooser, filter ); gtk_file_chooser_set_filter( chooser, filter );
g_object_set_data( G_OBJECT( filter ), "flatlaf-filter-index", GINT_TO_POINTER( filterIndex + 1 ) );
filter = NULL; filter = NULL;
filterIndex++; filterIndex++;
} }
@@ -130,32 +134,42 @@ struct ResponseData {
JNIEnv* env; JNIEnv* env;
jobject callback; jobject callback;
GSList* fileList; GSList* fileList;
int filterIndex;
ResponseData( JNIEnv* _env, jobject _callback ) { ResponseData( JNIEnv* _env, jobject _callback ) {
env = _env; env = _env;
callback = _callback; callback = _callback;
fileList = NULL; fileList = NULL;
filterIndex = -1;
} }
}; };
static void handle_response( GtkWidget* dialog, gint responseId, gpointer data ) { static void handle_response( GtkWidget* dialog, gint responseId, gpointer data ) {
GtkFileChooser* chooser = GTK_FILE_CHOOSER( dialog );
ResponseData *response = static_cast<ResponseData*>( data );
// get selected filter (even if user cancels dialog)
GtkFileFilter* filter = gtk_file_chooser_get_filter( chooser );
response->filterIndex = (filter != NULL)
? GPOINTER_TO_INT( g_object_get_data( G_OBJECT( filter ), "flatlaf-filter-index" ) ) - 1
: -1;
// get filenames if user pressed OK // get filenames if user pressed OK
if( responseId == GTK_RESPONSE_ACCEPT ) { if( responseId == GTK_RESPONSE_ACCEPT ) {
ResponseData *response = static_cast<ResponseData*>( data );
if( response->callback != NULL ) { if( response->callback != NULL ) {
GSList* fileList = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER( dialog ) ); GSList* fileList = gtk_file_chooser_get_filenames( chooser );
jobjectArray files = fileListToStringArray( response->env, fileList ); jobjectArray files = fileListToStringArray( response->env, fileList );
jint filterIndex = response->filterIndex;
GtkWindow* window = GTK_WINDOW( dialog ); GtkWindow* window = GTK_WINDOW( dialog );
// invoke callback: boolean approve( String[] files, long hwnd ); // invoke callback: boolean approve( String[] files, long hwnd );
jclass cls = response->env->GetObjectClass( response->callback ); jclass cls = response->env->GetObjectClass( response->callback );
jmethodID approveID = response->env->GetMethodID( cls, "approve", "([Ljava/lang/String;J)Z" ); jmethodID approveID = response->env->GetMethodID( cls, "approve", "([Ljava/lang/String;IJ)Z" );
if( approveID != NULL && !response->env->CallBooleanMethod( response->callback, approveID, files, window ) ) if( approveID != NULL && !response->env->CallBooleanMethod( response->callback, approveID, files, filterIndex, window ) )
return; // keep dialog open return; // keep dialog open
} }
response->fileList = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER( dialog ) ); response->fileList = gtk_file_chooser_get_filenames( chooser );
} }
// hide/destroy file dialog and quit loop // hide/destroy file dialog and quit loop
@@ -170,12 +184,29 @@ extern "C"
JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser
( JNIEnv* env, jclass cls, jobject owner, jint dark, jboolean open, ( JNIEnv* env, jclass cls, jobject owner, jint dark, jboolean open,
jstring title, jstring okButtonLabel, jstring currentName, jstring currentFolder, jstring title, jstring okButtonLabel, jstring currentName, jstring currentFolder,
jint optionsSet, jint optionsClear, jobject callback, jint fileTypeIndex, jobjectArray fileTypes ) jint optionsSet, jint optionsClear, jobject callback,
jint fileTypeIndex, jobjectArray fileTypes, jintArray retFileTypeIndex )
{ {
// initialize GTK // initialize GTK
if( !gtk_init_check( NULL, NULL ) ) if( !gtk_init_check( NULL, NULL ) )
return NULL; return NULL;
// check whether required GSettings schemas are installed (e.g. on NixOS)
// this avoids output of following message on console, followed by an application crash:
// GLib-GIO-ERROR: No GSettings schemas are installed on the system
if( settingsSchemaInstalled < 0 ) {
GSettingsSchemaSource* schemaSource = g_settings_schema_source_get_default();
GSettingsSchema* schema = (schemaSource != NULL)
? g_settings_schema_source_lookup( schemaSource, "org.gtk.Settings.FileChooser", FALSE )
: NULL;
if( schema != NULL )
g_settings_schema_unref( schema );
settingsSchemaInstalled = (schema != NULL);
}
if( settingsSchemaInstalled <= 0 )
return NULL;
// convert Java strings to C strings // convert Java strings to C strings
AutoReleaseStringUTF8 ctitle( env, title ); AutoReleaseStringUTF8 ctitle( env, title );
AutoReleaseStringUTF8 cokButtonLabel( env, okButtonLabel ); AutoReleaseStringUTF8 cokButtonLabel( env, okButtonLabel );
@@ -282,7 +313,11 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrar
// start event loop (will be quit in respone handler) // start event loop (will be quit in respone handler)
gtk_main(); gtk_main();
// canceled? // return selected filter
jint selectedFilterIndex = responseData.filterIndex;
env->SetIntArrayRegion( retFileTypeIndex, 0, 1, &selectedFilterIndex );
// return empty array if canceled
if( responseData.fileList == NULL ) if( responseData.fileList == NULL )
return newJavaStringArray( env, 0 ); return newJavaStringArray( env, 0 );

View File

@@ -64,10 +64,10 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_is
/* /*
* Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary
* Method: showFileChooser * Method: showFileChooser
* Signature: (Ljava/awt/Window;IZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILcom/formdev/flatlaf/ui/FlatNativeLinuxLibrary/FileChooserCallback;I[Ljava/lang/String;)[Ljava/lang/String; * Signature: (Ljava/awt/Window;IZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILcom/formdev/flatlaf/ui/FlatNativeLinuxLibrary/FileChooserCallback;I[Ljava/lang/String;[I)[Ljava/lang/String;
*/ */
JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser
(JNIEnv *, jclass, jobject, jint, jboolean, jstring, jstring, jstring, jstring, jint, jint, jobject, jint, jobjectArray); (JNIEnv *, jclass, jobject, jint, jboolean, jstring, jstring, jstring, jstring, jint, jint, jobject, jint, jobjectArray, jintArray);
/* /*
* Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary

View File

@@ -82,10 +82,10 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_togg
/* /*
* Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary
* Method: showFileChooser * 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; * 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;[I)[Ljava/lang/String;
*/ */
JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_showFileChooser 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); (JNIEnv *, jclass, jobject, jint, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jint, jint, jobject, jint, jobjectArray, jintArray);
/* /*
* Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary

View File

@@ -24,7 +24,7 @@
// increase this version if changing API or functionality of native library // increase this version if changing API or functionality of native library
// also update version in Java class com.formdev.flatlaf.ui.FlatNativeMacLibrary // also update version in Java class com.formdev.flatlaf.ui.FlatNativeMacLibrary
#define API_VERSION_MACOS 2002 #define API_VERSION_MACOS 2003
//---- JNI methods ------------------------------------------------------------ //---- JNI methods ------------------------------------------------------------

View File

@@ -35,6 +35,7 @@ static NSArray* getDialogURLs( NSSavePanel* dialog );
@interface FileChooserDelegate : NSObject <NSOpenSavePanelDelegate, NSWindowDelegate> { @interface FileChooserDelegate : NSObject <NSOpenSavePanelDelegate, NSWindowDelegate> {
NSArray* _filters; NSArray* _filters;
int _selectedFormatIndex;
JavaVM* _jvm; JavaVM* _jvm;
jobject _callback; jobject _callback;
@@ -43,18 +44,26 @@ static NSArray* getDialogURLs( NSSavePanel* dialog );
@property (nonatomic, assign) NSSavePanel* dialog; @property (nonatomic, assign) NSSavePanel* dialog;
- (id) init;
- (void) initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex - (void) initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex
:(NSString*)filterFieldLabel :(bool)showSingleFilterField; :(NSString*)filterFieldLabel :(bool)showSingleFilterField;
- (void) selectFormat: (id)sender; - (void) selectFormat: (id)sender;
- (void) selectFormatAtIndex: (int)index; - (void) selectFormatAtIndex: (int)index;
- (int) selectedFormatIndex;
@end @end
@implementation FileChooserDelegate @implementation FileChooserDelegate
- (id) init {
_selectedFormatIndex = -1;
return self;
}
- (void) initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex - (void) initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex
:(NSString*)filterFieldLabel :(bool)showSingleFilterField :(NSString*)filterFieldLabel :(bool)showSingleFilterField
{ {
_filters = filters; _filters = filters;
_selectedFormatIndex = filterIndex;
// get filter names // get filter names
NSArray* filterNames = filters.lastObject; NSArray* filterNames = filters.lastObject;
@@ -125,6 +134,12 @@ static NSArray* getDialogURLs( NSSavePanel* dialog );
// to support older macOS versions 10.14+ and because of some problems with allowedContentTypes: // 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 // 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; _dialog.allowedFileTypes = [fileTypes containsObject:@"*"] ? nil : fileTypes;
_selectedFormatIndex = index;
}
- (int) selectedFormatIndex {
return _selectedFormatIndex;
} }
//---- NSOpenSavePanelDelegate ---- //---- NSOpenSavePanelDelegate ----
@@ -161,12 +176,13 @@ static NSArray* getDialogURLs( NSSavePanel* dialog );
JNI_THREAD_ENTER( _jvm, true ) JNI_THREAD_ENTER( _jvm, true )
jobjectArray files = urlsToStringArray( env, urls ); jobjectArray files = urlsToStringArray( env, urls );
jint selectedFormatIndex = ((FileChooserDelegate*)((NSSavePanel*)sender).delegate).selectedFormatIndex;
jlong window = (jlong) sender; jlong window = (jlong) sender;
// invoke callback: boolean approve( String[] files, long hwnd ); // invoke callback: boolean approve( String[] files, long hwnd );
jclass cls = env->GetObjectClass( _callback ); jclass cls = env->GetObjectClass( _callback );
jmethodID approveID = env->GetMethodID( cls, "approve", "([Ljava/lang/String;J)Z" ); jmethodID approveID = env->GetMethodID( cls, "approve", "([Ljava/lang/String;IJ)Z" );
if( approveID != NULL && !env->CallBooleanMethod( _callback, approveID, files, window ) ) { if( approveID != NULL && !env->CallBooleanMethod( _callback, approveID, files, selectedFormatIndex, window ) ) {
_urlsSet = NULL; _urlsSet = NULL;
return false; // keep dialog open return false; // keep dialog open
} }
@@ -265,7 +281,8 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_
( JNIEnv* env, jclass cls, jobject owner, jint dark, jboolean open, ( JNIEnv* env, jclass cls, jobject owner, jint dark, jboolean open,
jstring title, jstring prompt, jstring message, jstring filterFieldLabel, jstring title, jstring prompt, jstring message, jstring filterFieldLabel,
jstring nameFieldLabel, jstring nameFieldStringValue, jstring directoryURL, jstring nameFieldLabel, jstring nameFieldStringValue, jstring directoryURL,
jint optionsSet, jint optionsClear, jobject callback, jint fileTypeIndex, jobjectArray fileTypes ) jint optionsSet, jint optionsClear, jobject callback,
jint fileTypeIndex, jobjectArray fileTypes, jintArray retFileTypeIndex )
{ {
JNI_COCOA_ENTER() JNI_COCOA_ENTER()
@@ -365,17 +382,26 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_
// show dialog // show dialog
NSModalResponse response = [dialog runModal]; NSModalResponse response = [dialog runModal];
// return selected filter
jint selectedFormatIndex = delegate.selectedFormatIndex;
env->SetIntArrayRegion( retFileTypeIndex, 0, 1, &selectedFormatIndex );
[delegate release]; [delegate release];
// return empty array if canceled
if( response != NSModalResponseOK ) { if( response != NSModalResponseOK ) {
*purls = @[]; *purls = @[];
return; return;
} }
// get selected file(s)
*purls = getDialogURLs( dialog ); *purls = getDialogURLs( dialog );
JNI_COCOA_CATCH() JNI_COCOA_CATCH()
}]; }];
// return null on failures
if( urls == NULL ) if( urls == NULL )
return NULL; return NULL;

View File

@@ -24,7 +24,7 @@
// increase this version if changing API or functionality of native library // increase this version if changing API or functionality of native library
// also update version in Java class com.formdev.flatlaf.ui.FlatNativeWindowsLibrary // also update version in Java class com.formdev.flatlaf.ui.FlatNativeWindowsLibrary
#define API_VERSION_WINDOWS 1002 #define API_VERSION_WINDOWS 1003
//---- JNI methods ------------------------------------------------------------ //---- JNI methods ------------------------------------------------------------

View File

@@ -128,6 +128,11 @@ public:
} else } else
files = getFiles( env, false, dialog ); files = getFiles( env, false, dialog );
// get selected filter
UINT selectedFileTypeIndex = 0;
dialog->GetFileTypeIndex( &selectedFileTypeIndex );
jint jselectedFileTypeIndex = selectedFileTypeIndex - 1;
// get hwnd of file dialog // get hwnd of file dialog
HWND hwndFileDialog = 0; HWND hwndFileDialog = 0;
AutoReleasePtr<IOleWindow> window; AutoReleasePtr<IOleWindow> window;
@@ -136,10 +141,10 @@ public:
// invoke callback: boolean approve( String[] files, long hwnd ); // invoke callback: boolean approve( String[] files, long hwnd );
jclass cls = env->GetObjectClass( callback ); jclass cls = env->GetObjectClass( callback );
jmethodID approveID = env->GetMethodID( cls, "approve", "([Ljava/lang/String;J)Z" ); jmethodID approveID = env->GetMethodID( cls, "approve", "([Ljava/lang/String;IJ)Z" );
if( approveID == NULL ) if( approveID == NULL )
return S_OK; return S_OK;
return env->CallBooleanMethod( callback, approveID, files, hwndFileDialog ) ? S_OK : S_FALSE; return env->CallBooleanMethod( callback, approveID, files, jselectedFileTypeIndex, hwndFileDialog ) ? S_OK : S_FALSE;
} }
IFACEMETHODIMP OnFolderChange( IFileDialog* ) { return S_OK; } IFACEMETHODIMP OnFolderChange( IFileDialog* ) { return S_OK; }
@@ -147,7 +152,7 @@ public:
IFACEMETHODIMP OnHelp( IFileDialog* ) { return S_OK; } IFACEMETHODIMP OnHelp( IFileDialog* ) { return S_OK; }
IFACEMETHODIMP OnSelectionChange( IFileDialog* ) { return S_OK; } IFACEMETHODIMP OnSelectionChange( IFileDialog* ) { return S_OK; }
IFACEMETHODIMP OnShareViolation( IFileDialog*, IShellItem*, FDE_SHAREVIOLATION_RESPONSE* ) { return S_OK; } IFACEMETHODIMP OnShareViolation( IFileDialog*, IShellItem*, FDE_SHAREVIOLATION_RESPONSE* ) { return S_OK; }
IFACEMETHODIMP OnTypeChange( IFileDialog*pfd ) { return S_OK; } IFACEMETHODIMP OnTypeChange( IFileDialog* ) { return S_OK; }
IFACEMETHODIMP OnOverwrite( IFileDialog*, IShellItem*, FDE_OVERWRITE_RESPONSE* ) { return S_OK; } IFACEMETHODIMP OnOverwrite( IFileDialog*, IShellItem*, FDE_OVERWRITE_RESPONSE* ) { return S_OK; }
//---- IUnknown methods ---- //---- IUnknown methods ----
@@ -213,7 +218,8 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr
( JNIEnv* env, jclass cls, jobject owner, jboolean open, ( JNIEnv* env, jclass cls, jobject owner, jboolean open,
jstring title, jstring okButtonLabel, jstring fileNameLabel, jstring fileName, jstring title, jstring okButtonLabel, jstring fileNameLabel, jstring fileName,
jstring folder, jstring saveAsItem, jstring defaultFolder, jstring defaultExtension, jstring folder, jstring saveAsItem, jstring defaultFolder, jstring defaultExtension,
jint optionsSet, jint optionsClear, jobject callback, jint fileTypeIndex, jobjectArray fileTypes ) jint optionsSet, jint optionsClear, jobject callback,
jint fileTypeIndex, jobjectArray fileTypes, jintArray retFileTypeIndex )
{ {
// initialize COM library // initialize COM library
CoInitializer coInitializer; CoInitializer coInitializer;
@@ -285,6 +291,14 @@ JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibr
HWND hwndOwner = (owner != NULL) ? getWindowHandle( env, owner ) : NULL; HWND hwndOwner = (owner != NULL) ? getWindowHandle( env, owner ) : NULL;
HRESULT hr = dialog->Show( hwndOwner ); HRESULT hr = dialog->Show( hwndOwner );
dialog->Unadvise( dwCookie ); dialog->Unadvise( dwCookie );
// return selected filter
UINT selectedFileTypeIndex = 0;
CHECK_HRESULT( dialog->GetFileTypeIndex( &selectedFileTypeIndex ) );
jint jselectedFileTypeIndex = selectedFileTypeIndex - 1;
env->SetIntArrayRegion( retFileTypeIndex, 0, 1, &jselectedFileTypeIndex );
// return empty array if canceled
if( hr == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) if( hr == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
return newJavaStringArray( env, 0 ); return newJavaStringArray( env, 0 );
CHECK_HRESULT( hr ); CHECK_HRESULT( hr );

View File

@@ -116,10 +116,10 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_
/* /*
* Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary
* Method: showFileChooser * 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; * 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;[I)[Ljava/lang/String;
*/ */
JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showFileChooser 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); (JNIEnv *, jclass, jobject, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jint, jint, jobject, jint, jobjectArray, jintArray);
/* /*
* Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary

View File

@@ -110,8 +110,8 @@ public class FlatSystemFileChooserLinuxTest
} }
int fileTypeIndex = fileTypeIndexSlider.getValue(); int fileTypeIndex = fileTypeIndexSlider.getValue();
FlatNativeLinuxLibrary.FileChooserCallback callback = (files, hwndFileDialog) -> { FlatNativeLinuxLibrary.FileChooserCallback callback = (files, fileTypeIndex2, hwndFileDialog) -> {
System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) ); System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) + " " + fileTypeIndex2 );
if( showMessageDialogOnOKCheckBox.isSelected() ) { if( showMessageDialogOnOKCheckBox.isSelected() ) {
System.out.println( FlatNativeLinuxLibrary.showMessageDialog( hwndFileDialog, System.out.println( FlatNativeLinuxLibrary.showMessageDialog( hwndFileDialog,
JOptionPane.INFORMATION_MESSAGE, JOptionPane.INFORMATION_MESSAGE,
@@ -124,24 +124,28 @@ public class FlatSystemFileChooserLinuxTest
int dark = FlatLaf.isLafDark() ? 1 : 0; int dark = FlatLaf.isLafDark() ? 1 : 0;
if( direct ) { if( direct ) {
int[] retFileTypeIndex = { -1 };
String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, dark, open, String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, dark, open,
title, okButtonLabel, currentName, currentFolder, title, okButtonLabel, currentName, currentFolder,
optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes ); optionsSet.get(), optionsClear.get(), callback,
fileTypeIndex, fileTypes, retFileTypeIndex );
filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); outputResult( files, retFileTypeIndex[0] );
} else { } else {
SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop();
String[] fileTypes2 = fileTypes; String[] fileTypes2 = fileTypes;
new Thread( () -> { new Thread( () -> {
int[] retFileTypeIndex = { -1 };
String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, dark, open, String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, dark, open,
title, okButtonLabel, currentName, currentFolder, title, okButtonLabel, currentName, currentFolder,
optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 ); optionsSet.get(), optionsClear.get(), callback,
fileTypeIndex, fileTypes2, retFileTypeIndex );
System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() );
EventQueue.invokeLater( () -> { EventQueue.invokeLater( () -> {
filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); outputResult( files, retFileTypeIndex[0] );
} ); } );
} ).start(); } ).start();
@@ -150,6 +154,11 @@ public class FlatSystemFileChooserLinuxTest
} }
} }
private void outputResult( String[] files, int retFileTypeIndex ) {
filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" );
filesField.append( "\n\nretFileTypeIndex " + retFileTypeIndex );
}
private static String n( String s ) { private static String n( String s ) {
return s != null && !s.isEmpty() ? s : null; return s != null && !s.isEmpty() ? s : null;
} }

View File

@@ -145,8 +145,8 @@ public class FlatSystemFileChooserMacTest
} }
int fileTypeIndex = fileTypeIndexSlider.getValue(); int fileTypeIndex = fileTypeIndexSlider.getValue();
FlatNativeMacLibrary.FileChooserCallback callback = (files, hwndFileDialog) -> { FlatNativeMacLibrary.FileChooserCallback callback = (files, fileTypeIndex2, hwndFileDialog) -> {
System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) ); System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) + " " + fileTypeIndex2 );
if( showMessageDialogOnOKCheckBox.isSelected() ) { if( showMessageDialogOnOKCheckBox.isSelected() ) {
int result = FlatNativeMacLibrary.showMessageDialog( hwndFileDialog, int result = FlatNativeMacLibrary.showMessageDialog( hwndFileDialog,
JOptionPane.INFORMATION_MESSAGE, JOptionPane.INFORMATION_MESSAGE,
@@ -160,26 +160,30 @@ public class FlatSystemFileChooserMacTest
int dark = FlatLaf.isLafDark() ? 1 : 0; int dark = FlatLaf.isLafDark() ? 1 : 0;
if( direct ) { if( direct ) {
int[] retFileTypeIndex = { -1 };
String[] files = FlatNativeMacLibrary.showFileChooser( owner, dark, open, String[] files = FlatNativeMacLibrary.showFileChooser( owner, dark, open,
title, prompt, message, filterFieldLabel, title, prompt, message, filterFieldLabel,
nameFieldLabel, nameFieldStringValue, directoryURL, nameFieldLabel, nameFieldStringValue, directoryURL,
optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes ); optionsSet.get(), optionsClear.get(), callback,
fileTypeIndex, fileTypes, retFileTypeIndex );
filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); outputResult( files, retFileTypeIndex[0] );
} else { } else {
SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop();
String[] fileTypes2 = fileTypes; String[] fileTypes2 = fileTypes;
new Thread( () -> { new Thread( () -> {
int[] retFileTypeIndex = { -1 };
String[] files = FlatNativeMacLibrary.showFileChooser( owner, dark, open, String[] files = FlatNativeMacLibrary.showFileChooser( owner, dark, open,
title, prompt, message, filterFieldLabel, title, prompt, message, filterFieldLabel,
nameFieldLabel, nameFieldStringValue, directoryURL, nameFieldLabel, nameFieldStringValue, directoryURL,
optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 ); optionsSet.get(), optionsClear.get(), callback,
fileTypeIndex, fileTypes2, retFileTypeIndex );
System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() );
SwingUtilities.invokeLater( () -> { SwingUtilities.invokeLater( () -> {
filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); outputResult( files, retFileTypeIndex[0] );
} ); } );
} ).start(); } ).start();
@@ -188,6 +192,11 @@ public class FlatSystemFileChooserMacTest
} }
} }
private void outputResult( String[] files, int retFileTypeIndex ) {
filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" );
filesField.append( "\n\nretFileTypeIndex " + retFileTypeIndex );
}
private static String n( String s ) { private static String n( String s ) {
return s != null && !s.isEmpty() ? s : null; return s != null && !s.isEmpty() ? s : null;
} }

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 )
@@ -310,7 +312,8 @@ public class FlatSystemFileChooserTest
"result", result, "result", result,
"currentDirectory", fc.getCurrentDirectory(), "currentDirectory", fc.getCurrentDirectory(),
"selectedFile", fc.getSelectedFile(), "selectedFile", fc.getSelectedFile(),
"selectedFiles", fc.getSelectedFiles() ); "selectedFiles", fc.getSelectedFiles(),
"fileFilter", fc.getFileFilter() );
} }
private void outputSwingFileChooser( String type, JFileChooser fc, int result ) { private void outputSwingFileChooser( String type, JFileChooser fc, int result ) {
@@ -319,7 +322,8 @@ public class FlatSystemFileChooserTest
"result", result, "result", result,
"currentDirectory", fc.getCurrentDirectory(), "currentDirectory", fc.getCurrentDirectory(),
"selectedFile", fc.getSelectedFile(), "selectedFile", fc.getSelectedFile(),
"selectedFiles", fc.getSelectedFiles() ); "selectedFiles", fc.getSelectedFiles(),
"fileFilter", fc.getFileFilter() );
} }
private void outputAWTFileChooser( FileDialog fc ) { private void outputAWTFileChooser( FileDialog fc ) {

View File

@@ -131,8 +131,8 @@ public class FlatSystemFileChooserWindowsTest
fileTypes = fileTypesStr.trim().split( "[,]+" ); fileTypes = fileTypesStr.trim().split( "[,]+" );
int fileTypeIndex = fileTypeIndexSlider.getValue(); int fileTypeIndex = fileTypeIndexSlider.getValue();
FlatNativeWindowsLibrary.FileChooserCallback callback = (files, hwndFileDialog) -> { FlatNativeWindowsLibrary.FileChooserCallback callback = (files, fileTypeIndex2, hwndFileDialog) -> {
System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) ); System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) + " " + fileTypeIndex2 );
if( showMessageDialogOnOKCheckBox.isSelected() ) { if( showMessageDialogOnOKCheckBox.isSelected() ) {
System.out.println( FlatNativeWindowsLibrary.showMessageDialog( hwndFileDialog, System.out.println( FlatNativeWindowsLibrary.showMessageDialog( hwndFileDialog,
JOptionPane.INFORMATION_MESSAGE, JOptionPane.INFORMATION_MESSAGE,
@@ -142,26 +142,30 @@ public class FlatSystemFileChooserWindowsTest
}; };
if( direct ) { if( direct ) {
int[] retFileTypeIndex = { -1 };
String[] files = FlatNativeWindowsLibrary.showFileChooser( owner, open, String[] files = FlatNativeWindowsLibrary.showFileChooser( owner, open,
title, okButtonLabel, fileNameLabel, fileName, title, okButtonLabel, fileNameLabel, fileName,
folder, saveAsItem, defaultFolder, defaultExtension, folder, saveAsItem, defaultFolder, defaultExtension,
optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes ); optionsSet.get(), optionsClear.get(), callback,
fileTypeIndex, fileTypes, retFileTypeIndex );
filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); outputResult( files, retFileTypeIndex[0] );
} else { } else {
SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop();
String[] fileTypes2 = fileTypes; String[] fileTypes2 = fileTypes;
new Thread( () -> { new Thread( () -> {
int[] retFileTypeIndex = { -1 };
String[] files = FlatNativeWindowsLibrary.showFileChooser( owner, open, String[] files = FlatNativeWindowsLibrary.showFileChooser( owner, open,
title, okButtonLabel, fileNameLabel, fileName, title, okButtonLabel, fileNameLabel, fileName,
folder, saveAsItem, defaultFolder, defaultExtension, folder, saveAsItem, defaultFolder, defaultExtension,
optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 ); optionsSet.get(), optionsClear.get(), callback,
fileTypeIndex, fileTypes2, retFileTypeIndex );
System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() );
EventQueue.invokeLater( () -> { EventQueue.invokeLater( () -> {
filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); outputResult( files, retFileTypeIndex[0] );
} ); } );
} ).start(); } ).start();
@@ -170,6 +174,11 @@ public class FlatSystemFileChooserWindowsTest
} }
} }
private void outputResult( String[] files, int retFileTypeIndex ) {
filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" );
filesField.append( "\n\nretFileTypeIndex " + retFileTypeIndex );
}
private static String n( String s ) { private static String n( String s ) {
return s != null && !s.isEmpty() ? s : null; return s != null && !s.isEmpty() ? s : null;
} }