| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.ui.mac; |
| |
| import com.intellij.openapi.extensions.Extensions; |
| import com.intellij.openapi.fileChooser.FileChooser; |
| import com.intellij.openapi.fileChooser.FileChooserDescriptor; |
| import com.intellij.openapi.fileChooser.PathChooserDialog; |
| import com.intellij.openapi.fileChooser.impl.FileChooserUtil; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.SystemInfo; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.util.registry.Registry; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.LocalFileSystem; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.openapi.wm.ex.WindowManagerEx; |
| import com.intellij.openapi.wm.impl.IdeMenuBar; |
| import com.intellij.projectImport.ProjectOpenProcessor; |
| import com.intellij.ui.mac.foundation.Foundation; |
| import com.intellij.ui.mac.foundation.ID; |
| import com.intellij.ui.mac.foundation.MacUtil; |
| import com.intellij.util.Consumer; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.ui.UIUtil; |
| import com.sun.jna.Callback; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.awt.*; |
| import java.io.File; |
| import java.util.*; |
| import java.util.List; |
| |
| /** |
| * @author spleaner |
| */ |
| @SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod") |
| public class MacFileChooserDialogImpl implements PathChooserDialog { |
| private static final int OK = 1; |
| |
| private static final Map<ID, MacFileChooserDialogImpl> ourImplMap = new HashMap<ID, MacFileChooserDialogImpl>(2); |
| |
| private final FileChooserDescriptor myChooserDescriptor; |
| private final Project myProject; |
| private Consumer<List<VirtualFile>> myCallback; |
| |
| private static boolean checkFile(@NotNull ID self, ID url, boolean checkDirectories) { |
| MacFileChooserDialogImpl dialog = ourImplMap.get(self); |
| if (dialog == null) { |
| // Since it has already been removed from the map, the file is likely to be valid if the user was able to select it |
| return true; |
| } |
| |
| if (url == null || url.intValue() == 0) { |
| return false; |
| } |
| |
| ID filename = Foundation.invoke(url, "path"); |
| String fileName = Foundation.toStringViaUTF8(filename); |
| if (fileName == null) { |
| return false; |
| } |
| |
| VirtualFile file = LocalFileSystem.getInstance().findFileByPath(fileName); |
| return file == null || (!checkDirectories && file.isDirectory()) || dialog.myChooserDescriptor.isFileSelectable(file); |
| } |
| |
| private static final Callback SHOULD_ENABLE_CALLBACK = new Callback() { |
| @SuppressWarnings("UnusedDeclaration") |
| public boolean callback(ID self, String selector, ID panel, ID url) { |
| // allow any directory - ability to select nested directories |
| return checkFile(self, url, false); |
| } |
| }; |
| |
| private static final Callback VALIDATE_URL_CALLBACK = new Callback() { |
| @SuppressWarnings("UnusedDeclaration") |
| public boolean callback(ID self, String selector, ID panel, ID url, ID outError) { |
| if (checkFile(self, url, true)) { |
| return true; |
| } |
| |
| /* |
| if (!outError.equals(ID.NIL)) { |
| ID error = Foundation.invoke("NSError", "errorWithDomain:code:userInfo:", Foundation.nsString("org.jetbrains"), |
| Foundation.createDict(new String[]{"NSLocalizedDescriptionKey"}, new Object[]{"Not allowed"})); |
| // todo "*outError = error" |
| } |
| */ |
| return false; |
| } |
| }; |
| |
| private static final Callback OPEN_PANEL_DID_END = new Callback() { |
| @SuppressWarnings("UnusedDeclaration") |
| public void callback(ID self, String selector, ID openPanelDidEnd, ID returnCode, ID contextInfo) { |
| final MacFileChooserDialogImpl impl = ourImplMap.remove(self); |
| |
| try { |
| //noinspection SSBasedInspection |
| SwingUtilities.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| final IdeMenuBar bar = getMenuBar(); |
| if (bar != null) { |
| bar.enableUpdates(); |
| } |
| } |
| }); |
| |
| final List<String> resultPaths = processResult(returnCode, openPanelDidEnd); |
| if (resultPaths.size() > 0) { |
| //noinspection SSBasedInspection |
| SwingUtilities.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| final List<VirtualFile> files = getChosenFiles(resultPaths); |
| if (files.size() > 0) { |
| FileChooserUtil.setLastOpenedFile(impl.myProject, files.get(files.size() - 1)); |
| impl.myCallback.consume(files); |
| } |
| } |
| }); |
| } else if (impl.myCallback instanceof FileChooser.FileChooserConsumer) { |
| //noinspection SSBasedInspection |
| SwingUtilities.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| ((FileChooser.FileChooserConsumer)impl.myCallback).cancelled(); |
| } |
| }); |
| } |
| } |
| finally { |
| Foundation.cfRelease(self); |
| Foundation.cfRelease(contextInfo); |
| JDK7WindowReorderingWorkaround.enableReordering(); |
| } |
| } |
| }; |
| |
| @NotNull |
| private static List<String> processResult(final ID result, final ID panel) { |
| final List<String> resultPaths = new ArrayList<String>(); |
| |
| if (result != null && OK == result.intValue()) { |
| final ID fileNamesArray = invoke(panel, "URLs"); |
| final ID enumerator = invoke(fileNamesArray, "objectEnumerator"); |
| |
| while (true) { |
| final ID url = invoke(enumerator, "nextObject"); |
| if (url == null || 0 == url.intValue()) break; |
| |
| final ID filename = invoke(url, "path"); |
| final String path = Foundation.toStringViaUTF8(filename); |
| if (path != null) { |
| resultPaths.add(path); |
| } |
| } |
| } |
| |
| return resultPaths; |
| } |
| |
| @NotNull |
| private static List<VirtualFile> getChosenFiles(final List<String> paths) { |
| if (ContainerUtil.isEmpty(paths)) { |
| return Collections.emptyList(); |
| } |
| |
| final LocalFileSystem fs = LocalFileSystem.getInstance(); |
| final List<VirtualFile> files = ContainerUtil.newArrayListWithCapacity(paths.size()); |
| for (String path : paths) { |
| final String vfsPath = FileUtil.toSystemIndependentName(path); |
| final VirtualFile file = fs.refreshAndFindFileByPath(vfsPath); |
| if (file != null && file.isValid()) { |
| files.add(file); |
| } |
| } |
| |
| return files; |
| } |
| |
| private static final Callback MAIN_THREAD_RUNNABLE = new Callback() { |
| @SuppressWarnings("UnusedDeclaration") |
| public void callback(ID self, String selector, ID toSelect) { |
| final ID nsOpenPanel = Foundation.getObjcClass("NSOpenPanel"); |
| final ID chooser = invoke(nsOpenPanel, "openPanel"); |
| // Release in OPEN_PANEL_DID_END panel |
| Foundation.cfRetain(chooser); |
| |
| final FileChooserDescriptor chooserDescriptor = ourImplMap.get(self).myChooserDescriptor; |
| |
| invoke(chooser, "setPrompt:", Foundation.nsString("Choose")); |
| invoke(chooser, "setCanChooseFiles:", chooserDescriptor.isChooseFiles() || chooserDescriptor.isChooseJars()); |
| invoke(chooser, "setCanChooseDirectories:", chooserDescriptor.isChooseFolders()); |
| invoke(chooser, "setAllowsMultipleSelection:", chooserDescriptor.isChooseMultiple()); |
| invoke(chooser, "setTreatsFilePackagesAsDirectories:", chooserDescriptor.isChooseFolders()); |
| invoke(chooser, "setResolvesAliases:", false); |
| |
| String description = chooserDescriptor.getDescription(); |
| if (!StringUtil.isEmpty(description)) { |
| invoke(chooser, "setMessage:", Foundation.nsString(description)); |
| } |
| |
| if (Foundation.isClassRespondsToSelector(nsOpenPanel, Foundation.createSelector("setCanCreateDirectories:"))) { |
| invoke(chooser, "setCanCreateDirectories:", true); |
| } |
| else if (Foundation.isClassRespondsToSelector(nsOpenPanel, Foundation.createSelector("_setIncludeNewFolderButton:"))) { |
| invoke(chooser, "_setIncludeNewFolderButton:", true); |
| } |
| |
| @SuppressWarnings("deprecation") boolean showHidden = |
| chooserDescriptor.isShowHiddenFiles() || |
| chooserDescriptor.getUserData(PathChooserDialog.NATIVE_MAC_CHOOSER_SHOW_HIDDEN_FILES) == Boolean.TRUE || |
| Registry.is("ide.mac.file.chooser.show.hidden.files"); |
| if (showHidden) { |
| if (Foundation.isClassRespondsToSelector(nsOpenPanel, Foundation.createSelector("setShowsHiddenFiles:"))) { |
| invoke(chooser, "setShowsHiddenFiles:", true); |
| } |
| } |
| |
| invoke(chooser, "setDelegate:", self); |
| |
| ID directory = null; |
| ID file = null; |
| final String toSelectPath = toSelect == null || toSelect.intValue() == 0 ? null : Foundation.toStringViaUTF8(toSelect); |
| if (toSelectPath != null) { |
| final File toSelectFile = new File(toSelectPath); |
| if (toSelectFile.isDirectory()) { |
| directory = toSelect; |
| } |
| else if (toSelectFile.isFile()) { |
| directory = Foundation.nsString(toSelectFile.getParent()); |
| file = Foundation.nsString(toSelectFile.getName()); |
| } |
| } |
| |
| ID types = null; |
| if (!chooserDescriptor.isChooseFiles() && chooserDescriptor.isChooseJars()) { |
| types = invoke("NSArray", "arrayWithObjects:", Foundation.nsString("jar"), Foundation.nsString("zip"), null); |
| } |
| |
| final Window activeWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow(); |
| if (activeWindow != null) { |
| String activeWindowTitle = null; |
| if (activeWindow instanceof Frame) { |
| activeWindowTitle = ((Frame)activeWindow).getTitle(); |
| } |
| else if (activeWindow instanceof JDialog) { |
| activeWindowTitle = ((JDialog)activeWindow).getTitle(); |
| } |
| |
| final ID focusedWindow = MacUtil.findWindowForTitle(activeWindowTitle); |
| if (focusedWindow != null) { |
| invoke(chooser, "beginSheetForDirectory:file:types:modalForWindow:modalDelegate:didEndSelector:contextInfo:", |
| directory, file, types, focusedWindow, self, Foundation.createSelector("openPanelDidEnd:returnCode:contextInfo:"), chooser); |
| } |
| } |
| } |
| }; |
| |
| static { |
| ID delegate = Foundation.allocateObjcClassPair(Foundation.getObjcClass("NSObject"), "NSOpenPanelDelegate_"); |
| addFoundationMethod(delegate, "showOpenPanel:", MAIN_THREAD_RUNNABLE, "v*"); |
| addFoundationMethod(delegate, "openPanelDidEnd:returnCode:contextInfo:", OPEN_PANEL_DID_END, "v*i"); |
| addFoundationMethod(delegate, "panel:shouldEnableURL:", SHOULD_ENABLE_CALLBACK, "B@@"); |
| if (SystemInfo.isMacOSSnowLeopard) { |
| addFoundationMethod(delegate, "panel:validateURL:error:", VALIDATE_URL_CALLBACK, "B@@o"); |
| } |
| Foundation.registerObjcClassPair(delegate); |
| } |
| |
| private static void addFoundationMethod(@NotNull ID delegate, @NotNull String selector, @NotNull Callback callback, @NotNull String types) { |
| if (!Foundation.addMethod(delegate, Foundation.createSelector(selector), callback, types)) { |
| throw new RuntimeException("Unable to add method " + selector + " to objective-c delegate class!"); |
| } |
| } |
| |
| public MacFileChooserDialogImpl(@NotNull final FileChooserDescriptor chooserDescriptor, final Project project) { |
| myChooserDescriptor = chooserDescriptor; |
| myProject = project; |
| } |
| |
| @Override |
| public void choose(@Nullable final VirtualFile toSelect, @NotNull final Consumer<List<VirtualFile>> callback) { |
| |
| ExtensionsInitializer.initialize(); |
| |
| myCallback = callback; |
| |
| final VirtualFile lastOpenedFile = FileChooserUtil.getLastOpenedFile(myProject); |
| final VirtualFile selectFile = FileChooserUtil.getFileToSelect(myChooserDescriptor, myProject, toSelect, lastOpenedFile); |
| final String selectPath = selectFile != null ? FileUtil.toSystemDependentName(selectFile.getPath()) : null; |
| |
| //noinspection SSBasedInspection |
| SwingUtilities.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| showNativeChooserAsSheet(MacFileChooserDialogImpl.this, selectPath); |
| } |
| }); |
| } |
| |
| @Nullable |
| private static IdeMenuBar getMenuBar() { |
| Window cur = WindowManagerEx.getInstanceEx().getMostRecentFocusedWindow(); |
| |
| while (cur != null) { |
| if (cur instanceof JFrame) { |
| final JMenuBar menuBar = ((JFrame)cur).getJMenuBar(); |
| if (menuBar instanceof IdeMenuBar) { |
| return (IdeMenuBar)menuBar; |
| } |
| } |
| cur = cur.getOwner(); |
| } |
| return null; |
| } |
| |
| private static void showNativeChooserAsSheet(@NotNull final MacFileChooserDialogImpl impl, @Nullable final String toSelect) { |
| final IdeMenuBar bar = getMenuBar(); |
| if (bar != null) { |
| bar.disableUpdates(); |
| } |
| |
| // Release in OPEN_PANEL_DID_END panel |
| final ID delegate = invoke(Foundation.getObjcClass("NSOpenPanelDelegate_"), "new"); |
| ourImplMap.put(delegate, impl); |
| |
| final ID select = toSelect == null ? null : Foundation.nsString(toSelect); |
| JDK7WindowReorderingWorkaround.disableReordering(); |
| invoke(delegate, "performSelectorOnMainThread:withObject:waitUntilDone:", Foundation.createSelector("showOpenPanel:"), select, false); |
| } |
| |
| private static ID invoke(@NotNull final String className, @NotNull final String selector, Object... args) { |
| return invoke(Foundation.getObjcClass(className), selector, args); |
| } |
| |
| private static ID invoke(@NotNull final ID id, @NotNull final String selector, Object... args) { |
| return Foundation.invoke(id, Foundation.createSelector(selector), args); |
| } |
| |
| /** This class is intended to force extensions initialization on EDT thread (IDEA-107271) |
| */ |
| private static class ExtensionsInitializer { |
| private ExtensionsInitializer() {} |
| private static boolean initialized; |
| private static void initialize () { |
| if (initialized) return; |
| UIUtil.invokeAndWaitIfNeeded(new Runnable() { |
| @Override |
| public void run() { |
| Extensions.getExtensions(ProjectOpenProcessor.EXTENSION_POINT_NAME); |
| } |
| }); |
| initialized = true; |
| } |
| } |
| |
| } |