1 // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.dvcs.push.ui;
4 import com.intellij.dvcs.push.PushSettings;
5 import com.intellij.dvcs.ui.DvcsBundle;
6 import com.intellij.icons.AllIcons;
7 import com.intellij.openapi.actionSystem.ActionManager;
8 import com.intellij.openapi.actionSystem.AnActionEvent;
9 import com.intellij.openapi.actionSystem.CommonShortcuts;
10 import com.intellij.openapi.actionSystem.DataProvider;
11 import com.intellij.openapi.project.DumbAware;
12 import com.intellij.openapi.project.Project;
13 import com.intellij.openapi.vcs.VcsDataKeys;
14 import com.intellij.openapi.vcs.changes.Change;
15 import com.intellij.openapi.vcs.changes.TextRevisionNumber;
16 import com.intellij.openapi.vcs.changes.committed.CommittedChangesTreeBrowser;
17 import com.intellij.openapi.vcs.changes.ui.EditSourceForDialogAction;
18 import com.intellij.openapi.vcs.changes.ui.SimpleChangesBrowser;
19 import com.intellij.openapi.vcs.history.VcsRevisionNumber;
20 import com.intellij.ui.*;
21 import com.intellij.ui.components.JBScrollPane;
22 import com.intellij.ui.components.JBViewport;
23 import com.intellij.ui.components.labels.LinkLabel;
24 import com.intellij.ui.components.labels.LinkListener;
25 import com.intellij.ui.render.RenderingUtil;
26 import com.intellij.ui.treeStructure.actions.CollapseAllAction;
27 import com.intellij.ui.treeStructure.actions.ExpandAllAction;
28 import com.intellij.util.containers.ContainerUtil;
29 import com.intellij.util.ui.JBUI;
30 import com.intellij.util.ui.ThreeStateCheckBox;
31 import com.intellij.util.ui.components.BorderLayoutPanel;
32 import com.intellij.util.ui.tree.TreeUtil;
33 import com.intellij.util.ui.tree.WideSelectionTreeUI;
34 import com.intellij.vcs.log.Hash;
35 import com.intellij.vcs.log.VcsFullCommitDetails;
36 import com.intellij.vcs.log.ui.VcsLogActionIds;
37 import com.intellij.vcs.log.ui.details.commit.CommitDetailsPanel;
38 import com.intellij.vcs.log.ui.frame.CommitPresentationUtil;
40 import one.util.streamex.StreamEx;
41 import org.jetbrains.annotations.Nls;
42 import org.jetbrains.annotations.NonNls;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
47 import javax.swing.event.*;
48 import javax.swing.tree.*;
50 import java.awt.event.*;
51 import java.beans.PropertyChangeEvent;
52 import java.beans.PropertyChangeListener;
53 import java.util.List;
55 import java.util.function.Consumer;
57 import static com.intellij.openapi.actionSystem.IdeActions.ACTION_COLLAPSE_ALL;
58 import static com.intellij.openapi.actionSystem.IdeActions.ACTION_EXPAND_ALL;
59 import static com.intellij.util.containers.ContainerUtil.emptyList;
61 public final class PushLog extends JPanel implements DataProvider {
62 @NonNls private static final String CONTEXT_MENU = "Vcs.Push.ContextMenu";
63 @NonNls private static final String START_EDITING = "startEditing";
64 @NonNls private static final String TREE_SPLITTER_PROPORTION = "Vcs.Push.Splitter.Tree.Proportion";
65 @NonNls private static final String DETAILS_SPLITTER_PROPORTION = "Vcs.Push.Splitter.Details.Proportion";
66 private final SimpleChangesBrowser myChangesBrowser;
67 private final CheckboxTree myTree;
68 private final MyTreeCellRenderer myTreeCellRenderer;
69 private final JScrollPane myScrollPane;
70 private final CommitDetailsPanel myDetailsPanel;
71 private final MyShowDetailsAction myShowDetailsAction;
72 private boolean myShouldRepaint = false;
73 private boolean mySyncStrategy;
74 @Nullable private @Nls String mySyncRenderedText;
75 private final @NotNull Project myProject;
76 private final boolean myAllowSyncStrategy;
78 public PushLog(@NotNull Project project, final CheckedTreeNode root, final boolean allowSyncStrategy) {
80 myAllowSyncStrategy = allowSyncStrategy;
81 DefaultTreeModel treeModel = new DefaultTreeModel(root);
82 treeModel.nodeStructureChanged(root);
83 myTreeCellRenderer = new MyTreeCellRenderer();
84 myTree = new CheckboxTree(myTreeCellRenderer, root) {
87 protected boolean shouldShowBusyIconIfNeeded() {
92 public boolean isPathEditable(TreePath path) {
93 return isEditable() && path.getLastPathComponent() instanceof DefaultMutableTreeNode;
97 protected void onNodeStateChanged(CheckedTreeNode node) {
98 if (node instanceof EditableTreeNode) {
99 ((EditableTreeNode)node).fireOnSelectionChange(node.isChecked());
104 public String getToolTipText(MouseEvent event) {
105 final TreePath path = myTree.getPathForLocation(event.getX(), event.getY());
109 Object node = path.getLastPathComponent();
110 if ((!(node instanceof DefaultMutableTreeNode))) {
113 if (node instanceof TooltipNode) {
114 String select = DvcsBundle.message("push.select.all.commit.details");
115 return ((TooltipNode)node).getTooltip() + "<p style='font-style:italic;color:gray;'>" + select + "</p>"; //NON-NLS
121 public boolean stopEditing() {
122 DefaultMutableTreeNode node = (DefaultMutableTreeNode)myTree.getLastSelectedPathComponent();
123 if (node instanceof EditableTreeNode) {
124 JComponent editedComponent = (JComponent)node.getUserObject();
125 InputVerifier verifier = editedComponent.getInputVerifier();
126 if (verifier != null && !verifier.verify(editedComponent)) return false;
128 boolean result = super.stopEditing();
129 if (myShouldRepaint) {
132 restoreSelection(node);
137 public void cancelEditing() {
138 DefaultMutableTreeNode lastSelectedPathComponent = (DefaultMutableTreeNode)myTree.getLastSelectedPathComponent();
139 super.cancelEditing();
140 if (myShouldRepaint) {
143 restoreSelection(lastSelectedPathComponent);
147 protected void installSpeedSearch() {
148 new TreeSpeedSearch(this, path -> {
149 Object pathComponent = path.getLastPathComponent();
150 if (pathComponent instanceof RepositoryNode) {
151 return ((RepositoryNode)pathComponent).getRepositoryName();
153 return pathComponent.toString();
157 myTree.setUI(new MyTreeUi());
158 myTree.setBorder(JBUI.Borders.emptyTop(10));
159 myTree.setEditable(true);
160 myTree.setShowsRootHandles(root.getChildCount() > 1);
161 MyTreeCellEditor treeCellEditor = new MyTreeCellEditor();
162 myTree.setCellEditor(treeCellEditor);
163 treeCellEditor.addCellEditorListener(new CellEditorListener() {
165 public void editingStopped(ChangeEvent e) {
166 DefaultMutableTreeNode node = (DefaultMutableTreeNode)myTree.getLastSelectedPathComponent();
167 if (node instanceof EditableTreeNode) {
168 JComponent editedComponent = (JComponent)node.getUserObject();
169 InputVerifier verifier = editedComponent.getInputVerifier();
170 if (verifier != null && !verifier.verify(editedComponent)) {
171 // if invalid and interrupted, then revert
172 ((EditableTreeNode)node).fireOnCancel();
175 if (mySyncStrategy) {
177 ContainerUtil.process(getChildNodesByType(root, RepositoryNode.class, false), node1 -> {
178 node1.fireOnChange();
183 ((EditableTreeNode)node).fireOnChange();
187 myTree.firePropertyChange(PushLogTreeUtil.EDIT_MODE_PROP, true, false);
191 public void editingCanceled(ChangeEvent e) {
192 DefaultMutableTreeNode node = (DefaultMutableTreeNode)myTree.getLastSelectedPathComponent();
193 if (node instanceof EditableTreeNode) {
194 ((EditableTreeNode)node).fireOnCancel();
197 myTree.firePropertyChange(PushLogTreeUtil.EDIT_MODE_PROP, true, false);
200 // complete editing when interrupt
201 myTree.setInvokesStopCellEditing(true);
202 myTree.setRootVisible(false);
203 TreeUtil.collapseAll(myTree, 1);
204 final VcsBranchEditorListener linkMouseListener = new VcsBranchEditorListener(myTreeCellRenderer);
205 linkMouseListener.installOn(myTree);
206 myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
207 myTree.addTreeSelectionListener(new TreeSelectionListener() {
209 public void valueChanged(TreeSelectionEvent e) {
213 myTree.addFocusListener(new FocusAdapter() {
215 public void focusLost(FocusEvent e) {
216 DefaultMutableTreeNode node = (DefaultMutableTreeNode)myTree.getLastSelectedPathComponent();
217 if (node instanceof RepositoryNode && myTree.isEditing()) {
218 //need to force repaint foreground for non-focused editing node
219 myTree.getCellEditor().getTreeCellEditorComponent(myTree, node, true, false, false, myTree.getRowForPath(
220 TreeUtil.getPathFromRoot(node)));
224 myTree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), START_EDITING);
225 myTree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "");
226 ExpandAllAction expandAllAction = new ExpandAllAction(myTree);
227 expandAllAction.registerCustomShortcutSet(ActionManager.getInstance().getAction(ACTION_EXPAND_ALL).getShortcutSet(), myTree);
228 CollapseAllAction collapseAll = new CollapseAllAction(myTree);
229 collapseAll.registerCustomShortcutSet(ActionManager.getInstance().getAction(ACTION_COLLAPSE_ALL).getShortcutSet(), myTree);
231 ToolTipManager.sharedInstance().registerComponent(myTree);
232 PopupHandler.installPopupMenu(myTree, VcsLogActionIds.POPUP_ACTION_GROUP, CONTEXT_MENU);
234 myChangesBrowser = new SimpleChangesBrowser(project, false, false);
235 myChangesBrowser.hideViewerBorder();
236 myChangesBrowser.getDiffAction().registerCustomShortcutSet(myChangesBrowser.getDiffAction().getShortcutSet(), myTree);
237 final EditSourceForDialogAction editSourceAction = new EditSourceForDialogAction(myChangesBrowser);
238 editSourceAction.registerCustomShortcutSet(CommonShortcuts.getEditSource(), myChangesBrowser);
239 myChangesBrowser.addToolbarAction(editSourceAction);
240 setDefaultEmptyText();
242 myDetailsPanel = new CommitDetailsPanel();
243 JScrollPane detailsScrollPane =
244 new JBScrollPane(myDetailsPanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
245 detailsScrollPane.setBorder(JBUI.Borders.empty());
246 detailsScrollPane.setViewportBorder(JBUI.Borders.empty());
247 BorderLayoutPanel detailsContentPanel = new BorderLayoutPanel();
248 detailsContentPanel.addToCenter(detailsScrollPane);
250 JBSplitter detailsSplitter = new OnePixelSplitter(true, DETAILS_SPLITTER_PROPORTION, 0.67f);
251 detailsSplitter.setFirstComponent(myChangesBrowser);
253 myShowDetailsAction = new MyShowDetailsAction(project, (state) -> {
254 detailsSplitter.setSecondComponent(state ? detailsContentPanel : null);
256 myShowDetailsAction.setEnabled(false);
257 myChangesBrowser.addToolbarSeparator();
258 myChangesBrowser.addToolbarAction(myShowDetailsAction);
260 JBSplitter splitter = new OnePixelSplitter(TREE_SPLITTER_PROPORTION, 0.5f);
261 final JComponent syncStrategyPanel = myAllowSyncStrategy ? createStrategyPanel() : null;
262 myScrollPane = new JBScrollPane(myTree) {
265 public void layout() {
267 if (syncStrategyPanel != null) {
268 Rectangle bounds = this.getViewport().getBounds();
269 int height = bounds.height - syncStrategyPanel.getPreferredSize().height;
270 this.getViewport().setBounds(bounds.x, bounds.y, bounds.width, height);
271 syncStrategyPanel.setBounds(bounds.x, bounds.y + height, bounds.width,
272 syncStrategyPanel.getPreferredSize().height);
276 if (syncStrategyPanel != null) {
277 myScrollPane.setViewport(new MyTreeViewPort(myTree, syncStrategyPanel.getPreferredSize().height));
279 myScrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
280 myScrollPane.setOpaque(false);
281 if (syncStrategyPanel != null) {
282 myScrollPane.add(syncStrategyPanel);
284 myScrollPane.setBorder(JBUI.Borders.empty());
285 splitter.setFirstComponent(myScrollPane);
286 splitter.setSecondComponent(detailsSplitter);
288 setBorder(IdeBorderFactory.createBorder(SideBorder.BOTTOM));
289 setLayout(new BorderLayout());
291 myTree.setRowHeight(0);
294 public void highlightNodeOrFirst(@Nullable RepositoryNode repositoryNode, boolean shouldScrollTo) {
295 TreePath selectionPath = repositoryNode != null ? TreeUtil.getPathFromRoot(repositoryNode) : TreeUtil.getFirstNodePath(myTree);
296 myTree.setSelectionPath(selectionPath);
297 if (shouldScrollTo) {
298 myTree.scrollPathToVisible(selectionPath);
302 private void restoreSelection(@Nullable DefaultMutableTreeNode node) {
304 TreeUtil.selectNode(myTree, node);
308 private JComponent createStrategyPanel() {
309 final JPanel labelPanel = new JPanel(new BorderLayout());
310 labelPanel.setBackground(RenderingUtil.getBackground(myTree));
311 final LinkLabel<String> linkLabel = new LinkLabel<>(DvcsBundle.message("push.edit.all.targets"), null);
312 linkLabel.setBorder(JBUI.Borders.empty(2));
313 linkLabel.setListener(new LinkListener<>() {
315 public void linkSelected(LinkLabel<String> aSource, String aLinkData) {
316 if (linkLabel.isEnabled()) {
321 myTree.addPropertyChangeListener(PushLogTreeUtil.EDIT_MODE_PROP, new PropertyChangeListener() {
323 public void propertyChange(PropertyChangeEvent evt) {
324 Boolean editMode = (Boolean)evt.getNewValue();
325 linkLabel.setEnabled(!editMode);
326 linkLabel.setPaintUnderline(!editMode);
330 labelPanel.add(linkLabel, BorderLayout.EAST);
334 private void startSyncEditing() {
335 mySyncStrategy = true;
336 DefaultMutableTreeNode nodeToEdit = getFirstNodeToEdit();
337 if (nodeToEdit != null) {
338 myTree.startEditingAtPath(TreeUtil.getPathFromRoot(nodeToEdit));
343 private static List<Change> collectAllChanges(@NotNull List<? extends CommitNode> commitNodes) {
344 return CommittedChangesTreeBrowser.zipChanges(collectChanges(commitNodes));
348 private static List<CommitNode> collectSelectedCommitNodes(@NotNull List<DefaultMutableTreeNode> selectedNodes) {
349 //addAll Commit nodes from selected Repository nodes;
350 List<CommitNode> nodes = StreamEx.of(selectedNodes)
351 .select(RepositoryNode.class)
352 .toFlatList(node -> getChildNodesByType(node, CommitNode.class, true));
353 // add all others selected Commit nodes;
354 nodes.addAll(StreamEx.of(selectedNodes)
355 .select(CommitNode.class)
356 .filter(node -> !nodes.contains(node))
362 private static List<Change> collectChanges(@NotNull List<? extends CommitNode> commitNodes) {
363 List<Change> changes = new ArrayList<>();
364 for (CommitNode node : commitNodes) {
365 changes.addAll(node.getUserObject().getChanges());
371 private static <T> List<T> getChildNodesByType(@NotNull DefaultMutableTreeNode node, Class<T> type, boolean reverseOrder) {
372 List<T> nodes = new ArrayList<>();
373 if (node.getChildCount() < 1) {
376 for (DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)node.getFirstChild();
378 childNode = (DefaultMutableTreeNode)node.getChildAfter(childNode)) {
379 if (type.isInstance(childNode)) {
380 @SuppressWarnings("unchecked")
381 T nodeT = (T)childNode;
394 private static List<Integer> getSortedRows(int @NotNull [] rows) {
395 List<Integer> sorted = new ArrayList<>();
396 for (int row : rows) {
399 sorted.sort(Collections.reverseOrder());
403 private void updateChangesView() {
404 List<CommitNode> commitNodes = getSelectedCommitNodes();
405 if (!commitNodes.isEmpty()) {
406 myChangesBrowser.getViewer().setEmptyText(DvcsBundle.message("push.no.differences"));
409 setDefaultEmptyText();
411 myChangesBrowser.setChangesToDisplay(collectAllChanges(commitNodes));
412 if (commitNodes.size() == 1 && getSelectedTreeNodes().stream().noneMatch(it -> it instanceof RepositoryNode)) {
413 VcsFullCommitDetails commitDetails = commitNodes.get(0).getUserObject();
414 CommitPresentationUtil.CommitPresentation presentation =
415 CommitPresentationUtil.buildPresentation(myProject, commitDetails, new HashSet<>());
416 myDetailsPanel.setCommit(presentation);
417 myShowDetailsAction.setEnabled(true);
420 myShowDetailsAction.setEnabled(false);
424 private void setDefaultEmptyText() {
425 myChangesBrowser.getViewer().setEmptyText(DvcsBundle.message("push.no.commits.selected"));
428 // Make changes available for diff action; revisionNumber for create patch and copy revision number actions
431 public Object getData(@NotNull String id) {
432 if (VcsDataKeys.CHANGES.is(id)) {
433 List<CommitNode> commitNodes = getSelectedCommitNodes();
434 return collectAllChanges(commitNodes).toArray(new Change[0]);
436 else if (VcsDataKeys.VCS_REVISION_NUMBERS.is(id)) {
437 List<CommitNode> commitNodes = getSelectedCommitNodes();
438 return ContainerUtil.map2Array(commitNodes, VcsRevisionNumber.class, commitNode -> {
439 Hash hash = commitNode.getUserObject().getId();
440 return new TextRevisionNumber(hash.asString(), hash.toShortString());
447 private List<CommitNode> getSelectedCommitNodes() {
448 List<DefaultMutableTreeNode> selectedNodes = getSelectedTreeNodes();
449 return selectedNodes.isEmpty() ? Collections.emptyList() : collectSelectedCommitNodes(selectedNodes);
453 private List<DefaultMutableTreeNode> getSelectedTreeNodes() {
454 int[] rows = myTree.getSelectionRows();
455 return (rows != null && rows.length != 0) ? getNodesForRows(getSortedRows(rows)) : emptyList();
459 private List<DefaultMutableTreeNode> getNodesForRows(@NotNull List<Integer> rows) {
460 List<DefaultMutableTreeNode> nodes = new ArrayList<>();
461 for (Integer row : rows) {
462 TreePath path = myTree.getPathForRow(row);
463 Object pathComponent = path == null ? null : path.getLastPathComponent();
464 if (pathComponent instanceof DefaultMutableTreeNode) {
465 nodes.add((DefaultMutableTreeNode)pathComponent);
472 protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
473 if (e.getKeyCode() == KeyEvent.VK_ENTER && myTree.isEditing() && e.getModifiers() == 0 && pressed) {
474 myTree.stopEditing();
477 if (myAllowSyncStrategy && e.getKeyCode() == KeyEvent.VK_F2 && e.getModifiers() == InputEvent.ALT_MASK && pressed) {
481 if (CheckboxTreeHelper.isToggleEvent(e, myTree) && pressed) {
482 toggleRepositoriesFromCommits();
485 return super.processKeyBinding(ks, e, condition, pressed);
488 private void toggleRepositoriesFromCommits() {
489 LinkedHashSet<CheckedTreeNode> checkedNodes = StreamEx.of(getSelectedTreeNodes())
490 .map(n -> n instanceof CommitNode ? n.getParent() : n)
491 .select(CheckedTreeNode.class)
492 .filter(CheckedTreeNode::isEnabled)
493 .toCollection(LinkedHashSet::new);
494 if (checkedNodes.isEmpty()) return;
495 // use new state from first lead node;
496 boolean newState = !checkedNodes.iterator().next().isChecked();
497 checkedNodes.forEach(n -> myTree.setNodeState(n, newState));
501 private DefaultMutableTreeNode getFirstNodeToEdit() {
502 // start edit last selected component if editable
503 if (myTree.getLastSelectedPathComponent() instanceof RepositoryNode) {
504 RepositoryNode selectedNode = ((RepositoryNode)myTree.getLastSelectedPathComponent());
505 if (selectedNode.isEditableNow()) return selectedNode;
507 List<RepositoryNode> repositoryNodes = getChildNodesByType((DefaultMutableTreeNode)myTree.getModel().getRoot(),
508 RepositoryNode.class, false);
509 RepositoryNode editableNode = ContainerUtil.find(repositoryNodes, repositoryNode -> repositoryNode.isEditableNow());
510 if (editableNode != null) {
511 TreeUtil.selectNode(myTree, editableNode);
516 public JComponent getPreferredFocusedComponent() {
521 public CheckboxTree getTree() {
525 public void selectIfNothingSelected(@NotNull TreeNode node) {
526 if (myTree.isSelectionEmpty()) {
527 myTree.setSelectionPath(TreeUtil.getPathFromRoot(node));
531 public void setChildren(@NotNull DefaultMutableTreeNode parentNode,
532 @NotNull Collection<? extends DefaultMutableTreeNode> childrenNodes) {
533 parentNode.removeAllChildren();
534 for (DefaultMutableTreeNode child : childrenNodes) {
535 parentNode.add(child);
537 if (!myTree.isEditing()) {
538 refreshNode(parentNode);
539 TreePath path = TreeUtil.getPathFromRoot(parentNode);
540 if (myTree.getSelectionModel().isPathSelected(path)) {
545 myShouldRepaint = true;
549 private void refreshNode(@NotNull DefaultMutableTreeNode parentNode) {
550 //todo should be optimized in case of start loading just edited node
551 final DefaultTreeModel model = ((DefaultTreeModel)myTree.getModel());
552 model.nodeStructureChanged(parentNode);
553 autoExpandChecked(parentNode);
554 myShouldRepaint = false;
557 private void autoExpandChecked(@NotNull DefaultMutableTreeNode node) {
558 if (node.getChildCount() <= 0) return;
559 if (node instanceof RepositoryNode) {
560 expandIfChecked((RepositoryNode)node);
563 for (DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)node.getFirstChild();
565 childNode = (DefaultMutableTreeNode)node.getChildAfter(childNode)) {
566 if (!(childNode instanceof RepositoryNode)) return;
567 expandIfChecked((RepositoryNode)childNode);
571 private void expandIfChecked(@NotNull RepositoryNode node) {
572 if (node.isChecked()) {
573 TreePath path = TreeUtil.getPathFromRoot(node);
574 myTree.expandPath(path);
578 private void setSyncText(@Nls String value) {
579 mySyncRenderedText = value;
582 public void fireEditorUpdated(@NotNull @Nls String currentText) {
583 if (mySyncStrategy) {
585 List<RepositoryNode> repositoryNodes =
586 getChildNodesByType((DefaultMutableTreeNode)myTree.getModel().getRoot(), RepositoryNode.class, false);
587 for (RepositoryNode node : repositoryNodes) {
588 if (node.isEditableNow()) {
589 node.forceUpdateUiModelWithTypedText(currentText);
592 setSyncText(currentText);
597 private void resetEditSync() {
598 if (mySyncStrategy) {
599 mySyncStrategy = false;
600 mySyncRenderedText = null;
604 private class MyTreeCellRenderer extends CheckboxTree.CheckboxTreeCellRenderer {
607 public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
608 if (!(value instanceof DefaultMutableTreeNode)) {
611 myCheckbox.setBorder(null); //checkBox may have no border by default, but insets are not null,
612 // it depends on LaF, OS and isItRenderedPane, see com.intellij.ide.ui.laf.darcula.ui.DarculaCheckBoxBorder.
613 // null border works as expected always.
614 ColoredTreeCellRenderer renderer = getTextRenderer();
615 renderer.setIpad(JBUI.emptyInsets());
616 if (value instanceof RepositoryNode) {
617 //todo simplify, remove instance of
618 RepositoryNode valueNode = (RepositoryNode)value;
619 boolean isCheckboxVisible = valueNode.isCheckboxVisible();
620 myCheckbox.setVisible(isCheckboxVisible);
621 if (!isCheckboxVisible) {
622 // if we don't set right inset, "new" icon will be cropped
623 renderer.setIpad(JBUI.insets(0, 10));
625 if (valueNode.isChecked() && valueNode.isLoading()) {
626 myCheckbox.setState(ThreeStateCheckBox.State.DONT_CARE);
629 myCheckbox.setSelected(valueNode.isChecked());
632 Object userObject = ((DefaultMutableTreeNode)value).getUserObject();
633 if (value instanceof CustomRenderedTreeNode) {
634 if (tree.isEditing() && mySyncStrategy && value instanceof RepositoryNode) {
635 //sync rendering all editable fields
636 ((RepositoryNode)value).render(renderer, mySyncRenderedText);
639 ((CustomRenderedTreeNode)value).render(renderer);
643 renderer.append(userObject == null ? "" : userObject.toString()); //NON-NLS
648 private class MyTreeCellEditor extends AbstractCellEditor implements TreeCellEditor {
650 private RepositoryWithBranchPanel myValue;
653 public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) {
654 RepositoryWithBranchPanel panel = (RepositoryWithBranchPanel)((DefaultMutableTreeNode)value).getUserObject();
656 myTree.firePropertyChange(PushLogTreeUtil.EDIT_MODE_PROP, false, true);
657 return panel.getTreeCellEditorComponent(tree, value, isSelected, expanded, leaf, row, true);
661 public boolean isCellEditable(EventObject anEvent) {
662 if (anEvent instanceof MouseEvent) {
663 MouseEvent me = ((MouseEvent)anEvent);
664 final TreePath path = myTree.getClosestPathForLocation(me.getX(), me.getY());
665 final int row = myTree.getRowForLocation(me.getX(), me.getY());
666 myTree.getCellRenderer().getTreeCellRendererComponent(myTree, path.getLastPathComponent(), false, false, true, row, true);
667 Object tag = me.getClickCount() >= 1
668 ? PushLogTreeUtil.getTagAtForRenderer(myTreeCellRenderer, me)
670 return tag instanceof VcsEditableComponent;
672 //if keyboard event - then anEvent will be null =( See BasicTreeUi
673 TreePath treePath = myTree.getAnchorSelectionPath();
674 //there is no selection path if we start editing during initial validation//
675 if (treePath == null) return true;
676 Object treeNode = treePath.getLastPathComponent();
677 return treeNode instanceof EditableTreeNode && ((EditableTreeNode)treeNode).isEditableNow();
681 public Object getCellEditorValue() {
686 private class MyTreeUi extends WideSelectionTreeUI {
688 private final ComponentListener myTreeSizeListener = new ComponentAdapter() {
690 public void componentResized(ComponentEvent e) {
691 // invalidate, revalidate etc may have no 'size' effects, you need to manually invalidateSizes before.
696 private final AncestorListener myTreeAncestorListener = new AncestorListenerAdapter() {
698 public void ancestorMoved(AncestorEvent event) {
699 super.ancestorMoved(event);
704 private void updateSizes() {
705 treeState.invalidateSizes();
710 protected void installListeners() {
711 super.installListeners();
712 tree.addComponentListener(myTreeSizeListener);
713 tree.addAncestorListener(myTreeAncestorListener);
718 protected void uninstallListeners() {
719 tree.removeComponentListener(myTreeSizeListener);
720 tree.removeAncestorListener(myTreeAncestorListener);
721 super.uninstallListeners();
725 protected AbstractLayoutCache.NodeDimensions createNodeDimensions() {
726 return new NodeDimensionsHandler() {
728 public Rectangle getNodeDimensions(Object value, int row, int depth, boolean expanded, Rectangle size) {
729 Rectangle dimensions = super.getNodeDimensions(value, row, depth, expanded, size);
730 dimensions.width = Math.max(
731 myScrollPane != null ? myScrollPane.getViewport().getWidth() - getRowX(row, depth) : myTree.getMinimumSize().width,
739 private static class MyTreeViewPort extends JBViewport {
741 final int myHeightToReduce;
743 MyTreeViewPort(@Nullable Component view, int heightToReduce) {
746 myHeightToReduce = heightToReduce;
750 public Dimension getExtentSize() {
751 Dimension defaultSize = super.getExtentSize();
752 return new Dimension(defaultSize.width, defaultSize.height - myHeightToReduce);
756 private static class MyShowDetailsAction extends ToggleActionButton implements DumbAware {
757 @NotNull private final PushSettings mySettings;
758 @NotNull private final Consumer<Boolean> myOnUpdate;
760 MyShowDetailsAction(@NotNull Project project, @NotNull Consumer<Boolean> onUpdate) {
761 super(DvcsBundle.message("push.show.details"), AllIcons.Actions.PreviewDetailsVertically);
762 mySettings = project.getService(PushSettings.class);
763 myOnUpdate = onUpdate;
766 private boolean getValue() {
767 return mySettings.getShowDetailsInPushDialog();
771 public boolean isSelected(AnActionEvent e) {
776 public void setSelected(AnActionEvent e, boolean state) {
777 mySettings.setShowDetailsInPushDialog(state);
778 myOnUpdate.accept(state);
782 public void setEnabled(boolean enabled) {
783 myOnUpdate.accept(enabled && getValue());
784 super.setEnabled(enabled);