2 * Copyright 2000-2014 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.openapi.fileChooser.ex;
18 import com.intellij.icons.AllIcons;
19 import com.intellij.ide.IdeEventQueue;
20 import com.intellij.ide.SaveAndSyncHandlerImpl;
21 import com.intellij.ide.util.PropertiesComponent;
22 import com.intellij.ide.util.treeView.NodeRenderer;
23 import com.intellij.openapi.actionSystem.*;
24 import com.intellij.openapi.application.ApplicationActivationListener;
25 import com.intellij.openapi.application.ApplicationManager;
26 import com.intellij.openapi.application.ModalityState;
27 import com.intellij.openapi.fileChooser.*;
28 import com.intellij.openapi.fileChooser.impl.FileChooserFactoryImpl;
29 import com.intellij.openapi.fileChooser.impl.FileChooserUtil;
30 import com.intellij.openapi.project.Project;
31 import com.intellij.openapi.ui.DialogWrapper;
32 import com.intellij.openapi.ui.Messages;
33 import com.intellij.openapi.ui.popup.JBPopupFactory;
34 import com.intellij.openapi.util.Disposer;
35 import com.intellij.openapi.util.Iconable;
36 import com.intellij.openapi.util.text.StringUtil;
37 import com.intellij.openapi.vfs.LocalFileSystem;
38 import com.intellij.openapi.vfs.VfsUtil;
39 import com.intellij.openapi.vfs.VfsUtilCore;
40 import com.intellij.openapi.vfs.VirtualFile;
41 import com.intellij.openapi.wm.IdeFrame;
42 import com.intellij.ui.*;
43 import com.intellij.ui.components.JBList;
44 import com.intellij.ui.components.labels.LinkLabel;
45 import com.intellij.util.ArrayUtil;
46 import com.intellij.util.Consumer;
47 import com.intellij.util.IconUtil;
48 import com.intellij.util.containers.HashMap;
49 import com.intellij.util.ui.UIUtil;
50 import com.intellij.util.ui.update.MergingUpdateQueue;
51 import com.intellij.util.ui.update.UiNotifyConnector;
52 import com.intellij.util.ui.update.Update;
53 import org.jetbrains.annotations.NonNls;
54 import org.jetbrains.annotations.NotNull;
55 import org.jetbrains.annotations.Nullable;
58 import javax.swing.border.EmptyBorder;
59 import javax.swing.event.TreeExpansionEvent;
60 import javax.swing.event.TreeExpansionListener;
61 import javax.swing.event.TreeSelectionEvent;
62 import javax.swing.event.TreeSelectionListener;
63 import javax.swing.tree.DefaultMutableTreeNode;
64 import javax.swing.tree.TreePath;
66 import java.awt.event.KeyEvent;
67 import java.awt.event.MouseEvent;
69 import java.util.ArrayList;
70 import java.util.Arrays;
71 import java.util.List;
74 public class FileChooserDialogImpl extends DialogWrapper implements FileChooserDialog, PathChooserDialog, FileLookup {
75 @NonNls public static final String FILE_CHOOSER_SHOW_PATH_PROPERTY = "FileChooser.ShowPath";
76 public static final String RECENT_FILES_KEY = "file.chooser.recent.files";
77 private final FileChooserDescriptor myChooserDescriptor;
78 protected FileSystemTreeImpl myFileSystemTree;
79 private Project myProject;
80 private VirtualFile[] myChosenFiles = VirtualFile.EMPTY_ARRAY;
82 private JPanel myNorthPanel;
83 private TextFieldAction myTextFieldAction;
84 protected FileTextFieldImpl myPathTextField;
85 private JComponent myPathTextFieldWrapper;
87 private MergingUpdateQueue myUiUpdater;
88 private boolean myTreeIsUpdating;
90 public static DataKey<PathField> PATH_FIELD = DataKey.create("PathField");
92 public FileChooserDialogImpl(@NotNull final FileChooserDescriptor descriptor, @Nullable Project project) {
94 myChooserDescriptor = descriptor;
96 setTitle(getChooserTitle(descriptor));
99 public FileChooserDialogImpl(@NotNull final FileChooserDescriptor descriptor, @NotNull Component parent) {
100 this(descriptor, parent, null);
103 public FileChooserDialogImpl(@NotNull final FileChooserDescriptor descriptor, @NotNull Component parent, @Nullable Project project) {
105 myChooserDescriptor = descriptor;
107 setTitle(getChooserTitle(descriptor));
110 private static String getChooserTitle(final FileChooserDescriptor descriptor) {
111 final String title = descriptor.getTitle();
112 return title != null ? title : UIBundle.message("file.chooser.default.title");
117 public VirtualFile[] choose(@Nullable final Project project, @NotNull final VirtualFile... toSelect) {
119 if ((myProject == null) && (project != null)) {
122 if (toSelect.length == 1) {
123 restoreSelection(toSelect[0]);
126 selectInTree(toSelect, true);
130 return myChosenFiles;
136 public VirtualFile[] choose(@Nullable final VirtualFile toSelect, @Nullable final Project project) {
137 return choose(project, toSelect);
141 public void choose(@Nullable VirtualFile toSelect, @NotNull Consumer<List<VirtualFile>> callback) {
143 restoreSelection(toSelect);
145 if (myChosenFiles.length > 0) {
146 callback.consume(Arrays.asList(myChosenFiles));
148 else if (callback instanceof FileChooser.FileChooserConsumer) {
149 ((FileChooser.FileChooserConsumer)callback).cancelled();
153 protected void restoreSelection(@Nullable VirtualFile toSelect) {
154 final VirtualFile lastOpenedFile = FileChooserUtil.getLastOpenedFile(myProject);
155 final VirtualFile file = FileChooserUtil.getFileToSelect(myChooserDescriptor, myProject, toSelect, lastOpenedFile);
157 if (file != null && file.isValid()) {
158 myFileSystemTree.select(file, new Runnable() {
160 if (!file.equals(myFileSystemTree.getSelectedFile())) {
161 VirtualFile parent = file.getParent();
162 if (parent != null) {
163 myFileSystemTree.select(parent, null);
166 else if (file.isDirectory()) {
167 myFileSystemTree.expand(file, null);
174 protected void storeSelection(@Nullable VirtualFile file) {
175 FileChooserUtil.setLastOpenedFile(myProject, file);
176 if (file != null && file.getFileSystem() instanceof LocalFileSystem) {
177 saveRecent(file.getPath());
181 protected void saveRecent(String path) {
182 final List<String> files = new ArrayList<String>(Arrays.asList(getRecentFiles()));
185 while (files.size() > 30) {
186 files.remove(files.size() - 1);
188 PropertiesComponent.getInstance().setValues(RECENT_FILES_KEY, ArrayUtil.toStringArray(files));
192 private String[] getRecentFiles() {
193 final String[] recent = PropertiesComponent.getInstance().getValues(RECENT_FILES_KEY);
194 if (recent != null) {
195 if (recent.length > 0 && myPathTextField.getField().getText().replace('\\', '/').equals(recent[0])) {
196 final String[] pathes = new String[recent.length - 1];
197 System.arraycopy(recent, 1, pathes, 0, recent.length - 1);
202 return ArrayUtil.EMPTY_STRING_ARRAY;
206 protected JComponent createHistoryButton() {
207 JLabel label = new JLabel(AllIcons.Actions.Get);
208 label.setToolTipText("Recent files");
209 new ClickListener() {
211 public boolean onClick(@NotNull MouseEvent event, int clickCount) {
212 showRecentFilesPopup();
219 public void actionPerformed(AnActionEvent e) {
220 showRecentFilesPopup();
224 public void update(AnActionEvent e) {
225 e.getPresentation().setEnabled(!IdeEventQueue.getInstance().isPopupActive());
227 }.registerCustomShortcutSet(KeyEvent.VK_DOWN, 0, myPathTextField.getField());
231 private void showRecentFilesPopup() {
232 final JBList files = new JBList(getRecentFiles()) {
234 public Dimension getPreferredSize() {
235 return new Dimension(myPathTextField.getField().getWidth(), super.getPreferredSize().height);
238 files.setCellRenderer(new ColoredListCellRenderer() {
240 protected void customizeCellRenderer(JList list, Object value, int index, boolean selected, boolean hasFocus) {
241 final String path = value.toString();
243 final VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(path));
245 setIcon(IconUtil.getIcon(file, Iconable.ICON_FLAG_READ_STATUS, null));
249 JBPopupFactory.getInstance()
250 .createListPopupBuilder(files)
251 .setItemChoosenCallback(new Runnable() {
254 myPathTextField.getField().setText(files.getSelectedValue().toString());
256 }).createPopup().showUnderneathOf(myPathTextField.getField());
260 protected DefaultActionGroup createActionGroup() {
261 registerFileChooserShortcut(IdeActions.ACTION_DELETE, "FileChooser.Delete");
262 registerFileChooserShortcut(IdeActions.ACTION_SYNCHRONIZE, "FileChooser.Refresh");
264 return (DefaultActionGroup)ActionManager.getInstance().getAction("FileChooserToolbar");
267 private void registerFileChooserShortcut(@NonNls final String baseActionId, @NonNls final String fileChooserActionId) {
268 final JTree tree = myFileSystemTree.getTree();
269 final AnAction syncAction = ActionManager.getInstance().getAction(fileChooserActionId);
271 AnAction original = ActionManager.getInstance().getAction(baseActionId);
272 syncAction.registerCustomShortcutSet(original.getShortcutSet(), tree, myDisposable);
276 protected final JComponent createTitlePane() {
277 final String description = myChooserDescriptor.getDescription();
278 if (StringUtil.isEmptyOrSpaces(description)) return null;
280 final JLabel label = new JLabel(description);
281 label.setBorder(BorderFactory.createCompoundBorder(
282 new SideBorder(UIUtil.getPanelBackground().darker(), SideBorder.BOTTOM),
283 BorderFactory.createEmptyBorder(0, 5, 10, 5)));
287 protected JComponent createCenterPanel() {
288 JPanel panel = new MyPanel();
290 myUiUpdater = new MergingUpdateQueue("FileChooserUpdater", 200, false, panel);
291 Disposer.register(myDisposable, myUiUpdater);
292 new UiNotifyConnector(panel, myUiUpdater);
294 panel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
298 final DefaultActionGroup group = createActionGroup();
299 ActionToolbar toolBar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, group, true);
300 toolBar.setTargetComponent(panel);
302 final JPanel toolbarPanel = new JPanel(new BorderLayout());
303 toolbarPanel.add(toolBar.getComponent(), BorderLayout.CENTER);
305 myTextFieldAction = new TextFieldAction() {
306 public void linkSelected(final LinkLabel aSource, final Object aLinkData) {
307 toggleShowTextField();
310 toolbarPanel.add(myTextFieldAction, BorderLayout.EAST);
312 myPathTextFieldWrapper = new JPanel(new BorderLayout());
313 myPathTextFieldWrapper.setBorder(new EmptyBorder(0, 0, 2, 0));
314 myPathTextField = new FileTextFieldImpl.Vfs(
315 FileChooserFactoryImpl.getMacroMap(), getDisposable(),
316 new LocalFsFinder.FileChooserFilter(myChooserDescriptor, myFileSystemTree)) {
317 protected void onTextChanged(final String newValue) {
318 myUiUpdater.cancelAllUpdates();
319 updateTreeFromPath(newValue);
322 Disposer.register(myDisposable, myPathTextField);
323 myPathTextFieldWrapper.add(myPathTextField.getField(), BorderLayout.CENTER);
324 if (getRecentFiles().length > 0) {
325 myPathTextFieldWrapper.add(createHistoryButton(), BorderLayout.EAST);
328 myNorthPanel = new JPanel(new BorderLayout());
329 myNorthPanel.add(toolbarPanel, BorderLayout.NORTH);
332 updateTextFieldShowing();
334 panel.add(myNorthPanel, BorderLayout.NORTH);
336 registerMouseListener(group);
338 JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myFileSystemTree.getTree());
339 //scrollPane.setBorder(BorderFactory.createLineBorder(new Color(148, 154, 156)));
340 panel.add(scrollPane, BorderLayout.CENTER);
341 panel.setPreferredSize(new Dimension(400, 400));
344 panel.add(new JLabel(
345 "<html><center><small><font color=gray>Drag and drop a file into the space above to quickly locate it in the tree.</font></small></center></html>",
346 SwingConstants.CENTER), BorderLayout.SOUTH);
349 ApplicationManager.getApplication().getMessageBus().connect(getDisposable())
350 .subscribe(ApplicationActivationListener.TOPIC, new ApplicationActivationListener.Adapter() {
352 public void applicationActivated(IdeFrame ideFrame) {
353 SaveAndSyncHandlerImpl.getInstance().maybeRefresh(ModalityState.current());
361 public JComponent getPreferredFocusedComponent() {
362 if (isToShowTextField()) {
363 return myPathTextField != null ? myPathTextField.getField() : null;
366 return myFileSystemTree != null ? myFileSystemTree.getTree() : null;
370 public final void dispose() {
371 LocalFileSystem.getInstance().removeWatchedRoots(myRequests.values());
375 private boolean isTextFieldActive() {
376 return myPathTextField.getField().getRootPane() != null;
379 protected void doOKAction() {
380 if (!isOKActionEnabled()) {
384 if (isTextFieldActive()) {
385 final String text = myPathTextField.getTextFieldText();
386 final LookupFile file = myPathTextField.getFile();
387 if (text == null || file == null || !file.exists()) {
388 setErrorText("Specified path cannot be found");
393 final List<VirtualFile> selectedFiles = Arrays.asList(getSelectedFilesInt());
394 final VirtualFile[] files = VfsUtilCore.toVirtualFileArray(FileChooserUtil.getChosenFiles(myChooserDescriptor, selectedFiles));
395 if (files.length == 0) {
396 myChosenFiles = VirtualFile.EMPTY_ARRAY;
397 close(CANCEL_EXIT_CODE);
402 myChooserDescriptor.validateSelectedFiles(files);
404 catch (Exception e) {
405 Messages.showErrorDialog(getContentPane(), e.getMessage(), getTitle());
409 myChosenFiles = files;
410 storeSelection(files[files.length - 1]);
415 public final void doCancelAction() {
416 myChosenFiles = VirtualFile.EMPTY_ARRAY;
417 super.doCancelAction();
420 protected JTree createTree() {
421 myFileSystemTree = new FileSystemTreeImpl(myProject, myChooserDescriptor);
422 Disposer.register(myDisposable, myFileSystemTree);
424 myFileSystemTree.addOkAction(new Runnable() {
429 JTree tree = myFileSystemTree.getTree();
430 tree.setCellRenderer(new NodeRenderer());
431 tree.getSelectionModel().addTreeSelectionListener(new FileTreeSelectionListener());
432 tree.addTreeExpansionListener(new FileTreeExpansionListener());
433 setOKActionEnabled(false);
435 myFileSystemTree.addListener(new FileSystemTree.Listener() {
436 public void selectionChanged(final List<VirtualFile> selection) {
437 updatePathFromTree(selection, false);
441 new FileDrop(tree, new FileDrop.Target() {
442 public FileChooserDescriptor getDescriptor() {
443 return myChooserDescriptor;
446 public boolean isHiddenShown() {
447 return myFileSystemTree.areHiddensShown();
450 public void dropFiles(final List<VirtualFile> files) {
451 if (!myChooserDescriptor.isChooseMultiple() && files.size() > 0) {
452 selectInTree(new VirtualFile[]{files.get(0)}, true);
455 selectInTree(VfsUtilCore.toVirtualFileArray(files), true);
463 protected final void registerMouseListener(final ActionGroup group) {
464 myFileSystemTree.registerMouseListener(group);
467 private VirtualFile[] getSelectedFilesInt() {
468 if (myTreeIsUpdating || !myUiUpdater.isEmpty()) {
469 if (isTextFieldActive() && !StringUtil.isEmpty(myPathTextField.getTextFieldText())) {
470 LookupFile toFind = myPathTextField.getFile();
471 if (toFind instanceof LocalFsFinder.VfsFile && toFind.exists()) {
472 VirtualFile file = ((LocalFsFinder.VfsFile)toFind).getFile();
474 return new VirtualFile[]{file};
478 return VirtualFile.EMPTY_ARRAY;
481 return myFileSystemTree.getSelectedFiles();
484 private final Map<String, LocalFileSystem.WatchRequest> myRequests = new HashMap<String, LocalFileSystem.WatchRequest>();
486 private static boolean isToShowTextField() {
487 return PropertiesComponent.getInstance().getBoolean(FILE_CHOOSER_SHOW_PATH_PROPERTY, true);
490 private static void setToShowTextField(boolean toShowTextField) {
491 PropertiesComponent.getInstance().setValue(FILE_CHOOSER_SHOW_PATH_PROPERTY, Boolean.toString(toShowTextField));
494 private final class FileTreeExpansionListener implements TreeExpansionListener {
495 public void treeExpanded(TreeExpansionEvent event) {
496 final Object[] path = event.getPath().getPath();
497 if (path.length == 2) {
498 // top node has been expanded => watch disk recursively
499 final DefaultMutableTreeNode node = (DefaultMutableTreeNode)path[1];
500 Object userObject = node.getUserObject();
501 if (userObject instanceof FileNodeDescriptor) {
502 final VirtualFile file = ((FileNodeDescriptor)userObject).getElement().getFile();
503 if (file != null && file.isDirectory()) {
504 final String rootPath = file.getPath();
505 if (myRequests.get(rootPath) == null) {
506 final LocalFileSystem.WatchRequest watchRequest = LocalFileSystem.getInstance().addRootToWatch(rootPath, true);
507 myRequests.put(rootPath, watchRequest);
514 public void treeCollapsed(TreeExpansionEvent event) {
518 private final class FileTreeSelectionListener implements TreeSelectionListener {
519 public void valueChanged(TreeSelectionEvent e) {
520 TreePath[] paths = e.getPaths();
522 boolean enabled = true;
523 for (TreePath treePath : paths) {
524 if (!e.isAddedPath(treePath)) {
527 DefaultMutableTreeNode node = (DefaultMutableTreeNode)treePath.getLastPathComponent();
528 Object userObject = node.getUserObject();
529 if (!(userObject instanceof FileNodeDescriptor)) {
533 FileElement descriptor = ((FileNodeDescriptor)userObject).getElement();
534 VirtualFile file = descriptor.getFile();
535 enabled = file != null && myChooserDescriptor.isFileSelectable(file);
537 setOKActionEnabled(enabled);
541 protected final class MyPanel extends JPanel implements DataProvider {
543 super(new BorderLayout(0, 0));
546 public Object getData(String dataId) {
547 if (CommonDataKeys.VIRTUAL_FILE_ARRAY.is(dataId)) {
548 return myFileSystemTree.getSelectedFiles();
550 else if (PATH_FIELD.is(dataId)) {
551 return new PathField() {
552 public void toggleVisible() {
553 toggleShowTextField();
557 else if (FileSystemTree.DATA_KEY.is(dataId)) {
558 return myFileSystemTree;
560 return myChooserDescriptor.getUserData(dataId);
564 public void toggleShowTextField() {
565 setToShowTextField(!isToShowTextField());
566 updateTextFieldShowing();
569 private void updateTextFieldShowing() {
570 myTextFieldAction.update();
571 myNorthPanel.remove(myPathTextFieldWrapper);
572 if (isToShowTextField()) {
573 final ArrayList<VirtualFile> selection = new ArrayList<VirtualFile>();
574 if (myFileSystemTree.getSelectedFile() != null) {
575 selection.add(myFileSystemTree.getSelectedFile());
577 updatePathFromTree(selection, true);
578 myNorthPanel.add(myPathTextFieldWrapper, BorderLayout.CENTER);
583 myPathTextField.getField().requestFocus();
585 myNorthPanel.revalidate();
586 myNorthPanel.repaint();
590 private void updatePathFromTree(final List<VirtualFile> selection, boolean now) {
591 if (!isToShowTextField() || myTreeIsUpdating) return;
594 if (selection.size() > 0) {
595 text = VfsUtil.getReadableUrl(selection.get(0));
598 final List<VirtualFile> roots = myChooserDescriptor.getRoots();
599 if (!myFileSystemTree.getTree().isRootVisible() && roots.size() == 1) {
600 text = VfsUtil.getReadableUrl(roots.get(0));
604 myPathTextField.setText(text, now, new Runnable() {
606 myPathTextField.getField().selectAll();
612 private void updateTreeFromPath(final String text) {
613 if (!isToShowTextField()) return;
614 if (myPathTextField.isPathUpdating()) return;
615 if (text == null) return;
617 myUiUpdater.queue(new Update("treeFromPath.1") {
619 ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
621 final LocalFsFinder.VfsFile toFind = (LocalFsFinder.VfsFile)myPathTextField.getFile();
622 if (toFind == null || !toFind.exists()) return;
624 myUiUpdater.queue(new Update("treeFromPath.2") {
626 selectInTree(toFind.getFile(), text);
635 private void selectInTree(final VirtualFile vFile, String fromText) {
636 if (vFile != null && vFile.isValid()) {
637 if (fromText == null || fromText.equalsIgnoreCase(myPathTextField.getTextFieldText())) {
638 selectInTree(new VirtualFile[]{vFile}, false);
642 reportFileNotFound();
646 private void selectInTree(final VirtualFile[] array, final boolean requestFocus) {
647 myTreeIsUpdating = true;
648 final List<VirtualFile> fileList = Arrays.asList(array);
649 if (!Arrays.asList(myFileSystemTree.getSelectedFiles()).containsAll(fileList)) {
650 myFileSystemTree.select(array, new Runnable() {
652 if (!myFileSystemTree.areHiddensShown() && !Arrays.asList(myFileSystemTree.getSelectedFiles()).containsAll(fileList)) {
653 myFileSystemTree.showHiddens(true);
654 selectInTree(array, requestFocus);
658 myTreeIsUpdating = false;
661 //noinspection SSBasedInspection
662 SwingUtilities.invokeLater(new Runnable() {
664 myFileSystemTree.getTree().requestFocus();
672 myTreeIsUpdating = false;
677 private void reportFileNotFound() {
678 myTreeIsUpdating = false;
683 protected String getDimensionServiceKey() {
684 return "FileChooserDialogImpl";
688 protected String getHelpId() {
689 return "select.path.dialog";