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
.keymap
.KeymapManager
;
21 import com
.intellij
.openapi
.project
.Project
;
22 import com
.intellij
.openapi
.util
.EmptyRunnable
;
23 import com
.intellij
.openapi
.util
.IconLoader
;
24 import com
.intellij
.openapi
.util
.Pair
;
25 import com
.intellij
.openapi
.util
.SystemInfo
;
26 import com
.intellij
.openapi
.vcs
.FilePath
;
27 import com
.intellij
.openapi
.vcs
.FileStatus
;
28 import com
.intellij
.openapi
.vcs
.FileStatusManager
;
29 import com
.intellij
.openapi
.vcs
.VcsBundle
;
30 import com
.intellij
.openapi
.vcs
.changes
.Change
;
31 import com
.intellij
.openapi
.vcs
.changes
.ChangesUtil
;
32 import com
.intellij
.openapi
.vfs
.VirtualFile
;
33 import com
.intellij
.ui
.*;
34 import com
.intellij
.ui
.treeStructure
.Tree
;
35 import com
.intellij
.ui
.treeStructure
.actions
.CollapseAllAction
;
36 import com
.intellij
.ui
.treeStructure
.actions
.ExpandAllAction
;
37 import com
.intellij
.util
.Icons
;
38 import com
.intellij
.util
.ui
.tree
.TreeUtil
;
39 import org
.jetbrains
.annotations
.NonNls
;
40 import org
.jetbrains
.annotations
.NotNull
;
41 import org
.jetbrains
.annotations
.Nullable
;
44 import javax
.swing
.tree
.DefaultTreeModel
;
45 import javax
.swing
.tree
.TreeCellRenderer
;
46 import javax
.swing
.tree
.TreePath
;
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());
123 myList
= new JList(new DefaultListModel());
124 myList
.setVisibleRowCount(10);
126 add(new JScrollPane(myList
), LIST_CARD
);
127 add(new JScrollPane(myTree
), TREE_CARD
);
129 new ListSpeedSearch(myList
) {
130 protected String
getElementText(Object element
) {
131 if (element
instanceof Change
) {
132 return ChangesUtil
.getFilePath((Change
)element
).getName();
134 return super.getElementText(element
);
138 myList
.setCellRenderer(new MyListCellRenderer());
140 new MyToggleSelectionAction().registerCustomShortcutSet(new CustomShortcutSet(KeyStroke
.getKeyStroke(KeyEvent
.VK_SPACE
, 0)), this);
142 registerKeyboardAction(new ActionListener() {
143 public void actionPerformed(ActionEvent e
) {
147 }, KeyStroke
.getKeyStroke(KeyEvent
.VK_INSERT
, 0), JComponent
.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
);
149 registerKeyboardAction(new ActionListener() {
150 public void actionPerformed(ActionEvent e
) {
153 }, KeyStroke
.getKeyStroke(KeyEvent
.VK_DELETE
, 0), JComponent
.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
);
155 myList
.addMouseListener(new MouseAdapter() {
156 public void mouseClicked(MouseEvent e
) {
157 final int idx
= myList
.locationToIndex(e
.getPoint());
159 final Rectangle baseRect
= myList
.getCellBounds(idx
, idx
);
160 baseRect
.setSize(checkboxWidth
, baseRect
.height
);
161 if (baseRect
.contains(e
.getPoint())) {
165 else if (e
.getClickCount() == 2) {
166 myDoubleClickHandler
.run();
173 myTree
.addMouseListener(new MouseAdapter() {
174 public void mouseClicked(MouseEvent e
) {
175 final int row
= myTree
.getRowForLocation(e
.getPoint().x
, e
.getPoint().y
);
177 final Rectangle baseRect
= myTree
.getRowBounds(row
);
178 baseRect
.setSize(checkboxWidth
, baseRect
.height
);
179 if (!baseRect
.contains(e
.getPoint()) && e
.getClickCount() == 2) {
180 myDoubleClickHandler
.run();
187 setShowFlatten(PropertiesComponent
.getInstance(myProject
).isTrueValue(FLATTEN_OPTION_KEY
));
190 public void setDoubleClickHandler(final Runnable doubleClickHandler
) {
191 myDoubleClickHandler
= doubleClickHandler
;
194 public void installPopupHandler(ActionGroup group
) {
195 PopupHandler
.installUnknownPopupHandler(myList
, group
, ActionManager
.getInstance());
196 PopupHandler
.installUnknownPopupHandler(myTree
, group
, ActionManager
.getInstance());
199 public Dimension
getPreferredSize() {
200 return new Dimension(400, 400);
203 public boolean isShowFlatten() {
204 return myShowFlatten
;
207 public void setShowFlatten(final boolean showFlatten
) {
208 myShowFlatten
= showFlatten
;
209 myCards
.show(this, myShowFlatten ? LIST_CARD
: TREE_CARD
);
210 if (myList
.hasFocus() || myTree
.hasFocus()) {
211 SwingUtilities
.invokeLater(new Runnable() {
220 public void requestFocus() {
222 myList
.requestFocus();
225 myTree
.requestFocus();
229 public void setChangesToDisplay(final List
<T
> changes
) {
230 final DefaultListModel listModel
= (DefaultListModel
)myList
.getModel();
231 final List
<T
> sortedChanges
= new ArrayList
<T
>(changes
);
232 Collections
.sort(sortedChanges
, new Comparator
<T
>() {
233 public int compare(final T o1
, final T o2
) {
234 return TreeModelBuilder
.getPathForObject(o1
).getName().compareToIgnoreCase(TreeModelBuilder
.getPathForObject(o2
).getName());
238 listModel
.removeAllElements();
239 for (T change
: sortedChanges
) {
240 listModel
.addElement(change
);
243 final DefaultTreeModel model
= buildTreeModel(changes
, myChangeDecorator
);
244 myTree
.setModel(model
);
246 SwingUtilities
.invokeLater(new Runnable() {
248 if (myProject
.isDisposed()) return;
249 TreeUtil
.expandAll(myTree
);
251 if (myIncludedChanges
.size() > 0) {
252 int listSelection
= 0;
254 for (T change
: changes
) {
255 if (myIncludedChanges
.contains(change
)) {
256 listSelection
= count
;
262 ChangesBrowserNode root
= (ChangesBrowserNode
)model
.getRoot();
263 Enumeration enumeration
= root
.depthFirstEnumeration();
265 while (enumeration
.hasMoreElements()) {
266 ChangesBrowserNode node
= (ChangesBrowserNode
)enumeration
.nextElement();
267 final CheckboxTree
.NodeState state
= getNodeStatus(node
);
268 if (node
!= root
&& state
== CheckboxTree
.NodeState
.CLEAR
) {
269 myTree
.collapsePath(new TreePath(node
.getPath()));
273 enumeration
= root
.depthFirstEnumeration();
275 while (enumeration
.hasMoreElements()) {
276 ChangesBrowserNode node
= (ChangesBrowserNode
)enumeration
.nextElement();
277 final CheckboxTree
.NodeState state
= getNodeStatus(node
);
278 if (state
== CheckboxTree
.NodeState
.FULL
&& node
.isLeaf()) {
279 scrollRow
= myTree
.getRowForPath(new TreePath(node
.getPath()));
284 if (changes
.size() > 0) {
285 myList
.setSelectedIndex(listSelection
);
286 myList
.ensureIndexIsVisible(listSelection
);
288 myTree
.setSelectionRow(scrollRow
);
289 TreeUtil
.showRowCentered(myTree
, scrollRow
, false);
296 protected abstract DefaultTreeModel
buildTreeModel(final List
<T
> changes
, final ChangeNodeDecorator changeNodeDecorator
);
298 @SuppressWarnings({"SuspiciousMethodCalls"})
299 private void toggleSelection() {
300 boolean hasExcluded
= false;
301 for (T value
: getSelectedChanges()) {
302 if (!myIncludedChanges
.contains(value
)) {
317 private void includeSelection() {
318 for (T change
: getSelectedChanges()) {
319 myIncludedChanges
.add(change
);
321 notifyInclusionListener();
325 @SuppressWarnings({"SuspiciousMethodCalls"})
326 private void excludeSelection() {
327 for (T change
: getSelectedChanges()) {
328 myIncludedChanges
.remove(change
);
330 notifyInclusionListener();
334 public int getSelectionCount() {
336 return myList
.getSelectedIndices().length
;
338 return myTree
.getSelectionCount();
343 public List
<T
> getSelectedChanges() {
345 final Object
[] o
= myList
.getSelectedValues();
346 final List
<T
> changes
= new ArrayList
<T
>();
347 for (Object anO
: o
) {
354 List
<T
> changes
= new ArrayList
<T
>();
355 final TreePath
[] paths
= myTree
.getSelectionPaths();
357 for (TreePath path
: paths
) {
358 ChangesBrowserNode node
= (ChangesBrowserNode
)path
.getLastPathComponent();
359 changes
.addAll(getSelectedObjects(node
));
367 protected abstract List
<T
> getSelectedObjects(final ChangesBrowserNode
<T
> node
);
370 protected abstract T
getLeadSelectedObject(final ChangesBrowserNode node
);
373 public T
getHighestLeadSelection() {
375 final int index
= myList
.getLeadSelectionIndex();
376 ListModel listModel
= myList
.getModel();
377 if (index
< 0 || index
>= listModel
.getSize()) return null;
378 //noinspection unchecked
379 return (T
)listModel
.getElementAt(index
);
382 final TreePath path
= myTree
.getSelectionPath();
383 if (path
== null) return null;
384 return getLeadSelectedObject((ChangesBrowserNode
<T
>)path
.getLastPathComponent());
389 public T
getLeadSelection() {
391 final int index
= myList
.getLeadSelectionIndex();
392 ListModel listModel
= myList
.getModel();
393 if (index
< 0 || index
>= listModel
.getSize()) return null;
394 //noinspection unchecked
395 return (T
)listModel
.getElementAt(index
);
398 final TreePath path
= myTree
.getSelectionPath();
399 if (path
== null) return null;
400 final List
<T
> changes
= getSelectedObjects(((ChangesBrowserNode
<T
>)path
.getLastPathComponent()));
401 return changes
.size() > 0 ? changes
.get(0) : null;
405 private void notifyInclusionListener() {
406 if (myInclusionListener
!= null) {
407 myInclusionListener
.run();
411 // no listener supposed to be called
412 public void setIncludedChanges(final Collection
<T
> changes
) {
413 myIncludedChanges
.clear();
414 myIncludedChanges
.addAll(changes
);
419 public void includeChange(final T change
) {
420 myIncludedChanges
.add(change
);
421 notifyInclusionListener();
426 public void includeChanges(final Collection
<T
> changes
) {
427 myIncludedChanges
.addAll(changes
);
428 notifyInclusionListener();
433 public void excludeChange(final T change
) {
434 myIncludedChanges
.remove(change
);
435 notifyInclusionListener();
440 public void excludeChanges(final Collection
<T
> changes
) {
441 myIncludedChanges
.removeAll(changes
);
442 notifyInclusionListener();
447 public boolean isIncluded(final T change
) {
448 return myIncludedChanges
.contains(change
);
451 public Collection
<T
> getIncludedChanges() {
452 return myIncludedChanges
;
455 public void expandAll() {
456 TreeUtil
.expandAll(myTree
);
459 public AnAction
[] getTreeActions() {
460 final ToggleShowDirectoriesAction directoriesAction
= new ToggleShowDirectoriesAction();
461 final ExpandAllAction expandAllAction
= new ExpandAllAction(myTree
) {
462 public void update(AnActionEvent e
) {
463 e
.getPresentation().setVisible(!myShowFlatten
);
466 final CollapseAllAction collapseAllAction
= new CollapseAllAction(myTree
) {
467 public void update(AnActionEvent e
) {
468 e
.getPresentation().setVisible(!myShowFlatten
);
471 final SelectAllAction selectAllAction
= new SelectAllAction();
472 final AnAction
[] actions
= new AnAction
[]{directoriesAction
, expandAllAction
, collapseAllAction
, selectAllAction
};
473 directoriesAction
.registerCustomShortcutSet(
474 new CustomShortcutSet(KeyStroke
.getKeyStroke(KeyEvent
.VK_P
, SystemInfo
.isMac ? KeyEvent
.META_DOWN_MASK
: KeyEvent
.CTRL_DOWN_MASK
)),
476 expandAllAction
.registerCustomShortcutSet(
477 new CustomShortcutSet(KeymapManager
.getInstance().getActiveKeymap().getShortcuts(IdeActions
.ACTION_EXPAND_ALL
)),
479 collapseAllAction
.registerCustomShortcutSet(
480 new CustomShortcutSet(KeymapManager
.getInstance().getActiveKeymap().getShortcuts(IdeActions
.ACTION_COLLAPSE_ALL
)),
482 selectAllAction
.registerCustomShortcutSet(
483 new CustomShortcutSet(KeyStroke
.getKeyStroke(KeyEvent
.VK_A
, SystemInfo
.isMac ? KeyEvent
.META_DOWN_MASK
: KeyEvent
.CTRL_DOWN_MASK
)),
488 private class MyTreeCellRenderer
extends JPanel
implements TreeCellRenderer
{
489 private final ChangesBrowserNodeRenderer myTextRenderer
;
490 private final JCheckBox myCheckBox
;
493 public MyTreeCellRenderer() {
494 super(new BorderLayout());
495 myCheckBox
= new JCheckBox();
496 myTextRenderer
= new ChangesBrowserNodeRenderer(myProject
, false, myHighlightProblems
);
498 myCheckBox
.setBackground(null);
501 if (myShowCheckboxes
) {
502 add(myCheckBox
, BorderLayout
.WEST
);
505 add(myTextRenderer
, BorderLayout
.CENTER
);
508 public Component
getTreeCellRendererComponent(JTree tree
,
515 myTextRenderer
.getTreeCellRendererComponent(tree
, value
, selected
, expanded
, leaf
, row
, hasFocus
);
516 if (myShowCheckboxes
) {
517 ChangesBrowserNode node
= (ChangesBrowserNode
)value
;
519 CheckboxTree
.NodeState state
= getNodeStatus(node
);
520 myCheckBox
.setSelected(state
!= CheckboxTree
.NodeState
.CLEAR
);
521 myCheckBox
.setEnabled(state
!= CheckboxTree
.NodeState
.PARTIAL
);
527 return myTextRenderer
;
533 private CheckboxTree
.NodeState
getNodeStatus(ChangesBrowserNode node
) {
534 boolean hasIncluded
= false;
535 boolean hasExcluded
= false;
537 for (T change
: getSelectedObjects(node
)) {
538 if (myIncludedChanges
.contains(change
)) {
546 if (hasIncluded
&& hasExcluded
) return CheckboxTree
.NodeState
.PARTIAL
;
547 if (hasIncluded
) return CheckboxTree
.NodeState
.FULL
;
548 return CheckboxTree
.NodeState
.CLEAR
;
551 private class MyListCellRenderer
extends JPanel
implements ListCellRenderer
{
552 private final ColoredListCellRenderer myTextRenderer
;
553 public final JCheckBox myCheckbox
;
555 public MyListCellRenderer() {
556 super(new BorderLayout());
557 myCheckbox
= new JCheckBox();
558 myTextRenderer
= new ColoredListCellRenderer() {
559 protected void customizeCellRenderer(JList list
, Object value
, int index
, boolean selected
, boolean hasFocus
) {
560 final FilePath path
= TreeModelBuilder
.getPathForObject(value
);
561 if (path
.isDirectory()) {
562 setIcon(Icons
.DIRECTORY_CLOSED_ICON
);
564 setIcon(path
.getFileType().getIcon());
566 final FileStatus fileStatus
;
567 if (value
instanceof Change
) {
568 fileStatus
= ((Change
) value
).getFileStatus();
571 final VirtualFile virtualFile
= path
.getVirtualFile();
572 if (virtualFile
!= null) {
573 fileStatus
= FileStatusManager
.getInstance(myProject
).getStatus(virtualFile
);
576 fileStatus
= FileStatus
.NOT_CHANGED
;
579 append(path
.getName(), new SimpleTextAttributes(SimpleTextAttributes
.STYLE_PLAIN
, fileStatus
.getColor(), null));
580 final boolean applyChangeDecorator
= (value
instanceof Change
) && myChangeDecorator
!= null;
581 final File parentFile
= path
.getIOFile().getParentFile();
582 if (parentFile
!= null) {
583 final String parentPath
= parentFile
.getPath();
584 List
<Pair
<String
,ChangeNodeDecorator
.Stress
>> parts
= null;
585 if (applyChangeDecorator
) {
586 parts
= myChangeDecorator
.stressPartsOfFileName((Change
)value
, parentPath
);
589 parts
= Collections
.singletonList(new Pair
<String
, ChangeNodeDecorator
.Stress
>(parentPath
, ChangeNodeDecorator
.Stress
.PLAIN
));
593 for (Pair
<String
, ChangeNodeDecorator
.Stress
> part
: parts
) {
594 append(part
.getFirst(), part
.getSecond().derive(SimpleTextAttributes
.GRAYED_ATTRIBUTES
));
596 append(")", SimpleTextAttributes
.GRAYED_ATTRIBUTES
);
598 if (applyChangeDecorator
) {
599 myChangeDecorator
.decorate((Change
) value
, this, isShowFlatten());
604 myCheckbox
.setBackground(null);
607 if (myShowCheckboxes
) {
608 add(myCheckbox
, BorderLayout
.WEST
);
610 add(myTextRenderer
, BorderLayout
.CENTER
);
613 public Component
getListCellRendererComponent(JList list
, Object value
, int index
, boolean isSelected
, boolean cellHasFocus
) {
614 myTextRenderer
.getListCellRendererComponent(list
, value
, index
, isSelected
, cellHasFocus
);
615 if (myShowCheckboxes
) {
616 myCheckbox
.setSelected(myIncludedChanges
.contains(value
));
620 return myTextRenderer
;
625 private class MyToggleSelectionAction
extends AnAction
{
626 public void actionPerformed(AnActionEvent e
) {
631 public class ToggleShowDirectoriesAction
extends ToggleAction
{
632 public ToggleShowDirectoriesAction() {
633 super(VcsBundle
.message("changes.action.show.directories.text"),
634 VcsBundle
.message("changes.action.show.directories.description"),
635 Icons
.DIRECTORY_CLOSED_ICON
);
638 public boolean isSelected(AnActionEvent e
) {
639 return !PropertiesComponent
.getInstance(myProject
).isTrueValue(FLATTEN_OPTION_KEY
);
642 public void setSelected(AnActionEvent e
, boolean state
) {
643 PropertiesComponent
.getInstance(myProject
).setValue(FLATTEN_OPTION_KEY
, String
.valueOf(!state
));
644 setShowFlatten(!state
);
648 private class SelectAllAction
extends AnAction
{
649 private SelectAllAction() {
650 super("Select All", "Select all items", IconLoader
.getIcon("/actions/selectall.png"));
653 public void actionPerformed(final AnActionEvent e
) {
655 final int count
= myList
.getModel().getSize();
657 myList
.setSelectionInterval(0, count
-1);
661 final int count
= myTree
.getRowCount();
663 myTree
.setSelectionInterval(0, count
-1);