package com.intellij.vcs.log.ui.frame; import com.google.common.primitives.Ints; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.progress.util.ProgressWindow; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Splitter; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.vcs.VcsDataKeys; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.TextRevisionNumber; import com.intellij.openapi.vcs.changes.committed.CommittedChangesTreeBrowser; import com.intellij.openapi.vcs.changes.committed.RepositoryChangesBrowser; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.*; import com.intellij.ui.components.JBLoadingPanel; import com.intellij.ui.components.panels.Wrapper; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.table.ComponentsListFocusTraversalPolicy; import com.intellij.vcs.CommittedChangeListForRevision; import com.intellij.vcs.log.*; import com.intellij.vcs.log.data.MainVcsLogUiProperties; import com.intellij.vcs.log.data.VcsLogData; import com.intellij.vcs.log.data.VcsLogProgress; import com.intellij.vcs.log.data.VisiblePack; import com.intellij.vcs.log.impl.VcsLogUtil; import com.intellij.vcs.log.ui.VcsLogActionPlaces; import com.intellij.vcs.log.ui.VcsLogInternalDataKeys; import com.intellij.vcs.log.ui.VcsLogUiImpl; import com.intellij.vcs.log.ui.actions.IntelliSortChooserPopupAction; import com.intellij.vcs.log.ui.filter.VcsLogClassicFilterUi; import com.intellij.vcs.log.util.BekUtil; import com.intellij.vcs.log.util.VcsUserUtil; import net.miginfocom.swing.MigLayout; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.util.*; import java.util.List; import static com.intellij.util.ObjectUtils.assertNotNull; import static com.intellij.util.containers.ContainerUtil.getFirstItem; public class MainFrame extends JPanel implements DataProvider, Disposable { private static final String HELP_ID = "reference.changesToolWindow.log"; @NotNull private final VcsLogData myLogData; @NotNull private final VcsLogUiImpl myUi; @NotNull private final VcsLog myLog; @NotNull private final VcsLogClassicFilterUi myFilterUi; @NotNull private final JBLoadingPanel myChangesLoadingPane; @NotNull private final VcsLogGraphTable myGraphTable; @NotNull private final DetailsPanel myDetailsPanel; @NotNull private final Splitter myDetailsSplitter; @NotNull private final JComponent myToolbar; @NotNull private final RepositoryChangesBrowser myChangesBrowser; @NotNull private final Splitter myChangesBrowserSplitter; @NotNull private final SearchTextField myTextFilter; @NotNull private final MainVcsLogUiProperties myUiProperties; @NotNull private Runnable myContainingBranchesListener; @NotNull private Runnable myMiniDetailsLoadedListener; public MainFrame(@NotNull VcsLogData logData, @NotNull VcsLogUiImpl ui, @NotNull Project project, @NotNull MainVcsLogUiProperties uiProperties, @NotNull VcsLog log, @NotNull VisiblePack initialDataPack) { // collect info myLogData = logData; myUi = ui; myLog = log; myUiProperties = uiProperties; myFilterUi = new VcsLogClassicFilterUi(myUi, logData, myUiProperties, initialDataPack); // initialize components myGraphTable = new VcsLogGraphTable(ui, logData, initialDataPack); myDetailsPanel = new DetailsPanel(logData, ui.getColorManager(), this); myChangesBrowser = new RepositoryChangesBrowser(project, null, Collections.emptyList(), null) { @Override protected void buildToolBar(DefaultActionGroup toolBarGroup) { super.buildToolBar(toolBarGroup); toolBarGroup.add(ActionManager.getInstance().getAction(VcsLogActionPlaces.VCS_LOG_SHOW_DETAILS_ACTION)); } }; myChangesBrowser.getViewerScrollPane().setBorder(IdeBorderFactory.createBorder(SideBorder.TOP)); myChangesBrowser.getDiffAction().registerCustomShortcutSet(myChangesBrowser.getDiffAction().getShortcutSet(), getGraphTable()); myChangesBrowser.getEditSourceAction().registerCustomShortcutSet(CommonShortcuts.getEditSource(), getGraphTable()); myChangesBrowser.getViewer().setEmptyText(""); myChangesLoadingPane = new JBLoadingPanel(new BorderLayout(), this, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS); myChangesLoadingPane.add(myChangesBrowser); myDetailsSplitter = new OnePixelSplitter(true, "vcs.log.details.splitter.proportion", 0.7f); myDetailsSplitter.setFirstComponent(myChangesLoadingPane); setupDetailsSplitter(myUiProperties.get(MainVcsLogUiProperties.SHOW_DETAILS)); myGraphTable.getSelectionModel().addListSelectionListener(new CommitSelectionListenerForDiff()); myDetailsPanel.installCommitSelectionListener(myGraphTable); updateWhenDetailsAreLoaded(); myTextFilter = myFilterUi.createTextFilter(); myToolbar = createActionsToolbar(); ProgressStripe progressStripe = new ProgressStripe(setupScrolledGraph(), this, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS) { @Override public void updateUI() { super.updateUI(); if (myDecorator != null && myLogData.getProgress().isRunning()) startLoadingImmediately(); } }; myLogData.getProgress().addProgressIndicatorListener(new VcsLogProgress.ProgressListener() { @Override public void progressStarted() { progressStripe.startLoading(); } @Override public void progressStopped() { progressStripe.stopLoading(); } }, this); JComponent toolbars = new JPanel(new BorderLayout()); toolbars.add(myToolbar, BorderLayout.NORTH); JComponent toolbarsAndTable = new JPanel(new BorderLayout()); toolbarsAndTable.add(toolbars, BorderLayout.NORTH); toolbarsAndTable.add(progressStripe, BorderLayout.CENTER); myChangesBrowserSplitter = new OnePixelSplitter(false, "vcs.log.changes.splitter.proportion", 0.7f); myChangesBrowserSplitter.setFirstComponent(toolbarsAndTable); myChangesBrowserSplitter.setSecondComponent(myDetailsSplitter); setLayout(new BorderLayout()); add(myChangesBrowserSplitter); Disposer.register(ui, this); myGraphTable.resetDefaultFocusTraversalKeys(); setFocusCycleRoot(true); setFocusTraversalPolicy(new MyFocusPolicy()); } /** * Informs components that the actual DataPack has been updated (e.g. due to a log refresh). <br/> * Components may want to update their fields and/or rebuild. * * @param dataPack new data pack. * @param permGraphChanged true if permanent graph itself was changed. */ public void updateDataPack(@NotNull VisiblePack dataPack, boolean permGraphChanged) { myFilterUi.updateDataPack(dataPack); myGraphTable.updateDataPack(dataPack, permGraphChanged); } private void updateWhenDetailsAreLoaded() { myMiniDetailsLoadedListener = () -> { myGraphTable.initColumnSize(); myGraphTable.repaint(); }; myContainingBranchesListener = () -> { myDetailsPanel.branchesChanged(); myGraphTable.repaint(); // we may need to repaint highlighters }; myLogData.getMiniDetailsGetter().addDetailsLoadedListener(myMiniDetailsLoadedListener); myLogData.getContainingBranchesGetter().addTaskCompletedListener(myContainingBranchesListener); } public void setupDetailsSplitter(boolean state) { myDetailsSplitter.setSecondComponent(state ? myDetailsPanel : null); } @NotNull private JScrollPane setupScrolledGraph() { JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myGraphTable, SideBorder.TOP); myGraphTable.viewportSet(scrollPane.getViewport()); return scrollPane; } @NotNull public VcsLogGraphTable getGraphTable() { return myGraphTable; } @NotNull public VcsLogFilterUi getFilterUi() { return myFilterUi; } private JComponent createActionsToolbar() { DefaultActionGroup toolbarGroup = new DefaultActionGroup(); toolbarGroup.add(ActionManager.getInstance().getAction(VcsLogActionPlaces.TOOLBAR_ACTION_GROUP)); DefaultActionGroup mainGroup = new DefaultActionGroup(); mainGroup.add(ActionManager.getInstance().getAction(VcsLogActionPlaces.VCS_LOG_TEXT_FILTER_SETTINGS_ACTION)); mainGroup.add(new AnSeparator()); mainGroup.add(myFilterUi.createActionGroup()); mainGroup.addSeparator(); if (BekUtil.isBekEnabled()) { if (BekUtil.isLinearBekEnabled()) { mainGroup.add(new IntelliSortChooserPopupAction()); // can not register both of the actions in xml file, choosing to register an action for the "outer world" // I can of course if linear bek is enabled replace the action on start but why bother } else { mainGroup.add(ActionManager.getInstance().getAction(VcsLogActionPlaces.VCS_LOG_INTELLI_SORT_ACTION)); } } mainGroup.add(toolbarGroup); ActionToolbar toolbar = createActionsToolbar(mainGroup); Wrapper textFilter = new Wrapper(myTextFilter); textFilter.setVerticalSizeReferent(toolbar.getComponent()); textFilter.setBorder(JBUI.Borders.emptyLeft(5)); ActionToolbar settings = createActionsToolbar(new DefaultActionGroup(ActionManager.getInstance().getAction(VcsLogActionPlaces.VCS_LOG_QUICK_SETTINGS_ACTION))); settings.setReservePlaceAutoPopupIcon(false); settings.setLayoutPolicy(ActionToolbar.NOWRAP_LAYOUT_POLICY); JPanel panel = new JPanel(new MigLayout("ins 0, fill", "[left]0[left, fill]push[right]", "center")); panel.add(textFilter); panel.add(toolbar.getComponent()); panel.add(settings.getComponent()); return panel; } @NotNull private ActionToolbar createActionsToolbar(@NotNull DefaultActionGroup mainGroup) { ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.CHANGES_VIEW_TOOLBAR, mainGroup, true); toolbar.setTargetComponent(this); return toolbar; } @NotNull public JComponent getMainComponent() { return this; } @Nullable @Override public Object getData(@NonNls String dataId) { if (VcsLogDataKeys.VCS_LOG.is(dataId)) { return myLog; } else if (VcsLogDataKeys.VCS_LOG_UI.is(dataId)) { return myUi; } else if (VcsLogDataKeys.VCS_LOG_DATA_PROVIDER.is(dataId)) { return myLogData; } else if (VcsDataKeys.CHANGES.is(dataId) || VcsDataKeys.SELECTED_CHANGES.is(dataId)) { return ArrayUtil.toObjectArray(myChangesBrowser.getCurrentDisplayedChanges(), Change.class); } else if (VcsDataKeys.CHANGE_LISTS.is(dataId)) { List<VcsFullCommitDetails> details = myLog.getSelectedDetails(); if (details.size() > VcsLogUtil.MAX_SELECTED_COMMITS) return null; return ContainerUtil.map2Array(details, CommittedChangeListForRevision.class, detail -> new CommittedChangeListForRevision(detail.getSubject(), detail.getFullMessage(), VcsUserUtil.getShortPresentation(detail.getCommitter()), new Date(detail.getCommitTime()), detail.getChanges(), convertToRevisionNumber(detail.getId()))); } else if (VcsDataKeys.VCS_REVISION_NUMBERS.is(dataId)) { List<CommitId> hashes = myLog.getSelectedCommits(); if (hashes.size() > VcsLogUtil.MAX_SELECTED_COMMITS) return null; return ArrayUtil .toObjectArray(ContainerUtil.map(hashes, commitId -> convertToRevisionNumber(commitId.getHash())), VcsRevisionNumber.class); } else if (VcsDataKeys.VCS.is(dataId)) { int[] selectedRows = myGraphTable.getSelectedRows(); if (selectedRows.length == 0 || selectedRows.length > VcsLogUtil.MAX_SELECTED_COMMITS) return null; Set<VirtualFile> roots = ContainerUtil.map2Set(Ints.asList(selectedRows), row -> myGraphTable.getModel().getRoot(row)); if (roots.size() == 1) { return myLogData.getLogProvider(assertNotNull(getFirstItem(roots))).getSupportedVcs(); } } else if (VcsLogDataKeys.VCS_LOG_BRANCHES.is(dataId)) { int[] selectedRows = myGraphTable.getSelectedRows(); if (selectedRows.length != 1) return null; return myGraphTable.getModel().getBranchesAtRow(selectedRows[0]); } else if (PlatformDataKeys.HELP_ID.is(dataId)) { return HELP_ID; } else if (VcsLogInternalDataKeys.LOG_UI_PROPERTIES.is(dataId)) { return myUiProperties; } return null; } @NotNull public JComponent getToolbar() { return myToolbar; } @NotNull public SearchTextField getTextFilter() { return myTextFilter; } public boolean areGraphActionsEnabled() { return myGraphTable.getRowCount() > 0; } @NotNull private static TextRevisionNumber convertToRevisionNumber(@NotNull Hash hash) { return new TextRevisionNumber(hash.asString(), hash.toShortString()); } public void showDetails(boolean state) { myDetailsSplitter.setSecondComponent(state ? myDetailsPanel : null); } @Override public void dispose() { myLogData.getMiniDetailsGetter().removeDetailsLoadedListener(myMiniDetailsLoadedListener); myLogData.getContainingBranchesGetter().removeTaskCompletedListener(myContainingBranchesListener); myDetailsSplitter.dispose(); myChangesBrowserSplitter.dispose(); } private class CommitSelectionListenerForDiff extends CommitSelectionListener { protected CommitSelectionListenerForDiff() { super(myLogData, MainFrame.this.myGraphTable, myChangesLoadingPane); } @Override protected void onDetailsLoaded(@NotNull List<VcsFullCommitDetails> detailsList) { List<Change> changes = ContainerUtil.newArrayList(); List<VcsFullCommitDetails> detailsListReversed = ContainerUtil.reverse(detailsList); for (VcsFullCommitDetails details : detailsListReversed) { changes.addAll(details.getChanges()); } changes = CommittedChangesTreeBrowser.zipChanges(changes); myChangesBrowser.setChangesToDisplay(changes); } @Override protected void onSelection(@NotNull int[] selection) { // just reset and wait for details to be loaded myChangesBrowser.setChangesToDisplay(Collections.emptyList()); myChangesBrowser.getViewer().setEmptyText(""); } @Override protected void onEmptySelection() { myChangesBrowser.getViewer().setEmptyText("No commits selected"); myChangesBrowser.setChangesToDisplay(Collections.emptyList()); } } private class MyFocusPolicy extends ComponentsListFocusTraversalPolicy { @NotNull @Override protected List<Component> getOrderedComponents() { return Arrays.asList(myGraphTable, myChangesBrowser.getPreferredFocusedComponent(), myTextFilter.getTextEditor()); } } }