/* * Copyright 2000-2010 JetBrains s.r.o. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.community.intellij.plugins.communitycase.history.wholeTree; import com.intellij.execution.ui.RunnerLayoutUi; import com.intellij.execution.ui.layout.LayoutViewOptions; import com.intellij.execution.ui.layout.PlaceInGrid; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.MultiLineLabelUI; import com.intellij.openapi.ui.Splitter; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.IconLoader; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.AbstractFilterChildren; import com.intellij.openapi.vcs.ComparableComparator; import com.intellij.openapi.vcs.VcsDataKeys; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.committed.CommittedChangesTreeBrowser; import com.intellij.openapi.vcs.changes.committed.RepositoryChangesBrowser; import com.intellij.openapi.vcs.changes.issueLinks.IssueLinkHtmlRenderer; import com.intellij.openapi.vcs.changes.issueLinks.IssueLinkRenderer; import com.intellij.openapi.vcs.changes.issueLinks.TableLinkMouseListener; import com.intellij.openapi.vcs.ui.SearchFieldAction; import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.*; import com.intellij.ui.content.Content; import com.intellij.ui.table.JBTable; import com.intellij.util.Consumer; import com.intellij.util.Icons; import com.intellij.util.containers.Convertor; import com.intellij.util.containers.MultiMap; import com.intellij.util.text.DateFormatUtil; import com.intellij.util.ui.ColumnInfo; import com.intellij.util.ui.UIUtil; import org.community.intellij.plugins.communitycase.history.browser.*; import org.community.intellij.plugins.communitycase.history.browser.Commit; import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; import java.awt.*; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.geom.Rectangle2D; import java.util.*; import java.util.List; /** * @author irengrig */ public class LogUI implements Disposable { private final static Logger LOG = Logger.getInstance("#"+LogUI.class.getName()); public static final SimpleTextAttributes HIGHLIGHT_TEXT_ATTRIBUTES = new SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, new Color(255,128,0)); private final Project myProject; private BigTableTableModel myTableModel; private DetailsCache myDetailsCache; private final Mediator myMediator; private RunnerLayoutUi myUi; private TableScrollChangeListener myMyChangeListener; private List<VirtualFile> myRootsUnderVcs; private final Map<VirtualFile, SymbolicRefs> myRefs; private final SymbolicRefs myRecalculatedCommon; private UIRefresh myUIRefresh; private JBTable myJBTable; private RepositoryChangesBrowser myRepositoryChangesBrowser; private boolean myDataBeingAdded; private boolean myMissingSelectionData; private CardLayout myRepoLayout; private JPanel myRepoPanel; private boolean myStarted; // private JTextField myFilterField; private String myPreviousFilter; private List<String> mySearchContext; private String mySelectedBranch; private BranchSelectorAction myBranchSelectorAction; private MySpecificDetails myDetails; private final DescriptionRenderer myDescriptionRenderer; private FilterAction myFilterAction; private final Speedometer mySelectionSpeedometer; public LogUI(Project project, final Mediator mediator) { myProject = project; myMediator = mediator; myRefs = new HashMap<VirtualFile, SymbolicRefs>(); myRecalculatedCommon = new SymbolicRefs(); myPreviousFilter = ""; myDetails = new MySpecificDetails(myProject); myDescriptionRenderer = new DescriptionRenderer(); mySelectionSpeedometer = new Speedometer(20, 400); createTableModel(); mySearchContext = new ArrayList<String>(); myUIRefresh = new UIRefresh() { @Override public void detailsLoaded() { fireTableRepaint(); } @Override public void linesReloaded() { fireTableRepaint(); } @Override public void acceptException(Exception e) { LOG.info(e); } @Override public void reportSymbolicRefs(VirtualFile root, SymbolicRefs symbolicRefs) { myRefs.put(root, symbolicRefs); myRecalculatedCommon.clear(); if (myRefs.isEmpty()) return; String current = null; boolean same = true; for (SymbolicRefs refs : myRefs.values()) { myRecalculatedCommon.addLocals(refs.getLocalBranches()); myRecalculatedCommon.addRemotes(refs.getRemoteBranches()); myRecalculatedCommon.addTags(refs.getTags()); if (current == null) { current = refs.getCurrent().getFullName(); } else if (! current.equals(refs.getCurrent().getFullName())) { same = false; } } if (same) { myRecalculatedCommon.setCurrent(myRefs.values().iterator().next().getCurrent()); } myBranchSelectorAction.setSymbolicRefs(myRecalculatedCommon); } }; } private void fireTableRepaint() { final TableSelectionKeeper keeper = new TableSelectionKeeper(myJBTable, myTableModel); keeper.put(); myDataBeingAdded = true; myTableModel.fireTableDataChanged(); keeper.restore(); myDataBeingAdded = false; myJBTable.repaint(); } private void start() { myStarted = true; myMyChangeListener.start(); rootsChanged(myRootsUnderVcs); } private static class TableSelectionKeeper { private final List<Pair<Integer, AbstractHash>> myData; private final JBTable myTable; private final BigTableTableModel myModel; private int[] mySelectedRows; private TableSelectionKeeper(final JBTable table, final BigTableTableModel model) { myTable = table; myModel = model; myData = new ArrayList<Pair<Integer,AbstractHash>>(); } public void put() { mySelectedRows = myTable.getSelectedRows(); for (int row : mySelectedRows) { final CommitI commitI = myModel.getCommitAt(row); if (commitI != null) { myData.add(new Pair<Integer, AbstractHash>(commitI.selectRepository(SelectorList.getInstance()), commitI.getHash())); } } } public void restore() { final int rowCount = myModel.getRowCount(); final ListSelectionModel selectionModel = myTable.getSelectionModel(); for (int row : mySelectedRows) { final CommitI commitI = myModel.getCommitAt(row); if (commitI != null) { final Pair<Integer, AbstractHash> pair = new Pair<Integer, AbstractHash>(commitI.selectRepository(SelectorList.getInstance()), commitI.getHash()); if (myData.remove(pair)) { selectionModel.addSelectionInterval(row, row); if (myData.isEmpty()) return; } } } if (myData.isEmpty()) return; for (int i = 0; i < rowCount; i++) { final CommitI commitI = myModel.getCommitAt(i); if (commitI == null) continue; final Pair<Integer, AbstractHash> pair = new Pair<Integer, AbstractHash>(commitI.selectRepository(SelectorList.getInstance()), commitI.getHash()); if (myData.remove(pair)) { selectionModel.addSelectionInterval(i, i); if (myData.isEmpty()) break; } } } } @Override public void dispose() { } private static class SelectorList extends AbstractList<Integer> { private final static SelectorList ourInstance = new SelectorList(); public static SelectorList getInstance() { return ourInstance; } @Override public Integer get(int index) { return index; } @Override public int size() { return Integer.MAX_VALUE; } } public UIRefresh getUIRefresh() { return myUIRefresh; } public void createMe() { // todo think of disposable parent myUi = RunnerLayoutUi.Factory.getInstance(myProject).create(" log", " log", "", this); //myUi.getDefaults().initTabDefaults(0, " log", null); myUi.getDefaults().initFocusContent("log1120", LayoutViewOptions.STARTUP); //myUi.getOptions().setTopToolbar(group, " log"); myUi.getOptions().setMoveToGridActionEnabled(true); myUi.getOptions().setMinimizeActionEnabled(false); final JPanel wrapper = createMainTable(); final Content content = myUi.createContent("log1120", wrapper, "Commits list", null, null); myUi.addContent(content, 0, PlaceInGrid.center, false); content.setCloseable(false); content.setPinned(true); final JComponent component = createRepositoryBrowserDetails(); final Content repoContent = myUi.createContent("Commit details11210", component, "Changed files", Icons.FILE_ICON, null); myUi.addContent(repoContent, 0, PlaceInGrid.right, false); repoContent.setCloseable(false); repoContent.setPinned(true); Disposer.register(content, new Disposable() { @Override public void dispose() { if (myMyChangeListener != null) { myMyChangeListener.stop(); } } }); /*final JComponent specificDetails = myDetails.create(); final Content specificDetailsContent = myUi.createContent("Specific0", specificDetails, "Details", Icons.UNSELECT_ALL_ICON, null); myUi.addContent(specificDetailsContent, 0, PlaceInGrid.bottom, false); repoContent.setCloseable(false); repoContent.setPinned(true); myUi.getDefaults().initTabDefaults(0, " log", null);*/ // todo should look like it, but the behaviour of search differs /*new TableSpeedSearch(myJBTable, new Convertor<Object, String>() { @Override public String convert(Object o) { if (o instanceof CommitI) { return ((CommitI) o).getDecorationString(); } return o == null ? null : o.toString(); } });*/ //myUi.getDefaults().initTabDefaults(0, " log", ); /* myUi = RunnerLayoutUi.Factory.getInstance(project).create("Debug", "unknown!", sessionName, this); myUi.getDefaults().initTabDefaults(0, "Debug", null); myUi.getOptions().setTopToolbar(createTopToolbar(), ActionPlaces.DEBUGGER_TOOLBAR); */ } private JComponent createRepositoryBrowserDetails() { myRepoLayout = new CardLayout(); myRepoPanel = new JPanel(myRepoLayout); myRepositoryChangesBrowser = new RepositoryChangesBrowser(myProject, Collections.<CommittedChangeList>emptyList(), Collections.<Change>emptyList(), null); myRepositoryChangesBrowser.getDiffAction().registerCustomShortcutSet(CommonShortcuts.getDiff(), myJBTable); myJBTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { if (! myDataBeingAdded) { selectionChanged(); } } }); myRepoPanel.add("main", myRepositoryChangesBrowser); // todo loading circle myRepoPanel.add("loading", panelWithCenteredText("Loading...")); myRepoPanel.add("tooMuch", panelWithCenteredText("Too many rows selected")); myRepoPanel.add("empty", panelWithCenteredText("Nothing selected")); myRepoLayout.show(myRepoPanel, "empty"); return myRepoPanel; } private void selectionChanged() { mySelectionSpeedometer.event(); final int[] rows = myJBTable.getSelectedRows(); myDetails.refresh(null, null); if (rows.length == 0) { myRepoLayout.show(myRepoPanel, "empty"); myRepoPanel.repaint(); myMissingSelectionData = false; return; } else if (rows.length >= 10) { myRepoLayout.show(myRepoPanel, "tooMuch"); myRepoPanel.repaint(); myMissingSelectionData = false; return; } myRepoLayout.show(myRepoPanel, "loading"); myRepoPanel.repaint(); updateDetailsFromSelection(); } private static JPanel panelWithCenteredText(final String text) { final JPanel jPanel = new JPanel(new BorderLayout()); jPanel.setBackground(UIUtil.getTableBackground()); final JLabel label = new JLabel(text, JLabel.CENTER); label.setUI(new MultiLineLabelUI()); jPanel.add(label, BorderLayout.CENTER); return jPanel; } public void updateSelection() { updateBranchesFor(); if (! myMissingSelectionData) return; updateDetailsFromSelection(); } private void updateBranchesFor() { final int[] rows = myJBTable.getSelectedRows(); if (rows.length == 1 && myDetails.isMissingBranchesInfo() && mySelectionSpeedometer.getSpeed() < 0.1) { final CommitI commit = myTableModel.getCommitAt(rows[0]); final VirtualFile root = commit.selectRepository(myRootsUnderVcs); if (commit.holdsDecoration()) return; final Commit Commit = myDetailsCache.convert(root, commit.getHash()); if (Commit == null) return; final List<String> branches = myDetailsCache.getBranches(root, commit.getHash()); if (branches != null) { myDetails.putBranches(Commit, branches); } final Application application = ApplicationManager.getApplication(); application.executeOnPooledThread(new Runnable() { @Override public void run() { final CommitI commitI = myTableModel.getCommitAt(rows[0]); if (!commit.equals(commitI)) return; try { final List<String> branches = new LowLevelAccessImpl(myProject, root).getBranchesWithCommit(Commit.getHash()); myDetailsCache.putBranches(root, commit.getHash(), branches); application.invokeLater(new Runnable() { @Override public void run() { final int[] afterRows = myJBTable.getSelectedRows(); if (myDetails.isMissingBranchesInfo() && afterRows.length == 1 && afterRows[0] == rows[0]) { final CommitI afterCommit = myTableModel.getCommitAt(rows[0]); if (afterCommit.holdsDecoration() || (! afterCommit.equals(commit))) return; myDetails.putBranches(Commit, branches); } } }, myProject.getDisposed()); if (!branches.isEmpty()) { } } catch (VcsException e) { LOG.info(e); } } }); } } private void updateDetailsFromSelection() { if (myDataBeingAdded) return; myMissingSelectionData = false; final int[] rows = myJBTable.getSelectedRows(); if (rows.length == 1) { final CommitI commitI = myTableModel.getCommitAt(rows[0]); if (commitI != null && (! commitI.holdsDecoration())) { final VirtualFile root = commitI.selectRepository(myRootsUnderVcs); final Commit convert = myDetailsCache.convert(root, commitI.getHash()); if (convert != null) { myDetails.refresh(root, convert); } } } final List<Commit> commits = new ArrayList<Commit>(); final MultiMap<VirtualFile,AbstractHash> missingHashes = new MultiMap<VirtualFile, AbstractHash>(); for (int i = rows.length - 1; i >= 0; --i) { final int row = rows[i]; final CommitI commitI = myTableModel.getCommitAt(row); if (commitI == null || commitI.holdsDecoration()) continue; VirtualFile root = commitI.selectRepository(myRootsUnderVcs); AbstractHash hash = commitI.getHash(); final Commit details = myDetailsCache.convert(root, hash); commits.add(details); myMissingSelectionData |= details == null; missingHashes.putValue(root, hash); } if (myMissingSelectionData) { myDetailsCache.acceptQuestion(missingHashes); return; } final List<Change> changes = new ArrayList<Change>(); for (Commit commit : commits) { changes.addAll(commit.getChanges()); } final List<Change> zipped = CommittedChangesTreeBrowser.zipChanges(changes); myRepositoryChangesBrowser.setChangesToDisplay(zipped); myRepoLayout.show(myRepoPanel, "main"); myRepoPanel.repaint(); } private JPanel createMainTable() { final DefaultActionGroup group = new DefaultActionGroup(); myBranchSelectorAction = new BranchSelectorAction(myProject, new Consumer<String>() { @Override public void consume(String s) { mySelectedBranch = s; reloadRequest(); } }); myFilterAction = new FilterAction(myProject); group.add(new MyTextFieldAction()); group.add(myBranchSelectorAction); // first create filters... //group.add(myFilterAction); group.add(ActionManager.getInstance().getAction("ChangesView.CreatePatchFromChanges")); group.add(new MyRefreshAction()); final ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(" log", group, true); myJBTable = new JBTable(myTableModel) { @Override public TableCellRenderer getCellRenderer(int row, int column) { final TableCellRenderer custom = myTableModel.getColumnInfo(column).getRenderer(myTableModel.getValueAt(row, column)); return custom == null ? super.getCellRenderer(row, column) : custom; } }; final TableLinkMouseListener tableLinkListener = new TableLinkMouseListener() { @Override protected Object tryGetTag(MouseEvent e, JTable table, int row, int column) { myDescriptionRenderer.getTableCellRendererComponent(table, table.getValueAt(row, column), false, false, row, column); final Rectangle rc = table.getCellRect(row, column, false); int index = myDescriptionRenderer.myInner.findFragmentAt(e.getPoint().x - rc.x - myDescriptionRenderer.getCurrentWidth()); if (index >= 0) { return myDescriptionRenderer.myInner.getFragmentTag(index); } return null; } }; tableLinkListener.install(myJBTable); myJBTable.getExpandableItemsHandler().setEnabled(false); //myJBTable.setTableHeader(null); myJBTable.setShowGrid(false); myJBTable.setModel(myTableModel); final JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myJBTable); final ComponentListener listener = new ComponentListener() { @Override public void componentResized(ComponentEvent e) { if (myStarted) { if (adjustColumnSizes(scrollPane)) { myJBTable.removeComponentListener(this); } } } @Override public void componentMoved(ComponentEvent e) { } @Override public void componentShown(ComponentEvent e) { if (myStarted) { if (adjustColumnSizes(scrollPane)) { myJBTable.removeComponentListener(this); } } } @Override public void componentHidden(ComponentEvent e) { } }; myJBTable.addComponentListener(listener); myMyChangeListener = new TableScrollChangeListener(myJBTable, myDetailsCache, myTableModel, new Runnable() { @Override public void run() { updateSelection(); } }); scrollPane.getViewport().addChangeListener(myMyChangeListener); final JPanel wrapper = new DataProviderPanel(new BorderLayout()); wrapper.add(actionToolbar.getComponent(), BorderLayout.NORTH); wrapper.add(scrollPane, BorderLayout.CENTER); final JComponent specificDetails = myDetails.create(); final Splitter splitter = new Splitter(true, 0.6f); splitter.setFirstComponent(wrapper); splitter.setSecondComponent(specificDetails); splitter.setDividerWidth(4); return splitter; } private class DataProviderPanel extends JPanel implements TypeSafeDataProvider { private DataProviderPanel(LayoutManager layout) { super(layout); } @Override public void calcData(DataKey key, DataSink sink) { if (VcsDataKeys.CHANGES.equals(key)) { final int[] rows = myJBTable.getSelectedRows(); if (rows.length != 1) return; final List<Change> changes = new ArrayList<Change>(); for (int row : rows) { final CommitI commitAt = myTableModel.getCommitAt(row); if (commitAt == null) return; final Commit Commit = myDetailsCache.convert(commitAt.selectRepository(myRootsUnderVcs), commitAt.getHash()); if (Commit == null) return; changes.addAll(Commit.getChanges()); } sink.put(key, changes.toArray(new Change[changes.size()])); } else if (VcsDataKeys.PRESET_COMMIT_MESSAGE.equals(key)) { final int[] rows = myJBTable.getSelectedRows(); if (rows.length != 1) return; final CommitI commitAt = myTableModel.getCommitAt(rows[0]); if (commitAt == null) return; final Commit Commit = myDetailsCache.convert(commitAt.selectRepository(myRootsUnderVcs), commitAt.getHash()); if (Commit == null) return; sink.put(key, Commit.getDescription()); } } } private boolean adjustColumnSizes(JScrollPane scrollPane) { if (myJBTable.getWidth() <= 0) return false; //myJBTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); final TableColumnModel columnModel = myJBTable.getColumnModel(); final FontMetrics metrics = myJBTable.getFontMetrics(myJBTable.getFont()); final int dateWidth = metrics.stringWidth("Yesterday 00:00:00 " + scrollPane.getVerticalScrollBar().getWidth()) + columnModel.getColumnMargin(); final int nameWidth = metrics.stringWidth("Somelong W. UsernameToDisplay"); int widthWas = 0; for (int i = 0; i < columnModel.getColumnCount(); i++) { widthWas += columnModel.getColumn(i).getWidth(); } columnModel.getColumn(1).setWidth(nameWidth); columnModel.getColumn(1).setPreferredWidth(nameWidth); columnModel.getColumn(2).setWidth(dateWidth); columnModel.getColumn(2).setPreferredWidth(dateWidth); final int nullWidth = widthWas - dateWidth - nameWidth - columnModel.getColumnMargin() * 3; columnModel.getColumn(0).setWidth(nullWidth); columnModel.getColumn(0).setPreferredWidth(nullWidth); return true; } private Pair<String, List<String>> preparse(String previousFilter) { final String[] strings = previousFilter.split("[\\s]"); StringBuilder sb = new StringBuilder(); mySearchContext.clear(); final List<String> words = new ArrayList<String>(); for (String string : strings) { mySearchContext.add(string.toLowerCase()); final String word = StringUtil.escapeToRegexp(string); sb.append(word).append(".*"); words.add(word); } new SubstringsFilter().doFilter(mySearchContext); return new Pair<String, List<String>>(sb.toString(), words); } public static class SubstringsFilter extends AbstractFilterChildren<String> { @Override protected boolean isAncestor(String parent, String child) { return parent.startsWith(child); } @Override protected void sortAscending(List<String> strings) { Collections.sort(strings, new ComparableComparator.Descending<String>()); } } public JComponent getPanel() { return myUi.getComponent(); } public void rootsChanged(List<VirtualFile> rootsUnderVcs) { myRootsUnderVcs = rootsUnderVcs; final RootsHolder rootsHolder = new RootsHolder(rootsUnderVcs); myTableModel.setRootsHolder(rootsHolder); myDetailsCache.rootsChanged(rootsUnderVcs); if (myStarted) { reloadRequest(); } } public UIRefresh getRefreshObject() { return myUIRefresh; } public BigTableTableModel getTableModel() { return myTableModel; } private void createTableModel() { myTableModel = new BigTableTableModel(columns(), new Runnable() { @Override public void run() { start(); } }); } List<ColumnInfo> columns() { return Arrays.asList((ColumnInfo)COMMENT, AUTHOR, DATE); } private final ColumnInfo<Object, Object> COMMENT = new ColumnInfo<Object, Object>("Comment") { private final TableCellRenderer mySimpleRenderer = new SimpleRenderer(SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES, true); @Override public Object valueOf(Object o) { if (o instanceof Commit) { return o; } if (BigTableTableModel.LOADING == o) return o; return o == null ? "" : o.toString(); } @Override public TableCellRenderer getRenderer(Object o) { return o instanceof Commit ? myDescriptionRenderer : mySimpleRenderer; } }; private abstract class HighlightingRendererBase { protected abstract void usual(final String s); protected abstract void highlight(final String s); void tryHighlight(String text) { final String lower = text.toLowerCase(); int idxFrom = 0; while (idxFrom < text.length()) { boolean adjusted = false; int adjustedIdx = text.length() + 1; int adjLen = 0; for (String word : mySearchContext) { final int next = lower.indexOf(word, idxFrom); if ((next != -1) && (adjustedIdx > next)) { adjustedIdx = next; adjLen = word.length(); adjusted = true; } } if (adjusted) { if (idxFrom != adjustedIdx) { usual(text.substring(idxFrom, adjustedIdx)); } idxFrom = adjustedIdx + adjLen; highlight(text.substring(adjustedIdx, idxFrom)); continue; } usual(text.substring(idxFrom)); return; } } } private class HighLightingRenderer extends ColoredTableCellRenderer { private final SimpleTextAttributes myHighlightAttributes; private final SimpleTextAttributes myUsualAttributes; protected final HighlightingRendererBase myWorker; public HighLightingRenderer(SimpleTextAttributes highlightAttributes, SimpleTextAttributes usualAttributes) { myHighlightAttributes = highlightAttributes; myUsualAttributes = usualAttributes == null ? SimpleTextAttributes.REGULAR_ATTRIBUTES : usualAttributes; myWorker = new HighlightingRendererBase() { @Override protected void usual(String s) { append(s, myUsualAttributes); } @Override protected void highlight(String s) { append(s, myHighlightAttributes); } }; } @Override protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) { setBackground(getLogicBackground(selected, row)); if (BigTableTableModel.LOADING == value) { return; } final String text = value.toString(); if (mySearchContext.isEmpty()) { append(text, myUsualAttributes); return; } myWorker.tryHighlight(text); } } private static class PositionAndBorderContainer implements TableCellRenderer { private final JPanel myPanel; private final ColoredTableCellRenderer myDelegate; private final DottedBorder myDottedBorder; public PositionAndBorderContainer(ColoredTableCellRenderer delegate) { myDelegate = delegate; myPanel = new JPanel(new BorderLayout()); myPanel.setBackground(UIUtil.getTableBackground()); myDottedBorder = new DottedBorder(UIUtil.getFocusedBoundsColor()); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { myPanel.removeAll(); final Component component = myDelegate.getTableCellRendererComponent(table, value, isSelected, false, row, column); myPanel.add(component, BorderLayout.SOUTH); if (hasFocus) { myDelegate.setBorder(myDottedBorder); } else { myDelegate.setBorder(null); } return myPanel; } } private class SimpleRenderer extends ColoredTableCellRenderer { private final SimpleTextAttributes myAtt; private final boolean myShowLoading; public SimpleRenderer(SimpleTextAttributes att, boolean showLoading) { myAtt = att; myShowLoading = showLoading; } @Override protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) { setBackground(getLogicBackground(selected, row)); if (BigTableTableModel.LOADING == value) { if (myShowLoading) { append("Loading..."); } return; } append(value.toString(), myAtt); } } private class DescriptionRenderer implements TableCellRenderer { private final Map<String, Icon> myTagMap; private final Map<String, Icon> myBranchMap; private final JPanel myPanel; private final Inner myInner; private int myCurrentWidth; private DescriptionRenderer() { myInner = new Inner(); myTagMap = new HashMap<String, Icon>(); myBranchMap = new HashMap<String, Icon>(); myPanel = new JPanel(); final BoxLayout layout = new BoxLayout(myPanel, BoxLayout.X_AXIS); myPanel.setLayout(layout); myCurrentWidth = 0; } public void resetIcons() { myBranchMap.clear(); myTagMap.clear(); } public int getCurrentWidth() { return myCurrentWidth; } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { myCurrentWidth = 0; if (value instanceof Commit) { final Commit commit = (Commit)value; final int localSize = commit.getLocalBranches() == null ? 0 : commit.getLocalBranches().size(); final int remoteSize = commit.getRemoteBranches() == null ? 0 : commit.getRemoteBranches().size(); final int tagsSize = commit.getTags().size(); if (localSize + remoteSize > 0) { final String branch = localSize == 0 ? (commit.getRemoteBranches().get(0)) : commit.getLocalBranches().get(0); Icon icon = myBranchMap.get(branch); if (icon == null) { final boolean plus = localSize + remoteSize + tagsSize > 1; final Color color = localSize == 0 ? Colors.remote : Colors.local; icon = new CaptionIcon(color, table.getFont().deriveFont((float) table.getFont().getSize() - 1), branch, table, CaptionIcon.Form.SQUARE, plus, branch.equals(commit.getCurrentBranch())); myBranchMap.put(branch, icon); } myCurrentWidth = icon.getIconWidth(); myPanel.removeAll(); myPanel.setBackground(getLogicBackground(isSelected, row)); myPanel.add(new JLabel(icon)); myInner.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); myPanel.add(myInner); return myPanel; } if ((localSize + remoteSize == 0) && (tagsSize > 0)) { final String tag = commit.getTags().get(0); Icon icon = myTagMap.get(tag); if (icon == null) { icon = new CaptionIcon(Colors.tag, table.getFont().deriveFont((float) table.getFont().getSize() - 1), tag, table, CaptionIcon.Form.ROUNDED, tagsSize > 1, false); myTagMap.put(tag, icon); } myCurrentWidth = icon.getIconWidth(); myPanel.removeAll(); myPanel.setBackground(getLogicBackground(isSelected, row)); myPanel.add(new JLabel(icon)); myInner.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); myPanel.add(myInner); return myPanel; } } myInner.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); return myInner; } private class Inner extends HighLightingRenderer { private final IssueLinkRenderer myIssueLinkRenderer; private final Consumer<String> myConsumer; private Inner() { super(HIGHLIGHT_TEXT_ATTRIBUTES, null); myIssueLinkRenderer = new IssueLinkRenderer(myProject, this); myConsumer = new Consumer<String>() { @Override public void consume(String s) { myWorker.tryHighlight(s); } }; } @Override protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) { setBackground(getLogicBackground(selected, row)); if (value instanceof Commit) { final Commit Commit = (Commit)value; myIssueLinkRenderer.appendTextWithLinks(Commit.getDescription(), SimpleTextAttributes.REGULAR_ATTRIBUTES, myConsumer); //super.customizeCellRenderer(table, ((Commit) value).getDescription(), selected, hasFocus, row, column); } else { super.customizeCellRenderer(table, value, selected, hasFocus, row, column); } } } } private Color getLogicBackground(final boolean isSelected, final int row) { final Color bkgColor; final CommitI commitAt = myTableModel.getCommitAt(row); Commit Commit = null; if (commitAt != null & (! commitAt.holdsDecoration())) { Commit = myDetailsCache.convert(commitAt.selectRepository(myRootsUnderVcs), commitAt.getHash()); } if (isSelected) { bkgColor = UIUtil.getTableSelectionBackground(); } else { if (Commit != null && Commit.isOnLocal() && Commit.isOnTracked()) { bkgColor = Colors.commonThisBranch; } else if (Commit != null && Commit.isOnLocal()) { bkgColor = Colors.ownThisBranch; } else { bkgColor = UIUtil.getTableBackground(); } } return bkgColor; } private class MyTestDescriptionRenderer implements TableCellRenderer { private JPanel myJPanel; private final ColoredTableCellRenderer myHeaderRenderer; private final ColoredTableCellRenderer myInnerRenderer; private int myHeight; private final DottedBorder myDottedBorder; private int myLeading; private MyTestDescriptionRenderer() { myJPanel = new JPanel(new BorderLayout()); myDottedBorder = new DottedBorder(UIUtil.getFocusedBoundsColor()); myJPanel.setBackground(UIUtil.getTableBackground()); myHeaderRenderer = new SimpleRenderer(SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES, false); myInnerRenderer = new HighLightingRenderer(HIGHLIGHT_TEXT_ATTRIBUTES, null); myInnerRenderer.setBorder(null); myHeight = -1; } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (myHeight == -1) { final Font font = myJBTable.getFont(); final FontMetrics fm = myJBTable.getFontMetrics(font); final Graphics g = myJBTable.getGraphics(); final Rectangle2D bounds = fm.getStringBounds("AWQ", g); final FontMetrics metrics = g.getFontMetrics(); myLeading = metrics.getLeading(); myHeight = (int) (metrics.getHeight() * 1.3); } if (! (value instanceof Commit)) { myInnerRenderer.getTableCellRendererComponent(table, value.toString(), isSelected, hasFocus, row, column); table.setRowHeight(row, myHeight); return myInnerRenderer; } final Commit Commit = (Commit) value; final CommitI commitAt = myTableModel.getCommitAt(row); if (commitAt != null && commitAt.holdsDecoration()) { myJPanel.removeAll(); myJPanel.add(myHeaderRenderer, BorderLayout.NORTH); myJPanel.add(myInnerRenderer, BorderLayout.CENTER); myInnerRenderer.getTableCellRendererComponent(table, Commit.getDescription(), isSelected, false, row, column); myHeaderRenderer.getTableCellRendererComponent(table, commitAt.getDecorationString(), false, false, row, column); /*if (hasFocus) { myJPanel.setBorder(myDottedBorder); } else { myJPanel.setBorder(null); }*/ if (hasFocus) { myInnerRenderer.setBorder(myDottedBorder); } else { myInnerRenderer.setBorder(null); } table.setRowHeight(row, (int) (2 * myHeight)); return myJPanel; } else { myInnerRenderer.getTableCellRendererComponent(table, Commit.getDescription(), isSelected, false, row, column); if (hasFocus) { myInnerRenderer.setBorder(myDottedBorder); } else { myInnerRenderer.setBorder(null); } table.setRowHeight(row, myHeight); return myInnerRenderer; } } } private final ColumnInfo<Object, String> AUTHOR = new ColumnInfo<Object, String>("Author") { private final TableCellRenderer myRenderer = new HighLightingRenderer(HIGHLIGHT_TEXT_ATTRIBUTES, SimpleTextAttributes.REGULAR_ATTRIBUTES); @Override public String valueOf(Object o) { if (o instanceof Commit) { return ((Commit) o).getAuthor(); } return ""; } @Override public TableCellRenderer getRenderer(Object o) { return myRenderer; } }; private final ColumnInfo<Object, String> DATE = new ColumnInfo<Object, String>("Date") { private final TableCellRenderer myRenderer = new SimpleRenderer(SimpleTextAttributes.REGULAR_ATTRIBUTES, false); @Override public String valueOf(Object o) { if (o instanceof Commit) { return DateFormatUtil.formatPrettyDateTime(((Commit)o).getDate()); } return ""; } @Override public TableCellRenderer getRenderer(Object o) { return myRenderer; } }; public void setDetailsCache(DetailsCache detailsCache) { myDetailsCache = detailsCache; } private class MyRefreshAction extends DumbAwareAction { private MyRefreshAction() { super("Refresh", "Refresh", IconLoader.getIcon("/actions/sync.png")); } @Override public void actionPerformed(AnActionEvent e) { rootsChanged(myRootsUnderVcs); } } private class MyTextFieldAction extends SearchFieldAction { private MyTextFieldAction() { super("Find:"); } @Override public void actionPerformed(AnActionEvent e) { checkIfFilterChanged(); } private void checkIfFilterChanged() { final String newValue = getText().trim(); if (! Comparing.equal(myPreviousFilter, newValue)) { myPreviousFilter = newValue; reloadRequest(); } } } private void reloadRequest() { final int was = myTableModel.getRowCount(); final Collection<String> startingPoints = mySelectedBranch == null ? Collections.<String>emptyList() : Collections.singletonList(mySelectedBranch); myDescriptionRenderer.resetIcons(); if (StringUtil.isEmptyOrSpaces(myPreviousFilter)) { mySearchContext.clear(); myMediator.reload(new RootsHolder(myRootsUnderVcs), startingPoints, Collections.<ChangesFilter.Filter>emptyList(), null); } else { final List<ChangesFilter.Filter> filters = new ArrayList<ChangesFilter.Filter>(); final Pair<String, List<String>> preparse = preparse(myPreviousFilter); filters.add(new ChangesFilter.Comment(preparse.getFirst())); for (String s : preparse.getSecond()) { filters.add(new ChangesFilter.Author(s)); filters.add(new ChangesFilter.Committer(s)); } myMediator.reload(new RootsHolder(myRootsUnderVcs), startingPoints, filters, myPreviousFilter.split("[\\s]")); } selectionChanged(); fireTableRepaint(); myTableModel.fireTableRowsDeleted(0, was); } private interface Colors { Color tag = new Color(241, 239, 158); Color remote = new Color(188,188,252); Color local = new Color(117,238,199); Color ownThisBranch = new Color(198,255,226); Color commonThisBranch = new Color(223,223,255); } private class MySpecificDetails { private JEditorPane myJEditorPane; private JPanel myMarksPanel; private BoxLayout myBoxLayout; private final Project myProject; private boolean myMissingBranchesInfo; private MySpecificDetails(final Project project) { myProject = project; } public JComponent create() { myJEditorPane = new JEditorPane(UIUtil.HTML_MIME, ""); myJEditorPane.setPreferredSize(new Dimension(150, 100)); myJEditorPane.setEditable(false); myJEditorPane.setBackground(UIUtil.getComboBoxDisabledBackground()); myJEditorPane.addHyperlinkListener(new BrowserHyperlinkListener()); myMarksPanel = new JPanel(); myBoxLayout = new BoxLayout(myMarksPanel, BoxLayout.X_AXIS); myMarksPanel.setLayout(myBoxLayout); final JPanel wrapper = new JPanel(new BorderLayout()); wrapper.add(myMarksPanel, BorderLayout.NORTH); wrapper.add(myJEditorPane, BorderLayout.CENTER); final Color color = UIUtil.getTableBackground(); myJEditorPane.setBackground(color); wrapper.setBackground(color); myMarksPanel.setBackground(color); final JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(wrapper); return scrollPane; } public void putBranches(final Commit commit, final List<String> branches) { myMissingBranchesInfo = false; if (! branches.isEmpty()) { myJEditorPane.setText(parseDetails(commit, branches)); } } public boolean isMissingBranchesInfo() { return myMissingBranchesInfo; } public void refresh(VirtualFile root, final Commit commit) { if (commit == null) { myJEditorPane.setText(""); myMarksPanel.removeAll(); myMissingBranchesInfo = false; } else { final List<String> branches = myDetailsCache.getBranches(root, commit.getShortHash()); myMissingBranchesInfo = branches == null; final Font font = myJEditorPane.getFont().deriveFont((float) (myJEditorPane.getFont().getSize() - 1)); final String currentBranch = commit.getCurrentBranch(); myMarksPanel.removeAll(); for (String s : commit.getLocalBranches()) { myMarksPanel.add(new JLabel(new CaptionIcon(Colors.local, font, s, myMarksPanel, CaptionIcon.Form.SQUARE, false, s.equals(currentBranch)))); } for (String s : commit.getRemoteBranches()) { myMarksPanel.add(new JLabel(new CaptionIcon(Colors.remote, font, s, myMarksPanel, CaptionIcon.Form.SQUARE, false, s.equals(currentBranch)))); } for (String s : commit.getTags()) { myMarksPanel.add(new JLabel(new CaptionIcon(Colors.tag, font, s, myMarksPanel, CaptionIcon.Form.ROUNDED, false, s.equals(currentBranch)))); } myMarksPanel.repaint(); myJEditorPane.setText(parseDetails(commit, branches)); } } private String parseDetails(final Commit c, final List<String> branches) { final String hash = new HtmlHighlighter(c.getHash().getValue()).getResult(); final String author = new HtmlHighlighter(c.getAuthor()).getResult(); final String committer = new HtmlHighlighter(c.getCommitter()).getResult(); final String comment = IssueLinkHtmlRenderer.formatTextWithLinks(myProject, c.getDescription(), new Convertor<String, String>() { @Override public String convert(String o) { return new HtmlHighlighter(o).getResult(); } }); final StringBuilder sb = new StringBuilder().append("<html><head>").append(UIUtil.getCssFontDeclaration(UIUtil.getLabelFont())) .append("</head><body><table><tr valign=\"top\"><td><i>Hash:</i></td><td>").append( hash).append("</td></tr>" + "<tr valign=\"top\"><td><i>Author:</i></td><td>") .append(author).append(" (").append(c.getAuthorEmail()).append(") <i>at</i> ") .append(DateFormatUtil.formatPrettyDateTime(c.getAuthorTime())) .append("</td></tr>" + "<tr valign=\"top\"><td><i>Commiter:</i></td><td>") .append(committer).append(" (").append(c.getComitterEmail()).append(") <i>at</i> ") .append(DateFormatUtil.formatPrettyDateTime(c.getDate())).append( "</td></tr>" + "<tr valign=\"top\"><td><i>Description:</i></td><td><b>") .append(comment).append("</b></td></tr>"); sb.append("<tr valign=\"top\"><td><i>Contained in branches:<i></td><td>"); if (branches != null && (! branches.isEmpty())) { for (int i = 0; i < branches.size(); i++) { String s = branches.get(i); sb.append(s); if (i + 1 < branches.size()) { sb.append(", "); } } } else { sb.append("<font color=gray>Loading...</font>"); } sb.append("</td></tr>"); sb.append("</table></body></html>"); return sb.toString(); } } private class HtmlHighlighter extends HighlightingRendererBase { private final String myText; private final StringBuilder mySb; private HtmlHighlighter(String text) { myText = text; mySb = new StringBuilder(); } @Override protected void highlight(String s) { mySb.append("<font color=rgb(255,128,0)>").append(s).append("</font>"); } @Override protected void usual(String s) { mySb.append(s); } public String getResult() { tryHighlight(myText); return mySb.toString(); } } }