mac native filechooser as sheet added
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / mac / MacFileChooserDialogImpl.java
1 /*
2  * Copyright 2000-2010 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.ui.mac;
17
18 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
19 import com.intellij.openapi.fileChooser.MacFileChooserDialog;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.vfs.LocalFileSystem;
22 import com.intellij.openapi.vfs.VirtualFile;
23 import com.intellij.ui.mac.foundation.Foundation;
24 import com.intellij.ui.mac.foundation.ID;
25 import com.sun.jna.Callback;
26 import com.sun.jna.Pointer;
27 import org.jetbrains.annotations.NotNull;
28 import org.jetbrains.annotations.Nullable;
29
30 import javax.swing.*;
31 import java.awt.*;
32 import java.util.ArrayList;
33 import java.util.List;
34
35 /**
36  * @author spleaner
37  */
38 public class MacFileChooserDialogImpl implements MacFileChooserDialog {
39   private static final int OK = 1;
40
41   private static JDialog myFakeDialog;
42   private static List<VirtualFile> myResultFiles;
43
44   private static FileChooserDescriptor myChooserDescriptor;
45   private static boolean myFileChooserActive = false;
46
47   private static MacFileChooserCallback mySheetCallback = null;
48
49   private Project myProject;
50
51   private static final Callback SHOULD_SHOW_FILENAME_CALLBACK = new Callback() {
52       public boolean callback(ID self, String selector, ID panel, ID filename) {
53         final String fileName = Foundation.toStringViaUTF8(filename);
54         final VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(fileName);
55         return virtualFile != null && (virtualFile.isDirectory() || getDescriptor().isFileSelectable(virtualFile));
56       }
57     };
58
59   private static final Callback IS_VALID_FILENAME_CALLBACK = new Callback() {
60       public boolean callback(ID self, String selector, ID panel, ID filename) {
61         final String fileName = Foundation.toStringViaUTF8(filename);
62         final VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(fileName);
63         return virtualFile != null && (!virtualFile.isDirectory() || getDescriptor().isFileSelectable(virtualFile));
64       }
65     };
66
67   private static final Callback OPEN_PANEL_DID_END = new Callback() {
68       public void callback(ID self, String selector, ID openPanelDidEnd, ID returnCode, ID contextInfo) {
69         processResult(returnCode, openPanelDidEnd);
70
71         try {
72           if (myResultFiles != null) {
73             final VirtualFile[] chosenFiles = myResultFiles.toArray(new VirtualFile[myResultFiles.size()]);
74             final MacFileChooserCallback callback = mySheetCallback;
75             SwingUtilities.invokeLater(new Runnable() {
76               public void run() {
77                 callback.onChosen(chosenFiles);
78               }
79             });
80           }
81         }
82         finally {
83           myFileChooserActive = false;
84           myResultFiles = null;
85           mySheetCallback = null;
86         }
87       }
88     };
89
90
91   private static final Callback MAIN_THREAD_RUNNABLE = new Callback() {
92     public void callback(ID self, String selector, ID toSelect) {
93       final ID chooser = invoke("NSOpenPanel", "openPanel");
94
95       invoke(chooser, "setPrompt:", Foundation.cfString("Choose"));
96       invoke(chooser, "setCanChooseFiles:", myChooserDescriptor.isChooseFiles());
97       invoke(chooser, "setCanChooseDirectories:", myChooserDescriptor.isChooseFolders());
98       invoke(chooser, "setAllowsMultipleSelection:", myChooserDescriptor.isChooseMultiple());
99       if (Foundation.isClassRespondsToSelector(Foundation.getClass("NSOpenPanel"), Foundation.createSelector("_setIncludeNewFolderButton:"))) {
100         invoke(chooser, "_setIncludeNewFolderButton:", true);
101       }
102
103       invoke(chooser, "setDelegate:", self);
104
105       final String toSelectPath = toSelect.intValue() == 0 ? null : Foundation.toStringViaUTF8(toSelect);
106       final VirtualFile toSelectFile = toSelectPath == null ? null : LocalFileSystem.getInstance().findFileByPath(toSelectPath);
107       final ID directory = toSelectFile == null ? null : toSelectFile.isDirectory() ? toSelect : null;
108       final ID file = toSelectFile == null ? null : !toSelectFile.isDirectory() ? toSelect : null;
109
110       if (mySheetCallback != null) {
111         final Window activeWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
112         if (activeWindow != null && activeWindow instanceof Frame) {
113           final String frameTitle = ((Frame)activeWindow).getTitle();
114
115           final ID sharedApplication = invoke("NSApplication", "sharedApplication");
116           final ID windows = invoke(sharedApplication, "windows");
117           final ID windowEnumerator = invoke(windows, "objectEnumerator");
118
119           ID focusedWindow = null;
120           while (true) {
121             // dirty hack: walks through all the windows to find a cocoa window to show sheet for
122             final ID window = invoke(windowEnumerator, "nextObject");
123             if (0 == window.intValue()) break;
124
125             final ID windowTitle = invoke(window, "title");
126             final String titleString = Foundation.toStringViaUTF8(windowTitle);
127             if (titleString.equals(frameTitle)) focusedWindow = window;
128           }
129
130           if (focusedWindow != null) {
131             invoke(chooser, "beginSheetForDirectory:file:types:modalForWindow:modalDelegate:didEndSelector:contextInfo:",
132                    directory, file, null, focusedWindow, self, Foundation.createSelector("openPanelDidEnd:returnCode:contextInfo:"), null);
133           }
134         }
135       } else {
136         final ID result = invoke(chooser, "runModalForDirectory:file:", directory, file);
137         processResult(result, chooser);
138       }
139     }
140   };
141
142   private static void processResult(final ID result, final ID panel) {
143     SwingUtilities.invokeLater(new Runnable() {
144       public void run() {
145         if (myFakeDialog != null) {
146           myFakeDialog.dispose();
147           myFakeDialog = null;
148         }
149       }
150     });
151
152     final List<VirtualFile> resultFiles = new ArrayList<VirtualFile>();
153     if (result != null && OK == result.intValue()) {
154       ID fileNamesArray = invoke(panel, "filenames");
155       ID enumerator = invoke(fileNamesArray, "objectEnumerator");
156
157       while (true) {
158         final ID filename = invoke(enumerator, "nextObject");
159         if (0 == filename.intValue()) break;
160
161         String s = Foundation.toStringViaUTF8(filename);
162         if (s != null) {
163           VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(s);
164           if (virtualFile != null && virtualFile.isValid()) resultFiles.add(virtualFile);
165         }
166       }
167
168       myResultFiles = resultFiles;
169     }
170   }
171
172   static {
173     final ID delegateClass = Foundation.registerObjcClass(Foundation.getClass("NSObject"), "NSOpenPanelDelegate_");
174     if (!Foundation.addMethod(delegateClass, Foundation.createSelector("panel:shouldShowFilename:"), SHOULD_SHOW_FILENAME_CALLBACK, "B*"))
175       throw new RuntimeException("Unable to add method to objective-c delegate class!");
176     if (!Foundation.addMethod(delegateClass, Foundation.createSelector("panel:isValidFilename:"), IS_VALID_FILENAME_CALLBACK, "B*"))
177       throw new RuntimeException("Unable to add method to objective-c delegate class!");
178     if (!Foundation.addMethod(delegateClass, Foundation.createSelector("showOpenPanel:"), MAIN_THREAD_RUNNABLE, "v*"))
179       throw new RuntimeException("Unable to add method to objective-c delegate class!");
180     if (!Foundation.addMethod(delegateClass, Foundation.createSelector("openPanelDidEnd:returnCode:contextInfo:"), OPEN_PANEL_DID_END, "v*i^void"))
181       throw new RuntimeException("Unable to add method to objective-c delegate class!");
182     Foundation.registerObjcClassPair(delegateClass);
183   }
184
185   public MacFileChooserDialogImpl(final FileChooserDescriptor chooserDescriptor, final Project project) {
186     setDescriptor(chooserDescriptor);
187     myProject = project;
188   }
189
190   public MacFileChooserDialogImpl(final FileChooserDescriptor chooserDescriptor, final Component component) {
191     setDescriptor(chooserDescriptor);
192   }
193
194   private static void setDescriptor(@Nullable final FileChooserDescriptor descriptor) {
195     myChooserDescriptor = descriptor;
196   }
197
198   private static FileChooserDescriptor getDescriptor() {
199     return myChooserDescriptor;
200   }
201
202   private static void showNativeChooser(@Nullable VirtualFile toSelect) {
203     final ID autoReleasePool = createAutoReleasePool();
204
205     try {
206       final ID delegate = invoke(Foundation.getClass("NSOpenPanelDelegate_"), "new");
207
208       final Pointer select = toSelect == null ? null : Foundation.cfString(toSelect.getPath());
209       Foundation.cfRetain(delegate);
210
211       invoke(delegate, "performSelectorOnMainThread:withObject:waitUntilDone:", Foundation.createSelector("showOpenPanel:"), select, false);
212     }
213     finally {
214       invoke(autoReleasePool, "release");
215     }
216   }
217
218   private static void showNativeChooserAsSheet(@Nullable VirtualFile toSelect, @NotNull final MacFileChooserCallback callback) {
219     final ID autoReleasePool = createAutoReleasePool();
220
221     try {
222       final ID delegate = invoke(Foundation.getClass("NSOpenPanelDelegate_"), "new");
223
224       final Pointer select = toSelect == null ? null : Foundation.cfString(toSelect.getPath());
225       Foundation.cfRetain(delegate);
226
227       invoke(delegate, "performSelectorOnMainThread:withObject:waitUntilDone:", Foundation.createSelector("showOpenPanel:"), select, false);
228     }
229     finally {
230       invoke(autoReleasePool, "release");
231     }
232   }
233
234   public void chooseWithSheet(@Nullable final VirtualFile toSelect, @Nullable final Project project,
235                                        @NotNull final MacFileChooserCallback callback) {
236     assert !myFileChooserActive: "Current native file chooser should finish before next usage!";
237     mySheetCallback = callback;
238
239     SwingUtilities.invokeLater(new Runnable() {
240       public void run() {
241         showNativeChooserAsSheet(getToSelect(toSelect, project), callback);
242       }
243     });
244   }
245
246   @NotNull
247   public VirtualFile[] choose(@Nullable final VirtualFile toSelect, @Nullable final Project project) {
248     assert !myFileChooserActive: "Current native file chooser should finish before next usage!";
249
250     myFileChooserActive = true;
251
252     SwingUtilities.invokeLater(new Runnable() {
253       public void run() {
254         showNativeChooser(getToSelect(toSelect, project));
255       }
256     });
257
258     Frame parentFrame = null;
259     final Window parent = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
260     if (parent instanceof Frame) {
261       parentFrame = (Frame) parent;
262     }
263
264     myFakeDialog = new JDialog(parentFrame);
265     myFakeDialog.setModal(true);
266     myFakeDialog.setUndecorated(true);
267     myFakeDialog.getRootPane().putClientProperty( "Window.shadow", Boolean.FALSE );
268
269     myFakeDialog.setBounds(0, 0, 0, 0);
270     myFakeDialog.setVisible(true);
271
272     try {
273       if (myResultFiles == null) {
274         return new VirtualFile[0];
275       } else {
276         return myResultFiles.toArray(new VirtualFile[myResultFiles.size()]);
277       }
278     }
279     finally {
280       myFileChooserActive = false;
281       myResultFiles = null;
282     }
283   }
284
285   private static VirtualFile getToSelect(VirtualFile toSelect, Project project) {
286     final VirtualFile[] selectFile = new VirtualFile[] {null};
287     if (toSelect == null) {
288       if (project != null && project.getBaseDir() != null) {
289         selectFile[0] = project.getBaseDir();
290       }
291     } else {
292       selectFile[0] = toSelect.isValid() ? toSelect : null;
293     }
294     return selectFile[0];
295   }
296
297   private static ID createAutoReleasePool() {
298     return invoke("NSAutoreleasePool", "new");
299   }
300
301   private static ID invoke(@NotNull final String className, @NotNull final String selector, Object... args) {
302     return invoke(Foundation.getClass(className), selector, args);
303   }
304
305   private static ID invoke(@NotNull final ID id, @NotNull final String selector, Object... args) {
306     return Foundation.invoke(id, Foundation.createSelector(selector), args);
307   }
308 }