Skip to content

Commit 4848fad

Browse files
committed
CAY-2948 Modeler: use native FileDialog on Mac
1 parent 9008d8c commit 4848fad

15 files changed

Lines changed: 381 additions & 239 deletions

File tree

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ CAY-2943 CayenneModeler MCP: open_project tool
4545
CAY-2945 CayenneModeler universal "main"
4646
CAY-2946 Claude Code "plugin" for agentic coding with Cayenne
4747
CAY-2947 Merge "cayenne-commitlog" into the core
48+
CAY-2948 Modeler: use native FileDialog on Mac
4849

4950
Bug Fixes:
5051

modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/Application.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.apache.cayenne.modeler.dbconnector.DBConnectors;
3232
import org.apache.cayenne.modeler.log.ModelerLogFactory;
3333
import org.apache.cayenne.modeler.platform.UIInitializer;
34+
import org.apache.cayenne.modeler.toolkit.filechooser.FileChooserFactory;
3435
import org.apache.cayenne.modeler.pref.adapters.ClasspathPrefs;
3536
import org.apache.cayenne.modeler.pref.adapters.DBConnectorPrefs;
3637
import org.apache.cayenne.modeler.pref.adapters.GeneralPrefs;
@@ -88,6 +89,7 @@ public static void launch(String[] args, UIInitializer platformInit) {
8889

8990
private final Injector injector;
9091
private final UIInitializer platformInit;
92+
private final FileChooserFactory fileChooserFactory;
9193
private final ModelerClassLoader classLoader;
9294
private final PrefsLocator prefsLocator;
9395
private final PrefsManager prefsManager;
@@ -102,6 +104,7 @@ public static void launch(String[] args, UIInitializer platformInit) {
102104
public Application(Injector injector, UIInitializer platformInit, CliArgs cli) {
103105
this.injector = injector;
104106
this.platformInit = platformInit;
107+
this.fileChooserFactory = platformInit.fileChooserFactory();
105108
this.cli = cli;
106109

107110
this.classLoader = new ModelerClassLoader();
@@ -143,6 +146,10 @@ public UIInitializer getPlatformInit() {
143146
return platformInit;
144147
}
145148

149+
public FileChooserFactory getFileChooserFactory() {
150+
return fileChooserFactory;
151+
}
152+
146153
public ConfigurationNodeParentGetter getConfigurationNodeParentGetter() {
147154
return injector.getInstance(ConfigurationNodeParentGetter.class);
148155
}

modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/platform/UIInitializer.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
package org.apache.cayenne.modeler.platform;
2020

2121
import org.apache.cayenne.modeler.Application;
22+
import org.apache.cayenne.modeler.toolkit.filechooser.FileChooserFactory;
23+
import org.apache.cayenne.modeler.toolkit.filechooser.JFileChooserFactory;
2224

2325
/**
2426
* A base callback for platform-specific Modeler initialization.
@@ -38,4 +40,12 @@ default void beforeSwingLaunch() {
3840
*/
3941
default void afterFrameCreated(Application app) {
4042
}
43+
44+
/**
45+
* Returns the platform-appropriate {@link FileChooserFactory}. Defaults to Swing's
46+
* {@code JFileChooser}; overridden on macOS to use the native {@code FileDialog}.
47+
*/
48+
default FileChooserFactory fileChooserFactory() {
49+
return new JFileChooserFactory();
50+
}
4151
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
****************************************************************/
19+
20+
package org.apache.cayenne.modeler.platform.mac;
21+
22+
import org.apache.cayenne.modeler.toolkit.filechooser.FileChooserFactory;
23+
24+
import javax.swing.SwingUtilities;
25+
import javax.swing.filechooser.FileFilter;
26+
import java.awt.Component;
27+
import java.awt.FileDialog;
28+
import java.awt.Frame;
29+
import java.awt.Window;
30+
import java.io.File;
31+
import java.io.FilenameFilter;
32+
33+
/**
34+
* {@link FileChooserFactory} implementation backed by the native OS {@link FileDialog}.
35+
* On macOS this delegates to NSOpenPanel / NSSavePanel, providing Quick Look preview,
36+
* Spotlight search, and the standard Finder sidebar.
37+
*
38+
* <p>Directory picking requires {@code apple.awt.fileDialogForDirectories=true} to be set
39+
* as a global JVM property before Swing starts (set by {@code MacUIInitializer}).
40+
*/
41+
public class MacFileChooserFactory extends FileChooserFactory {
42+
43+
private static final String DIRS_DIALOG_PROPERTY = "apple.awt.fileDialogForDirectories";
44+
45+
@Override
46+
public File openFile(Component parent, String title, File initialDir, FileFilter filter) {
47+
Frame frame = toFrame(parent);
48+
String titleStr = title != null ? title : "";
49+
FilenameFilter fnFilter = filter != null
50+
? (dir, name) -> filter.accept(new File(dir, name))
51+
: null;
52+
53+
File startDir = initialDir;
54+
while (true) {
55+
FileDialog fd = new FileDialog(frame, titleStr, FileDialog.LOAD);
56+
if (startDir != null) {
57+
fd.setDirectory(startDir.getAbsolutePath());
58+
}
59+
if (fnFilter != null) {
60+
fd.setFilenameFilter(fnFilter);
61+
}
62+
fd.setVisible(true);
63+
64+
String file = fd.getFile();
65+
if (file == null) {
66+
return null; // canceled
67+
}
68+
File selected = new File(fd.getDirectory(), file);
69+
if (selected.isFile()) {
70+
return selected;
71+
}
72+
// User selected a directory (possible when apple.awt.fileDialogForDirectories=true);
73+
// re-show starting inside it so they can navigate to a file.
74+
startDir = selected;
75+
}
76+
}
77+
78+
@Override
79+
public File saveFile(Component parent, String title, File initialDir, String defaultName) {
80+
FileDialog fd = new FileDialog(toFrame(parent), title != null ? title : "", FileDialog.SAVE);
81+
if (initialDir != null) {
82+
fd.setDirectory(initialDir.getAbsolutePath());
83+
}
84+
if (defaultName != null) {
85+
fd.setFile(defaultName);
86+
}
87+
fd.setVisible(true);
88+
String file = fd.getFile();
89+
return file != null ? new File(fd.getDirectory(), file) : null;
90+
}
91+
92+
@Override
93+
public File openDirectory(Component parent, String title, File initialDir) {
94+
// Toggle the property only for the duration of this modal dialog.
95+
// FileDialog is EDT-blocking, so no other dialog can run concurrently.
96+
System.setProperty(DIRS_DIALOG_PROPERTY, "true");
97+
try {
98+
FileDialog fd = new FileDialog(toFrame(parent), title != null ? title : "", FileDialog.LOAD);
99+
if (initialDir != null) {
100+
fd.setDirectory(initialDir.getAbsolutePath());
101+
}
102+
fd.setVisible(true);
103+
String file = fd.getFile();
104+
// getFile() returns the selected directory name; getDirectory() is its parent.
105+
return file != null ? new File(fd.getDirectory(), file) : null;
106+
} finally {
107+
System.clearProperty(DIRS_DIALOG_PROPERTY);
108+
}
109+
}
110+
111+
private static Frame toFrame(Component c) {
112+
Window w = SwingUtilities.getWindowAncestor(c);
113+
while (w != null) {
114+
if (w instanceof Frame f) {
115+
return f;
116+
}
117+
w = w.getOwner();
118+
}
119+
return null;
120+
}
121+
}

modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/platform/mac/MacUIInitializer.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.apache.cayenne.modeler.Application;
2222
import org.apache.cayenne.modeler.service.action.GlobalActions;
2323
import org.apache.cayenne.modeler.platform.UIInitializer;
24+
import org.apache.cayenne.modeler.toolkit.filechooser.FileChooserFactory;
2425
import org.apache.cayenne.modeler.ui.action.AboutAction;
2526
import org.apache.cayenne.modeler.ui.action.ConfigurePreferencesAction;
2627
import org.apache.cayenne.modeler.ui.action.ExitAction;
@@ -40,7 +41,6 @@ public void beforeSwingLaunch() {
4041
System.setProperty("apple.awt.application.name", "CayenneModeler");
4142

4243
// override some default styles and colors, assuming that Aqua theme will be used
43-
4444
Color lightGrey = new Color(0xEEEEEE);
4545
Color darkGrey = new Color(225, 225, 225);
4646
Border darkBorder = BorderFactory.createLineBorder(darkGrey);
@@ -91,6 +91,11 @@ public void paintBorder(Component c, Graphics g, int x, int y, int width, int he
9191
UIManager.put("MenuItem.selectionForeground", Color.BLACK);
9292
}
9393

94+
@Override
95+
public FileChooserFactory fileChooserFactory() {
96+
return new MacFileChooserFactory();
97+
}
98+
9499
@Override
95100
public void afterFrameCreated(Application app) {
96101

modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/pref/adapters/FileChooserPrefs.java

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import org.apache.cayenne.modeler.pref.PreferenceAdapter;
2323

2424
import javax.swing.JFileChooser;
25+
import java.awt.FileDialog;
26+
import java.awt.event.ComponentAdapter;
27+
import java.awt.event.ComponentEvent;
2528
import java.io.File;
2629
import java.util.prefs.Preferences;
2730

@@ -34,24 +37,47 @@ public FileChooserPrefs(Preferences prefs) {
3437
}
3538

3639
public void bind(JFileChooser chooser) {
37-
File startDir = resolveExistingDirectory(prefs.get(PATH_PROPERTY, null));
40+
File startDir = loadDir();
3841
if (startDir != null) {
3942
chooser.setCurrentDirectory(startDir);
4043
}
41-
4244
chooser.addActionListener(e -> {
4345
if (JFileChooser.APPROVE_SELECTION.equals(e.getActionCommand())) {
44-
File selected = chooser.getSelectedFile();
45-
if (selected != null) {
46-
prefs.put(PATH_PROPERTY,
47-
selected.isFile()
48-
? selected.getParentFile().getAbsolutePath()
49-
: selected.getAbsolutePath());
46+
saveDir(chooser.getSelectedFile());
47+
}
48+
});
49+
}
50+
51+
public void bind(FileDialog dialog) {
52+
File startDir = loadDir();
53+
if (startDir != null) {
54+
dialog.setDirectory(startDir.getAbsolutePath());
55+
}
56+
dialog.addComponentListener(new ComponentAdapter() {
57+
@Override
58+
public void componentHidden(ComponentEvent e) {
59+
String file = dialog.getFile();
60+
String dir = dialog.getDirectory();
61+
if (file != null && dir != null) {
62+
prefs.put(PATH_PROPERTY, dir);
5063
}
5164
}
5265
});
5366
}
5467

68+
public File loadDir() {
69+
return resolveExistingDirectory(prefs.get(PATH_PROPERTY, null));
70+
}
71+
72+
public void saveDir(File f) {
73+
if (f != null) {
74+
prefs.put(PATH_PROPERTY,
75+
f.isFile()
76+
? f.getParentFile().getAbsolutePath()
77+
: f.getAbsolutePath());
78+
}
79+
}
80+
5581
private static File resolveExistingDirectory(String path) {
5682
if (path == null) {
5783
return null;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
****************************************************************/
19+
20+
package org.apache.cayenne.modeler.toolkit.filechooser;
21+
22+
import javax.swing.filechooser.FileFilter;
23+
import java.awt.Component;
24+
import java.io.File;
25+
26+
/**
27+
* Platform-specific factory for file and directory chooser dialogs.
28+
* Implementations may use either a native OS dialog or Swing's JFileChooser.
29+
*/
30+
public abstract class FileChooserFactory {
31+
32+
/**
33+
* Shows a file-open dialog and returns the selected file, or {@code null} if canceled.
34+
*/
35+
public abstract File openFile(Component parent, String title, File initialDir, FileFilter filter);
36+
37+
/**
38+
* Shows a file-save dialog and returns the selected file, or {@code null} if canceled.
39+
*/
40+
public abstract File saveFile(Component parent, String title, File initialDir, String defaultName);
41+
42+
/**
43+
* Shows a directory-selection dialog and returns the selected directory, or {@code null} if canceled.
44+
*/
45+
public abstract File openDirectory(Component parent, String title, File initialDir);
46+
}

0 commit comments

Comments
 (0)