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 753225f33ddc3abb051eb51430fbf032aa96177e Author: Andrus Adamchik <[email protected]> AuthorDate: Sun May 24 09:28:46 2026 -0400 CAY-2948 Modeler: use native FileDialog on Mac reverting to JFileChooser for Save on Mac --- .../modeler/platform/mac/MacFileChooser.java | 78 +++----------------- .../toolkit/filechooser/SwingFileChooser.java | 86 +++++++++++++++++++--- 2 files changed, 85 insertions(+), 79 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 d03eeb20e..b60bd01e5 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 @@ -21,16 +21,11 @@ package org.apache.cayenne.modeler.platform.mac; import org.apache.cayenne.modeler.pref.adapters.FileChooserPrefs; import org.apache.cayenne.modeler.toolkit.filechooser.CMFileChooser; +import org.apache.cayenne.modeler.toolkit.filechooser.SwingFileChooser; -import javax.swing.JButton; -import javax.swing.JFileChooser; -import javax.swing.SwingUtilities; import javax.swing.filechooser.FileFilter; -import java.awt.Component; -import java.awt.Container; import java.awt.FileDialog; import java.awt.Frame; -import java.awt.event.HierarchyEvent; import java.io.File; import java.io.FilenameFilter; import java.util.function.Consumer; @@ -53,7 +48,9 @@ public class MacFileChooser implements CMFileChooser { @Override public File openFile(File initialDir, FileFilter filter) { return showOpen(toFilenameFilter(filter), - fd -> { if (initialDir != null) fd.setDirectory(initialDir.getAbsolutePath()); }); + fd -> { + if (initialDir != null) fd.setDirectory(initialDir.getAbsolutePath()); + }); } @Override @@ -63,12 +60,14 @@ public class MacFileChooser implements CMFileChooser { @Override public File openDir(File initialDir) { - return showOpenDir(c -> { if (initialDir != null) c.setCurrentDirectory(initialDir); }); + // must use custom JFileChooser, as MacOS native dialog doesn't allow to block file name selection + return SwingFileChooser.showOpenDir(parent, title, c -> { if (initialDir != null) c.setCurrentDirectory(initialDir); }); } @Override public File openDir(FileChooserPrefs prefs) { - return showOpenDir(prefs::bind); + // must use custom JFileChooser, as MacOS native dialog doesn't allow to block file name selection + return SwingFileChooser.showOpenDir(parent, title, prefs::bind); } @Override @@ -78,7 +77,8 @@ public class MacFileChooser implements CMFileChooser { @Override public File saveDir(File initialDir) { - return showOpenDir(c -> { if (initialDir != null) c.setCurrentDirectory(initialDir); }); + // must use custom JFileChooser, as MacOS native dialog doesn't allow to block file name selection + return SwingFileChooser.showOpenDir(parent, title, c -> { if (initialDir != null) c.setCurrentDirectory(initialDir); }); } private File showOpen(FilenameFilter fnFilter, Consumer<FileDialog> init) { @@ -103,64 +103,6 @@ public class MacFileChooser implements CMFileChooser { return file != null ? new File(fd.getDirectory(), file) : null; } - private File showOpenDir(Consumer<JFileChooser> init) { - - // JFileChooser instead of FileDialog: apple.awt.fileDialogForDirectories makes - // FileDialog pick directories, but the button is labeled "Load" with no way to - // override it. JFileChooser shows "Open" and lets us set a dialog title. - - // FILES_AND_DIRECTORIES + directory filter instead of DIRECTORIES_ONLY: on macOS - // Aqua L&F the approval button stays disabled in DIRECTORIES_ONLY mode. - JFileChooser chooser = new JFileChooser(); - chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); - chooser.setFileFilter(DIRS_ONLY_FILTER); - - // AquaFileChooserUI disables the approve button when the user navigates into a - // directory with nothing selected. Directly enabling buttons in the component tree - // (via invokeLater, after Aqua's own listener runs) keeps it always active so the - // user can confirm the current directory without selecting a child item. - // No state mutations — setSelectedFile(dir) makes Aqua navigate into that dir, - // firing DIRECTORY_CHANGED again and causing an infinite loop. - chooser.addPropertyChangeListener(e -> { - String prop = e.getPropertyName(); - if (JFileChooser.DIRECTORY_CHANGED_PROPERTY.equals(prop) - || JFileChooser.SELECTED_FILE_CHANGED_PROPERTY.equals(prop)) { - SwingUtilities.invokeLater(() -> enableButtons(chooser)); - } - }); - - chooser.addHierarchyListener(e -> { - if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && chooser.isShowing()) { - enableButtons(chooser); - } - }); - - init.accept(chooser); - if (title != null) { - chooser.setDialogTitle(title); - } - if (chooser.showOpenDialog(parent) != JFileChooser.APPROVE_OPTION) { - return null; - } - File selected = chooser.getSelectedFile(); - return selected != null ? selected : chooser.getCurrentDirectory(); - } - - private static void enableButtons(Container container) { - for (Component c : container.getComponents()) { - if (c instanceof JButton btn) { - btn.setEnabled(true); - } else if (c instanceof Container cont) { - enableButtons(cont); - } - } - } - - private static final FileFilter DIRS_ONLY_FILTER = new FileFilter() { - @Override public boolean accept(File f) { return f.isDirectory(); } - @Override public String getDescription() { return "Directories"; } - }; - private static FilenameFilter toFilenameFilter(FileFilter filter) { return filter != null ? (dir, name) -> filter.accept(new File(dir, name)) : null; } 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 6aa0890ea..b80886661 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 @@ -21,9 +21,18 @@ 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; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; import javax.swing.filechooser.FileFilter; +import java.awt.BorderLayout; import java.awt.Component; +import java.awt.Dialog; +import java.awt.FlowLayout; +import java.awt.Window; import java.io.File; import java.util.function.Consumer; @@ -55,13 +64,12 @@ public class SwingFileChooser implements CMFileChooser { @Override public File openDir(File initialDir) { - return showOpen(JFileChooser.DIRECTORIES_ONLY, - c -> { if (initialDir != null) c.setCurrentDirectory(initialDir); }, null); + return showOpenDir(parent, title, c -> { if (initialDir != null) c.setCurrentDirectory(initialDir); }); } @Override public File openDir(FileChooserPrefs prefs) { - return showOpen(JFileChooser.DIRECTORIES_ONLY, prefs::bind, null); + return showOpenDir(parent, title, prefs::bind); } @Override @@ -71,17 +79,73 @@ public class SwingFileChooser implements CMFileChooser { @Override public File saveDir(File initialDir) { + return showOpenDir(parent, title, c -> { if (initialDir != null) c.setCurrentDirectory(initialDir); }); + } + + // setControlButtonsAreShown(false) + custom buttons: 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). The custom dialog owns its own always-enabled "Select" + // button and a "New Folder" button, and is used on all platforms for consistency. + public static File showOpenDir(Component parent, String title, Consumer<JFileChooser> init) { JFileChooser chooser = new JFileChooser(); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - if (initialDir != null) { - chooser.setCurrentDirectory(initialDir); - } - if (title != null) { - chooser.setDialogTitle(title); + chooser.setControlButtonsAreShown(false); + init.accept(chooser); + + boolean[] approved = {false}; + JButton selectBtn = new JButton("Select"); + JButton cancelBtn = new JButton("Cancel"); + JButton newFolderBtn = new JButton("New Folder"); + + selectBtn.addActionListener(e -> { + approved[0] = true; + SwingUtilities.getWindowAncestor(chooser).dispose(); + }); + cancelBtn.addActionListener(e -> SwingUtilities.getWindowAncestor(chooser).dispose()); + newFolderBtn.addActionListener(e -> { + String name = JOptionPane.showInputDialog(SwingUtilities.getWindowAncestor(chooser), + "Folder name:", "New Folder", JOptionPane.PLAIN_MESSAGE); + if (name != null && !name.isBlank()) { + File newDir = new File(chooser.getCurrentDirectory(), name); + if (newDir.mkdir()) { + chooser.setCurrentDirectory(newDir); + } else { + JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(chooser), + "Could not create folder \"" + name + "\".", "Error", JOptionPane.ERROR_MESSAGE); + } + } + }); + chooser.addActionListener(e -> { + if (JFileChooser.APPROVE_SELECTION.equals(e.getActionCommand())) { + approved[0] = true; + } + SwingUtilities.getWindowAncestor(chooser).dispose(); + }); + + JPanel left = new JPanel(new FlowLayout(FlowLayout.LEADING)); + left.add(newFolderBtn); + JPanel right = new JPanel(new FlowLayout(FlowLayout.TRAILING)); + right.add(cancelBtn); + right.add(selectBtn); + JPanel buttons = new JPanel(new BorderLayout()); + buttons.add(left, BorderLayout.WEST); + buttons.add(right, BorderLayout.EAST); + + Window owner = (parent instanceof Window w) ? w : (parent != null ? SwingUtilities.getWindowAncestor(parent) : null); + JDialog dialog = new JDialog(owner, title != null ? title : "", Dialog.ModalityType.APPLICATION_MODAL); + dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + dialog.add(chooser, BorderLayout.CENTER); + dialog.add(buttons, BorderLayout.SOUTH); + dialog.getRootPane().setDefaultButton(selectBtn); + dialog.pack(); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + + if (!approved[0]) { + return null; } - return chooser.showDialog(parent, "Select") == JFileChooser.APPROVE_OPTION - ? chooser.getSelectedFile() - : null; + File selected = chooser.getSelectedFile(); + return selected != null ? selected : chooser.getCurrentDirectory(); } private File showOpen(int mode, Consumer<JFileChooser> init, FileFilter filter) {
