5c873cc839e0c92640614bd7cb81a055af19d620
[idea/community.git] / platform / dvcs-impl / src / com / intellij / dvcs / push / ui / PushLog.java
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;
3
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;
39 import kotlin.Unit;
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;
45
46 import javax.swing.*;
47 import javax.swing.event.*;
48 import javax.swing.tree.*;
49 import java.awt.*;
50 import java.awt.event.*;
51 import java.beans.PropertyChangeEvent;
52 import java.beans.PropertyChangeListener;
53 import java.util.List;
54 import java.util.*;
55 import java.util.function.Consumer;
56
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;
60
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;
77
78   public PushLog(@NotNull Project project, final CheckedTreeNode root, final boolean allowSyncStrategy) {
79     myProject = project;
80     myAllowSyncStrategy = allowSyncStrategy;
81     DefaultTreeModel treeModel = new DefaultTreeModel(root);
82     treeModel.nodeStructureChanged(root);
83     myTreeCellRenderer = new MyTreeCellRenderer();
84     myTree = new CheckboxTree(myTreeCellRenderer, root) {
85
86       @Override
87       protected boolean shouldShowBusyIconIfNeeded() {
88         return true;
89       }
90
91       @Override
92       public boolean isPathEditable(TreePath path) {
93         return isEditable() && path.getLastPathComponent() instanceof DefaultMutableTreeNode;
94       }
95
96       @Override
97       protected void onNodeStateChanged(CheckedTreeNode node) {
98         if (node instanceof EditableTreeNode) {
99           ((EditableTreeNode)node).fireOnSelectionChange(node.isChecked());
100         }
101       }
102
103       @Override
104       public String getToolTipText(MouseEvent event) {
105         final TreePath path = myTree.getPathForLocation(event.getX(), event.getY());
106         if (path == null) {
107           return "";
108         }
109         Object node = path.getLastPathComponent();
110         if ((!(node instanceof DefaultMutableTreeNode))) {
111           return "";
112         }
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
116         }
117         return "";
118       }
119
120       @Override
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;
127         }
128         boolean result = super.stopEditing();
129         if (myShouldRepaint) {
130           refreshNode(root);
131         }
132         restoreSelection(node);
133         return result;
134       }
135
136       @Override
137       public void cancelEditing() {
138         DefaultMutableTreeNode lastSelectedPathComponent = (DefaultMutableTreeNode)myTree.getLastSelectedPathComponent();
139         super.cancelEditing();
140         if (myShouldRepaint) {
141           refreshNode(root);
142         }
143         restoreSelection(lastSelectedPathComponent);
144       }
145
146       @Override
147       protected void installSpeedSearch() {
148         new TreeSpeedSearch(this, path -> {
149           Object pathComponent = path.getLastPathComponent();
150           if (pathComponent instanceof RepositoryNode) {
151             return ((RepositoryNode)pathComponent).getRepositoryName();
152           }
153           return pathComponent.toString();
154         });
155       }
156     };
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() {
164       @Override
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();
173           }
174           else {
175             if (mySyncStrategy) {
176               resetEditSync();
177               ContainerUtil.process(getChildNodesByType(root, RepositoryNode.class, false), node1 -> {
178                 node1.fireOnChange();
179                 return true;
180               });
181             }
182             else {
183               ((EditableTreeNode)node).fireOnChange();
184             }
185           }
186         }
187         myTree.firePropertyChange(PushLogTreeUtil.EDIT_MODE_PROP, true, false);
188       }
189
190       @Override
191       public void editingCanceled(ChangeEvent e) {
192         DefaultMutableTreeNode node = (DefaultMutableTreeNode)myTree.getLastSelectedPathComponent();
193         if (node instanceof EditableTreeNode) {
194           ((EditableTreeNode)node).fireOnCancel();
195         }
196         resetEditSync();
197         myTree.firePropertyChange(PushLogTreeUtil.EDIT_MODE_PROP, true, false);
198       }
199     });
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() {
208       @Override
209       public void valueChanged(TreeSelectionEvent e) {
210         updateChangesView();
211       }
212     });
213     myTree.addFocusListener(new FocusAdapter() {
214       @Override
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)));
221         }
222       }
223     });
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);
230
231     ToolTipManager.sharedInstance().registerComponent(myTree);
232     PopupHandler.installPopupMenu(myTree, VcsLogActionIds.POPUP_ACTION_GROUP, CONTEXT_MENU);
233
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();
241
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);
249
250     JBSplitter detailsSplitter = new OnePixelSplitter(true, DETAILS_SPLITTER_PROPORTION, 0.67f);
251     detailsSplitter.setFirstComponent(myChangesBrowser);
252
253     myShowDetailsAction = new MyShowDetailsAction(project, (state) -> {
254       detailsSplitter.setSecondComponent(state ? detailsContentPanel : null);
255     });
256     myShowDetailsAction.setEnabled(false);
257     myChangesBrowser.addToolbarSeparator();
258     myChangesBrowser.addToolbarAction(myShowDetailsAction);
259
260     JBSplitter splitter = new OnePixelSplitter(TREE_SPLITTER_PROPORTION, 0.5f);
261     final JComponent syncStrategyPanel = myAllowSyncStrategy ? createStrategyPanel() : null;
262     myScrollPane = new JBScrollPane(myTree) {
263
264       @Override
265       public void layout() {
266         super.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);
273         }
274       }
275     };
276     if (syncStrategyPanel != null) {
277       myScrollPane.setViewport(new MyTreeViewPort(myTree, syncStrategyPanel.getPreferredSize().height));
278     }
279     myScrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
280     myScrollPane.setOpaque(false);
281     if (syncStrategyPanel != null) {
282       myScrollPane.add(syncStrategyPanel);
283     }
284     myScrollPane.setBorder(JBUI.Borders.empty());
285     splitter.setFirstComponent(myScrollPane);
286     splitter.setSecondComponent(detailsSplitter);
287
288     setBorder(IdeBorderFactory.createBorder(SideBorder.BOTTOM));
289     setLayout(new BorderLayout());
290     add(splitter);
291     myTree.setRowHeight(0);
292   }
293
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);
299     }
300   }
301
302   private void restoreSelection(@Nullable DefaultMutableTreeNode node) {
303     if (node != null) {
304       TreeUtil.selectNode(myTree, node);
305     }
306   }
307
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<>() {
314       @Override
315       public void linkSelected(LinkLabel<String> aSource, String aLinkData) {
316         if (linkLabel.isEnabled()) {
317           startSyncEditing();
318         }
319       }
320     }, null);
321     myTree.addPropertyChangeListener(PushLogTreeUtil.EDIT_MODE_PROP, new PropertyChangeListener() {
322       @Override
323       public void propertyChange(PropertyChangeEvent evt) {
324         Boolean editMode = (Boolean)evt.getNewValue();
325         linkLabel.setEnabled(!editMode);
326         linkLabel.setPaintUnderline(!editMode);
327         linkLabel.repaint();
328       }
329     });
330     labelPanel.add(linkLabel, BorderLayout.EAST);
331     return labelPanel;
332   }
333
334   private void startSyncEditing() {
335     mySyncStrategy = true;
336     DefaultMutableTreeNode nodeToEdit = getFirstNodeToEdit();
337     if (nodeToEdit != null) {
338       myTree.startEditingAtPath(TreeUtil.getPathFromRoot(nodeToEdit));
339     }
340   }
341
342   @NotNull
343   private static List<Change> collectAllChanges(@NotNull List<? extends CommitNode> commitNodes) {
344     return CommittedChangesTreeBrowser.zipChanges(collectChanges(commitNodes));
345   }
346
347   @NotNull
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))
357                    .toList());
358     return nodes;
359   }
360
361   @NotNull
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());
366     }
367     return changes;
368   }
369
370   @NotNull
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) {
374       return nodes;
375     }
376     for (DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)node.getFirstChild();
377          childNode != null;
378          childNode = (DefaultMutableTreeNode)node.getChildAfter(childNode)) {
379       if (type.isInstance(childNode)) {
380         @SuppressWarnings("unchecked")
381         T nodeT = (T)childNode;
382         if (reverseOrder) {
383           nodes.add(0, nodeT);
384         }
385         else {
386           nodes.add(nodeT);
387         }
388       }
389     }
390     return nodes;
391   }
392
393   @NotNull
394   private static List<Integer> getSortedRows(int @NotNull [] rows) {
395     List<Integer> sorted = new ArrayList<>();
396     for (int row : rows) {
397       sorted.add(row);
398     }
399     sorted.sort(Collections.reverseOrder());
400     return sorted;
401   }
402
403   private void updateChangesView() {
404     List<CommitNode> commitNodes = getSelectedCommitNodes();
405     if (!commitNodes.isEmpty()) {
406       myChangesBrowser.getViewer().setEmptyText(DvcsBundle.message("push.no.differences"));
407     }
408     else {
409       setDefaultEmptyText();
410     }
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);
418     }
419     else {
420       myShowDetailsAction.setEnabled(false);
421     }
422   }
423
424   private void setDefaultEmptyText() {
425     myChangesBrowser.getViewer().setEmptyText(DvcsBundle.message("push.no.commits.selected"));
426   }
427
428   // Make changes available for diff action; revisionNumber for create patch and copy revision number actions
429   @Nullable
430   @Override
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]);
435     }
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());
441       });
442     }
443     return null;
444   }
445
446   @NotNull
447   private List<CommitNode> getSelectedCommitNodes() {
448     List<DefaultMutableTreeNode> selectedNodes = getSelectedTreeNodes();
449     return selectedNodes.isEmpty() ? Collections.emptyList() : collectSelectedCommitNodes(selectedNodes);
450   }
451
452   @NotNull
453   private List<DefaultMutableTreeNode> getSelectedTreeNodes() {
454     int[] rows = myTree.getSelectionRows();
455     return (rows != null && rows.length != 0) ? getNodesForRows(getSortedRows(rows)) : emptyList();
456   }
457
458   @NotNull
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);
466       }
467     }
468     return nodes;
469   }
470
471   @Override
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();
475       return true;
476     }
477     if (myAllowSyncStrategy && e.getKeyCode() == KeyEvent.VK_F2 && e.getModifiers() == InputEvent.ALT_MASK && pressed) {
478       startSyncEditing();
479       return true;
480     }
481     if (CheckboxTreeHelper.isToggleEvent(e, myTree) && pressed) {
482       toggleRepositoriesFromCommits();
483       return true;
484     }
485     return super.processKeyBinding(ks, e, condition, pressed);
486   }
487
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));
498   }
499
500   @Nullable
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;
506     }
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);
512     }
513     return editableNode;
514   }
515
516   public JComponent getPreferredFocusedComponent() {
517     return myTree;
518   }
519
520   @NotNull
521   public CheckboxTree getTree() {
522     return myTree;
523   }
524
525   public void selectIfNothingSelected(@NotNull TreeNode node) {
526     if (myTree.isSelectionEmpty()) {
527       myTree.setSelectionPath(TreeUtil.getPathFromRoot(node));
528     }
529   }
530
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);
536     }
537     if (!myTree.isEditing()) {
538       refreshNode(parentNode);
539       TreePath path = TreeUtil.getPathFromRoot(parentNode);
540       if (myTree.getSelectionModel().isPathSelected(path)) {
541         updateChangesView();
542       }
543     }
544     else {
545       myShouldRepaint = true;
546     }
547   }
548
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;
555   }
556
557   private void autoExpandChecked(@NotNull DefaultMutableTreeNode node) {
558     if (node.getChildCount() <= 0) return;
559     if (node instanceof RepositoryNode) {
560       expandIfChecked((RepositoryNode)node);
561       return;
562     }
563     for (DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)node.getFirstChild();
564          childNode != null;
565          childNode = (DefaultMutableTreeNode)node.getChildAfter(childNode)) {
566       if (!(childNode instanceof RepositoryNode)) return;
567       expandIfChecked((RepositoryNode)childNode);
568     }
569   }
570
571   private void expandIfChecked(@NotNull RepositoryNode node) {
572     if (node.isChecked()) {
573       TreePath path = TreeUtil.getPathFromRoot(node);
574       myTree.expandPath(path);
575     }
576   }
577
578   private void setSyncText(@Nls String value) {
579     mySyncRenderedText = value;
580   }
581
582   public void fireEditorUpdated(@NotNull @Nls String currentText) {
583     if (mySyncStrategy) {
584       //update ui model
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);
590         }
591       }
592       setSyncText(currentText);
593       myTree.repaint();
594     }
595   }
596
597   private void resetEditSync() {
598     if (mySyncStrategy) {
599       mySyncStrategy = false;
600       mySyncRenderedText = null;
601     }
602   }
603
604   private class MyTreeCellRenderer extends CheckboxTree.CheckboxTreeCellRenderer {
605
606     @Override
607     public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
608       if (!(value instanceof DefaultMutableTreeNode)) {
609         return;
610       }
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));
624         }
625         if (valueNode.isChecked() && valueNode.isLoading()) {
626           myCheckbox.setState(ThreeStateCheckBox.State.DONT_CARE);
627         }
628         else {
629           myCheckbox.setSelected(valueNode.isChecked());
630         }
631       }
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);
637         }
638         else {
639           ((CustomRenderedTreeNode)value).render(renderer);
640         }
641       }
642       else {
643         renderer.append(userObject == null ? "" : userObject.toString()); //NON-NLS
644       }
645     }
646   }
647
648   private class MyTreeCellEditor extends AbstractCellEditor implements TreeCellEditor {
649
650     private RepositoryWithBranchPanel myValue;
651
652     @Override
653     public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) {
654       RepositoryWithBranchPanel panel = (RepositoryWithBranchPanel)((DefaultMutableTreeNode)value).getUserObject();
655       myValue = panel;
656       myTree.firePropertyChange(PushLogTreeUtil.EDIT_MODE_PROP, false, true);
657       return panel.getTreeCellEditorComponent(tree, value, isSelected, expanded, leaf, row, true);
658     }
659
660     @Override
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)
669                      : null;
670         return tag instanceof VcsEditableComponent;
671       }
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();
678     }
679
680     @Override
681     public Object getCellEditorValue() {
682       return myValue;
683     }
684   }
685
686   private class MyTreeUi extends WideSelectionTreeUI {
687
688     private final ComponentListener myTreeSizeListener = new ComponentAdapter() {
689       @Override
690       public void componentResized(ComponentEvent e) {
691         // invalidate, revalidate etc may have no 'size' effects, you need to manually invalidateSizes before.
692         updateSizes();
693       }
694     };
695
696     private final AncestorListener myTreeAncestorListener = new AncestorListenerAdapter() {
697       @Override
698       public void ancestorMoved(AncestorEvent event) {
699         super.ancestorMoved(event);
700         updateSizes();
701       }
702     };
703
704     private void updateSizes() {
705       treeState.invalidateSizes();
706       tree.repaint();
707     }
708
709     @Override
710     protected void installListeners() {
711       super.installListeners();
712       tree.addComponentListener(myTreeSizeListener);
713       tree.addAncestorListener(myTreeAncestorListener);
714     }
715
716
717     @Override
718     protected void uninstallListeners() {
719       tree.removeComponentListener(myTreeSizeListener);
720       tree.removeAncestorListener(myTreeAncestorListener);
721       super.uninstallListeners();
722     }
723
724     @Override
725     protected AbstractLayoutCache.NodeDimensions createNodeDimensions() {
726       return new NodeDimensionsHandler() {
727         @Override
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,
732             dimensions.width);
733           return dimensions;
734         }
735       };
736     }
737   }
738
739   private static class MyTreeViewPort extends JBViewport {
740
741     final int myHeightToReduce;
742
743     MyTreeViewPort(@Nullable Component view, int heightToReduce) {
744       super();
745       setView(view);
746       myHeightToReduce = heightToReduce;
747     }
748
749     @Override
750     public Dimension getExtentSize() {
751       Dimension defaultSize = super.getExtentSize();
752       return new Dimension(defaultSize.width, defaultSize.height - myHeightToReduce);
753     }
754   }
755
756   private static class MyShowDetailsAction extends ToggleActionButton implements DumbAware {
757     @NotNull private final PushSettings mySettings;
758     @NotNull private final Consumer<Boolean> myOnUpdate;
759
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;
764     }
765
766     private boolean getValue() {
767       return mySettings.getShowDetailsInPushDialog();
768     }
769
770     @Override
771     public boolean isSelected(AnActionEvent e) {
772       return getValue();
773     }
774
775     @Override
776     public void setSelected(AnActionEvent e, boolean state) {
777       mySettings.setShowDetailsInPushDialog(state);
778       myOnUpdate.accept(state);
779     }
780
781     @Override
782     public void setEnabled(boolean enabled) {
783       myOnUpdate.accept(enabled && getValue());
784       super.setEnabled(enabled);
785     }
786   }
787 }
788
789