add colored speed search for AbstractFileTreeTable
[idea/community.git] / platform / lang-impl / src / com / intellij / util / ui / tree / AbstractFileTreeTable.java
1 /*
2  * Copyright 2000-2016 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
17 package com.intellij.util.ui.tree;
18
19 import com.intellij.icons.AllIcons;
20 import com.intellij.ide.CommonActionsManager;
21 import com.intellij.ide.DefaultTreeExpander;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.roots.ProjectFileIndex;
24 import com.intellij.openapi.roots.ProjectRootManager;
25 import com.intellij.openapi.ui.Messages;
26 import com.intellij.openapi.util.Comparing;
27 import com.intellij.openapi.vfs.VfsUtilCore;
28 import com.intellij.openapi.vfs.VirtualFile;
29 import com.intellij.openapi.vfs.VirtualFileFilter;
30 import com.intellij.ui.SimpleColoredComponent;
31 import com.intellij.ui.TableUtil;
32 import com.intellij.ui.TreeTableSpeedSearch;
33 import com.intellij.ui.speedSearch.SpeedSearchUtil;
34 import com.intellij.ui.treeStructure.treetable.TreeTable;
35 import com.intellij.ui.treeStructure.treetable.TreeTableCellRenderer;
36 import com.intellij.ui.treeStructure.treetable.TreeTableModel;
37 import com.intellij.util.IconUtil;
38 import com.intellij.util.PlatformIcons;
39 import com.intellij.util.containers.Convertor;
40 import com.intellij.util.containers.HashMap;
41 import com.intellij.util.ui.UIUtil;
42 import gnu.trove.THashMap;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
45
46 import javax.swing.*;
47 import javax.swing.table.TableColumn;
48 import javax.swing.tree.*;
49 import java.awt.*;
50 import java.util.*;
51 import java.util.List;
52
53 public class AbstractFileTreeTable<T> extends TreeTable {
54   private final MyModel<T> myModel;
55   private final Project myProject;
56
57   public AbstractFileTreeTable(@NotNull Project project,
58                                @NotNull Class<T> valueClass,
59                                @NotNull String valueTitle,
60                                @NotNull VirtualFileFilter filter,
61                                boolean showProjectNode) {
62     this(project, valueClass, valueTitle, filter, showProjectNode, true);
63   }
64
65   /**
66    * Due to historical reasons, passed filter does not perform all jobs - fileIndex.isInContent is checked in addition.
67    * Flag showContentFilesOnly allows you to disable such behavior.
68    */
69   public AbstractFileTreeTable(@NotNull Project project,
70                                @NotNull Class<T> valueClass,
71                                @NotNull String valueTitle,
72                                @NotNull VirtualFileFilter filter,
73                                boolean showProjectNode,
74                                boolean showContentFilesOnly) {
75     super(new MyModel<T>(project, valueClass, valueTitle, showContentFilesOnly ? new ProjectContentFileFilter(project, filter) : filter));
76     myProject = project;
77
78     //noinspection unchecked
79     myModel = (MyModel)getTableModel();
80     myModel.setTreeTable(this);
81
82     new TreeTableSpeedSearch(this, new Convertor<TreePath, String>() {
83       @Override
84       public String convert(final TreePath o) {
85         final DefaultMutableTreeNode node = (DefaultMutableTreeNode)o.getLastPathComponent();
86         final Object userObject = node.getUserObject();
87         if (userObject == null) {
88           return getProjectNodeText();
89         }
90         if (userObject instanceof VirtualFile) {
91           return ((VirtualFile)userObject).getName();
92         }
93         return node.toString();
94       }
95     });
96     final DefaultTreeExpander treeExpander = new DefaultTreeExpander(getTree());
97     CommonActionsManager.getInstance().createExpandAllAction(treeExpander, this);
98     CommonActionsManager.getInstance().createCollapseAllAction(treeExpander, this);
99
100     getTree().setShowsRootHandles(true);
101     getTree().setLineStyleAngled();
102     getTree().setRootVisible(showProjectNode);
103     final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
104     getTree().setCellRenderer(new DefaultTreeCellRenderer() {
105       @Override
106       public Component getTreeCellRendererComponent(JTree tree, 
107                                                     Object value, 
108                                                     boolean sel, 
109                                                     boolean expanded,
110                                                     boolean leaf, 
111                                                     int row, 
112                                                     boolean hasFocus) {
113         SimpleColoredComponent c = new SimpleColoredComponent();
114         if (value instanceof ProjectRootNode) {
115           c.append(getProjectNodeText());
116           c.setIcon(AllIcons.Nodes.Project);
117         }
118         else {
119           FileNode fileNode = (FileNode)value;
120           VirtualFile file = fileNode.getObject();
121           c.append(fileNode.getParent() instanceof FileNode ? file.getName() : file.getPresentableUrl());
122           Icon icon = file.isDirectory() 
123                       ? fileIndex.isExcluded(file) ? AllIcons.Modules.ExcludeRoot
124                                                    : PlatformIcons.DIRECTORY_CLOSED_ICON : IconUtil.getIcon(file, 0, null);
125           c.setIcon(icon);
126         }
127         SpeedSearchUtil.applySpeedSearchHighlighting(AbstractFileTreeTable.this, c, false, selected);
128         return c;
129       }
130     });
131     getTableHeader().setReorderingAllowed(false);
132
133     setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
134     setPreferredScrollableViewportSize(new Dimension(300, getRowHeight() * 10));
135
136     getColumnModel().getColumn(0).setPreferredWidth(280);
137     getColumnModel().getColumn(1).setPreferredWidth(60);
138   }
139
140   protected boolean isNullObject(final T value) {
141     return false;
142   }
143
144   private static String getProjectNodeText() {
145     return "Project";
146   }
147
148   public Project getProject() {
149     return myProject;
150   }
151
152   public TableColumn getValueColumn() {
153     return getColumnModel().getColumn(1);
154   }
155
156   protected boolean isValueEditableForFile(final VirtualFile virtualFile) {
157     return true;
158   }
159
160   public static void press(final Container comboComponent) {
161     if (comboComponent instanceof JButton) {
162       ((JButton)comboComponent).doClick();
163     }
164     else {
165       for (int i = 0; i < comboComponent.getComponentCount(); i++) {
166         Component child = comboComponent.getComponent(i);
167         if (child instanceof Container) {
168           press((Container)child);
169         }
170       }
171     }
172   }
173
174   public boolean clearSubdirectoriesOnDemandOrCancel(final VirtualFile parent, final String message, final String title) {
175     Map<VirtualFile, T> mappings = myModel.myCurrentMapping;
176     Map<VirtualFile, T> subdirectoryMappings = new THashMap<VirtualFile, T>();
177     for (VirtualFile file : mappings.keySet()) {
178       if (file != null && (parent == null || VfsUtilCore.isAncestor(parent, file, true))) {
179         subdirectoryMappings.put(file, mappings.get(file));
180       }
181     }
182     if (subdirectoryMappings.isEmpty()) {
183       return true;
184     }
185     int ret = Messages.showYesNoCancelDialog(myProject, message, title, "Override", "Do Not Override", "Cancel",
186                                              Messages.getWarningIcon());
187     if (ret == Messages.YES) {
188       for (VirtualFile file : subdirectoryMappings.keySet()) {
189         myModel.setValueAt(null, new DefaultMutableTreeNode(file), 1);
190       }
191     }
192     return ret != Messages.CANCEL;
193   }
194
195   @NotNull
196   public Map<VirtualFile, T> getValues() {
197     return myModel.getValues();
198   }
199
200   @Override
201   public TreeTableCellRenderer createTableRenderer(TreeTableModel treeTableModel) {
202     TreeTableCellRenderer tableRenderer = super.createTableRenderer(treeTableModel);
203     UIUtil.setLineStyleAngled(tableRenderer);
204     tableRenderer.setRootVisible(false);
205     tableRenderer.setShowsRootHandles(true);
206
207     return tableRenderer;
208   }
209
210   public void reset(@NotNull Map<VirtualFile, T> mappings) {
211     myModel.reset(mappings);
212     myModel.nodeChanged((TreeNode)myModel.getRoot());
213     getTree().setModel(null);
214     getTree().setModel(myModel);
215     TreeUtil.expandRootChildIfOnlyOne(getTree());
216   }
217
218   public void select(@Nullable final VirtualFile toSelect) {
219     if (toSelect != null) {
220       select(toSelect, (TreeNode)myModel.getRoot());
221     }
222   }
223
224   private void select(@NotNull VirtualFile toSelect, final TreeNode root) {
225     for (int i = 0; i < root.getChildCount(); i++) {
226       TreeNode child = root.getChildAt(i);
227       VirtualFile file = ((FileNode)child).getObject();
228       if (VfsUtilCore.isAncestor(file, toSelect, false)) {
229         if (Comparing.equal(file, toSelect)) {
230           TreeUtil.selectNode(getTree(), child);
231           getSelectionModel().clearSelection();
232           addSelectedPath(TreeUtil.getPathFromRoot(child));
233           TableUtil.scrollSelectionToVisible(this);
234         }
235         else {
236           select(toSelect, child);
237         }
238         return;
239       }
240     }
241   }
242
243   private static class MyModel<T> extends DefaultTreeModel implements TreeTableModel {
244     private final Map<VirtualFile, T> myCurrentMapping = new HashMap<VirtualFile, T>();
245     private final Class<T> myValueClass;
246     private final String myValueTitle;
247     private AbstractFileTreeTable<T> myTreeTable;
248
249     private MyModel(@NotNull Project project, @NotNull Class<T> valueClass, @NotNull String valueTitle, @NotNull VirtualFileFilter filter) {
250       super(new ProjectRootNode(project, filter));
251       myValueClass = valueClass;
252       myValueTitle = valueTitle;
253     }
254
255     private Map<VirtualFile, T> getValues() {
256       return new HashMap<VirtualFile, T>(myCurrentMapping);
257     }
258
259     @Override
260     public void setTree(JTree tree) {
261     }
262
263     @Override
264     public int getColumnCount() {
265       return 2;
266     }
267
268     @Override
269     public String getColumnName(final int column) {
270       switch (column) {
271         case 0:
272           return "File/Directory";
273         case 1:
274           return myValueTitle;
275         default:
276           throw new RuntimeException("invalid column " + column);
277       }
278     }
279
280     @Override
281     public Class getColumnClass(final int column) {
282       switch (column) {
283         case 0:
284           return TreeTableModel.class;
285         case 1:
286           return myValueClass;
287         default:
288           throw new RuntimeException("invalid column " + column);
289       }
290     }
291
292     @Override
293     public Object getValueAt(final Object node, final int column) {
294       Object userObject = ((DefaultMutableTreeNode)node).getUserObject();
295       if (userObject instanceof Project) {
296         switch (column) {
297           case 0:
298             return userObject;
299           case 1:
300             return myCurrentMapping.get(null);
301         }
302       }
303       VirtualFile file = (VirtualFile)userObject;
304       switch (column) {
305         case 0:
306           return file;
307         case 1:
308           return myCurrentMapping.get(file);
309         default:
310           throw new RuntimeException("invalid column " + column);
311       }
312     }
313
314     @Override
315     public boolean isCellEditable(final Object node, final int column) {
316       switch (column) {
317         case 0:
318           return false;
319         case 1:
320           final Object userObject = ((DefaultMutableTreeNode)node).getUserObject();
321           return !(userObject instanceof VirtualFile || userObject == null) || myTreeTable.isValueEditableForFile((VirtualFile)userObject);
322         default:
323           throw new RuntimeException("invalid column " + column);
324       }
325     }
326
327     @Override
328     public void setValueAt(final Object aValue, final Object node, final int column) {
329       final Object userObject = ((DefaultMutableTreeNode)node).getUserObject();
330       if (userObject instanceof Project) {
331         return;
332       }
333
334       final VirtualFile file = (VirtualFile)userObject;
335       @SuppressWarnings("unchecked")
336       T t = (T)aValue;
337       if (t == null || myTreeTable.isNullObject(t)) {
338         myCurrentMapping.remove(file);
339       }
340       else {
341         myCurrentMapping.put(file, t);
342       }
343       fireTreeNodesChanged(this, new Object[]{getRoot()}, null, null);
344     }
345
346     public void reset(@NotNull Map<VirtualFile, T> mappings) {
347       myCurrentMapping.clear();
348       myCurrentMapping.putAll(mappings);
349       ((ProjectRootNode)getRoot()).clearCachedChildren();
350     }
351
352     void setTreeTable(final AbstractFileTreeTable<T> treeTable) {
353       myTreeTable = treeTable;
354     }
355   }
356
357   public static class ProjectRootNode extends ConvenientNode<Project> {
358     private final VirtualFileFilter myFilter;
359
360     public ProjectRootNode(@NotNull Project project) {
361       this(project, VirtualFileFilter.ALL);
362     }
363
364     public ProjectRootNode(@NotNull Project project, @NotNull VirtualFileFilter filter) {
365       super(project);
366       myFilter = filter;
367     }
368
369     @Override
370     protected void appendChildrenTo(@NotNull final Collection<ConvenientNode> children) {
371       Project project = getObject();
372       VirtualFile[] roots = ProjectRootManager.getInstance(project).getContentRoots();
373
374       NextRoot:
375       for (VirtualFile root : roots) {
376         for (VirtualFile candidate : roots) {
377           if (VfsUtilCore.isAncestor(candidate, root, true)) continue NextRoot;
378         }
379         if (myFilter.accept(root)) {
380           children.add(new FileNode(root, project, myFilter));
381         }
382       }
383     }
384   }
385
386   public abstract static class ConvenientNode<T> extends DefaultMutableTreeNode {
387     private final T myObject;
388
389     private ConvenientNode(T object) {
390       myObject = object;
391     }
392
393     public T getObject() {
394       return myObject;
395     }
396
397     protected abstract void appendChildrenTo(@NotNull Collection<ConvenientNode> children);
398
399     @Override
400     public int getChildCount() {
401       init();
402       return super.getChildCount();
403     }
404
405     @Override
406     public TreeNode getChildAt(final int childIndex) {
407       init();
408       return super.getChildAt(childIndex);
409     }
410
411     @Override
412     public Enumeration children() {
413       init();
414       return super.children();
415     }
416
417     private void init() {
418       if (getUserObject() == null) {
419         setUserObject(myObject);
420         final List<ConvenientNode> children = new ArrayList<ConvenientNode>();
421         appendChildrenTo(children);
422         Collections.sort(children, new Comparator<ConvenientNode>() {
423           @Override
424           public int compare(final ConvenientNode node1, final ConvenientNode node2) {
425             Object o1 = node1.getObject();
426             Object o2 = node2.getObject();
427             if (o1 == o2) return 0;
428             if (o1 instanceof Project) return -1;
429             if (o2 instanceof Project) return 1;
430             VirtualFile file1 = (VirtualFile)o1;
431             VirtualFile file2 = (VirtualFile)o2;
432             if (file1.isDirectory() != file2.isDirectory()) {
433               return file1.isDirectory() ? -1 : 1;
434             }
435             return file1.getName().compareTo(file2.getName());
436           }
437         });
438         int i = 0;
439         for (ConvenientNode child : children) {
440           insert(child, i++);
441         }
442       }
443     }
444
445     public void clearCachedChildren() {
446       if (children != null) {
447         for (Object child : children) {
448           //noinspection unchecked
449           ((ConvenientNode<T>)child).clearCachedChildren();
450         }
451       }
452       removeAllChildren();
453       setUserObject(null);
454     }
455   }
456
457   public static class FileNode extends ConvenientNode<VirtualFile> {
458     private final Project myProject;
459     private final VirtualFileFilter myFilter;
460
461     public FileNode(@NotNull VirtualFile file, @NotNull final Project project) {
462       this(file, project, VirtualFileFilter.ALL);
463     }
464
465     public FileNode(@NotNull VirtualFile file, @NotNull final Project project, @NotNull VirtualFileFilter filter) {
466       super(file);
467       myProject = project;
468       myFilter = filter;
469     }
470
471     @Override
472     protected void appendChildrenTo(@NotNull final Collection<ConvenientNode> children) {
473       for (VirtualFile child : getObject().getChildren()) {
474         if (myFilter.accept(child)) {
475           children.add(new FileNode(child, myProject, myFilter));
476         }
477       }
478     }
479   }
480 }