[vcs-log] do not try to resize last table column by dagging table edge (does not...
[idea/community.git] / platform / vcs-log / impl / src / com / intellij / vcs / log / ui / frame / VcsLogGraphTable.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.vcs.log.ui.frame;
17
18 import com.google.common.primitives.Ints;
19 import com.intellij.ide.CopyProvider;
20 import com.intellij.ide.IdeTooltip;
21 import com.intellij.ide.IdeTooltipManager;
22 import com.intellij.openapi.actionSystem.DataContext;
23 import com.intellij.openapi.actionSystem.DataProvider;
24 import com.intellij.openapi.actionSystem.PlatformDataKeys;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.ide.CopyPasteManager;
27 import com.intellij.openapi.ui.LoadingDecorator;
28 import com.intellij.openapi.ui.popup.Balloon;
29 import com.intellij.openapi.util.Couple;
30 import com.intellij.openapi.util.Pair;
31 import com.intellij.openapi.util.text.StringUtil;
32 import com.intellij.openapi.vcs.changes.issueLinks.TableLinkMouseListener;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.ui.*;
35 import com.intellij.ui.components.JBLabel;
36 import com.intellij.ui.components.panels.Wrapper;
37 import com.intellij.ui.table.JBTable;
38 import com.intellij.util.containers.ContainerUtil;
39 import com.intellij.util.text.DateFormatUtil;
40 import com.intellij.util.ui.JBUI;
41 import com.intellij.util.ui.UIUtil;
42 import com.intellij.vcs.log.*;
43 import com.intellij.vcs.log.data.LoadingDetails;
44 import com.intellij.vcs.log.data.VcsLogData;
45 import com.intellij.vcs.log.data.VcsLogProgress;
46 import com.intellij.vcs.log.data.VisiblePack;
47 import com.intellij.vcs.log.graph.*;
48 import com.intellij.vcs.log.graph.actions.GraphAction;
49 import com.intellij.vcs.log.graph.actions.GraphAnswer;
50 import com.intellij.vcs.log.impl.VcsLogUtil;
51 import com.intellij.vcs.log.paint.GraphCellPainter;
52 import com.intellij.vcs.log.paint.PositionUtil;
53 import com.intellij.vcs.log.paint.SimpleGraphCellPainter;
54 import com.intellij.vcs.log.ui.VcsLogActionPlaces;
55 import com.intellij.vcs.log.ui.VcsLogColorManager;
56 import com.intellij.vcs.log.ui.VcsLogColorManagerImpl;
57 import com.intellij.vcs.log.ui.VcsLogUiImpl;
58 import com.intellij.vcs.log.ui.render.GraphCommitCell;
59 import com.intellij.vcs.log.ui.render.GraphCommitCellRenderer;
60 import com.intellij.vcs.log.ui.tables.GraphTableModel;
61 import com.intellij.vcs.log.util.VcsUserUtil;
62 import gnu.trove.TIntHashSet;
63 import org.jetbrains.annotations.NonNls;
64 import org.jetbrains.annotations.NotNull;
65 import org.jetbrains.annotations.Nullable;
66
67 import javax.swing.*;
68 import javax.swing.event.*;
69 import javax.swing.plaf.basic.BasicTableHeaderUI;
70 import javax.swing.table.*;
71 import java.awt.*;
72 import java.awt.datatransfer.StringSelection;
73 import java.awt.event.ComponentAdapter;
74 import java.awt.event.ComponentEvent;
75 import java.awt.event.MouseAdapter;
76 import java.awt.event.MouseEvent;
77 import java.util.*;
78 import java.util.List;
79
80 public class VcsLogGraphTable extends TableWithProgress implements DataProvider, CopyProvider {
81   private static final Logger LOG = Logger.getInstance(VcsLogGraphTable.class);
82
83   public static final int ROOT_INDICATOR_COLORED_WIDTH = 8;
84   public static final int ROOT_INDICATOR_WHITE_WIDTH = 5;
85   private static final int ROOT_INDICATOR_WIDTH = ROOT_INDICATOR_WHITE_WIDTH + ROOT_INDICATOR_COLORED_WIDTH;
86   private static final int ROOT_NAME_MAX_WIDTH = 200;
87   private static final int MAX_DEFAULT_AUTHOR_COLUMN_WIDTH = 200;
88   private static final int MAX_ROWS_TO_CALC_WIDTH = 1000;
89   private static final int MAX_ROWS_TO_CALC_OFFSET = 100;
90
91   @NotNull private final VcsLogUiImpl myUi;
92   @NotNull private final VcsLogData myLogData;
93   @NotNull private final MyDummyTableCellEditor myDummyEditor = new MyDummyTableCellEditor();
94   @NotNull private final TableCellRenderer myDummyRenderer = new DefaultTableCellRenderer();
95   @NotNull private final GraphCommitCellRenderer myGraphCommitCellRenderer;
96   private boolean myColumnsSizeInitialized = false;
97   @Nullable private Selection mySelection = null;
98
99   @NotNull private final Collection<VcsLogHighlighter> myHighlighters = ContainerUtil.newArrayList();
100
101   @NotNull private final GraphCellPainter myGraphCellPainter = new SimpleGraphCellPainter(new DefaultColorGenerator()) {
102     @Override
103     protected int getRowHeight() {
104       return VcsLogGraphTable.this.getRowHeight();
105     }
106   };
107
108   public VcsLogGraphTable(@NotNull VcsLogUiImpl ui, @NotNull VcsLogData logData, @NotNull VisiblePack initialDataPack) {
109     super(new GraphTableModel(initialDataPack, logData, ui));
110     getEmptyText().setText("Changes log");
111
112     myUi = ui;
113     myLogData = logData;
114     myGraphCommitCellRenderer = new GraphCommitCellRenderer(logData, myGraphCellPainter, this);
115
116     myLogData.getProgress().addProgressIndicatorListener(new MyProgressListener(), ui);
117
118     setDefaultRenderer(VirtualFile.class, new RootCellRenderer(myUi));
119     setDefaultRenderer(GraphCommitCell.class, myGraphCommitCellRenderer);
120     setDefaultRenderer(String.class, new StringCellRenderer());
121
122     setShowHorizontalLines(false);
123     setIntercellSpacing(JBUI.emptySize());
124     setTableHeader(new InvisibleResizableHeader());
125
126     MouseAdapter mouseAdapter = new MyMouseAdapter();
127     addMouseMotionListener(mouseAdapter);
128     addMouseListener(mouseAdapter);
129
130     getSelectionModel().addListSelectionListener(new MyListSelectionListener());
131
132     PopupHandler.installPopupHandler(this, VcsLogActionPlaces.POPUP_ACTION_GROUP, VcsLogActionPlaces.VCS_LOG_TABLE_PLACE);
133     ScrollingUtil.installActions(this, false);
134
135     initColumnSize();
136     addComponentListener(new ComponentAdapter() {
137       @Override
138       public void componentResized(ComponentEvent e) {
139         updateCommitColumnWidth();
140       }
141     });
142   }
143
144   public void updateDataPack(@NotNull VisiblePack visiblePack, boolean permGraphChanged) {
145     VcsLogGraphTable.Selection previousSelection = getSelection();
146     getModel().setVisiblePack(visiblePack);
147     previousSelection.restore(visiblePack.getVisibleGraph(), true, permGraphChanged);
148
149     for (VcsLogHighlighter highlighter : myHighlighters) {
150       highlighter.update(visiblePack, permGraphChanged);
151     }
152
153     setPaintBusy(false);
154     initColumnSize();
155   }
156
157   boolean initColumnSize() {
158     if (!myColumnsSizeInitialized && getModel().getRowCount() > 0) {
159       myColumnsSizeInitialized = setColumnPreferredSize();
160       if (myColumnsSizeInitialized) {
161         setAutoCreateColumnsFromModel(false); // otherwise sizes are recalculated after each TableColumn re-initialization
162         for (int column = 0; column < getColumnCount(); column++) {
163           getColumnModel().getColumn(column).setResizable(column != GraphTableModel.ROOT_COLUMN);
164         }
165       }
166       return myColumnsSizeInitialized;
167     }
168     return false;
169   }
170
171   private boolean setColumnPreferredSize() {
172     boolean sizeCalculated = false;
173     Font tableFont = UIManager.getFont("Table.font");
174     for (int i = 0; i < getColumnCount(); i++) {
175       TableColumn column = getColumnModel().getColumn(i);
176       if (i == GraphTableModel.ROOT_COLUMN) { // thin stripe, or root name, or nothing
177         setRootColumnSize(column);
178       }
179       else if (i == GraphTableModel.AUTHOR_COLUMN) { // detect author with the longest name
180         // to avoid querying the last row (it would lead to full graph loading)
181         int maxRowsToCheck = Math.min(MAX_ROWS_TO_CALC_WIDTH, getRowCount() - MAX_ROWS_TO_CALC_OFFSET);
182         if (maxRowsToCheck < 0) { // but if the log is small, check all of them
183           maxRowsToCheck = getRowCount();
184         }
185         int maxWidth = 0;
186         for (int row = 0; row < maxRowsToCheck; row++) {
187           String value = getModel().getValueAt(row, i).toString();
188           maxWidth = Math.max(getFontMetrics(tableFont.deriveFont(Font.BOLD)).stringWidth(value), maxWidth);
189           if (!value.isEmpty()) sizeCalculated = true;
190         }
191         int min = Math.min(maxWidth + UIUtil.DEFAULT_HGAP, MAX_DEFAULT_AUTHOR_COLUMN_WIDTH);
192         column.setPreferredWidth(min);
193       }
194       else if (i == GraphTableModel.DATE_COLUMN) { // all dates have nearly equal sizes
195         int min = getFontMetrics(tableFont.deriveFont(Font.BOLD)).stringWidth("mm" + DateFormatUtil.formatDateTime(new Date()));
196         column.setPreferredWidth(min);
197       }
198     }
199
200     updateCommitColumnWidth();
201
202     return sizeCalculated;
203   }
204
205   private void updateCommitColumnWidth() {
206     int size = getWidth();
207     for (int i = 0; i < getColumnCount(); i++) {
208       if (i == GraphTableModel.COMMIT_COLUMN) continue;
209       TableColumn column = getColumnModel().getColumn(i);
210       size -= column.getPreferredWidth();
211     }
212
213     TableColumn commitColumn = getColumnModel().getColumn(GraphTableModel.COMMIT_COLUMN);
214     commitColumn.setPreferredWidth(size);
215   }
216
217   private void setRootColumnSize(@NotNull TableColumn column) {
218     int rootWidth;
219     if (!myUi.isMultipleRoots()) {
220       rootWidth = 0;
221     }
222     else if (!myUi.isShowRootNames()) {
223       rootWidth = ROOT_INDICATOR_WIDTH;
224     }
225     else {
226       rootWidth = Math.min(calculateMaxRootWidth(), ROOT_NAME_MAX_WIDTH);
227     }
228
229     // NB: all further instructions and their order are important, otherwise the minimum size which is less than 15 won't be applied
230     column.setMinWidth(rootWidth);
231     column.setMaxWidth(rootWidth);
232     column.setPreferredWidth(rootWidth);
233   }
234
235   private int calculateMaxRootWidth() {
236     int width = 0;
237     for (VirtualFile file : myLogData.getRoots()) {
238       Font tableFont = UIManager.getFont("Table.font");
239       width = Math.max(getFontMetrics(tableFont).stringWidth(file.getName() + "  "), width);
240     }
241     return width;
242   }
243
244   @Override
245   public String getToolTipText(@NotNull MouseEvent event) {
246     int row = rowAtPoint(event.getPoint());
247     int column = columnAtPoint(event.getPoint());
248     if (column < 0 || row < 0) {
249       return null;
250     }
251     if (column == GraphTableModel.ROOT_COLUMN) {
252       Object at = getValueAt(row, column);
253       if (at instanceof VirtualFile) {
254         return "<html><b>" +
255                ((VirtualFile)at).getPresentableUrl() +
256                "</b><br/>Click to " +
257                (myUi.isShowRootNames() ? "collapse" : "expand") +
258                "</html>";
259       }
260     }
261     return null;
262   }
263
264   public void jumpToRow(int rowIndex) {
265     if (rowIndex >= 0 && rowIndex <= getRowCount() - 1) {
266       scrollRectToVisible(getCellRect(rowIndex, 0, false));
267       setRowSelectionInterval(rowIndex, rowIndex);
268       scrollRectToVisible(getCellRect(rowIndex, 0, false));
269     }
270   }
271
272   @Nullable
273   @Override
274   public Object getData(@NonNls String dataId) {
275     if (PlatformDataKeys.COPY_PROVIDER.is(dataId)) {
276       return this;
277     }
278     return null;
279   }
280
281   @Override
282   public void performCopy(@NotNull DataContext dataContext) {
283     VcsLog log = VcsLogDataKeys.VCS_LOG.getData(dataContext);
284     if (log != null) {
285       List<VcsFullCommitDetails> details = VcsLogUtil.collectFirstPackOfLoadedSelectedDetails(log);
286       if (!details.isEmpty()) {
287         CopyPasteManager.getInstance()
288           .setContents(new StringSelection(StringUtil.join(details, VcsShortCommitDetails::getSubject, "\n")));
289       }
290     }
291   }
292
293   @Override
294   public boolean isCopyEnabled(@NotNull DataContext dataContext) {
295     return getSelectedRowCount() > 0;
296   }
297
298   @Override
299   public boolean isCopyVisible(@NotNull DataContext dataContext) {
300     return true;
301   }
302
303   public void addHighlighter(@NotNull VcsLogHighlighter highlighter) {
304     myHighlighters.add(highlighter);
305   }
306
307   public void removeHighlighter(@NotNull VcsLogHighlighter highlighter) {
308     myHighlighters.remove(highlighter);
309   }
310
311   public void removeAllHighlighters() {
312     myHighlighters.clear();
313   }
314
315   public SimpleTextAttributes applyHighlighters(@NotNull Component rendererComponent,
316                                                 int row,
317                                                 int column,
318                                                 String text,
319                                                 boolean hasFocus,
320                                                 final boolean selected) {
321     VcsLogHighlighter.VcsCommitStyle style = getStyle(row, column, text, hasFocus, selected);
322
323     assert style.getBackground() != null && style.getForeground() != null && style.getTextStyle() != null;
324
325     rendererComponent.setBackground(style.getBackground());
326     rendererComponent.setForeground(style.getForeground());
327
328     switch (style.getTextStyle()) {
329       case BOLD:
330         return SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
331       case ITALIC:
332         return SimpleTextAttributes.REGULAR_ITALIC_ATTRIBUTES;
333       default:
334     }
335     return SimpleTextAttributes.REGULAR_ATTRIBUTES;
336   }
337
338   private VcsLogHighlighter.VcsCommitStyle getStyle(int row, int column, String text, boolean hasFocus, final boolean selected) {
339     Component dummyRendererComponent = myDummyRenderer.getTableCellRendererComponent(this, text, selected, hasFocus, row, column);
340
341     VisibleGraph<Integer> visibleGraph = getVisibleGraph();
342     if (row < 0 || row >= visibleGraph.getVisibleCommitCount()) {
343       LOG.error("Visible graph has " + visibleGraph.getVisibleCommitCount() + " commits, yet we want row " + row);
344       return VcsCommitStyleFactory
345         .createStyle(dummyRendererComponent.getForeground(), dummyRendererComponent.getBackground(), VcsLogHighlighter.TextStyle.NORMAL);
346     }
347
348     RowInfo<Integer> rowInfo = visibleGraph.getRowInfo(row);
349
350     VcsLogHighlighter.VcsCommitStyle defaultStyle = VcsCommitStyleFactory
351       .createStyle(rowInfo.getRowType() == RowType.UNMATCHED ? JBColor.GRAY : dummyRendererComponent.getForeground(),
352                    dummyRendererComponent.getBackground(), VcsLogHighlighter.TextStyle.NORMAL);
353
354     final VcsShortCommitDetails details = myLogData.getMiniDetailsGetter().getCommitDataIfAvailable(rowInfo.getCommit());
355     if (details == null || details instanceof LoadingDetails) return defaultStyle;
356
357     List<VcsLogHighlighter.VcsCommitStyle> styles =
358       ContainerUtil.map(myHighlighters, highlighter -> highlighter.getStyle(details, selected));
359     return VcsCommitStyleFactory.combine(ContainerUtil.append(styles, defaultStyle));
360   }
361
362   public void viewportSet(JViewport viewport) {
363     viewport.addChangeListener(e -> {
364       AbstractTableModel model = getModel();
365       Couple<Integer> visibleRows = ScrollingUtil.getVisibleRows(this);
366       model.fireTableChanged(new TableModelEvent(model, visibleRows.first - 1, visibleRows.second, GraphTableModel.ROOT_COLUMN));
367     });
368   }
369
370   private boolean expandOrCollapseRoots(@NotNull MouseEvent e) {
371     TableColumn column = getRootColumnOrNull(e);
372     if (column != null) {
373       VcsLogUtil.triggerUsage("RootColumnClick");
374       myUi.setShowRootNames(!myUi.isShowRootNames());
375       return true;
376     }
377     return false;
378   }
379
380   public void rootColumnUpdated() {
381     setRootColumnSize(getColumnModel().getColumn(GraphTableModel.ROOT_COLUMN));
382     updateCommitColumnWidth();
383   }
384
385   @Nullable
386   private TableColumn getRootColumnOrNull(@NotNull MouseEvent e) {
387     if (!myLogData.isMultiRoot()) return null;
388     int column = convertColumnIndexToModel(columnAtPoint(e.getPoint()));
389     if (column == GraphTableModel.ROOT_COLUMN) {
390       return getColumnModel().getColumn(column);
391     }
392     return null;
393   }
394
395   public static JBColor getRootBackgroundColor(@NotNull VirtualFile root, @NotNull VcsLogColorManager colorManager) {
396     return VcsLogColorManagerImpl.getBackgroundColor(colorManager.getRootColor(root));
397   }
398
399   public void handleAnswer(@Nullable GraphAnswer<Integer> answer,
400                            boolean dataCouldChange,
401                            @Nullable Selection previousSelection,
402                            @Nullable MouseEvent e) {
403     if (dataCouldChange) {
404       getModel().fireTableDataChanged();
405
406       // since fireTableDataChanged clears selection we restore it here
407       if (previousSelection != null) {
408         previousSelection.restore(getVisibleGraph(), answer == null || (answer.getCommitToJump() != null && answer.doJump()), false);
409       }
410     }
411
412     myUi.repaintUI(); // in case of repaintUI doing something more than just repainting this table in some distant future
413
414     if (answer == null) {
415       return;
416     }
417
418     if (answer.getCursorToSet() != null) {
419       setCursor(answer.getCursorToSet());
420     }
421     if (answer.getCommitToJump() != null) {
422       Integer row = getModel().getVisiblePack().getVisibleGraph().getVisibleRowIndex(answer.getCommitToJump());
423       if (row != null && row >= 0 && answer.doJump()) {
424         jumpToRow(row);
425         // TODO wait for the full log and then jump
426         return;
427       }
428       if (e != null) showToolTip(getArrowTooltipText(answer.getCommitToJump(), row), e);
429     }
430   }
431
432   @Override
433   public void setCursor(Cursor cursor) {
434     super.setCursor(cursor);
435     Component layeredPane = UIUtil.findParentByCondition(this, component -> component instanceof LoadingDecorator.CursorAware);
436     if (layeredPane != null) {
437       layeredPane.setCursor(cursor);
438     }
439   }
440
441   @NotNull
442   private String getArrowTooltipText(int commit, @Nullable Integer row) {
443     VcsShortCommitDetails details;
444     if (row != null && row >= 0) {
445       details = getModel().getShortDetails(row); // preload rows around the commit
446     }
447     else {
448       details = myLogData.getMiniDetailsGetter().getCommitData(commit, Collections.singleton(commit)); // preload just the commit
449     }
450
451     String balloonText = "";
452     if (details instanceof LoadingDetails) {
453       CommitId commitId = myLogData.getCommitId(commit);
454       if (commitId != null) {
455         balloonText = "Jump to commit" + " " + commitId.getHash().toShortString();
456         if (myUi.isMultipleRoots()) {
457           balloonText += " in " + commitId.getRoot().getName();
458         }
459       }
460     }
461     else {
462       balloonText = "Jump to <b>\"" +
463                     StringUtil.shortenTextWithEllipsis(details.getSubject(), 50, 0, "...") +
464                     "\"</b> by " +
465                     VcsUserUtil.getShortPresentation(details.getAuthor()) +
466                     CommitPanel.formatDateTime(details.getAuthorTime());
467     }
468     return balloonText;
469   }
470
471   protected void showToolTip(@NotNull String text, @NotNull MouseEvent e) {
472     // standard tooltip does not allow to customize its location, and locating tooltip above can obscure some important info
473     Point point = new Point(e.getX() + 5, e.getY());
474
475     JEditorPane tipComponent = IdeTooltipManager.initPane(text, new HintHint(this, point).setAwtTooltip(true), null);
476     IdeTooltip tooltip = new IdeTooltip(this, point, new Wrapper(tipComponent)).setPreferredPosition(Balloon.Position.atRight);
477     IdeTooltipManager.getInstance().show(tooltip, false);
478   }
479
480   @Override
481   @NotNull
482   public GraphTableModel getModel() {
483     return (GraphTableModel)super.getModel();
484   }
485
486   @NotNull
487   public Selection getSelection() {
488     if (mySelection == null) mySelection = new Selection(this);
489     return mySelection;
490   }
491
492   private static class Selection {
493     @NotNull private final VcsLogGraphTable myTable;
494     @NotNull private final TIntHashSet mySelectedCommits;
495     @Nullable private final Integer myVisibleSelectedCommit;
496     @Nullable private final Integer myDelta;
497     private final boolean myIsOnTop;
498
499
500     public Selection(@NotNull VcsLogGraphTable table) {
501       myTable = table;
502       List<Integer> selectedRows = ContainerUtil.sorted(Ints.asList(myTable.getSelectedRows()));
503       Couple<Integer> visibleRows = ScrollingUtil.getVisibleRows(myTable);
504       myIsOnTop = visibleRows.first - 1 == 0;
505
506       VisibleGraph<Integer> graph = myTable.getVisibleGraph();
507
508       mySelectedCommits = new TIntHashSet();
509
510       Integer visibleSelectedCommit = null;
511       Integer delta = null;
512       for (int row : selectedRows) {
513         if (row < graph.getVisibleCommitCount()) {
514           Integer commit = graph.getRowInfo(row).getCommit();
515           mySelectedCommits.add(commit);
516           if (visibleRows.first - 1 <= row && row <= visibleRows.second && visibleSelectedCommit == null) {
517             visibleSelectedCommit = commit;
518             delta = myTable.getCellRect(row, 0, false).y - myTable.getVisibleRect().y;
519           }
520         }
521       }
522       if (visibleSelectedCommit == null && visibleRows.first - 1 >= 0) {
523         visibleSelectedCommit = graph.getRowInfo(visibleRows.first - 1).getCommit();
524         delta = myTable.getCellRect(visibleRows.first - 1, 0, false).y - myTable.getVisibleRect().y;
525       }
526
527       myVisibleSelectedCommit = visibleSelectedCommit;
528       myDelta = delta;
529     }
530
531     public void restore(@NotNull VisibleGraph<Integer> newVisibleGraph, boolean scrollToSelection, boolean permGraphChanged) {
532       Pair<TIntHashSet, Integer> toSelectAndScroll = findRowsToSelectAndScroll(myTable.getModel(), newVisibleGraph);
533       if (!toSelectAndScroll.first.isEmpty()) {
534         myTable.getSelectionModel().setValueIsAdjusting(true);
535         toSelectAndScroll.first.forEach(row -> {
536           myTable.addRowSelectionInterval(row, row);
537           return true;
538         });
539         myTable.getSelectionModel().setValueIsAdjusting(false);
540       }
541       if (scrollToSelection) {
542         if (myIsOnTop && permGraphChanged) { // scroll on top when some fresh commits arrive
543           scrollToRow(0, 0);
544         }
545         else if (toSelectAndScroll.second != null) {
546           assert myDelta != null;
547           scrollToRow(toSelectAndScroll.second, myDelta);
548         }
549       }
550       // sometimes commits that were selected are now collapsed
551       // currently in this case selection disappears
552       // in the future we need to create a method in LinearGraphController that allows to calculate visible commit for our commit
553       // or answer from collapse action could return a map that gives us some information about what commits were collapsed and where
554     }
555
556     private void scrollToRow(Integer row, Integer delta) {
557       Rectangle startRect = myTable.getCellRect(row, 0, true);
558       myTable.scrollRectToVisible(
559         new Rectangle(startRect.x, Math.max(startRect.y - delta, 0), startRect.width, myTable.getVisibleRect().height));
560     }
561
562     @NotNull
563     private Pair<TIntHashSet, Integer> findRowsToSelectAndScroll(@NotNull GraphTableModel model,
564                                                                  @NotNull VisibleGraph<Integer> visibleGraph) {
565       TIntHashSet rowsToSelect = new TIntHashSet();
566
567       if (model.getRowCount() == 0) {
568         // this should have been covered by facade.getVisibleCommitCount,
569         // but if the table is empty (no commits match the filter), the GraphFacade is not updated, because it can't handle it
570         // => it has previous values set.
571         return Pair.create(rowsToSelect, null);
572       }
573
574       Integer rowToScroll = null;
575       for (int row = 0;
576            row < visibleGraph.getVisibleCommitCount() && (rowsToSelect.size() < mySelectedCommits.size() || rowToScroll == null);
577            row++) { //stop iterating if found all hashes
578         int commit = visibleGraph.getRowInfo(row).getCommit();
579         if (mySelectedCommits.contains(commit)) {
580           rowsToSelect.add(row);
581         }
582         if (myVisibleSelectedCommit != null && myVisibleSelectedCommit == commit) {
583           rowToScroll = row;
584         }
585       }
586       return Pair.create(rowsToSelect, rowToScroll);
587     }
588   }
589
590   private class MyMouseAdapter extends MouseAdapter {
591     @NotNull private final TableLinkMouseListener myLinkListener = new TableLinkMouseListener();
592
593     @Override
594     public void mouseClicked(MouseEvent e) {
595       if (myLinkListener.onClick(e, e.getClickCount())) {
596         return;
597       }
598
599       if (e.getClickCount() == 1 && !expandOrCollapseRoots(e)) {
600         performAction(e, GraphAction.Type.MOUSE_CLICK);
601       }
602     }
603
604     @Override
605     public void mouseMoved(MouseEvent e) {
606       if (isAboveLink(e) || isAboveRoots(e)) {
607         setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
608       }
609       else if (!(VcsLogGraphTable.this.getCursor() == Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR))) {
610         performAction(e, GraphAction.Type.MOUSE_OVER);
611       }
612     }
613
614     private void performAction(@NotNull MouseEvent e, @NotNull final GraphAction.Type actionType) {
615       int row = PositionUtil.getRowIndex(e.getPoint(), getRowHeight());
616       if (row < 0 || row > getRowCount() - 1) {
617         return;
618       }
619       Point point = calcPoint4Graph(e.getPoint());
620       Collection<? extends PrintElement> printElements = getVisibleGraph().getRowInfo(row).getPrintElements();
621       PrintElement printElement = myGraphCellPainter.getElementUnderCursor(printElements, point.x, point.y);
622
623       boolean isClickOnGraphElement = actionType == GraphAction.Type.MOUSE_CLICK && printElement != null;
624       if (isClickOnGraphElement) {
625         triggerElementClick(printElement);
626       }
627
628       Selection previousSelection = getSelection();
629       GraphAnswer<Integer> answer =
630         getVisibleGraph().getActionController().performAction(new GraphAction.GraphActionImpl(printElement, actionType));
631       handleAnswer(answer, isClickOnGraphElement, previousSelection, e);
632     }
633
634     private boolean isAboveLink(MouseEvent e) {
635       return myLinkListener.getTagAt(e) != null;
636     }
637
638     private boolean isAboveRoots(MouseEvent e) {
639       TableColumn column = getRootColumnOrNull(e);
640       int row = rowAtPoint(e.getPoint());
641       return column != null && (row >= 0 && row < getRowCount());
642     }
643
644     @Override
645     public void mouseEntered(MouseEvent e) {
646       // Do nothing
647     }
648
649     @Override
650     public void mouseExited(MouseEvent e) {
651       // Do nothing
652     }
653   }
654
655   private static void triggerElementClick(@NotNull PrintElement printElement) {
656     if (printElement instanceof NodePrintElement) {
657       VcsLogUtil.triggerUsage("GraphNodeClick");
658     }
659     else if (printElement instanceof EdgePrintElement) {
660       if (((EdgePrintElement)printElement).hasArrow()) {
661         VcsLogUtil.triggerUsage("GraphArrowClick");
662       }
663     }
664   }
665
666   @NotNull
667   public VisibleGraph<Integer> getVisibleGraph() {
668     return getModel().getVisiblePack().getVisibleGraph();
669   }
670
671   @NotNull
672   private Point calcPoint4Graph(@NotNull Point clickPoint) {
673     return new Point(clickPoint.x - getXOffset(), PositionUtil.getYInsideRow(clickPoint, getRowHeight()));
674   }
675
676   private int getXOffset() {
677     TableColumn rootColumn = getColumnModel().getColumn(GraphTableModel.ROOT_COLUMN);
678     return myLogData.isMultiRoot() ? rootColumn.getWidth() : 0;
679   }
680
681   @Override
682   public TableCellEditor getCellEditor() {
683     // this fixes selection problems by prohibiting selection when user clicks on graph (CellEditor does that)
684     // what is fun about this code is that if you set cell editor in constructor with setCellEditor method it would not work
685     return myDummyEditor;
686   }
687
688   @Override
689   public int getRowHeight() {
690     return myGraphCommitCellRenderer.getPreferredHeight();
691   }
692
693   @Override
694   protected void paintFooter(@NotNull Graphics g, int x, int y, int width, int height) {
695     g.setColor(getStyle(getRowCount() - 1, GraphTableModel.COMMIT_COLUMN, "", hasFocus(), false).getBackground());
696     g.fillRect(x, y, width, height);
697     if (myUi.isMultipleRoots()) {
698       g.setColor(getRootBackgroundColor(getModel().getRoot(getRowCount() - 1), myUi.getColorManager()));
699
700       int rootWidth = getColumnModel().getColumn(GraphTableModel.ROOT_COLUMN).getWidth();
701       if (!myUi.isShowRootNames()) rootWidth -= ROOT_INDICATOR_WHITE_WIDTH;
702
703       g.fillRect(x, y, rootWidth, height);
704     }
705   }
706
707   private static class RootCellRenderer extends JBLabel implements TableCellRenderer {
708     @NotNull private final VcsLogUiImpl myUi;
709     @NotNull private Color myColor = UIUtil.getTableBackground();
710     @NotNull private Color myBorderColor = UIUtil.getTableBackground();
711     private boolean isNarrow = true;
712
713     RootCellRenderer(@NotNull VcsLogUiImpl ui) {
714       super("", CENTER);
715       myUi = ui;
716     }
717
718     @Override
719     protected void paintComponent(Graphics g) {
720       setFont(UIManager.getFont("Table.font"));
721       g.setColor(myColor);
722
723       int width = getWidth();
724
725       if (isNarrow) {
726         g.fillRect(0, 0, width - ROOT_INDICATOR_WHITE_WIDTH, myUi.getTable().getRowHeight());
727         g.setColor(myBorderColor);
728         g.fillRect(width - ROOT_INDICATOR_WHITE_WIDTH, 0, ROOT_INDICATOR_WHITE_WIDTH, myUi.getTable().getRowHeight());
729       }
730       else {
731         g.fillRect(0, 0, width, myUi.getTable().getRowHeight());
732       }
733
734       super.paintComponent(g);
735     }
736
737     @Override
738     public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
739       String text;
740       Color color;
741
742       if (value instanceof VirtualFile) {
743         VirtualFile root = (VirtualFile)value;
744         int readableRow = ScrollingUtil.getReadableRow(table, Math.round(myUi.getTable().getRowHeight() * 0.5f));
745         if (row < readableRow) {
746           text = "";
747         }
748         else if (row == 0 || !value.equals(table.getModel().getValueAt(row - 1, column)) || readableRow == row) {
749           text = root.getName();
750         }
751         else {
752           text = "";
753         }
754         color = getRootBackgroundColor(root, myUi.getColorManager());
755       }
756       else {
757         text = null;
758         color = UIUtil.getTableBackground(isSelected);
759       }
760
761       myColor = color;
762       Color background = ((VcsLogGraphTable)table).getStyle(row, column, text, hasFocus, isSelected).getBackground();
763       assert background != null;
764       myBorderColor = background;
765       setForeground(UIUtil.getTableForeground(false));
766
767       if (myUi.isShowRootNames()) {
768         setText(text);
769         isNarrow = false;
770       }
771       else {
772         setText("");
773         isNarrow = true;
774       }
775
776       return this;
777     }
778
779     @Override
780     public void setBackground(Color bg) {
781       myBorderColor = bg;
782     }
783   }
784
785   private class StringCellRenderer extends ColoredTableCellRenderer {
786     @Override
787     protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
788       if (value == null) {
789         return;
790       }
791       append(value.toString(), applyHighlighters(this, row, column, value.toString(), hasFocus, selected));
792       setBorder(null);
793     }
794   }
795
796   private class MyDummyTableCellEditor implements TableCellEditor {
797     @Override
798     public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
799       return null;
800     }
801
802     @Override
803     public Object getCellEditorValue() {
804       return null;
805     }
806
807     @Override
808     public boolean isCellEditable(EventObject anEvent) {
809       return false;
810     }
811
812     @Override
813     public boolean shouldSelectCell(EventObject anEvent) {
814       if (!(anEvent instanceof MouseEvent)) return true;
815       MouseEvent e = (MouseEvent)anEvent;
816
817       int row = PositionUtil.getRowIndex(e.getPoint(), getRowHeight());
818       if (row > getRowCount() - 1) {
819         return false;
820       }
821       Point point = calcPoint4Graph(e.getPoint());
822       Collection<? extends PrintElement> printElements = getVisibleGraph().getRowInfo(row).getPrintElements();
823       PrintElement printElement = myGraphCellPainter.getElementUnderCursor(printElements, point.x, point.y);
824       return printElement == null;
825     }
826
827     @Override
828     public boolean stopCellEditing() {
829       return false;
830     }
831
832     @Override
833     public void cancelCellEditing() {
834
835     }
836
837     @Override
838     public void addCellEditorListener(CellEditorListener l) {
839
840     }
841
842     @Override
843     public void removeCellEditorListener(CellEditorListener l) {
844
845     }
846   }
847
848   private class InvisibleResizableHeader extends JBTable.JBTableHeader {
849     @NotNull private final MyBasicTableHeaderUI myHeaderUI;
850     @Nullable private Cursor myCursor = null;
851
852     public InvisibleResizableHeader() {
853       myHeaderUI = new MyBasicTableHeaderUI(this);
854       // need a header to resize columns, so use header that is not visible
855       setDefaultRenderer(new EmptyTableCellRenderer());
856       setReorderingAllowed(false);
857     }
858
859     @Override
860     public void setTable(JTable table) {
861       JTable oldTable = getTable();
862       if (oldTable != null) {
863         oldTable.removeMouseListener(myHeaderUI);
864         oldTable.removeMouseMotionListener(myHeaderUI);
865       }
866
867       super.setTable(table);
868
869       if (table != null) {
870         table.addMouseListener(myHeaderUI);
871         table.addMouseMotionListener(myHeaderUI);
872       }
873     }
874
875     @Override
876     public void setCursor(@Nullable Cursor cursor) {
877       /* this method and the next one fixes cursor:
878          BasicTableHeaderUI.MouseInputHandler behaves like nobody else sets cursor
879          so we remember what it set last time and keep it unaffected by other cursor changes in the table
880        */
881       JTable table = getTable();
882       if (table != null) {
883         table.setCursor(cursor);
884         myCursor = cursor;
885       }
886       else {
887         super.setCursor(cursor);
888       }
889     }
890
891     @Override
892     public Cursor getCursor() {
893       if (myCursor == null) {
894         JTable table = getTable();
895         if (table == null) return super.getCursor();
896         return table.getCursor();
897       }
898       return myCursor;
899     }
900
901     @NotNull
902     @Override
903     public Rectangle getHeaderRect(int column) {
904       // if a header has zero height, mouse pointer can never be inside it, so we pretend it is one pixel high
905       Rectangle headerRect = super.getHeaderRect(column);
906       return new Rectangle(headerRect.x, headerRect.y, headerRect.width, 1);
907     }
908   }
909
910   private static class EmptyTableCellRenderer implements TableCellRenderer {
911     @NotNull
912     @Override
913     public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
914       JPanel panel = new JPanel(new BorderLayout());
915       panel.setMaximumSize(new Dimension(0, 0));
916       return panel;
917     }
918   }
919
920   // this class redirects events from the table to BasicTableHeaderUI.MouseInputHandler
921   private static class MyBasicTableHeaderUI extends BasicTableHeaderUI implements MouseInputListener {
922     public MyBasicTableHeaderUI(@NotNull JTableHeader tableHeader) {
923       header = tableHeader;
924       mouseInputListener = createMouseInputListener();
925     }
926
927     @NotNull
928     private MouseEvent convertMouseEvent(@NotNull MouseEvent e) {
929       // create a new event, almost exactly the same, but in the header
930       return new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), e.getModifiers(), e.getX(), 0, e.getXOnScreen(), header.getY(),
931                             e.getClickCount(), e.isPopupTrigger(), e.getButton());
932     }
933
934     @Override
935     public void mouseClicked(@NotNull MouseEvent e) {
936     }
937
938     @Override
939     public void mousePressed(@NotNull MouseEvent e) {
940       if (isOnBorder(e)) return;
941       mouseInputListener.mousePressed(convertMouseEvent(e));
942     }
943
944     @Override
945     public void mouseReleased(@NotNull MouseEvent e) {
946       if (isOnBorder(e)) return;
947       mouseInputListener.mouseReleased(convertMouseEvent(e));
948     }
949
950     @Override
951     public void mouseEntered(@NotNull MouseEvent e) {
952     }
953
954     @Override
955     public void mouseExited(@NotNull MouseEvent e) {
956     }
957
958     @Override
959     public void mouseDragged(@NotNull MouseEvent e) {
960       if (isOnBorder(e)) return;
961       mouseInputListener.mouseDragged(convertMouseEvent(e));
962     }
963
964     @Override
965     public void mouseMoved(@NotNull MouseEvent e) {
966       if (isOnBorder(e)) return;
967       mouseInputListener.mouseMoved(convertMouseEvent(e));
968     }
969
970     public boolean isOnBorder(@NotNull MouseEvent e) {
971       return Math.abs(header.getTable().getWidth() - e.getPoint().x) <= JBUI.scale(3);
972     }
973   }
974
975   private class MyListSelectionListener implements ListSelectionListener {
976     @Override
977     public void valueChanged(ListSelectionEvent e) {
978       mySelection = null;
979     }
980   }
981
982   private class MyProgressListener implements VcsLogProgress.ProgressListener {
983     @NotNull private String myText = "";
984
985     @Override
986     public void progressStarted() {
987       myText = getEmptyText().getText();
988       getEmptyText().setText("Loading History...");
989     }
990
991     @Override
992     public void progressStopped() {
993       getEmptyText().setText(myText);
994     }
995   }
996 }