| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.openapi.vcs.changes.ui; |
| |
| import com.intellij.icons.AllIcons; |
| import com.intellij.ide.util.PropertiesComponent; |
| import com.intellij.ide.util.treeView.TreeState; |
| import com.intellij.openapi.actionSystem.*; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diff.DiffBundle; |
| import com.intellij.openapi.keymap.KeymapManager; |
| import com.intellij.openapi.project.DumbAware; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.EmptyRunnable; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.SystemInfo; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.util.registry.Registry; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vcs.FilePath; |
| import com.intellij.openapi.vcs.FilePathImpl; |
| import com.intellij.openapi.vcs.VcsBundle; |
| import com.intellij.openapi.vcs.changes.Change; |
| import com.intellij.openapi.vcs.changes.ChangesUtil; |
| import com.intellij.openapi.vcs.changes.ContentRevision; |
| import com.intellij.openapi.vfs.LocalFileSystem; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.ui.*; |
| import com.intellij.ui.components.JBList; |
| import com.intellij.ui.components.panels.NonOpaquePanel; |
| import com.intellij.ui.treeStructure.Tree; |
| import com.intellij.ui.treeStructure.actions.CollapseAllAction; |
| import com.intellij.ui.treeStructure.actions.ExpandAllAction; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.Convertor; |
| import com.intellij.util.ui.UIUtil; |
| import com.intellij.util.ui.tree.TreeUtil; |
| import com.intellij.util.ui.tree.WideSelectionTreeUI; |
| import gnu.trove.THashSet; |
| import gnu.trove.TIntArrayList; |
| import gnu.trove.TIntHashSet; |
| import org.intellij.lang.annotations.JdkConstants; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.border.Border; |
| import javax.swing.event.ListSelectionEvent; |
| import javax.swing.event.ListSelectionListener; |
| import javax.swing.event.TreeSelectionEvent; |
| import javax.swing.event.TreeSelectionListener; |
| import javax.swing.tree.*; |
| import java.awt.*; |
| import java.awt.event.*; |
| import java.util.*; |
| import java.util.List; |
| |
| /** |
| * @author max |
| */ |
| public abstract class ChangesTreeList<T> extends JPanel implements TypeSafeDataProvider { |
| private final Tree myTree; |
| private final JBList myList; |
| private final JScrollPane myTreeScrollPane; |
| private final JScrollPane myListScrollPane; |
| protected final Project myProject; |
| private final boolean myShowCheckboxes; |
| private final boolean myHighlightProblems; |
| private boolean myShowFlatten; |
| |
| private final Collection<T> myIncludedChanges; |
| private Runnable myDoubleClickHandler = EmptyRunnable.getInstance(); |
| private boolean myAlwaysExpandList; |
| |
| @NonNls private static final String TREE_CARD = "Tree"; |
| @NonNls private static final String LIST_CARD = "List"; |
| @NonNls private static final String ROOT = "root"; |
| private final CardLayout myCards; |
| |
| @NonNls private final static String FLATTEN_OPTION_KEY = "ChangesBrowser.SHOW_FLATTEN"; |
| |
| private final Runnable myInclusionListener; |
| @Nullable private ChangeNodeDecorator myChangeDecorator; |
| private Runnable myGenericSelectionListener; |
| |
| public ChangesTreeList(final Project project, Collection<T> initiallyIncluded, final boolean showCheckboxes, |
| final boolean highlightProblems, @Nullable final Runnable inclusionListener, @Nullable final ChangeNodeDecorator decorator) { |
| myProject = project; |
| myShowCheckboxes = showCheckboxes; |
| myHighlightProblems = highlightProblems; |
| myInclusionListener = inclusionListener; |
| myChangeDecorator = decorator; |
| myIncludedChanges = new HashSet<T>(initiallyIncluded); |
| myAlwaysExpandList = true; |
| |
| myCards = new CardLayout(); |
| |
| setLayout(myCards); |
| |
| final int checkboxWidth = new JCheckBox().getPreferredSize().width; |
| myTree = new MyTree(project, checkboxWidth); |
| myTree.setHorizontalAutoScrollingEnabled(false); |
| myTree.setRootVisible(false); |
| myTree.setShowsRootHandles(true); |
| myTree.setOpaque(false); |
| myTree.setCellRenderer(new MyTreeCellRenderer()); |
| new TreeSpeedSearch(myTree, new Convertor<TreePath, String>() { |
| @Override |
| public String convert(TreePath o) { |
| ChangesBrowserNode node = (ChangesBrowserNode) o.getLastPathComponent(); |
| return node.getTextPresentation(); |
| } |
| }); |
| |
| myList = new JBList(new DefaultListModel()); |
| myList.setVisibleRowCount(10); |
| |
| add(myListScrollPane = ScrollPaneFactory.createScrollPane(myList), LIST_CARD); |
| add(myTreeScrollPane = ScrollPaneFactory.createScrollPane(myTree), TREE_CARD); |
| |
| new ListSpeedSearch(myList) { |
| @Override |
| protected String getElementText(Object element) { |
| if (element instanceof Change) { |
| return ChangesUtil.getFilePath((Change)element).getName(); |
| } |
| return super.getElementText(element); |
| } |
| }; |
| |
| myList.setCellRenderer(new MyListCellRenderer()); |
| |
| new MyToggleSelectionAction().registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0)), this); |
| if (myShowCheckboxes) { |
| registerKeyboardAction(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| includeSelection(); |
| } |
| |
| }, KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); |
| |
| registerKeyboardAction(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| excludeSelection(); |
| } |
| }, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); |
| } |
| |
| registerKeyboardAction(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| myDoubleClickHandler.run(); |
| } |
| }, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); |
| |
| myTree.addKeyListener(new KeyAdapter() { |
| @Override |
| public void keyPressed(KeyEvent e) { |
| if (KeyEvent.VK_ENTER == e.getKeyCode() && e.getModifiers() == 0) { |
| if (myTree.getSelectionCount() <= 1) { |
| Object lastPathComponent = myTree.getLastSelectedPathComponent(); |
| if (!(lastPathComponent instanceof DefaultMutableTreeNode)) { |
| return; |
| } |
| DefaultMutableTreeNode node = (DefaultMutableTreeNode)lastPathComponent; |
| if (!node.isLeaf()) { |
| return; |
| } |
| } |
| myDoubleClickHandler.run(); |
| e.consume(); |
| } |
| } |
| }); |
| |
| new ClickListener() { |
| @Override |
| public boolean onClick(@NotNull MouseEvent e, int clickCount) { |
| final int idx = myList.locationToIndex(e.getPoint()); |
| if (idx >= 0) { |
| final Rectangle baseRect = myList.getCellBounds(idx, idx); |
| baseRect.setSize(checkboxWidth, baseRect.height); |
| if (baseRect.contains(e.getPoint())) { |
| toggleSelection(); |
| return true; |
| } |
| else if (clickCount == 2) { |
| myDoubleClickHandler.run(); |
| return true; |
| } |
| } |
| return false; |
| } |
| }.installOn(myList); |
| |
| new DoubleClickListener() { |
| @Override |
| protected boolean onDoubleClick(MouseEvent e) { |
| final TreePath clickPath = myTree.getUI() instanceof WideSelectionTreeUI |
| ? myTree.getClosestPathForLocation(e.getX(), e.getY()) |
| : myTree.getPathForLocation(e.getX(), e.getY()); |
| if (clickPath == null) return false; |
| |
| myDoubleClickHandler.run(); |
| return true; |
| } |
| }.installOn(myTree); |
| |
| setShowFlatten(PropertiesComponent.getInstance(myProject).isTrueValue(FLATTEN_OPTION_KEY)); |
| |
| String emptyText = StringUtil.capitalize(DiffBundle.message("diff.count.differences.status.text", 0)); |
| setEmptyText(emptyText); |
| } |
| |
| public void setEmptyText(@NotNull String emptyText) { |
| myTree.getEmptyText().setText(emptyText); |
| myList.getEmptyText().setText(emptyText); |
| } |
| |
| // generic, both for tree and list |
| public void addSelectionListener(final Runnable runnable) { |
| myGenericSelectionListener = runnable; |
| myList.addListSelectionListener(new ListSelectionListener() { |
| @Override |
| public void valueChanged(ListSelectionEvent e) { |
| myGenericSelectionListener.run(); |
| } |
| }); |
| myTree.addTreeSelectionListener(new TreeSelectionListener() { |
| @Override |
| public void valueChanged(TreeSelectionEvent e) { |
| myGenericSelectionListener.run(); |
| } |
| }); |
| } |
| |
| public void setChangeDecorator(@Nullable ChangeNodeDecorator changeDecorator) { |
| myChangeDecorator = changeDecorator; |
| } |
| |
| public void setDoubleClickHandler(final Runnable doubleClickHandler) { |
| myDoubleClickHandler = doubleClickHandler; |
| } |
| |
| public void installPopupHandler(ActionGroup group) { |
| PopupHandler.installUnknownPopupHandler(myList, group, ActionManager.getInstance()); |
| PopupHandler.installUnknownPopupHandler(myTree, group, ActionManager.getInstance()); |
| } |
| |
| public JComponent getPreferredFocusedComponent() { |
| return myShowFlatten ? myList : myTree; |
| } |
| |
| @Override |
| public Dimension getPreferredSize() { |
| return new Dimension(400, 400); |
| } |
| |
| public boolean isShowFlatten() { |
| return myShowFlatten; |
| } |
| |
| public void setScrollPaneBorder(Border border) { |
| myListScrollPane.setBorder(border); |
| myTreeScrollPane.setBorder(border); |
| } |
| |
| public void setShowFlatten(final boolean showFlatten) { |
| final List<T> wasSelected = getSelectedChanges(); |
| myShowFlatten = showFlatten; |
| myCards.show(this, myShowFlatten ? LIST_CARD : TREE_CARD); |
| select(wasSelected); |
| if (myList.hasFocus() || myTree.hasFocus()) { |
| SwingUtilities.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| requestFocus(); |
| } |
| }); |
| } |
| } |
| |
| |
| @Override |
| public void requestFocus() { |
| if (myShowFlatten) { |
| myList.requestFocus(); |
| } |
| else { |
| myTree.requestFocus(); |
| } |
| } |
| |
| public void setChangesToDisplay(final List<T> changes) { |
| setChangesToDisplay(changes, null); |
| } |
| |
| public void setChangesToDisplay(final List<T> changes, @Nullable final VirtualFile toSelect) { |
| final boolean wasEmpty = myList.isEmpty(); |
| final List<T> sortedChanges = new ArrayList<T>(changes); |
| Collections.sort(sortedChanges, new Comparator<T>() { |
| @Override |
| public int compare(final T o1, final T o2) { |
| return TreeModelBuilder.getPathForObject(o1).getName().compareToIgnoreCase(TreeModelBuilder.getPathForObject(o2).getName()); |
| } |
| }); |
| |
| @SuppressWarnings("deprecation") |
| final Set<Object> wasSelected = new THashSet<Object>(Arrays.asList(myList.getSelectedValues())); |
| //noinspection unchecked |
| myList.setModel(new AbstractListModel() { |
| @Override |
| public int getSize() { |
| return sortedChanges.size(); |
| } |
| |
| @Override |
| public Object getElementAt(int index) { |
| return sortedChanges.get(index); |
| } |
| }); |
| |
| final DefaultTreeModel model = buildTreeModel(changes, myChangeDecorator); |
| TreeState state = null; |
| if (! myAlwaysExpandList && ! wasEmpty) { |
| state = TreeState.createOn(myTree, (DefaultMutableTreeNode) myTree.getModel().getRoot()); |
| } |
| myTree.setModel(model); |
| if (! myAlwaysExpandList && ! wasEmpty) { |
| //noinspection ConstantConditions |
| state.applyTo(myTree, (DefaultMutableTreeNode) myTree.getModel().getRoot()); |
| |
| final TIntArrayList indices = new TIntArrayList(); |
| for (int i = 0; i < sortedChanges.size(); i++) { |
| T t = sortedChanges.get(i); |
| if (wasSelected.contains(t)) { |
| indices.add(i); |
| } |
| } |
| myList.setSelectedIndices(indices.toNativeArray()); |
| return; |
| } |
| |
| final Runnable runnable = new Runnable() { |
| @Override |
| public void run() { |
| if (myProject.isDisposed()) return; |
| TreeUtil.expandAll(myTree); |
| |
| int listSelection = 0; |
| int scrollRow = 0; |
| |
| if (myShowCheckboxes) { |
| if (myIncludedChanges.size() > 0) { |
| for (int i = 0; i < sortedChanges.size(); i++) { |
| T t = sortedChanges.get(i); |
| if (myIncludedChanges.contains(t)) { |
| listSelection = i; |
| break; |
| } |
| } |
| |
| ChangesBrowserNode root = (ChangesBrowserNode)model.getRoot(); |
| Enumeration enumeration = root.depthFirstEnumeration(); |
| |
| while (enumeration.hasMoreElements()) { |
| ChangesBrowserNode node = (ChangesBrowserNode)enumeration.nextElement(); |
| @SuppressWarnings("unchecked") |
| final CheckboxTree.NodeState state = getNodeStatus(node); |
| if (node != root && state == CheckboxTree.NodeState.CLEAR) { |
| myTree.collapsePath(new TreePath(node.getPath())); |
| } |
| } |
| |
| enumeration = root.depthFirstEnumeration(); |
| while (enumeration.hasMoreElements()) { |
| ChangesBrowserNode node = (ChangesBrowserNode)enumeration.nextElement(); |
| @SuppressWarnings("unchecked") |
| final CheckboxTree.NodeState state = getNodeStatus(node); |
| if (state == CheckboxTree.NodeState.FULL && node.isLeaf()) { |
| scrollRow = myTree.getRowForPath(new TreePath(node.getPath())); |
| break; |
| } |
| } |
| } |
| } else { |
| if (toSelect != null) { |
| ChangesBrowserNode root = (ChangesBrowserNode)model.getRoot(); |
| final int[] rowToSelect = new int[] {-1}; |
| TreeUtil.traverse(root, new TreeUtil.Traverse() { |
| @Override |
| public boolean accept(Object node) { |
| if (node instanceof DefaultMutableTreeNode) { |
| Object userObject = ((DefaultMutableTreeNode)node).getUserObject(); |
| if (userObject instanceof Change) { |
| Change change = (Change)userObject; |
| VirtualFile virtualFile = change.getVirtualFile(); |
| if ((virtualFile != null && virtualFile.equals(toSelect)) || seemsToBeMoved(change, toSelect)) { |
| TreeNode[] path = ((DefaultMutableTreeNode)node).getPath(); |
| rowToSelect[0] = myTree.getRowForPath(new TreePath(path)); |
| } |
| } |
| } |
| |
| return rowToSelect[0] == -1; |
| } |
| }); |
| |
| scrollRow = rowToSelect[0] == -1 ? scrollRow : rowToSelect[0]; |
| } |
| } |
| |
| if (changes.size() > 0) { |
| myList.setSelectedIndex(listSelection); |
| myList.ensureIndexIsVisible(listSelection); |
| |
| myTree.setSelectionRow(scrollRow); |
| TreeUtil.showRowCentered(myTree, scrollRow, false); |
| } |
| } |
| }; |
| if (ApplicationManager.getApplication().isDispatchThread()) { |
| runnable.run(); |
| } else { |
| SwingUtilities.invokeLater(runnable); |
| } |
| } |
| |
| private static boolean seemsToBeMoved(Change change, VirtualFile toSelect) { |
| ContentRevision afterRevision = change.getAfterRevision(); |
| if (afterRevision == null) return false; |
| FilePath file = afterRevision.getFile(); |
| return FileUtil.pathsEqual(file.getPath(), toSelect.getPath()); |
| } |
| |
| protected abstract DefaultTreeModel buildTreeModel(final List<T> changes, final ChangeNodeDecorator changeNodeDecorator); |
| |
| @SuppressWarnings({"SuspiciousMethodCalls"}) |
| private void toggleSelection() { |
| boolean hasExcluded = false; |
| for (T value : getSelectedChanges()) { |
| if (!myIncludedChanges.contains(value)) { |
| hasExcluded = true; |
| } |
| } |
| |
| if (hasExcluded) { |
| includeSelection(); |
| } |
| else { |
| excludeSelection(); |
| } |
| |
| repaint(); |
| } |
| |
| private void includeSelection() { |
| for (T change : getSelectedChanges()) { |
| myIncludedChanges.add(change); |
| } |
| notifyInclusionListener(); |
| repaint(); |
| } |
| |
| @SuppressWarnings({"SuspiciousMethodCalls"}) |
| private void excludeSelection() { |
| for (T change : getSelectedChanges()) { |
| myIncludedChanges.remove(change); |
| } |
| notifyInclusionListener(); |
| repaint(); |
| } |
| |
| public List<T> getChanges() { |
| if (myShowFlatten) { |
| ListModel m = myList.getModel(); |
| int size = m.getSize(); |
| List<T> result = new ArrayList<T>(size); |
| for (int i = 0; i < size; i++) { |
| //noinspection unchecked |
| result.add((T)m.getElementAt(i)); |
| } |
| return result; |
| } |
| else { |
| final LinkedHashSet<T> result = new LinkedHashSet<T>(); |
| TreeUtil.traverseDepth((ChangesBrowserNode)myTree.getModel().getRoot(), new TreeUtil.Traverse() { |
| @Override |
| public boolean accept(Object node) { |
| ChangesBrowserNode changeNode = (ChangesBrowserNode)node; |
| if (changeNode.isLeaf()) { |
| //noinspection unchecked |
| result.addAll(changeNode.getAllChangesUnder()); |
| } |
| return true; |
| } |
| }); |
| return new ArrayList<T>(result); |
| } |
| } |
| |
| public int getSelectionCount() { |
| if (myShowFlatten) { |
| return myList.getSelectedIndices().length; |
| } else { |
| return myTree.getSelectionCount(); |
| } |
| } |
| |
| @NotNull |
| public List<T> getSelectedChanges() { |
| if (myShowFlatten) { |
| final List<T> changes = new ArrayList<T>(); |
| //noinspection deprecation |
| for (Object anO : myList.getSelectedValues()) { |
| //noinspection unchecked |
| changes.add((T)anO); |
| } |
| return changes; |
| } |
| else { |
| final TreePath[] paths = myTree.getSelectionPaths(); |
| if (paths == null) { |
| return Collections.emptyList(); |
| } |
| else { |
| final List<T> changes = new ArrayList<T>(); |
| final TIntHashSet checkSet = new TIntHashSet(); |
| for (TreePath path : paths) { |
| //noinspection unchecked |
| List<T> list = getSelectedObjects((ChangesBrowserNode)path.getLastPathComponent()); |
| for (T object : list) { |
| if (checkSet.add(object.hashCode()) || !changes.contains(object)) { |
| changes.add(object); |
| } |
| } |
| } |
| return changes; |
| } |
| } |
| } |
| |
| protected abstract List<T> getSelectedObjects(final ChangesBrowserNode<T> node); |
| |
| @Nullable |
| protected abstract T getLeadSelectedObject(final ChangesBrowserNode node); |
| |
| @Nullable |
| public T getHighestLeadSelection() { |
| if (myShowFlatten) { |
| final int index = myList.getLeadSelectionIndex(); |
| ListModel listModel = myList.getModel(); |
| if (index < 0 || index >= listModel.getSize()) return null; |
| //noinspection unchecked |
| return (T)listModel.getElementAt(index); |
| } |
| else { |
| final TreePath path = myTree.getSelectionPath(); |
| if (path == null) { |
| return null; |
| } |
| //noinspection unchecked |
| return getLeadSelectedObject((ChangesBrowserNode<T>)path.getLastPathComponent()); |
| } |
| } |
| |
| @Nullable |
| public T getLeadSelection() { |
| if (myShowFlatten) { |
| final int index = myList.getLeadSelectionIndex(); |
| ListModel listModel = myList.getModel(); |
| //noinspection unchecked |
| return index < 0 || index >= listModel.getSize() ? null : (T)listModel.getElementAt(index); |
| } |
| else { |
| final TreePath path = myTree.getSelectionPath(); |
| //noinspection unchecked |
| return path == null ? null : ContainerUtil.getFirstItem(getSelectedObjects(((ChangesBrowserNode<T>)path.getLastPathComponent()))); |
| } |
| } |
| |
| private void notifyInclusionListener() { |
| if (myInclusionListener != null) { |
| myInclusionListener.run(); |
| } |
| } |
| |
| // no listener supposed to be called |
| public void setIncludedChanges(final Collection<T> changes) { |
| myIncludedChanges.clear(); |
| myIncludedChanges.addAll(changes); |
| myTree.repaint(); |
| myList.repaint(); |
| } |
| |
| public void includeChange(final T change) { |
| myIncludedChanges.add(change); |
| notifyInclusionListener(); |
| myTree.repaint(); |
| myList.repaint(); |
| } |
| |
| public void includeChanges(final Collection<T> changes) { |
| myIncludedChanges.addAll(changes); |
| notifyInclusionListener(); |
| myTree.repaint(); |
| myList.repaint(); |
| } |
| |
| public void excludeChange(final T change) { |
| myIncludedChanges.remove(change); |
| notifyInclusionListener(); |
| myTree.repaint(); |
| myList.repaint(); |
| } |
| |
| public void excludeChanges(final Collection<T> changes) { |
| myIncludedChanges.removeAll(changes); |
| notifyInclusionListener(); |
| myTree.repaint(); |
| myList.repaint(); |
| } |
| |
| public boolean isIncluded(final T change) { |
| return myIncludedChanges.contains(change); |
| } |
| |
| public Collection<T> getIncludedChanges() { |
| return myIncludedChanges; |
| } |
| |
| public void expandAll() { |
| TreeUtil.expandAll(myTree); |
| } |
| |
| public AnAction[] getTreeActions() { |
| final ToggleShowDirectoriesAction directoriesAction = new ToggleShowDirectoriesAction(); |
| final ExpandAllAction expandAllAction = new ExpandAllAction(myTree) { |
| @Override |
| public void update(AnActionEvent e) { |
| e.getPresentation().setVisible(!myShowFlatten); |
| } |
| }; |
| final CollapseAllAction collapseAllAction = new CollapseAllAction(myTree) { |
| @Override |
| public void update(AnActionEvent e) { |
| e.getPresentation().setVisible(!myShowFlatten); |
| } |
| }; |
| final AnAction[] actions = new AnAction[]{directoriesAction, expandAllAction, collapseAllAction}; |
| directoriesAction.registerCustomShortcutSet( |
| new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_P, SystemInfo.isMac ? InputEvent.META_DOWN_MASK : InputEvent.CTRL_DOWN_MASK)), |
| this); |
| expandAllAction.registerCustomShortcutSet( |
| new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_EXPAND_ALL)), |
| myTree); |
| collapseAllAction.registerCustomShortcutSet( |
| new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_COLLAPSE_ALL)), |
| myTree); |
| return actions; |
| } |
| |
| public void setSelectionMode(@JdkConstants.ListSelectionMode int mode) { |
| myList.setSelectionMode(mode); |
| myTree.getSelectionModel().setSelectionMode(getTreeSelectionModeFromListSelectionMode(mode)); |
| } |
| |
| @JdkConstants.TreeSelectionMode |
| private static int getTreeSelectionModeFromListSelectionMode(@JdkConstants.ListSelectionMode int mode) { |
| switch (mode) { |
| case ListSelectionModel.SINGLE_SELECTION: return TreeSelectionModel.SINGLE_TREE_SELECTION; |
| case ListSelectionModel.SINGLE_INTERVAL_SELECTION: return TreeSelectionModel.CONTIGUOUS_TREE_SELECTION; |
| case ListSelectionModel.MULTIPLE_INTERVAL_SELECTION: return TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION; |
| } |
| throw new IllegalArgumentException("Illegal selection mode: " + mode); |
| } |
| |
| private class MyTreeCellRenderer extends JPanel implements TreeCellRenderer { |
| private final ChangesBrowserNodeRenderer myTextRenderer; |
| private final JCheckBox myCheckBox; |
| |
| |
| public MyTreeCellRenderer() { |
| super(new BorderLayout()); |
| myCheckBox = new JCheckBox(); |
| myTextRenderer = new ChangesBrowserNodeRenderer(myProject, false, myHighlightProblems); |
| |
| if (myShowCheckboxes) { |
| add(myCheckBox, BorderLayout.WEST); |
| } |
| |
| add(myTextRenderer, BorderLayout.CENTER); |
| setOpaque(false); |
| } |
| |
| @Override |
| public Component getTreeCellRendererComponent(JTree tree, |
| Object value, |
| boolean selected, |
| boolean expanded, |
| boolean leaf, |
| int row, |
| boolean hasFocus) { |
| |
| if (UIUtil.isUnderGTKLookAndFeel() || UIUtil.isUnderNimbusLookAndFeel()) { |
| NonOpaquePanel.setTransparent(this); |
| NonOpaquePanel.setTransparent(myCheckBox); |
| } else { |
| setBackground(null); |
| myCheckBox.setBackground(null); |
| myCheckBox.setOpaque(false); |
| } |
| |
| myTextRenderer.setOpaque(false); |
| myTextRenderer.setTransparentIconBackground(true); |
| myTextRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); |
| if (myShowCheckboxes) { |
| @SuppressWarnings("unchecked") |
| CheckboxTree.NodeState state = getNodeStatus((ChangesBrowserNode)value); |
| myCheckBox.setSelected(state != CheckboxTree.NodeState.CLEAR); |
| myCheckBox.setEnabled(state != CheckboxTree.NodeState.PARTIAL); |
| revalidate(); |
| |
| return this; |
| } |
| else { |
| return myTextRenderer; |
| } |
| } |
| } |
| |
| |
| private CheckboxTree.NodeState getNodeStatus(ChangesBrowserNode<T> node) { |
| boolean hasIncluded = false; |
| boolean hasExcluded = false; |
| |
| for (T change : getSelectedObjects(node)) { |
| if (myIncludedChanges.contains(change)) { |
| hasIncluded = true; |
| } |
| else { |
| hasExcluded = true; |
| } |
| } |
| |
| if (hasIncluded && hasExcluded) return CheckboxTree.NodeState.PARTIAL; |
| if (hasIncluded) return CheckboxTree.NodeState.FULL; |
| return CheckboxTree.NodeState.CLEAR; |
| } |
| |
| private class MyListCellRenderer extends JPanel implements ListCellRenderer { |
| private final ColoredListCellRenderer myTextRenderer; |
| public final JCheckBox myCheckbox; |
| |
| public MyListCellRenderer() { |
| super(new BorderLayout()); |
| myCheckbox = new JCheckBox(); |
| myTextRenderer = new VirtualFileListCellRenderer(myProject) { |
| @Override |
| protected void putParentPath(Object value, FilePath path, FilePath self) { |
| super.putParentPath(value, path, self); |
| final boolean applyChangeDecorator = (value instanceof Change) && myChangeDecorator != null; |
| if (applyChangeDecorator) { |
| myChangeDecorator.decorate((Change) value, this, isShowFlatten()); |
| } |
| } |
| |
| @Override |
| protected void putParentPathImpl(Object value, String parentPath, FilePath self) { |
| final boolean applyChangeDecorator = (value instanceof Change) && myChangeDecorator != null; |
| List<Pair<String,ChangeNodeDecorator.Stress>> parts = null; |
| if (applyChangeDecorator) { |
| parts = myChangeDecorator.stressPartsOfFileName((Change)value, parentPath); |
| } |
| if (parts == null) { |
| super.putParentPathImpl(value, parentPath, self); |
| return; |
| } |
| |
| for (Pair<String, ChangeNodeDecorator.Stress> part : parts) { |
| append(part.getFirst(), part.getSecond().derive(SimpleTextAttributes.GRAYED_ATTRIBUTES)); |
| } |
| } |
| |
| @Override |
| public Component getListCellRendererComponent(JList list, |
| Object value, |
| int index, |
| boolean selected, |
| boolean hasFocus) { |
| final Component component = super.getListCellRendererComponent(list, value, index, selected, hasFocus); |
| final FileColorManager colorManager = FileColorManager.getInstance(myProject); |
| if (!selected) { |
| if (Registry.is("file.colors.in.commit.dialog") && colorManager.isEnabled() && colorManager.isEnabledForProjectView()) { |
| if (value instanceof Change) { |
| final VirtualFile file = ((Change)value).getVirtualFile(); |
| if (file != null) { |
| final Color color = colorManager.getFileColor(file); |
| if (color != null) { |
| component.setBackground(color); |
| } |
| } |
| } |
| } |
| } |
| return component; |
| } |
| }; |
| |
| myCheckbox.setBackground(null); |
| setBackground(null); |
| |
| if (myShowCheckboxes) { |
| add(myCheckbox, BorderLayout.WEST); |
| } |
| add(myTextRenderer, BorderLayout.CENTER); |
| } |
| |
| @Override |
| public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { |
| myTextRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); |
| if (myShowCheckboxes) { |
| //noinspection SuspiciousMethodCalls |
| myCheckbox.setSelected(myIncludedChanges.contains(value)); |
| return this; |
| } |
| else { |
| return myTextRenderer; |
| } |
| } |
| } |
| |
| private class MyToggleSelectionAction extends AnAction implements DumbAware { |
| @Override |
| public void actionPerformed(AnActionEvent e) { |
| toggleSelection(); |
| } |
| } |
| |
| public class ToggleShowDirectoriesAction extends ToggleAction implements DumbAware { |
| public ToggleShowDirectoriesAction() { |
| super(VcsBundle.message("changes.action.show.directories.text"), |
| VcsBundle.message("changes.action.show.directories.description"), |
| AllIcons.Actions.GroupByPackage); |
| } |
| |
| @Override |
| public boolean isSelected(AnActionEvent e) { |
| return (! myProject.isDisposed()) && !PropertiesComponent.getInstance(myProject).isTrueValue(FLATTEN_OPTION_KEY); |
| } |
| |
| @Override |
| public void setSelected(AnActionEvent e, boolean state) { |
| PropertiesComponent.getInstance(myProject).setValue(FLATTEN_OPTION_KEY, String.valueOf(!state)); |
| setShowFlatten(!state); |
| } |
| } |
| |
| private class SelectAllAction extends AnAction { |
| private SelectAllAction() { |
| super("Select All", "Select all items", AllIcons.Actions.Selectall); |
| } |
| |
| @Override |
| public void actionPerformed(final AnActionEvent e) { |
| if (myShowFlatten) { |
| final int count = myList.getModel().getSize(); |
| if (count > 0) { |
| myList.setSelectionInterval(0, count-1); |
| } |
| } |
| else { |
| final int countTree = myTree.getRowCount(); |
| if (countTree > 0) { |
| myTree.setSelectionInterval(0, countTree-1); |
| } |
| } |
| } |
| } |
| |
| public void select(final List<T> changes) { |
| final DefaultTreeModel treeModel = (DefaultTreeModel) myTree.getModel(); |
| final TreeNode root = (TreeNode) treeModel.getRoot(); |
| final List<TreePath> treeSelection = new ArrayList<TreePath>(changes.size()); |
| TreeUtil.traverse(root, new TreeUtil.Traverse() { |
| @Override |
| public boolean accept(Object node) { |
| @SuppressWarnings("unchecked") |
| final T change = (T) ((DefaultMutableTreeNode) node).getUserObject(); |
| if (changes.contains(change)) { |
| treeSelection.add(new TreePath(((DefaultMutableTreeNode) node).getPath())); |
| } |
| return true; |
| } |
| }); |
| myTree.setSelectionPaths(treeSelection.toArray(new TreePath[treeSelection.size()])); |
| |
| // list |
| final ListModel model = myList.getModel(); |
| final int size = model.getSize(); |
| final List<Integer> listSelection = new ArrayList<Integer>(changes.size()); |
| for (int i = 0; i < size; i++) { |
| @SuppressWarnings("unchecked") |
| final T el = (T) model.getElementAt(i); |
| if (changes.contains(el)) { |
| listSelection.add(i); |
| } |
| } |
| myList.setSelectedIndices(int2int(listSelection)); |
| } |
| |
| private static int[] int2int(List<Integer> treeSelection) { |
| final int[] toPass = new int[treeSelection.size()]; |
| int i = 0; |
| for (Integer integer : treeSelection) { |
| toPass[i] = integer; |
| ++ i; |
| } |
| return toPass; |
| } |
| |
| public void enableSelection(final boolean value) { |
| myTree.setEnabled(value); |
| } |
| |
| public void setAlwaysExpandList(boolean alwaysExpandList) { |
| myAlwaysExpandList = alwaysExpandList; |
| } |
| |
| public void setPaintBusy(final boolean value) { |
| myTree.setPaintBusy(value); |
| myList.setPaintBusy(value); |
| } |
| |
| @Override |
| public void calcData(DataKey key, DataSink sink) { |
| } |
| |
| private class MyTree extends Tree implements TypeSafeDataProvider { |
| |
| private final Project myProject; |
| private final int myCheckboxWidth; |
| |
| public MyTree(Project project, int checkboxWidth) { |
| super(ChangesBrowserNode.create(ChangesTreeList.this.myProject, ROOT)); |
| myProject = project; |
| myCheckboxWidth = checkboxWidth; |
| } |
| |
| @Override |
| public boolean isFileColorsEnabled() { |
| final boolean enabled = Registry.is("file.colors.in.commit.dialog") |
| && FileColorManager.getInstance(myProject).isEnabled() |
| && FileColorManager.getInstance(myProject).isEnabledForProjectView(); |
| final boolean opaque = isOpaque(); |
| if (enabled && opaque) { |
| setOpaque(false); |
| } else if (!enabled && !opaque) { |
| setOpaque(true); |
| } |
| return enabled; |
| } |
| |
| @Override |
| public Color getFileColorFor(Object object) { |
| VirtualFile file = null; |
| if (object instanceof FilePathImpl) { |
| file = LocalFileSystem.getInstance().findFileByPath(((FilePathImpl)object).getPath()); |
| } else if (object instanceof Change) { |
| file = ((Change)object).getVirtualFile(); |
| } |
| |
| if (file != null) { |
| return FileColorManager.getInstance(myProject).getFileColor(file); |
| } |
| return super.getFileColorFor(object); |
| } |
| |
| @Override |
| public Dimension getPreferredScrollableViewportSize() { |
| Dimension size = super.getPreferredScrollableViewportSize(); |
| size = new Dimension(size.width + 10, size.height); |
| return size; |
| } |
| |
| @Override |
| protected void processMouseEvent(MouseEvent e) { |
| if (e.getID() == MouseEvent.MOUSE_PRESSED) { |
| if (! myTree.isEnabled()) return; |
| int row = myTree.getRowForLocation(e.getX(), e.getY()); |
| if (row >= 0) { |
| final Rectangle baseRect = myTree.getRowBounds(row); |
| baseRect.setSize(myCheckboxWidth, baseRect.height); |
| if (baseRect.contains(e.getPoint())) { |
| myTree.setSelectionRow(row); |
| toggleSelection(); |
| } |
| } |
| } |
| super.processMouseEvent(e); |
| } |
| |
| @Override |
| public int getToggleClickCount() { |
| return -1; |
| } |
| |
| @Override |
| public void calcData(DataKey key, DataSink sink) { |
| // just delegate to the change list |
| ChangesTreeList.this.calcData(key, sink); |
| } |
| } |
| } |