IDEA-51543 (Add Quick Search to Commit View popup)
[fedora-idea.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / ui / ChangesTreeList.java
blob31389f408c72d98a43c9b448b3d1d4719c8b2f22
1 /*
2 * Copyright 2000-2009 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.vcs.changes.ui;
18 import com.intellij.ide.util.PropertiesComponent;
19 import com.intellij.openapi.actionSystem.*;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.keymap.KeymapManager;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.util.EmptyRunnable;
24 import com.intellij.openapi.util.IconLoader;
25 import com.intellij.openapi.util.Pair;
26 import com.intellij.openapi.util.SystemInfo;
27 import com.intellij.openapi.vcs.FilePath;
28 import com.intellij.openapi.vcs.FileStatus;
29 import com.intellij.openapi.vcs.FileStatusManager;
30 import com.intellij.openapi.vcs.VcsBundle;
31 import com.intellij.openapi.vcs.changes.Change;
32 import com.intellij.openapi.vcs.changes.ChangesUtil;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.ui.*;
35 import com.intellij.ui.treeStructure.Tree;
36 import com.intellij.ui.treeStructure.actions.CollapseAllAction;
37 import com.intellij.ui.treeStructure.actions.ExpandAllAction;
38 import com.intellij.util.Icons;
39 import com.intellij.util.containers.Convertor;
40 import com.intellij.util.ui.tree.TreeUtil;
41 import org.jetbrains.annotations.NonNls;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
45 import javax.swing.*;
46 import javax.swing.tree.*;
47 import java.awt.*;
48 import java.awt.event.*;
49 import java.io.File;
50 import java.util.*;
51 import java.util.List;
53 /**
54 * @author max
56 public abstract class ChangesTreeList<T> extends JPanel {
57 private final Tree myTree;
58 private final JList myList;
59 protected final Project myProject;
60 private final boolean myShowCheckboxes;
61 private final boolean myHighlightProblems;
62 private boolean myShowFlatten;
64 private final Collection<T> myIncludedChanges;
65 private Runnable myDoubleClickHandler = EmptyRunnable.getInstance();
67 @NonNls private static final String TREE_CARD = "Tree";
68 @NonNls private static final String LIST_CARD = "List";
69 @NonNls private static final String ROOT = "root";
70 private final CardLayout myCards;
72 @NonNls private final static String FLATTEN_OPTION_KEY = "ChangesBrowser.SHOW_FLATTEN";
74 private final Runnable myInclusionListener;
75 @Nullable private final ChangeNodeDecorator myChangeDecorator;
77 public ChangesTreeList(final Project project, Collection<T> initiallyIncluded, final boolean showCheckboxes,
78 final boolean highlightProblems, @Nullable final Runnable inclusionListener, @Nullable final ChangeNodeDecorator decorator) {
79 myProject = project;
80 myShowCheckboxes = showCheckboxes;
81 myHighlightProblems = highlightProblems;
82 myInclusionListener = inclusionListener;
83 myChangeDecorator = decorator;
84 myIncludedChanges = new HashSet<T>(initiallyIncluded);
86 myCards = new CardLayout();
88 setLayout(myCards);
90 final int checkboxWidth = new JCheckBox().getPreferredSize().width;
91 myTree = new Tree(ChangesBrowserNode.create(myProject, ROOT)) {
92 public Dimension getPreferredScrollableViewportSize() {
93 Dimension size = super.getPreferredScrollableViewportSize();
94 size = new Dimension(size.width + 10, size.height);
95 return size;
98 protected void processMouseEvent(MouseEvent e) {
99 if (e.getID() == MouseEvent.MOUSE_PRESSED) {
100 int row = myTree.getRowForLocation(e.getX(), e.getY());
101 if (row >= 0) {
102 final Rectangle baseRect = myTree.getRowBounds(row);
103 baseRect.setSize(checkboxWidth, baseRect.height);
104 if (baseRect.contains(e.getPoint())) {
105 myTree.setSelectionRow(row);
106 toggleSelection();
110 super.processMouseEvent(e);
113 public int getToggleClickCount() {
114 return -1;
118 myTree.setRootVisible(false);
119 myTree.setShowsRootHandles(true);
121 myTree.setCellRenderer(new MyTreeCellRenderer());
122 new TreeSpeedSearch(myTree, new Convertor<TreePath, String>() {
123 public String convert(TreePath o) {
124 ChangesBrowserNode node = (ChangesBrowserNode) o.getLastPathComponent();
125 return node.getTextPresentation();
129 myList = new JList(new DefaultListModel());
130 myList.setVisibleRowCount(10);
132 add(new JScrollPane(myList), LIST_CARD);
133 add(new JScrollPane(myTree), TREE_CARD);
135 new ListSpeedSearch(myList) {
136 protected String getElementText(Object element) {
137 if (element instanceof Change) {
138 return ChangesUtil.getFilePath((Change)element).getName();
140 return super.getElementText(element);
144 myList.setCellRenderer(new MyListCellRenderer());
146 new MyToggleSelectionAction().registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0)), this);
148 registerKeyboardAction(new ActionListener() {
149 public void actionPerformed(ActionEvent e) {
150 includeSelection();
153 }, KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
155 registerKeyboardAction(new ActionListener() {
156 public void actionPerformed(ActionEvent e) {
157 excludeSelection();
159 }, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
161 myList.addMouseListener(new MouseAdapter() {
162 public void mouseClicked(MouseEvent e) {
163 final int idx = myList.locationToIndex(e.getPoint());
164 if (idx >= 0) {
165 final Rectangle baseRect = myList.getCellBounds(idx, idx);
166 baseRect.setSize(checkboxWidth, baseRect.height);
167 if (baseRect.contains(e.getPoint())) {
168 toggleSelection();
169 e.consume();
171 else if (e.getClickCount() == 2) {
172 myDoubleClickHandler.run();
173 e.consume();
179 myTree.addMouseListener(new MouseAdapter() {
180 public void mouseClicked(MouseEvent e) {
181 final int row = myTree.getRowForLocation(e.getPoint().x, e.getPoint().y);
182 if (row >= 0) {
183 final Rectangle baseRect = myTree.getRowBounds(row);
184 baseRect.setSize(checkboxWidth, baseRect.height);
185 if (!baseRect.contains(e.getPoint()) && e.getClickCount() == 2) {
186 myDoubleClickHandler.run();
187 e.consume();
193 setShowFlatten(PropertiesComponent.getInstance(myProject).isTrueValue(FLATTEN_OPTION_KEY));
196 public void setDoubleClickHandler(final Runnable doubleClickHandler) {
197 myDoubleClickHandler = doubleClickHandler;
200 public void installPopupHandler(ActionGroup group) {
201 PopupHandler.installUnknownPopupHandler(myList, group, ActionManager.getInstance());
202 PopupHandler.installUnknownPopupHandler(myTree, group, ActionManager.getInstance());
205 public Dimension getPreferredSize() {
206 return new Dimension(400, 400);
209 public boolean isShowFlatten() {
210 return myShowFlatten;
213 public void setShowFlatten(final boolean showFlatten) {
214 final List<T> wasSelected = getSelectedChanges();
215 myShowFlatten = showFlatten;
216 myCards.show(this, myShowFlatten ? LIST_CARD : TREE_CARD);
217 select(wasSelected);
218 if (myList.hasFocus() || myTree.hasFocus()) {
219 SwingUtilities.invokeLater(new Runnable() {
220 public void run() {
221 requestFocus();
228 public void requestFocus() {
229 if (myShowFlatten) {
230 myList.requestFocus();
232 else {
233 myTree.requestFocus();
237 public void setChangesToDisplay(final List<T> changes) {
238 final DefaultListModel listModel = (DefaultListModel)myList.getModel();
239 final List<T> sortedChanges = new ArrayList<T>(changes);
240 Collections.sort(sortedChanges, new Comparator<T>() {
241 public int compare(final T o1, final T o2) {
242 return TreeModelBuilder.getPathForObject(o1).getName().compareToIgnoreCase(TreeModelBuilder.getPathForObject(o2).getName());
246 listModel.removeAllElements();
247 for (T change : sortedChanges) {
248 listModel.addElement(change);
251 final DefaultTreeModel model = buildTreeModel(changes, myChangeDecorator);
252 myTree.setModel(model);
254 final Runnable runnable = new Runnable() {
255 public void run() {
256 if (myProject.isDisposed()) return;
257 TreeUtil.expandAll(myTree);
259 if (myIncludedChanges.size() > 0) {
260 int listSelection = 0;
261 int count = 0;
262 for (T change : changes) {
263 if (myIncludedChanges.contains(change)) {
264 listSelection = count;
265 break;
267 count++;
270 ChangesBrowserNode root = (ChangesBrowserNode)model.getRoot();
271 Enumeration enumeration = root.depthFirstEnumeration();
273 while (enumeration.hasMoreElements()) {
274 ChangesBrowserNode node = (ChangesBrowserNode)enumeration.nextElement();
275 final CheckboxTree.NodeState state = getNodeStatus(node);
276 if (node != root && state == CheckboxTree.NodeState.CLEAR) {
277 myTree.collapsePath(new TreePath(node.getPath()));
281 enumeration = root.depthFirstEnumeration();
282 int scrollRow = 0;
283 while (enumeration.hasMoreElements()) {
284 ChangesBrowserNode node = (ChangesBrowserNode)enumeration.nextElement();
285 final CheckboxTree.NodeState state = getNodeStatus(node);
286 if (state == CheckboxTree.NodeState.FULL && node.isLeaf()) {
287 scrollRow = myTree.getRowForPath(new TreePath(node.getPath()));
288 break;
292 if (changes.size() > 0) {
293 myList.setSelectedIndex(listSelection);
294 myList.ensureIndexIsVisible(listSelection);
296 myTree.setSelectionRow(scrollRow);
297 TreeUtil.showRowCentered(myTree, scrollRow, false);
302 if (ApplicationManager.getApplication().isDispatchThread()) {
303 runnable.run();
304 } else {
305 SwingUtilities.invokeLater(runnable);
309 protected abstract DefaultTreeModel buildTreeModel(final List<T> changes, final ChangeNodeDecorator changeNodeDecorator);
311 @SuppressWarnings({"SuspiciousMethodCalls"})
312 private void toggleSelection() {
313 boolean hasExcluded = false;
314 for (T value : getSelectedChanges()) {
315 if (!myIncludedChanges.contains(value)) {
316 hasExcluded = true;
320 if (hasExcluded) {
321 includeSelection();
323 else {
324 excludeSelection();
327 repaint();
330 private void includeSelection() {
331 for (T change : getSelectedChanges()) {
332 myIncludedChanges.add(change);
334 notifyInclusionListener();
335 repaint();
338 @SuppressWarnings({"SuspiciousMethodCalls"})
339 private void excludeSelection() {
340 for (T change : getSelectedChanges()) {
341 myIncludedChanges.remove(change);
343 notifyInclusionListener();
344 repaint();
347 public int getSelectionCount() {
348 if (myShowFlatten) {
349 return myList.getSelectedIndices().length;
350 } else {
351 return myTree.getSelectionCount();
355 @NotNull
356 public List<T> getSelectedChanges() {
357 if (myShowFlatten) {
358 final Object[] o = myList.getSelectedValues();
359 final List<T> changes = new ArrayList<T>();
360 for (Object anO : o) {
361 changes.add((T)anO);
364 return changes;
366 else {
367 List<T> changes = new ArrayList<T>();
368 final TreePath[] paths = myTree.getSelectionPaths();
369 if (paths != null) {
370 for (TreePath path : paths) {
371 ChangesBrowserNode node = (ChangesBrowserNode)path.getLastPathComponent();
372 changes.addAll(getSelectedObjects(node));
376 return changes;
380 protected abstract List<T> getSelectedObjects(final ChangesBrowserNode<T> node);
382 @Nullable
383 protected abstract T getLeadSelectedObject(final ChangesBrowserNode node);
385 @Nullable
386 public T getHighestLeadSelection() {
387 if (myShowFlatten) {
388 final int index = myList.getLeadSelectionIndex();
389 ListModel listModel = myList.getModel();
390 if (index < 0 || index >= listModel.getSize()) return null;
391 //noinspection unchecked
392 return (T)listModel.getElementAt(index);
394 else {
395 final TreePath path = myTree.getSelectionPath();
396 if (path == null) return null;
397 return getLeadSelectedObject((ChangesBrowserNode<T>)path.getLastPathComponent());
401 @Nullable
402 public T getLeadSelection() {
403 if (myShowFlatten) {
404 final int index = myList.getLeadSelectionIndex();
405 ListModel listModel = myList.getModel();
406 if (index < 0 || index >= listModel.getSize()) return null;
407 //noinspection unchecked
408 return (T)listModel.getElementAt(index);
410 else {
411 final TreePath path = myTree.getSelectionPath();
412 if (path == null) return null;
413 final List<T> changes = getSelectedObjects(((ChangesBrowserNode<T>)path.getLastPathComponent()));
414 return changes.size() > 0 ? changes.get(0) : null;
418 private void notifyInclusionListener() {
419 if (myInclusionListener != null) {
420 myInclusionListener.run();
424 // no listener supposed to be called
425 public void setIncludedChanges(final Collection<T> changes) {
426 myIncludedChanges.clear();
427 myIncludedChanges.addAll(changes);
428 myTree.repaint();
429 myList.repaint();
432 public void includeChange(final T change) {
433 myIncludedChanges.add(change);
434 notifyInclusionListener();
435 myTree.repaint();
436 myList.repaint();
439 public void includeChanges(final Collection<T> changes) {
440 myIncludedChanges.addAll(changes);
441 notifyInclusionListener();
442 myTree.repaint();
443 myList.repaint();
446 public void excludeChange(final T change) {
447 myIncludedChanges.remove(change);
448 notifyInclusionListener();
449 myTree.repaint();
450 myList.repaint();
453 public void excludeChanges(final Collection<T> changes) {
454 myIncludedChanges.removeAll(changes);
455 notifyInclusionListener();
456 myTree.repaint();
457 myList.repaint();
460 public boolean isIncluded(final T change) {
461 return myIncludedChanges.contains(change);
464 public Collection<T> getIncludedChanges() {
465 return myIncludedChanges;
468 public void expandAll() {
469 TreeUtil.expandAll(myTree);
472 public AnAction[] getTreeActions() {
473 final ToggleShowDirectoriesAction directoriesAction = new ToggleShowDirectoriesAction();
474 final ExpandAllAction expandAllAction = new ExpandAllAction(myTree) {
475 public void update(AnActionEvent e) {
476 e.getPresentation().setVisible(!myShowFlatten);
479 final CollapseAllAction collapseAllAction = new CollapseAllAction(myTree) {
480 public void update(AnActionEvent e) {
481 e.getPresentation().setVisible(!myShowFlatten);
484 final SelectAllAction selectAllAction = new SelectAllAction();
485 final AnAction[] actions = new AnAction[]{directoriesAction, expandAllAction, collapseAllAction, selectAllAction};
486 directoriesAction.registerCustomShortcutSet(
487 new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_P, SystemInfo.isMac ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK)),
488 this);
489 expandAllAction.registerCustomShortcutSet(
490 new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_EXPAND_ALL)),
491 myTree);
492 collapseAllAction.registerCustomShortcutSet(
493 new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_COLLAPSE_ALL)),
494 myTree);
495 selectAllAction.registerCustomShortcutSet(
496 new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_A, SystemInfo.isMac ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK)),
497 this);
498 return actions;
501 private class MyTreeCellRenderer extends JPanel implements TreeCellRenderer {
502 private final ChangesBrowserNodeRenderer myTextRenderer;
503 private final JCheckBox myCheckBox;
506 public MyTreeCellRenderer() {
507 super(new BorderLayout());
508 myCheckBox = new JCheckBox();
509 myTextRenderer = new ChangesBrowserNodeRenderer(myProject, false, myHighlightProblems);
511 myCheckBox.setBackground(null);
512 setBackground(null);
514 if (myShowCheckboxes) {
515 add(myCheckBox, BorderLayout.WEST);
518 add(myTextRenderer, BorderLayout.CENTER);
521 public Component getTreeCellRendererComponent(JTree tree,
522 Object value,
523 boolean selected,
524 boolean expanded,
525 boolean leaf,
526 int row,
527 boolean hasFocus) {
528 myTextRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
529 if (myShowCheckboxes) {
530 ChangesBrowserNode node = (ChangesBrowserNode)value;
532 CheckboxTree.NodeState state = getNodeStatus(node);
533 myCheckBox.setSelected(state != CheckboxTree.NodeState.CLEAR);
534 myCheckBox.setEnabled(state != CheckboxTree.NodeState.PARTIAL);
535 revalidate();
537 return this;
539 else {
540 return myTextRenderer;
546 private CheckboxTree.NodeState getNodeStatus(ChangesBrowserNode node) {
547 boolean hasIncluded = false;
548 boolean hasExcluded = false;
550 for (T change : getSelectedObjects(node)) {
551 if (myIncludedChanges.contains(change)) {
552 hasIncluded = true;
554 else {
555 hasExcluded = true;
559 if (hasIncluded && hasExcluded) return CheckboxTree.NodeState.PARTIAL;
560 if (hasIncluded) return CheckboxTree.NodeState.FULL;
561 return CheckboxTree.NodeState.CLEAR;
564 private class MyListCellRenderer extends JPanel implements ListCellRenderer {
565 private final ColoredListCellRenderer myTextRenderer;
566 public final JCheckBox myCheckbox;
568 public MyListCellRenderer() {
569 super(new BorderLayout());
570 myCheckbox = new JCheckBox();
571 myTextRenderer = new ColoredListCellRenderer() {
572 protected void customizeCellRenderer(JList list, Object value, int index, boolean selected, boolean hasFocus) {
573 final FilePath path = TreeModelBuilder.getPathForObject(value);
574 if (path.isDirectory()) {
575 setIcon(Icons.DIRECTORY_CLOSED_ICON);
576 } else {
577 setIcon(path.getFileType().getIcon());
579 final FileStatus fileStatus;
580 if (value instanceof Change) {
581 fileStatus = ((Change) value).getFileStatus();
583 else {
584 final VirtualFile virtualFile = path.getVirtualFile();
585 if (virtualFile != null) {
586 fileStatus = FileStatusManager.getInstance(myProject).getStatus(virtualFile);
588 else {
589 fileStatus = FileStatus.NOT_CHANGED;
592 append(path.getName(), new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, fileStatus.getColor(), null));
593 final boolean applyChangeDecorator = (value instanceof Change) && myChangeDecorator != null;
594 final File parentFile = path.getIOFile().getParentFile();
595 if (parentFile != null) {
596 final String parentPath = parentFile.getPath();
597 List<Pair<String,ChangeNodeDecorator.Stress>> parts = null;
598 if (applyChangeDecorator) {
599 parts = myChangeDecorator.stressPartsOfFileName((Change)value, parentPath);
601 if (parts == null) {
602 parts = Collections.singletonList(new Pair<String, ChangeNodeDecorator.Stress>(parentPath, ChangeNodeDecorator.Stress.PLAIN));
605 append(" (");
606 for (Pair<String, ChangeNodeDecorator.Stress> part : parts) {
607 append(part.getFirst(), part.getSecond().derive(SimpleTextAttributes.GRAYED_ATTRIBUTES));
609 append(")", SimpleTextAttributes.GRAYED_ATTRIBUTES);
611 if (applyChangeDecorator) {
612 myChangeDecorator.decorate((Change) value, this, isShowFlatten());
617 myCheckbox.setBackground(null);
618 setBackground(null);
620 if (myShowCheckboxes) {
621 add(myCheckbox, BorderLayout.WEST);
623 add(myTextRenderer, BorderLayout.CENTER);
626 public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
627 myTextRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
628 if (myShowCheckboxes) {
629 myCheckbox.setSelected(myIncludedChanges.contains(value));
630 return this;
632 else {
633 return myTextRenderer;
638 private class MyToggleSelectionAction extends AnAction {
639 public void actionPerformed(AnActionEvent e) {
640 toggleSelection();
644 public class ToggleShowDirectoriesAction extends ToggleAction {
645 public ToggleShowDirectoriesAction() {
646 super(VcsBundle.message("changes.action.show.directories.text"),
647 VcsBundle.message("changes.action.show.directories.description"),
648 Icons.DIRECTORY_CLOSED_ICON);
651 public boolean isSelected(AnActionEvent e) {
652 return !PropertiesComponent.getInstance(myProject).isTrueValue(FLATTEN_OPTION_KEY);
655 public void setSelected(AnActionEvent e, boolean state) {
656 PropertiesComponent.getInstance(myProject).setValue(FLATTEN_OPTION_KEY, String.valueOf(!state));
657 setShowFlatten(!state);
661 private class SelectAllAction extends AnAction {
662 private SelectAllAction() {
663 super("Select All", "Select all items", IconLoader.getIcon("/actions/selectall.png"));
666 public void actionPerformed(final AnActionEvent e) {
667 if (myShowFlatten) {
668 final int count = myList.getModel().getSize();
669 if (count > 0) {
670 myList.setSelectionInterval(0, count-1);
673 else {
674 final int countTree = myTree.getRowCount();
675 if (countTree > 0) {
676 myTree.setSelectionInterval(0, countTree-1);
682 public void select(final List<T> changes) {
683 final DefaultTreeModel treeModel = (DefaultTreeModel) myTree.getModel();
684 final TreeNode root = (TreeNode) treeModel.getRoot();
685 final List<TreePath> treeSelection = new ArrayList<TreePath>(changes.size());
686 TreeUtil.traverse(root, new TreeUtil.Traverse() {
687 public boolean accept(Object node) {
688 final T change = (T) ((DefaultMutableTreeNode) node).getUserObject();
689 if (changes.contains(change)) {
690 treeSelection.add(new TreePath(((DefaultMutableTreeNode) node).getPath()));
692 return true;
695 myTree.setSelectionPaths(treeSelection.toArray(new TreePath[treeSelection.size()]));
697 // list
698 final ListModel model = myList.getModel();
699 final int size = model.getSize();
700 final List<Integer> listSelection = new ArrayList<Integer>(changes.size());
701 for (int i = 0; i < size; i++) {
702 final T el = (T) model.getElementAt(i);
703 if (changes.contains(el)) {
704 listSelection.add(i);
707 myList.setSelectedIndices(int2int(listSelection));
710 private static int[] int2int(List<Integer> treeSelection) {
711 final int[] toPass = new int[treeSelection.size()];
712 int i = 0;
713 for (Integer integer : treeSelection) {
714 toPass[i] = integer;
715 ++ i;
717 return toPass;