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
;
46 import javax
.swing
.tree
.*;
48 import java
.awt
.event
.*;
51 import java
.util
.List
;
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
) {
80 myShowCheckboxes
= showCheckboxes
;
81 myHighlightProblems
= highlightProblems
;
82 myInclusionListener
= inclusionListener
;
83 myChangeDecorator
= decorator
;
84 myIncludedChanges
= new HashSet
<T
>(initiallyIncluded
);
86 myCards
= new CardLayout();
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
);
98 protected void processMouseEvent(MouseEvent e
) {
99 if (e
.getID() == MouseEvent
.MOUSE_PRESSED
) {
100 int row
= myTree
.getRowForLocation(e
.getX(), e
.getY());
102 final Rectangle baseRect
= myTree
.getRowBounds(row
);
103 baseRect
.setSize(checkboxWidth
, baseRect
.height
);
104 if (baseRect
.contains(e
.getPoint())) {
105 myTree
.setSelectionRow(row
);
110 super.processMouseEvent(e
);
113 public int getToggleClickCount() {
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
) {
153 }, KeyStroke
.getKeyStroke(KeyEvent
.VK_INSERT
, 0), JComponent
.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
);
155 registerKeyboardAction(new ActionListener() {
156 public void actionPerformed(ActionEvent e
) {
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());
165 final Rectangle baseRect
= myList
.getCellBounds(idx
, idx
);
166 baseRect
.setSize(checkboxWidth
, baseRect
.height
);
167 if (baseRect
.contains(e
.getPoint())) {
171 else if (e
.getClickCount() == 2) {
172 myDoubleClickHandler
.run();
179 myTree
.addMouseListener(new MouseAdapter() {
180 public void mouseClicked(MouseEvent e
) {
181 final int row
= myTree
.getRowForLocation(e
.getPoint().x
, e
.getPoint().y
);
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();
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
);
218 if (myList
.hasFocus() || myTree
.hasFocus()) {
219 SwingUtilities
.invokeLater(new Runnable() {
228 public void requestFocus() {
230 myList
.requestFocus();
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() {
256 if (myProject
.isDisposed()) return;
257 TreeUtil
.expandAll(myTree
);
259 if (myIncludedChanges
.size() > 0) {
260 int listSelection
= 0;
262 for (T change
: changes
) {
263 if (myIncludedChanges
.contains(change
)) {
264 listSelection
= 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();
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()));
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()) {
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
)) {
330 private void includeSelection() {
331 for (T change
: getSelectedChanges()) {
332 myIncludedChanges
.add(change
);
334 notifyInclusionListener();
338 @SuppressWarnings({"SuspiciousMethodCalls"})
339 private void excludeSelection() {
340 for (T change
: getSelectedChanges()) {
341 myIncludedChanges
.remove(change
);
343 notifyInclusionListener();
347 public int getSelectionCount() {
349 return myList
.getSelectedIndices().length
;
351 return myTree
.getSelectionCount();
356 public List
<T
> getSelectedChanges() {
358 final Object
[] o
= myList
.getSelectedValues();
359 final List
<T
> changes
= new ArrayList
<T
>();
360 for (Object anO
: o
) {
367 List
<T
> changes
= new ArrayList
<T
>();
368 final TreePath
[] paths
= myTree
.getSelectionPaths();
370 for (TreePath path
: paths
) {
371 ChangesBrowserNode node
= (ChangesBrowserNode
)path
.getLastPathComponent();
372 changes
.addAll(getSelectedObjects(node
));
380 protected abstract List
<T
> getSelectedObjects(final ChangesBrowserNode
<T
> node
);
383 protected abstract T
getLeadSelectedObject(final ChangesBrowserNode node
);
386 public T
getHighestLeadSelection() {
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
);
395 final TreePath path
= myTree
.getSelectionPath();
396 if (path
== null) return null;
397 return getLeadSelectedObject((ChangesBrowserNode
<T
>)path
.getLastPathComponent());
402 public T
getLeadSelection() {
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
);
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
);
432 public void includeChange(final T change
) {
433 myIncludedChanges
.add(change
);
434 notifyInclusionListener();
439 public void includeChanges(final Collection
<T
> changes
) {
440 myIncludedChanges
.addAll(changes
);
441 notifyInclusionListener();
446 public void excludeChange(final T change
) {
447 myIncludedChanges
.remove(change
);
448 notifyInclusionListener();
453 public void excludeChanges(final Collection
<T
> changes
) {
454 myIncludedChanges
.removeAll(changes
);
455 notifyInclusionListener();
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
)),
489 expandAllAction
.registerCustomShortcutSet(
490 new CustomShortcutSet(KeymapManager
.getInstance().getActiveKeymap().getShortcuts(IdeActions
.ACTION_EXPAND_ALL
)),
492 collapseAllAction
.registerCustomShortcutSet(
493 new CustomShortcutSet(KeymapManager
.getInstance().getActiveKeymap().getShortcuts(IdeActions
.ACTION_COLLAPSE_ALL
)),
495 selectAllAction
.registerCustomShortcutSet(
496 new CustomShortcutSet(KeyStroke
.getKeyStroke(KeyEvent
.VK_A
, SystemInfo
.isMac ? KeyEvent
.META_DOWN_MASK
: KeyEvent
.CTRL_DOWN_MASK
)),
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);
514 if (myShowCheckboxes
) {
515 add(myCheckBox
, BorderLayout
.WEST
);
518 add(myTextRenderer
, BorderLayout
.CENTER
);
521 public Component
getTreeCellRendererComponent(JTree tree
,
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
);
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
)) {
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
);
577 setIcon(path
.getFileType().getIcon());
579 final FileStatus fileStatus
;
580 if (value
instanceof Change
) {
581 fileStatus
= ((Change
) value
).getFileStatus();
584 final VirtualFile virtualFile
= path
.getVirtualFile();
585 if (virtualFile
!= null) {
586 fileStatus
= FileStatusManager
.getInstance(myProject
).getStatus(virtualFile
);
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
);
602 parts
= Collections
.singletonList(new Pair
<String
, ChangeNodeDecorator
.Stress
>(parentPath
, ChangeNodeDecorator
.Stress
.PLAIN
));
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);
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
));
633 return myTextRenderer
;
638 private class MyToggleSelectionAction
extends AnAction
{
639 public void actionPerformed(AnActionEvent e
) {
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
) {
668 final int count
= myList
.getModel().getSize();
670 myList
.setSelectionInterval(0, count
-1);
674 final int countTree
= myTree
.getRowCount();
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()));
695 myTree
.setSelectionPaths(treeSelection
.toArray(new TreePath
[treeSelection
.size()]));
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()];
713 for (Integer integer
: treeSelection
) {