d70d799dc3ecbd753e906277b28318c68ac412ab
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / ui / ChangesTreeList.java
1 /*
2  * Copyright 2000-2012 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 package com.intellij.openapi.vcs.changes.ui;
17
18 import com.intellij.icons.AllIcons;
19 import com.intellij.ide.util.PropertiesComponent;
20 import com.intellij.ide.util.treeView.TreeState;
21 import com.intellij.openapi.actionSystem.*;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.diff.DiffBundle;
24 import com.intellij.openapi.keymap.KeymapManager;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.util.EmptyRunnable;
27 import com.intellij.openapi.util.Pair;
28 import com.intellij.openapi.util.SystemInfo;
29 import com.intellij.openapi.util.registry.Registry;
30 import com.intellij.openapi.util.text.StringUtil;
31 import com.intellij.openapi.vcs.FilePath;
32 import com.intellij.openapi.vcs.FilePathImpl;
33 import com.intellij.openapi.vcs.VcsBundle;
34 import com.intellij.openapi.vcs.changes.Change;
35 import com.intellij.openapi.vcs.changes.ChangesUtil;
36 import com.intellij.openapi.vcs.changes.ContentRevision;
37 import com.intellij.openapi.vfs.LocalFileSystem;
38 import com.intellij.openapi.vfs.VirtualFile;
39 import com.intellij.ui.*;
40 import com.intellij.ui.components.JBList;
41 import com.intellij.ui.components.panels.NonOpaquePanel;
42 import com.intellij.ui.treeStructure.Tree;
43 import com.intellij.ui.treeStructure.actions.CollapseAllAction;
44 import com.intellij.ui.treeStructure.actions.ExpandAllAction;
45 import com.intellij.util.PlatformIcons;
46 import com.intellij.util.containers.Convertor;
47 import com.intellij.util.ui.UIUtil;
48 import com.intellij.util.ui.tree.TreeUtil;
49 import gnu.trove.TIntArrayList;
50 import org.jetbrains.annotations.NonNls;
51 import org.jetbrains.annotations.NotNull;
52 import org.jetbrains.annotations.Nullable;
53
54 import javax.swing.*;
55 import javax.swing.border.Border;
56 import javax.swing.event.ListSelectionEvent;
57 import javax.swing.event.ListSelectionListener;
58 import javax.swing.event.TreeSelectionEvent;
59 import javax.swing.event.TreeSelectionListener;
60 import javax.swing.tree.*;
61 import java.awt.*;
62 import java.awt.event.ActionEvent;
63 import java.awt.event.ActionListener;
64 import java.awt.event.KeyEvent;
65 import java.awt.event.MouseEvent;
66 import java.util.*;
67 import java.util.List;
68
69 /**
70  * @author max
71  */
72 public abstract class ChangesTreeList<T> extends JPanel {
73   private final Tree myTree;
74   private final JBList myList;
75   private final JScrollPane myTreeScrollPane;
76   private final JScrollPane myListScrollPane;
77   protected final Project myProject;
78   private final boolean myShowCheckboxes;
79   private final boolean myHighlightProblems;
80   private boolean myShowFlatten;
81
82   private final Collection<T> myIncludedChanges;
83   private Runnable myDoubleClickHandler = EmptyRunnable.getInstance();
84   private boolean myAlwaysExpandList;
85
86   @NonNls private static final String TREE_CARD = "Tree";
87   @NonNls private static final String LIST_CARD = "List";
88   @NonNls private static final String ROOT = "root";
89   private final CardLayout myCards;
90
91   @NonNls private final static String FLATTEN_OPTION_KEY = "ChangesBrowser.SHOW_FLATTEN";
92
93   private final Runnable myInclusionListener;
94   @Nullable private ChangeNodeDecorator myChangeDecorator;
95   private Runnable myGenericSelectionListener;
96
97   public ChangesTreeList(final Project project, Collection<T> initiallyIncluded, final boolean showCheckboxes,
98                          final boolean highlightProblems, @Nullable final Runnable inclusionListener, @Nullable final ChangeNodeDecorator decorator) {
99     myProject = project;
100     myShowCheckboxes = showCheckboxes;
101     myHighlightProblems = highlightProblems;
102     myInclusionListener = inclusionListener;
103     myChangeDecorator = decorator;
104     myIncludedChanges = new HashSet<T>(initiallyIncluded);
105     myAlwaysExpandList = true;
106
107     myCards = new CardLayout();
108
109     setLayout(myCards);
110
111     final int checkboxWidth = new JCheckBox().getPreferredSize().width;
112     myTree = new Tree(ChangesBrowserNode.create(myProject, ROOT)) {
113
114       @Override
115       public boolean isFileColorsEnabled() {
116         final boolean enabled = Registry.is("file.colors.in.commit.dialog")
117                           && FileColorManager.getInstance(project).isEnabled()
118                           && FileColorManager.getInstance(project).isEnabledForProjectView();
119         final boolean opaque = isOpaque();
120         if (enabled && opaque) {
121           setOpaque(false);
122         } else if (!enabled && !opaque) {
123           setOpaque(true);
124         }
125         return enabled;
126       }
127
128       @Override
129       public Color getFileColorFor(Object object) {
130         VirtualFile file = null;
131         if (object instanceof FilePathImpl) {
132           file = LocalFileSystem.getInstance().findFileByPath(((FilePathImpl)object).getPath());
133         } else if (object instanceof Change) {
134           file = ((Change)object).getVirtualFile();
135         }
136
137         if (file != null) {
138           return FileColorManager.getInstance(project).getFileColor(file);
139         }
140         return super.getFileColorFor(object);
141       }
142
143       public Dimension getPreferredScrollableViewportSize() {
144         Dimension size = super.getPreferredScrollableViewportSize();
145         size = new Dimension(size.width + 10, size.height);
146         return size;
147       }
148
149       protected void processMouseEvent(MouseEvent e) {
150         if (e.getID() == MouseEvent.MOUSE_PRESSED) {
151           if (! myTree.isEnabled()) return;
152           int row = myTree.getRowForLocation(e.getX(), e.getY());
153           if (row >= 0) {
154             final Rectangle baseRect = myTree.getRowBounds(row);
155             baseRect.setSize(checkboxWidth, baseRect.height);
156             if (baseRect.contains(e.getPoint())) {
157               myTree.setSelectionRow(row);
158               toggleSelection();
159             }
160           }
161         }
162         super.processMouseEvent(e);
163       }
164
165       public int getToggleClickCount() {
166         return -1;
167       }
168     };
169
170     myTree.setRootVisible(false);
171     myTree.setShowsRootHandles(true);
172     myTree.setOpaque(false);
173     myTree.setCellRenderer(new MyTreeCellRenderer());
174     new TreeSpeedSearch(myTree, new Convertor<TreePath, String>() {
175       public String convert(TreePath o) {
176         ChangesBrowserNode node = (ChangesBrowserNode) o.getLastPathComponent();
177         return node.getTextPresentation();
178       }
179     });
180
181     myList = new JBList(new DefaultListModel());
182     myList.setVisibleRowCount(10);
183
184     add(myListScrollPane = ScrollPaneFactory.createScrollPane(myList), LIST_CARD);
185     add(myTreeScrollPane = ScrollPaneFactory.createScrollPane(myTree), TREE_CARD);
186
187     new ListSpeedSearch(myList) {
188       protected String getElementText(Object element) {
189         if (element instanceof Change) {
190           return ChangesUtil.getFilePath((Change)element).getName();
191         }
192         return super.getElementText(element);
193       }
194     };
195
196     myList.setCellRenderer(new MyListCellRenderer());
197
198     new MyToggleSelectionAction().registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0)), this);
199     if (myShowCheckboxes) {
200       registerKeyboardAction(new ActionListener() {
201         public void actionPerformed(ActionEvent e) {
202           includeSelection();
203         }
204
205       }, KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
206
207       registerKeyboardAction(new ActionListener() {
208         public void actionPerformed(ActionEvent e) {
209           excludeSelection();
210         }
211       }, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
212     }
213
214     new ClickListener() {
215       @Override
216       public boolean onClick(MouseEvent e, int clickCount) {
217         final int idx = myList.locationToIndex(e.getPoint());
218         if (idx >= 0) {
219           final Rectangle baseRect = myList.getCellBounds(idx, idx);
220           baseRect.setSize(checkboxWidth, baseRect.height);
221           if (baseRect.contains(e.getPoint())) {
222             toggleSelection();
223             return true;
224           }
225           else if (clickCount == 2) {
226             myDoubleClickHandler.run();
227             return true;
228           }
229         }
230         return false;
231       }
232     }.installOn(myList);
233
234     new ClickListener() {
235       @Override
236       public boolean onClick(MouseEvent e, int clickCount) {
237         final int row = myTree.getRowForLocation(e.getPoint().x, e.getPoint().y);
238         if (row >= 0) {
239           final Rectangle baseRect = myTree.getRowBounds(row);
240           baseRect.setSize(checkboxWidth, baseRect.height);
241           if (!baseRect.contains(e.getPoint()) && clickCount == 2) {
242             myDoubleClickHandler.run();
243             return true;
244           }
245         }
246         return false;
247       }
248     }.installOn(myTree);
249
250     setShowFlatten(PropertiesComponent.getInstance(myProject).isTrueValue(FLATTEN_OPTION_KEY));
251
252     String emptyText = StringUtil.capitalize(DiffBundle.message("diff.count.differences.status.text", 0));
253     setEmptyText(emptyText);
254   }
255
256   public void setEmptyText(@NotNull String emptyText) {
257     myTree.getEmptyText().setText(emptyText);
258     myList.getEmptyText().setText(emptyText);
259   }
260
261   // generic, both for tree and list
262   public void addSelectionListener(final Runnable runnable) {
263     myGenericSelectionListener = runnable;
264     myList.addListSelectionListener(new ListSelectionListener() {
265       @Override
266       public void valueChanged(ListSelectionEvent e) {
267         myGenericSelectionListener.run();
268       }
269     });
270     myTree.addTreeSelectionListener(new TreeSelectionListener() {
271       @Override
272       public void valueChanged(TreeSelectionEvent e) {
273         myGenericSelectionListener.run();
274       }
275     });
276   }
277
278   public void setChangeDecorator(@Nullable ChangeNodeDecorator changeDecorator) {
279     myChangeDecorator = changeDecorator;
280   }
281
282   public void setDoubleClickHandler(final Runnable doubleClickHandler) {
283     myDoubleClickHandler = doubleClickHandler;
284   }
285
286   public void installPopupHandler(ActionGroup group) {
287     PopupHandler.installUnknownPopupHandler(myList, group, ActionManager.getInstance());
288     PopupHandler.installUnknownPopupHandler(myTree, group, ActionManager.getInstance());
289   }
290   
291   public JComponent getPreferredFocusedComponent() {
292     return myShowFlatten ? myList : myTree;
293   }
294
295   public Dimension getPreferredSize() {
296     return new Dimension(400, 400);
297   }
298
299   public boolean isShowFlatten() {
300     return myShowFlatten;
301   }
302
303   public void setScrollPaneBorder(Border border) {
304     myListScrollPane.setBorder(border);
305     myTreeScrollPane.setBorder(border);
306   }
307
308   public void setShowFlatten(final boolean showFlatten) {
309     final List<T> wasSelected = getSelectedChanges();
310     myShowFlatten = showFlatten;
311     myCards.show(this, myShowFlatten ? LIST_CARD : TREE_CARD);
312     select(wasSelected);
313     if (myList.hasFocus() || myTree.hasFocus()) {
314       SwingUtilities.invokeLater(new Runnable() {
315         public void run() {
316           requestFocus();
317         }
318       });
319     }
320   }
321
322
323   public void requestFocus() {
324     if (myShowFlatten) {
325       myList.requestFocus();
326     }
327     else {
328       myTree.requestFocus();
329     }
330   }
331
332   public void setChangesToDisplay(final List<T> changes) {
333     setChangesToDisplay(changes, null);
334   }
335   
336   public void setChangesToDisplay(final List<T> changes, @Nullable final VirtualFile toSelect) {
337     final boolean wasEmpty = myList.isEmpty();
338     final List<T> sortedChanges = new ArrayList<T>(changes);
339     Collections.sort(sortedChanges, new Comparator<T>() {
340       public int compare(final T o1, final T o2) {
341         return TreeModelBuilder.getPathForObject(o1).getName().compareToIgnoreCase(TreeModelBuilder.getPathForObject(o2).getName());
342       }
343     });
344
345     final Set<Object> wasSelected = new HashSet<Object>(Arrays.asList(myList.getSelectedValues()));
346     myList.setModel(new AbstractListModel() {
347       @Override
348       public int getSize() {
349         return sortedChanges.size();
350       }
351
352       @Override
353       public Object getElementAt(int index) {
354         return sortedChanges.get(index);
355       }
356     });
357
358     final DefaultTreeModel model = buildTreeModel(changes, myChangeDecorator);
359     TreeState state = null;
360     if (! myAlwaysExpandList && ! wasEmpty) {
361       state = TreeState.createOn(myTree, (DefaultMutableTreeNode) myTree.getModel().getRoot());
362     }
363     myTree.setModel(model);
364     if (! myAlwaysExpandList && ! wasEmpty) {
365       state.applyTo(myTree, (DefaultMutableTreeNode) myTree.getModel().getRoot());
366
367       final TIntArrayList indices = new TIntArrayList();
368       for (int i = 0; i < sortedChanges.size(); i++) {
369         T t = sortedChanges.get(i);
370         if (wasSelected.contains(t)) {
371           indices.add(i);
372         }
373       }
374       myList.setSelectedIndices(indices.toNativeArray());
375       return;
376     }
377
378     final Runnable runnable = new Runnable() {
379       public void run() {
380         if (myProject.isDisposed()) return;
381         TreeUtil.expandAll(myTree);
382
383         int listSelection = 0;
384         int scrollRow = 0;
385
386         if (myShowCheckboxes) {
387           if (myIncludedChanges.size() > 0) {
388             int count = 0;
389             for (T change : changes) {
390               if (myIncludedChanges.contains(change)) {
391                 listSelection = count;
392                 break;
393               }
394               count++;
395             }
396
397             ChangesBrowserNode root = (ChangesBrowserNode)model.getRoot();
398             Enumeration enumeration = root.depthFirstEnumeration();
399
400             while (enumeration.hasMoreElements()) {
401               ChangesBrowserNode node = (ChangesBrowserNode)enumeration.nextElement();
402               final CheckboxTree.NodeState state = getNodeStatus(node);
403               if (node != root && state == CheckboxTree.NodeState.CLEAR) {
404                 myTree.collapsePath(new TreePath(node.getPath()));
405               }
406             }
407
408             enumeration = root.depthFirstEnumeration();
409             while (enumeration.hasMoreElements()) {
410               ChangesBrowserNode node = (ChangesBrowserNode)enumeration.nextElement();
411               final CheckboxTree.NodeState state = getNodeStatus(node);
412               if (state == CheckboxTree.NodeState.FULL && node.isLeaf()) {
413                 scrollRow = myTree.getRowForPath(new TreePath(node.getPath()));
414                 break;
415               }
416             }
417           }
418         } else {
419           if (toSelect != null) {
420             ChangesBrowserNode root = (ChangesBrowserNode)model.getRoot();
421             final int[] rowToSelect = new int[] {-1}; 
422             TreeUtil.traverse(root, new TreeUtil.Traverse() {
423               @Override
424               public boolean accept(Object node) {
425                 if (node instanceof DefaultMutableTreeNode) {
426                   Object userObject = ((DefaultMutableTreeNode)node).getUserObject();
427                   if (userObject instanceof Change) {
428                     Change change = (Change)userObject;
429                     VirtualFile virtualFile = change.getVirtualFile();
430                     if ((virtualFile != null && virtualFile.equals(toSelect)) || seemsToBeMoved(change, toSelect)) {
431                       TreeNode[] path = ((DefaultMutableTreeNode)node).getPath();
432                       rowToSelect[0] = myTree.getRowForPath(new TreePath(path));
433                     }
434                   }
435                 }
436
437                 return rowToSelect[0] == -1;
438               }
439             });
440             
441             scrollRow = rowToSelect[0] == -1 ? scrollRow : rowToSelect[0];
442           }
443         }
444         
445         if (changes.size() > 0) {
446           myList.setSelectedIndex(listSelection);
447           myList.ensureIndexIsVisible(listSelection);
448
449           myTree.setSelectionRow(scrollRow);
450           TreeUtil.showRowCentered(myTree, scrollRow, false);
451         }
452       }
453     };
454     if (ApplicationManager.getApplication().isDispatchThread()) {
455       runnable.run();
456     } else {
457       SwingUtilities.invokeLater(runnable);
458     }
459   }
460
461   private static boolean seemsToBeMoved(Change change, VirtualFile toSelect) {
462     ContentRevision afterRevision = change.getAfterRevision();
463     if (afterRevision == null) return false;
464     FilePath file = afterRevision.getFile();
465     return file.getName().equals(toSelect.getName());
466   }
467
468   protected abstract DefaultTreeModel buildTreeModel(final List<T> changes, final ChangeNodeDecorator changeNodeDecorator);
469
470   @SuppressWarnings({"SuspiciousMethodCalls"})
471   private void toggleSelection() {
472     boolean hasExcluded = false;
473     for (T value : getSelectedChanges()) {
474       if (!myIncludedChanges.contains(value)) {
475         hasExcluded = true;
476       }
477     }
478
479     if (hasExcluded) {
480       includeSelection();
481     }
482     else {
483       excludeSelection();
484     }
485
486     repaint();
487   }
488
489   private void includeSelection() {
490     for (T change : getSelectedChanges()) {
491       myIncludedChanges.add(change);
492     }
493     notifyInclusionListener();
494     repaint();
495   }
496
497   @SuppressWarnings({"SuspiciousMethodCalls"})
498   private void excludeSelection() {
499     for (T change : getSelectedChanges()) {
500       myIncludedChanges.remove(change);
501     }
502     notifyInclusionListener();
503     repaint();
504   }
505
506   public List<T> getChanges() {
507     if (myShowFlatten) {
508       ListModel m = myList.getModel();
509       int size = m.getSize();
510       List result = new ArrayList(size);
511       for (int i = 0; i < size; i++) {
512         result.add(m.getElementAt(i));
513       }
514       return result;
515     }
516     else {
517       final LinkedHashSet result = new LinkedHashSet();
518       TreeUtil.traverseDepth((ChangesBrowserNode)myTree.getModel().getRoot(), new TreeUtil.Traverse() {
519         public boolean accept(Object node) {
520           ChangesBrowserNode changeNode = (ChangesBrowserNode)node;
521           if (changeNode.isLeaf()) result.addAll(changeNode.getAllChangesUnder());
522           return true;
523         }
524       });
525       return new ArrayList<T>(result);
526     }
527   }
528
529   public int getSelectionCount() {
530     if (myShowFlatten) {
531       return myList.getSelectedIndices().length;
532     } else {
533       return myTree.getSelectionCount();
534     }
535   }
536
537   @NotNull
538   public List<T> getSelectedChanges() {
539     if (myShowFlatten) {
540       final Object[] o = myList.getSelectedValues();
541       final List<T> changes = new ArrayList<T>();
542       for (Object anO : o) {
543         changes.add((T)anO);
544       }
545
546       return changes;
547     }
548     else {
549       final List<T> changes = new ArrayList<T>();
550       final Set<Integer> checkSet = new HashSet<Integer>();
551       final TreePath[] paths = myTree.getSelectionPaths();
552       if (paths != null) {
553         for (TreePath path : paths) {
554           final ChangesBrowserNode node = (ChangesBrowserNode)path.getLastPathComponent();
555           final List<T> objects = getSelectedObjects(node);
556           for (T object : objects) {
557             final int hash = object.hashCode();
558             if (! checkSet.contains(hash)) {
559               changes.add(object);
560               checkSet.add(hash);
561             } else {
562               if (! changes.contains(object)) {
563                 changes.add(object);
564               }
565             }
566           }
567         }
568       }
569
570       return changes;
571     }
572   }
573
574   protected abstract List<T> getSelectedObjects(final ChangesBrowserNode<T> node);
575
576   @Nullable
577   protected abstract T getLeadSelectedObject(final ChangesBrowserNode node);
578
579   @Nullable
580   public T getHighestLeadSelection() {
581     if (myShowFlatten) {
582       final int index = myList.getLeadSelectionIndex();
583       ListModel listModel = myList.getModel();
584       if (index < 0 || index >= listModel.getSize()) return null;
585       //noinspection unchecked
586       return (T)listModel.getElementAt(index);
587     }
588     else {
589       final TreePath path = myTree.getSelectionPath();
590       if (path == null) return null;
591       return getLeadSelectedObject((ChangesBrowserNode<T>)path.getLastPathComponent());
592     }
593   }
594
595   @Nullable
596   public T getLeadSelection() {
597     if (myShowFlatten) {
598       final int index = myList.getLeadSelectionIndex();
599       ListModel listModel = myList.getModel();
600       if (index < 0 || index >= listModel.getSize()) return null;
601       //noinspection unchecked
602       return (T)listModel.getElementAt(index);
603     }
604     else {
605       final TreePath path = myTree.getSelectionPath();
606       if (path == null) return null;
607       final List<T> changes = getSelectedObjects(((ChangesBrowserNode<T>)path.getLastPathComponent()));
608       return changes.size() > 0 ? changes.get(0) : null;
609     }
610   }
611
612   private void notifyInclusionListener() {
613     if (myInclusionListener != null) {
614       myInclusionListener.run();
615     }
616   }
617
618   // no listener supposed to be called
619   public void setIncludedChanges(final Collection<T> changes) {
620     myIncludedChanges.clear();
621     myIncludedChanges.addAll(changes);
622     myTree.repaint();
623     myList.repaint();
624   }
625
626   public void includeChange(final T change) {
627     myIncludedChanges.add(change);
628     notifyInclusionListener();
629     myTree.repaint();
630     myList.repaint();
631   }
632
633   public void includeChanges(final Collection<T> changes) {
634     myIncludedChanges.addAll(changes);
635     notifyInclusionListener();
636     myTree.repaint();
637     myList.repaint();
638   }
639
640   public void excludeChange(final T change) {
641     myIncludedChanges.remove(change);
642     notifyInclusionListener();
643     myTree.repaint();
644     myList.repaint();
645   }
646
647   public void excludeChanges(final Collection<T> changes) {
648     myIncludedChanges.removeAll(changes);
649     notifyInclusionListener();
650     myTree.repaint();
651     myList.repaint();
652   }
653
654   public boolean isIncluded(final T change) {
655     return myIncludedChanges.contains(change);
656   }
657
658   public Collection<T> getIncludedChanges() {
659     return myIncludedChanges;
660   }
661
662   public void expandAll() {
663     TreeUtil.expandAll(myTree);
664   }
665
666   public AnAction[] getTreeActions() {
667     final ToggleShowDirectoriesAction directoriesAction = new ToggleShowDirectoriesAction();
668     final ExpandAllAction expandAllAction = new ExpandAllAction(myTree) {
669       public void update(AnActionEvent e) {
670         e.getPresentation().setVisible(!myShowFlatten);
671       }
672     };
673     final CollapseAllAction collapseAllAction = new CollapseAllAction(myTree) {
674       public void update(AnActionEvent e) {
675         e.getPresentation().setVisible(!myShowFlatten);
676       }
677     };
678     final SelectAllAction selectAllAction = new SelectAllAction();
679     final AnAction[] actions = new AnAction[]{directoriesAction, expandAllAction, collapseAllAction, selectAllAction};
680     directoriesAction.registerCustomShortcutSet(
681       new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_P, SystemInfo.isMac ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK)),
682       this);
683     expandAllAction.registerCustomShortcutSet(
684       new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_EXPAND_ALL)),
685       myTree);
686     collapseAllAction.registerCustomShortcutSet(
687       new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_COLLAPSE_ALL)),
688       myTree);
689     selectAllAction.registerCustomShortcutSet(
690       new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_A, SystemInfo.isMac ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK)),
691       this);
692     return actions;
693   }
694
695   private class MyTreeCellRenderer extends JPanel implements TreeCellRenderer {
696     private final ChangesBrowserNodeRenderer myTextRenderer;
697     private final JCheckBox myCheckBox;
698
699
700     public MyTreeCellRenderer() {
701       super(new BorderLayout());
702       myCheckBox = new JCheckBox();
703       myTextRenderer = new ChangesBrowserNodeRenderer(myProject, false, myHighlightProblems);
704
705       if (myShowCheckboxes) {
706         add(myCheckBox, BorderLayout.WEST);
707       }
708
709       add(myTextRenderer, BorderLayout.CENTER);
710       setOpaque(false);
711     }
712
713     public Component getTreeCellRendererComponent(JTree tree,
714                                                   Object value,
715                                                   boolean selected,
716                                                   boolean expanded,
717                                                   boolean leaf,
718                                                   int row,
719                                                   boolean hasFocus) {
720
721       if (UIUtil.isUnderGTKLookAndFeel() || UIUtil.isUnderNimbusLookAndFeel()) {
722         NonOpaquePanel.setTransparent(this);
723         NonOpaquePanel.setTransparent(myCheckBox);
724       } else {
725         setBackground(null);
726         myCheckBox.setBackground(null);
727         myCheckBox.setOpaque(false);
728       }
729
730       myTextRenderer.setOpaque(false);
731       myTextRenderer.setTransparentIconBackground(true);
732       myTextRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
733       if (myShowCheckboxes) {
734         ChangesBrowserNode node = (ChangesBrowserNode)value;
735
736         CheckboxTree.NodeState state = getNodeStatus(node);
737         myCheckBox.setSelected(state != CheckboxTree.NodeState.CLEAR);
738         myCheckBox.setEnabled(state != CheckboxTree.NodeState.PARTIAL);
739         revalidate();
740
741         return this;
742       }
743       else {
744         return myTextRenderer;
745       }
746     }
747   }
748
749
750   private CheckboxTree.NodeState getNodeStatus(ChangesBrowserNode<T> node) {
751     boolean hasIncluded = false;
752     boolean hasExcluded = false;
753
754     for (T change : getSelectedObjects(node)) {
755       if (myIncludedChanges.contains(change)) {
756         hasIncluded = true;
757       }
758       else {
759         hasExcluded = true;
760       }
761     }
762
763     if (hasIncluded && hasExcluded) return CheckboxTree.NodeState.PARTIAL;
764     if (hasIncluded) return CheckboxTree.NodeState.FULL;
765     return CheckboxTree.NodeState.CLEAR;
766   }
767
768   private class MyListCellRenderer extends JPanel implements ListCellRenderer {
769     private final ColoredListCellRenderer myTextRenderer;
770     public final JCheckBox myCheckbox;
771
772     public MyListCellRenderer() {
773       super(new BorderLayout());
774       myCheckbox = new JCheckBox();
775       myTextRenderer = new VirtualFileListCellRenderer(myProject) {
776         @Override
777         protected void putParentPath(Object value, FilePath path, FilePath self) {
778           super.putParentPath(value, path, self);
779           final boolean applyChangeDecorator = (value instanceof Change) && myChangeDecorator != null;
780           if (applyChangeDecorator) {
781             myChangeDecorator.decorate((Change) value, this, isShowFlatten());
782           }
783         }
784
785         @Override
786         protected void putParentPathImpl(Object value, String parentPath, FilePath self) {
787           final boolean applyChangeDecorator = (value instanceof Change) && myChangeDecorator != null;
788           List<Pair<String,ChangeNodeDecorator.Stress>> parts = null;
789           if (applyChangeDecorator) {
790             parts = myChangeDecorator.stressPartsOfFileName((Change)value, parentPath);
791           }
792           if (parts == null) {
793             super.putParentPathImpl(value, parentPath, self);
794             return;
795           }
796
797           for (Pair<String, ChangeNodeDecorator.Stress> part : parts) {
798             append(part.getFirst(), part.getSecond().derive(SimpleTextAttributes.GRAYED_ATTRIBUTES));
799           }
800         }
801
802         @Override
803         public Component getListCellRendererComponent(JList list,
804                                                       Object value,
805                                                       int index,
806                                                       boolean selected,
807                                                       boolean hasFocus) {
808           final Component component = super.getListCellRendererComponent(list, value, index, selected, hasFocus);
809           final FileColorManager colorManager = FileColorManager.getInstance(myProject);
810           if (!selected) {
811             if (Registry.is("file.colors.in.commit.dialog") && colorManager.isEnabled() && colorManager.isEnabledForProjectView()) {
812               if (value instanceof Change) {
813                 final VirtualFile file = ((Change)value).getVirtualFile();
814                 if (file != null) {
815                   final Color color = colorManager.getFileColor(file);
816                   if (color != null) {
817                       component.setBackground(color);
818                   }
819                 }
820               }
821             }
822           }
823           return component;
824         }
825       };
826
827       myCheckbox.setBackground(null);
828       setBackground(null);
829
830       if (myShowCheckboxes) {
831         add(myCheckbox, BorderLayout.WEST);
832       }
833       add(myTextRenderer, BorderLayout.CENTER);
834     }
835
836     public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
837       myTextRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
838       if (myShowCheckboxes) {
839         myCheckbox.setSelected(myIncludedChanges.contains(value));
840         return this;
841       }
842       else {
843         return myTextRenderer;
844       }
845     }
846   }
847
848   private class MyToggleSelectionAction extends AnAction {
849     public void actionPerformed(AnActionEvent e) {
850       toggleSelection();
851     }
852   }
853
854   public class ToggleShowDirectoriesAction extends ToggleAction {
855     public ToggleShowDirectoriesAction() {
856       super(VcsBundle.message("changes.action.show.directories.text"),
857             VcsBundle.message("changes.action.show.directories.description"),
858             PlatformIcons.DIRECTORY_CLOSED_ICON);
859     }
860
861     public boolean isSelected(AnActionEvent e) {
862       return (! myProject.isDisposed()) && !PropertiesComponent.getInstance(myProject).isTrueValue(FLATTEN_OPTION_KEY);
863     }
864
865     public void setSelected(AnActionEvent e, boolean state) {
866       PropertiesComponent.getInstance(myProject).setValue(FLATTEN_OPTION_KEY, String.valueOf(!state));
867       setShowFlatten(!state);
868     }
869   }
870
871   private class SelectAllAction extends AnAction {
872     private SelectAllAction() {
873       super("Select All", "Select all items", AllIcons.Actions.Selectall);
874     }
875
876     public void actionPerformed(final AnActionEvent e) {
877       if (myShowFlatten) {
878         final int count = myList.getModel().getSize();
879         if (count > 0) {
880           myList.setSelectionInterval(0, count-1);
881         }
882       }
883       else {
884         final int countTree = myTree.getRowCount();
885         if (countTree > 0) {
886           myTree.setSelectionInterval(0, countTree-1);
887         }
888       }
889     }
890   }
891
892   public void select(final List<T> changes) {
893     final DefaultTreeModel treeModel = (DefaultTreeModel) myTree.getModel();
894     final TreeNode root = (TreeNode) treeModel.getRoot();
895     final List<TreePath> treeSelection = new ArrayList<TreePath>(changes.size());
896     TreeUtil.traverse(root, new TreeUtil.Traverse() {
897       public boolean accept(Object node) {
898         final T change = (T) ((DefaultMutableTreeNode) node).getUserObject();
899         if (changes.contains(change)) {
900           treeSelection.add(new TreePath(((DefaultMutableTreeNode) node).getPath()));
901         }
902         return true;
903       }
904     });
905     myTree.setSelectionPaths(treeSelection.toArray(new TreePath[treeSelection.size()]));
906
907     // list
908     final ListModel model = myList.getModel();
909     final int size = model.getSize();
910     final List<Integer> listSelection = new ArrayList<Integer>(changes.size());
911     for (int i = 0; i < size; i++) {
912       final T el = (T) model.getElementAt(i);
913       if (changes.contains(el)) {
914         listSelection.add(i);
915       }
916     }
917     myList.setSelectedIndices(int2int(listSelection));
918   }
919
920   private static int[] int2int(List<Integer> treeSelection) {
921     final int[] toPass = new int[treeSelection.size()];
922     int i = 0;
923     for (Integer integer : treeSelection) {
924       toPass[i] = integer;
925       ++ i;
926     }
927     return toPass;
928   }
929
930   public void enableSelection(final boolean value) {
931     myTree.setEnabled(value);
932   }
933
934   public void setAlwaysExpandList(boolean alwaysExpandList) {
935     myAlwaysExpandList = alwaysExpandList;
936   }
937
938   public void setPaintBusy(final boolean value) {
939     myTree.setPaintBusy(value);
940     myList.setPaintBusy(value);
941   }
942 }