Merge remote-tracking branch 'origin/master'
[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.openapi.actionSystem.DataContext;
21 import com.intellij.openapi.actionSystem.DataProvider;
22 import com.intellij.openapi.actionSystem.PlatformDataKeys;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.ide.CopyPasteManager;
25 import com.intellij.openapi.ui.LoadingDecorator;
26 import com.intellij.openapi.util.Comparing;
27 import com.intellij.openapi.util.Couple;
28 import com.intellij.openapi.util.Pair;
29 import com.intellij.openapi.util.text.StringUtil;
30 import com.intellij.openapi.vfs.VirtualFile;
31 import com.intellij.ui.*;
32 import com.intellij.ui.components.JBLabel;
33 import com.intellij.ui.table.JBTable;
34 import com.intellij.util.containers.ContainerUtil;
35 import com.intellij.util.text.DateFormatUtil;
36 import com.intellij.util.ui.JBUI;
37 import com.intellij.util.ui.UIUtil;
38 import com.intellij.vcs.log.*;
39 import com.intellij.vcs.log.data.LoadingDetails;
40 import com.intellij.vcs.log.data.VcsLogData;
41 import com.intellij.vcs.log.data.VcsLogProgress;
42 import com.intellij.vcs.log.data.VisiblePack;
43 import com.intellij.vcs.log.graph.DefaultColorGenerator;
44 import com.intellij.vcs.log.graph.RowInfo;
45 import com.intellij.vcs.log.graph.RowType;
46 import com.intellij.vcs.log.graph.VisibleGraph;
47 import com.intellij.vcs.log.graph.actions.GraphAnswer;
48 import com.intellij.vcs.log.impl.VcsLogUtil;
49 import com.intellij.vcs.log.paint.GraphCellPainter;
50 import com.intellij.vcs.log.paint.SimpleGraphCellPainter;
51 import com.intellij.vcs.log.ui.VcsLogActionPlaces;
52 import com.intellij.vcs.log.ui.VcsLogColorManager;
53 import com.intellij.vcs.log.ui.VcsLogColorManagerImpl;
54 import com.intellij.vcs.log.ui.VcsLogUiImpl;
55 import com.intellij.vcs.log.ui.render.GraphCommitCell;
56 import com.intellij.vcs.log.ui.render.GraphCommitCellRenderer;
57 import com.intellij.vcs.log.ui.tables.GraphTableModel;
58 import gnu.trove.TIntHashSet;
59 import org.jetbrains.annotations.NonNls;
60 import org.jetbrains.annotations.NotNull;
61 import org.jetbrains.annotations.Nullable;
62
63 import javax.swing.*;
64 import javax.swing.event.*;
65 import javax.swing.plaf.basic.BasicTableHeaderUI;
66 import javax.swing.table.*;
67 import java.awt.*;
68 import java.awt.datatransfer.StringSelection;
69 import java.awt.event.ComponentAdapter;
70 import java.awt.event.ComponentEvent;
71 import java.awt.event.MouseEvent;
72 import java.util.Collection;
73 import java.util.Date;
74 import java.util.EventObject;
75 import java.util.List;
76
77 import static com.intellij.vcs.log.VcsLogHighlighter.TextStyle.BOLD;
78 import static com.intellij.vcs.log.VcsLogHighlighter.TextStyle.ITALIC;
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_WHITE_WIDTH = 5;
84   private static final int ROOT_INDICATOR_WIDTH = ROOT_INDICATOR_WHITE_WIDTH + 8;
85   private static final int ROOT_NAME_MAX_WIDTH = 200;
86   private static final int MAX_DEFAULT_AUTHOR_COLUMN_WIDTH = 200;
87   private static final int MAX_ROWS_TO_CALC_WIDTH = 1000;
88   private static final int MAX_ROWS_TO_CALC_OFFSET = 100;
89
90   @NotNull private final VcsLogUiImpl myUi;
91   @NotNull private final VcsLogData myLogData;
92   @NotNull private final MyDummyTableCellEditor myDummyEditor = new MyDummyTableCellEditor();
93   @NotNull private final TableCellRenderer myDummyRenderer = new DefaultTableCellRenderer();
94   @NotNull private final GraphCommitCellRenderer myGraphCommitCellRenderer;
95   @NotNull private final GraphTableController myController;
96   @NotNull private final StringCellRenderer myStringCellRenderer;
97   private boolean myColumnsSizeInitialized = false;
98
99   @Nullable private Selection mySelection = null;
100
101   @NotNull private final Collection<VcsLogHighlighter> myHighlighters = ContainerUtil.newArrayList();
102
103   public VcsLogGraphTable(@NotNull VcsLogUiImpl ui, @NotNull VcsLogData logData, @NotNull VisiblePack initialDataPack) {
104     super(new GraphTableModel(initialDataPack, logData, ui));
105     getEmptyText().setText("Changes Log");
106
107     myUi = ui;
108     myLogData = logData;
109     GraphCellPainter graphCellPainter = new SimpleGraphCellPainter(new DefaultColorGenerator()) {
110       @Override
111       protected int getRowHeight() {
112         return VcsLogGraphTable.this.getRowHeight();
113       }
114     };
115     myGraphCommitCellRenderer = new GraphCommitCellRenderer(logData, graphCellPainter, this);
116     myStringCellRenderer = new StringCellRenderer();
117
118     myLogData.getProgress().addProgressIndicatorListener(new MyProgressListener(), ui);
119
120     setDefaultRenderer(VirtualFile.class, new RootCellRenderer(myUi));
121     setDefaultRenderer(GraphCommitCell.class, myGraphCommitCellRenderer);
122     setDefaultRenderer(String.class, myStringCellRenderer);
123
124     setShowHorizontalLines(false);
125     setIntercellSpacing(JBUI.emptySize());
126     setTableHeader(new InvisibleResizableHeader());
127
128     myController = new GraphTableController(this, ui, logData, graphCellPainter, myGraphCommitCellRenderer);
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           Font font = tableFont;
189           VcsLogHighlighter.TextStyle style = getStyle(row, i, false, false).getTextStyle();
190           if (BOLD.equals(style)) {
191             font = tableFont.deriveFont(Font.BOLD);
192           }
193           else if (ITALIC.equals(style)) {
194             font = tableFont.deriveFont(Font.ITALIC);
195           }
196           maxWidth = Math.max(getFontMetrics(font).stringWidth(value + "*"), maxWidth);
197           if (!value.isEmpty()) sizeCalculated = true;
198         }
199         int min = Math.min(maxWidth + myStringCellRenderer.getHorizontalTextPadding(), JBUI.scale(MAX_DEFAULT_AUTHOR_COLUMN_WIDTH));
200         column.setPreferredWidth(min);
201       }
202       else if (i == GraphTableModel.DATE_COLUMN) { // all dates have nearly equal sizes
203         int min = getFontMetrics(tableFont.deriveFont(Font.BOLD)).stringWidth(DateFormatUtil.formatDateTime(new Date())) +
204                   myStringCellRenderer.getHorizontalTextPadding();
205         column.setPreferredWidth(min);
206       }
207     }
208
209     updateCommitColumnWidth();
210
211     return sizeCalculated;
212   }
213
214   private void updateCommitColumnWidth() {
215     int size = getWidth();
216     for (int i = 0; i < getColumnCount(); i++) {
217       if (i == GraphTableModel.COMMIT_COLUMN) continue;
218       TableColumn column = getColumnModel().getColumn(i);
219       size -= column.getPreferredWidth();
220     }
221
222     TableColumn commitColumn = getColumnModel().getColumn(GraphTableModel.COMMIT_COLUMN);
223     commitColumn.setPreferredWidth(size);
224   }
225
226   private void setRootColumnSize(@NotNull TableColumn column) {
227     int rootWidth;
228     if (!myUi.isMultipleRoots()) {
229       rootWidth = 0;
230     }
231     else if (!myUi.isShowRootNames()) {
232       rootWidth = JBUI.scale(ROOT_INDICATOR_WIDTH);
233     }
234     else {
235       rootWidth = Math.min(calculateMaxRootWidth(), JBUI.scale(ROOT_NAME_MAX_WIDTH));
236     }
237
238     // NB: all further instructions and their order are important, otherwise the minimum size which is less than 15 won't be applied
239     column.setMinWidth(rootWidth);
240     column.setMaxWidth(rootWidth);
241     column.setPreferredWidth(rootWidth);
242   }
243
244   private int calculateMaxRootWidth() {
245     int width = 0;
246     for (VirtualFile file : myLogData.getRoots()) {
247       Font tableFont = UIManager.getFont("Table.font");
248       width = Math.max(getFontMetrics(tableFont).stringWidth(file.getName() + "  "), width);
249     }
250     return width;
251   }
252
253   @Override
254   public String getToolTipText(@NotNull MouseEvent event) {
255     int row = rowAtPoint(event.getPoint());
256     int column = columnAtPoint(event.getPoint());
257     if (column < 0 || row < 0) {
258       return null;
259     }
260     if (column == GraphTableModel.ROOT_COLUMN) {
261       Object at = getValueAt(row, column);
262       if (at instanceof VirtualFile) {
263         return "<html><b>" +
264                ((VirtualFile)at).getPresentableUrl() +
265                "</b><br/>Click to " +
266                (myUi.isShowRootNames() ? "collapse" : "expand") +
267                "</html>";
268       }
269     }
270     return null;
271   }
272
273   public void jumpToRow(int rowIndex) {
274     if (rowIndex >= 0 && rowIndex <= getRowCount() - 1) {
275       scrollRectToVisible(getCellRect(rowIndex, 0, false));
276       setRowSelectionInterval(rowIndex, rowIndex);
277       scrollRectToVisible(getCellRect(rowIndex, 0, false));
278     }
279   }
280
281   @Nullable
282   @Override
283   public Object getData(@NonNls String dataId) {
284     if (PlatformDataKeys.COPY_PROVIDER.is(dataId)) {
285       return this;
286     }
287     return null;
288   }
289
290   @Override
291   public void performCopy(@NotNull DataContext dataContext) {
292     VcsLog log = VcsLogDataKeys.VCS_LOG.getData(dataContext);
293     if (log == null) return;
294     List<VcsFullCommitDetails> details = VcsLogUtil.collectFirstPackOfLoadedSelectedDetails(log);
295     if (details.isEmpty()) return;
296     String text = StringUtil.join(details, commit -> getPresentableText(commit, true), "\n");
297     CopyPasteManager.getInstance().setContents(new StringSelection(text));
298   }
299
300   @Override
301   public boolean isCopyEnabled(@NotNull DataContext dataContext) {
302     return getSelectedRowCount() > 0;
303   }
304
305   @NotNull
306   private static String getPresentableText(@NotNull VcsFullCommitDetails commit, boolean withMessage) {
307     // implementation reflected by com.intellij.openapi.vcs.history.FileHistoryPanelImpl.getPresentableText()
308     StringBuilder sb = new StringBuilder();
309     sb.append(commit.getId().toShortString()).append(" ");
310     sb.append(commit.getAuthor().getName());
311     long time = commit.getAuthorTime();
312     sb.append(" on ").append(DateFormatUtil.formatDate(time)).append(" at ").append(DateFormatUtil.formatTime(time));
313     if (!Comparing.equal(commit.getAuthor(), commit.getCommitter())) {
314       sb.append(" (committed by ").append(commit.getCommitter().getName()).append(")");
315     }
316     if (withMessage) {
317       sb.append(" ").append(commit.getSubject());
318     }
319     return sb.toString();
320   }
321
322   @Override
323   public boolean isCopyVisible(@NotNull DataContext dataContext) {
324     return true;
325   }
326
327   public void addHighlighter(@NotNull VcsLogHighlighter highlighter) {
328     myHighlighters.add(highlighter);
329   }
330
331   public void removeHighlighter(@NotNull VcsLogHighlighter highlighter) {
332     myHighlighters.remove(highlighter);
333   }
334
335   public void removeAllHighlighters() {
336     myHighlighters.clear();
337   }
338
339   @NotNull
340   public SimpleTextAttributes applyHighlighters(@NotNull Component rendererComponent,
341                                                 int row,
342                                                 int column,
343                                                 boolean hasFocus,
344                                                 final boolean selected) {
345     VcsLogHighlighter.VcsCommitStyle style = getStyle(row, column, hasFocus, selected);
346
347     assert style.getBackground() != null && style.getForeground() != null && style.getTextStyle() != null;
348
349     rendererComponent.setBackground(style.getBackground());
350     rendererComponent.setForeground(style.getForeground());
351
352     switch (style.getTextStyle()) {
353       case BOLD:
354         return SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
355       case ITALIC:
356         return SimpleTextAttributes.REGULAR_ITALIC_ATTRIBUTES;
357       default:
358     }
359     return SimpleTextAttributes.REGULAR_ATTRIBUTES;
360   }
361
362   public VcsLogHighlighter.VcsCommitStyle getBaseStyle(int row, int column, boolean hasFocus, boolean selected) {
363     Component dummyRendererComponent = myDummyRenderer.getTableCellRendererComponent(this, "", selected, hasFocus, row, column);
364     return VcsCommitStyleFactory
365       .createStyle(dummyRendererComponent.getForeground(), dummyRendererComponent.getBackground(), VcsLogHighlighter.TextStyle.NORMAL);
366   }
367
368   private VcsLogHighlighter.VcsCommitStyle getStyle(int row, int column, boolean hasFocus, boolean selected) {
369     VcsLogHighlighter.VcsCommitStyle baseStyle = getBaseStyle(row, column, hasFocus, selected);
370
371     VisibleGraph<Integer> visibleGraph = getVisibleGraph();
372     if (row < 0 || row >= visibleGraph.getVisibleCommitCount()) {
373       LOG.error("Visible graph has " + visibleGraph.getVisibleCommitCount() + " commits, yet we want row " + row);
374       return baseStyle;
375     }
376
377     RowInfo<Integer> rowInfo = visibleGraph.getRowInfo(row);
378
379     VcsLogHighlighter.VcsCommitStyle defaultStyle = VcsCommitStyleFactory
380       .createStyle(rowInfo.getRowType() == RowType.UNMATCHED ? JBColor.GRAY : baseStyle.getForeground(), baseStyle.getBackground(),
381                    VcsLogHighlighter.TextStyle.NORMAL);
382
383     final VcsShortCommitDetails details = myLogData.getMiniDetailsGetter().getCommitDataIfAvailable(rowInfo.getCommit());
384     if (details == null || details instanceof LoadingDetails) return defaultStyle;
385
386     List<VcsLogHighlighter.VcsCommitStyle> styles =
387       ContainerUtil.map(myHighlighters, highlighter -> highlighter.getStyle(details, selected));
388     return VcsCommitStyleFactory.combine(ContainerUtil.append(styles, defaultStyle));
389   }
390
391   public void viewportSet(JViewport viewport) {
392     viewport.addChangeListener(e -> {
393       AbstractTableModel model = getModel();
394       Couple<Integer> visibleRows = ScrollingUtil.getVisibleRows(this);
395       model.fireTableChanged(new TableModelEvent(model, visibleRows.first - 1, visibleRows.second, GraphTableModel.ROOT_COLUMN));
396     });
397   }
398
399   public void rootColumnUpdated() {
400     setRootColumnSize(getColumnModel().getColumn(GraphTableModel.ROOT_COLUMN));
401     updateCommitColumnWidth();
402   }
403
404   public static JBColor getRootBackgroundColor(@NotNull VirtualFile root, @NotNull VcsLogColorManager colorManager) {
405     return VcsLogColorManagerImpl.getBackgroundColor(colorManager.getRootColor(root));
406   }
407
408   @Override
409   public void setCursor(Cursor cursor) {
410     super.setCursor(cursor);
411     Component layeredPane = UIUtil.findParentByCondition(this, component -> component instanceof LoadingDecorator.CursorAware);
412     if (layeredPane != null) {
413       layeredPane.setCursor(cursor);
414     }
415   }
416
417   @Override
418   @NotNull
419   public GraphTableModel getModel() {
420     return (GraphTableModel)super.getModel();
421   }
422
423   @NotNull
424   public Selection getSelection() {
425     if (mySelection == null) mySelection = new Selection(this);
426     return mySelection;
427   }
428
429   public void handleAnswer(@Nullable GraphAnswer<Integer> answer, boolean dataCouldChange) {
430     myController.handleGraphAnswer(answer, dataCouldChange, null, null);
431   }
432
433   static class Selection {
434     @NotNull private final VcsLogGraphTable myTable;
435     @NotNull private final TIntHashSet mySelectedCommits;
436     @Nullable private final Integer myVisibleSelectedCommit;
437     @Nullable private final Integer myDelta;
438     private final boolean myIsOnTop;
439
440
441     public Selection(@NotNull VcsLogGraphTable table) {
442       myTable = table;
443       List<Integer> selectedRows = ContainerUtil.sorted(Ints.asList(myTable.getSelectedRows()));
444       Couple<Integer> visibleRows = ScrollingUtil.getVisibleRows(myTable);
445       myIsOnTop = visibleRows.first - 1 == 0;
446
447       VisibleGraph<Integer> graph = myTable.getVisibleGraph();
448
449       mySelectedCommits = new TIntHashSet();
450
451       Integer visibleSelectedCommit = null;
452       Integer delta = null;
453       for (int row : selectedRows) {
454         if (row < graph.getVisibleCommitCount()) {
455           Integer commit = graph.getRowInfo(row).getCommit();
456           mySelectedCommits.add(commit);
457           if (visibleRows.first - 1 <= row && row <= visibleRows.second && visibleSelectedCommit == null) {
458             visibleSelectedCommit = commit;
459             delta = myTable.getCellRect(row, 0, false).y - myTable.getVisibleRect().y;
460           }
461         }
462       }
463       if (visibleSelectedCommit == null && visibleRows.first - 1 >= 0) {
464         visibleSelectedCommit = graph.getRowInfo(visibleRows.first - 1).getCommit();
465         delta = myTable.getCellRect(visibleRows.first - 1, 0, false).y - myTable.getVisibleRect().y;
466       }
467
468       myVisibleSelectedCommit = visibleSelectedCommit;
469       myDelta = delta;
470     }
471
472     public void restore(@NotNull VisibleGraph<Integer> newVisibleGraph, boolean scrollToSelection, boolean permGraphChanged) {
473       Pair<TIntHashSet, Integer> toSelectAndScroll = findRowsToSelectAndScroll(myTable.getModel(), newVisibleGraph);
474       if (!toSelectAndScroll.first.isEmpty()) {
475         myTable.getSelectionModel().setValueIsAdjusting(true);
476         toSelectAndScroll.first.forEach(row -> {
477           myTable.addRowSelectionInterval(row, row);
478           return true;
479         });
480         myTable.getSelectionModel().setValueIsAdjusting(false);
481       }
482       if (scrollToSelection) {
483         if (myIsOnTop && permGraphChanged) { // scroll on top when some fresh commits arrive
484           scrollToRow(0, 0);
485         }
486         else if (toSelectAndScroll.second != null) {
487           assert myDelta != null;
488           scrollToRow(toSelectAndScroll.second, myDelta);
489         }
490       }
491       // sometimes commits that were selected are now collapsed
492       // currently in this case selection disappears
493       // in the future we need to create a method in LinearGraphController that allows to calculate visible commit for our commit
494       // or answer from collapse action could return a map that gives us some information about what commits were collapsed and where
495     }
496
497     private void scrollToRow(Integer row, Integer delta) {
498       Rectangle startRect = myTable.getCellRect(row, 0, true);
499       myTable.scrollRectToVisible(
500         new Rectangle(startRect.x, Math.max(startRect.y - delta, 0), startRect.width, myTable.getVisibleRect().height));
501     }
502
503     @NotNull
504     private Pair<TIntHashSet, Integer> findRowsToSelectAndScroll(@NotNull GraphTableModel model,
505                                                                  @NotNull VisibleGraph<Integer> visibleGraph) {
506       TIntHashSet rowsToSelect = new TIntHashSet();
507
508       if (model.getRowCount() == 0) {
509         // this should have been covered by facade.getVisibleCommitCount,
510         // but if the table is empty (no commits match the filter), the GraphFacade is not updated, because it can't handle it
511         // => it has previous values set.
512         return Pair.create(rowsToSelect, null);
513       }
514
515       Integer rowToScroll = null;
516       for (int row = 0;
517            row < visibleGraph.getVisibleCommitCount() && (rowsToSelect.size() < mySelectedCommits.size() || rowToScroll == null);
518            row++) { //stop iterating if found all hashes
519         int commit = visibleGraph.getRowInfo(row).getCommit();
520         if (mySelectedCommits.contains(commit)) {
521           rowsToSelect.add(row);
522         }
523         if (myVisibleSelectedCommit != null && myVisibleSelectedCommit == commit) {
524           rowToScroll = row;
525         }
526       }
527       return Pair.create(rowsToSelect, rowToScroll);
528     }
529   }
530
531   @NotNull
532   public VisibleGraph<Integer> getVisibleGraph() {
533     return getModel().getVisiblePack().getVisibleGraph();
534   }
535
536   @Override
537   public TableCellEditor getCellEditor() {
538     // this fixes selection problems by prohibiting selection when user clicks on graph (CellEditor does that)
539     // what is fun about this code is that if you set cell editor in constructor with setCellEditor method it would not work
540     return myDummyEditor;
541   }
542
543   @Override
544   public int getRowHeight() {
545     return myGraphCommitCellRenderer.getPreferredHeight();
546   }
547
548   @Override
549   protected void paintFooter(@NotNull Graphics g, int x, int y, int width, int height) {
550     int lastRow = getRowCount() - 1;
551     if (lastRow >= 0) {
552       g.setColor(getStyle(lastRow, GraphTableModel.COMMIT_COLUMN, hasFocus(), false).getBackground());
553       g.fillRect(x, y, width, height);
554       if (myUi.isMultipleRoots()) {
555         g.setColor(getRootBackgroundColor(getModel().getRoot(lastRow), myUi.getColorManager()));
556
557         int rootWidth = getColumnModel().getColumn(GraphTableModel.ROOT_COLUMN).getWidth();
558         if (!myUi.isShowRootNames()) rootWidth -= JBUI.scale(ROOT_INDICATOR_WHITE_WIDTH);
559
560         g.fillRect(x, y, rootWidth, height);
561       }
562     }
563     else {
564       g.setColor(getBaseStyle(lastRow, GraphTableModel.COMMIT_COLUMN, hasFocus(), false).getBackground());
565       g.fillRect(x, y, width, height);
566     }
567   }
568
569   boolean isResizingColumns() {
570     return getCursor() == Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
571   }
572
573   private static class RootCellRenderer extends JBLabel implements TableCellRenderer {
574     @NotNull private final VcsLogUiImpl myUi;
575     @NotNull private Color myColor = UIUtil.getTableBackground();
576     @NotNull private Color myBorderColor = UIUtil.getTableBackground();
577     private boolean isNarrow = true;
578
579     RootCellRenderer(@NotNull VcsLogUiImpl ui) {
580       super("", CENTER);
581       myUi = ui;
582     }
583
584     @Override
585     protected void paintComponent(Graphics g) {
586       setFont(UIManager.getFont("Table.font"));
587       g.setColor(myColor);
588
589       int width = getWidth();
590
591       if (isNarrow) {
592         g.fillRect(0, 0, width - JBUI.scale(ROOT_INDICATOR_WHITE_WIDTH), myUi.getTable().getRowHeight());
593         g.setColor(myBorderColor);
594         g.fillRect(width - JBUI.scale(ROOT_INDICATOR_WHITE_WIDTH), 0, JBUI.scale(ROOT_INDICATOR_WHITE_WIDTH),
595                    myUi.getTable().getRowHeight());
596       }
597       else {
598         g.fillRect(0, 0, width, myUi.getTable().getRowHeight());
599       }
600
601       super.paintComponent(g);
602     }
603
604     @Override
605     public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
606       String text;
607       Color color;
608
609       if (value instanceof VirtualFile) {
610         VirtualFile root = (VirtualFile)value;
611         int readableRow = ScrollingUtil.getReadableRow(table, Math.round(myUi.getTable().getRowHeight() * 0.5f));
612         if (row < readableRow) {
613           text = "";
614         }
615         else if (row == 0 || !value.equals(table.getModel().getValueAt(row - 1, column)) || readableRow == row) {
616           text = root.getName();
617         }
618         else {
619           text = "";
620         }
621         color = getRootBackgroundColor(root, myUi.getColorManager());
622       }
623       else {
624         text = null;
625         color = UIUtil.getTableBackground(isSelected);
626       }
627
628       myColor = color;
629       Color background = ((VcsLogGraphTable)table).getStyle(row, column, hasFocus, isSelected).getBackground();
630       assert background != null;
631       myBorderColor = background;
632       setForeground(UIUtil.getTableForeground(false));
633
634       if (myUi.isShowRootNames()) {
635         setText(text);
636         isNarrow = false;
637       }
638       else {
639         setText("");
640         isNarrow = true;
641       }
642
643       return this;
644     }
645
646     @Override
647     public void setBackground(Color bg) {
648       myBorderColor = bg;
649     }
650   }
651
652   private class StringCellRenderer extends ColoredTableCellRenderer {
653     @Override
654     protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
655       if (value == null) {
656         return;
657       }
658       append(value.toString(), applyHighlighters(this, row, column, hasFocus, selected));
659     }
660
661     public int getHorizontalTextPadding() {
662       Insets borderInsets = getMyBorder().getBorderInsets(this);
663       Insets ipad = getIpad();
664       return borderInsets.left + borderInsets.right + ipad.left + ipad.right;
665     }
666   }
667
668   private class MyDummyTableCellEditor implements TableCellEditor {
669     @Override
670     public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
671       return null;
672     }
673
674     @Override
675     public Object getCellEditorValue() {
676       return null;
677     }
678
679     @Override
680     public boolean isCellEditable(EventObject anEvent) {
681       return false;
682     }
683
684     @Override
685     public boolean shouldSelectCell(EventObject anEvent) {
686       if (!(anEvent instanceof MouseEvent)) return true;
687
688       return myController.findPrintElement((MouseEvent)anEvent) == null;
689     }
690
691     @Override
692     public boolean stopCellEditing() {
693       return false;
694     }
695
696     @Override
697     public void cancelCellEditing() {
698
699     }
700
701     @Override
702     public void addCellEditorListener(CellEditorListener l) {
703
704     }
705
706     @Override
707     public void removeCellEditorListener(CellEditorListener l) {
708
709     }
710   }
711
712   private class InvisibleResizableHeader extends JBTable.JBTableHeader {
713     @NotNull private final MyBasicTableHeaderUI myHeaderUI;
714     @Nullable private Cursor myCursor = null;
715
716     public InvisibleResizableHeader() {
717       myHeaderUI = new MyBasicTableHeaderUI(this);
718       // need a header to resize columns, so use header that is not visible
719       setDefaultRenderer(new EmptyTableCellRenderer());
720       setReorderingAllowed(false);
721     }
722
723     @Override
724     public void setTable(JTable table) {
725       JTable oldTable = getTable();
726       if (oldTable != null) {
727         oldTable.removeMouseListener(myHeaderUI);
728         oldTable.removeMouseMotionListener(myHeaderUI);
729       }
730
731       super.setTable(table);
732
733       if (table != null) {
734         table.addMouseListener(myHeaderUI);
735         table.addMouseMotionListener(myHeaderUI);
736       }
737     }
738
739     @Override
740     public void setCursor(@Nullable Cursor cursor) {
741       /* this method and the next one fixes cursor:
742          BasicTableHeaderUI.MouseInputHandler behaves like nobody else sets cursor
743          so we remember what it set last time and keep it unaffected by other cursor changes in the table
744        */
745       JTable table = getTable();
746       if (table != null) {
747         table.setCursor(cursor);
748         myCursor = cursor;
749       }
750       else {
751         super.setCursor(cursor);
752       }
753     }
754
755     @Override
756     public Cursor getCursor() {
757       if (myCursor == null) {
758         JTable table = getTable();
759         if (table == null) return super.getCursor();
760         return table.getCursor();
761       }
762       return myCursor;
763     }
764
765     @NotNull
766     @Override
767     public Rectangle getHeaderRect(int column) {
768       // if a header has zero height, mouse pointer can never be inside it, so we pretend it is one pixel high
769       Rectangle headerRect = super.getHeaderRect(column);
770       return new Rectangle(headerRect.x, headerRect.y, headerRect.width, 1);
771     }
772   }
773
774   private static class EmptyTableCellRenderer implements TableCellRenderer {
775     @NotNull
776     @Override
777     public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
778       JPanel panel = new JPanel(new BorderLayout());
779       panel.setMaximumSize(new Dimension(0, 0));
780       return panel;
781     }
782   }
783
784   // this class redirects events from the table to BasicTableHeaderUI.MouseInputHandler
785   private static class MyBasicTableHeaderUI extends BasicTableHeaderUI implements MouseInputListener {
786     public MyBasicTableHeaderUI(@NotNull JTableHeader tableHeader) {
787       header = tableHeader;
788       mouseInputListener = createMouseInputListener();
789     }
790
791     @NotNull
792     private MouseEvent convertMouseEvent(@NotNull MouseEvent e) {
793       // create a new event, almost exactly the same, but in the header
794       return new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), e.getModifiers(), e.getX(), 0, e.getXOnScreen(), header.getY(),
795                             e.getClickCount(), e.isPopupTrigger(), e.getButton());
796     }
797
798     @Override
799     public void mouseClicked(@NotNull MouseEvent e) {
800     }
801
802     @Override
803     public void mousePressed(@NotNull MouseEvent e) {
804       if (isOnBorder(e)) return;
805       mouseInputListener.mousePressed(convertMouseEvent(e));
806     }
807
808     @Override
809     public void mouseReleased(@NotNull MouseEvent e) {
810       if (isOnBorder(e)) return;
811       mouseInputListener.mouseReleased(convertMouseEvent(e));
812     }
813
814     @Override
815     public void mouseEntered(@NotNull MouseEvent e) {
816     }
817
818     @Override
819     public void mouseExited(@NotNull MouseEvent e) {
820     }
821
822     @Override
823     public void mouseDragged(@NotNull MouseEvent e) {
824       if (isOnBorder(e)) return;
825       mouseInputListener.mouseDragged(convertMouseEvent(e));
826     }
827
828     @Override
829     public void mouseMoved(@NotNull MouseEvent e) {
830       if (isOnBorder(e)) return;
831       mouseInputListener.mouseMoved(convertMouseEvent(e));
832     }
833
834     public boolean isOnBorder(@NotNull MouseEvent e) {
835       return Math.abs(header.getTable().getWidth() - e.getPoint().x) <= JBUI.scale(3);
836     }
837   }
838
839   private class MyListSelectionListener implements ListSelectionListener {
840     @Override
841     public void valueChanged(ListSelectionEvent e) {
842       mySelection = null;
843     }
844   }
845
846   private class MyProgressListener implements VcsLogProgress.ProgressListener {
847     @NotNull private String myText = "";
848
849     @Override
850     public void progressStarted() {
851       myText = getEmptyText().getText();
852       getEmptyText().setText("Loading History...");
853     }
854
855     @Override
856     public void progressStopped() {
857       getEmptyText().setText(myText);
858     }
859   }
860 }