This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit e473ef299fc0af0e4915a0cfd6f300ca7a18b601 Author: Andrus Adamchik <[email protected]> AuthorDate: Mon May 25 10:54:15 2026 -0400 CAY-2948 Modeler: use native FileDialog on Mac for performance reasons turning individual dialogs to reusable singletons --- .../modeler/platform/mac/MacFileChooser.java | 8 ++-- .../toolkit/filechooser/SwingDirChooserDialog.java | 52 +++++++++++++++------ .../toolkit/filechooser/SwingFileChooser.java | 18 +++----- .../toolkit/filechooser/SwingFileOpenerDialog.java | 54 +++++++++++++++------- .../toolkit/filechooser/SwingFileSaverDialog.java | 52 +++++++++++++++------ 5 files changed, 121 insertions(+), 63 deletions(-) diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/platform/mac/MacFileChooser.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/platform/mac/MacFileChooser.java index c81e4533f..8924f9974 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/platform/mac/MacFileChooser.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/platform/mac/MacFileChooser.java @@ -57,15 +57,13 @@ public class MacFileChooser implements CMFileChooser { @Override public File openDir(File initialDir) { // must use custom JFileChooser, as macOS native dialog doesn't allow to block file name selection - return new SwingDirChooserDialog(parent, title, initialDir).open(); + return SwingDirChooserDialog.getInstance().open(parent, title, initialDir, null); } @Override public File openDir(FileChooserPrefs prefs) { // must use custom JFileChooser, as macOS native dialog doesn't allow to block file name selection - SwingDirChooserDialog dialog = new SwingDirChooserDialog(parent, title, prefs.getDir()); - prefs.bind(dialog); - return dialog.open(); + return SwingDirChooserDialog.getInstance().open(parent, title, prefs.getDir(), prefs); } @Override @@ -76,7 +74,7 @@ public class MacFileChooser implements CMFileChooser { @Override public File saveDir(File initialDir) { // must use custom JFileChooser, as macOS native dialog doesn't allow to block file name selection - return new SwingDirChooserDialog(parent, title, initialDir).open(); + return SwingDirChooserDialog.getInstance().open(parent, title, initialDir, null); } private File showOpen(FilenameFilter fnFilter, File startDir, Consumer<FileDialog> init) { diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingDirChooserDialog.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingDirChooserDialog.java index 8638c2a97..0c91a1870 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingDirChooserDialog.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingDirChooserDialog.java @@ -19,6 +19,8 @@ package org.apache.cayenne.modeler.toolkit.filechooser; +import org.apache.cayenne.modeler.pref.adapters.FileChooserPrefs; + import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JFileChooser; @@ -27,6 +29,7 @@ import javax.swing.JPanel; import java.awt.BorderLayout; import java.awt.Component; import java.awt.FlowLayout; +import java.awt.event.ActionListener; import java.io.File; /** @@ -35,32 +38,50 @@ import java.io.File; */ public final class SwingDirChooserDialog extends JFileChooser { - private final Component parent; + private static SwingDirChooserDialog instance; + + /** + * Returns the lazily-initialized cached singleton. Constructing a {@link JFileChooser} is + * expensive — on Windows it triggers a full L&F UI delegate install and a Shell32 scan + * that can take hundreds of milliseconds, making first-time dialog open feel sluggish. + * Since Swing is single-threaded, one instance is safely reused across all callers; transient + * state (listeners, current directory, selection) is reset on each {@link #open}. + */ + public static SwingDirChooserDialog getInstance() { + if (instance == null) { + instance = new SwingDirChooserDialog(); + } + return instance; + } + private final JButton selectButton = new JButton("Select"); private final JButton cancelButton = new JButton("Cancel"); private final JButton newFolderButton = new JButton("New Folder"); - public SwingDirChooserDialog(Component parent, String title, File startDir) { - super(startDir); - this.parent = parent; - + private SwingDirChooserDialog() { setFileSelectionMode(DIRECTORIES_ONLY); // Using setControlButtonsAreShown(false) because on macOS Aqua L&F the built-in approve button is // continuously re-disabled when nothing is selected (e.g. after navigating into a directory). setControlButtonsAreShown(false); - if (title != null) { - setDialogTitle(title); - } - - initBindings(); + selectButton.addActionListener(e -> approveSelection()); + cancelButton.addActionListener(e -> cancelSelection()); + newFolderButton.addActionListener(e -> promptNewFolder()); } /** * Shows the dialog modally and returns the selected directory, or {@code null} if the user cancelled. */ - public File open() { + public File open(Component parent, String title, File startDir, FileChooserPrefs prefs) { + reset(); + if (startDir != null) { + setCurrentDirectory(startDir); + } + setDialogTitle(title); + if (prefs != null) { + prefs.bind(this); + } if (showOpenDialog(parent) != APPROVE_OPTION) { return null; } @@ -90,10 +111,11 @@ public final class SwingDirChooserDialog extends JFileChooser { return dialog; } - private void initBindings() { - selectButton.addActionListener(e -> approveSelection()); - cancelButton.addActionListener(e -> cancelSelection()); - newFolderButton.addActionListener(e -> promptNewFolder()); + private void reset() { + for (ActionListener l : getActionListeners()) { + removeActionListener(l); + } + setSelectedFile(null); } private void promptNewFolder() { diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileChooser.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileChooser.java index b4d4f46eb..3fa85c92c 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileChooser.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileChooser.java @@ -37,37 +37,31 @@ public class SwingFileChooser implements CMFileChooser { @Override public File openFile(File initialDir, FileFilter filter) { - return new SwingFileOpenerDialog(parent, title, initialDir, filter).open(); + return SwingFileOpenerDialog.getInstance().open(parent, title, initialDir, filter, null); } @Override public File openFile(FileChooserPrefs prefs, FileFilter filter) { - SwingFileOpenerDialog dialog = new SwingFileOpenerDialog(parent, title, prefs.getDir(), filter); - prefs.bind(dialog); - return dialog.open(); + return SwingFileOpenerDialog.getInstance().open(parent, title, prefs.getDir(), filter, prefs); } @Override public File openDir(File initialDir) { - return new SwingDirChooserDialog(parent, title, initialDir).open(); + return SwingDirChooserDialog.getInstance().open(parent, title, initialDir, null); } @Override public File openDir(FileChooserPrefs prefs) { - SwingDirChooserDialog dialog = new SwingDirChooserDialog(parent, title, prefs.getDir()); - prefs.bind(dialog); - return dialog.open(); + return SwingDirChooserDialog.getInstance().open(parent, title, prefs.getDir(), prefs); } @Override public File saveFile(FileChooserPrefs prefs, String defaultName) { - SwingFileSaverDialog dialog = new SwingFileSaverDialog(parent, title, prefs.getDir(), defaultName); - prefs.bind(dialog); - return dialog.open(); + return SwingFileSaverDialog.getInstance().open(parent, title, prefs.getDir(), defaultName, prefs); } @Override public File saveDir(File initialDir) { - return new SwingDirChooserDialog(parent, title, initialDir).open(); + return SwingDirChooserDialog.getInstance().open(parent, title, initialDir, null); } } diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileOpenerDialog.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileOpenerDialog.java index 849b8b210..ebb570adc 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileOpenerDialog.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileOpenerDialog.java @@ -19,9 +19,12 @@ package org.apache.cayenne.modeler.toolkit.filechooser; +import org.apache.cayenne.modeler.pref.adapters.FileChooserPrefs; + import javax.swing.JFileChooser; import javax.swing.filechooser.FileFilter; import java.awt.Component; +import java.awt.event.ActionListener; import java.io.File; /** @@ -29,31 +32,50 @@ import java.io.File; */ public final class SwingFileOpenerDialog extends JFileChooser { - private final Component parent; - - public SwingFileOpenerDialog( - Component parent, - String title, - File startDir, - FileFilter filter) { + private static SwingFileOpenerDialog instance; - super(startDir); + /** + * Returns the lazily-initialized cached singleton. Constructing a {@link JFileChooser} is + * expensive — on Windows it triggers a full L&F UI delegate install and a Shell32 scan + * that can take hundreds of milliseconds, making first-time dialog open feel sluggish. + * Since Swing is single-threaded, one instance is safely reused across all callers; transient + * state (listeners, filters, current directory, selection) is reset on each {@link #open}. + */ + public static SwingFileOpenerDialog getInstance() { + if (instance == null) { + instance = new SwingFileOpenerDialog(); + } + return instance; + } - this.parent = parent; + private SwingFileOpenerDialog() { setFileSelectionMode(FILES_ONLY); - if (title != null) { - setDialogTitle(title); + } + + /** + * Shows the dialog modally and returns the selected file, or {@code null} if the user cancelled. + */ + public File open(Component parent, String title, File startDir, FileFilter filter, FileChooserPrefs prefs) { + reset(); + if (startDir != null) { + setCurrentDirectory(startDir); } + setDialogTitle(title); if (filter != null) { addChoosableFileFilter(filter); setFileFilter(filter); } + if (prefs != null) { + prefs.bind(this); + } + return showOpenDialog(parent) == APPROVE_OPTION ? getSelectedFile() : null; } - /** - * Shows the dialog modally and returns the selected file, or {@code null} if the user cancelled. - */ - public File open() { - return showOpenDialog(parent) == APPROVE_OPTION ? getSelectedFile() : null; + private void reset() { + for (ActionListener l : getActionListeners()) { + removeActionListener(l); + } + resetChoosableFileFilters(); + setSelectedFile(null); } } diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileSaverDialog.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileSaverDialog.java index 661f4ef6d..980e3662f 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileSaverDialog.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/toolkit/filechooser/SwingFileSaverDialog.java @@ -19,8 +19,11 @@ package org.apache.cayenne.modeler.toolkit.filechooser; +import org.apache.cayenne.modeler.pref.adapters.FileChooserPrefs; + import javax.swing.JFileChooser; import java.awt.Component; +import java.awt.event.ActionListener; import java.io.File; /** @@ -28,29 +31,48 @@ import java.io.File; */ public final class SwingFileSaverDialog extends JFileChooser { - private final Component parent; + private static SwingFileSaverDialog instance; - public SwingFileSaverDialog( - Component parent, - String title, - File startDir, - String defaultName) { + /** + * Returns the lazily-initialized cached singleton. Constructing a {@link JFileChooser} is + * expensive — on Windows it triggers a full L&F UI delegate install and a Shell32 scan + * that can take hundreds of milliseconds, making first-time dialog open feel sluggish. + * Since Swing is single-threaded, one instance is safely reused across all callers; transient + * state (listeners, filters, current directory, selection) is reset on each {@link #open}. + */ + public static SwingFileSaverDialog getInstance() { + if (instance == null) { + instance = new SwingFileSaverDialog(); + } + return instance; + } - super(startDir); + private SwingFileSaverDialog() { + } - this.parent = parent; - if (title != null) { - setDialogTitle(title); + /** + * Shows the dialog modally and returns the selected file, or {@code null} if the user cancelled. + */ + public File open(Component parent, String title, File startDir, String defaultName, FileChooserPrefs prefs) { + reset(); + if (startDir != null) { + setCurrentDirectory(startDir); } + setDialogTitle(title); if (defaultName != null) { setSelectedFile(new File(defaultName)); } + if (prefs != null) { + prefs.bind(this); + } + return showSaveDialog(parent) == APPROVE_OPTION ? getSelectedFile() : null; } - /** - * Shows the dialog modally and returns the selected file, or {@code null} if the user cancelled. - */ - public File open() { - return showSaveDialog(parent) == APPROVE_OPTION ? getSelectedFile() : null; + private void reset() { + for (ActionListener l : getActionListeners()) { + removeActionListener(l); + } + resetChoosableFileFilters(); + setSelectedFile(null); } }
