diff --git a/.github/workflows/natives.yml b/.github/workflows/natives.yml index 7ec105f58..3817c17f5 100644 --- a/.github/workflows/natives.yml +++ b/.github/workflows/natives.yml @@ -33,13 +33,30 @@ jobs: - uses: gradle/actions/wrapper-validation@v4 - - name: install libxt-dev + - name: apt update (Linux) if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' - run: sudo apt install libxt-dev + run: sudo apt-get update - - name: install g++-aarch64-linux-gnu - if: matrix.os == 'ubuntu-latest' - run: sudo apt install g++-aarch64-linux-gnu + - name: install libxt-dev and libgtk-3-dev (Linux) + if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-24.04-arm' + run: sudo apt-get install libxt-dev libgtk-3-dev + +# - name: Download libgtk-3.so for arm64 (Linux) +# if: matrix.os == 'ubuntu-latest' +# working-directory: flatlaf-natives/flatlaf-natives-linux/lib/aarch64 +# run: | +# pwd +# ls -l /usr/lib/x86_64-linux-gnu/libgtk* +# wget --no-verbose https://ports.ubuntu.com/pool/main/g/gtk%2b3.0/libgtk-3-0_3.24.18-1ubuntu1_arm64.deb +# ls -l +# ar -x libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz +# tar -xvf data.tar.xz --wildcards --to-stdout "./usr/lib/aarch64-linux-gnu/libgtk-3.so.0.*" > libgtk-3.so +# rm libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz +# ls -l + +# - name: install g++-aarch64-linux-gnu (Linux) +# if: matrix.os == 'ubuntu-latest' +# run: sudo apt-get install g++-aarch64-linux-gnu - name: Setup Java 11 uses: actions/setup-java@v4 @@ -53,10 +70,60 @@ jobs: # tar.exe: Couldn't open ~/.gradle/caches/modules-2/modules-2.lock: Permission denied run: ./gradlew build-natives --no-daemon + - name: Sign Windows DLLs + if: matrix.os == 'windows-latest' + uses: skymatic/code-sign-action@v3 + with: + certificate: '${{ secrets.CODE_SIGN_CERT_BASE64 }}' + password: '${{ secrets.CODE_SIGN_CERT_PASSWORD }}' + certificatesha1: '${{ secrets.CODE_SIGN_CERT_SHA1 }}' + folder: 'flatlaf-core/src/main/resources/com/formdev/flatlaf/natives' + + - name: Sign macOS natives + if: matrix.os == 'DISABLED--macos-latest' + env: + CERT_BASE64: ${{ secrets.CODE_SIGN_CERT_BASE64 }} + CERT_PASSWORD: ${{ secrets.CODE_SIGN_CERT_PASSWORD }} + CERT_IDENTITY: ${{ secrets.CODE_SIGN_CERT_IDENTITY }} + run: | + # https://docs.github.com/en/actions/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development + # create variables + CERTIFICATE_PATH=$RUNNER_TEMP/cert.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/signing.keychain-db + KEYCHAIN_PASSWORD=$CERT_PASSWORD + # decode certificate + printenv CERT_BASE64 | base64 --decode > $CERTIFICATE_PATH + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$CERT_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + # set partition list (required for codesign) + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + # add keychain to keychain search list + security list-keychains -d user -s $KEYCHAIN_PATH + # sign code + codesign --sign "$CERT_IDENTITY" --force --verbose=4 --timestamp \ + flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-*.dylib + codesign --display --verbose=4 flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-*.dylib + # cleanup + security delete-keychain $KEYCHAIN_PATH + + - name: Set artifacts pattern + shell: bash + run: | + case ${{ matrix.os }} in + windows-latest) echo "artifactPattern=flatlaf-windows-*.dll" >> $GITHUB_ENV ;; + macos-latest) echo "artifactPattern=libflatlaf-macos-*.dylib" >> $GITHUB_ENV ;; + ubuntu-latest) echo "artifactPattern=libflatlaf-linux-x86_64.so" >> $GITHUB_ENV ;; + ubuntu-24.04-arm) echo "artifactPattern=libflatlaf-linux-arm64.so" >> $GITHUB_ENV ;; + esac + - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: FlatLaf-natives-build-artifacts-${{ matrix.os }} path: | - flatlaf-core/src/main/resources/com/formdev/flatlaf/natives + flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/${{ env.artifactPattern }} flatlaf-natives/flatlaf-natives-*/build diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java index d84ee8554..e9ee613bf 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java @@ -16,7 +16,9 @@ package com.formdev.flatlaf; +import javax.swing.JFileChooser; import javax.swing.SwingUtilities; +import com.formdev.flatlaf.util.SystemFileChooser; import com.formdev.flatlaf.util.UIScale; /** @@ -226,6 +228,17 @@ public interface FlatSystemProperties */ String USE_SUB_MENU_SAFE_TRIANGLE = "flatlaf.useSubMenuSafeTriangle"; + /** + * Specifies whether {@link SystemFileChooser} uses operating system file dialogs. + * If set to {@code false}, the {@link JFileChooser} is used instead. + *

