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) {

Reply via email to