2 * Copyright 2000-2015 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.vcs.log.ui.frame;
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;
68 import javax.swing.event.*;
69 import javax.swing.plaf.basic.BasicTableHeaderUI;
70 import javax.swing.table.*;
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;
78 import java.util.List;
80 public class VcsLogGraphTable extends TableWithProgress implements DataProvider, CopyProvider {
81 private static final Logger LOG = Logger.getInstance(VcsLogGraphTable.class);
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;
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;
99 @NotNull private final Collection<VcsLogHighlighter> myHighlighters = ContainerUtil.newArrayList();
101 @NotNull private final GraphCellPainter myGraphCellPainter = new SimpleGraphCellPainter(new DefaultColorGenerator()) {
103 protected int getRowHeight() {
104 return VcsLogGraphTable.this.getRowHeight();
108 public VcsLogGraphTable(@NotNull VcsLogUiImpl ui, @NotNull VcsLogData logData, @NotNull VisiblePack initialDataPack) {
109 super(new GraphTableModel(initialDataPack, logData, ui));
110 getEmptyText().setText("Changes log");
114 myGraphCommitCellRenderer = new GraphCommitCellRenderer(logData, myGraphCellPainter, this);
116 myLogData.getProgress().addProgressIndicatorListener(new MyProgressListener(), ui);
118 setDefaultRenderer(VirtualFile.class, new RootCellRenderer(myUi));
119 setDefaultRenderer(GraphCommitCell.class, myGraphCommitCellRenderer);
120 setDefaultRenderer(String.class, new StringCellRenderer());
122 setShowHorizontalLines(false);
123 setIntercellSpacing(JBUI.emptySize());
124 setTableHeader(new InvisibleResizableHeader());
126 MouseAdapter mouseAdapter = new MyMouseAdapter();
127 addMouseMotionListener(mouseAdapter);
128 addMouseListener(mouseAdapter);
130 getSelectionModel().addListSelectionListener(new MyListSelectionListener());
132 PopupHandler.installPopupHandler(this, VcsLogActionPlaces.POPUP_ACTION_GROUP, VcsLogActionPlaces.VCS_LOG_TABLE_PLACE);
133 ScrollingUtil.installActions(this, false);
136 addComponentListener(new ComponentAdapter() {
138 public void componentResized(ComponentEvent e) {
139 updateCommitColumnWidth();
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);
149 for (VcsLogHighlighter highlighter : myHighlighters) {
150 highlighter.update(visiblePack, permGraphChanged);
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);
166 return myColumnsSizeInitialized;
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);
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();
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;
191 int min = Math.min(maxWidth + UIUtil.DEFAULT_HGAP, MAX_DEFAULT_AUTHOR_COLUMN_WIDTH);
192 column.setPreferredWidth(min);
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);
200 updateCommitColumnWidth();
202 return sizeCalculated;
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();
213 TableColumn commitColumn = getColumnModel().getColumn(GraphTableModel.COMMIT_COLUMN);
214 commitColumn.setPreferredWidth(size);
217 private void setRootColumnSize(@NotNull TableColumn column) {
219 if (!myUi.isMultipleRoots()) {
222 else if (!myUi.isShowRootNames()) {
223 rootWidth = ROOT_INDICATOR_WIDTH;
226 rootWidth = Math.min(calculateMaxRootWidth(), ROOT_NAME_MAX_WIDTH);
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);
235 private int calculateMaxRootWidth() {
237 for (VirtualFile file : myLogData.getRoots()) {
238 Font tableFont = UIManager.getFont("Table.font");
239 width = Math.max(getFontMetrics(tableFont).stringWidth(file.getName() + " "), width);
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) {
251 if (column == GraphTableModel.ROOT_COLUMN) {
252 Object at = getValueAt(row, column);
253 if (at instanceof VirtualFile) {
255 ((VirtualFile)at).getPresentableUrl() +
256 "</b><br/>Click to " +
257 (myUi.isShowRootNames() ? "collapse" : "expand") +
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));
274 public Object getData(@NonNls String dataId) {
275 if (PlatformDataKeys.COPY_PROVIDER.is(dataId)) {
282 public void performCopy(@NotNull DataContext dataContext) {
283 VcsLog log = VcsLogDataKeys.VCS_LOG.getData(dataContext);
285 List<VcsFullCommitDetails> details = VcsLogUtil.collectFirstPackOfLoadedSelectedDetails(log);
286 if (!details.isEmpty()) {
287 CopyPasteManager.getInstance()
288 .setContents(new StringSelection(StringUtil.join(details, VcsShortCommitDetails::getSubject, "\n")));
294 public boolean isCopyEnabled(@NotNull DataContext dataContext) {
295 return getSelectedRowCount() > 0;
299 public boolean isCopyVisible(@NotNull DataContext dataContext) {
303 public void addHighlighter(@NotNull VcsLogHighlighter highlighter) {
304 myHighlighters.add(highlighter);
307 public void removeHighlighter(@NotNull VcsLogHighlighter highlighter) {
308 myHighlighters.remove(highlighter);
311 public void removeAllHighlighters() {
312 myHighlighters.clear();
315 public SimpleTextAttributes applyHighlighters(@NotNull Component rendererComponent,
320 final boolean selected) {
321 VcsLogHighlighter.VcsCommitStyle style = getStyle(row, column, text, hasFocus, selected);
323 assert style.getBackground() != null && style.getForeground() != null && style.getTextStyle() != null;
325 rendererComponent.setBackground(style.getBackground());
326 rendererComponent.setForeground(style.getForeground());
328 switch (style.getTextStyle()) {
330 return SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
332 return SimpleTextAttributes.REGULAR_ITALIC_ATTRIBUTES;
335 return SimpleTextAttributes.REGULAR_ATTRIBUTES;
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);
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);
348 RowInfo<Integer> rowInfo = visibleGraph.getRowInfo(row);
350 VcsLogHighlighter.VcsCommitStyle defaultStyle = VcsCommitStyleFactory
351 .createStyle(rowInfo.getRowType() == RowType.UNMATCHED ? JBColor.GRAY : dummyRendererComponent.getForeground(),
352 dummyRendererComponent.getBackground(), VcsLogHighlighter.TextStyle.NORMAL);
354 final VcsShortCommitDetails details = myLogData.getMiniDetailsGetter().getCommitDataIfAvailable(rowInfo.getCommit());
355 if (details == null || details instanceof LoadingDetails) return defaultStyle;
357 List<VcsLogHighlighter.VcsCommitStyle> styles =
358 ContainerUtil.map(myHighlighters, highlighter -> highlighter.getStyle(details, selected));
359 return VcsCommitStyleFactory.combine(ContainerUtil.append(styles, defaultStyle));
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));
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());
380 public void rootColumnUpdated() {
381 setRootColumnSize(getColumnModel().getColumn(GraphTableModel.ROOT_COLUMN));
382 updateCommitColumnWidth();
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);
395 public static JBColor getRootBackgroundColor(@NotNull VirtualFile root, @NotNull VcsLogColorManager colorManager) {
396 return VcsLogColorManagerImpl.getBackgroundColor(colorManager.getRootColor(root));
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();
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);
412 myUi.repaintUI(); // in case of repaintUI doing something more than just repainting this table in some distant future
414 if (answer == null) {
418 if (answer.getCursorToSet() != null) {
419 setCursor(answer.getCursorToSet());
421 if (answer.getCommitToJump() != null) {
422 Integer row = getModel().getVisiblePack().getVisibleGraph().getVisibleRowIndex(answer.getCommitToJump());
423 if (row != null && row >= 0 && answer.doJump()) {
425 // TODO wait for the full log and then jump
428 if (e != null) showToolTip(getArrowTooltipText(answer.getCommitToJump(), row), e);
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);
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
448 details = myLogData.getMiniDetailsGetter().getCommitData(commit, Collections.singleton(commit)); // preload just the commit
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();
462 balloonText = "Jump to <b>\"" +
463 StringUtil.shortenTextWithEllipsis(details.getSubject(), 50, 0, "...") +
465 VcsUserUtil.getShortPresentation(details.getAuthor()) +
466 CommitPanel.formatDateTime(details.getAuthorTime());
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());
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);
482 public GraphTableModel getModel() {
483 return (GraphTableModel)super.getModel();
487 public Selection getSelection() {
488 if (mySelection == null) mySelection = new Selection(this);
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;
500 public Selection(@NotNull VcsLogGraphTable table) {
502 List<Integer> selectedRows = ContainerUtil.sorted(Ints.asList(myTable.getSelectedRows()));
503 Couple<Integer> visibleRows = ScrollingUtil.getVisibleRows(myTable);
504 myIsOnTop = visibleRows.first - 1 == 0;
506 VisibleGraph<Integer> graph = myTable.getVisibleGraph();
508 mySelectedCommits = new TIntHashSet();
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;
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;
527 myVisibleSelectedCommit = visibleSelectedCommit;
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);
539 myTable.getSelectionModel().setValueIsAdjusting(false);
541 if (scrollToSelection) {
542 if (myIsOnTop && permGraphChanged) { // scroll on top when some fresh commits arrive
545 else if (toSelectAndScroll.second != null) {
546 assert myDelta != null;
547 scrollToRow(toSelectAndScroll.second, myDelta);
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
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));
563 private Pair<TIntHashSet, Integer> findRowsToSelectAndScroll(@NotNull GraphTableModel model,
564 @NotNull VisibleGraph<Integer> visibleGraph) {
565 TIntHashSet rowsToSelect = new TIntHashSet();
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);
574 Integer rowToScroll = null;
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);
582 if (myVisibleSelectedCommit != null && myVisibleSelectedCommit == commit) {
586 return Pair.create(rowsToSelect, rowToScroll);
590 private class MyMouseAdapter extends MouseAdapter {
591 @NotNull private final TableLinkMouseListener myLinkListener = new TableLinkMouseListener();
594 public void mouseClicked(MouseEvent e) {
595 if (myLinkListener.onClick(e, e.getClickCount())) {
599 if (e.getClickCount() == 1 && !expandOrCollapseRoots(e)) {
600 performAction(e, GraphAction.Type.MOUSE_CLICK);
605 public void mouseMoved(MouseEvent e) {
606 if (isAboveLink(e) || isAboveRoots(e)) {
607 setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
609 else if (!(VcsLogGraphTable.this.getCursor() == Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR))) {
610 performAction(e, GraphAction.Type.MOUSE_OVER);
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) {
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);
623 boolean isClickOnGraphElement = actionType == GraphAction.Type.MOUSE_CLICK && printElement != null;
624 if (isClickOnGraphElement) {
625 triggerElementClick(printElement);
628 Selection previousSelection = getSelection();
629 GraphAnswer<Integer> answer =
630 getVisibleGraph().getActionController().performAction(new GraphAction.GraphActionImpl(printElement, actionType));
631 handleAnswer(answer, isClickOnGraphElement, previousSelection, e);
634 private boolean isAboveLink(MouseEvent e) {
635 return myLinkListener.getTagAt(e) != null;
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());
645 public void mouseEntered(MouseEvent e) {
650 public void mouseExited(MouseEvent e) {
655 private static void triggerElementClick(@NotNull PrintElement printElement) {
656 if (printElement instanceof NodePrintElement) {
657 VcsLogUtil.triggerUsage("GraphNodeClick");
659 else if (printElement instanceof EdgePrintElement) {
660 if (((EdgePrintElement)printElement).hasArrow()) {
661 VcsLogUtil.triggerUsage("GraphArrowClick");
667 public VisibleGraph<Integer> getVisibleGraph() {
668 return getModel().getVisiblePack().getVisibleGraph();
672 private Point calcPoint4Graph(@NotNull Point clickPoint) {
673 return new Point(clickPoint.x - getXOffset(), PositionUtil.getYInsideRow(clickPoint, getRowHeight()));
676 private int getXOffset() {
677 TableColumn rootColumn = getColumnModel().getColumn(GraphTableModel.ROOT_COLUMN);
678 return myLogData.isMultiRoot() ? rootColumn.getWidth() : 0;
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;
689 public int getRowHeight() {
690 return myGraphCommitCellRenderer.getPreferredHeight();
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()));
700 int rootWidth = getColumnModel().getColumn(GraphTableModel.ROOT_COLUMN).getWidth();
701 if (!myUi.isShowRootNames()) rootWidth -= ROOT_INDICATOR_WHITE_WIDTH;
703 g.fillRect(x, y, rootWidth, height);
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;
713 RootCellRenderer(@NotNull VcsLogUiImpl ui) {
719 protected void paintComponent(Graphics g) {
720 setFont(UIManager.getFont("Table.font"));
723 int width = getWidth();
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());
731 g.fillRect(0, 0, width, myUi.getTable().getRowHeight());
734 super.paintComponent(g);
738 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
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) {
748 else if (row == 0 || !value.equals(table.getModel().getValueAt(row - 1, column)) || readableRow == row) {
749 text = root.getName();
754 color = getRootBackgroundColor(root, myUi.getColorManager());
758 color = UIUtil.getTableBackground(isSelected);
762 Color background = ((VcsLogGraphTable)table).getStyle(row, column, text, hasFocus, isSelected).getBackground();
763 assert background != null;
764 myBorderColor = background;
765 setForeground(UIUtil.getTableForeground(false));
767 if (myUi.isShowRootNames()) {
780 public void setBackground(Color bg) {
785 private class StringCellRenderer extends ColoredTableCellRenderer {
787 protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
791 append(value.toString(), applyHighlighters(this, row, column, value.toString(), hasFocus, selected));
796 private class MyDummyTableCellEditor implements TableCellEditor {
798 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
803 public Object getCellEditorValue() {
808 public boolean isCellEditable(EventObject anEvent) {
813 public boolean shouldSelectCell(EventObject anEvent) {
814 if (!(anEvent instanceof MouseEvent)) return true;
815 MouseEvent e = (MouseEvent)anEvent;
817 int row = PositionUtil.getRowIndex(e.getPoint(), getRowHeight());
818 if (row > getRowCount() - 1) {
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;
828 public boolean stopCellEditing() {
833 public void cancelCellEditing() {
838 public void addCellEditorListener(CellEditorListener l) {
843 public void removeCellEditorListener(CellEditorListener l) {
848 private class InvisibleResizableHeader extends JBTable.JBTableHeader {
849 @NotNull private final MyBasicTableHeaderUI myHeaderUI;
850 @Nullable private Cursor myCursor = null;
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);
860 public void setTable(JTable table) {
861 JTable oldTable = getTable();
862 if (oldTable != null) {
863 oldTable.removeMouseListener(myHeaderUI);
864 oldTable.removeMouseMotionListener(myHeaderUI);
867 super.setTable(table);
870 table.addMouseListener(myHeaderUI);
871 table.addMouseMotionListener(myHeaderUI);
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
881 JTable table = getTable();
883 table.setCursor(cursor);
887 super.setCursor(cursor);
892 public Cursor getCursor() {
893 if (myCursor == null) {
894 JTable table = getTable();
895 if (table == null) return super.getCursor();
896 return table.getCursor();
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);
910 private static class EmptyTableCellRenderer implements TableCellRenderer {
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));
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();
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());
935 public void mouseClicked(@NotNull MouseEvent e) {
939 public void mousePressed(@NotNull MouseEvent e) {
940 if (isOnBorder(e)) return;
941 mouseInputListener.mousePressed(convertMouseEvent(e));
945 public void mouseReleased(@NotNull MouseEvent e) {
946 if (isOnBorder(e)) return;
947 mouseInputListener.mouseReleased(convertMouseEvent(e));
951 public void mouseEntered(@NotNull MouseEvent e) {
955 public void mouseExited(@NotNull MouseEvent e) {
959 public void mouseDragged(@NotNull MouseEvent e) {
960 if (isOnBorder(e)) return;
961 mouseInputListener.mouseDragged(convertMouseEvent(e));
965 public void mouseMoved(@NotNull MouseEvent e) {
966 if (isOnBorder(e)) return;
967 mouseInputListener.mouseMoved(convertMouseEvent(e));
970 public boolean isOnBorder(@NotNull MouseEvent e) {
971 return Math.abs(header.getTable().getWidth() - e.getPoint().x) <= JBUI.scale(3);
975 private class MyListSelectionListener implements ListSelectionListener {
977 public void valueChanged(ListSelectionEvent e) {
982 private class MyProgressListener implements VcsLogProgress.ProgressListener {
983 @NotNull private String myText = "";
986 public void progressStarted() {
987 myText = getEmptyText().getText();
988 getEmptyText().setText("Loading History...");
992 public void progressStopped() {
993 getEmptyText().setText(myText);