+ * Allowed Values {@code false} and {@code true}
+ * Default {@code true} + * + * @since 3.7 + */ + String USE_SYSTEM_FILE_CHOOSER = "flatlaf.useSystemFileChooser"; + /** * Checks whether a system property is set and returns {@code true} if its value * is {@code "true"} (case-insensitive), otherwise it returns {@code false}. diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java index 6dc170e17..1aceddf40 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java @@ -24,6 +24,7 @@ import java.awt.geom.AffineTransform; import javax.swing.JDialog; import javax.swing.JFrame; +import javax.swing.JOptionPane; import com.formdev.flatlaf.util.SystemInfo; /** @@ -34,9 +35,9 @@ * @author Karl Tauber * @since 2.5 */ -class FlatNativeLinuxLibrary +public class FlatNativeLinuxLibrary { - private static int API_VERSION_LINUX = 3001; + private static int API_VERSION_LINUX = 3002; /** * Checks whether native library is loaded/available. @@ -44,10 +45,13 @@ class FlatNativeLinuxLibrary * Note: It is required to invoke this method before invoking any other * method of this class. Otherwise, the native library may not be loaded. */ - static boolean isLoaded() { + public static boolean isLoaded() { return SystemInfo.isLinux && FlatNativeLibrary.isLoaded( API_VERSION_LINUX ); } + + //---- X Window System ---------------------------------------------------- + // direction for _NET_WM_MOVERESIZE message // see https://specifications.freedesktop.org/wm-spec/latest/ar01s04.html static final int @@ -124,4 +128,109 @@ private static boolean hasCustomDecoration( Window window ) { return (window instanceof JFrame && JFrame.isDefaultLookAndFeelDecorated() && ((JFrame)window).isUndecorated()) || (window instanceof JDialog && JDialog.isDefaultLookAndFeelDecorated() && ((JDialog)window).isUndecorated()); } + + + //---- GTK ---------------------------------------------------------------- + + private static Boolean isGtk3Available; + + /** + * Checks whether GTK 3 is available. + * Use this before invoking any native method that uses GTK. + * Otherwise the app may terminate immediately if GTK is not installed. + *

+ * This works because Java uses {@code dlopen(RTLD_LAZY)} to load JNI libraries, + * which only resolves symbols as the code that references them is executed. + * + * @since 3.7 + */ + public static boolean isGtk3Available() { + if( isGtk3Available == null ) + isGtk3Available = isLibAvailable( "libgtk-3.so.0" ) || isLibAvailable( "libgtk-3.so" ); + return isGtk3Available; + } + + private native static boolean isLibAvailable( String libname ); + + /** + * https://docs.gtk.org/gtk3/iface.FileChooser.html#properties + * + * @since 3.7 + */ + public static final int + FC_select_folder = 1 << 0, + FC_select_multiple = 1 << 1, + FC_show_hidden = 1 << 2, + FC_local_only = 1 << 3, // default + FC_do_overwrite_confirmation = 1 << 4, // GTK 3 only; removed and always-on in GTK 4 + FC_create_folders = 1 << 5; // default for Save + + /** + * Shows the Linux/GTK system file dialog + * GtkFileChooserDialog. + *

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

+ * Note: This method blocks the current thread until the user closes + * the file dialog. It is highly recommended to invoke it from a new thread + * to avoid blocking the AWT event dispatching thread. + * + * @param owner the owner of the file dialog; or {@code null} + * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog + * @param title text displayed in dialog title; or {@code null} + * @param okButtonLabel text displayed in default button; or {@code null}. + * Use '_' for mnemonics (e.g. "_Choose") + * Use '__' for '_' character (e.g. "Choose__and__Quit"). + * @param currentName user-editable filename currently shown in the filename field in save dialog; or {@code null} + * @param currentFolder current directory shown in the dialog; or {@code null} + * @param optionsSet options to set; see {@code FOS_*} constants + * @param optionsClear options to clear; see {@code FOS_*} constants + * @param callback approve callback; or {@code null} + * @param fileTypeIndex the file type that appears as selected (zero-based) + * @param fileTypes file types that the dialog can open or save. + * Two or more strings and {@code null} are required for each filter. + * First string is the display name of the filter shown in the combobox (e.g. "Text Files"). + * Subsequent strings are the filter patterns (e.g. "*.txt" or "*"). + * {@code null} is required to mark end of filter. + * @return file path(s) that the user selected; an empty array if canceled; + * or {@code null} on failures (no dialog shown) + * + * @since 3.7 + */ + public native static String[] showFileChooser( Window owner, boolean open, + String title, String okButtonLabel, String currentName, String currentFolder, + int optionsSet, int optionsClear, FileChooserCallback callback, + int fileTypeIndex, String... fileTypes ); + + /** @since 3.7 */ + public interface FileChooserCallback { + boolean approve( String[] files, long hwndFileDialog ); + } + + /** + * Shows a GTK message box + * GtkMessageDialog. + *

+ * For use in {@link FileChooserCallback} only. + * + * @param hwndParent the parent of the message box + * @param messageType type of message being displayed: + * {@link JOptionPane#ERROR_MESSAGE}, {@link JOptionPane#INFORMATION_MESSAGE}, + * {@link JOptionPane#WARNING_MESSAGE}, {@link JOptionPane#QUESTION_MESSAGE} or + * {@link JOptionPane#PLAIN_MESSAGE} + * @param primaryText primary text; if the dialog has a secondary text, + * this will appear as title in a larger bold font + * @param secondaryText secondary text; shown below of primary text; or {@code null} + * @param defaultButton index of the default button, which can be pressed using ENTER key + * @param buttons texts of the buttons; if no buttons given the a default "OK" button is shown. + * Use '_' for mnemonics (e.g. "_Choose") + * Use '__' for '_' character (e.g. "Choose__and__Quit"). + * @return index of pressed button; or -1 for ESC key + * + * @since 3.7 + */ + public native static int showMessageDialog( long hwndParent, int messageType, + String primaryText, String secondaryText, int defaultButton, String... buttons ); } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java index 7d08376a0..460ab8999 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java @@ -18,6 +18,7 @@ import java.awt.Rectangle; import java.awt.Window; +import javax.swing.JOptionPane; import com.formdev.flatlaf.util.SystemInfo; /** @@ -44,7 +45,7 @@ */ public class FlatNativeMacLibrary { - private static int API_VERSION_MACOS = 2001; + private static int API_VERSION_MACOS = 2002; /** * Checks whether native library is loaded/available. @@ -68,4 +69,88 @@ public static boolean isLoaded() { /** @since 3.4 */ public native static Rectangle getWindowButtonsBounds( Window window ); /** @since 3.4 */ public native static boolean isWindowFullScreen( Window window ); /** @since 3.4 */ public native static boolean toggleWindowFullScreen( Window window ); + + + /** @since 3.7 */ + public static final int + // NSOpenPanel (extends NSSavePanel) + FC_canChooseFiles = 1 << 0, // default + FC_canChooseDirectories = 1 << 1, + FC_resolvesAliases = 1 << 2, // default + FC_allowsMultipleSelection = 1 << 3, + FC_accessoryViewDisclosed = 1 << 4, + // NSSavePanel + FC_showsTagField = 1 << 8, // default for Save + FC_canCreateDirectories = 1 << 9, // default for Save + FC_canSelectHiddenExtension = 1 << 10, + FC_showsHiddenFiles = 1 << 11, + FC_extensionHidden = 1 << 12, + FC_allowsOtherFileTypes = 1 << 13, + FC_treatsFilePackagesAsDirectories = 1 << 14, + // custom + FC_showSingleFilterField = 1 << 24; + + /** + * Shows the macOS system file dialogs + * NSOpenPanel or + * NSSavePanel. + *

+ * Note: This method blocks the current thread until the user closes + * the file dialog. It is highly recommended to invoke it from a new thread + * to avoid blocking the AWT event dispatching thread. + * + * @param owner the owner of the file dialog; or {@code null} + * @param dark appearance of the file dialog: {@code 1} = dark, {@code 0} = light, {@code -1} = default + * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog + * @param title text displayed at top of save dialog (not used in open dialog); or {@code null} + * @param prompt text displayed in default button; or {@code null} + * @param message text displayed at top of open/save dialogs; or {@code null} + * @param filterFieldLabel text displayed in front of the filter combobox; or {@code null} + * @param nameFieldLabel text displayed in front of the filename text field in save dialog (not used in open dialog); or {@code null} + * @param nameFieldStringValue user-editable filename currently shown in the name field in save dialog (not used in open dialog); or {@code null} + * @param directoryURL current directory shown in the dialog; or {@code null} + * @param optionsSet options to set; see {@code FC_*} constants + * @param optionsClear options to clear; see {@code FC_*} constants + * @param fileTypeIndex the file type that appears as selected (zero-based) + * @param fileTypes file types that the dialog can open or save. + * Two or more strings and {@code null} are required for each filter. + * First string is the display name of the filter shown in the combobox (e.g. "Text Files"). + * Subsequent strings are the filter patterns (e.g. "txt" or "*"). + * {@code null} is required to mark end of filter. + * @return file path(s) that the user selected; an empty array if canceled; + * or {@code null} on failures (no dialog shown) + * + * @since 3.7 + */ + public native static String[] showFileChooser( Window owner, int dark, boolean open, + String title, String prompt, String message, String filterFieldLabel, + String nameFieldLabel, String nameFieldStringValue, String directoryURL, + int optionsSet, int optionsClear, FileChooserCallback callback, + int fileTypeIndex, String... fileTypes ); + + /** @since 3.7 */ + public interface FileChooserCallback { + boolean approve( String[] files, long hwndFileDialog ); + } + + /** + * Shows a macOS alert + * NSAlert. + *

+ * For use in {@link FileChooserCallback} only. + * + * @param hwndParent the parent of the message box + * @param alertStyle type of alert being displayed: + * {@link JOptionPane#ERROR_MESSAGE}, {@link JOptionPane#INFORMATION_MESSAGE} or + * {@link JOptionPane#WARNING_MESSAGE} + * @param messageText main message of the alert + * @param informativeText additional information about the alert; shown below of main message; or {@code null} + * @param defaultButton index of the default button, which can be pressed using ENTER key + * @param buttons texts of the buttons; if no buttons given the a default "OK" button is shown + * @return index of pressed button + * + * @since 3.7 + */ + public native static int showMessageDialog( long hwndParent, int alertStyle, + String messageText, String informativeText, int defaultButton, String... buttons ); } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java index 8a4ce3cf0..a5caaef7a 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeWindowsLibrary.java @@ -18,6 +18,7 @@ import java.awt.Color; import java.awt.Window; +import javax.swing.JOptionPane; import com.formdev.flatlaf.util.SystemInfo; /** @@ -30,7 +31,7 @@ */ public class FlatNativeWindowsLibrary { - private static int API_VERSION_WINDOWS = 1001; + private static int API_VERSION_WINDOWS = 1002; private static long osBuildNumber = Long.MIN_VALUE; @@ -158,4 +159,125 @@ public static boolean dwmSetWindowAttributeCOLORREF( long hwnd, int attribute, C // DwmSetWindowAttribute() expects COLORREF as attribute value, which is defined as DWORD return dwmSetWindowAttributeDWORD( hwnd, attribute, rgb ); } + + + /** + * FILEOPENDIALOGOPTIONS + * see https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/ne-shobjidl_core-_fileopendialogoptions + * + * @since 3.7 + */ + public static final int + FOS_OVERWRITEPROMPT = 0x2, // default for Save + FOS_STRICTFILETYPES = 0x4, + FOS_NOCHANGEDIR = 0x8, // default + FOS_PICKFOLDERS = 0x20, + FOS_FORCEFILESYSTEM = 0x40, + FOS_ALLNONSTORAGEITEMS = 0x80, + FOS_NOVALIDATE = 0x100, + FOS_ALLOWMULTISELECT = 0x200, + FOS_PATHMUSTEXIST = 0x800, // default + FOS_FILEMUSTEXIST = 0x1000, // default for Open + FOS_CREATEPROMPT = 0x2000, + FOS_SHAREAWARE = 0x4000, + FOS_NOREADONLYRETURN = 0x8000, // default for Save + FOS_NOTESTFILECREATE = 0x10000, + FOS_HIDEMRUPLACES = 0x20000, + FOS_HIDEPINNEDPLACES = 0x40000, + FOS_NODEREFERENCELINKS = 0x100000, + FOS_OKBUTTONNEEDSINTERACTION = 0x200000, + FOS_DONTADDTORECENT = 0x2000000, + FOS_FORCESHOWHIDDEN = 0x10000000, + FOS_DEFAULTNOMINIMODE = 0x20000000, + FOS_FORCEPREVIEWPANEON = 0x40000000, + FOS_SUPPORTSTREAMABLEITEMS = 0x80000000; + + /** + * Shows the Windows system + * file dialogs + * IFileOpenDialog or + * IFileSaveDialog. + *

+ * Note: This method blocks the current thread until the user closes + * the file dialog. It is highly recommended to invoke it from a new thread + * to avoid blocking the AWT event dispatching thread. + * + * @param owner the owner of the file dialog; or {@code null} + * @param open if {@code true}, shows the open dialog; if {@code false}, shows the save dialog + * @param title text displayed in dialog title; or {@code null} + * @param okButtonLabel text displayed in default button; or {@code null}. + * Use '&' for mnemonics (e.g. "&Choose"). + * Use '&&' for '&' character (e.g. "Choose && Quit"). + * @param fileNameLabel text displayed in front of the filename text field; or {@code null} + * @param fileName user-editable filename currently shown in the filename field; or {@code null} + * @param folder current directory shown in the dialog; or {@code null} + * @param saveAsItem file to be used as the initial entry in a Save As dialog; or {@code null}. + * File name is shown in filename text field, folder is selected in view. + * To be used for saving files that already exist. For new files use {@code fileName}. + * @param defaultFolder folder used as a default if there is not a recently used folder value available; or {@code null}. + * Windows somewhere stores default folder on a per-app basis. + * So this is probably used only once when the app opens a file dialog for first time. + * @param defaultExtension default extension to be added to file name in save dialog; or {@code null} + * @param optionsSet options to set; see {@code FOS_*} constants + * @param optionsClear options to clear; see {@code FOS_*} constants + * @param callback approve callback; or {@code null} + * @param fileTypeIndex the file type that appears as selected (zero-based) + * @param fileTypes file types that the dialog can open or save. + * Pairs of strings are required for each filter. + * First string is the display name of the filter shown in the combobox (e.g. "Text Files"). + * Second string is the filter pattern (e.g. "*.txt", "*.exe;*.dll" or "*.*"). + * @return file path(s) that the user selected; an empty array if canceled; + * or {@code null} on failures (no dialog shown) + * + * @since 3.7 + */ + public native static String[] showFileChooser( Window owner, boolean open, + String title, String okButtonLabel, String fileNameLabel, String fileName, + String folder, String saveAsItem, String defaultFolder, String defaultExtension, + int optionsSet, int optionsClear, FileChooserCallback callback, + int fileTypeIndex, String... fileTypes ); + + /** @since 3.7 */ + public interface FileChooserCallback { + boolean approve( String[] files, long hwndFileDialog ); + } + + /** + * Shows a modal Windows message dialog. + *

+ * For use in {@link FileChooserCallback} only. + * + * @param hwndParent the parent of the message box + * @param messageType type of message being displayed: + * {@link JOptionPane#ERROR_MESSAGE}, {@link JOptionPane#INFORMATION_MESSAGE}, + * {@link JOptionPane#WARNING_MESSAGE}, {@link JOptionPane#QUESTION_MESSAGE} or + * {@link JOptionPane#PLAIN_MESSAGE} + * @param title dialog box title; or {@code null} to use title from parent window + * @param text message to be displayed + * @param defaultButton index of the default button, which can be pressed using ENTER key + * @param buttons texts of the buttons. + * Use '&' for mnemonics (e.g. "&Choose"). + * Use '&&' for '&' character (e.g. "Choose && Quit"). + * @return index of pressed button; or -1 for ESC key + * + * @since 3.7 + */ + public native static int showMessageDialog( long hwndParent, int messageType, + String title, String text, int defaultButton, String... buttons ); + + /** + * Shows a Windows message box + * MessageBox. + *

+ * For use in {@link FileChooserCallback} only. + * + * @param hwndParent the parent of the message box + * @param text message to be displayed + * @param caption dialog box title + * @param type see MessageBox parameter uType + * @return see MessageBox Return value + * + * @since 3.7 + */ + public native static int showMessageBox( long hwndParent, String text, String caption, int type ); } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemFileChooser.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemFileChooser.java new file mode 100644 index 000000000..56af434e2 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemFileChooser.java @@ -0,0 +1,1451 @@ +/* + * Copyright 2025 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.util; + +import java.awt.Component; +import java.awt.KeyboardFocusManager; +import java.awt.SecondaryLoop; +import java.awt.Toolkit; +import java.awt.Window; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.filechooser.FileSystemView; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.FlatSystemProperties; +import com.formdev.flatlaf.ui.FlatNativeLinuxLibrary; +import com.formdev.flatlaf.ui.FlatNativeMacLibrary; +import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; + +/** + * Gives access to operating system file dialogs. + *

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

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

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

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

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

Limitations/incompatibilities compared to JFileChooser

+ * + * + * + * @author Karl Tauber + * @since 3.7 + */ +public class SystemFileChooser +{ + /** @see JFileChooser#OPEN_DIALOG */ + public static final int OPEN_DIALOG = JFileChooser.OPEN_DIALOG; + + /** @see JFileChooser#SAVE_DIALOG */ + public static final int SAVE_DIALOG = JFileChooser.SAVE_DIALOG; + + /** @see JFileChooser#CANCEL_OPTION */ + public static final int CANCEL_OPTION = JFileChooser.CANCEL_OPTION; + + /** @see JFileChooser#APPROVE_OPTION */ + public static final int APPROVE_OPTION = JFileChooser.APPROVE_OPTION; + + /** @see JFileChooser#FILES_ONLY */ + public static final int FILES_ONLY = JFileChooser.FILES_ONLY; + + /** @see JFileChooser#DIRECTORIES_ONLY */ + public static final int DIRECTORIES_ONLY = JFileChooser.DIRECTORIES_ONLY; + + private int dialogType = OPEN_DIALOG; + private String dialogTitle; + private String approveButtonText; + private int approveButtonMnemonic = 0; + private int fileSelectionMode = FILES_ONLY; + private boolean multiSelection; + private boolean useFileHiding = true; + + private File currentDirectory; + private File selectedFile; + private File[] selectedFiles; + + private final ArrayList filters = new ArrayList<>(); + private FileFilter fileFilter; + private AcceptAllFileFilter acceptAllFileFilter; + private boolean useAcceptAllFileFilter = true; + + /** + * If {@code fc.addChoosableFileFilter(fc.getAcceptAllFileFilter())} is invoked from user code, + * then this flag is set to {@code false} and subsequent invocations of {@code fc.addChoosableFileFilter(...)} + * no longer insert added filters before the "All Files" filter. + * This allows custom ordering the "All Files" filter. + */ + private boolean keepAcceptAllAtEnd = true; + + private ApproveCallback approveCallback; + private int approveResult = APPROVE_OPTION; + + + /** + * Windows: Text displayed in front of the filename text field. + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String WINDOWS_FILE_NAME_LABEL = "windows.fileNameLabel"; + + /** + * Windows: Folder used as a default if there is not a recently used folder value available. + * Windows somewhere stores default folder on a per-app basis. + * So this is probably used only once when the app opens a file dialog for first time. + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String WINDOWS_DEFAULT_FOLDER = "windows.defaultFolder"; + + /** + * Windows: Default extension to be added to file name in save dialog. + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String WINDOWS_DEFAULT_EXTENSION = "windows.defaultExtension"; + + /** + * macOS: Text displayed at top of open/save dialogs. + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_MESSAGE = "mac.message"; + + /** + * macOS: Text displayed in front of the filter combobox. + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_FILTER_FIELD_LABEL = "mac.filterFieldLabel"; + + /** + * macOS: Text displayed in front of the filename text field in save dialog (not used in open dialog). + * Value type must be {@link String}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_NAME_FIELD_LABEL = "mac.nameFieldLabel"; + + /** + * macOS: If {@code true}, displays file packages (e.g. applications) as directories + * and allows the user to navigate into the file package. + * Value type must be {@link Boolean}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_TREATS_FILE_PACKAGES_AS_DIRECTORIES = "mac.treatsFilePackagesAsDirectories"; + + /** + * Windows: Low-level options to set. See {@code FOS_*} constants in {@link FlatNativeWindowsLibrary}. + * Options {@code FOS_PICKFOLDERS}, {@code FOS_ALLOWMULTISELECT} and {@code FOS_FORCESHOWHIDDEN} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String WINDOWS_OPTIONS_SET = "windows.optionsSet"; + + /** + * Windows: Low-level options to clear. See {@code FOS_*} constants in {@link FlatNativeWindowsLibrary}. + * Options {@code FOS_PICKFOLDERS}, {@code FOS_ALLOWMULTISELECT} and {@code FOS_FORCESHOWHIDDEN} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String WINDOWS_OPTIONS_CLEAR = "windows.optionsClear"; + + /** + * macOS: Low-level options to set. See {@code FC_*} constants in {@link FlatNativeMacLibrary}. + * Options {@code FC_canChooseFiles}, {@code FC_canChooseDirectories}, + * {@code FC_allowsMultipleSelection} and {@code FC_showsHiddenFiles} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_OPTIONS_SET = "mac.optionsSet"; + + /** + * macOS: Low-level options to clear. See {@code FC_*} constants in {@link FlatNativeMacLibrary}. + * Options {@code FC_canChooseFiles}, {@code FC_canChooseDirectories}, + * {@code FC_allowsMultipleSelection} and {@code FC_showsHiddenFiles} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String MAC_OPTIONS_CLEAR = "mac.optionsClear"; + + /** + * Linux: Low-level options to set. See {@code FC_*} constants in {@link FlatNativeLinuxLibrary}. + * Options {@code FC_select_folder}, {@code FC_select_multiple} and {@code FC_show_hidden} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String LINUX_OPTIONS_SET = "linux.optionsSet"; + + /** + * Linux: Low-level options to clear. See {@code FC_*} constants in {@link FlatNativeLinuxLibrary}. + * Options {@code FC_select_folder}, {@code FC_select_multiple} and {@code FC_show_hidden} can not be modified. + * Value type must be {@link Integer}. + * @see #putPlatformProperty(String, Object) + */ + public static final String LINUX_OPTIONS_CLEAR = "linux.optionsClear"; + + private Map platformProperties; + + private static final StateStore inMemoryStateStore = new StateStore() { + private final Map state = new HashMap<>(); + + @Override + public String get( String key, String def ) { + return state.getOrDefault( key, def ); + } + + @Override + public void put( String key, String value ) { + if( value != null ) + state.put( key, value ); + else + state.remove( key ); + } + }; + + private static StateStore stateStore; + private String stateStoreID; + + /** @see JFileChooser#JFileChooser() */ + public SystemFileChooser() { + this( (File) null ); + } + + /** @see JFileChooser#JFileChooser(String) */ + public SystemFileChooser( String currentDirectoryPath ) { + this( (currentDirectoryPath != null) + ? FileSystemView.getFileSystemView().createFileObject( currentDirectoryPath ) + : null ); + } + + /** @see JFileChooser#JFileChooser(File) */ + public SystemFileChooser( File currentDirectory ) { + setCurrentDirectory( currentDirectory ); + + addChoosableFileFilter( getAcceptAllFileFilter() ); + keepAcceptAllAtEnd = true; + } + + /** @see JFileChooser#showOpenDialog(Component) */ + public int showOpenDialog( Component parent ) { + setDialogType( OPEN_DIALOG ); + return showDialogImpl( parent ); + } + + /** @see JFileChooser#showSaveDialog(Component) */ + public int showSaveDialog( Component parent ) { + setDialogType( SAVE_DIALOG ); + return showDialogImpl( parent ); + } + + /** @see JFileChooser#showDialog(Component, String) */ + public int showDialog( Component parent, String approveButtonText ) { + if( approveButtonText != null ) + setApproveButtonText( approveButtonText ); + return showDialogImpl( parent ); + } + + /** @see JFileChooser#getDialogType() */ + public int getDialogType() { + return dialogType; + } + + /** @see JFileChooser#setDialogType(int) */ + public void setDialogType( int dialogType ) { + if( dialogType != OPEN_DIALOG && dialogType != SAVE_DIALOG ) + throw new IllegalArgumentException( "Invalid dialog type " + dialogType ); + + this.dialogType = dialogType; + } + + /** @see JFileChooser#getDialogTitle() */ + public String getDialogTitle() { + return dialogTitle; + } + + /** @see JFileChooser#setDialogTitle(String) */ + public void setDialogTitle( String dialogTitle ) { + this.dialogTitle = dialogTitle; + } + + /** @see JFileChooser#getApproveButtonText() */ + public String getApproveButtonText() { + return approveButtonText; + } + + /** @see JFileChooser#setApproveButtonText(String) */ + public void setApproveButtonText( String approveButtonText ) { + this.approveButtonText = approveButtonText; + } + + /** @see JFileChooser#getApproveButtonMnemonic() */ + public int getApproveButtonMnemonic() { + return approveButtonMnemonic; + } + + /** @see JFileChooser#setApproveButtonMnemonic(int) */ + public void setApproveButtonMnemonic( int mnemonic ) { + approveButtonMnemonic = mnemonic; + } + + /** @see JFileChooser#setApproveButtonMnemonic(char) */ + public void setApproveButtonMnemonic( char mnemonic ) { + int vk = mnemonic; + if( vk >= 'a' && vk <= 'z' ) + vk -= 'a' - 'A'; + setApproveButtonMnemonic( vk ); + } + + /** @see JFileChooser#getFileSelectionMode() */ + public int getFileSelectionMode() { + return fileSelectionMode; + } + + /** @see JFileChooser#setFileSelectionMode(int) */ + public void setFileSelectionMode( int fileSelectionMode ) { + if( fileSelectionMode != FILES_ONLY && fileSelectionMode != DIRECTORIES_ONLY ) + throw new IllegalArgumentException( "Invalid file selection mode " + fileSelectionMode ); + + this.fileSelectionMode = fileSelectionMode; + } + + /** @see JFileChooser#isFileSelectionEnabled() */ + public boolean isFileSelectionEnabled() { + return fileSelectionMode == FILES_ONLY; + } + + /** @see JFileChooser#isDirectorySelectionEnabled() */ + public boolean isDirectorySelectionEnabled() { + return fileSelectionMode == DIRECTORIES_ONLY; + } + + /** @see JFileChooser#isMultiSelectionEnabled() */ + public boolean isMultiSelectionEnabled() { + return multiSelection; + } + + /** @see JFileChooser#setMultiSelectionEnabled(boolean) */ + public void setMultiSelectionEnabled( boolean multiSelection ) { + this.multiSelection = multiSelection; + } + + /** @see JFileChooser#isFileHidingEnabled() */ + public boolean isFileHidingEnabled() { + return useFileHiding; + } + + /** @see JFileChooser#setFileHidingEnabled(boolean) */ + public void setFileHidingEnabled( boolean useFileHiding ) { + this.useFileHiding = useFileHiding; + } + + /** @see JFileChooser#getCurrentDirectory() */ + public File getCurrentDirectory() { + if( currentDirectory == null ) { + // get current directory from state store + StateStore store = (stateStore != null) ? stateStore : inMemoryStateStore; + String path = store.get( buildStateKey( StateStore.KEY_CURRENT_DIRECTORY ), null ); + if( path != null ) + currentDirectory = getTraversableDirectory( FileSystemView.getFileSystemView().createFileObject( path ) ); + + // for compatibility with JFileChooser + if( currentDirectory == null ) + currentDirectory = getTraversableDirectory( FileSystemView.getFileSystemView().getDefaultDirectory() ); + } + + return currentDirectory; + } + + /** @see JFileChooser#setCurrentDirectory(File) */ + public void setCurrentDirectory( File dir ) { + currentDirectory = getTraversableDirectory( dir ); + } + + private File getTraversableDirectory( File dir ) { + if( dir == null ) + return null; + + // make sure to use existing (traversable) directory + FileSystemView fsv = FileSystemView.getFileSystemView(); + while( dir != null && !fsv.isTraversable( dir ) ) + dir = fsv.getParentDirectory( dir ); + return dir; + } + + /** @see JFileChooser#getSelectedFile() */ + public File getSelectedFile() { + return selectedFile; + } + + /** @see JFileChooser#setSelectedFile(File) */ + public void setSelectedFile( File file ) { + selectedFile = file; + + // for compatibility with JFileChooser + if( file != null && + file.isAbsolute() && + !FileSystemView.getFileSystemView().isParent( getCurrentDirectory(), file ) ) + setCurrentDirectory( file.getParentFile() ); + } + + /** @see JFileChooser#getSelectedFiles() */ + public File[] getSelectedFiles() { + return (selectedFiles != null) ? selectedFiles.clone() : new File[0]; + } + + /** @see JFileChooser#setSelectedFiles(File[]) */ + public void setSelectedFiles( File[] selectedFiles ) { + if( selectedFiles != null && selectedFiles.length > 0 ) { + this.selectedFiles = selectedFiles.clone(); + setSelectedFile( selectedFiles[0] ); + } else { + this.selectedFiles = null; + setSelectedFile( null ); + } + } + + /** @see JFileChooser#getChoosableFileFilters() */ + public FileFilter[] getChoosableFileFilters() { + return filters.toArray( new FileFilter[filters.size()] ); + } + + /** @see JFileChooser#addChoosableFileFilter(javax.swing.filechooser.FileFilter) */ + public void addChoosableFileFilter( FileFilter filter ) { + if( filter == getAcceptAllFileFilter() ) + keepAcceptAllAtEnd = false; + + if( filter == null || filters.contains( filter ) ) + return; + + if( !(filter instanceof FileNameExtensionFilter) && !(filter instanceof AcceptAllFileFilter) ) + throw new IllegalArgumentException( "Filter class not supported: " + filter.getClass().getName() ); + + // either insert filter before "All Files" filter, or append to the end + int size = filters.size(); + if( keepAcceptAllAtEnd && size > 0 && (filters.get( size - 1 ) == getAcceptAllFileFilter()) ) + filters.add( size - 1, filter ); + else + filters.add( filter ); + + // initialize current filter + if( fileFilter == null || (filters.size() == 2 && filters.get( 1 ) == getAcceptAllFileFilter()) ) + setFileFilter( filter ); + } + + /** @see JFileChooser#removeChoosableFileFilter(javax.swing.filechooser.FileFilter) */ + public boolean removeChoosableFileFilter( FileFilter filter ) { + if( !filters.remove( filter ) ) + return false; + + // update current filter if necessary + if( filter == getFileFilter() ) { + if( isAcceptAllFileFilterUsed() && filter != getAcceptAllFileFilter() ) + setFileFilter( getAcceptAllFileFilter() ); + else + setFileFilter( !filters.isEmpty() ? filters.get( 0 ) : null ); + } + + return true; + } + + /** @see JFileChooser#resetChoosableFileFilters() */ + public void resetChoosableFileFilters() { + filters.clear(); + setFileFilter( null ); + if( isAcceptAllFileFilterUsed() ) { + addChoosableFileFilter( getAcceptAllFileFilter() ); + keepAcceptAllAtEnd = true; + } + } + + /** @see JFileChooser#getAcceptAllFileFilter() */ + public FileFilter getAcceptAllFileFilter() { + if( acceptAllFileFilter == null ) + acceptAllFileFilter = new AcceptAllFileFilter(); + return acceptAllFileFilter; + } + + /** @see JFileChooser#isAcceptAllFileFilterUsed() */ + public boolean isAcceptAllFileFilterUsed() { + return useAcceptAllFileFilter; + } + + /** @see JFileChooser#setAcceptAllFileFilterUsed(boolean) */ + public void setAcceptAllFileFilterUsed( boolean acceptAll ) { + useAcceptAllFileFilter = acceptAll; + + removeChoosableFileFilter( getAcceptAllFileFilter() ); + if( acceptAll ) { + addChoosableFileFilter( getAcceptAllFileFilter() ); + keepAcceptAllAtEnd = true; + } + } + + /** @see JFileChooser#getFileFilter() */ + public FileFilter getFileFilter() { + return fileFilter; + } + + /** @see JFileChooser#setFileFilter(javax.swing.filechooser.FileFilter) */ + public void setFileFilter( FileFilter filter ) { + this.fileFilter = filter; + } + + private int indexOfCurrentFilter() { + return filters.indexOf( fileFilter ); + } + + private boolean hasOnlyAcceptAll() { + return filters.size() == 1 && filters.get( 0 ) == getAcceptAllFileFilter(); + } + + public ApproveCallback getApproveCallback() { + return approveCallback; + } + + /** + * Sets a callback that is invoked when user presses "OK" button (or double-clicks a file). + * The file dialog is still open. + * If the callback returns {@link #CANCEL_OPTION}, then the file dialog stays open. + * If it returns {@link #APPROVE_OPTION} (or any value other than {@link #CANCEL_OPTION}), + * the file dialog is closed and the {@code show...Dialog()} methods return that value. + *

+ * The callback has two parameters: + *

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

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

{@code
+	 * chooser.setApproveCallback( (selectedFiles, context) -> {
+	 *     if( !selectedFiles[0].getName().startsWith( "blabla" ) ) {
+	 *         context.showMessageDialog( JOptionPane.WARNING_MESSAGE,
+	 *             "File name must start with 'blabla' :)", null, 0 );
+	 *         return SystemFileChooser.CANCEL_OPTION;
+	 *     }
+	 *     return SystemFileChooser.APPROVE_OPTION;
+	 * } );
+	 * }
+ * + * @see ApproveContext + * @see JFileChooser#approveSelection() + */ + public void setApproveCallback( ApproveCallback approveCallback ) { + this.approveCallback = approveCallback; + } + + @SuppressWarnings( "unchecked" ) + public T getPlatformProperty( String key ) { + return (platformProperties != null) ? (T) platformProperties.get( key ) : null; + } + + /** + * Set a platform specific file dialog property. + *

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

{@code
+	 * chooser.putPlatformProperty( SystemFileChooser.WINDOWS_FILE_NAME_LABEL, "My filename label:" );
+	 * chooser.putPlatformProperty( SystemFileChooser.MAC_TREATS_FILE_PACKAGES_AS_DIRECTORIES, true );
+	 * chooser.putPlatformProperty( SystemFileChooser.LINUX_OPTIONS_CLEAR,
+	 *     FlatNativeLinuxLibrary.FC_create_folders | FlatNativeLinuxLibrary.FC_do_overwrite_confirmation );
+	 * }
+ */ + public void putPlatformProperty( String key, Object value ) { + if( platformProperties == null ) + platformProperties = new HashMap<>(); + + if( value != null ) + platformProperties.put( key, value ); + else + platformProperties.remove( key ); + } + + private int getPlatformOptions( String key, int optionsBlocked ) { + Object value = getPlatformProperty( key ); + return (value instanceof Integer) ? (Integer) value & ~optionsBlocked : 0; + } + + /** + * Returns state storage used to persist file chooser state (e.g. last used directory). + * Or {@code null} if there is no state storage (the default). + */ + public static StateStore getStateStore() { + return stateStore; + } + + /** + * Sets state storage used to persist file chooser state (e.g. last used directory). + */ + public static void setStateStore( StateStore stateStore ) { + SystemFileChooser.stateStore = stateStore; + } + + /** + * Returns the ID used to prefix keys in state storage. Or {@code null} (the default). + */ + public String getStateStoreID() { + return stateStoreID; + } + + /** + * Sets the ID used to prefix keys in state storage. Or {@code null} (the default). + *

+ * By specifying an ID, an application can have different persisted states + * for different kinds of file dialogs within the application. E.g. Import/Export + * file dialogs could use a different ID then Open/Save file dialogs. + */ + public void setStateStoreID( String stateStoreID ) { + this.stateStoreID = stateStoreID; + } + + private String buildStateKey( String key ) { + return (stateStoreID != null) ? stateStoreID + '.' + key : key; + } + + private int showDialogImpl( Component parent ) { + Window owner = (parent instanceof Window) + ? (Window) parent + : (parent != null) ? SwingUtilities.windowForComponent( parent ) : null; + if( owner == null ) + owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow(); + + approveResult = APPROVE_OPTION; + File[] files = getProvider().showDialog( owner, this ); + setSelectedFiles( files ); + if( files == null ) + return CANCEL_OPTION; + + // remember current directory in state store + File currentDirectory = getCurrentDirectory(); + StateStore store = (stateStore != null) ? stateStore : inMemoryStateStore; + store.put( buildStateKey( StateStore.KEY_CURRENT_DIRECTORY ), + (currentDirectory != null) ? currentDirectory.getAbsolutePath() : null ); + + return approveResult; + } + + private FileChooserProvider getProvider() { + if( !FlatSystemProperties.getBoolean( FlatSystemProperties.USE_SYSTEM_FILE_CHOOSER, true ) ) + return new SwingFileChooserProvider(); + + if( SystemInfo.isWindows_10_orLater && FlatNativeWindowsLibrary.isLoaded() ) + return new WindowsFileChooserProvider(); + else if( SystemInfo.isMacOS && FlatNativeMacLibrary.isLoaded() ) + return new MacFileChooserProvider(); + else if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isLoaded() && FlatNativeLinuxLibrary.isGtk3Available() ) + return new LinuxFileChooserProvider(); + else // unknown platform or FlatLaf native library not loaded + return new SwingFileChooserProvider(); + } + + //---- interface FileChooserProvider -------------------------------------- + + private interface FileChooserProvider { + File[] showDialog( Window owner, SystemFileChooser fc ); + } + + //---- class SystemFileChooserProvider ------------------------------------ + + private static abstract class SystemFileChooserProvider + implements FileChooserProvider + { + @Override + public File[] showDialog( Window owner, SystemFileChooser fc ) { + AtomicReference filenamesRef = new AtomicReference<>(); + + // create secondary event look and invoke system file dialog on a new thread + SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); + new Thread( () -> { + filenamesRef.set( showSystemDialog( owner, fc ) ); + secondaryLoop.exit(); + }, "FlatLaf SystemFileChooser" ).start(); + secondaryLoop.enter(); + + String[] filenames = filenamesRef.get(); + + // fallback to Swing file chooser if system file dialog failed or is not available + if( filenames == null ) + return new SwingFileChooserProvider().showDialog( owner, fc ); + + // canceled? + if( filenames.length == 0 ) + return null; + + // convert file names to file objects + return filenames2files( filenames ); + } + + abstract String[] showSystemDialog( Window owner, SystemFileChooser fc ); + + boolean invokeApproveCallback( SystemFileChooser fc, String[] files, ApproveContext context ) { + if( files == null || files.length == 0 ) + return false; // should never happen + + ApproveCallback approveCallback = fc.getApproveCallback(); + int result = approveCallback.approve( filenames2files( files ), context ); + if( result == CANCEL_OPTION ) + return false; + + fc.approveResult = result; + return true; + } + + private static File[] filenames2files( String[] filenames ) { + FileSystemView fsv = FileSystemView.getFileSystemView(); + File[] files = new File[filenames.length]; + for( int i = 0; i < filenames.length; i++ ) + files[i] = fsv.createFileObject( filenames[i] ); + return files; + } + } + + //---- class WindowsFileChooserProvider ----------------------------------- + + private static class WindowsFileChooserProvider + extends SystemFileChooserProvider + { + @Override + String[] showSystemDialog( Window owner, SystemFileChooser fc ) { + boolean open = (fc.getDialogType() == OPEN_DIALOG); + String approveButtonText = fc.getApproveButtonText(); + int approveButtonMnemonic = fc.getApproveButtonMnemonic(); + String fileName = null; + String folder = null; + String saveAsItem = null; + + // approve button text and mnemonic + if( approveButtonText != null ) { + approveButtonText = approveButtonText.replace( "&", "&&" ); + if( approveButtonMnemonic > 0 ) { + int mnemonicIndex = approveButtonText.toUpperCase( Locale.ENGLISH ).indexOf( approveButtonMnemonic ); + if( mnemonicIndex >= 0 ) { + approveButtonText = approveButtonText.substring( 0, mnemonicIndex ) + + '&' + approveButtonText.substring( mnemonicIndex ); + } + } + } + + // paths + File currentDirectory = fc.getCurrentDirectory(); + File selectedFile = fc.getSelectedFile(); + if( selectedFile != null ) { + if( selectedFile.exists() && !open ) + saveAsItem = selectedFile.getAbsolutePath(); + else { + fileName = selectedFile.getName(); + folder = selectedFile.getParent(); + } + } else if( currentDirectory != null ) + folder = currentDirectory.getAbsolutePath(); + + // options + int optionsBlocked = FlatNativeWindowsLibrary.FOS_PICKFOLDERS + | FlatNativeWindowsLibrary.FOS_ALLOWMULTISELECT + | FlatNativeWindowsLibrary.FOS_FORCESHOWHIDDEN; + int optionsSet = fc.getPlatformOptions( WINDOWS_OPTIONS_SET, optionsBlocked ); + int optionsClear = fc.getPlatformOptions( WINDOWS_OPTIONS_CLEAR, optionsBlocked ); + if( (optionsClear & FlatNativeWindowsLibrary.FOS_OVERWRITEPROMPT) == 0 ) + optionsSet |= FlatNativeWindowsLibrary.FOS_OVERWRITEPROMPT; + if( fc.isDirectorySelectionEnabled() ) + optionsSet |= FlatNativeWindowsLibrary.FOS_PICKFOLDERS; + if( fc.isMultiSelectionEnabled() ) + optionsSet |= FlatNativeWindowsLibrary.FOS_ALLOWMULTISELECT; + if( !fc.isFileHidingEnabled() ) + optionsSet |= FlatNativeWindowsLibrary.FOS_FORCESHOWHIDDEN; + + // filter + int fileTypeIndex = 0; + ArrayList fileTypes = new ArrayList<>(); + if( !fc.isDirectorySelectionEnabled() ) { + if( !fc.hasOnlyAcceptAll() ) { + fileTypeIndex = fc.indexOfCurrentFilter(); + for( FileFilter filter : fc.getChoosableFileFilters() ) { + if( filter instanceof FileNameExtensionFilter ) { + fileTypes.add( filter.getDescription() ); + fileTypes.add( "*." + String.join( ";*.", ((FileNameExtensionFilter)filter).getExtensions() ) ); + } else if( filter instanceof AcceptAllFileFilter ) { + fileTypes.add( filter.getDescription() ); + fileTypes.add( "*.*" ); + } + } + } + + // if there are no file types + // - for Save dialog add "All Files", otherwise Windows would show an empty "Save as type" combobox + // - for Open dialog, Windows hides the combobox + if( !open && fileTypes.isEmpty() ) { + fileTypes.add( fc.getAcceptAllFileFilter().getDescription() ); + fileTypes.add( "*.*" ); + } + } + + // callback + FlatNativeWindowsLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null) + ? (files, hwndFileDialog) -> { + return invokeApproveCallback( fc, files, new WindowsApproveContext( hwndFileDialog ) ); + } : null; + + // show system file dialog + return FlatNativeWindowsLibrary.showFileChooser( owner, open, + fc.getDialogTitle(), approveButtonText, + fc.getPlatformProperty( WINDOWS_FILE_NAME_LABEL ), + fileName, folder, saveAsItem, + fc.getPlatformProperty( WINDOWS_DEFAULT_FOLDER ), + fc.getPlatformProperty( WINDOWS_DEFAULT_EXTENSION ), + optionsSet, optionsClear, callback, + fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); + } + + //---- class WindowsApproveContext ---- + + private static class WindowsApproveContext + extends ApproveContext + { + private final long hwndFileDialog; + + WindowsApproveContext( long hwndFileDialog ) { + this.hwndFileDialog = hwndFileDialog; + } + + @Override + public int showMessageDialog( int messageType, String primaryText, + String secondaryText, int defaultButton, String... buttons ) + { + // concat primary and secondary texts + if( secondaryText != null ) + primaryText = primaryText + "\n\n" + secondaryText; + + // button menmonics ("&" -> "&&", "__" -> "_", "_" -> "&") + for( int i = 0; i < buttons.length; i++ ) + buttons[i] = buttons[i].replace( "&", "&&" ).replace( "__", "\u0001" ).replace( '_', '&' ).replace( '\u0001', '_' ); + + // use "OK" button if no buttons given + if( buttons.length == 0 ) + buttons = new String[] { UIManager.getString( "OptionPane.okButtonText", Locale.getDefault() ) }; + + return FlatNativeWindowsLibrary.showMessageDialog( hwndFileDialog, + messageType, null, primaryText, defaultButton, buttons ); + } + } + } + + //---- class MacFileChooserProvider --------------------------------------- + + private static class MacFileChooserProvider + extends SystemFileChooserProvider + { + @Override + String[] showSystemDialog( Window owner, SystemFileChooser fc ) { + int dark = FlatLaf.isLafDark() ? 1 : 0; + boolean open = (fc.getDialogType() == OPEN_DIALOG); + String nameFieldStringValue = null; + String directoryURL = null; + + // paths + File currentDirectory = fc.getCurrentDirectory(); + File selectedFile = fc.getSelectedFile(); + if( selectedFile != null ) { + if( selectedFile.isDirectory() ) + directoryURL = selectedFile.getAbsolutePath(); + else { + nameFieldStringValue = selectedFile.getName(); + directoryURL = selectedFile.getParent(); + } + } else if( currentDirectory != null ) + directoryURL = currentDirectory.getAbsolutePath(); + + // options + int optionsBlocked = FlatNativeMacLibrary.FC_canChooseFiles + | FlatNativeMacLibrary.FC_canChooseDirectories + | FlatNativeMacLibrary.FC_allowsMultipleSelection + | FlatNativeMacLibrary.FC_showsHiddenFiles; + int optionsSet = fc.getPlatformOptions( MAC_OPTIONS_SET, optionsBlocked ); + int optionsClear = fc.getPlatformOptions( MAC_OPTIONS_CLEAR, optionsBlocked ); + if( (optionsClear & FlatNativeMacLibrary.FC_accessoryViewDisclosed) == 0 ) + optionsSet |= FlatNativeMacLibrary.FC_accessoryViewDisclosed; + if( fc.isDirectorySelectionEnabled() ) { + optionsSet |= FlatNativeMacLibrary.FC_canChooseDirectories | FlatNativeMacLibrary.FC_canCreateDirectories; + optionsClear |= FlatNativeMacLibrary.FC_canChooseFiles; + open = true; + } + if( fc.isMultiSelectionEnabled() ) + optionsSet |= FlatNativeMacLibrary.FC_allowsMultipleSelection; + if( !fc.isFileHidingEnabled() ) + optionsSet |= FlatNativeMacLibrary.FC_showsHiddenFiles; + if( Boolean.TRUE.equals( fc.getPlatformProperty( MAC_TREATS_FILE_PACKAGES_AS_DIRECTORIES ) ) ) + optionsSet |= FlatNativeMacLibrary.FC_treatsFilePackagesAsDirectories; + + // filter + int fileTypeIndex = 0; + ArrayList fileTypes = new ArrayList<>(); + if( !fc.isDirectorySelectionEnabled() && !fc.hasOnlyAcceptAll() ) { + fileTypeIndex = fc.indexOfCurrentFilter(); + for( FileFilter filter : fc.getChoosableFileFilters() ) { + if( filter instanceof FileNameExtensionFilter ) { + fileTypes.add( filter.getDescription() ); + for( String ext : ((FileNameExtensionFilter)filter).getExtensions() ) + fileTypes.add( ext ); + fileTypes.add( null ); + } else if( filter instanceof AcceptAllFileFilter ) { + fileTypes.add( filter.getDescription() ); + fileTypes.add( "*" ); + fileTypes.add( null ); + } + } + } + + // callback + FlatNativeMacLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null) + ? (files, hwndFileDialog) -> { + return invokeApproveCallback( fc, files, new MacApproveContext( hwndFileDialog ) ); + } : null; + + // show system file dialog + return FlatNativeMacLibrary.showFileChooser( owner, dark, open, + fc.getDialogTitle(), fc.getApproveButtonText(), + fc.getPlatformProperty( MAC_MESSAGE ), + fc.getPlatformProperty( MAC_FILTER_FIELD_LABEL ), + fc.getPlatformProperty( MAC_NAME_FIELD_LABEL ), + nameFieldStringValue, directoryURL, optionsSet, optionsClear, callback, + fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); + } + + //---- class MacApproveContext ---- + + private static class MacApproveContext + extends ApproveContext + { + private final long hwndFileDialog; + + MacApproveContext( long hwndFileDialog ) { + this.hwndFileDialog = hwndFileDialog; + } + + @Override + public int showMessageDialog( int messageType, String primaryText, + String secondaryText, int defaultButton, String... buttons ) + { + // remove button menmonics ("__" -> "_", "_" -> "") + for( int i = 0; i < buttons.length; i++ ) + buttons[i] = buttons[i].replace( "__", "\u0001" ).replace( "_", "" ).replace( "\u0001", "_" ); + + return FlatNativeMacLibrary.showMessageDialog( hwndFileDialog, + messageType, primaryText, secondaryText, defaultButton, buttons ); + } + } + } + + //---- class LinuxFileChooserProvider ------------------------------------- + + private static class LinuxFileChooserProvider + extends SystemFileChooserProvider + { + @Override + String[] showSystemDialog( Window owner, SystemFileChooser fc ) { + boolean open = (fc.getDialogType() == OPEN_DIALOG); + String approveButtonText = fc.getApproveButtonText(); + int approveButtonMnemonic = fc.getApproveButtonMnemonic(); + String currentName = null; + String currentFolder = null; + + // approve button text and mnemonic + if( approveButtonText != null ) { + approveButtonText = approveButtonText.replace( "_", "__" ); + if( approveButtonMnemonic > 0 ) { + int mnemonicIndex = approveButtonText.toUpperCase( Locale.ENGLISH ).indexOf( approveButtonMnemonic ); + if( mnemonicIndex >= 0 ) { + approveButtonText = approveButtonText.substring( 0, mnemonicIndex ) + + '_' + approveButtonText.substring( mnemonicIndex ); + } + } + } + + // paths + File currentDirectory = fc.getCurrentDirectory(); + File selectedFile = fc.getSelectedFile(); + if( selectedFile != null ) { + if( selectedFile.isDirectory() ) + currentFolder = selectedFile.getAbsolutePath(); + else { + currentName = selectedFile.getName(); + currentFolder = selectedFile.getParent(); + } + } else if( currentDirectory != null ) + currentFolder = currentDirectory.getAbsolutePath(); + + // options + int optionsBlocked = FlatNativeLinuxLibrary.FC_select_folder + | FlatNativeLinuxLibrary.FC_select_multiple + | FlatNativeLinuxLibrary.FC_show_hidden; + int optionsSet = fc.getPlatformOptions( LINUX_OPTIONS_SET, optionsBlocked ); + int optionsClear = fc.getPlatformOptions( LINUX_OPTIONS_CLEAR, optionsBlocked ); + if( (optionsClear & FlatNativeLinuxLibrary.FC_do_overwrite_confirmation) == 0 ) + optionsSet |= FlatNativeLinuxLibrary.FC_do_overwrite_confirmation; + if( fc.isDirectorySelectionEnabled() ) + optionsSet |= FlatNativeLinuxLibrary.FC_select_folder; + if( fc.isMultiSelectionEnabled() ) + optionsSet |= FlatNativeLinuxLibrary.FC_select_multiple; + if( !fc.isFileHidingEnabled() ) + optionsSet |= FlatNativeLinuxLibrary.FC_show_hidden; + else // necessary because GTK seems to be remember last state and re-use it for new file dialogs + optionsClear |= FlatNativeLinuxLibrary.FC_show_hidden; + + // filter + int fileTypeIndex = 0; + ArrayList fileTypes = new ArrayList<>(); + if( !fc.isDirectorySelectionEnabled() && !fc.hasOnlyAcceptAll() ) { + fileTypeIndex = fc.indexOfCurrentFilter(); + for( FileFilter filter : fc.getChoosableFileFilters() ) { + if( filter instanceof FileNameExtensionFilter ) { + fileTypes.add( filter.getDescription() ); + for( String ext : ((FileNameExtensionFilter)filter).getExtensions() ) + fileTypes.add( caseInsensitiveGlobPattern( ext ) ); + fileTypes.add( null ); + } else if( filter instanceof AcceptAllFileFilter ) { + fileTypes.add( filter.getDescription() ); + fileTypes.add( "*" ); + fileTypes.add( null ); + } + } + } + + // callback + FlatNativeLinuxLibrary.FileChooserCallback callback = (fc.getApproveCallback() != null) + ? (files, hwndFileDialog) -> { + return invokeApproveCallback( fc, files, new LinuxApproveContext( hwndFileDialog ) ); + } : null; + + // show system file dialog + return FlatNativeLinuxLibrary.showFileChooser( owner, open, + fc.getDialogTitle(), approveButtonText, currentName, currentFolder, + optionsSet, optionsClear, callback, + fileTypeIndex, fileTypes.toArray( new String[fileTypes.size()] ) ); + } + + private String caseInsensitiveGlobPattern( String ext ) { + StringBuilder buf = new StringBuilder(); + buf.append( "*." ); + int len = ext.length(); + for( int i = 0; i < len; i++ ) { + char ch = ext.charAt( i ); + if( Character.isLetter( ch ) ) { + buf.append( '[' ) + .append( Character.toLowerCase( ch ) ) + .append( Character.toUpperCase( ch ) ) + .append( ']' ); + } else + buf.append( ch ); + } + return buf.toString(); + } + + //---- class LinuxApproveContext ---- + + private static class LinuxApproveContext + extends ApproveContext + { + private final long hwndFileDialog; + + LinuxApproveContext( long hwndFileDialog ) { + this.hwndFileDialog = hwndFileDialog; + } + + @Override + public int showMessageDialog( int messageType, String primaryText, + String secondaryText, int defaultButton, String... buttons ) + { + return FlatNativeLinuxLibrary.showMessageDialog( hwndFileDialog, + messageType, primaryText, secondaryText, defaultButton, buttons ); + } + } + } + + //---- class SwingFileChooserProvider ------------------------------------- + + private static class SwingFileChooserProvider + implements FileChooserProvider + { + @Override + public File[] showDialog( Window owner, SystemFileChooser fc ) { + JFileChooser chooser = new JFileChooser() { + @Override + public void approveSelection() { + File[] files = isMultiSelectionEnabled() + ? getSelectedFiles() + : new File[] { getSelectedFile() }; + if( files == null || files.length == 0 ) + return; // should never happen + + if( getDialogType() == OPEN_DIALOG || isDirectorySelectionEnabled() ) { + if( !checkMustExist( this, files ) ) + return; + } else { + if( !checkOverwrite( this, files ) ) + return; + } + + // callback + ApproveCallback approveCallback = fc.getApproveCallback(); + if( approveCallback != null ) { + int result = approveCallback.approve( files, new SwingApproveContext( this ) ); + if( result == CANCEL_OPTION ) + return; + + fc.approveResult = result; + } + + super.approveSelection(); + } + }; + + chooser.setDialogType( fc.getDialogType() ); + chooser.setDialogTitle( fc.getDialogTitle() ); + chooser.setApproveButtonText( fc.getApproveButtonText() ); + chooser.setApproveButtonMnemonic( fc.getApproveButtonMnemonic() ); + chooser.setFileSelectionMode( fc.getFileSelectionMode() ); + chooser.setMultiSelectionEnabled( fc.isMultiSelectionEnabled() ); + chooser.setFileHidingEnabled( fc.isFileHidingEnabled() ); + + // system file dialogs do not support multi-selection for Save File dialogs + if( chooser.isMultiSelectionEnabled() && + chooser.getDialogType() == JFileChooser.SAVE_DIALOG && + !chooser.isDirectorySelectionEnabled() ) + chooser.setMultiSelectionEnabled( false ); + + // filter + if( !fc.isDirectorySelectionEnabled() && !fc.hasOnlyAcceptAll() ) { + FileFilter currentFilter = fc.getFileFilter(); + for( FileFilter filter : fc.getChoosableFileFilters() ) { + javax.swing.filechooser.FileFilter jfilter = convertFilter( filter, chooser ); + if( jfilter == null ) + continue; + + chooser.addChoosableFileFilter( jfilter ); + if( filter == currentFilter ) { + chooser.setFileFilter( jfilter ); + currentFilter = null; + } + } + if( currentFilter != null ) { + javax.swing.filechooser.FileFilter jfilter = convertFilter( currentFilter, chooser ); + if( jfilter != null ) + chooser.setFileFilter( jfilter ); + } + } + + // paths + chooser.setCurrentDirectory( fc.getCurrentDirectory() ); + chooser.setSelectedFile( fc.getSelectedFile() ); + + if( chooser.showDialog( owner, null ) != JFileChooser.APPROVE_OPTION ) + return null; + + return chooser.isMultiSelectionEnabled() + ? chooser.getSelectedFiles() + : new File[] { chooser.getSelectedFile() }; + } + + private javax.swing.filechooser.FileFilter convertFilter( FileFilter filter, JFileChooser chooser ) { + if( filter instanceof FileNameExtensionFilter ) { + return new javax.swing.filechooser.FileNameExtensionFilter( + ((FileNameExtensionFilter)filter).getDescription(), + ((FileNameExtensionFilter)filter).getExtensions() ); + } else if( filter instanceof AcceptAllFileFilter ) + return chooser.getAcceptAllFileFilter(); + else + return null; + } + + private static boolean checkMustExist( JFileChooser chooser, File[] files ) { + for( File file : files ) { + if( !file.exists() ) { + String title = chooser.getDialogTitle(); + JOptionPane.showMessageDialog( chooser, + file.getName() + (chooser.isDirectorySelectionEnabled() + ? "\nPath does not exist.\nCheck the path and try again." + : "\nFile not found.\nCheck the file name and try again."), + (title != null) ? title : "Open", + JOptionPane.WARNING_MESSAGE ); + return false; + } + } + return true; + } + + private static boolean checkOverwrite( JFileChooser chooser, File[] files ) { + for( File file : files ) { + if( file.exists() ) { + String title = chooser.getDialogTitle(); + Locale l = chooser.getLocale(); + Object[] options = { + UIManager.getString( "OptionPane.yesButtonText", l ), + UIManager.getString( "OptionPane.noButtonText", l ), }; + int result = JOptionPane.showOptionDialog( chooser, + file.getName() + " already exists.\nDo you want to replace it?", + "Confirm " + (title != null ? title : "Save"), + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, + null, options, options[1] ); + return (result == 0); + } + } + return true; + } + + //---- class SwingApproveContext ---- + + private static class SwingApproveContext + extends ApproveContext + { + private final JFileChooser chooser; + + SwingApproveContext( JFileChooser chooser ) { + this.chooser = chooser; + } + + @Override + public int showMessageDialog( int messageType, String primaryText, + String secondaryText, int defaultButton, String... buttons ) + { + // title + String title = chooser.getDialogTitle(); + if( title == null ) { + Window window = SwingUtilities.windowForComponent( chooser ); + if( window instanceof JDialog ) + title = ((JDialog)window).getTitle(); + } + + // concat primary and secondary texts + if( secondaryText != null ) + primaryText = primaryText + "\n\n" + secondaryText; + + // remove button menmonics ("__" -> "_", "_" -> "") + for( int i = 0; i < buttons.length; i++ ) + buttons[i] = buttons[i].replace( "__", "\u0001" ).replace( "_", "" ).replace( "\u0001", "_" ); + + // use "OK" button if no buttons given + if( buttons.length == 0 ) + buttons = new String[] { UIManager.getString( "OptionPane.okButtonText", Locale.getDefault() ) }; + + return JOptionPane.showOptionDialog( chooser, + primaryText, title, JOptionPane.YES_NO_OPTION, messageType, + null, buttons, buttons[Math.min( Math.max( defaultButton, 0 ), buttons.length - 1 )] ); + } + } + } + + //---- class FileFilter --------------------------------------------------- + + /** @see javax.swing.filechooser.FileFilter */ + public static abstract class FileFilter { + /** @see javax.swing.filechooser.FileFilter#getDescription() */ + public abstract String getDescription(); + } + + //---- class FileNameExtensionFilter -------------------------------------- + + /** @see javax.swing.filechooser.FileNameExtensionFilter */ + public static final class FileNameExtensionFilter + extends FileFilter + { + private final String description; + private final String[] extensions; + + /** @see javax.swing.filechooser.FileNameExtensionFilter#FileNameExtensionFilter(String, String...) */ + public FileNameExtensionFilter( String description, String... extensions ) { + if( extensions == null || extensions.length == 0 ) + throw new IllegalArgumentException( "Missing extensions" ); + for( String extension : extensions ) { + if( extension == null || extension.isEmpty() ) + throw new IllegalArgumentException( "Extension is null or empty string" ); + if( extension.indexOf( '.' ) >= 0 || extension.indexOf( '*' ) >= 0 ) + throw new IllegalArgumentException( "Extension must not contain '.' or '*'" ); + } + + this.description = description; + this.extensions = extensions.clone(); + } + + /** @see javax.swing.filechooser.FileNameExtensionFilter#getDescription() */ + @Override + public String getDescription() { + return description; + } + + /** @see javax.swing.filechooser.FileNameExtensionFilter#getExtensions() */ + public String[] getExtensions() { + return extensions.clone(); + } + + @Override + public String toString() { + return super.toString() + "[description=" + description + " extensions=" + Arrays.toString( extensions ) + "]"; + } + } + + //---- class AcceptAllFileFilter ------------------------------------------ + + private static final class AcceptAllFileFilter + extends FileFilter + { + @Override + public String getDescription() { + return UIManager.getString( "FileChooser.acceptAllFileFilterText" ); + } + } + + //---- class ApproveCallback ---------------------------------------------- + + public interface ApproveCallback { + /** + * @param selectedFiles one or more selected files + * @param context context object that provides additional methods + * @return If the callback returns {@link #CANCEL_OPTION}, then the file dialog stays open. + * If it returns {@link #APPROVE_OPTION} (or any other value other than {@link #CANCEL_OPTION}), + * the file dialog is closed and the {@code show...Dialog()} methods return that value. + */ + int approve( File[] selectedFiles, ApproveContext context ); + } + + //---- class ApproveContext ----------------------------------------------- + + public static abstract class ApproveContext { + /** + * Shows a modal (operating system) message dialog as child of the system file dialog. + *

+ * Use this instead of {@link JOptionPane} in approve callbacks. + * + * @param messageType type of message being displayed: + * {@link JOptionPane#ERROR_MESSAGE}, {@link JOptionPane#INFORMATION_MESSAGE}, + * {@link JOptionPane#WARNING_MESSAGE}, {@link JOptionPane#QUESTION_MESSAGE} or + * {@link JOptionPane#PLAIN_MESSAGE} + * @param primaryText primary text + * @param secondaryText secondary text; shown below of primary text; or {@code null} + * @param defaultButton index of the default button, which can be pressed using ENTER key + * @param buttons texts of the buttons; if no buttons given the a default "OK" button is shown. + * Use '_' for mnemonics (e.g. "_Choose") + * Use '__' for '_' character (e.g. "Choose__and__Quit"). + * @return index of pressed button; or -1 for ESC key + */ + public abstract int showMessageDialog( int messageType, String primaryText, + String secondaryText, int defaultButton, String... buttons ); + } + + //---- class StateStore --------------------------------------------------- + + /** + * Simple state storage used to persist file chooser state (e.g. last used directory). + * + * @see SystemFileChooser#setStateStore(StateStore) + * @see SystemFileChooser#setStateStoreID(String) + */ + public interface StateStore { + String KEY_CURRENT_DIRECTORY = "currentDirectory"; + + /** + * Returns the value for the given key, or the default value if there is no value stored. + */ + String get( String key, String def ); + + /** + * Stores the given key and value. If value is {@code null}, it is removed from the store. + */ + void put( String key, String value ); + } +} diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-arm64.dll b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-arm64.dll index 716527e57..9fbd790ac 100644 Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-arm64.dll and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-arm64.dll differ diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86.dll b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86.dll index fb0f8325d..96e41e992 100644 Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86.dll and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86.dll differ diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86_64.dll b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86_64.dll index e810d4a4e..6b3d15c51 100644 Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86_64.dll and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/flatlaf-windows-x86_64.dll differ diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-arm64.so b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-arm64.so index bcbafedca..39168077f 100755 Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-arm64.so and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-arm64.so differ diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-x86_64.so b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-x86_64.so index 3d0ddc5a2..a405e10c2 100644 Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-x86_64.so and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-x86_64.so differ diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-arm64.dylib b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-arm64.dylib index 5d683a244..5dc29eb0c 100755 Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-arm64.dylib and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-arm64.dylib differ diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib index b356acc91..88c461426 100755 Binary files a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib and b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib differ diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java index 54b0e33ec..e37112540 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java @@ -18,6 +18,7 @@ import java.awt.*; import java.awt.event.*; +import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -49,6 +50,7 @@ import com.formdev.flatlaf.util.ColorFunctions; import com.formdev.flatlaf.util.FontUtils; import com.formdev.flatlaf.util.LoggingFacade; +import com.formdev.flatlaf.util.SystemFileChooser; import com.formdev.flatlaf.util.SystemInfo; import net.miginfocom.layout.ConstraintParser; import net.miginfocom.layout.LC; @@ -172,6 +174,48 @@ private void saveAsActionPerformed() { chooser.showSaveDialog( this ); } + private void openSystemActionPerformed() { + SystemFileChooser chooser = new SystemFileChooser(); + chooser.setMultiSelectionEnabled( true ); + chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( + "Text Files", "txt", "md" ) ); + chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( + "PDF Files", "pdf" ) ); + chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( + "Archives", "zip", "tar", "jar", "7z" ) ); + + if( chooser.showOpenDialog( this ) != SystemFileChooser.APPROVE_OPTION ) + return; + + File[] files = chooser.getSelectedFiles(); + System.out.println( Arrays.toString( files ).replace( ",", "\n" ) ); + } + + private void saveAsSystemActionPerformed() { + SystemFileChooser chooser = new SystemFileChooser(); + chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( + "Text Files", "txt", "md" ) ); + chooser.addChoosableFileFilter( new SystemFileChooser.FileNameExtensionFilter( + "Images", "png", "gif", "jpg" ) ); + + if( chooser.showSaveDialog( this ) != SystemFileChooser.APPROVE_OPTION ) + return; + + File file = chooser.getSelectedFile(); + System.out.println( file ); + } + + private void selectFolderSystemActionPerformed() { + SystemFileChooser chooser = new SystemFileChooser(); + chooser.setFileSelectionMode( SystemFileChooser.DIRECTORIES_ONLY ); + + if( chooser.showOpenDialog( this ) != SystemFileChooser.APPROVE_OPTION ) + return; + + File directory = chooser.getSelectedFile(); + System.out.println( directory ); + } + private void exitActionPerformed() { dispose(); } @@ -508,6 +552,9 @@ private void initComponents() { JMenuItem newMenuItem = new JMenuItem(); JMenuItem openMenuItem = new JMenuItem(); JMenuItem saveAsMenuItem = new JMenuItem(); + JMenuItem openSystemMenuItem = new JMenuItem(); + JMenuItem saveAsSystemMenuItem = new JMenuItem(); + JMenuItem selectFolderSystemMenuItem = new JMenuItem(); JMenuItem closeMenuItem = new JMenuItem(); exitMenuItem = new JMenuItem(); JMenu editMenu = new JMenu(); @@ -608,6 +655,25 @@ private void initComponents() { fileMenu.add(saveAsMenuItem); fileMenu.addSeparator(); + //---- openSystemMenuItem ---- + openSystemMenuItem.setText("Open (System)..."); + openSystemMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()|KeyEvent.SHIFT_DOWN_MASK)); + openSystemMenuItem.addActionListener(e -> openSystemActionPerformed()); + fileMenu.add(openSystemMenuItem); + + //---- saveAsSystemMenuItem ---- + saveAsSystemMenuItem.setText("Save As (System)..."); + saveAsSystemMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()|KeyEvent.SHIFT_DOWN_MASK)); + saveAsSystemMenuItem.addActionListener(e -> saveAsSystemActionPerformed()); + fileMenu.add(saveAsSystemMenuItem); + + //---- selectFolderSystemMenuItem ---- + selectFolderSystemMenuItem.setText("Select Folder (System)..."); + selectFolderSystemMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()|KeyEvent.SHIFT_DOWN_MASK)); + selectFolderSystemMenuItem.addActionListener(e -> selectFolderSystemActionPerformed()); + fileMenu.add(selectFolderSystemMenuItem); + fileMenu.addSeparator(); + //---- closeMenuItem ---- closeMenuItem.setText("Close"); closeMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd index 08248c1d6..70bb49ac2 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "8.2.1.0.348" Java: "21.0.1" encoding: "UTF-8" +JFDML JFormDesigner: "8.3" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -182,6 +182,27 @@ new FormModel { "mnemonic": 83 addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "saveAsActionPerformed", false ) ) } ) + add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { + name: "separator9" + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "openSystemMenuItem" + "text": "Open (System)..." + "accelerator": static javax.swing.KeyStroke getKeyStroke( 79, 4291, false ) + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openSystemActionPerformed", false ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "saveAsSystemMenuItem" + "text": "Save As (System)..." + "accelerator": static javax.swing.KeyStroke getKeyStroke( 83, 4291, false ) + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "saveAsSystemActionPerformed", false ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "selectFolderSystemMenuItem" + "text": "Select Folder (System)..." + "accelerator": static javax.swing.KeyStroke getKeyStroke( 70, 4291, false ) + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "selectFolderSystemActionPerformed", false ) ) + } ) add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { name: "separator2" } ) diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java index 09768e3e6..18ce55e52 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java @@ -17,6 +17,7 @@ package com.formdev.flatlaf.demo; import java.awt.Dimension; +import java.util.prefs.Preferences; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.SwingUtilities; @@ -27,6 +28,7 @@ import com.formdev.flatlaf.fonts.jetbrains_mono.FlatJetBrainsMonoFont; import com.formdev.flatlaf.fonts.roboto.FlatRobotoFont; import com.formdev.flatlaf.fonts.roboto_mono.FlatRobotoMonoFont; +import com.formdev.flatlaf.util.SystemFileChooser; import com.formdev.flatlaf.util.SystemInfo; /** @@ -73,6 +75,28 @@ public static void main( String[] args ) { DemoPrefs.init( PREFS_ROOT_PATH ); DemoPrefs.initSystemScale(); + // SystemFileChooser state storage + SystemFileChooser.setStateStore( new SystemFileChooser.StateStore() { + private static final String KEY_PREFIX = "fileChooser."; + private final Preferences state = Preferences.userRoot().node( PREFS_ROOT_PATH ); + + @Override + public String get( String key, String def ) { + String value = state.get( KEY_PREFIX + key, def ); + System.out.println( "SystemFileChooser State GET " + key + " = " + value ); + return value; + } + + @Override + public void put( String key, String value ) { + System.out.println( "SystemFileChooser State PUT " + key + " = " + value ); + if( value != null ) + state.put( KEY_PREFIX + key, value ); + else + state.remove( KEY_PREFIX + key ); + } + } ); + SwingUtilities.invokeLater( () -> { // install fonts for lazy loading FlatInterFont.installLazy(); diff --git a/flatlaf-natives/flatlaf-natives-linux/README.md b/flatlaf-natives/flatlaf-natives-linux/README.md index 4be74776e..0054eefae 100644 --- a/flatlaf-natives/flatlaf-natives-linux/README.md +++ b/flatlaf-natives/flatlaf-natives-linux/README.md @@ -25,6 +25,7 @@ To build the library on Linux, some packages needs to be installed: - `build-essential` - GCC and development tools - `libxt-dev` - X11 toolkit development headers +- `libgtk-3-dev` - GTK 3 toolkit development headers - `g++-aarch64-linux-gnu` - GNU C++ compiler for the arm64 architecture (only on x86_64 Linux for cross-compiling for arm64 architecture) @@ -32,19 +33,39 @@ To build the library on Linux, some packages needs to be installed: ### Ubuntu ~~~ -sudo apt update -sudo apt install build-essential libxt-dev +sudo apt-get update +sudo apt-get install build-essential libxt-dev libgtk-3-dev ~~~ -Only on x86_64 Linux for cross-compiling for arm64 architecture: +#### Cross-compile for arm64 architecture on x86_64 Linux + +Only needed on x86_64 Linux if you want cross-compile for arm64 architecture: + +~~~ +sudo apt-get install g++-aarch64-linux-gnu +~~~ + +Download `libgtk-3.so` for arm64 architecture: + +~~~ +cd flatlaf-natives/flatlaf-natives-linux/lib/aarch64 +wget --no-verbose https://ports.ubuntu.com/pool/main/g/gtk%2b3.0/libgtk-3-0_3.24.18-1ubuntu1_arm64.deb +ar -x libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz +tar -xvf data.tar.xz --wildcards --to-stdout "./usr/lib/aarch64-linux-gnu/libgtk-3.so.0.*" > libgtk-3.so +rm libgtk-3-0_3.24.18-1ubuntu1_arm64.deb data.tar.xz +~~~ + + +### Fedora ~~~ -sudo apt install g++-aarch64-linux-gnu +sudo dnf group install c-development +sudo dnf install libXt-devel gtk3-devel ~~~ ### CentOS ~~~ -sudo yum install libXt-devel +sudo yum install libXt-devel gtk3-devel ~~~ diff --git a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts index dc9de1e59..d73ab05f2 100644 --- a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts @@ -65,15 +65,37 @@ tasks { includes.from( "${javaHome}/include", - "${javaHome}/include/linux" + "${javaHome}/include/linux", + + // for GTK + "/usr/include/gtk-3.0", + "/usr/include/glib-2.0", + if( name.contains( "X86-64" ) ) "/usr/lib/x86_64-linux-gnu/glib-2.0/include" + else "/usr/lib/aarch64-linux-gnu/glib-2.0/include", + "/usr/include/gdk-pixbuf-2.0", + "/usr/include/atk-1.0", + "/usr/include/cairo", + "/usr/include/pango-1.0", + "/usr/include/harfbuzz", ) compilerArgs.addAll( toolChain.map { when( it ) { - is Gcc, is Clang -> listOf() + is Gcc, is Clang -> listOf( "-fvisibility=hidden" ) else -> emptyList() } } ) + + doFirst { + // check required Java version + if( JavaVersion.current() < JavaVersion.VERSION_11 ) { + println() + println( "WARNING: Java 11 or later required to build Linux native library (running ${System.getProperty( "java.version" )})" ) + println( " Native library built with older Java versions throw following exception when running in Java 17+:" ) + println( " java.lang.UnsatisfiedLinkError: .../libjawt.so: version `SUNWprivate_1.1' not found" ) + println() + } + } } withType().configureEach { @@ -88,7 +110,7 @@ tasks { linkerArgs.addAll( toolChain.map { when( it ) { - is Gcc, is Clang -> listOf( "-L${jawtPath}", "-l${jawt}" ) + is Gcc, is Clang -> listOf( "-L${jawtPath}", "-l${jawt}", "-lgtk-3" ) else -> emptyList() } } ) @@ -128,7 +150,20 @@ tasks { "-I", "${javaHome}/include/linux", "-I", "$include", + // for GTK + "-I", "/usr/include/gtk-3.0", + "-I", "/usr/include/glib-2.0", + "-I", "/usr/lib/x86_64-linux-gnu/glib-2.0/include", + "-I", "/usr/include/gdk-pixbuf-2.0", + "-I", "/usr/include/atk-1.0", + "-I", "/usr/include/cairo", + "-I", "/usr/include/pango-1.0", + "-I", "/usr/include/harfbuzz", + "$src/ApiVersion.cpp", + "$src/GtkFileChooser.cpp", + "$src/GtkMessageDialog.cpp", + "$src/JNIUtils.cpp", "$src/X11WmUtils.cpp", ) } @@ -152,10 +187,15 @@ tasks { "-o", "$outDir/$libraryName", "$objDir/ApiVersion.o", + "$objDir/GtkFileChooser.o", + "$objDir/GtkMessageDialog.o", + "$objDir/JNIUtils.o", "$objDir/X11WmUtils.o", + "-lstdc++", "-L${layout.projectDirectory}/lib/aarch64", "-ljawt", + "-lgtk-3", ) doLast { diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/ApiVersion.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/ApiVersion.cpp index 02454fb3c..cdd701a90 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/ApiVersion.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/ApiVersion.cpp @@ -24,7 +24,7 @@ // increase this version if changing API or functionality of native library // also update version in Java class com.formdev.flatlaf.ui.FlatNativeLinuxLibrary -#define API_VERSION_LINUX 3001 +#define API_VERSION_LINUX 3002 //---- JNI methods ------------------------------------------------------------ diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp new file mode 100644 index 000000000..5603e4ae1 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkFileChooser.cpp @@ -0,0 +1,277 @@ +/* + * Copyright 2025 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include "JNIUtils.h" +#include "com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h" + +/** + * @author Karl Tauber + * @since 3.7 + */ + +// declare external methods +extern Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display** display_return ); + +// declare internal methods +static jobjectArray fileListToStringArray( JNIEnv* env, GSList* fileList ); + +//---- helper ----------------------------------------------------------------- + +#define isOptionSet( option ) ((optionsSet & com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_ ## option) != 0) +#define isOptionClear( option ) ((optionsClear & com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_ ## option) != 0) +#define isOptionSetOrClear( option ) isOptionSet( option ) || isOptionClear( option ) + +static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { + jclass stringClass = env->FindClass( "java/lang/String" ); + return env->NewObjectArray( count, stringClass, NULL ); +} + +static void initFilters( GtkFileChooser* chooser, JNIEnv* env, jint fileTypeIndex, jobjectArray fileTypes ) { + jint length = env->GetArrayLength( fileTypes ); + if( length <= 0 ) + return; + + GtkFileFilter* filter = NULL; + int filterIndex = 0; + for( int i = 0; i < length; i++ ) { + jstring jstr = (jstring) env->GetObjectArrayElement( fileTypes, i ); + if( jstr == NULL ) { + if( filter != NULL ) { + gtk_file_chooser_add_filter( chooser, filter ); + if( fileTypeIndex == filterIndex ) + gtk_file_chooser_set_filter( chooser, filter ); + filter = NULL; + filterIndex++; + } + continue; + } + + AutoReleaseStringUTF8 str( env, jstr ); + if( filter == NULL ) { + filter = gtk_file_filter_new(); + gtk_file_filter_set_name( filter, str ); + } else + gtk_file_filter_add_pattern( filter, str ); + } +} + +static GdkWindow* getGdkWindow( JNIEnv* env, jobject window ) { + // get the AWT + JAWT awt; + awt.version = JAWT_VERSION_1_4; + if( !JAWT_GetAWT( env, &awt ) ) + return NULL; + + // get Xlib window and display from AWT window + Display* display; + Window w = getWindowHandle( env, &awt, window, &display ); + if( w == 0 ) + return NULL; + + // based on GetAllocNativeWindowHandle() from https://github.com/btzy/nativefiledialog-extended + // https://github.com/btzy/nativefiledialog-extended/blob/29e3bcb578345b9fa345d1d7683f00c150565ca3/src/nfd_gtk.cpp#L384-L437 + GdkDisplay* gdkDisplay = gdk_x11_lookup_xdisplay( display ); + if( gdkDisplay == NULL ) { + // search for existing X11 display (there should only be one, even if multiple screens are connected) + GdkDisplayManager* displayManager = gdk_display_manager_get(); + GSList* displays = gdk_display_manager_list_displays( displayManager ); + for( GSList* l = displays; l; l = l->next ) { + if( GDK_IS_X11_DISPLAY( l->data ) ) { + gdkDisplay = GDK_DISPLAY( l->data ); + break; + } + } + g_slist_free( displays ); + + // create our own X11 display + if( gdkDisplay == NULL ) { + gdk_set_allowed_backends( "x11" ); + gdkDisplay = gdk_display_manager_open_display( displayManager, NULL ); + gdk_set_allowed_backends( NULL ); + + if( gdkDisplay == NULL ) + return NULL; + } + } + + return gdk_x11_window_foreign_new_for_display( gdkDisplay, w ); +} + +static void handle_realize( GtkWidget* dialog, gpointer data ) { + GdkWindow* gdkOwner = static_cast( data ); + + // make file dialog a transient of owner window, + // which centers file dialog on owner and keeps file dialog above owner + gdk_window_set_transient_for( gtk_widget_get_window( dialog ), gdkOwner ); + + // necessary because gdk_x11_window_foreign_new_for_display() increases the reference counter + g_object_unref( gdkOwner ); +} + +struct ResponseData { + JNIEnv* env; + jobject callback; + GSList* fileList; + + ResponseData( JNIEnv* _env, jobject _callback ) { + env = _env; + callback = _callback; + fileList = NULL; + } +}; + +static void handle_response( GtkWidget* dialog, gint responseId, gpointer data ) { + // get filenames if user pressed OK + if( responseId == GTK_RESPONSE_ACCEPT ) { + ResponseData *response = static_cast( data ); + if( response->callback != NULL ) { + GSList* fileList = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER( dialog ) ); + jobjectArray files = fileListToStringArray( response->env, fileList ); + + GtkWindow* window = GTK_WINDOW( dialog ); + + // invoke callback: boolean approve( String[] files, long hwnd ); + jclass cls = response->env->GetObjectClass( response->callback ); + jmethodID approveID = response->env->GetMethodID( cls, "approve", "([Ljava/lang/String;J)Z" ); + if( approveID != NULL && !response->env->CallBooleanMethod( response->callback, approveID, files, window ) ) + return; // keep dialog open + } + + response->fileList = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER( dialog ) ); + } + + // hide/destroy file dialog and quit loop + gtk_widget_hide( dialog ); + gtk_widget_destroy( dialog ); + gtk_main_quit(); +} + +//---- JNI methods ------------------------------------------------------------ + +extern "C" +JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser + ( JNIEnv* env, jclass cls, jobject owner, jboolean open, + jstring title, jstring okButtonLabel, jstring currentName, jstring currentFolder, + jint optionsSet, jint optionsClear, jobject callback, jint fileTypeIndex, jobjectArray fileTypes ) +{ + // initialize GTK + if( !gtk_init_check( NULL, NULL ) ) + return NULL; + + // convert Java strings to C strings + AutoReleaseStringUTF8 ctitle( env, title ); + AutoReleaseStringUTF8 cokButtonLabel( env, okButtonLabel ); + AutoReleaseStringUTF8 ccurrentName( env, currentName ); + AutoReleaseStringUTF8 ccurrentFolder( env, currentFolder ); + + // create GTK file chooser dialog + // https://docs.gtk.org/gtk3/class.FileChooserDialog.html + bool selectFolder = isOptionSet( FC_select_folder ); + bool multiSelect = isOptionSet( FC_select_multiple ); + GtkWidget* dialog = gtk_file_chooser_dialog_new( + (ctitle != NULL) ? ctitle + : (selectFolder ? (multiSelect ? _("Select Folders") : _("Select Folder")) + : (open ? ((multiSelect ? _("Open Files") : _("Open File"))) : _("Save File"))), + NULL, // can not use AWT X11 window as parent because GtkWindow is required + selectFolder ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER + : (open ? GTK_FILE_CHOOSER_ACTION_OPEN : GTK_FILE_CHOOSER_ACTION_SAVE), + _("_Cancel"), GTK_RESPONSE_CANCEL, + (cokButtonLabel != NULL) ? cokButtonLabel + : (selectFolder ? _("_Select") : (open ? _("_Open") : _("_Save"))), GTK_RESPONSE_ACCEPT, + NULL ); // marks end of buttons + GtkFileChooser* chooser = GTK_FILE_CHOOSER( dialog ); + + // set current name and folder + if( !open && ccurrentName != NULL ) + gtk_file_chooser_set_current_name( chooser, ccurrentName ); + if( ccurrentFolder != NULL ) + gtk_file_chooser_set_current_folder( chooser, ccurrentFolder ); + + // set options + if( isOptionSetOrClear( FC_select_multiple ) ) + gtk_file_chooser_set_select_multiple( chooser, isOptionSet( FC_select_multiple ) ); + if( isOptionSetOrClear( FC_show_hidden ) ) + gtk_file_chooser_set_show_hidden( chooser, isOptionSet( FC_show_hidden ) ); + if( isOptionSetOrClear( FC_local_only ) ) + gtk_file_chooser_set_local_only( chooser, isOptionSet( FC_local_only ) ); + if( isOptionSetOrClear( FC_do_overwrite_confirmation ) ) + gtk_file_chooser_set_do_overwrite_confirmation( chooser, isOptionSet( FC_do_overwrite_confirmation ) ); + if( isOptionSetOrClear( FC_create_folders ) ) + gtk_file_chooser_set_create_folders( chooser, isOptionSet( FC_create_folders ) ); + + // initialize filter + initFilters( chooser, env, fileTypeIndex, fileTypes ); + + // setup modality + GdkWindow* gdkOwner = (owner != NULL) ? getGdkWindow( env, owner ) : NULL; + if( gdkOwner != NULL ) { + gtk_window_set_modal( GTK_WINDOW( dialog ), true ); + + // file dialog should use same screen as owner + gtk_window_set_screen( GTK_WINDOW( dialog ), gdk_window_get_screen( gdkOwner ) ); + + // set the transient when the file dialog is realized + g_signal_connect( dialog, "realize", G_CALLBACK( handle_realize ), gdkOwner ); + } + + // show dialog + // (similar to what's done in sun_awt_X11_GtkFileDialogPeer.c) + ResponseData responseData( env, callback ); + g_signal_connect( dialog, "response", G_CALLBACK( handle_response ), &responseData ); + gtk_widget_show( dialog ); + + // necessary to bring file dialog to the front (and make it active) + // see issues: + // https://github.com/btzy/nativefiledialog-extended/issues/31 + // https://github.com/mlabbe/nativefiledialog/pull/92 + // https://github.com/guillaumechereau/noc/pull/11 + if( GDK_IS_X11_DISPLAY( gtk_widget_get_display( GTK_WIDGET( dialog ) ) ) ) { + GdkWindow* gdkWindow = gtk_widget_get_window( GTK_WIDGET( dialog ) ); + gdk_window_set_events( gdkWindow, static_cast( gdk_window_get_events( gdkWindow ) | GDK_PROPERTY_CHANGE_MASK ) ); + gtk_window_present_with_time( GTK_WINDOW( dialog ), gdk_x11_get_server_time( gdkWindow ) ); + } + + // start event loop (will be quit in respone handler) + gtk_main(); + + // canceled? + if( responseData.fileList == NULL ) + return newJavaStringArray( env, 0 ); + + // convert GSList to Java string array + return fileListToStringArray( env, responseData.fileList ); +} + +static jobjectArray fileListToStringArray( JNIEnv* env, GSList* fileList ) { + guint count = g_slist_length( fileList ); + jobjectArray array = newJavaStringArray( env, count ); + GSList* it = fileList; + for( int i = 0; i < count; i++, it = it->next ) { + gchar* path = (gchar*) it->data; + jstring jpath = env->NewStringUTF( path ); + g_free( path ); + + env->SetObjectArrayElement( array, i, jpath ); + env->DeleteLocalRef( jpath ); + } + g_slist_free( fileList ); + return array; +} diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp new file mode 100644 index 000000000..3f67fd5fa --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/GtkMessageDialog.cpp @@ -0,0 +1,76 @@ +/* + * Copyright 2025 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include "JNIUtils.h" +#include "com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h" + +/** + * @author Karl Tauber + * @since 3.7 + */ + +extern "C" +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showMessageDialog + ( JNIEnv* env, jclass cls, jlong hwndParent, jint messageType, jstring primaryText, jstring secondaryText, + jint defaultButton, jobjectArray buttons ) +{ + GtkWindow* window = (GtkWindow*) hwndParent; + + // convert message type + GtkMessageType gmessageType; + switch( messageType ) { + case /* JOptionPane.ERROR_MESSAGE */ 0: gmessageType = GTK_MESSAGE_ERROR; break; + case /* JOptionPane.INFORMATION_MESSAGE */ 1: gmessageType = GTK_MESSAGE_INFO; break; + case /* JOptionPane.WARNING_MESSAGE */ 2: gmessageType = GTK_MESSAGE_WARNING; break; + case /* JOptionPane.QUESTION_MESSAGE */ 3: gmessageType = GTK_MESSAGE_QUESTION; break; + default: + case /* JOptionPane.PLAIN_MESSAGE */ -1: gmessageType = GTK_MESSAGE_OTHER; break; + } + + // convert Java strings to C strings + AutoReleaseStringUTF8 cprimaryText( env, primaryText ); + AutoReleaseStringUTF8 csecondaryText( env, secondaryText ); + + // create GTK file chooser dialog + // https://docs.gtk.org/gtk3/class.MessageDialog.html + jint buttonCount = env->GetArrayLength( buttons ); + GtkWidget* dialog = gtk_message_dialog_new( window, GTK_DIALOG_MODAL, gmessageType, + (buttonCount > 0) ? GTK_BUTTONS_NONE : GTK_BUTTONS_OK, + "%s", (const gchar*) cprimaryText ); + if( csecondaryText != NULL ) + gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( dialog ), "%s", (const gchar*) csecondaryText ); + + // add buttons + for( int i = 0; i < buttonCount; i++ ) { + AutoReleaseStringUTF8 str( env, (jstring) env->GetObjectArrayElement( buttons, i ) ); + gtk_dialog_add_button( GTK_DIALOG( dialog ), str, i ); + } + + // set default button + gtk_dialog_set_default_response( GTK_DIALOG( dialog ), MIN( MAX( defaultButton, 0 ), buttonCount - 1 ) ); + + // show message dialog + gint responseID = gtk_dialog_run( GTK_DIALOG( dialog ) ); + gtk_widget_destroy( dialog ); + + // return -1 if closed with ESC key + return (responseID >= 0) ? responseID : -1; +} diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp new file mode 100644 index 000000000..4edea7e47 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/JNIUtils.cpp @@ -0,0 +1,54 @@ +/* + * Copyright 2024 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// avoid inlining of printf() +#define _NO_CRT_STDIO_INLINE + +#include +#include "JNIUtils.h" + +/** + * @author Karl Tauber + */ + +//---- class AutoReleaseStringUTF8 -------------------------------------------- + +AutoReleaseStringUTF8::AutoReleaseStringUTF8( JNIEnv* _env, jstring _javaString ) { + env = _env; + javaString = _javaString; + chars = (javaString != NULL) ? env->GetStringUTFChars( javaString, NULL ) : NULL; +} + +AutoReleaseStringUTF8::~AutoReleaseStringUTF8() { + if( chars != NULL ) + env->ReleaseStringUTFChars( javaString, chars ); +} + +//---- JNI methods ------------------------------------------------------------ + +extern "C" +JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_isLibAvailable + ( JNIEnv* env, jclass cls, jstring libname ) +{ + AutoReleaseStringUTF8 clibname( env, libname ); + + void* lib = dlopen( clibname, RTLD_LAZY ); + if( lib == NULL ) + return false; + + dlclose( lib ); + return true; +} diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp index 8cbc57a97..3fc604f18 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp @@ -25,18 +25,21 @@ */ -bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, - long data0, long data1, long data2, long data3, long data4 ); -bool isWMHintSupported( Display* display, Window rootWindow, Atom atom ); +// declare exported methods Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display** display_return ); +// declare internal methods +static bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, + long data0, long data1, long data2, long data3, long data4 ); +static bool isWMHintSupported( Display* display, Window rootWindow, Atom atom ); + //---- JNI methods ------------------------------------------------------------ /** * Send _NET_WM_MOVERESIZE to window to initiate moving or resizing. * - * https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45446104441728 + * https://specifications.freedesktop.org/wm-spec/latest/ar01s04.html#id-1.5.4 * https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gdk/x11/gdksurface-x11.c#L3841-3881 */ extern "C" @@ -79,7 +82,7 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xS 0 ); } -bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, +static bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, long data0, long data1, long data2, long data3, long data4 ) { // get the AWT @@ -131,7 +134,7 @@ bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, } -bool isWMHintSupported( Display* display, Window rootWindow, Atom atom ) { +static bool isWMHintSupported( Display* display, Window rootWindow, Atom atom ) { Atom type; int format; unsigned long n_atoms; diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/JNIUtils.h b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/JNIUtils.h new file mode 100644 index 000000000..6d7d8c22c --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/JNIUtils.h @@ -0,0 +1,36 @@ +/* + * Copyright 2025 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +/** + * @author Karl Tauber + */ + +//---- class AutoReleaseStringUTF8 -------------------------------------------- + +class AutoReleaseStringUTF8 { + JNIEnv* env; + jstring javaString; + const char* chars; + +public: + AutoReleaseStringUTF8( JNIEnv* _env, jstring _javaString ); + ~AutoReleaseStringUTF8(); + + operator const gchar*() { return chars; } +}; diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h index c3b8c4b25..0c5816409 100644 --- a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h @@ -9,6 +9,18 @@ extern "C" { #endif #undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_MOVE #define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_MOVE 8L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_select_folder +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_select_folder 1L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_select_multiple +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_select_multiple 2L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_show_hidden +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_show_hidden 4L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_local_only +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_local_only 8L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_do_overwrite_confirmation +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_do_overwrite_confirmation 16L +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_create_folders +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_FC_create_folders 32L /* * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary * Method: xMoveOrResizeWindow @@ -25,6 +37,30 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xM JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xShowWindowMenu (JNIEnv *, jclass, jobject, jint, jint); +/* + * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary + * Method: isLibAvailable + * Signature: (Ljava/lang/String;)Z + */ +JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_isLibAvailable + (JNIEnv *, jclass, jstring); + +/* + * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary + * Method: showFileChooser + * Signature: (Ljava/awt/Window;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILcom/formdev/flatlaf/ui/FlatNativeLinuxLibrary/FileChooserCallback;I[Ljava/lang/String;)[Ljava/lang/String; + */ +JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showFileChooser + (JNIEnv *, jclass, jobject, jboolean, jstring, jstring, jstring, jstring, jint, jint, jobject, jint, jobjectArray); + +/* + * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary + * Method: showMessageDialog + * Signature: (JILjava/lang/String;Ljava/lang/String;I[Ljava/lang/String;)I + */ +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_showMessageDialog + (JNIEnv *, jclass, jlong, jint, jstring, jstring, jint, jobjectArray); + #ifdef __cplusplus } #endif diff --git a/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts b/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts index 961121d8c..22e913182 100644 --- a/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts @@ -75,7 +75,7 @@ tasks { compilerArgs.addAll( toolChain.map { when( it ) { - is Gcc, is Clang -> listOf( "-x", "objective-c++", "-mmacosx-version-min=$minOs" ) + is Gcc, is Clang -> listOf( "-x", "objective-c++", "-mmacosx-version-min=$minOs", "-fvisibility=hidden" ) else -> emptyList() } } ) diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h index f0a8604c9..b2dfe6743 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h @@ -46,7 +46,33 @@ JNI_COCOA_CATCH() \ } +#define JNI_THREAD_ENTER( jvm, returnValue ) \ + JNIEnv *env; \ + bool detach = false; \ + switch( jvm->GetEnv( (void**) &env, JNI_VERSION_1_6 ) ) { \ + case JNI_OK: break; \ + case JNI_EDETACHED: \ + if( jvm->AttachCurrentThread( (void**) &env, NULL ) != JNI_OK ) \ + return returnValue; \ + detach = true; \ + break; \ + default: return returnValue; \ + } \ + @try { + +#define JNI_THREAD_EXIT( jvm ) \ + } @finally { \ + if( env->ExceptionCheck() ) \ + env->ExceptionDescribe(); \ + if( detach ) \ + jvm->DetachCurrentThread(); \ + } + jclass findClass( JNIEnv *env, const char* className, bool globalRef ); jfieldID getFieldID( JNIEnv *env, jclass cls, const char* fieldName, const char* fieldSignature, bool staticField ); jmethodID getMethodID( JNIEnv *env, jclass cls, const char* methodName, const char* methodSignature, bool staticMethod ); + +NSString* JavaToNSString( JNIEnv *env, jstring javaString ); +jstring NSToJavaString( JNIEnv *env, NSString *nsString ); +jstring NormalizedPathJavaFromNSString( JNIEnv* env, NSString *nsString ); diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h index f99549e5a..301039b21 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h @@ -13,6 +13,32 @@ extern "C" { #define com_formdev_flatlaf_ui_FlatNativeMacLibrary_BUTTONS_SPACING_MEDIUM 1L #undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_BUTTONS_SPACING_LARGE #define com_formdev_flatlaf_ui_FlatNativeMacLibrary_BUTTONS_SPACING_LARGE 2L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canChooseFiles +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canChooseFiles 1L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canChooseDirectories +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canChooseDirectories 2L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_resolvesAliases +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_resolvesAliases 4L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsMultipleSelection +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsMultipleSelection 8L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_accessoryViewDisclosed +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_accessoryViewDisclosed 16L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsTagField +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsTagField 256L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canCreateDirectories +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canCreateDirectories 512L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canSelectHiddenExtension +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_canSelectHiddenExtension 1024L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsHiddenFiles +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showsHiddenFiles 2048L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_extensionHidden +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_extensionHidden 4096L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsOtherFileTypes +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_allowsOtherFileTypes 8192L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_treatsFilePackagesAsDirectories +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_treatsFilePackagesAsDirectories 16384L +#undef com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showSingleFilterField +#define com_formdev_flatlaf_ui_FlatNativeMacLibrary_FC_showSingleFilterField 16777216L /* * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary * Method: setWindowRoundedBorder @@ -53,6 +79,22 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_isWi JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_toggleWindowFullScreen (JNIEnv *, jclass, jobject); +/* + * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary + * Method: showFileChooser + * Signature: (Ljava/awt/Window;IZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILcom/formdev/flatlaf/ui/FlatNativeMacLibrary/FileChooserCallback;I[Ljava/lang/String;)[Ljava/lang/String; + */ +JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_showFileChooser + (JNIEnv *, jclass, jobject, jint, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jint, jint, jobject, jint, jobjectArray); + +/* + * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary + * Method: showMessageDialog + * Signature: (JILjava/lang/String;Ljava/lang/String;I[Ljava/lang/String;)I + */ +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_showMessageDialog + (JNIEnv *, jclass, jlong, jint, jstring, jstring, jint, jobjectArray); + #ifdef __cplusplus } #endif diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm index ef0ff0f3c..b9b9a9b4d 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/ApiVersion.mm @@ -14,8 +14,8 @@ * limitations under the License. */ -#include -#include "com_formdev_flatlaf_ui_FlatNativeLibrary.h" +#import +#import "com_formdev_flatlaf_ui_FlatNativeLibrary.h" /** * @author Karl Tauber @@ -24,7 +24,7 @@ // increase this version if changing API or functionality of native library // also update version in Java class com.formdev.flatlaf.ui.FlatNativeMacLibrary -#define API_VERSION_MACOS 2001 +#define API_VERSION_MACOS 2002 //---- JNI methods ------------------------------------------------------------ diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNIUtils.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNIUtils.mm index c000a9e2e..a5fb10e75 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNIUtils.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNIUtils.mm @@ -75,3 +75,38 @@ jmethodID getMethodID( JNIEnv *env, jclass cls, const char* methodName, const ch return methodID; } + +NSString* JavaToNSString( JNIEnv *env, jstring javaString ) { + if( javaString == NULL ) + return NULL; + + int len = env->GetStringLength( javaString ); + const jchar* chars = env->GetStringChars( javaString, NULL ); + if( chars == NULL ) + return NULL; + + NSString* nsString = [NSString stringWithCharacters:(unichar*)chars length:len]; + env->ReleaseStringChars( javaString, chars ); + return nsString; +} + +jstring NSToJavaString( JNIEnv *env, NSString *nsString ) { + if( nsString == NULL ) + return NULL; + + jsize len = [nsString length]; + unichar* buffer = (unichar*) calloc( len, sizeof( unichar ) ); + if( buffer == NULL ) + return NULL; + + [nsString getCharacters:buffer]; + jstring javaString = env->NewString( buffer, len ); + free( buffer ); + return javaString; +} + +jstring NormalizedPathJavaFromNSString( JNIEnv* env, NSString *nsString ) { + return (nsString != NULL) + ? NSToJavaString( env, [nsString precomposedStringWithCanonicalMapping] ) + : NULL; +} diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm new file mode 100644 index 000000000..52c2b28a8 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacFileChooser.mm @@ -0,0 +1,406 @@ +/* + * Copyright 2024 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import +#import "JNIUtils.h" +#import "JNFRunLoop.h" +#import "com_formdev_flatlaf_ui_FlatNativeMacLibrary.h" + +/** + * @author Karl Tauber + * @since 3.7 + */ + +// declare internal methods +static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ); +static jobjectArray urlsToStringArray( JNIEnv* env, NSArray* urls ); +static NSArray* getDialogURLs( NSSavePanel* dialog ); + +//---- class FileChooserDelegate ---------------------------------------------- + +@interface FileChooserDelegate : NSObject { + NSArray* _filters; + + JavaVM* _jvm; + jobject _callback; + NSMutableSet* _urlsSet; + } + + @property (nonatomic, assign) NSSavePanel* dialog; + + - (void) initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex + :(NSString*)filterFieldLabel :(bool)showSingleFilterField; + - (void) selectFormat: (id)sender; + - (void) selectFormatAtIndex: (int)index; +@end + +@implementation FileChooserDelegate + + - (void) initFilterAccessoryView: (NSMutableArray*)filters :(int)filterIndex + :(NSString*)filterFieldLabel :(bool)showSingleFilterField + { + _filters = filters; + + // get filter names + NSArray* filterNames = filters.lastObject; + [filters removeLastObject]; + + // do not add filter/format combobox if there is only one filter + if( filters.count <= 1 && !showSingleFilterField ) { + [self selectFormatAtIndex:0]; + return; + } + + // create label + NSTextField* label = [[NSTextField alloc] initWithFrame:NSZeroRect]; + label.stringValue = (filterFieldLabel != NULL) ? filterFieldLabel : @"Format:"; + label.editable = NO; + label.bordered = NO; + label.bezeled = NO; + label.drawsBackground = NO; + + // create combobox + NSPopUpButton* popupButton = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO]; + [popupButton addItemsWithTitles:filterNames]; + [popupButton selectItemAtIndex:MIN( MAX( filterIndex, 0 ), filterNames.count - 1 )]; + [popupButton setTarget:self]; + [popupButton setAction:@selector(selectFormat:)]; + + // create view + NSView* accessoryView = [[NSView alloc] initWithFrame:NSZeroRect]; + [accessoryView addSubview:label]; + [accessoryView addSubview:popupButton]; + + // autolayout + label.translatesAutoresizingMaskIntoConstraints = NO; + popupButton.translatesAutoresizingMaskIntoConstraints = NO; + int labelWidth = label.intrinsicContentSize.width; + int gap = 12; + int popupButtonWidth = popupButton.intrinsicContentSize.width; + int popupButtonMinimumWidth = 140; + int totalWidth = labelWidth + gap + MAX( popupButtonWidth, popupButtonMinimumWidth ); + [accessoryView addConstraints:@[ + // horizontal layout + [label.leadingAnchor constraintEqualToAnchor:accessoryView.centerXAnchor constant:-(totalWidth / 2)], + [popupButton.leadingAnchor constraintEqualToAnchor:label.trailingAnchor constant:gap], + [popupButton.widthAnchor constraintGreaterThanOrEqualToConstant:popupButtonMinimumWidth], + + // vertical layout + [popupButton.topAnchor constraintEqualToAnchor:accessoryView.topAnchor constant:8], + [popupButton.bottomAnchor constraintEqualToAnchor:accessoryView.bottomAnchor constant:-8], + [label.firstBaselineAnchor constraintEqualToAnchor:popupButton.firstBaselineAnchor], + ]]; + + [_dialog setAccessoryView:accessoryView]; + + // initial filter + [self selectFormatAtIndex:filterIndex]; + } + + - (void) selectFormat: (id)sender { + NSPopUpButton* popupButton = (NSPopUpButton*) sender; + [self selectFormatAtIndex:popupButton.indexOfSelectedItem]; + } + + - (void) selectFormatAtIndex: (int)index { + index = MIN( MAX( index, 0 ), _filters.count - 1 ); + NSArray* fileTypes = [_filters objectAtIndex:index]; + + // use deprecated allowedFileTypes instead of newer allowedContentTypes (since macOS 11+) + // to support older macOS versions 10.14+ and because of some problems with allowedContentTypes: + // https://github.com/chromium/chromium/blob/d8e0032963b7ca4728ff4117933c0feb3e479b7a/components/remote_cocoa/app_shim/select_file_dialog_bridge.mm#L209-232 + _dialog.allowedFileTypes = [fileTypes containsObject:@"*"] ? nil : fileTypes; + } + + //---- NSOpenSavePanelDelegate ---- + + - (void) initCallback: (JavaVM*)jvm :(jobject)callback { + _jvm = jvm; + _callback = callback; + } + + - (BOOL) panel: (id) sender validateURL:(NSURL*) url error:(NSError**) outError { + JNI_COCOA_TRY() + + if( _callback == NULL ) + return true; + + NSArray* urls = getDialogURLs( sender ); + + // if multiple files are selected for opening, then the validateURL method + // is invoked for earch file, but our callback should be invoked only once for all files + if( urls != NULL && urls.count > 1 ) { + if( _urlsSet == NULL ) { + // invoked for first selected file --> invoke callback + _urlsSet = [NSMutableSet setWithArray:urls]; + [_urlsSet removeObject:url]; + } else { + // invoked for other selected files --> do not invoke callback + [_urlsSet removeObject:url]; + if( _urlsSet.count == 0 ) + _urlsSet = NULL; + return true; + } + } + + JNI_THREAD_ENTER( _jvm, true ) + + jobjectArray files = urlsToStringArray( env, urls ); + jlong window = (jlong) sender; + + // invoke callback: boolean approve( String[] files, long hwnd ); + jclass cls = env->GetObjectClass( _callback ); + jmethodID approveID = env->GetMethodID( cls, "approve", "([Ljava/lang/String;J)Z" ); + if( approveID != NULL && !env->CallBooleanMethod( _callback, approveID, files, window ) ) { + _urlsSet = NULL; + return false; // keep dialog open + } + + JNI_THREAD_EXIT( _jvm ) + JNI_COCOA_CATCH() + + return true; + } + + //---- NSWindowDelegate ---- + + - (void) windowDidBecomeMain:(NSNotification *) notification { + JNI_COCOA_TRY() + + // Disable main menu bar because the file dialog is modal and it should be not possible + // to select any menu item. Otherwiese an action could show a Swing dialog, which would + // be shown under the file dialog. + // + // NOTE: It is not necessary to re-enable the main menu bar because Swing does this itself. + // When the file dialog is closed and a Swing window becomes active, + // macOS sends windowDidBecomeMain (and windowDidBecomeKey) message to AWTWindow, + // which invokes [self activateWindowMenuBar], + // which invokes [CMenuBar activate:menuBar modallyDisabled:isDisabled], + // which updates main menu bar. + NSMenu* mainMenu = [NSApp mainMenu]; + int count = [mainMenu numberOfItems]; + for( int i = 0; i < count; i++ ) { + NSMenuItem* menuItem = [mainMenu itemAtIndex:i]; + NSMenu *subenu = [menuItem submenu]; + if( [subenu isJavaMenu] ) + [menuItem setEnabled:NO]; + } + + JNI_COCOA_CATCH() + } + +@end + +//---- helper ----------------------------------------------------------------- + +#define isOptionSet( option ) ((optionsSet & com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ## option) != 0) +#define isOptionClear( option ) ((optionsClear & com_formdev_flatlaf_ui_FlatNativeMacLibrary_ ## option) != 0) +#define isOptionSetOrClear( option ) isOptionSet( option ) || isOptionClear( option ) + +static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { + jclass stringClass = env->FindClass( "java/lang/String" ); + return env->NewObjectArray( count, stringClass, NULL ); +} + +static NSMutableArray* initFilters( JNIEnv* env, jobjectArray fileTypes ) { + jint length = env->GetArrayLength( fileTypes ); + if( length <= 0 ) + return NULL; + + NSMutableArray* filterNames = [NSMutableArray array]; + NSMutableArray* filters = [NSMutableArray array]; + NSString* filterName = NULL; + NSMutableArray* filter = NULL; + for( int i = 0; i < length; i++ ) { + jstring jstr = (jstring) env->GetObjectArrayElement( fileTypes, i ); + if( jstr == NULL ) { + if( filter != NULL ) { + if( filter.count > 0 ) { + [filterNames addObject:filterName]; + [filters addObject:filter]; + } + filterName = NULL; + filter = NULL; + } + continue; + } + + NSString* str = JavaToNSString( env, jstr ); + env->DeleteLocalRef( jstr ); + if( filter == NULL ) { + filterName = str; + filter = [NSMutableArray array]; + } else + [filter addObject:str]; + } + + if( filters.count == 0 ) + return NULL; + + // add filter names to array (removed again after creating combobox) + [filters addObject:filterNames]; + + return filters; +} + +//---- JNI methods ------------------------------------------------------------ + +extern "C" +JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_showFileChooser + ( JNIEnv* env, jclass cls, jobject owner, jint dark, jboolean open, + jstring title, jstring prompt, jstring message, jstring filterFieldLabel, + jstring nameFieldLabel, jstring nameFieldStringValue, jstring directoryURL, + jint optionsSet, jint optionsClear, jobject callback, jint fileTypeIndex, jobjectArray fileTypes ) +{ + JNI_COCOA_ENTER() + + JavaVM* jvm; + if( env->GetJavaVM( &jvm ) != JNI_OK ) + return NULL; + + // convert Java strings to NSString (on Java thread) + NSString* nsTitle = JavaToNSString( env, title ); + NSString* nsPrompt = JavaToNSString( env, prompt ); + NSString* nsMessage = JavaToNSString( env, message ); + NSString* nsFilterFieldLabel = JavaToNSString( env, filterFieldLabel ); + NSString* nsNameFieldLabel = JavaToNSString( env, nameFieldLabel ); + NSString* nsNameFieldStringValue = JavaToNSString( env, nameFieldStringValue ); + NSString* nsDirectoryURL = JavaToNSString( env, directoryURL ); + NSMutableArray* filters = initFilters( env, fileTypes ); + + NSArray* urls = NULL; + NSArray** purls = &urls; + + // show file dialog on macOS thread + [FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){ + JNI_COCOA_TRY() + + // create open/save panel + NSSavePanel* dialog = open ? [NSOpenPanel openPanel] : [NSSavePanel savePanel]; + + // set appearance + if( dark == 1 ) + dialog.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + else if( dark == 0 ) + dialog.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + + if( nsTitle != NULL ) + dialog.title = nsTitle; + if( nsPrompt != NULL ) + dialog.prompt = nsPrompt; + if( nsMessage != NULL ) + dialog.message = nsMessage; + if( nsNameFieldLabel != NULL ) + dialog.nameFieldLabel = nsNameFieldLabel; + if( nsNameFieldStringValue != NULL ) + dialog.nameFieldStringValue = nsNameFieldStringValue; + if( nsDirectoryURL != NULL ) + dialog.directoryURL = [NSURL fileURLWithPath:nsDirectoryURL isDirectory:YES]; + + // set open options + if( open ) { + NSOpenPanel* openDialog = (NSOpenPanel*) dialog; + + bool canChooseFiles = isOptionSet( FC_canChooseFiles ); + bool canChooseDirectories = isOptionSet( FC_canChooseDirectories ); + if( !canChooseFiles && !canChooseDirectories ) + canChooseFiles = true; + openDialog.canChooseFiles = canChooseFiles; + openDialog.canChooseDirectories = canChooseDirectories; + + if( isOptionSetOrClear( FC_resolvesAliases ) ) + openDialog.resolvesAliases = isOptionSet( FC_resolvesAliases ); + if( isOptionSetOrClear( FC_allowsMultipleSelection ) ) + openDialog.allowsMultipleSelection = isOptionSet( FC_allowsMultipleSelection ); + } + + // set options + if( isOptionSetOrClear( FC_showsTagField ) ) + dialog.showsTagField = isOptionSet( FC_showsTagField ); + if( isOptionSetOrClear( FC_canCreateDirectories ) ) + dialog.canCreateDirectories = isOptionSet( FC_canCreateDirectories ); + if( isOptionSetOrClear( FC_canSelectHiddenExtension ) ) + dialog.canSelectHiddenExtension = isOptionSet( FC_canSelectHiddenExtension ); + if( isOptionSetOrClear( FC_showsHiddenFiles) ) + dialog.showsHiddenFiles = isOptionSet( FC_showsHiddenFiles); + if( isOptionSetOrClear( FC_extensionHidden ) ) + dialog.extensionHidden = isOptionSet( FC_extensionHidden ); + if( isOptionSetOrClear( FC_allowsOtherFileTypes ) ) + dialog.allowsOtherFileTypes = isOptionSet( FC_allowsOtherFileTypes ); + if( isOptionSetOrClear( FC_treatsFilePackagesAsDirectories ) ) + dialog.treatsFilePackagesAsDirectories = isOptionSet( FC_treatsFilePackagesAsDirectories ); + + FileChooserDelegate* delegate = [FileChooserDelegate new]; + delegate.dialog = dialog; + + // initialize filter accessory view + if( filters != NULL ) { + [delegate initFilterAccessoryView:filters :fileTypeIndex :nsFilterFieldLabel :isOptionSet( FC_showSingleFilterField )]; + + if( open && isOptionSetOrClear( FC_accessoryViewDisclosed ) ) + ((NSOpenPanel*)dialog).accessoryViewDisclosed = isOptionSet( FC_accessoryViewDisclosed ); + } + + // initialize callback + if( callback != NULL ) + [delegate initCallback :jvm :callback]; + + // set file dialog delegate + dialog.delegate = delegate; + + // show dialog + NSModalResponse response = [dialog runModal]; + [delegate release]; + if( response != NSModalResponseOK ) { + *purls = @[]; + return; + } + + *purls = getDialogURLs( dialog ); + + JNI_COCOA_CATCH() + }]; + + if( urls == NULL ) + return NULL; + + // convert URLs to Java string array + return urlsToStringArray( env, urls ); + + JNI_COCOA_EXIT() +} + +static NSArray* getDialogURLs( NSSavePanel* dialog ) { + if( [dialog isKindOfClass:[NSOpenPanel class]] ) + return [[NSArray alloc] initWithArray: static_cast(dialog).URLs]; + + NSURL* url = dialog.URL; + // use '[[NSArray alloc] initWithObject:url]' here because '@[url]' crashes on macOS 10.14 + return (url != NULL) ? [[NSArray alloc] initWithObject:url] : @[]; +} + +static jobjectArray urlsToStringArray( JNIEnv* env, NSArray* urls ) { + jsize count = (urls != NULL) ? urls.count : 0; + jobjectArray array = newJavaStringArray( env, count ); + for( int i = 0; i < count; i++ ) { + jstring filename = NormalizedPathJavaFromNSString( env, [urls[i] path] ); + env->SetObjectArrayElement( array, i, filename ); + env->DeleteLocalRef( filename ); + } + return array; +} diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm new file mode 100644 index 000000000..def00f929 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacMessageDialog.mm @@ -0,0 +1,90 @@ +/* + * Copyright 2024 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import +#import "JNIUtils.h" +#import "JNFRunLoop.h" +#import "com_formdev_flatlaf_ui_FlatNativeMacLibrary.h" + +/** + * @author Karl Tauber + * @since 3.7 + */ + +extern "C" +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_showMessageDialog + ( JNIEnv* env, jclass cls, jlong hwndParent, jint alertStyle, jstring messageText, jstring informativeText, + jint defaultButton, jobjectArray buttons ) +{ + JNI_COCOA_ENTER() + + // convert Java strings to NSString (on Java thread) + NSString* nsMessageText = JavaToNSString( env, messageText ); + NSString* nsInformativeText = JavaToNSString( env, informativeText ); + + jint buttonCount = env->GetArrayLength( buttons ); + NSMutableArray* nsButtons = [NSMutableArray array]; + for( int i = 0; i < buttonCount; i++ ) { + NSString* nsButton = JavaToNSString( env, (jstring) env->GetObjectArrayElement( buttons, i ) ); + [nsButtons addObject:nsButton]; + } + + jint result = -1; + jint* presult = &result; + + // show alert on macOS thread + [FlatJNFRunLoop performOnMainThreadWaiting:YES withBlock:^(){ + NSAlert* alert = [[NSAlert alloc] init]; + + // use appearance from parent window + NSWindow* parent = (NSWindow*) hwndParent; + if( parent != NULL ) + alert.window.appearance = parent.appearance; + + // use empty string because if alert.messageText is not set it displays "Alert" + alert.messageText = (nsMessageText != NULL) ? nsMessageText : @""; + if( nsInformativeText != NULL ) + alert.informativeText = nsInformativeText; + + // alert style + switch( alertStyle ) { + case /* JOptionPane.ERROR_MESSAGE */ 0: alert.alertStyle = NSAlertStyleCritical; break; + default: + case /* JOptionPane.INFORMATION_MESSAGE */ 1: alert.alertStyle = NSAlertStyleInformational; break; + case /* JOptionPane.WARNING_MESSAGE */ 2: alert.alertStyle = NSAlertStyleWarning; break; + } + + // add buttons + for( int i = 0; i < nsButtons.count; i++ ) { + NSButton* b = [alert addButtonWithTitle:nsButtons[i]]; + if( i == defaultButton ) + alert.window.defaultButtonCell = b.cell; + } + + // show alert + NSInteger response = [alert runModal]; + + // if no buttons added, which shows a single OK button, the response is 0 when clicking OK + // if buttons added, response is 1000+buttonIndex + *presult = MAX( response - NSAlertFirstButtonReturn, 0 ); + }]; + + return result; + + JNI_COCOA_EXIT() +} diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm index 6676a4b25..c45f0bb85 100644 --- a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm @@ -39,13 +39,15 @@ @interface WindowData : NSObject @implementation WindowData @end -// declare internal methods +// declare exported methods NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window ); -WindowData* getWindowData( NSWindow* nsWindow, bool allocate ); -void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ); -int getWindowButtonAreaWidth( NSWindow* nsWindow ); -int getWindowTitleBarHeight( NSWindow* nsWindow ); -bool isWindowFullScreen( NSWindow* nsWindow ); + +// declare internal methods +static WindowData* getWindowData( NSWindow* nsWindow, bool allocate ); +static void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ); +static int getWindowButtonAreaWidth( NSWindow* nsWindow ); +static int getWindowTitleBarHeight( NSWindow* nsWindow ); +static bool isWindowFullScreen( NSWindow* nsWindow ); NSWindow* getNSWindow( JNIEnv* env, jclass cls, jobject window ) { @@ -79,7 +81,7 @@ @implementation WindowData return (NSWindow *) jlong_to_ptr( env->GetLongField( platformWindow, ptrID ) ); } -WindowData* getWindowData( NSWindow* nsWindow, bool allocate ) { +static WindowData* getWindowData( NSWindow* nsWindow, bool allocate ) { static char key; WindowData* windowData = objc_getAssociatedObject( nsWindow, &key ); if( windowData == NULL && allocate ) { @@ -252,7 +254,7 @@ @implementation WindowData return FALSE; } -void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ) { +static void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ) { // get buttons NSView* buttons[3] = { [nsWindow standardWindowButton:NSWindowCloseButton], @@ -312,7 +314,7 @@ void setWindowButtonsHidden( NSWindow* nsWindow, bool hidden ) { return NULL; } -int getWindowButtonAreaWidth( NSWindow* nsWindow ) { +static int getWindowButtonAreaWidth( NSWindow* nsWindow ) { // get buttons NSView* buttons[3] = { [nsWindow standardWindowButton:NSWindowCloseButton], @@ -344,7 +346,7 @@ int getWindowButtonAreaWidth( NSWindow* nsWindow ) { return right + left; } -int getWindowTitleBarHeight( NSWindow* nsWindow ) { +static int getWindowTitleBarHeight( NSWindow* nsWindow ) { NSView* closeButton = [nsWindow standardWindowButton:NSWindowCloseButton]; if( closeButton == NULL ) return -1; @@ -369,7 +371,7 @@ int getWindowTitleBarHeight( NSWindow* nsWindow ) { return FALSE; } -bool isWindowFullScreen( NSWindow* nsWindow ) { +static bool isWindowFullScreen( NSWindow* nsWindow ) { return ((nsWindow.styleMask & NSWindowStyleMaskFullScreen) != 0); } diff --git a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts index 0a7eee0d4..3892d870f 100644 --- a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts @@ -64,7 +64,7 @@ tasks { compilerArgs.addAll( toolChain.map { when( it ) { is Gcc, is Clang -> listOf( "-O2", "-DUNICODE" ) - is VisualCpp -> listOf( "/O2", "/Zl", "/GS-", "/DUNICODE" ) + is VisualCpp -> listOf( "/O2", "/GS-", "/DUNICODE" ) else -> emptyList() } } ) @@ -80,8 +80,8 @@ tasks { linkerArgs.addAll( toolChain.map { when( it ) { - is Gcc, is Clang -> listOf( "-lUser32", "-lGdi32", "-lshell32", "-lAdvAPI32", "-lKernel32", "-lDwmapi" ) - is VisualCpp -> listOf( "User32.lib", "Gdi32.lib", "shell32.lib", "AdvAPI32.lib", "Kernel32.lib", "Dwmapi.lib", "/NODEFAULTLIB" ) + is Gcc, is Clang -> listOf( "-lUser32", "-lGdi32", "-lshell32", "-lAdvAPI32", "-lKernel32", "-lDwmapi", "-lOle32", "-luuid" ) + is VisualCpp -> listOf( "User32.lib", "Gdi32.lib", "shell32.lib", "AdvAPI32.lib", "Kernel32.lib", "Dwmapi.lib", "Ole32.lib", "uuid.lib" ) else -> emptyList() } } ) @@ -93,6 +93,15 @@ tasks { into( nativesDir ) rename( linkedFile.get().asFile.name, libraryName ) } + +/*dump + val dumpbin = "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.42.34433/bin/Hostx64/x64/dumpbin.exe" + val dll = linkedFile.asFile.get() + val dllDir = dll.parent + exec { commandLine( dumpbin, "/all", "/rawdata:none", "/out:$dllDir/objdump.txt", dll ) } + exec { commandLine( dumpbin, "/all", "/out:$dllDir/full-contents.txt", dll ) } + exec { commandLine( dumpbin, "/disasm", "/out:$dllDir/disassemble.txt", dll ) } +dump*/ } } } diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/ApiVersion.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/ApiVersion.cpp index a32f0d6dc..0f2e7a971 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/ApiVersion.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/ApiVersion.cpp @@ -24,7 +24,7 @@ // increase this version if changing API or functionality of native library // also update version in Java class com.formdev.flatlaf.ui.FlatNativeWindowsLibrary -#define API_VERSION_WINDOWS 1001 +#define API_VERSION_WINDOWS 1002 //---- JNI methods ------------------------------------------------------------ diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp index d45f758a8..d2e1ab2be 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp @@ -30,6 +30,7 @@ * @author Karl Tauber */ +// declare exported methods HWND getWindowHandle( JNIEnv* env, jobject window ); //---- JNI methods ------------------------------------------------------------ diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/JNIUtils.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/JNIUtils.cpp new file mode 100644 index 000000000..bc24a10e5 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/JNIUtils.cpp @@ -0,0 +1,67 @@ +/* + * Copyright 2024 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// avoid inlining of printf() +#define _NO_CRT_STDIO_INLINE + +#include "JNIUtils.h" + +/** + * @author Karl Tauber + */ + +//---- class AutoReleaseString ------------------------------------------------ + +AutoReleaseString::AutoReleaseString( JNIEnv* _env, jstring _javaString ) { + env = _env; + javaString = _javaString; + chars = (javaString != NULL) ? env->GetStringChars( javaString, NULL ) : NULL; +} + +AutoReleaseString::~AutoReleaseString() { + if( chars != NULL ) + env->ReleaseStringChars( javaString, chars ); +} + +//---- class AutoReleaseStringArray ------------------------------------------- + +AutoReleaseStringArray::AutoReleaseStringArray( JNIEnv* _env, jobjectArray _javaStringArray ) { + env = _env; + count = (_javaStringArray != NULL) ? env->GetArrayLength( _javaStringArray ) : 0; + if( count <= 0 ) + return; + + javaStringArray = new jstring[count]; + charsArray = new const jchar*[count]; + + for( int i = 0; i < count; i++ ) { + javaStringArray[i] = (jstring) env->GetObjectArrayElement( _javaStringArray, i ); + charsArray[i] = env->GetStringChars( javaStringArray[i] , NULL ); + } +} + +AutoReleaseStringArray::~AutoReleaseStringArray() { + if( count == 0 ) + return; + + for( int i = 0; i < count; i++ ) { + env->ReleaseStringChars( javaStringArray[i], charsArray[i] ); + env->DeleteLocalRef( javaStringArray[i] ); + } + + delete[] javaStringArray; + delete[] charsArray; +} diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/Runtime.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/Runtime.cpp index cbd5163d8..cea43d132 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/Runtime.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/Runtime.cpp @@ -36,8 +36,11 @@ * @author Karl Tauber */ +HINSTANCE _instance; + extern "C" BOOL WINAPI _DllMainCRTStartup( HINSTANCE instance, DWORD reason, LPVOID reserved ) { + _instance = instance; return TRUE; } diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp new file mode 100644 index 000000000..002c7cdb6 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinFileChooser.cpp @@ -0,0 +1,333 @@ +/* + * Copyright 2024 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// avoid inlining of printf() +#define _NO_CRT_STDIO_INLINE + +#include +#include +#include "JNIUtils.h" +#include "com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h" + +/** + * @author Karl Tauber + * @since 3.7 + */ + +// declare external methods +extern HWND getWindowHandle( JNIEnv* env, jobject window ); + +// declare internal methods +static jobjectArray getFiles( JNIEnv* env, jboolean open, IFileDialog* dialog ); + +//---- class AutoReleasePtr --------------------------------------------------- + +template class AutoReleasePtr { + T* ptr; + +public: + AutoReleasePtr() { + ptr = NULL; + } + AutoReleasePtr( T* p ) { + ptr = p; + ptr->AddRef(); + } + ~AutoReleasePtr() { + if( ptr != NULL ) + ptr->Release(); + } + T** operator&() { return &ptr; } + T* operator->() { return ptr; } + operator T*() { return ptr; } +}; + +//---- class AutoReleaseIShellItem -------------------------------------------- + +class AutoReleaseIShellItem : public AutoReleasePtr { +public: + AutoReleaseIShellItem( JNIEnv* env, jstring path ) { + AutoReleaseString cpath( env, path ); + ::SHCreateItemFromParsingName( cpath, NULL, IID_IShellItem, reinterpret_cast( &*this ) ); + } +}; + +//---- class FilterSpec ------------------------------------------------------- + +class FilterSpec { + AutoReleaseStringArray fileTypes; + +public: + UINT count = 0; + COMDLG_FILTERSPEC* specs = NULL; + +public: + FilterSpec( JNIEnv* _env, jobjectArray _fileTypes ) + : fileTypes( _env, _fileTypes ) + { + if( fileTypes.count == 0 ) + return; + + count = fileTypes.count / 2; + specs = new COMDLG_FILTERSPEC[fileTypes.count]; + + for( int i = 0; i < count; i++ ) { + specs[i].pszName = fileTypes[i * 2]; + specs[i].pszSpec = fileTypes[(i * 2) + 1]; + } + } + ~FilterSpec() { + if( specs != NULL ) + delete[] specs; + } +}; + +//---- class DialogEventHandler ----------------------------------------------- + +// see https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appplatform/commonfiledialog/CommonFileDialogApp.cpp + +class DialogEventHandler : public IFileDialogEvents { + JNIEnv* env; + jboolean open; + jobject callback; + LONG refCount = 1; + +public: + DialogEventHandler( JNIEnv* _env, jboolean _open, jobject _callback ) { + env = _env; + open = _open; + callback = _callback; + } + + //---- IFileDialogEvents methods ---- + + IFACEMETHODIMP OnFileOk( IFileDialog* dialog ) { + if( callback == NULL ) + return S_OK; + + // get files + jobjectArray files; + if( open ) { + AutoReleasePtr openDialog; + HRESULT hr = dialog->QueryInterface( &openDialog ); + files = SUCCEEDED( hr ) ? getFiles( env, true, openDialog ) : getFiles( env, false, dialog ); + } else + files = getFiles( env, false, dialog ); + + // get hwnd of file dialog + HWND hwndFileDialog = 0; + AutoReleasePtr window; + if( SUCCEEDED( dialog->QueryInterface( &window ) ) ) + window->GetWindow( &hwndFileDialog ); + + // invoke callback: boolean approve( String[] files, long hwnd ); + jclass cls = env->GetObjectClass( callback ); + jmethodID approveID = env->GetMethodID( cls, "approve", "([Ljava/lang/String;J)Z" ); + if( approveID == NULL ) + return S_OK; + return env->CallBooleanMethod( callback, approveID, files, hwndFileDialog ) ? S_OK : S_FALSE; + } + + IFACEMETHODIMP OnFolderChange( IFileDialog* ) { return S_OK; } + IFACEMETHODIMP OnFolderChanging( IFileDialog*, IShellItem* ) { return S_OK; } + IFACEMETHODIMP OnHelp( IFileDialog* ) { return S_OK; } + IFACEMETHODIMP OnSelectionChange( IFileDialog* ) { return S_OK; } + IFACEMETHODIMP OnShareViolation( IFileDialog*, IShellItem*, FDE_SHAREVIOLATION_RESPONSE* ) { return S_OK; } + IFACEMETHODIMP OnTypeChange( IFileDialog*pfd ) { return S_OK; } + IFACEMETHODIMP OnOverwrite( IFileDialog*, IShellItem*, FDE_OVERWRITE_RESPONSE* ) { return S_OK; } + + //---- IUnknown methods ---- + + IFACEMETHODIMP QueryInterface( REFIID riid, void** ppv ) { + if( riid != IID_IFileDialogEvents && riid != IID_IUnknown ) + return E_NOINTERFACE; + + *ppv = static_cast( this ); + AddRef(); + return S_OK; + } + + IFACEMETHODIMP_(ULONG) AddRef() { + return InterlockedIncrement( &refCount ); + } + + IFACEMETHODIMP_(ULONG) Release() { + LONG newRefCount = InterlockedDecrement( &refCount ); + if( newRefCount == 0 ) + delete this; + return newRefCount; + } + +private: + ~DialogEventHandler() {} +}; + +//---- class CoInitializer ---------------------------------------------------- + +class CoInitializer { +public: + bool initialized; + + CoInitializer() { + HRESULT result = ::CoInitializeEx( NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE ); + initialized = SUCCEEDED( result ); + } + ~CoInitializer() { + if( initialized ) + ::CoUninitialize(); + } +}; + +//---- helper ----------------------------------------------------------------- + +#define isOptionSet( option ) ((optionsSet & com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_ ## option) != 0) +#define CHECK_HRESULT( code ) { if( (code) != S_OK ) return NULL; } + +static jobjectArray newJavaStringArray( JNIEnv* env, jsize count ) { + jclass stringClass = env->FindClass( "java/lang/String" ); + return env->NewObjectArray( count, stringClass, NULL ); +} + +static jstring newJavaString( JNIEnv* env, LPWSTR str ) { + return env->NewString( reinterpret_cast( str ), static_cast( wcslen( str ) ) ); +} + +//---- JNI methods ------------------------------------------------------------ + +extern "C" +JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showFileChooser + ( JNIEnv* env, jclass cls, jobject owner, jboolean open, + jstring title, jstring okButtonLabel, jstring fileNameLabel, jstring fileName, + jstring folder, jstring saveAsItem, jstring defaultFolder, jstring defaultExtension, + jint optionsSet, jint optionsClear, jobject callback, jint fileTypeIndex, jobjectArray fileTypes ) +{ + // initialize COM library + CoInitializer coInitializer; + if( !coInitializer.initialized ) + return NULL; + + // handle limitations (without this, some Win32 method fails and this method returns NULL) + if( isOptionSet( FOS_PICKFOLDERS ) ) { + open = true; // always use IFileOpenDialog for picking folders + fileTypes = NULL; // no filter allowed for picking folders + } + if( !open && isOptionSet( FOS_ALLOWMULTISELECT ) ) + optionsSet &= ~FOS_ALLOWMULTISELECT; + + // convert Java strings to C strings + AutoReleaseString ctitle( env, title ); + AutoReleaseString cokButtonLabel( env, okButtonLabel ); + AutoReleaseString cfileNameLabel( env, fileNameLabel ); + AutoReleaseString cfileName( env, fileName ); + AutoReleaseIShellItem cfolder( env, folder ); + AutoReleaseIShellItem csaveAsItem( env, saveAsItem ); + AutoReleaseIShellItem cdefaultFolder( env, defaultFolder ); + AutoReleaseString cdefaultExtension( env, defaultExtension ); + FilterSpec specs( env, fileTypes ); + + // create IFileOpenDialog or IFileSaveDialog + // https://learn.microsoft.com/en-us/windows/win32/shell/common-file-dialog + AutoReleasePtr dialog; + CHECK_HRESULT( ::CoCreateInstance( open ? CLSID_FileOpenDialog : CLSID_FileSaveDialog, + NULL, CLSCTX_INPROC_SERVER, open ? IID_IFileOpenDialog : IID_IFileSaveDialog, + reinterpret_cast( &dialog ) ) ); + + // set title, etc. + if( ctitle != NULL ) + CHECK_HRESULT( dialog->SetTitle( ctitle ) ); + if( cokButtonLabel != NULL ) + CHECK_HRESULT( dialog->SetOkButtonLabel( cokButtonLabel ) ); + if( cfileNameLabel != NULL ) + CHECK_HRESULT( dialog->SetFileNameLabel( cfileNameLabel ) ); + if( cfileName != NULL ) + CHECK_HRESULT( dialog->SetFileName( cfileName ) ); + if( cfolder != NULL ) + CHECK_HRESULT( dialog->SetFolder( cfolder ) ); + if( !open && csaveAsItem != NULL ) + CHECK_HRESULT( ((IFileSaveDialog*)(IFileDialog*)dialog)->SetSaveAsItem( csaveAsItem ) ); + if( cdefaultFolder != NULL ) + CHECK_HRESULT( dialog->SetDefaultFolder( cdefaultFolder ) ); + if( cdefaultExtension != NULL ) + CHECK_HRESULT( dialog->SetDefaultExtension( cdefaultExtension ) ); + + // set options + FILEOPENDIALOGOPTIONS existingOptions; + CHECK_HRESULT( dialog->GetOptions( &existingOptions ) ); + CHECK_HRESULT( dialog->SetOptions ( (existingOptions & ~optionsClear) | optionsSet ) ); + + // initialize filter + if( specs.count > 0 ) { + CHECK_HRESULT( dialog->SetFileTypes( specs.count, specs.specs ) ); + if( fileTypeIndex > 0 ) + CHECK_HRESULT( dialog->SetFileTypeIndex( min( fileTypeIndex + 1, specs.count ) ) ); + } + + // add event handler + AutoReleasePtr handler( new DialogEventHandler( env, open, callback ) ); + DWORD dwCookie = 0; + CHECK_HRESULT( dialog->Advise( handler, &dwCookie ) ); + + // show dialog + HWND hwndOwner = (owner != NULL) ? getWindowHandle( env, owner ) : NULL; + HRESULT hr = dialog->Show( hwndOwner ); + dialog->Unadvise( dwCookie ); + if( hr == HRESULT_FROM_WIN32(ERROR_CANCELLED) ) + return newJavaStringArray( env, 0 ); + CHECK_HRESULT( hr ); + + // get selected files as Java string array + return getFiles( env, open, dialog ); +} + +static jobjectArray getFiles( JNIEnv* env, jboolean open, IFileDialog* dialog ) { + if( open ) { + AutoReleasePtr shellItems; + DWORD count; + CHECK_HRESULT( ((IFileOpenDialog*)(IFileDialog*)dialog)->GetResults( &shellItems ) ); + CHECK_HRESULT( shellItems->GetCount( &count ) ); + + // convert shell items to Java string array + jobjectArray array = newJavaStringArray( env, count ); + for( int i = 0; i < count; i++ ) { + AutoReleasePtr shellItem; + LPWSTR path; + CHECK_HRESULT( shellItems->GetItemAt( i, &shellItem ) ); + CHECK_HRESULT( shellItem->GetDisplayName( SIGDN_FILESYSPATH, &path ) ); + + jstring jpath = newJavaString( env, path ); + CoTaskMemFree( path ); + + env->SetObjectArrayElement( array, i, jpath ); + env->DeleteLocalRef( jpath ); + } + return array; + } else { + AutoReleasePtr shellItem; + LPWSTR path; + CHECK_HRESULT( dialog->GetResult( &shellItem ) ); + CHECK_HRESULT( shellItem->GetDisplayName( SIGDN_FILESYSPATH, &path ) ); + + // convert shell item to Java string array + jstring jpath = newJavaString( env, path ); + CoTaskMemFree( path ); + + jobjectArray array = newJavaStringArray( env, 1 ); + env->SetObjectArrayElement( array, 0, jpath ); + env->DeleteLocalRef( jpath ); + + return array; + } +} diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp new file mode 100644 index 000000000..1445c4318 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinMessageDialog.cpp @@ -0,0 +1,419 @@ +/* + * Copyright 2025 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// avoid inlining of printf() +#define _NO_CRT_STDIO_INLINE + +#include +#include +#include "JNIUtils.h" +#include "com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h" + +/** + * @author Karl Tauber + * @since 3.7 + */ + +#define ID_BUTTON1 101 + +// declare external fields +extern HINSTANCE _instance; + +// declare internal methods +static byte* createInMemoryTemplate( HWND owner, int messageType, LPCWSTR title, LPCWSTR text, + int defaultButton, int buttonCount, LPCWSTR* buttons ); +static INT_PTR CALLBACK messageDialogProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ); +static int textLengthAsDLUs( HDC hdc, LPCWSTR str, int strLen ); +static LONG pixel2dluX( LONG px ); +static LONG pixel2dluY( LONG px ); +static LONG dluX2pixel( LONG dluX ); +static LPWORD lpwAlign( LPWORD lpIn ); + + +extern "C" +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageBox + ( JNIEnv* env, jclass cls, jlong hwndParent, jstring text, jstring caption, jint type ) +{ + // convert Java strings to C strings + AutoReleaseString ctext( env, text ); + AutoReleaseString ccaption( env, caption ); + + return ::MessageBox( reinterpret_cast( hwndParent ), ctext, ccaption, type ); +} + +extern "C" +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageDialog + ( JNIEnv* env, jclass cls, jlong hwndParent, jint messageType, jstring title, + jstring text, jint defaultButton, jobjectArray buttons ) +{ + HWND owner = reinterpret_cast( hwndParent ); + + // convert Java strings to C strings + AutoReleaseString ctitle( env, title ); + AutoReleaseString ctext( env, text ); + AutoReleaseStringArray cbuttons( env, buttons ); + + // get title from parent window if necessary + WCHAR parentTitle[100]; + if( ctitle == NULL ) + ::GetWindowText( owner, parentTitle, 100 ); + + byte* templ = createInMemoryTemplate( owner, messageType, (ctitle != NULL) ? ctitle : parentTitle, + ctext, defaultButton, cbuttons.count, cbuttons ); + if( templ == NULL ) + return -1; + + LRESULT ret = ::DialogBoxIndirect( _instance, (LPDLGTEMPLATE) templ, owner, messageDialogProc ); + delete templ; + return (ret >= ID_BUTTON1) ? ret - ID_BUTTON1 : -1; +} + + +// all values in DLUs + +#define INSETS_TOP 12 +#define INSETS_LEFT 12 +#define INSETS_RIGHT 12 +#define INSETS_BOTTOM 6 + +#define ICON_TEXT_GAP 8 + +#define LABEL_MIN_WIDTH 100 +#define LABEL_MAX_WIDTH 250 +#define LABEL_HEIGHT 8 + +#define BUTTON_WIDTH 50 +#define BUTTON_HEIGHT 12 +#define BUTTON_GAP 5 +#define BUTTON_TOP_GAP 14 +#define BUTTON_LEFT_RIGHT_GAP 8 + +// based on https://learn.microsoft.com/en-us/windows/win32/dlgbox/using-dialog-boxes#creating-a-template-in-memory +static byte* createInMemoryTemplate( HWND owner, int messageType, LPCWSTR title, LPCWSTR text, + int defaultButton, int buttonCount, LPCWSTR* buttons ) +{ + // get font info needed for DS_SETFONT + NONCLIENTMETRICS ncMetrics; + ncMetrics.cbSize = sizeof( NONCLIENTMETRICS ); + if( !::SystemParametersInfo( SPI_GETNONCLIENTMETRICS, 0, &ncMetrics, 0 ) ) + return NULL; + + // create DC to use message font + HDC hdcOwner = ::GetDC( owner ); + HDC hdc = ::CreateCompatibleDC( hdcOwner ); + ::ReleaseDC( owner, hdcOwner ); + if( hdc == NULL ) + return NULL; + + HFONT hfont = ::CreateFontIndirect( &ncMetrics.lfMessageFont ); + if( hfont == NULL ) { + ::DeleteDC( hdc ); + return NULL; + } + + if( ::SelectObject( hdc, hfont ) == NULL ) { + ::DeleteDC( hdc ); + ::DeleteObject( hfont ); + return NULL; + } + + //---- calculate layout (in DLUs) ---- + + // layout icon + LPWSTR icon; + switch( messageType ) { + case /* JOptionPane.ERROR_MESSAGE */ 0: icon = IDI_ERROR; break; + case /* JOptionPane.INFORMATION_MESSAGE */ 1: icon = IDI_INFORMATION; break; + case /* JOptionPane.WARNING_MESSAGE */ 2: icon = IDI_WARNING; break; + case /* JOptionPane.QUESTION_MESSAGE */ 3: icon = IDI_QUESTION; break; + default: + case /* JOptionPane.PLAIN_MESSAGE */ -1: icon = NULL; break; + } + int ix = INSETS_LEFT; + int iy = INSETS_TOP; + int iw = pixel2dluX( ::GetSystemMetrics( SM_CXICON ) ); + int ih = pixel2dluY( ::GetSystemMetrics( SM_CYICON ) ); + + // layout text + int tx = ix + (icon != NULL ? iw + ICON_TEXT_GAP : 0); + int ty = iy; + int tw = 0; + int th = 0; + if( text == NULL ) + text = L""; + LPWSTR wrappedText = new WCHAR[wcslen( text ) + 1]; + wcscpy( wrappedText, text ); + LPWSTR lineStart = wrappedText; + for( LPWSTR t = wrappedText; ; t++ ) { + if( *t != '\n' && *t != 0 ) + continue; + + // calculate line width (in pixels) and number of charaters that fit into LABEL_MAX_WIDTH + int lineLen = t - lineStart; + int fit = 0; + SIZE size{ 0 }; + if( !::GetTextExtentExPoint( hdc, lineStart, lineLen, dluX2pixel( LABEL_MAX_WIDTH ), &fit, NULL, &size ) ) + break; + + if( fit < lineLen ) { + // wrap too long line --> try to wrap at space character + bool wrapped = false; + for( LPWSTR t2 = lineStart + fit - 1; t2 > lineStart; t2-- ) { + if( *t2 == ' ' || *t2 == '\t' ) { + *t2 = '\n'; + int w = textLengthAsDLUs( hdc, lineStart, t2 - lineStart ); + tw = max( tw, w ); + th += LABEL_HEIGHT; + + // continue wrapping after inserted line break + t = t2; + lineStart = t + 1; + wrapped = true; + break; + } + } + if( !wrapped ) { + // not able to wrap at word --> break long word + int breakIndex = (lineStart + fit) - wrappedText; + int w = textLengthAsDLUs( hdc, lineStart, breakIndex ); + tw = max( tw, w ); + th += LABEL_HEIGHT; + + // duplicate string + LPWSTR wrappedText2 = new WCHAR[wcslen( wrappedText ) + 1 + 1]; + // use wcscpy(), instead of wcsncpy(), because this method is inlined and does not require linking to runtime lib + wcscpy( wrappedText2, wrappedText ); + wrappedText2[breakIndex] = '\n'; + wcscpy( wrappedText2 + breakIndex + 1, wrappedText + breakIndex ); + + // delete old text + delete[] wrappedText; + wrappedText = wrappedText2; + + // continue wrapping after inserted line break + t = wrappedText + breakIndex; + lineStart = t + 1; + } + } else { + // line fits into LABEL_MAX_WIDTH + int w = pixel2dluX( size.cx ); + tw = max( tw, w ); + th += LABEL_HEIGHT; + lineStart = t + 1; + } + + if( *t == 0 ) + break; + } + tw = min( max( tw, LABEL_MIN_WIDTH ), LABEL_MAX_WIDTH ); + th = max( th, LABEL_HEIGHT ); + if( icon != NULL && th < ih ) + ty += (ih - th) / 2; // vertically center text + + // layout buttons + int* bw = new int[buttonCount]; + int buttonTotalWidth = BUTTON_GAP * (buttonCount - 1); + for( int i = 0; i < buttonCount; i++ ) { + int w = textLengthAsDLUs( hdc, buttons[i], -1 ) + 16; + bw[i] = max( BUTTON_WIDTH, w ); + buttonTotalWidth += bw[i]; + } + + // layout dialog + int dx = 0; + int dy = 0; + int dw = max( tx + tw + INSETS_RIGHT, BUTTON_LEFT_RIGHT_GAP + buttonTotalWidth + BUTTON_LEFT_RIGHT_GAP ); + int dh = max( iy + ih, ty + th ) + BUTTON_TOP_GAP + BUTTON_HEIGHT + INSETS_BOTTOM; + + // center dialog in owner + RECT ownerRect{ 0 }; + if( ::GetClientRect( owner, &ownerRect ) ) { + dx = (pixel2dluX( ownerRect.right - ownerRect.left ) - dw) / 2; + dy = (pixel2dluY( ownerRect.bottom - ownerRect.top ) - dh) / 2; + } + + // layout button area + int bx = dw - buttonTotalWidth - BUTTON_LEFT_RIGHT_GAP; + int by = dh - BUTTON_HEIGHT - INSETS_BOTTOM; + + // get font info needed for DS_SETFONT + int fontPointSize = (ncMetrics.lfMessageFont.lfHeight < 0) + ? -MulDiv( ncMetrics.lfMessageFont.lfHeight, 72, ::GetDeviceCaps( hdc, LOGPIXELSY ) ) + : ncMetrics.lfMessageFont.lfHeight; + LPCWSTR fontFaceName = ncMetrics.lfMessageFont.lfFaceName; + + // delete DC and font + ::DeleteDC( hdc ); + ::DeleteObject( hfont ); + + // (approximately) calculate memory size needed for in-memory template + int templSize = (sizeof(DLGTEMPLATE) + /*menu*/ 2 + /*class*/ 2 + /*title*/ 2) + + ((sizeof(DLGITEMTEMPLATE) + /*class*/ 4 + /*title/icon*/ 4 + /*creation data*/ 2) * (/*icon+text*/2 + buttonCount)) + + (title != NULL ? (wcslen( title ) + 1) * sizeof(wchar_t) : 0) + + /*fontPointSize*/ 2 + ((wcslen( fontFaceName ) + 1) * sizeof(wchar_t)) + + ((wcslen( wrappedText ) + 1) * sizeof(wchar_t)); + for( int i = 0; i < buttonCount; i++ ) + templSize += ((wcslen( buttons[i] ) + 1) * sizeof(wchar_t)); + + templSize += (2 * (1 + 1 + buttonCount)); // necessary for DWORD alignment + templSize += 100; // some reserve + + // allocate memory for in-memory template + byte* templ = new byte[templSize]; + if( templ == NULL ) + return NULL; + + + //---- define dialog box ---- + + LPDLGTEMPLATE lpdt = (LPDLGTEMPLATE) templ; + lpdt->style = WS_POPUP | WS_BORDER | WS_SYSMENU | DS_MODALFRAME | WS_CAPTION | DS_SETFONT; + lpdt->cdit = /*text*/ 1 + buttonCount; // number of controls + lpdt->x = dx; + lpdt->y = dy; + lpdt->cx = dw; + lpdt->cy = dh; + + LPWORD lpw = (LPWORD) (lpdt + 1); + *lpw++ = 0; // no menu + *lpw++ = 0; // predefined dialog box class (by default) + if( title != NULL ) { + wcscpy( (LPWSTR) lpw, title ); + lpw += wcslen( title ) + 1; + } else + *lpw++ = 0; // no title + + // for DS_SETFONT + *lpw++ = fontPointSize; + wcscpy( (LPWSTR) lpw, fontFaceName ); + lpw += wcslen( fontFaceName ) + 1; + + //---- define icon ---- + + if( icon != NULL ) { + lpdt->cdit++; + + lpw = lpwAlign( lpw ); + LPDLGITEMTEMPLATE lpdit = (LPDLGITEMTEMPLATE) lpw; + lpdit->x = ix; + lpdit->y = iy; + lpdit->cx = iw; + lpdit->cy = ih; + lpdit->id = ID_BUTTON1 - 1; + lpdit->style = WS_CHILD | WS_VISIBLE | SS_ICON; + + lpw = (LPWORD) (lpdit + 1); + *lpw++ = 0xffff; *lpw++ = 0x0082; // Static class + *lpw++ = 0xffff; *lpw++ = (WORD) icon; // icon + *lpw++ = 0; // creation data + } + + + //---- define text ---- + + lpw = lpwAlign( lpw ); + LPDLGITEMTEMPLATE lpdit = (LPDLGITEMTEMPLATE) lpw; + lpdit->x = tx; + lpdit->y = ty; + lpdit->cx = tw; + lpdit->cy = th; + lpdit->id = ID_BUTTON1 - 2; + lpdit->style = WS_CHILD | WS_VISIBLE | SS_LEFT | SS_NOPREFIX | SS_EDITCONTROL; + + lpw = (LPWORD) (lpdit + 1); + *lpw++ = 0xffff; *lpw++ = 0x0082; // Static class + wcscpy( (LPWSTR) lpw, wrappedText ); lpw += wcslen( wrappedText ) + 1; // text + *lpw++ = 0; // creation data + + + //---- define buttons ---- + + defaultButton = min( max( defaultButton, 0 ), buttonCount - 1 ); + int buttonId = ID_BUTTON1; + for( int i = 0; i < buttonCount; i++ ) { + lpw = lpwAlign( lpw ); + LPDLGITEMTEMPLATE lpdit = (LPDLGITEMTEMPLATE) lpw; + lpdit->x = bx; + lpdit->y = by; + lpdit->cx = bw[i]; + lpdit->cy = BUTTON_HEIGHT; + lpdit->id = buttonId++; + lpdit->style = WS_CHILD | WS_VISIBLE | WS_TABSTOP | (i == 0 ? WS_GROUP : 0) + | BS_TEXT | (i == defaultButton ? BS_DEFPUSHBUTTON : BS_PUSHBUTTON); + + lpw = (LPWORD) (lpdit + 1); + *lpw++ = 0xffff; *lpw++ = 0x0080; // Button class + wcscpy( (LPWSTR) lpw, buttons[i] ); lpw += wcslen( buttons[i] ) + 1; // text + *lpw++ = 0; // creation data + + bx += bw[i] + BUTTON_GAP; + } + + delete[] wrappedText; + delete[] bw; + + return templ; +} + +static BOOL CALLBACK focusDefaultButtonProc( HWND hwnd, LPARAM lParam ) { + if( ::GetWindowLong( hwnd, GWL_ID ) >= ID_BUTTON1 ) { + LONG style = ::GetWindowLong( hwnd, GWL_STYLE ); + if( (style & BS_DEFPUSHBUTTON) != 0 ) { + ::SetFocus( hwnd ); + return FALSE; + } + } + return TRUE; +} + +static INT_PTR CALLBACK messageDialogProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { + switch( uMsg ) { + case WM_INITDIALOG: + ::EnumChildWindows( hwnd, focusDefaultButtonProc, 0 ); + break; + + case WM_COMMAND: + ::EndDialog( hwnd, wParam ); + return TRUE; + } + return FALSE; +} + +static int textLengthAsDLUs( HDC hdc, LPCWSTR str, int strLen ) { + SIZE size{ 0 }; + ::GetTextExtentPoint32( hdc, str, (strLen >= 0) ? strLen : wcslen( str ), &size ); + return pixel2dluX( size.cx ); +} + +static LONG pixel2dluX( LONG px ) { + return MulDiv( px, 4, LOWORD( ::GetDialogBaseUnits() ) ); +} + +static LONG pixel2dluY( LONG py ) { + return MulDiv( py, 8, HIWORD( ::GetDialogBaseUnits() ) ); +} + +static LONG dluX2pixel( LONG dluX ) { + return MulDiv( dluX, LOWORD( ::GetDialogBaseUnits() ), 4 ); +} + +static LPWORD lpwAlign( LPWORD lpIn ) { + ULONG_PTR ul = (ULONG_PTR) lpIn; + ul += 3; + ul >>= 2; + ul <<= 2; + return (LPWORD) ul; +} diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinWrapper.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinWrapper.cpp index c1276973f..b77ef1d5d 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinWrapper.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/WinWrapper.cpp @@ -25,8 +25,8 @@ * @author Karl Tauber */ -// see FlatWndProc.cpp -HWND getWindowHandle( JNIEnv* env, jobject window ); +// declare external methods +extern HWND getWindowHandle( JNIEnv* env, jobject window ); //---- Utility ---------------------------------------------------------------- diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/JNIUtils.h b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/JNIUtils.h new file mode 100644 index 000000000..5eaad97cd --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/JNIUtils.h @@ -0,0 +1,53 @@ +/* + * Copyright 2025 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +/** + * @author Karl Tauber + */ + +//---- class AutoReleaseString ------------------------------------------------ + +class AutoReleaseString { + JNIEnv* env; + jstring javaString; + const jchar* chars; + +public: + AutoReleaseString( JNIEnv* _env, jstring _javaString ); + ~AutoReleaseString(); + + operator LPCWSTR() { return (LPCWSTR) chars; } +}; + +//---- class AutoReleaseStringArray ------------------------------------------- + +class AutoReleaseStringArray { + JNIEnv* env; + jstring* javaStringArray; + const jchar** charsArray; + +public: + UINT count; + +public: + AutoReleaseStringArray( JNIEnv* _env, jobjectArray _javaStringArray ); + ~AutoReleaseStringArray(); + + operator LPCWSTR*() { return (LPCWSTR*) charsArray; } +}; diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h index 1701cb3bc..b5b332458 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatNativeWindowsLibrary.h @@ -27,6 +27,52 @@ extern "C" { #define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_DWMWA_COLOR_DEFAULT -1L #undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_DWMWA_COLOR_NONE #define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_DWMWA_COLOR_NONE -2L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_OVERWRITEPROMPT +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_OVERWRITEPROMPT 2L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_STRICTFILETYPES +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_STRICTFILETYPES 4L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOCHANGEDIR +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOCHANGEDIR 8L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_PICKFOLDERS +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_PICKFOLDERS 32L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCEFILESYSTEM +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCEFILESYSTEM 64L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_ALLNONSTORAGEITEMS +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_ALLNONSTORAGEITEMS 128L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOVALIDATE +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOVALIDATE 256L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_ALLOWMULTISELECT +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_ALLOWMULTISELECT 512L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_PATHMUSTEXIST +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_PATHMUSTEXIST 2048L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FILEMUSTEXIST +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FILEMUSTEXIST 4096L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_CREATEPROMPT +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_CREATEPROMPT 8192L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_SHAREAWARE +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_SHAREAWARE 16384L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOREADONLYRETURN +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOREADONLYRETURN 32768L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOTESTFILECREATE +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NOTESTFILECREATE 65536L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_HIDEMRUPLACES +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_HIDEMRUPLACES 131072L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_HIDEPINNEDPLACES +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_HIDEPINNEDPLACES 262144L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NODEREFERENCELINKS +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_NODEREFERENCELINKS 1048576L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_OKBUTTONNEEDSINTERACTION +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_OKBUTTONNEEDSINTERACTION 2097152L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_DONTADDTORECENT +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_DONTADDTORECENT 33554432L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCESHOWHIDDEN +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCESHOWHIDDEN 268435456L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_DEFAULTNOMINIMODE +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_DEFAULTNOMINIMODE 536870912L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCEPREVIEWPANEON +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_FORCEPREVIEWPANEON 1073741824L +#undef com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_SUPPORTSTREAMABLEITEMS +#define com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_FOS_SUPPORTSTREAMABLEITEMS -2147483648L /* * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary * Method: getOSBuildNumberImpl @@ -67,6 +113,30 @@ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_ JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_dwmSetWindowAttributeDWORD (JNIEnv *, jclass, jlong, jint, jint); +/* + * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary + * Method: showFileChooser + * Signature: (Ljava/awt/Window;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILcom/formdev/flatlaf/ui/FlatNativeWindowsLibrary/FileChooserCallback;I[Ljava/lang/String;)[Ljava/lang/String; + */ +JNIEXPORT jobjectArray JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showFileChooser + (JNIEnv *, jclass, jobject, jboolean, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jstring, jint, jint, jobject, jint, jobjectArray); + +/* + * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary + * Method: showMessageDialog + * Signature: (JILjava/lang/String;Ljava/lang/String;I[Ljava/lang/String;)I + */ +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageDialog + (JNIEnv *, jclass, jlong, jint, jstring, jstring, jint, jobjectArray); + +/* + * Class: com_formdev_flatlaf_ui_FlatNativeWindowsLibrary + * Method: showMessageBox + * Signature: (JLjava/lang/String;Ljava/lang/String;I)I + */ +JNIEXPORT jint JNICALL Java_com_formdev_flatlaf_ui_FlatNativeWindowsLibrary_showMessageBox + (JNIEnv *, jclass, jlong, jstring, jstring, jint); + #ifdef __cplusplus } #endif diff --git a/flatlaf-testing/build.gradle.kts b/flatlaf-testing/build.gradle.kts index 6fd1b64f8..7215de8b4 100644 --- a/flatlaf-testing/build.gradle.kts +++ b/flatlaf-testing/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation( libs.jide.oss ) implementation( libs.glazedlists ) implementation( libs.netbeans.api.awt ) + implementation( libs.nativejfilechooser ) components.all() } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java new file mode 100644 index 000000000..65c68b498 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.java @@ -0,0 +1,382 @@ +/* + * Copyright 2025 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.testing; + +import static com.formdev.flatlaf.ui.FlatNativeLinuxLibrary.*; +import java.awt.EventQueue; +import java.awt.SecondaryLoop; +import java.awt.Toolkit; +import java.awt.Window; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import javax.swing.*; +import com.formdev.flatlaf.extras.components.*; +import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; +import com.formdev.flatlaf.testing.FlatSystemFileChooserTest.DummyModalDialog; +import com.formdev.flatlaf.ui.FlatNativeLinuxLibrary; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatSystemFileChooserLinuxTest + extends FlatTestPanel +{ + public static void main( String[] args ) { + SwingUtilities.invokeLater( () -> { + if( !FlatNativeLinuxLibrary.isLoaded() ) { + JOptionPane.showMessageDialog( null, "FlatLaf native library not loaded" ); + return; + } + + FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserLinuxTest" ); + FlatSystemFileChooserTest.addListeners( frame ); + frame.showFrame( FlatSystemFileChooserLinuxTest::new ); + } ); + } + + FlatSystemFileChooserLinuxTest() { + initComponents(); + + fileTypesField.setSelectedItem( null ); + } + + private void open() { + openOrSave( true, false ); + } + + private void save() { + openOrSave( false, false ); + } + + private void openDirect() { + openOrSave( true, true ); + } + + private void saveDirect() { + openOrSave( false, true ); + } + + private void openOrSave( boolean open, boolean direct ) { + Window frame = SwingUtilities.windowForComponent( this ); + if( ownerFrameRadioButton.isSelected() ) + openOrSave( open, direct, frame ); + else if( ownerDialogRadioButton.isSelected() ) + new DummyModalDialog( frame, owner -> openOrSave( open, direct, owner ) ).setVisible( true ); + else + openOrSave( open, direct, null ); + } + + private void openOrSave( boolean open, boolean direct, Window owner ) { + String title = n( titleField.getText() ); + String okButtonLabel = n( okButtonLabelField.getText() ); + String currentName = n( currentNameField.getText() ); + String currentFolder = n( currentFolderField.getText() ); + AtomicInteger optionsSet = new AtomicInteger(); + AtomicInteger optionsClear = new AtomicInteger(); + + o( FC_select_folder, select_folderCheckBox, optionsSet, optionsClear ); + o( FC_select_multiple, select_multipleCheckBox, optionsSet, optionsClear ); + o( FC_show_hidden, show_hiddenCheckBox, optionsSet, optionsClear ); + o( FC_local_only, local_onlyCheckBox, optionsSet, optionsClear ); + o( FC_do_overwrite_confirmation, do_overwrite_confirmationCheckBox, optionsSet, optionsClear ); + o( FC_create_folders, create_foldersCheckBox, optionsSet, optionsClear ); + + String fileTypesStr = n( (String) fileTypesField.getSelectedItem() ); + String[] fileTypes = {}; + if( fileTypesStr != null ) { + if( !fileTypesStr.endsWith( ",null" ) ) + fileTypesStr += ",null"; + fileTypes = fileTypesStr.trim().split( "[,]+" ); + for( int i = 0; i < fileTypes.length; i++ ) { + if( "null".equals( fileTypes[i] ) ) + fileTypes[i] = null; + } + } + int fileTypeIndex = fileTypeIndexSlider.getValue(); + + FlatNativeLinuxLibrary.FileChooserCallback callback = (files, hwndFileDialog) -> { + System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) ); + if( showMessageDialogOnOKCheckBox.isSelected() ) { + System.out.println( FlatNativeLinuxLibrary.showMessageDialog( hwndFileDialog, + JOptionPane.INFORMATION_MESSAGE, + "primary text", "secondary text", 1, "Yes", "No" ) ); + } + return true; + }; + + System.out.println( FlatNativeLinuxLibrary.isGtk3Available() ); + + if( direct ) { + String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, open, + title, okButtonLabel, currentName, currentFolder, + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes ); + + filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); + } else { + SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); + + String[] fileTypes2 = fileTypes; + new Thread( () -> { + String[] files = FlatNativeLinuxLibrary.showFileChooser( owner, open, + title, okButtonLabel, currentName, currentFolder, + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 ); + + System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); + + EventQueue.invokeLater( () -> { + filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); + } ); + } ).start(); + + System.out.println( "---- enter secondary loop ----" ); + System.out.println( "---- secondary loop exited (secondaryLoop.enter() returned " + secondaryLoop.enter() + ") ----" ); + } + } + + private static String n( String s ) { + return s != null && !s.isEmpty() ? s : null; + } + + private static void o( int option, FlatTriStateCheckBox checkBox, AtomicInteger optionsSet, AtomicInteger optionsClear ) { + if( checkBox.getState() == State.SELECTED ) + optionsSet.set( optionsSet.get() | option ); + else if( checkBox.getState() == State.UNSELECTED ) + optionsClear.set( optionsClear.get() | option ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + JLabel ownerLabel = new JLabel(); + ownerFrameRadioButton = new JRadioButton(); + ownerDialogRadioButton = new JRadioButton(); + ownerNullRadioButton = new JRadioButton(); + JPanel ownerSpacer = new JPanel(null); + JLabel titleLabel = new JLabel(); + titleField = new JTextField(); + JPanel panel1 = new JPanel(); + select_folderCheckBox = new FlatTriStateCheckBox(); + select_multipleCheckBox = new FlatTriStateCheckBox(); + do_overwrite_confirmationCheckBox = new FlatTriStateCheckBox(); + create_foldersCheckBox = new FlatTriStateCheckBox(); + show_hiddenCheckBox = new FlatTriStateCheckBox(); + local_onlyCheckBox = new FlatTriStateCheckBox(); + JLabel okButtonLabelLabel = new JLabel(); + okButtonLabelField = new JTextField(); + JLabel currentNameLabel = new JLabel(); + currentNameField = new JTextField(); + JLabel currentFolderLabel = new JLabel(); + currentFolderField = new JTextField(); + JLabel fileTypesLabel = new JLabel(); + fileTypesField = new JComboBox<>(); + JLabel fileTypeIndexLabel = new JLabel(); + fileTypeIndexSlider = new JSlider(); + JButton openButton = new JButton(); + JButton saveButton = new JButton(); + JButton openDirectButton = new JButton(); + JButton saveDirectButton = new JButton(); + showMessageDialogOnOKCheckBox = new JCheckBox(); + JScrollPane filesScrollPane = new JScrollPane(); + filesField = new JTextArea(); + + //======== this ======== + setLayout(new MigLayout( + "ltr,insets dialog,hidemode 3", + // columns + "[left]" + + "[grow,fill]" + + "[fill]", + // rows + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[grow,fill]")); + + //---- ownerLabel ---- + ownerLabel.setText("owner"); + add(ownerLabel, "cell 0 0"); + + //---- ownerFrameRadioButton ---- + ownerFrameRadioButton.setText("JFrame"); + ownerFrameRadioButton.setSelected(true); + add(ownerFrameRadioButton, "cell 1 0"); + + //---- ownerDialogRadioButton ---- + ownerDialogRadioButton.setText("JDialog"); + add(ownerDialogRadioButton, "cell 1 0"); + + //---- ownerNullRadioButton ---- + ownerNullRadioButton.setText("null"); + add(ownerNullRadioButton, "cell 1 0"); + add(ownerSpacer, "cell 1 0,growx"); + + //---- titleLabel ---- + titleLabel.setText("title"); + add(titleLabel, "cell 0 1"); + add(titleField, "cell 1 1"); + + //======== panel1 ======== + { + panel1.setLayout(new MigLayout( + "insets 2,hidemode 3", + // columns + "[left]", + // rows + "[]0" + + "[]0" + + "[]0" + + "[]0" + + "[]0" + + "[]")); + + //---- select_folderCheckBox ---- + select_folderCheckBox.setText("select_folder"); + select_folderCheckBox.setAllowIndeterminate(false); + select_folderCheckBox.setState(FlatTriStateCheckBox.State.UNSELECTED); + panel1.add(select_folderCheckBox, "cell 0 0"); + + //---- select_multipleCheckBox ---- + select_multipleCheckBox.setText("select_multiple"); + select_multipleCheckBox.setState(FlatTriStateCheckBox.State.UNSELECTED); + select_multipleCheckBox.setAllowIndeterminate(false); + panel1.add(select_multipleCheckBox, "cell 0 1"); + + //---- do_overwrite_confirmationCheckBox ---- + do_overwrite_confirmationCheckBox.setText("do_overwrite_confirmation"); + panel1.add(do_overwrite_confirmationCheckBox, "cell 0 2"); + + //---- create_foldersCheckBox ---- + create_foldersCheckBox.setText("create_folders"); + panel1.add(create_foldersCheckBox, "cell 0 3"); + + //---- show_hiddenCheckBox ---- + show_hiddenCheckBox.setText("show_hidden"); + panel1.add(show_hiddenCheckBox, "cell 0 4"); + + //---- local_onlyCheckBox ---- + local_onlyCheckBox.setText("local_only"); + panel1.add(local_onlyCheckBox, "cell 0 5"); + } + add(panel1, "cell 2 1 1 6,aligny top,growy 0"); + + //---- okButtonLabelLabel ---- + okButtonLabelLabel.setText("okButtonLabel"); + add(okButtonLabelLabel, "cell 0 2"); + add(okButtonLabelField, "cell 1 2"); + + //---- currentNameLabel ---- + currentNameLabel.setText("currentName"); + add(currentNameLabel, "cell 0 3"); + add(currentNameField, "cell 1 3"); + + //---- currentFolderLabel ---- + currentFolderLabel.setText("currentFolder"); + add(currentFolderLabel, "cell 0 4"); + add(currentFolderField, "cell 1 4"); + + //---- fileTypesLabel ---- + fileTypesLabel.setText("fileTypes"); + add(fileTypesLabel, "cell 0 5"); + + //---- fileTypesField ---- + fileTypesField.setEditable(true); + fileTypesField.setModel(new DefaultComboBoxModel<>(new String[] { + "Text Files,*.txt,null", + "All Files,*,null", + "Text Files,*.txt,null,PDF Files,*.pdf,null,All Files,*,null", + "Text and PDF Files,*.txt,*.pdf,null" + })); + add(fileTypesField, "cell 1 5"); + + //---- fileTypeIndexLabel ---- + fileTypeIndexLabel.setText("fileTypeIndex"); + add(fileTypeIndexLabel, "cell 0 6"); + + //---- fileTypeIndexSlider ---- + fileTypeIndexSlider.setMaximum(10); + fileTypeIndexSlider.setMajorTickSpacing(1); + fileTypeIndexSlider.setValue(0); + fileTypeIndexSlider.setPaintLabels(true); + fileTypeIndexSlider.setSnapToTicks(true); + add(fileTypeIndexSlider, "cell 1 6"); + + //---- openButton ---- + openButton.setText("Open..."); + openButton.addActionListener(e -> open()); + add(openButton, "cell 0 7 3 1"); + + //---- saveButton ---- + saveButton.setText("Save..."); + saveButton.addActionListener(e -> save()); + add(saveButton, "cell 0 7 3 1"); + + //---- openDirectButton ---- + openDirectButton.setText("Open (no-thread)..."); + openDirectButton.addActionListener(e -> openDirect()); + add(openDirectButton, "cell 0 7 3 1"); + + //---- saveDirectButton ---- + saveDirectButton.setText("Save (no-thread)..."); + saveDirectButton.addActionListener(e -> saveDirect()); + add(saveDirectButton, "cell 0 7 3 1"); + + //---- showMessageDialogOnOKCheckBox ---- + showMessageDialogOnOKCheckBox.setText("show message dialog on OK"); + add(showMessageDialogOnOKCheckBox, "cell 0 7 3 1"); + + //======== filesScrollPane ======== + { + + //---- filesField ---- + filesField.setRows(8); + filesScrollPane.setViewportView(filesField); + } + add(filesScrollPane, "cell 0 8 3 1,growx"); + + //---- ownerButtonGroup ---- + ButtonGroup ownerButtonGroup = new ButtonGroup(); + ownerButtonGroup.add(ownerFrameRadioButton); + ownerButtonGroup.add(ownerDialogRadioButton); + ownerButtonGroup.add(ownerNullRadioButton); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JRadioButton ownerFrameRadioButton; + private JRadioButton ownerDialogRadioButton; + private JRadioButton ownerNullRadioButton; + private JTextField titleField; + private FlatTriStateCheckBox select_folderCheckBox; + private FlatTriStateCheckBox select_multipleCheckBox; + private FlatTriStateCheckBox do_overwrite_confirmationCheckBox; + private FlatTriStateCheckBox create_foldersCheckBox; + private FlatTriStateCheckBox show_hiddenCheckBox; + private FlatTriStateCheckBox local_onlyCheckBox; + private JTextField okButtonLabelField; + private JTextField currentNameField; + private JTextField currentFolderField; + private JComboBox fileTypesField; + private JSlider fileTypeIndexSlider; + private JCheckBox showMessageDialogOnOKCheckBox; + private JTextArea filesField; + // JFormDesigner - End of variables declaration //GEN-END:variables +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd new file mode 100644 index 000000000..f3ebc9968 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserLinuxTest.jfd @@ -0,0 +1,280 @@ +JFDML JFormDesigner: "8.2.2.0.9999" Java: "21.0.1" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + auxiliary() { + "JavaCodeGenerator.defaultVariableLocal": true + } + add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "ltr,insets dialog,hidemode 3" + "$columnConstraints": "[left][grow,fill][fill]" + "$rowConstraints": "[][][][][][][][][grow,fill]" + } ) { + name: "this" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "ownerLabel" + "text": "owner" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerFrameRadioButton" + "text": "JFrame" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerDialogRadioButton" + "text": "JDialog" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerNullRadioButton" + "text": "null" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) { + name: "ownerSpacer" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0,growx" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "titleLabel" + "text": "title" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "titleField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "insets 2,hidemode 3" + "$columnConstraints": "[left]" + "$rowConstraints": "[]0[]0[]0[]0[]0[]" + } ) { + name: "panel1" + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "select_folderCheckBox" + "text": "select_folder" + "allowIndeterminate": false + "state": enum com.formdev.flatlaf.extras.components.FlatTriStateCheckBox$State UNSELECTED + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "select_multipleCheckBox" + "text": "select_multiple" + "state": enum com.formdev.flatlaf.extras.components.FlatTriStateCheckBox$State UNSELECTED + "allowIndeterminate": false + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "do_overwrite_confirmationCheckBox" + "text": "do_overwrite_confirmation" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "create_foldersCheckBox" + "text": "create_folders" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "show_hiddenCheckBox" + "text": "show_hidden" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "local_onlyCheckBox" + "text": "local_only" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1 1 6,aligny top,growy 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "okButtonLabelLabel" + "text": "okButtonLabel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "okButtonLabelField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "currentNameLabel" + "text": "currentName" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "currentNameField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "currentFolderLabel" + "text": "currentFolder" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "currentFolderField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypesLabel" + "text": "fileTypes" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "fileTypesField" + "editable": true + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "Text Files,*.txt,null" + addElement( "Text Files,*.txt,null" ) + addElement( "All Files,*,null" ) + addElement( "Text Files,*.txt,null,PDF Files,*.pdf,null,All Files,*,null" ) + addElement( "Text and PDF Files,*.txt,*.pdf,null" ) + } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypeIndexLabel" + "text": "fileTypeIndex" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "fileTypeIndexSlider" + "maximum": 10 + "majorTickSpacing": 1 + "value": 0 + "paintLabels": true + "snapToTicks": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "openButton" + "text": "Open..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "open", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "saveButton" + "text": "Save..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "save", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "openDirectButton" + "text": "Open (no-thread)..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openDirect", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "saveDirectButton" + "text": "Save (no-thread)..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "saveDirect", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7 3 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showMessageDialogOnOKCheckBox" + "text": "show message dialog on OK" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7 3 1" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "filesScrollPane" + add( new FormComponent( "javax.swing.JTextArea" ) { + name: "filesField" + "rows": 8 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1,growx" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 690, 630 ) + } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "ownerButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 640 ) + } ) + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.java new file mode 100644 index 000000000..5c6c2a994 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.java @@ -0,0 +1,564 @@ +/* + * Copyright 2024 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.testing; + +import static com.formdev.flatlaf.ui.FlatNativeMacLibrary.*; +import java.awt.SecondaryLoop; +import java.awt.Toolkit; +import java.awt.Window; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import javax.swing.*; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.extras.components.*; +import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; +import com.formdev.flatlaf.testing.FlatSystemFileChooserTest.DummyModalDialog; +import com.formdev.flatlaf.ui.FlatNativeMacLibrary; +import com.formdev.flatlaf.util.SystemInfo; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatSystemFileChooserMacTest + extends FlatTestPanel +{ + public static void main( String[] args ) { + // macOS (see https://www.formdev.com/flatlaf/macos/) + if( SystemInfo.isMacOS ) { + // enable screen menu bar + // (moves menu bar from JFrame window to top of screen) + System.setProperty( "apple.laf.useScreenMenuBar", "true" ); + + // appearance of window title bars + // possible values: + // - "system": use current macOS appearance (light or dark) + // - "NSAppearanceNameAqua": use light appearance + // - "NSAppearanceNameDarkAqua": use dark appearance + // (needs to be set on main thread; setting it on AWT thread does not work) + System.setProperty( "apple.awt.application.appearance", "system" ); + } + + SwingUtilities.invokeLater( () -> { + if( !FlatNativeMacLibrary.isLoaded() ) { + JOptionPane.showMessageDialog( null, "FlatLaf native library not loaded" ); + return; + } + + FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserMacTest" ); + FlatSystemFileChooserTest.addListeners( frame ); + frame.showFrame( FlatSystemFileChooserMacTest::new ); + frame.setJMenuBar( menuBar1 ); + } ); + } + + FlatSystemFileChooserMacTest() { + initComponents(); + + fileTypesField.setSelectedItem( null ); + } + + private void open() { + openOrSave( true, false ); + } + + private void save() { + openOrSave( false, false ); + } + + private void openDirect() { + openOrSave( true, true ); + } + + private void saveDirect() { + openOrSave( false, true ); + } + + private void openOrSave( boolean open, boolean direct ) { + Window frame = SwingUtilities.windowForComponent( this ); + if( ownerFrameRadioButton.isSelected() ) + openOrSave( open, direct, frame ); + else if( ownerDialogRadioButton.isSelected() ) + new DummyModalDialog( frame, owner -> openOrSave( open, direct, owner ) ).setVisible( true ); + else + openOrSave( open, direct, null ); + } + + private void openOrSave( boolean open, boolean direct, Window owner ) { + String title = n( titleField.getText() ); + String prompt = n( promptField.getText() ); + String message = n( messageField.getText() ); + String filterFieldLabel = n( filterFieldLabelField.getText() ); + String nameFieldLabel = n( nameFieldLabelField.getText() ); + String nameFieldStringValue = n( nameFieldStringValueField.getText() ); + String directoryURL = n( directoryURLField.getText() ); + AtomicInteger optionsSet = new AtomicInteger(); + AtomicInteger optionsClear = new AtomicInteger(); + + // NSOpenPanel + if( canChooseFilesCheckBox.isSelected() ) + optionsSet.set( optionsSet.get() | FC_canChooseFiles ); + if( canChooseDirectoriesCheckBox.isSelected() ) + optionsSet.set( optionsSet.get() | FC_canChooseDirectories ); + o( FC_resolvesAliases, resolvesAliasesCheckBox, optionsSet, optionsClear ); + o( FC_allowsMultipleSelection, allowsMultipleSelectionCheckBox, optionsSet, optionsClear ); + if( accessoryViewDisclosedCheckBox.isSelected() ) + optionsSet.set( optionsSet.get() | FC_accessoryViewDisclosed ); + + // NSSavePanel + o( FC_showsTagField, showsTagFieldCheckBox, optionsSet, optionsClear ); + o( FC_canCreateDirectories, canCreateDirectoriesCheckBox, optionsSet, optionsClear ); + o( FC_canSelectHiddenExtension, canSelectHiddenExtensionCheckBox, optionsSet, optionsClear ); + o( FC_showsHiddenFiles, showsHiddenFilesCheckBox, optionsSet, optionsClear ); + o( FC_extensionHidden, extensionHiddenCheckBox, optionsSet, optionsClear ); + o( FC_allowsOtherFileTypes, allowsOtherFileTypesCheckBox, optionsSet, optionsClear ); + o( FC_treatsFilePackagesAsDirectories, treatsFilePackagesAsDirectoriesCheckBox, optionsSet, optionsClear ); + + // custom + if( showSingleFilterFieldCheckBox.isSelected() ) + optionsSet.set( optionsSet.get() | FC_showSingleFilterField ); + + String fileTypesStr = n( (String) fileTypesField.getSelectedItem() ); + String[] fileTypes = {}; + if( fileTypesStr != null ) { + if( !fileTypesStr.endsWith( ",null" ) ) + fileTypesStr += ",null"; + fileTypes = fileTypesStr.trim().split( "[,]+" ); + for( int i = 0; i < fileTypes.length; i++ ) { + if( "null".equals( fileTypes[i] ) ) + fileTypes[i] = null; + } + } + int fileTypeIndex = fileTypeIndexSlider.getValue(); + + FlatNativeMacLibrary.FileChooserCallback callback = (files, hwndFileDialog) -> { + System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) ); + if( showMessageDialogOnOKCheckBox.isSelected() ) { + int result = FlatNativeMacLibrary.showMessageDialog( hwndFileDialog, + JOptionPane.INFORMATION_MESSAGE, + "primary text", "secondary text", 0, "Yes", "No" ); + System.out.println( " result " + result ); + if( result != 0 ) + return false; + } + return true; + }; + + int dark = FlatLaf.isLafDark() ? 1 : 0; + if( direct ) { + String[] files = FlatNativeMacLibrary.showFileChooser( owner, dark, open, + title, prompt, message, filterFieldLabel, + nameFieldLabel, nameFieldStringValue, directoryURL, + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes ); + + filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); + } else { + SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); + + String[] fileTypes2 = fileTypes; + new Thread( () -> { + String[] files = FlatNativeMacLibrary.showFileChooser( owner, dark, open, + title, prompt, message, filterFieldLabel, + nameFieldLabel, nameFieldStringValue, directoryURL, + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 ); + + System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); + + SwingUtilities.invokeLater( () -> { + filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); + } ); + } ).start(); + + System.out.println( "---- enter secondary loop ----" ); + System.out.println( "---- secondary loop exited (secondaryLoop.enter() returned " + secondaryLoop.enter() + ") ----" ); + } + } + + private static String n( String s ) { + return s != null && !s.isEmpty() ? s : null; + } + + private static void o( int option, FlatTriStateCheckBox checkBox, AtomicInteger optionsSet, AtomicInteger optionsClear ) { + if( checkBox.getState() == State.SELECTED ) + optionsSet.set( optionsSet.get() | option ); + else if( checkBox.getState() == State.UNSELECTED ) + optionsClear.set( optionsClear.get() | option ); + } + + private void menuItemAction() { + System.out.println( "menu item action" ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + JLabel ownerLabel = new JLabel(); + ownerFrameRadioButton = new JRadioButton(); + ownerDialogRadioButton = new JRadioButton(); + ownerNullRadioButton = new JRadioButton(); + JPanel ownerSpacer = new JPanel(null); + JLabel titleLabel = new JLabel(); + titleField = new JTextField(); + JPanel panel1 = new JPanel(); + JLabel options1Label = new JLabel(); + canChooseFilesCheckBox = new JCheckBox(); + canChooseDirectoriesCheckBox = new JCheckBox(); + resolvesAliasesCheckBox = new FlatTriStateCheckBox(); + allowsMultipleSelectionCheckBox = new FlatTriStateCheckBox(); + accessoryViewDisclosedCheckBox = new JCheckBox(); + JLabel options2Label = new JLabel(); + showsTagFieldCheckBox = new FlatTriStateCheckBox(); + canCreateDirectoriesCheckBox = new FlatTriStateCheckBox(); + canSelectHiddenExtensionCheckBox = new FlatTriStateCheckBox(); + showsHiddenFilesCheckBox = new FlatTriStateCheckBox(); + extensionHiddenCheckBox = new FlatTriStateCheckBox(); + allowsOtherFileTypesCheckBox = new FlatTriStateCheckBox(); + treatsFilePackagesAsDirectoriesCheckBox = new FlatTriStateCheckBox(); + JLabel options3Label = new JLabel(); + showSingleFilterFieldCheckBox = new JCheckBox(); + JLabel promptLabel = new JLabel(); + promptField = new JTextField(); + JLabel messageLabel = new JLabel(); + messageField = new JTextField(); + JLabel filterFieldLabelLabel = new JLabel(); + filterFieldLabelField = new JTextField(); + JLabel nameFieldLabelLabel = new JLabel(); + nameFieldLabelField = new JTextField(); + JLabel nameFieldStringValueLabel = new JLabel(); + nameFieldStringValueField = new JTextField(); + JLabel directoryURLLabel = new JLabel(); + directoryURLField = new JTextField(); + JLabel fileTypesLabel = new JLabel(); + fileTypesField = new JComboBox<>(); + JLabel fileTypeIndexLabel = new JLabel(); + fileTypeIndexSlider = new JSlider(); + JButton openButton = new JButton(); + JButton saveButton = new JButton(); + JButton openDirectButton = new JButton(); + JButton saveDirectButton = new JButton(); + showMessageDialogOnOKCheckBox = new JCheckBox(); + JScrollPane filesScrollPane = new JScrollPane(); + filesField = new JTextArea(); + menuBar1 = new JMenuBar(); + JMenu menu1 = new JMenu(); + JMenuItem menuItem1 = new JMenuItem(); + JMenuItem menuItem2 = new JMenuItem(); + JMenu menu2 = new JMenu(); + JMenuItem menuItem3 = new JMenuItem(); + JMenuItem menuItem4 = new JMenuItem(); + + //======== this ======== + setLayout(new MigLayout( + "ltr,insets dialog,hidemode 3", + // columns + "[left]" + + "[grow,fill]" + + "[fill]", + // rows + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[grow,fill]")); + + //---- ownerLabel ---- + ownerLabel.setText("owner"); + add(ownerLabel, "cell 0 0"); + + //---- ownerFrameRadioButton ---- + ownerFrameRadioButton.setText("JFrame"); + ownerFrameRadioButton.setSelected(true); + add(ownerFrameRadioButton, "cell 1 0"); + + //---- ownerDialogRadioButton ---- + ownerDialogRadioButton.setText("JDialog"); + add(ownerDialogRadioButton, "cell 1 0"); + + //---- ownerNullRadioButton ---- + ownerNullRadioButton.setText("null"); + add(ownerNullRadioButton, "cell 1 0"); + add(ownerSpacer, "cell 1 0,growx"); + + //---- titleLabel ---- + titleLabel.setText("title"); + add(titleLabel, "cell 0 1"); + add(titleField, "cell 1 1"); + + //======== panel1 ======== + { + panel1.setLayout(new MigLayout( + "insets 2,hidemode 3", + // columns + "[left]", + // rows + "[]" + + "[]0" + + "[]0" + + "[]0" + + "[]0" + + "[]para" + + "[]" + + "[]0" + + "[]0" + + "[]0" + + "[]0" + + "[]0" + + "[]0" + + "[]para" + + "[]" + + "[]")); + + //---- options1Label ---- + options1Label.setText("NSOpenPanel options:"); + panel1.add(options1Label, "cell 0 0"); + + //---- canChooseFilesCheckBox ---- + canChooseFilesCheckBox.setText("canChooseFiles"); + canChooseFilesCheckBox.setSelected(true); + panel1.add(canChooseFilesCheckBox, "cell 0 1"); + + //---- canChooseDirectoriesCheckBox ---- + canChooseDirectoriesCheckBox.setText("canChooseDirectories"); + panel1.add(canChooseDirectoriesCheckBox, "cell 0 2"); + + //---- resolvesAliasesCheckBox ---- + resolvesAliasesCheckBox.setText("resolvesAliases"); + resolvesAliasesCheckBox.setState(FlatTriStateCheckBox.State.SELECTED); + panel1.add(resolvesAliasesCheckBox, "cell 0 3"); + + //---- allowsMultipleSelectionCheckBox ---- + allowsMultipleSelectionCheckBox.setText("allowsMultipleSelection"); + panel1.add(allowsMultipleSelectionCheckBox, "cell 0 4"); + + //---- accessoryViewDisclosedCheckBox ---- + accessoryViewDisclosedCheckBox.setText("accessoryViewDisclosed"); + panel1.add(accessoryViewDisclosedCheckBox, "cell 0 5"); + + //---- options2Label ---- + options2Label.setText("NSOpenPanel and NSSavePanel options:"); + panel1.add(options2Label, "cell 0 6"); + + //---- showsTagFieldCheckBox ---- + showsTagFieldCheckBox.setText("showsTagField"); + panel1.add(showsTagFieldCheckBox, "cell 0 7"); + + //---- canCreateDirectoriesCheckBox ---- + canCreateDirectoriesCheckBox.setText("canCreateDirectories"); + panel1.add(canCreateDirectoriesCheckBox, "cell 0 8"); + + //---- canSelectHiddenExtensionCheckBox ---- + canSelectHiddenExtensionCheckBox.setText("canSelectHiddenExtension"); + panel1.add(canSelectHiddenExtensionCheckBox, "cell 0 9"); + + //---- showsHiddenFilesCheckBox ---- + showsHiddenFilesCheckBox.setText("showsHiddenFiles"); + panel1.add(showsHiddenFilesCheckBox, "cell 0 10"); + + //---- extensionHiddenCheckBox ---- + extensionHiddenCheckBox.setText("extensionHidden"); + panel1.add(extensionHiddenCheckBox, "cell 0 11"); + + //---- allowsOtherFileTypesCheckBox ---- + allowsOtherFileTypesCheckBox.setText("allowsOtherFileTypes"); + panel1.add(allowsOtherFileTypesCheckBox, "cell 0 12"); + + //---- treatsFilePackagesAsDirectoriesCheckBox ---- + treatsFilePackagesAsDirectoriesCheckBox.setText("treatsFilePackagesAsDirectories"); + panel1.add(treatsFilePackagesAsDirectoriesCheckBox, "cell 0 13"); + + //---- options3Label ---- + options3Label.setText("Custom options:"); + panel1.add(options3Label, "cell 0 14"); + + //---- showSingleFilterFieldCheckBox ---- + showSingleFilterFieldCheckBox.setText("showSingleFilterField"); + panel1.add(showSingleFilterFieldCheckBox, "cell 0 15"); + } + add(panel1, "cell 2 1 1 10,aligny top,growy 0"); + + //---- promptLabel ---- + promptLabel.setText("prompt"); + add(promptLabel, "cell 0 2"); + add(promptField, "cell 1 2"); + + //---- messageLabel ---- + messageLabel.setText("message"); + add(messageLabel, "cell 0 3"); + add(messageField, "cell 1 3"); + + //---- filterFieldLabelLabel ---- + filterFieldLabelLabel.setText("filterFieldLabel"); + add(filterFieldLabelLabel, "cell 0 4"); + add(filterFieldLabelField, "cell 1 4"); + + //---- nameFieldLabelLabel ---- + nameFieldLabelLabel.setText("nameFieldLabel"); + add(nameFieldLabelLabel, "cell 0 5"); + add(nameFieldLabelField, "cell 1 5"); + + //---- nameFieldStringValueLabel ---- + nameFieldStringValueLabel.setText("nameFieldStringValue"); + add(nameFieldStringValueLabel, "cell 0 6"); + add(nameFieldStringValueField, "cell 1 6"); + + //---- directoryURLLabel ---- + directoryURLLabel.setText("directoryURL"); + add(directoryURLLabel, "cell 0 7"); + add(directoryURLField, "cell 1 7"); + + //---- fileTypesLabel ---- + fileTypesLabel.setText("fileTypes"); + add(fileTypesLabel, "cell 0 8"); + + //---- fileTypesField ---- + fileTypesField.setEditable(true); + fileTypesField.setModel(new DefaultComboBoxModel<>(new String[] { + "Text Files,txt,null", + "All Files,*,null", + "Text Files,txt,null,PDF Files,pdf,null,All Files,*,null", + "Text and PDF Files,txt,pdf,null", + "Compressed,zip,gz,null,Disk Images,dmg,null" + })); + add(fileTypesField, "cell 1 8"); + + //---- fileTypeIndexLabel ---- + fileTypeIndexLabel.setText("fileTypeIndex"); + add(fileTypeIndexLabel, "cell 0 9"); + + //---- fileTypeIndexSlider ---- + fileTypeIndexSlider.setMaximum(10); + fileTypeIndexSlider.setMajorTickSpacing(1); + fileTypeIndexSlider.setValue(0); + fileTypeIndexSlider.setPaintLabels(true); + fileTypeIndexSlider.setSnapToTicks(true); + add(fileTypeIndexSlider, "cell 1 9"); + + //---- openButton ---- + openButton.setText("Open..."); + openButton.addActionListener(e -> open()); + add(openButton, "cell 0 11 3 1"); + + //---- saveButton ---- + saveButton.setText("Save..."); + saveButton.addActionListener(e -> save()); + add(saveButton, "cell 0 11 3 1"); + + //---- openDirectButton ---- + openDirectButton.setText("Open (no-thread)..."); + openDirectButton.addActionListener(e -> openDirect()); + add(openDirectButton, "cell 0 11 3 1"); + + //---- saveDirectButton ---- + saveDirectButton.setText("Save (no-thread)..."); + saveDirectButton.addActionListener(e -> saveDirect()); + add(saveDirectButton, "cell 0 11 3 1"); + + //---- showMessageDialogOnOKCheckBox ---- + showMessageDialogOnOKCheckBox.setText("show message dialog on OK"); + add(showMessageDialogOnOKCheckBox, "cell 0 11 3 1"); + + //======== filesScrollPane ======== + { + + //---- filesField ---- + filesField.setRows(8); + filesScrollPane.setViewportView(filesField); + } + add(filesScrollPane, "cell 0 12 3 1,growx"); + + //======== menuBar1 ======== + { + + //======== menu1 ======== + { + menu1.setText("text"); + + //---- menuItem1 ---- + menuItem1.setText("text"); + menuItem1.addActionListener(e -> menuItemAction()); + menu1.add(menuItem1); + + //---- menuItem2 ---- + menuItem2.setText("text"); + menuItem2.addActionListener(e -> menuItemAction()); + menu1.add(menuItem2); + } + menuBar1.add(menu1); + + //======== menu2 ======== + { + menu2.setText("text"); + + //---- menuItem3 ---- + menuItem3.setText("text"); + menuItem3.addActionListener(e -> menuItemAction()); + menu2.add(menuItem3); + + //---- menuItem4 ---- + menuItem4.setText("text"); + menuItem4.addActionListener(e -> menuItemAction()); + menu2.add(menuItem4); + } + menuBar1.add(menu2); + } + + //---- ownerButtonGroup ---- + ButtonGroup ownerButtonGroup = new ButtonGroup(); + ownerButtonGroup.add(ownerFrameRadioButton); + ownerButtonGroup.add(ownerDialogRadioButton); + ownerButtonGroup.add(ownerNullRadioButton); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JRadioButton ownerFrameRadioButton; + private JRadioButton ownerDialogRadioButton; + private JRadioButton ownerNullRadioButton; + private JTextField titleField; + private JCheckBox canChooseFilesCheckBox; + private JCheckBox canChooseDirectoriesCheckBox; + private FlatTriStateCheckBox resolvesAliasesCheckBox; + private FlatTriStateCheckBox allowsMultipleSelectionCheckBox; + private JCheckBox accessoryViewDisclosedCheckBox; + private FlatTriStateCheckBox showsTagFieldCheckBox; + private FlatTriStateCheckBox canCreateDirectoriesCheckBox; + private FlatTriStateCheckBox canSelectHiddenExtensionCheckBox; + private FlatTriStateCheckBox showsHiddenFilesCheckBox; + private FlatTriStateCheckBox extensionHiddenCheckBox; + private FlatTriStateCheckBox allowsOtherFileTypesCheckBox; + private FlatTriStateCheckBox treatsFilePackagesAsDirectoriesCheckBox; + private JCheckBox showSingleFilterFieldCheckBox; + private JTextField promptField; + private JTextField messageField; + private JTextField filterFieldLabelField; + private JTextField nameFieldLabelField; + private JTextField nameFieldStringValueField; + private JTextField directoryURLField; + private JComboBox fileTypesField; + private JSlider fileTypeIndexSlider; + private JCheckBox showMessageDialogOnOKCheckBox; + private JTextArea filesField; + private static JMenuBar menuBar1; + // JFormDesigner - End of variables declaration //GEN-END:variables +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.jfd new file mode 100644 index 000000000..33ffd50af --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserMacTest.jfd @@ -0,0 +1,440 @@ +JFDML JFormDesigner: "8.2.2.0.9999" Java: "21.0.1" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + auxiliary() { + "JavaCodeGenerator.defaultVariableLocal": true + } + add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "ltr,insets dialog,hidemode 3" + "$columnConstraints": "[left][grow,fill][fill]" + "$rowConstraints": "[][][][][][][][][][][][][grow,fill]" + } ) { + name: "this" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "ownerLabel" + "text": "owner" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerFrameRadioButton" + "text": "JFrame" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerDialogRadioButton" + "text": "JDialog" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerNullRadioButton" + "text": "null" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) { + name: "ownerSpacer" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0,growx" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "titleLabel" + "text": "title" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "titleField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "insets 2,hidemode 3" + "$columnConstraints": "[left]" + "$rowConstraints": "[][]0[]0[]0[]0[]para[][]0[]0[]0[]0[]0[]0[]para[][]" + } ) { + name: "panel1" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "options1Label" + "text": "NSOpenPanel options:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "canChooseFilesCheckBox" + "text": "canChooseFiles" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "canChooseDirectoriesCheckBox" + "text": "canChooseDirectories" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "resolvesAliasesCheckBox" + "text": "resolvesAliases" + "state": enum com.formdev.flatlaf.extras.components.FlatTriStateCheckBox$State SELECTED + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "allowsMultipleSelectionCheckBox" + "text": "allowsMultipleSelection" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "accessoryViewDisclosedCheckBox" + "text": "accessoryViewDisclosed" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "options2Label" + "text": "NSOpenPanel and NSSavePanel options:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "showsTagFieldCheckBox" + "text": "showsTagField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "canCreateDirectoriesCheckBox" + "text": "canCreateDirectories" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "canSelectHiddenExtensionCheckBox" + "text": "canSelectHiddenExtension" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 9" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "showsHiddenFilesCheckBox" + "text": "showsHiddenFiles" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 10" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "extensionHiddenCheckBox" + "text": "extensionHidden" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "allowsOtherFileTypesCheckBox" + "text": "allowsOtherFileTypes" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 12" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "treatsFilePackagesAsDirectoriesCheckBox" + "text": "treatsFilePackagesAsDirectories" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 13" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "options3Label" + "text": "Custom options:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 14" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showSingleFilterFieldCheckBox" + "text": "showSingleFilterField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 15" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1 1 10,aligny top,growy 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "promptLabel" + "text": "prompt" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "promptField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "messageLabel" + "text": "message" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "messageField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "filterFieldLabelLabel" + "text": "filterFieldLabel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "filterFieldLabelField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "nameFieldLabelLabel" + "text": "nameFieldLabel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "nameFieldLabelField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "nameFieldStringValueLabel" + "text": "nameFieldStringValue" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "nameFieldStringValueField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "directoryURLLabel" + "text": "directoryURL" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "directoryURLField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypesLabel" + "text": "fileTypes" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "fileTypesField" + "editable": true + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "Text Files,txt,null" + addElement( "Text Files,txt,null" ) + addElement( "All Files,*,null" ) + addElement( "Text Files,txt,null,PDF Files,pdf,null,All Files,*,null" ) + addElement( "Text and PDF Files,txt,pdf,null" ) + addElement( "Compressed,zip,gz,null,Disk Images,dmg,null" ) + } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 8" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypeIndexLabel" + "text": "fileTypeIndex" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 9" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "fileTypeIndexSlider" + "maximum": 10 + "majorTickSpacing": 1 + "value": 0 + "paintLabels": true + "snapToTicks": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 9" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "openButton" + "text": "Open..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "open", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "saveButton" + "text": "Save..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "save", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "openDirectButton" + "text": "Open (no-thread)..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openDirect", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "saveDirectButton" + "text": "Save (no-thread)..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "saveDirect", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showMessageDialogOnOKCheckBox" + "text": "show message dialog on OK" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "filesScrollPane" + add( new FormComponent( "javax.swing.JTextArea" ) { + name: "filesField" + "rows": 8 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 12 3 1,growx" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 750, 565 ) + } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "ownerButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 575 ) + } ) + add( new FormContainer( "javax.swing.JMenuBar", new FormLayoutManager( class javax.swing.JMenuBar ) ) { + name: "menuBar1" + auxiliary() { + "JavaCodeGenerator.variableModifiers": 10 + "JavaCodeGenerator.variableLocal": false + } + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "menu1" + "text": "text" + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem1" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem2" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + } ) + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "menu2" + "text": "text" + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem3" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem4" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 630 ) + "size": new java.awt.Dimension( 76, 24 ) + } ) + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java new file mode 100644 index 000000000..adfd8ef2f --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.java @@ -0,0 +1,1024 @@ +/* + * Copyright 2025 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.testing; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dialog; +import java.awt.FileDialog; +import java.awt.Frame; +import java.awt.Point; +import java.awt.Window; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.event.WindowFocusListener; +import java.awt.event.WindowListener; +import java.awt.event.WindowStateListener; +import java.io.File; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.prefs.Preferences; +import java.util.stream.Stream; +import javax.swing.*; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; +import com.formdev.flatlaf.FlatSystemProperties; +import com.formdev.flatlaf.demo.DemoPrefs; +import com.formdev.flatlaf.util.SystemFileChooser; +import com.formdev.flatlaf.util.SystemInfo; +import li.flor.nativejfilechooser.NativeJFileChooser; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatSystemFileChooserTest + extends FlatTestPanel +{ + public static void main( String[] args ) { + // macOS (see https://www.formdev.com/flatlaf/macos/) + if( SystemInfo.isMacOS ) { + // enable screen menu bar + // (moves menu bar from JFrame window to top of screen) + System.setProperty( "apple.laf.useScreenMenuBar", "true" ); + + // appearance of window title bars + // possible values: + // - "system": use current macOS appearance (light or dark) + // - "NSAppearanceNameAqua": use light appearance + // - "NSAppearanceNameDarkAqua": use dark appearance + // (needs to be set on main thread; setting it on AWT thread does not work) + System.setProperty( "apple.awt.application.appearance", "system" ); + } + + SwingUtilities.invokeLater( () -> { + FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserTest" ); + frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); // necessary because of JavaFX + addListeners( frame ); + frame.showFrame( FlatSystemFileChooserTest::new ); + frame.setJMenuBar( menuBar1 ); + } ); + } + + FlatSystemFileChooserTest() { + initComponents(); + + if( !NativeJFileChooser.FX_AVAILABLE ) { + javafxOpenButton.setEnabled( false ); + javafxSaveButton.setEnabled( false ); + } + + Preferences state = DemoPrefs.getState(); + currentDirField.setText( state.get( "systemfilechooser.currentdir", "" ) ); + selectedFileField.setText( state.get( "systemfilechooser.selectedfile", "" ) ); + selectedFilesField.setText( state.get( "systemfilechooser.selectedfiles", "" ) ); + currentDirCheckBox.setSelected( state.getBoolean( "systemfilechooser.currentdir.enabled", false ) ); + selectedFileCheckBox.setSelected( state.getBoolean( "systemfilechooser.selectedfile.enabled", false ) ); + selectedFilesCheckBox.setSelected( state.getBoolean( "systemfilechooser.selectedfiles.enabled", false ) ); + persistStateCheckBox.setSelected( state.getBoolean( "systemfilechooser.persistState.enabled", false ) ); + + currentDirChanged(); + selectedFileChanged(); + selectedFilesChanged(); + persistStateChanged(); + } + + private void persistStateChanged() { + boolean b = persistStateCheckBox.isSelected(); + + SystemFileChooser.setStateStore( b + ? new SystemFileChooser.StateStore() { + private static final String KEY_PREFIX = "fileChooser."; + + @Override + public String get( String key, String def ) { + String value = DemoPrefs.getState().get( KEY_PREFIX + key, def ); + System.out.println( "GET " + key + " = " + value ); + return value; + } + + @Override + public void put( String key, String value ) { + System.out.println( "PUT " + key + " = " + value ); + if( value != null ) + DemoPrefs.getState().put( KEY_PREFIX + key, value ); + else + DemoPrefs.getState().remove( KEY_PREFIX + key ); + } + } : null ); + + DemoPrefs.getState().putBoolean( "systemfilechooser.persistState.enabled", b ); + } + + private void open() { + SystemFileChooser fc = new SystemFileChooser(); + configureSystemFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showOpenDialog( owner ); + outputSystemFileChooser( fc, result ); + } ); + } + + private void save() { + SystemFileChooser fc = new SystemFileChooser(); + configureSystemFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showSaveDialog( owner ); + outputSystemFileChooser( fc, result ); + } ); + } + + private void swingOpen() { + JFileChooser fc = new JFileChooser(); + configureSwingFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showOpenDialog( owner ); + outputSwingFileChooser( "Swing", fc, result ); + } ); + } + + private void swingSave() { + JFileChooser fc = new JFileChooser(); + configureSwingFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showSaveDialog( owner ); + outputSwingFileChooser( "Swing", fc, result ); + } ); + } + + private void awtOpen() { + showWithOwner( owner -> { + FileDialog fc = (owner instanceof Frame) + ? new FileDialog( (Frame) owner ) + : new FileDialog( (Dialog) owner ); + configureAWTFileChooser( fc, true ); + fc.setVisible( true ); + outputAWTFileChooser( fc ); + } ); + } + + private void awtSave() { + showWithOwner( owner -> { + FileDialog fc = (owner instanceof Frame) + ? new FileDialog( (Frame) owner ) + : new FileDialog( (Dialog) owner ); + configureAWTFileChooser( fc, false ); + fc.setVisible( true ); + outputAWTFileChooser( fc ); + } ); + } + + private void javafxOpen() { + JFileChooser fc = new NativeJFileChooser(); + configureSwingFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showOpenDialog( owner ); + outputSwingFileChooser( "JavaFX", fc, result ); + } ); + } + + private void javafxSave() { + JFileChooser fc = new NativeJFileChooser(); + configureSwingFileChooser( fc ); + showWithOwner( owner -> { + int result = fc.showSaveDialog( owner ); + outputSwingFileChooser( "JavaFX", fc, result ); + } ); + } + + private void configureSystemFileChooser( SystemFileChooser fc ) { + fc.setDialogTitle( n( dialogTitleField.getText() ) ); + fc.setApproveButtonText( n( approveButtonTextField.getText() ) ); + fc.setApproveButtonMnemonic( mnemonic( approveButtonMnemonicField.getText() ) ); + + // paths + if( currentDirCheckBox.isSelected() ) + fc.setCurrentDirectory( toFile( currentDirField.getText() ) ); + if( selectedFileCheckBox.isSelected() ) + fc.setSelectedFile( toFile( selectedFileField.getText() ) ); + if( selectedFilesCheckBox.isSelected() ) + fc.setSelectedFiles( toFiles( selectedFilesField.getText() ) ); + + // options + if( directorySelectionCheckBox.isSelected() ) + fc.setFileSelectionMode( SystemFileChooser.DIRECTORIES_ONLY ); + fc.setMultiSelectionEnabled( multiSelectionEnabledCheckBox.isSelected() ); + fc.setFileHidingEnabled( useFileHidingCheckBox.isSelected() ); + if( useSystemFileChooserCheckBox.isSelected() ) + System.clearProperty( FlatSystemProperties.USE_SYSTEM_FILE_CHOOSER ); + else + System.setProperty( FlatSystemProperties.USE_SYSTEM_FILE_CHOOSER, "false" ); + + // filter + String fileTypesStr = n( (String) fileTypesField.getSelectedItem() ); + String[] fileTypes = {}; + if( fileTypesStr != null ) + fileTypes = fileTypesStr.trim().split( "[,]+" ); + int fileTypeIndex = fileTypeIndexSlider.getValue(); + if( !useAcceptAllFileFilterCheckBox.isSelected() ) + fc.setAcceptAllFileFilterUsed( false ); + for( int i = 0; i < fileTypes.length; i += 2 ) { + fc.addChoosableFileFilter( "*".equals( fileTypes[i+1] ) + ? fc.getAcceptAllFileFilter() + : new SystemFileChooser.FileNameExtensionFilter( fileTypes[i], fileTypes[i+1].split( ";" ) ) ); + } + SystemFileChooser.FileFilter[] filters = fc.getChoosableFileFilters(); + if( filters.length > 0 ) + fc.setFileFilter( filters[Math.min( Math.max( fileTypeIndex, 0 ), filters.length - 1 )] ); + +// fc.putPlatformProperty( SystemFileChooser.WINDOWS_FILE_NAME_LABEL, "My filename label:" ); +// fc.putPlatformProperty( SystemFileChooser.WINDOWS_OPTIONS_SET, FlatNativeWindowsLibrary.FOS_HIDEMRUPLACES ); + +// fc.putPlatformProperty( SystemFileChooser.MAC_MESSAGE, "some message" ); +// fc.putPlatformProperty( SystemFileChooser.MAC_NAME_FIELD_LABEL, "My name label:" ); +// fc.putPlatformProperty( SystemFileChooser.MAC_FILTER_FIELD_LABEL, "My filter label" ); +// fc.putPlatformProperty( SystemFileChooser.MAC_TREATS_FILE_PACKAGES_AS_DIRECTORIES, true ); +// fc.putPlatformProperty( SystemFileChooser.MAC_OPTIONS_CLEAR, FlatNativeMacLibrary.FC_showsTagField ); + +// fc.putPlatformProperty( SystemFileChooser.LINUX_OPTIONS_CLEAR, FlatNativeLinuxLibrary.FC_create_folders | FlatNativeLinuxLibrary.FC_do_overwrite_confirmation ); + + String id = (String) stateStoreIDField.getSelectedItem(); + fc.setStateStoreID( id != null && !id.isEmpty() ? id : null ); + } + + private void configureSwingFileChooser( JFileChooser fc ) { + fc.setDialogTitle( n( dialogTitleField.getText() ) ); + fc.setApproveButtonText( n( approveButtonTextField.getText() ) ); + fc.setApproveButtonMnemonic( mnemonic( approveButtonMnemonicField.getText() ) ); + + // paths + if( currentDirCheckBox.isSelected() ) + fc.setCurrentDirectory( toFile( currentDirField.getText() ) ); + if( selectedFileCheckBox.isSelected() ) + fc.setSelectedFile( toFile( selectedFileField.getText() ) ); + if( selectedFilesCheckBox.isSelected() ) + fc.setSelectedFiles( toFiles( selectedFilesField.getText() ) ); + + // options + if( directorySelectionCheckBox.isSelected() ) + fc.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY ); + fc.setMultiSelectionEnabled( multiSelectionEnabledCheckBox.isSelected() ); + fc.setFileHidingEnabled( useFileHidingCheckBox.isSelected() ); + + // filter + String fileTypesStr = n( (String) fileTypesField.getSelectedItem() ); + String[] fileTypes = {}; + if( fileTypesStr != null ) + fileTypes = fileTypesStr.trim().split( "[,]+" ); + int fileTypeIndex = fileTypeIndexSlider.getValue(); + if( !useAcceptAllFileFilterCheckBox.isSelected() ) + fc.setAcceptAllFileFilterUsed( false ); + for( int i = 0; i < fileTypes.length; i += 2 ) { + fc.addChoosableFileFilter( "*".equals( fileTypes[i+1] ) + ? fc.getAcceptAllFileFilter() + : new FileNameExtensionFilter( fileTypes[i], fileTypes[i+1].split( ";" ) ) ); + } + FileFilter[] filters = fc.getChoosableFileFilters(); + if( filters.length > 0 ) + fc.setFileFilter( filters[Math.min( Math.max( fileTypeIndex, 0 ), filters.length - 1 )] ); + } + + private void configureAWTFileChooser( FileDialog fc, boolean open ) { + fc.setMode( open ? FileDialog.LOAD : FileDialog.SAVE ); + fc.setTitle( n( dialogTitleField.getText() ) ); + + // paths + if( currentDirCheckBox.isSelected() ) + fc.setDirectory( n( currentDirField.getText() ) ); + + // options + fc.setMultipleMode( multiSelectionEnabledCheckBox.isSelected() ); + } + + private void outputSystemFileChooser( SystemFileChooser fc, int result ) { + output( "System", fc.getDialogType() == SystemFileChooser.OPEN_DIALOG, + fc.isDirectorySelectionEnabled(), fc.isMultiSelectionEnabled(), + "result", result, + "currentDirectory", fc.getCurrentDirectory(), + "selectedFile", fc.getSelectedFile(), + "selectedFiles", fc.getSelectedFiles() ); + } + + private void outputSwingFileChooser( String type, JFileChooser fc, int result ) { + output( type, fc.getDialogType() == JFileChooser.OPEN_DIALOG, + fc.isDirectorySelectionEnabled(), fc.isMultiSelectionEnabled(), + "result", result, + "currentDirectory", fc.getCurrentDirectory(), + "selectedFile", fc.getSelectedFile(), + "selectedFiles", fc.getSelectedFiles() ); + } + + private void outputAWTFileChooser( FileDialog fc ) { + output( "AWT", fc.getMode() == FileDialog.LOAD, false, fc.isMultipleMode(), + "files", fc.getFiles(), + "directory", fc.getDirectory(), + "file", fc.getFile() ); + } + + private void output( String type, boolean open, boolean directorySelection, + boolean multiSelection, Object... values ) + { + outputField.append( "---- " + type + " " + (open ? "Open " : "Save ") + + (directorySelection ? " directory-sel " : "") + + (multiSelection ? " multi-sel " : "") + + "----\n" ); + + for( int i = 0; i < values.length; i += 2 ) { + outputField.append( values[i] + " = " ); + Object value = values[i+1]; + if( value instanceof File[] ) + outputField.append( Arrays.toString( (File[]) value ).replace( ",", "\n " ) ); + else + outputField.append( String.valueOf( value ) ); + outputField.append( "\n" ); + } + outputField.append( "\n" ); + outputField.setCaretPosition( outputField.getDocument().getLength() ); + } + + private static String n( String s ) { + return !s.isEmpty() ? s : null; + } + + private static char mnemonic( String s ) { + return !s.isEmpty() ? s.charAt( 0 ) : 0; + } + + private void showWithOwner( Consumer showConsumer ) { + Window frame = SwingUtilities.windowForComponent( this ); + if( ownerFrameRadioButton.isSelected() ) + showConsumer.accept( frame ); + else if( ownerDialogRadioButton.isSelected() ) + new DummyModalDialog( frame, showConsumer ).setVisible( true ); + else + showConsumer.accept( null ); + } + + private void currentDirChanged() { + boolean b = currentDirCheckBox.isSelected(); + currentDirField.setEditable( b ); + currentDirChooseButton.setEnabled( b ); + + DemoPrefs.getState().putBoolean( "systemfilechooser.currentdir.enabled", b ); + } + + private void selectedFileChanged() { + boolean b = selectedFileCheckBox.isSelected(); + selectedFileField.setEditable( b ); + selectedFileChooseButton.setEnabled( b ); + + DemoPrefs.getState().putBoolean( "systemfilechooser.selectedfile.enabled", b ); + } + + private void selectedFilesChanged() { + boolean b = selectedFilesCheckBox.isSelected(); + selectedFilesField.setEditable( b ); + selectedFilesChooseButton.setEnabled( b ); + + DemoPrefs.getState().putBoolean( "systemfilechooser.selectedfiles.enabled", b ); + } + + private void chooseCurrentDir() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle( "Current Directory" ); + chooser.setSelectedFile( toFile( currentDirField.getText() ) ); + chooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY ); + if( chooser.showOpenDialog( this ) == JFileChooser.APPROVE_OPTION ) { + currentDirField.setText( toString( chooser.getSelectedFile() ) ); + putState( "systemfilechooser.currentdir", currentDirField.getText() ); + } + } + + private void chooseSelectedFile() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle( "Selected File" ); + chooser.setSelectedFile( toFile( selectedFileField.getText() ) ); + chooser.setFileSelectionMode( JFileChooser.FILES_ONLY ); + if( chooser.showOpenDialog( this ) == JFileChooser.APPROVE_OPTION ) { + selectedFileField.setText( toString( chooser.getSelectedFile() ) ); + putState( "systemfilechooser.selectedfile", selectedFileField.getText() ); + } + } + + private void chooseSelectedFiles() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle( "Selected Files" ); + chooser.setSelectedFiles( toFiles( selectedFilesField.getText() ) ); + chooser.setFileSelectionMode( JFileChooser.FILES_ONLY ); + chooser.setMultiSelectionEnabled( true ); + if( chooser.showOpenDialog( this ) == JFileChooser.APPROVE_OPTION ) { + selectedFilesField.setText( toString( chooser.getSelectedFiles() ) ); + putState( "systemfilechooser.selectedfiles", selectedFilesField.getText() ); + } + } + + private static File toFile( String s ) { + return !s.isEmpty() ? new File( s ) : null; + } + + private static String toString( File file ) { + return (file != null) ? file.getAbsolutePath() : null; + } + + private static File[] toFiles( String s ) { + return !s.isEmpty() + ? Stream.of( s.split( "," ) ).map( name -> new File( name ) ).toArray( File[]::new ) + : new File[0]; + } + + private static String toString( File[] files ) { + return (files != null && files.length > 0) + ? String.join( ",", Stream.of( files ).map( file -> file.getAbsolutePath() ).toArray( String[]::new ) ) + : ""; + } + + private static void putState( String key, String value ) { + if( value.isEmpty() ) + DemoPrefs.getState().remove( key ); + else + DemoPrefs.getState().put( key, value ); + } + + private void menuItemAction() { + System.out.println( "menu item action" ); + } + + static void addListeners( Window w ) { + w.addWindowListener( new WindowListener() { + @Override + public void windowOpened( WindowEvent e ) { + printWindowEvent( e ); + } + + @Override + public void windowIconified( WindowEvent e ) { + printWindowEvent( e ); + } + + @Override + public void windowDeiconified( WindowEvent e ) { + printWindowEvent( e ); + } + + @Override + public void windowDeactivated( WindowEvent e ) { + printWindowEvent( e ); + } + + @Override + public void windowClosing( WindowEvent e ) { + printWindowEvent( e ); + } + + @Override + public void windowClosed( WindowEvent e ) { + printWindowEvent( e ); + } + + @Override + public void windowActivated( WindowEvent e ) { + printWindowEvent( e ); + } + } ); + w.addWindowStateListener( new WindowStateListener() { + @Override + public void windowStateChanged( WindowEvent e ) { + printWindowEvent( e ); + } + } ); + w.addWindowFocusListener( new WindowFocusListener() { + @Override + public void windowLostFocus( WindowEvent e ) { + printWindowEvent( e ); + } + + @Override + public void windowGainedFocus( WindowEvent e ) { + printWindowEvent( e ); + } + } ); + } + + static void printWindowEvent( WindowEvent e ) { + String typeStr; + switch( e.getID() ) { + case WindowEvent.WINDOW_OPENED: typeStr = "WINDOW_OPENED "; break; + case WindowEvent.WINDOW_CLOSING: typeStr = "WINDOW_CLOSING "; break; + case WindowEvent.WINDOW_CLOSED: typeStr = "WINDOW_CLOSED "; break; + case WindowEvent.WINDOW_ICONIFIED: typeStr = "WINDOW_ICONIFIED "; break; + case WindowEvent.WINDOW_DEICONIFIED: typeStr = "WINDOW_DEICONIFIED "; break; + case WindowEvent.WINDOW_ACTIVATED: typeStr = "WINDOW_ACTIVATED "; break; + case WindowEvent.WINDOW_DEACTIVATED: typeStr = "WINDOW_DEACTIVATED "; break; + case WindowEvent.WINDOW_GAINED_FOCUS: typeStr = "WINDOW_GAINED_FOCUS "; break; + case WindowEvent.WINDOW_LOST_FOCUS: typeStr = "WINDOW_LOST_FOCUS "; break; + case WindowEvent.WINDOW_STATE_CHANGED: typeStr = "WINDOW_STATE_CHANGED"; break; + default: typeStr = "unknown type "; break; + } + Object source = e.getSource(); + Window opposite = e.getOppositeWindow(); + String sourceStr = (source instanceof Component) ? ((Component)source).getName() : String.valueOf( source ); + String oppositeStr = (opposite != null) ? opposite.getName() : null; + System.out.println( typeStr + " source " + sourceStr + " opposite " + oppositeStr ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + JLabel ownerLabel = new JLabel(); + ownerFrameRadioButton = new JRadioButton(); + ownerDialogRadioButton = new JRadioButton(); + ownerNullRadioButton = new JRadioButton(); + JPanel ownerSpacer = new JPanel(null); + JLabel dialogTitleLabel = new JLabel(); + dialogTitleField = new JTextField(); + JPanel panel1 = new JPanel(); + directorySelectionCheckBox = new JCheckBox(); + multiSelectionEnabledCheckBox = new JCheckBox(); + useFileHidingCheckBox = new JCheckBox(); + useSystemFileChooserCheckBox = new JCheckBox(); + persistStateCheckBox = new JCheckBox(); + JLabel stateStoreIDLabel = new JLabel(); + stateStoreIDField = new JComboBox<>(); + JLabel approveButtonTextLabel = new JLabel(); + approveButtonTextField = new JTextField(); + JLabel approveButtonMnemonicLabel = new JLabel(); + approveButtonMnemonicField = new JTextField(); + currentDirCheckBox = new JCheckBox(); + currentDirField = new JTextField(); + currentDirChooseButton = new JButton(); + selectedFileCheckBox = new JCheckBox(); + selectedFileField = new JTextField(); + selectedFileChooseButton = new JButton(); + selectedFilesCheckBox = new JCheckBox(); + selectedFilesField = new JTextField(); + selectedFilesChooseButton = new JButton(); + JLabel fileTypesLabel = new JLabel(); + fileTypesField = new JComboBox<>(); + JLabel fileTypeIndexLabel = new JLabel(); + fileTypeIndexSlider = new JSlider(); + useAcceptAllFileFilterCheckBox = new JCheckBox(); + JButton openButton = new JButton(); + JButton saveButton = new JButton(); + JButton swingOpenButton = new JButton(); + JButton swingSaveButton = new JButton(); + JButton awtOpenButton = new JButton(); + JButton awtSaveButton = new JButton(); + javafxOpenButton = new JButton(); + javafxSaveButton = new JButton(); + JScrollPane outputScrollPane = new JScrollPane(); + outputField = new JTextArea(); + menuBar1 = new JMenuBar(); + JMenu menu1 = new JMenu(); + JMenuItem menuItem1 = new JMenuItem(); + JMenuItem menuItem2 = new JMenuItem(); + JMenu menu2 = new JMenu(); + JMenuItem menuItem3 = new JMenuItem(); + JMenuItem menuItem4 = new JMenuItem(); + + //======== this ======== + setLayout(new MigLayout( + "ltr,insets dialog,hidemode 3", + // columns + "[left]" + + "[grow,fill]" + + "[fill]", + // rows + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[grow,fill]")); + + //---- ownerLabel ---- + ownerLabel.setText("owner"); + add(ownerLabel, "cell 0 0"); + + //---- ownerFrameRadioButton ---- + ownerFrameRadioButton.setText("JFrame"); + ownerFrameRadioButton.setSelected(true); + add(ownerFrameRadioButton, "cell 1 0"); + + //---- ownerDialogRadioButton ---- + ownerDialogRadioButton.setText("JDialog"); + add(ownerDialogRadioButton, "cell 1 0"); + + //---- ownerNullRadioButton ---- + ownerNullRadioButton.setText("null"); + add(ownerNullRadioButton, "cell 1 0"); + add(ownerSpacer, "cell 1 0,growx"); + + //---- dialogTitleLabel ---- + dialogTitleLabel.setText("dialogTitle"); + add(dialogTitleLabel, "cell 0 1"); + add(dialogTitleField, "cell 1 1"); + + //======== panel1 ======== + { + panel1.setLayout(new MigLayout( + "insets 2,hidemode 3", + // columns + "[left]", + // rows + "[]0" + + "[]0" + + "[]" + + "[]para" + + "[]" + + "[]")); + + //---- directorySelectionCheckBox ---- + directorySelectionCheckBox.setText("directorySelection"); + panel1.add(directorySelectionCheckBox, "cell 0 0"); + + //---- multiSelectionEnabledCheckBox ---- + multiSelectionEnabledCheckBox.setText("multiSelectionEnabled"); + panel1.add(multiSelectionEnabledCheckBox, "cell 0 1"); + + //---- useFileHidingCheckBox ---- + useFileHidingCheckBox.setText("useFileHiding"); + useFileHidingCheckBox.setSelected(true); + panel1.add(useFileHidingCheckBox, "cell 0 2"); + + //---- useSystemFileChooserCheckBox ---- + useSystemFileChooserCheckBox.setText("use SystemFileChooser"); + useSystemFileChooserCheckBox.setSelected(true); + panel1.add(useSystemFileChooserCheckBox, "cell 0 3"); + + //---- persistStateCheckBox ---- + persistStateCheckBox.setText("persist state"); + persistStateCheckBox.addActionListener(e -> persistStateChanged()); + panel1.add(persistStateCheckBox, "cell 0 4"); + + //---- stateStoreIDLabel ---- + stateStoreIDLabel.setText("ID:"); + panel1.add(stateStoreIDLabel, "cell 0 5"); + + //---- stateStoreIDField ---- + stateStoreIDField.setModel(new DefaultComboBoxModel<>(new String[] { + "abc", + "def" + })); + stateStoreIDField.setEditable(true); + stateStoreIDField.setSelectedIndex(-1); + panel1.add(stateStoreIDField, "cell 0 5,growx"); + } + add(panel1, "cell 2 1 1 7,aligny top,growy 0"); + + //---- approveButtonTextLabel ---- + approveButtonTextLabel.setText("approveButtonText"); + add(approveButtonTextLabel, "cell 0 2"); + add(approveButtonTextField, "cell 1 2,growx"); + + //---- approveButtonMnemonicLabel ---- + approveButtonMnemonicLabel.setText("approveButtonMnemonic"); + add(approveButtonMnemonicLabel, "cell 1 2"); + + //---- approveButtonMnemonicField ---- + approveButtonMnemonicField.setColumns(3); + add(approveButtonMnemonicField, "cell 1 2"); + + //---- currentDirCheckBox ---- + currentDirCheckBox.setText("current directory"); + currentDirCheckBox.addActionListener(e -> currentDirChanged()); + add(currentDirCheckBox, "cell 0 3"); + add(currentDirField, "cell 1 3,growx"); + + //---- currentDirChooseButton ---- + currentDirChooseButton.setText("..."); + currentDirChooseButton.addActionListener(e -> chooseCurrentDir()); + add(currentDirChooseButton, "cell 1 3"); + + //---- selectedFileCheckBox ---- + selectedFileCheckBox.setText("selected file"); + selectedFileCheckBox.addActionListener(e -> selectedFileChanged()); + add(selectedFileCheckBox, "cell 0 4"); + add(selectedFileField, "cell 1 4,growx"); + + //---- selectedFileChooseButton ---- + selectedFileChooseButton.setText("..."); + selectedFileChooseButton.addActionListener(e -> chooseSelectedFile()); + add(selectedFileChooseButton, "cell 1 4"); + + //---- selectedFilesCheckBox ---- + selectedFilesCheckBox.setText("selected files"); + selectedFilesCheckBox.addActionListener(e -> selectedFilesChanged()); + add(selectedFilesCheckBox, "cell 0 5"); + add(selectedFilesField, "cell 1 5,growx"); + + //---- selectedFilesChooseButton ---- + selectedFilesChooseButton.setText("..."); + selectedFilesChooseButton.addActionListener(e -> chooseSelectedFiles()); + add(selectedFilesChooseButton, "cell 1 5"); + + //---- fileTypesLabel ---- + fileTypesLabel.setText("fileTypes"); + add(fileTypesLabel, "cell 0 6"); + + //---- fileTypesField ---- + fileTypesField.setEditable(true); + fileTypesField.setModel(new DefaultComboBoxModel<>(new String[] { + "Text Files,txt", + "All Files,*", + "Text Files,txt,PDF Files,pdf,All Files,*", + "Text and PDF Files,txt;pdf" + })); + add(fileTypesField, "cell 1 6"); + + //---- fileTypeIndexLabel ---- + fileTypeIndexLabel.setText("fileTypeIndex"); + add(fileTypeIndexLabel, "cell 0 7"); + + //---- fileTypeIndexSlider ---- + fileTypeIndexSlider.setMaximum(10); + fileTypeIndexSlider.setMajorTickSpacing(1); + fileTypeIndexSlider.setValue(0); + fileTypeIndexSlider.setPaintLabels(true); + fileTypeIndexSlider.setSnapToTicks(true); + add(fileTypeIndexSlider, "cell 1 7,growx"); + + //---- useAcceptAllFileFilterCheckBox ---- + useAcceptAllFileFilterCheckBox.setText("useAcceptAllFileFilter"); + useAcceptAllFileFilterCheckBox.setSelected(true); + add(useAcceptAllFileFilterCheckBox, "cell 1 7"); + + //---- openButton ---- + openButton.setText("Open..."); + openButton.addActionListener(e -> open()); + add(openButton, "cell 0 8 3 1"); + + //---- saveButton ---- + saveButton.setText("Save..."); + saveButton.addActionListener(e -> save()); + add(saveButton, "cell 0 8 3 1"); + + //---- swingOpenButton ---- + swingOpenButton.setText("Swing Open..."); + swingOpenButton.addActionListener(e -> swingOpen()); + add(swingOpenButton, "cell 0 8 3 1"); + + //---- swingSaveButton ---- + swingSaveButton.setText("Swing Save..."); + swingSaveButton.addActionListener(e -> swingSave()); + add(swingSaveButton, "cell 0 8 3 1"); + + //---- awtOpenButton ---- + awtOpenButton.setText("AWT Open..."); + awtOpenButton.addActionListener(e -> awtOpen()); + add(awtOpenButton, "cell 0 8 3 1"); + + //---- awtSaveButton ---- + awtSaveButton.setText("AWT Save..."); + awtSaveButton.addActionListener(e -> awtSave()); + add(awtSaveButton, "cell 0 8 3 1"); + + //---- javafxOpenButton ---- + javafxOpenButton.setText("JavaFX Open..."); + javafxOpenButton.addActionListener(e -> javafxOpen()); + add(javafxOpenButton, "cell 0 8 3 1"); + + //---- javafxSaveButton ---- + javafxSaveButton.setText("JavaFX Save..."); + javafxSaveButton.addActionListener(e -> javafxSave()); + add(javafxSaveButton, "cell 0 8 3 1"); + + //======== outputScrollPane ======== + { + + //---- outputField ---- + outputField.setRows(20); + outputScrollPane.setViewportView(outputField); + } + add(outputScrollPane, "cell 0 9 3 1,growx"); + + //======== menuBar1 ======== + { + + //======== menu1 ======== + { + menu1.setText("text"); + + //---- menuItem1 ---- + menuItem1.setText("text"); + menuItem1.addActionListener(e -> menuItemAction()); + menu1.add(menuItem1); + + //---- menuItem2 ---- + menuItem2.setText("text"); + menuItem2.addActionListener(e -> menuItemAction()); + menu1.add(menuItem2); + } + menuBar1.add(menu1); + + //======== menu2 ======== + { + menu2.setText("text"); + + //---- menuItem3 ---- + menuItem3.setText("text"); + menuItem3.addActionListener(e -> menuItemAction()); + menu2.add(menuItem3); + + //---- menuItem4 ---- + menuItem4.setText("text"); + menuItem4.addActionListener(e -> menuItemAction()); + menu2.add(menuItem4); + } + menuBar1.add(menu2); + } + + //---- ownerButtonGroup ---- + ButtonGroup ownerButtonGroup = new ButtonGroup(); + ownerButtonGroup.add(ownerFrameRadioButton); + ownerButtonGroup.add(ownerDialogRadioButton); + ownerButtonGroup.add(ownerNullRadioButton); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JRadioButton ownerFrameRadioButton; + private JRadioButton ownerDialogRadioButton; + private JRadioButton ownerNullRadioButton; + private JTextField dialogTitleField; + private JCheckBox directorySelectionCheckBox; + private JCheckBox multiSelectionEnabledCheckBox; + private JCheckBox useFileHidingCheckBox; + private JCheckBox useSystemFileChooserCheckBox; + private JCheckBox persistStateCheckBox; + private JComboBox stateStoreIDField; + private JTextField approveButtonTextField; + private JTextField approveButtonMnemonicField; + private JCheckBox currentDirCheckBox; + private JTextField currentDirField; + private JButton currentDirChooseButton; + private JCheckBox selectedFileCheckBox; + private JTextField selectedFileField; + private JButton selectedFileChooseButton; + private JCheckBox selectedFilesCheckBox; + private JTextField selectedFilesField; + private JButton selectedFilesChooseButton; + private JComboBox fileTypesField; + private JSlider fileTypeIndexSlider; + private JCheckBox useAcceptAllFileFilterCheckBox; + private JButton javafxOpenButton; + private JButton javafxSaveButton; + private JTextArea outputField; + private static JMenuBar menuBar1; + // JFormDesigner - End of variables declaration //GEN-END:variables + + //---- class DummyModalDialog --------------------------------------------- + + static class DummyModalDialog + extends JDialog + { + private final Consumer showConsumer; + + DummyModalDialog( Window owner, Consumer showConsumer ) { + super( owner ); + this.showConsumer = showConsumer; + initComponents(); + addListeners( this ); + ((JComponent)getContentPane()).registerKeyboardAction( + e -> dispose(), + KeyStroke.getKeyStroke( "ESCAPE" ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + + if( owner != null ) { + Point pt = owner.getLocationOnScreen(); + setLocation( pt.x + (getWidth() / 2), pt.y + 40 ); + } else + setLocationRelativeTo( null ); + } + + private void modalityTypeChanged() { + if( applicationRadioButton.isSelected() ) + setModalityType( ModalityType.APPLICATION_MODAL ); + else if( documentRadioButton.isSelected() ) + setModalityType( ModalityType.DOCUMENT_MODAL ); + else if( toolkitRadioButton.isSelected() ) + setModalityType( ModalityType.TOOLKIT_MODAL ); + else + setModalityType( ModalityType.MODELESS ); + + setVisible( false ); + setVisible( true ); + } + + private void showModalDialog() { + new DummyModalDialog( this, showConsumer ).setVisible( true ); + } + + private void showFileDialog() { + showConsumer.accept( this ); + } + + private void windowOpened() { + showConsumer.accept( this ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off + JLabel label1 = new JLabel(); + applicationRadioButton = new JRadioButton(); + documentRadioButton = new JRadioButton(); + toolkitRadioButton = new JRadioButton(); + modelessRadioButton = new JRadioButton(); + JButton showModalDialogButton = new JButton(); + JButton showFileDialogButton = new JButton(); + + //======== this ======== + setTitle("Dummy Modal Dialog"); + setModalityType(Dialog.ModalityType.APPLICATION_MODAL); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + addWindowListener(new WindowAdapter() { + @Override + public void windowOpened(WindowEvent e) { + DummyModalDialog.this.windowOpened(); + } + }); + Container contentPane = getContentPane(); + contentPane.setLayout(new MigLayout( + "hidemode 3", + // columns + "[fill]" + + "[fill]", + // rows + "[]0" + + "[]0" + + "[]0" + + "[]" + + "[]para" + + "[]" + + "[198]")); + + //---- label1 ---- + label1.setText("Modality type:"); + contentPane.add(label1, "cell 0 0"); + + //---- applicationRadioButton ---- + applicationRadioButton.setText("Application"); + applicationRadioButton.setSelected(true); + applicationRadioButton.addActionListener(e -> modalityTypeChanged()); + contentPane.add(applicationRadioButton, "cell 1 0"); + + //---- documentRadioButton ---- + documentRadioButton.setText("Document"); + documentRadioButton.addActionListener(e -> modalityTypeChanged()); + contentPane.add(documentRadioButton, "cell 1 1"); + + //---- toolkitRadioButton ---- + toolkitRadioButton.setText("Toolkit"); + toolkitRadioButton.addActionListener(e -> modalityTypeChanged()); + contentPane.add(toolkitRadioButton, "cell 1 2"); + + //---- modelessRadioButton ---- + modelessRadioButton.setText("modeless"); + modelessRadioButton.addActionListener(e -> modalityTypeChanged()); + contentPane.add(modelessRadioButton, "cell 1 3"); + + //---- showModalDialogButton ---- + showModalDialogButton.setText("Show Modal Dialog..."); + showModalDialogButton.addActionListener(e -> showModalDialog()); + contentPane.add(showModalDialogButton, "cell 0 4 2 1"); + + //---- showFileDialogButton ---- + showFileDialogButton.setText("Show File Dialog..."); + showFileDialogButton.addActionListener(e -> showFileDialog()); + contentPane.add(showFileDialogButton, "cell 0 5 2 1"); + pack(); + setLocationRelativeTo(getOwner()); + + //---- modalityTypeButtonGroup ---- + ButtonGroup modalityTypeButtonGroup = new ButtonGroup(); + modalityTypeButtonGroup.add(applicationRadioButton); + modalityTypeButtonGroup.add(documentRadioButton); + modalityTypeButtonGroup.add(toolkitRadioButton); + modalityTypeButtonGroup.add(modelessRadioButton); + // JFormDesigner - End of component initialization //GEN-END:initComponents @formatter:on + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables @formatter:off + private JRadioButton applicationRadioButton; + private JRadioButton documentRadioButton; + private JRadioButton toolkitRadioButton; + private JRadioButton modelessRadioButton; + // JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd new file mode 100644 index 000000000..9a37ee61c --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserTest.jfd @@ -0,0 +1,521 @@ +JFDML JFormDesigner: "8.3" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + auxiliary() { + "JavaCodeGenerator.defaultVariableLocal": true + } + add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "ltr,insets dialog,hidemode 3" + "$columnConstraints": "[left][grow,fill][fill]" + "$rowConstraints": "[][][][][][][][][][grow,fill]" + } ) { + name: "this" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "ownerLabel" + "text": "owner" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerFrameRadioButton" + "text": "JFrame" + "selected": true + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerDialogRadioButton" + "text": "JDialog" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerNullRadioButton" + "text": "null" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) { + name: "ownerSpacer" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0,growx" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "dialogTitleLabel" + "text": "dialogTitle" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "dialogTitleField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "insets 2,hidemode 3" + "$columnConstraints": "[left]" + "$rowConstraints": "[]0[]0[][]para[][]" + } ) { + name: "panel1" + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "directorySelectionCheckBox" + "text": "directorySelection" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "multiSelectionEnabledCheckBox" + "text": "multiSelectionEnabled" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "useFileHidingCheckBox" + "text": "useFileHiding" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "useSystemFileChooserCheckBox" + "text": "use SystemFileChooser" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "persistStateCheckBox" + "text": "persist state" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "persistStateChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "stateStoreIDLabel" + "text": "ID:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "stateStoreIDField" + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "abc" + addElement( "abc" ) + addElement( "def" ) + } + "editable": true + "selectedIndex": -1 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5,growx" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1 1 7,aligny top,growy 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "approveButtonTextLabel" + "text": "approveButtonText" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "approveButtonTextField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2,growx" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "approveButtonMnemonicLabel" + "text": "approveButtonMnemonic" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "approveButtonMnemonicField" + "columns": 3 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "currentDirCheckBox" + "text": "current directory" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "currentDirChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "currentDirField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3,growx" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "currentDirChooseButton" + "text": "..." + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "chooseCurrentDir", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "selectedFileCheckBox" + "text": "selected file" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "selectedFileChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "selectedFileField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4,growx" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "selectedFileChooseButton" + "text": "..." + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "chooseSelectedFile", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "selectedFilesCheckBox" + "text": "selected files" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "selectedFilesChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "selectedFilesField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5,growx" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "selectedFilesChooseButton" + "text": "..." + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "chooseSelectedFiles", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypesLabel" + "text": "fileTypes" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "fileTypesField" + "editable": true + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "Text Files,txt" + addElement( "Text Files,txt" ) + addElement( "All Files,*" ) + addElement( "Text Files,txt,PDF Files,pdf,All Files,*" ) + addElement( "Text and PDF Files,txt;pdf" ) + } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypeIndexLabel" + "text": "fileTypeIndex" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "fileTypeIndexSlider" + "maximum": 10 + "majorTickSpacing": 1 + "value": 0 + "paintLabels": true + "snapToTicks": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7,growx" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "useAcceptAllFileFilterCheckBox" + "text": "useAcceptAllFileFilter" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "openButton" + "text": "Open..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "open", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "saveButton" + "text": "Save..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "save", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "swingOpenButton" + "text": "Swing Open..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "swingOpen", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "swingSaveButton" + "text": "Swing Save..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "swingSave", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "awtOpenButton" + "text": "AWT Open..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "awtOpen", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "awtSaveButton" + "text": "AWT Save..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "awtSave", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "javafxOpenButton" + "text": "JavaFX Open..." + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "javafxOpen", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "javafxSaveButton" + "text": "JavaFX Save..." + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "javafxSave", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "outputScrollPane" + add( new FormComponent( "javax.swing.JTextArea" ) { + name: "outputField" + "rows": 20 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 9 3 1,growx" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 825, 465 ) + } ) + add( new FormContainer( "javax.swing.JMenuBar", new FormLayoutManager( class javax.swing.JMenuBar ) ) { + name: "menuBar1" + auxiliary() { + "JavaCodeGenerator.variableModifiers": 10 + "JavaCodeGenerator.variableLocal": false + } + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "menu1" + "text": "text" + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem1" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem2" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + } ) + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "menu2" + "text": "text" + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem3" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "menuItem4" + "text": "text" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemAction", false ) ) + } ) + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 10, 570 ) + } ) + add( new FormWindow( "javax.swing.JDialog", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "hidemode 3" + "$columnConstraints": "[fill][fill]" + "$rowConstraints": "[]0[]0[]0[][]para[][198]" + } ) { + name: "dialog1" + "title": "Dummy Modal Dialog" + "modalityType": enum java.awt.Dialog$ModalityType APPLICATION_MODAL + "defaultCloseOperation": 2 + auxiliary() { + "JavaCodeGenerator.className": "DummyModalDialog" + } + addEvent( new FormEvent( "java.awt.event.WindowListener", "windowOpened", "windowOpened", false ) ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label1" + "text": "Modality type:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "applicationRadioButton" + "text": "Application" + "selected": true + "$buttonGroup": new FormReference( "modalityTypeButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "modalityTypeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "documentRadioButton" + "text": "Document" + "$buttonGroup": new FormReference( "modalityTypeButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "modalityTypeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "toolkitRadioButton" + "text": "Toolkit" + "$buttonGroup": new FormReference( "modalityTypeButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "modalityTypeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "modelessRadioButton" + "text": "modeless" + "$buttonGroup": new FormReference( "modalityTypeButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "modalityTypeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "showModalDialogButton" + "text": "Show Modal Dialog..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showModalDialog", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4 2 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "showFileDialogButton" + "text": "Show File Dialog..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showFileDialog", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5 2 1" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 20, 635 ) + "size": new java.awt.Dimension( 290, 465 ) + } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "ownerButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 475 ) + } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "modalityTypeButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 115, 575 ) + } ) + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java new file mode 100644 index 000000000..b6ddbc4ec --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.java @@ -0,0 +1,622 @@ +/* + * Copyright 2024 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.testing; + +import static com.formdev.flatlaf.ui.FlatNativeWindowsLibrary.*; +import java.awt.EventQueue; +import java.awt.Font; +import java.awt.SecondaryLoop; +import java.awt.Toolkit; +import java.awt.Window; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.prefs.Preferences; +import javax.swing.*; +import javax.swing.border.TitledBorder; +import com.formdev.flatlaf.demo.DemoPrefs; +import com.formdev.flatlaf.extras.components.*; +import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox.State; +import com.formdev.flatlaf.testing.FlatSystemFileChooserTest.DummyModalDialog; +import com.formdev.flatlaf.ui.FlatNativeWindowsLibrary; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatSystemFileChooserWindowsTest + extends FlatTestPanel +{ + public static void main( String[] args ) { + SwingUtilities.invokeLater( () -> { + if( !FlatNativeWindowsLibrary.isLoaded() ) { + JOptionPane.showMessageDialog( null, "FlatLaf native library not loaded" ); + return; + } + + FlatTestFrame frame = FlatTestFrame.create( args, "FlatSystemFileChooserWindowsTest" ); + FlatSystemFileChooserTest.addListeners( frame ); + frame.showFrame( FlatSystemFileChooserWindowsTest::new ); + } ); + } + + FlatSystemFileChooserWindowsTest() { + initComponents(); + + fileTypesField.setSelectedItem( null ); + + Preferences state = DemoPrefs.getState(); + messageField.setText( state.get( "systemfilechooser.windows.message", "some message" ) ); + buttonsField.setText( state.get( "systemfilechooser.windows.buttons", "OK" ) ); + } + + private void open() { + openOrSave( true, false ); + } + + private void save() { + openOrSave( false, false ); + } + + private void openDirect() { + openOrSave( true, true ); + } + + private void saveDirect() { + openOrSave( false, true ); + } + + private void openOrSave( boolean open, boolean direct ) { + Window frame = SwingUtilities.windowForComponent( this ); + if( ownerFrameRadioButton.isSelected() ) + openOrSave( open, direct, frame ); + else if( ownerDialogRadioButton.isSelected() ) + new DummyModalDialog( frame, owner -> openOrSave( open, direct, owner ) ).setVisible( true ); + else + openOrSave( open, direct, null ); + } + + private void openOrSave( boolean open, boolean direct, Window owner ) { + String title = n( titleField.getText() ); + String okButtonLabel = n( okButtonLabelField.getText() ); + String fileNameLabel = n( fileNameLabelField.getText() ); + String fileName = n( fileNameField.getText() ); + String folder = n( folderField.getText() ); + String saveAsItem = n( saveAsItemField.getText() ); + String defaultFolder = n( defaultFolderField.getText() ); + String defaultExtension = n( defaultExtensionField.getText() ); + AtomicInteger optionsSet = new AtomicInteger(); + AtomicInteger optionsClear = new AtomicInteger(); + + o( FOS_OVERWRITEPROMPT, overwritePromptCheckBox, optionsSet, optionsClear ); + o( FOS_STRICTFILETYPES, strictFileTypesCheckBox, optionsSet, optionsClear ); + o( FOS_NOCHANGEDIR, noChangeDirCheckBox, optionsSet, optionsClear ); + o( FOS_PICKFOLDERS, pickFoldersCheckBox, optionsSet, optionsClear ); + o( FOS_FORCEFILESYSTEM, forceFileSystemCheckBox, optionsSet, optionsClear ); + o( FOS_ALLNONSTORAGEITEMS, allNonStorageItemsCheckBox, optionsSet, optionsClear ); + o( FOS_NOVALIDATE, noValidateCheckBox, optionsSet, optionsClear ); + o( FOS_ALLOWMULTISELECT, allowMultiSelectCheckBox, optionsSet, optionsClear ); + o( FOS_PATHMUSTEXIST, pathMustExistCheckBox, optionsSet, optionsClear ); + o( FOS_FILEMUSTEXIST, fileMustExistCheckBox, optionsSet, optionsClear ); + o( FOS_CREATEPROMPT, createPromptCheckBox, optionsSet, optionsClear ); + o( FOS_SHAREAWARE, shareAwareCheckBox, optionsSet, optionsClear ); + o( FOS_NOREADONLYRETURN, noReadOnlyReturnCheckBox, optionsSet, optionsClear ); + o( FOS_NOTESTFILECREATE, noTestFileCreateCheckBox, optionsSet, optionsClear ); + o( FOS_HIDEMRUPLACES, hideMruPlacesCheckBox, optionsSet, optionsClear ); + o( FOS_HIDEPINNEDPLACES, hidePinnedPlacesCheckBox, optionsSet, optionsClear ); + o( FOS_NODEREFERENCELINKS, noDereferenceLinksCheckBox, optionsSet, optionsClear ); + o( FOS_OKBUTTONNEEDSINTERACTION, okButtonNeedsInteractionCheckBox, optionsSet, optionsClear ); + o( FOS_DONTADDTORECENT, dontAddToRecentCheckBox, optionsSet, optionsClear ); + o( FOS_FORCESHOWHIDDEN, forceShowHiddenCheckBox, optionsSet, optionsClear ); + o( FOS_DEFAULTNOMINIMODE, defaultNoMiniModeCheckBox, optionsSet, optionsClear ); + o( FOS_FORCEPREVIEWPANEON, forcePreviewPaneonCheckBox, optionsSet, optionsClear ); + o( FOS_SUPPORTSTREAMABLEITEMS, supportStreamableItemsCheckBox, optionsSet, optionsClear ); + + String fileTypesStr = n( (String) fileTypesField.getSelectedItem() ); + String[] fileTypes = {}; + if( fileTypesStr != null ) + fileTypes = fileTypesStr.trim().split( "[,]+" ); + int fileTypeIndex = fileTypeIndexSlider.getValue(); + + FlatNativeWindowsLibrary.FileChooserCallback callback = (files, hwndFileDialog) -> { + System.out.println( " -- callback " + hwndFileDialog + " " + Arrays.toString( files ) ); + if( showMessageDialogOnOKCheckBox.isSelected() ) { + System.out.println( FlatNativeWindowsLibrary.showMessageDialog( hwndFileDialog, + JOptionPane.INFORMATION_MESSAGE, + null, "some text", 1, "Yes", "No" ) ); + } + return true; + }; + + if( direct ) { + String[] files = FlatNativeWindowsLibrary.showFileChooser( owner, open, + title, okButtonLabel, fileNameLabel, fileName, + folder, saveAsItem, defaultFolder, defaultExtension, + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes ); + + filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); + } else { + SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); + + String[] fileTypes2 = fileTypes; + new Thread( () -> { + String[] files = FlatNativeWindowsLibrary.showFileChooser( owner, open, + title, okButtonLabel, fileNameLabel, fileName, + folder, saveAsItem, defaultFolder, defaultExtension, + optionsSet.get(), optionsClear.get(), callback, fileTypeIndex, fileTypes2 ); + + System.out.println( " secondaryLoop.exit() returned " + secondaryLoop.exit() ); + + EventQueue.invokeLater( () -> { + filesField.setText( (files != null) ? Arrays.toString( files ).replace( ',', '\n' ) : "null" ); + } ); + } ).start(); + + System.out.println( "---- enter secondary loop ----" ); + System.out.println( "---- secondary loop exited (secondaryLoop.enter() returned " + secondaryLoop.enter() + ") ----" ); + } + } + + private static String n( String s ) { + return s != null && !s.isEmpty() ? s : null; + } + + private static void o( int option, FlatTriStateCheckBox checkBox, AtomicInteger optionsSet, AtomicInteger optionsClear ) { + if( checkBox.getState() == State.SELECTED ) + optionsSet.set( optionsSet.get() | option ); + else if( checkBox.getState() == State.UNSELECTED ) + optionsClear.set( optionsClear.get() | option ); + } + + private void messageDialog() { + long hwnd = getHWND( SwingUtilities.windowForComponent( this ) ); + String message = messageField.getText(); + String[] buttons = buttonsField.getText().trim().split( "[,]+" ); + + Preferences state = DemoPrefs.getState(); + state.put( "systemfilechooser.windows.message", message ); + state.put( "systemfilechooser.windows.buttons", buttonsField.getText() ); + + System.out.println( FlatNativeWindowsLibrary.showMessageDialog( hwnd, + JOptionPane.WARNING_MESSAGE, null, message, 1, buttons ) ); + } + + private void messageBox() { + long hwnd = getHWND( SwingUtilities.windowForComponent( this ) ); + String message = messageField.getText(); + + System.out.println( FlatNativeWindowsLibrary.showMessageBox( hwnd, message, null, + /* MB_ICONINFORMATION */ 0x00000040 | /* MB_YESNO */ 0x00000004 ) ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + JLabel ownerLabel = new JLabel(); + ownerFrameRadioButton = new JRadioButton(); + ownerDialogRadioButton = new JRadioButton(); + ownerNullRadioButton = new JRadioButton(); + JPanel ownerSpacer = new JPanel(null); + JLabel titleLabel = new JLabel(); + titleField = new JTextField(); + JPanel panel1 = new JPanel(); + overwritePromptCheckBox = new FlatTriStateCheckBox(); + pathMustExistCheckBox = new FlatTriStateCheckBox(); + noDereferenceLinksCheckBox = new FlatTriStateCheckBox(); + strictFileTypesCheckBox = new FlatTriStateCheckBox(); + fileMustExistCheckBox = new FlatTriStateCheckBox(); + okButtonNeedsInteractionCheckBox = new FlatTriStateCheckBox(); + noChangeDirCheckBox = new FlatTriStateCheckBox(); + createPromptCheckBox = new FlatTriStateCheckBox(); + dontAddToRecentCheckBox = new FlatTriStateCheckBox(); + pickFoldersCheckBox = new FlatTriStateCheckBox(); + shareAwareCheckBox = new FlatTriStateCheckBox(); + forceShowHiddenCheckBox = new FlatTriStateCheckBox(); + forceFileSystemCheckBox = new FlatTriStateCheckBox(); + noReadOnlyReturnCheckBox = new FlatTriStateCheckBox(); + defaultNoMiniModeCheckBox = new FlatTriStateCheckBox(); + allNonStorageItemsCheckBox = new FlatTriStateCheckBox(); + noTestFileCreateCheckBox = new FlatTriStateCheckBox(); + forcePreviewPaneonCheckBox = new FlatTriStateCheckBox(); + noValidateCheckBox = new FlatTriStateCheckBox(); + hideMruPlacesCheckBox = new FlatTriStateCheckBox(); + supportStreamableItemsCheckBox = new FlatTriStateCheckBox(); + allowMultiSelectCheckBox = new FlatTriStateCheckBox(); + hidePinnedPlacesCheckBox = new FlatTriStateCheckBox(); + JPanel messageDialogPanel = new JPanel(); + JLabel messageLabel = new JLabel(); + JScrollPane messageScrollPane = new JScrollPane(); + messageField = new JTextArea(); + JLabel buttonsLabel = new JLabel(); + buttonsField = new JTextField(); + JLabel okButtonLabelLabel = new JLabel(); + okButtonLabelField = new JTextField(); + JLabel fileNameLabelLabel = new JLabel(); + fileNameLabelField = new JTextField(); + JLabel fileNameLabel = new JLabel(); + fileNameField = new JTextField(); + JLabel folderLabel = new JLabel(); + folderField = new JTextField(); + JLabel saveAsItemLabel = new JLabel(); + saveAsItemField = new JTextField(); + JLabel defaultFolderLabel = new JLabel(); + defaultFolderField = new JTextField(); + JLabel defaultExtensionLabel = new JLabel(); + defaultExtensionField = new JTextField(); + JLabel fileTypesLabel = new JLabel(); + fileTypesField = new JComboBox<>(); + JLabel fileTypeIndexLabel = new JLabel(); + fileTypeIndexSlider = new JSlider(); + JButton openButton = new JButton(); + JButton saveButton = new JButton(); + JButton openDirectButton = new JButton(); + JButton saveDirectButton = new JButton(); + showMessageDialogOnOKCheckBox = new JCheckBox(); + JPanel hSpacer1 = new JPanel(null); + JButton messageDialogButton = new JButton(); + JButton messageBoxButton = new JButton(); + JScrollPane filesScrollPane = new JScrollPane(); + filesField = new JTextArea(); + + //======== this ======== + setLayout(new MigLayout( + "ltr,insets dialog,hidemode 3", + // columns + "[left]" + + "[grow,fill]" + + "[fill]", + // rows + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[grow,fill]")); + + //---- ownerLabel ---- + ownerLabel.setText("owner"); + add(ownerLabel, "cell 0 0"); + + //---- ownerFrameRadioButton ---- + ownerFrameRadioButton.setText("JFrame"); + ownerFrameRadioButton.setSelected(true); + add(ownerFrameRadioButton, "cell 1 0"); + + //---- ownerDialogRadioButton ---- + ownerDialogRadioButton.setText("JDialog"); + add(ownerDialogRadioButton, "cell 1 0"); + + //---- ownerNullRadioButton ---- + ownerNullRadioButton.setText("null"); + add(ownerNullRadioButton, "cell 1 0"); + add(ownerSpacer, "cell 1 0,growx"); + + //---- titleLabel ---- + titleLabel.setText("title"); + add(titleLabel, "cell 0 1"); + add(titleField, "cell 1 1"); + + //======== panel1 ======== + { + panel1.setLayout(new MigLayout( + "insets 2,hidemode 3", + // columns + "[left]para" + + "[left]para" + + "[left]", + // rows + "[]0" + + "[]0" + + "[]0" + + "[]" + + "[]0" + + "[]0" + + "[]0" + + "[]0" + + "[grow]")); + + //---- overwritePromptCheckBox ---- + overwritePromptCheckBox.setText("overwritePrompt"); + panel1.add(overwritePromptCheckBox, "cell 0 0"); + + //---- pathMustExistCheckBox ---- + pathMustExistCheckBox.setText("pathMustExist"); + panel1.add(pathMustExistCheckBox, "cell 1 0"); + + //---- noDereferenceLinksCheckBox ---- + noDereferenceLinksCheckBox.setText("noDereferenceLinks"); + panel1.add(noDereferenceLinksCheckBox, "cell 2 0"); + + //---- strictFileTypesCheckBox ---- + strictFileTypesCheckBox.setText("strictFileTypes"); + panel1.add(strictFileTypesCheckBox, "cell 0 1"); + + //---- fileMustExistCheckBox ---- + fileMustExistCheckBox.setText("fileMustExist"); + panel1.add(fileMustExistCheckBox, "cell 1 1"); + + //---- okButtonNeedsInteractionCheckBox ---- + okButtonNeedsInteractionCheckBox.setText("okButtonNeedsInteraction"); + panel1.add(okButtonNeedsInteractionCheckBox, "cell 2 1"); + + //---- noChangeDirCheckBox ---- + noChangeDirCheckBox.setText("noChangeDir"); + panel1.add(noChangeDirCheckBox, "cell 0 2"); + + //---- createPromptCheckBox ---- + createPromptCheckBox.setText("createPrompt"); + panel1.add(createPromptCheckBox, "cell 1 2"); + + //---- dontAddToRecentCheckBox ---- + dontAddToRecentCheckBox.setText("dontAddToRecent"); + panel1.add(dontAddToRecentCheckBox, "cell 2 2"); + + //---- pickFoldersCheckBox ---- + pickFoldersCheckBox.setText("pickFolders"); + pickFoldersCheckBox.setFont(pickFoldersCheckBox.getFont().deriveFont(pickFoldersCheckBox.getFont().getStyle() | Font.BOLD)); + panel1.add(pickFoldersCheckBox, "cell 0 3"); + + //---- shareAwareCheckBox ---- + shareAwareCheckBox.setText("shareAware"); + panel1.add(shareAwareCheckBox, "cell 1 3"); + + //---- forceShowHiddenCheckBox ---- + forceShowHiddenCheckBox.setText("forceShowHidden"); + forceShowHiddenCheckBox.setFont(forceShowHiddenCheckBox.getFont().deriveFont(forceShowHiddenCheckBox.getFont().getStyle() | Font.BOLD)); + panel1.add(forceShowHiddenCheckBox, "cell 2 3"); + + //---- forceFileSystemCheckBox ---- + forceFileSystemCheckBox.setText("forceFileSystem"); + panel1.add(forceFileSystemCheckBox, "cell 0 4"); + + //---- noReadOnlyReturnCheckBox ---- + noReadOnlyReturnCheckBox.setText("noReadOnlyReturn"); + panel1.add(noReadOnlyReturnCheckBox, "cell 1 4"); + + //---- defaultNoMiniModeCheckBox ---- + defaultNoMiniModeCheckBox.setText("defaultNoMiniMode"); + panel1.add(defaultNoMiniModeCheckBox, "cell 2 4"); + + //---- allNonStorageItemsCheckBox ---- + allNonStorageItemsCheckBox.setText("allNonStorageItems"); + panel1.add(allNonStorageItemsCheckBox, "cell 0 5"); + + //---- noTestFileCreateCheckBox ---- + noTestFileCreateCheckBox.setText("noTestFileCreate"); + panel1.add(noTestFileCreateCheckBox, "cell 1 5"); + + //---- forcePreviewPaneonCheckBox ---- + forcePreviewPaneonCheckBox.setText("forcePreviewPaneon"); + panel1.add(forcePreviewPaneonCheckBox, "cell 2 5"); + + //---- noValidateCheckBox ---- + noValidateCheckBox.setText("noValidate"); + panel1.add(noValidateCheckBox, "cell 0 6"); + + //---- hideMruPlacesCheckBox ---- + hideMruPlacesCheckBox.setText("hideMruPlaces"); + panel1.add(hideMruPlacesCheckBox, "cell 1 6"); + + //---- supportStreamableItemsCheckBox ---- + supportStreamableItemsCheckBox.setText("supportStreamableItems"); + panel1.add(supportStreamableItemsCheckBox, "cell 2 6"); + + //---- allowMultiSelectCheckBox ---- + allowMultiSelectCheckBox.setText("allowMultiSelect"); + allowMultiSelectCheckBox.setFont(allowMultiSelectCheckBox.getFont().deriveFont(allowMultiSelectCheckBox.getFont().getStyle() | Font.BOLD)); + panel1.add(allowMultiSelectCheckBox, "cell 0 7"); + + //---- hidePinnedPlacesCheckBox ---- + hidePinnedPlacesCheckBox.setText("hidePinnedPlaces"); + panel1.add(hidePinnedPlacesCheckBox, "cell 1 7"); + + //======== messageDialogPanel ======== + { + messageDialogPanel.setBorder(new TitledBorder("MessageDialog")); + messageDialogPanel.setLayout(new MigLayout( + "hidemode 3", + // columns + "[fill]" + + "[grow,fill]", + // rows + "[grow,fill]" + + "[]")); + + //---- messageLabel ---- + messageLabel.setText("Message"); + messageDialogPanel.add(messageLabel, "cell 0 0,aligny top,growy 0"); + + //======== messageScrollPane ======== + { + + //---- messageField ---- + messageField.setColumns(40); + messageField.setRows(4); + messageScrollPane.setViewportView(messageField); + } + messageDialogPanel.add(messageScrollPane, "cell 1 0"); + + //---- buttonsLabel ---- + buttonsLabel.setText("Buttons:"); + messageDialogPanel.add(buttonsLabel, "cell 0 1"); + messageDialogPanel.add(buttonsField, "cell 1 1"); + } + panel1.add(messageDialogPanel, "cell 0 8 3 1,grow"); + } + add(panel1, "cell 2 1 1 10,growy"); + + //---- okButtonLabelLabel ---- + okButtonLabelLabel.setText("okButtonLabel"); + add(okButtonLabelLabel, "cell 0 2"); + add(okButtonLabelField, "cell 1 2"); + + //---- fileNameLabelLabel ---- + fileNameLabelLabel.setText("fileNameLabel"); + add(fileNameLabelLabel, "cell 0 3"); + add(fileNameLabelField, "cell 1 3"); + + //---- fileNameLabel ---- + fileNameLabel.setText("fileName"); + add(fileNameLabel, "cell 0 4"); + add(fileNameField, "cell 1 4"); + + //---- folderLabel ---- + folderLabel.setText("folder"); + add(folderLabel, "cell 0 5"); + add(folderField, "cell 1 5"); + + //---- saveAsItemLabel ---- + saveAsItemLabel.setText("saveAsItem"); + add(saveAsItemLabel, "cell 0 6"); + add(saveAsItemField, "cell 1 6"); + + //---- defaultFolderLabel ---- + defaultFolderLabel.setText("defaultFolder"); + add(defaultFolderLabel, "cell 0 7"); + add(defaultFolderField, "cell 1 7"); + + //---- defaultExtensionLabel ---- + defaultExtensionLabel.setText("defaultExtension"); + add(defaultExtensionLabel, "cell 0 8"); + add(defaultExtensionField, "cell 1 8"); + + //---- fileTypesLabel ---- + fileTypesLabel.setText("fileTypes"); + add(fileTypesLabel, "cell 0 9"); + + //---- fileTypesField ---- + fileTypesField.setEditable(true); + fileTypesField.setModel(new DefaultComboBoxModel<>(new String[] { + "Text Files,*.txt", + "All Files,*.*", + "Text Files,*.txt,PDF Files,*.pdf,All Files,*.*", + "Text and PDF Files,*.txt;*.pdf" + })); + add(fileTypesField, "cell 1 9"); + + //---- fileTypeIndexLabel ---- + fileTypeIndexLabel.setText("fileTypeIndex"); + add(fileTypeIndexLabel, "cell 0 10"); + + //---- fileTypeIndexSlider ---- + fileTypeIndexSlider.setMaximum(10); + fileTypeIndexSlider.setMajorTickSpacing(1); + fileTypeIndexSlider.setValue(0); + fileTypeIndexSlider.setPaintLabels(true); + fileTypeIndexSlider.setSnapToTicks(true); + add(fileTypeIndexSlider, "cell 1 10"); + + //---- openButton ---- + openButton.setText("Open..."); + openButton.addActionListener(e -> open()); + add(openButton, "cell 0 11 3 1"); + + //---- saveButton ---- + saveButton.setText("Save..."); + saveButton.addActionListener(e -> save()); + add(saveButton, "cell 0 11 3 1"); + + //---- openDirectButton ---- + openDirectButton.setText("Open (no-thread)..."); + openDirectButton.addActionListener(e -> openDirect()); + add(openDirectButton, "cell 0 11 3 1"); + + //---- saveDirectButton ---- + saveDirectButton.setText("Save (no-thread)..."); + saveDirectButton.addActionListener(e -> saveDirect()); + add(saveDirectButton, "cell 0 11 3 1"); + + //---- showMessageDialogOnOKCheckBox ---- + showMessageDialogOnOKCheckBox.setText("show message dialog on OK"); + add(showMessageDialogOnOKCheckBox, "cell 0 11 3 1"); + add(hSpacer1, "cell 0 11 3 1,growx"); + + //---- messageDialogButton ---- + messageDialogButton.setText("MessageDialog..."); + messageDialogButton.addActionListener(e -> messageDialog()); + add(messageDialogButton, "cell 0 11 3 1,alignx right,growx 0"); + + //---- messageBoxButton ---- + messageBoxButton.setText("MessageBox..."); + messageBoxButton.addActionListener(e -> messageBox()); + add(messageBoxButton, "cell 0 11 3 1"); + + //======== filesScrollPane ======== + { + + //---- filesField ---- + filesField.setRows(8); + filesScrollPane.setViewportView(filesField); + } + add(filesScrollPane, "cell 0 12 3 1,growx"); + + //---- ownerButtonGroup ---- + ButtonGroup ownerButtonGroup = new ButtonGroup(); + ownerButtonGroup.add(ownerFrameRadioButton); + ownerButtonGroup.add(ownerDialogRadioButton); + ownerButtonGroup.add(ownerNullRadioButton); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JRadioButton ownerFrameRadioButton; + private JRadioButton ownerDialogRadioButton; + private JRadioButton ownerNullRadioButton; + private JTextField titleField; + private FlatTriStateCheckBox overwritePromptCheckBox; + private FlatTriStateCheckBox pathMustExistCheckBox; + private FlatTriStateCheckBox noDereferenceLinksCheckBox; + private FlatTriStateCheckBox strictFileTypesCheckBox; + private FlatTriStateCheckBox fileMustExistCheckBox; + private FlatTriStateCheckBox okButtonNeedsInteractionCheckBox; + private FlatTriStateCheckBox noChangeDirCheckBox; + private FlatTriStateCheckBox createPromptCheckBox; + private FlatTriStateCheckBox dontAddToRecentCheckBox; + private FlatTriStateCheckBox pickFoldersCheckBox; + private FlatTriStateCheckBox shareAwareCheckBox; + private FlatTriStateCheckBox forceShowHiddenCheckBox; + private FlatTriStateCheckBox forceFileSystemCheckBox; + private FlatTriStateCheckBox noReadOnlyReturnCheckBox; + private FlatTriStateCheckBox defaultNoMiniModeCheckBox; + private FlatTriStateCheckBox allNonStorageItemsCheckBox; + private FlatTriStateCheckBox noTestFileCreateCheckBox; + private FlatTriStateCheckBox forcePreviewPaneonCheckBox; + private FlatTriStateCheckBox noValidateCheckBox; + private FlatTriStateCheckBox hideMruPlacesCheckBox; + private FlatTriStateCheckBox supportStreamableItemsCheckBox; + private FlatTriStateCheckBox allowMultiSelectCheckBox; + private FlatTriStateCheckBox hidePinnedPlacesCheckBox; + private JTextArea messageField; + private JTextField buttonsField; + private JTextField okButtonLabelField; + private JTextField fileNameLabelField; + private JTextField fileNameField; + private JTextField folderField; + private JTextField saveAsItemField; + private JTextField defaultFolderField; + private JTextField defaultExtensionField; + private JComboBox fileTypesField; + private JSlider fileTypeIndexSlider; + private JCheckBox showMessageDialogOnOKCheckBox; + private JTextArea filesField; + // JFormDesigner - End of variables declaration //GEN-END:variables +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd new file mode 100644 index 000000000..86f6219ee --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSystemFileChooserWindowsTest.jfd @@ -0,0 +1,550 @@ +JFDML JFormDesigner: "8.3" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + auxiliary() { + "JavaCodeGenerator.defaultVariableLocal": true + } + add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "ltr,insets dialog,hidemode 3" + "$columnConstraints": "[left][grow,fill][fill]" + "$rowConstraints": "[][][][][][][][][][][][][grow,fill]" + } ) { + name: "this" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "ownerLabel" + "text": "owner" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerFrameRadioButton" + "text": "JFrame" + "selected": true + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerDialogRadioButton" + "text": "JDialog" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "ownerNullRadioButton" + "text": "null" + "$buttonGroup": new FormReference( "ownerButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) { + name: "ownerSpacer" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0,growx" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "titleLabel" + "text": "title" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "titleField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "insets 2,hidemode 3" + "$columnConstraints": "[left]para[left]para[left]" + "$rowConstraints": "[]0[]0[]0[][]0[]0[]0[]0[grow]" + } ) { + name: "panel1" + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "overwritePromptCheckBox" + "text": "overwritePrompt" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "pathMustExistCheckBox" + "text": "pathMustExist" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "noDereferenceLinksCheckBox" + "text": "noDereferenceLinks" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "strictFileTypesCheckBox" + "text": "strictFileTypes" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "fileMustExistCheckBox" + "text": "fileMustExist" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "okButtonNeedsInteractionCheckBox" + "text": "okButtonNeedsInteraction" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "noChangeDirCheckBox" + "text": "noChangeDir" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "createPromptCheckBox" + "text": "createPrompt" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "dontAddToRecentCheckBox" + "text": "dontAddToRecent" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "pickFoldersCheckBox" + "text": "pickFolders" + "font": new com.jformdesigner.model.SwingDerivedFont( null, 1, 0, false ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "shareAwareCheckBox" + "text": "shareAware" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "forceShowHiddenCheckBox" + "text": "forceShowHidden" + "font": new com.jformdesigner.model.SwingDerivedFont( null, 1, 0, false ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 3" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "forceFileSystemCheckBox" + "text": "forceFileSystem" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "noReadOnlyReturnCheckBox" + "text": "noReadOnlyReturn" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "defaultNoMiniModeCheckBox" + "text": "defaultNoMiniMode" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 4" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "allNonStorageItemsCheckBox" + "text": "allNonStorageItems" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "noTestFileCreateCheckBox" + "text": "noTestFileCreate" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "forcePreviewPaneonCheckBox" + "text": "forcePreviewPaneon" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 5" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "noValidateCheckBox" + "text": "noValidate" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "hideMruPlacesCheckBox" + "text": "hideMruPlaces" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "supportStreamableItemsCheckBox" + "text": "supportStreamableItems" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 6" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "allowMultiSelectCheckBox" + "text": "allowMultiSelect" + "font": new com.jformdesigner.model.SwingDerivedFont( null, 1, 0, false ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "hidePinnedPlacesCheckBox" + "text": "hidePinnedPlaces" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "hidemode 3" + "$columnConstraints": "[fill][grow,fill]" + "$rowConstraints": "[grow,fill][]" + } ) { + name: "messageDialogPanel" + "border": new javax.swing.border.TitledBorder( "MessageDialog" ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "messageLabel" + "text": "Message" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0,aligny top,growy 0" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "messageScrollPane" + add( new FormComponent( "javax.swing.JTextArea" ) { + name: "messageField" + "columns": 40 + "rows": 4 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "buttonsLabel" + "text": "Buttons:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "buttonsField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8 3 1,grow" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1 1 10,growy" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "okButtonLabelLabel" + "text": "okButtonLabel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "okButtonLabelField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileNameLabelLabel" + "text": "fileNameLabel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "fileNameLabelField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileNameLabel" + "text": "fileName" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "fileNameField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "folderLabel" + "text": "folder" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "folderField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "saveAsItemLabel" + "text": "saveAsItem" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "saveAsItemField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "defaultFolderLabel" + "text": "defaultFolder" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "defaultFolderField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "defaultExtensionLabel" + "text": "defaultExtension" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "defaultExtensionField" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 8" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypesLabel" + "text": "fileTypes" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 9" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "fileTypesField" + "editable": true + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "Text Files,*.txt" + addElement( "Text Files,*.txt" ) + addElement( "All Files,*.*" ) + addElement( "Text Files,*.txt,PDF Files,*.pdf,All Files,*.*" ) + addElement( "Text and PDF Files,*.txt;*.pdf" ) + } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 9" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "fileTypeIndexLabel" + "text": "fileTypeIndex" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 10" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "fileTypeIndexSlider" + "maximum": 10 + "majorTickSpacing": 1 + "value": 0 + "paintLabels": true + "snapToTicks": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 10" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "openButton" + "text": "Open..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "open", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "saveButton" + "text": "Save..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "save", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "openDirectButton" + "text": "Open (no-thread)..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openDirect", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "saveDirectButton" + "text": "Save (no-thread)..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "saveDirect", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showMessageDialogOnOKCheckBox" + "text": "show message dialog on OK" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) + add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) { + name: "hSpacer1" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1,growx" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "messageDialogButton" + "text": "MessageDialog..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "messageDialog", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1,alignx right,growx 0" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "messageBoxButton" + "text": "MessageBox..." + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "messageBox", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11 3 1" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "filesScrollPane" + add( new FormComponent( "javax.swing.JTextArea" ) { + name: "filesField" + "rows": 8 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 12 3 1,growx" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 890, 630 ) + } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "ownerButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 640 ) + } ) + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java index fbb936f54..ce86d8689 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java @@ -245,6 +245,9 @@ public void dispose() { super.dispose(); FlatUIDefaultsInspector.hide(); + + if( getDefaultCloseOperation() == JFrame.EXIT_ON_CLOSE ) + System.exit( 0 ); } private void updateTitle() { diff --git a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java index 9340627ac..ab4566711 100644 --- a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java +++ b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java @@ -73,6 +73,7 @@ import com.formdev.flatlaf.themes.FlatMacLightLaf; import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.util.StringUtils; +import com.formdev.flatlaf.util.SystemFileChooser; import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; @@ -96,6 +97,8 @@ class FlatThemeFileEditor private static final String KEY_SHOW_RGB_COLORS = "showRgbColors"; private static final String KEY_SHOW_COLOR_LUMA = "showColorLuma"; + private static final int NEW_PROPERTIES_FILE_OPTION = 100; + private File dir; private Preferences state; private boolean inLoadDirectory; @@ -227,48 +230,45 @@ private void openDirectory() { return; // choose directory - JFileChooser chooser = new JFileChooser( dir ) { - @Override - public void approveSelection() { - if( !checkDirectory( this, getSelectedFile() ) ) - return; - - super.approveSelection(); - } - }; - chooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY ); - if( chooser.showOpenDialog( this ) != JFileChooser.APPROVE_OPTION ) + SystemFileChooser chooser = new SystemFileChooser( dir ); + chooser.setFileSelectionMode( SystemFileChooser.DIRECTORIES_ONLY ); + chooser.setApproveCallback( this::checkDirectory ); + int result = chooser.showOpenDialog( this ); + if( result == SystemFileChooser.CANCEL_OPTION ) return; File selectedFile = chooser.getSelectedFile(); if( selectedFile == null || selectedFile.equals( dir ) ) return; + if( result == NEW_PROPERTIES_FILE_OPTION ) { + if( !newPropertiesFile( selectedFile ) ) + return; + } + // open new directory loadDirectory( selectedFile ); } - private boolean checkDirectory( Component parentComponent, File dir ) { + private int checkDirectory( File[] selectedFiles, SystemFileChooser.ApproveContext context ) { + File dir = selectedFiles[0]; if( !dir.isDirectory() ) { - JOptionPane.showMessageDialog( parentComponent, - "Directory '" + dir + "' does not exist.", - getTitle(), JOptionPane.INFORMATION_MESSAGE ); - return false; + showMessageDialog( context, "Directory '" + dir + "' does not exist.", null ); + return SystemFileChooser.CANCEL_OPTION; } if( getPropertiesFiles( dir ).length == 0 ) { UIManager.put( "OptionPane.sameSizeButtons", false ); - int result = JOptionPane.showOptionDialog( parentComponent, - "Directory '" + dir + "' does not contain properties files.\n\n" - + "Do you want create a new theme in this directory?\n\n" + int result = showMessageDialog( context, + "Directory '" + dir + "' does not contain properties files.", + "Do you want create a new theme in this directory?\n\n" + "Or do you want modify/extend core themes and create empty" + " 'FlatLightLaf.properties' and 'FlatDarkLaf.properties' files in this directory?", - getTitle(), JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, null, - new Object[] { "New Theme", "Modify Core Themes", "Cancel" }, null ); + "_New Theme", "_Modify Core Themes", "_Cancel" ); UIManager.put( "OptionPane.sameSizeButtons", null ); if( result == 0 ) - return newPropertiesFile( dir ); + return NEW_PROPERTIES_FILE_OPTION; else if( result == 1 ) { try { String content = @@ -280,18 +280,37 @@ else if( result == 1 ) { "\n"; writeFile( new File( dir, "FlatLightLaf.properties" ), content ); writeFile( new File( dir, "FlatDarkLaf.properties" ), content ); - return true; + return SystemFileChooser.APPROVE_OPTION; } catch( IOException ex ) { ex.printStackTrace(); - JOptionPane.showMessageDialog( parentComponent, - "Failed to create 'FlatLightLaf.properties' or 'FlatDarkLaf.properties'." ); + showMessageDialog( context, + "Failed to create 'FlatLightLaf.properties' or 'FlatDarkLaf.properties'.", null ); } } - return false; + return SystemFileChooser.CANCEL_OPTION; } - return true; + return SystemFileChooser.APPROVE_OPTION; + } + + private int showMessageDialog( SystemFileChooser.ApproveContext context, + String primaryText, String secondaryText, String... buttons ) + { + if( context != null ) { + // invoked from SystemFileChooser + return context.showMessageDialog( JOptionPane.INFORMATION_MESSAGE, + primaryText, secondaryText, 0, buttons ); + } else { + // invoked from directoryChanged() + if( secondaryText != null ) + primaryText = primaryText + "\n\n" + secondaryText; + for( int i = 0; i < buttons.length; i++ ) + buttons[i] = buttons[i].replace( "_", "" ); + return JOptionPane.showOptionDialog( this, primaryText, getTitle(), + JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, null, + (buttons.length > 0) ? buttons : null, null ); + } } private void directoryChanged() { @@ -302,7 +321,15 @@ private void directoryChanged() { if( dir == null ) return; - if( checkDirectory( this, dir ) ) + directoryField.hidePopup(); + + int result = checkDirectory( new File[] { dir }, null ); + if( result == NEW_PROPERTIES_FILE_OPTION ) { + if( !newPropertiesFile( dir ) ) + return; + } + + if( result != SystemFileChooser.CANCEL_OPTION ) loadDirectory( dir ); else { // remove from directories history @@ -390,6 +417,9 @@ private File[] getPropertiesFiles( File dir ) { File[] propertiesFiles = dir.listFiles( (d, name) -> { return name.endsWith( ".properties" ); } ); + if( propertiesFiles == null ) + propertiesFiles = new File[0]; + Arrays.sort( propertiesFiles, (f1, f2) -> { String n1 = toSortName( f1.getName() ); String n2 = toSortName( f2.getName() ); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 91bcfee89..ee8ec1ab8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ fifesoft-autocomplete = "com.fifesoft:autocomplete:3.3.1" # flatlaf-testing glazedlists = "com.glazedlists:glazedlists:1.11.0" netbeans-api-awt = "org.netbeans.api:org-openide-awt:RELEASE112" +nativejfilechooser = "li.flor:native-j-file-chooser:1.6.4" # flatlaf-natives-jna jna = "net.java.dev.jna:jna:5.15.0"