[log] Group branches in the filter and on the BranchesPanel

Introduce so called expandable groups:
* reference in them are displayed inline.
* in the filter component they are prefixed with a text separator
  indicating the group.
* on branches panel they are expanded and displayed as separate refs.

GitRefManager:
* group local branches into expanded group;
* group tracked remote branches into expanded group;
* group other remote branches into separate groups by the name of
  remote.

BranchesPanel:
* draw expanded groups inline.
* for true-groups draw the name of the group as if it is a reference.
* clicking on the group shows a popup with the list of references.
* clicking on the reference navigates to the commit, in the same way
  as it is done for single-references.

Existing problem: multiple-root branches with same names (both on the
panel and in the filter).
This commit is contained in:
Kirill Likhodedov
2013-10-28 17:09:17 +04:00
parent 83c5cf865e
commit d9a50108de
6 changed files with 451 additions and 59 deletions
@@ -2,16 +2,22 @@ package com.intellij.vcs.log;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.util.List;
/**
* Lets group {@link VcsRef references} to show them accordingly in the UI, for example on the branches panel.
* Grouping decision is made by the concrete {@link VcsLogRefManager}.
*
* @author Kirill Likhodedov
*/
public interface RefGroup {
/**
* If a group is not-expanded, its references won't be displayed until
* Otherwise, if a group is expanded, its references will be displayed immediately,
* but they may possibly be somehow visually united to indicated that they are from similar structure.
*/
boolean isExpanded();
/**
* Returns the name of the reference group. This reference will be displayed on the branches panel.
*/
@@ -24,4 +30,10 @@ public interface RefGroup {
@NotNull
List<VcsRef> getRefs();
/**
* Returns the background color of this ref group, which will be used to paint it on the Branches panel.
*/
@NotNull
Color getBgColor();
}
@@ -0,0 +1,58 @@
/*
* Copyright 2000-2013 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 com.intellij.vcs.log.impl;
import com.intellij.vcs.log.RefGroup;
import com.intellij.vcs.log.VcsRef;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.util.Collections;
import java.util.List;
/**
* {@link RefGroup} containing only one {@link VcsRef}.
*/
public class SingletonRefGroup implements RefGroup {
private final VcsRef myRef;
public SingletonRefGroup(VcsRef ref) {
myRef = ref;
}
@Override
public boolean isExpanded() {
return false;
}
@NotNull
@Override
public String getName() {
return myRef.getName();
}
@NotNull
@Override
public List<VcsRef> getRefs() {
return Collections.singletonList(myRef);
}
@NotNull
@Override
public Color getBgColor() {
return myRef.getType().getBackgroundColor();
}
}
@@ -38,6 +38,7 @@ public class VcsLogUI {
@NotNull private final VcsLogDataHolder myLogDataHolder;
@NotNull private final MainFrame myMainFrame;
@NotNull private final Project myProject;
@NotNull private final VcsLogColorManager myColorManager;
@NotNull private final VcsLogUiProperties myUiProperties;
@NotNull private final VcsLogFilterer myFilterer;
@@ -47,6 +48,7 @@ public class VcsLogUI {
public VcsLogUI(@NotNull VcsLogDataHolder logDataHolder, @NotNull Project project, @NotNull VcsLogSettings settings,
@NotNull VcsLogColorManager manager, @NotNull VcsLogUiProperties uiProperties) {
myLogDataHolder = logDataHolder;
myProject = project;
myColorManager = manager;
myUiProperties = uiProperties;
myFilterer = new VcsLogFilterer(logDataHolder, this);
@@ -215,4 +217,9 @@ public class VcsLogUI {
public VcsLogUiProperties getUiProperties() {
return myUiProperties;
}
@NotNull
public Project getProject() {
return myProject;
}
}
@@ -16,19 +16,18 @@
package com.intellij.vcs.log.ui.filter;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.Separator;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.vcs.log.RefGroup;
import com.intellij.vcs.log.VcsLogProvider;
import com.intellij.vcs.log.VcsLogRefManager;
import com.intellij.vcs.log.VcsRef;
import com.intellij.vcs.log.*;
import com.intellij.vcs.log.data.VcsLogBranchFilter;
import com.intellij.vcs.log.VcsLogFilter;
import com.intellij.vcs.log.impl.VcsLogUtil;
import com.intellij.vcs.log.ui.VcsLogUI;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -56,22 +55,41 @@ class BranchFilterPopupComponent extends FilterPopupComponent {
VcsLogRefManager refManager = provider.getReferenceManager();
List<RefGroup> groups = refManager.group(refs);
for (RefGroup group : groups) {
if (group.getRefs().size() == 1) {
actionGroup.add(new SetValueAction(group.getRefs().iterator().next().getName(), this));
}
else {
DefaultActionGroup innerGroup = new DefaultActionGroup();
for (VcsRef ref : group.getRefs()) {
innerGroup.add(new SetValueAction(ref.getName(), this));
}
actionGroup.add(innerGroup);
}
}
List<AnAction> orderedGroups = orderRefGroups(groups);
actionGroup.addAll(orderedGroups);
}
return actionGroup;
}
private List<AnAction> orderRefGroups(List<RefGroup> groups) {
DefaultActionGroup singletonGroup = new DefaultActionGroup();
DefaultActionGroup expandedGroup = new DefaultActionGroup();
DefaultActionGroup collapsedGroup = new DefaultActionGroup();
for (RefGroup group : groups) {
if (group.getRefs().size() == 1) {
singletonGroup.add(new SetValueAction(group.getRefs().iterator().next().getName(), this));
}
else if (group.isExpanded()) {
expandedGroup.addSeparator(group.getName());
expandedGroup.add(createActionGroup(group, false));
}
else {
collapsedGroup.add(createActionGroup(group, true));
}
}
return Arrays.asList(singletonGroup, expandedGroup, Separator.getInstance(), collapsedGroup);
}
private DefaultActionGroup createActionGroup(RefGroup group, boolean popup) {
DefaultActionGroup innerGroup = new DefaultActionGroup(group.getName(), popup);
for (VcsRef ref : group.getRefs()) {
innerGroup.add(new SetValueAction(ref.getName(), this));
}
return innerGroup;
}
@Nullable
@Override
protected VcsLogFilter getFilter() {
@@ -1,14 +1,22 @@
package com.intellij.vcs.log.ui.frame;
import com.google.common.collect.Ordering;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.ListUtil;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcs.log.RefGroup;
import com.intellij.vcs.log.VcsLogProvider;
import com.intellij.vcs.log.VcsLogRefManager;
import com.intellij.vcs.log.VcsRef;
import com.intellij.vcs.log.data.VcsLogDataHolder;
import com.intellij.vcs.log.graph.render.PrintParameters;
import com.intellij.vcs.log.impl.SingletonRefGroup;
import com.intellij.vcs.log.impl.VcsLogUtil;
import com.intellij.vcs.log.ui.VcsLogUI;
import com.intellij.vcs.log.ui.render.RefPainter;
@@ -17,48 +25,60 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static com.intellij.vcs.log.graph.render.PrintParameters.HEIGHT_CELL;
/**
* Panel with branch labels, above the graph.
*
* @author Kirill Likhodedov
*/
public class BranchesPanel extends JPanel {
private final VcsLogDataHolder myDataHolder;
private final VcsLogUI myUI;
private List<VcsRef> myRefs;
private List<RefGroup> myRefGroups;
private final RefPainter myRefPainter;
private Map<Integer, VcsRef> myRefPositions = new HashMap<Integer, VcsRef>();
private Map<Integer, RefGroup> myRefPositions = ContainerUtil.newHashMap();
public BranchesPanel(@NotNull VcsLogDataHolder dataHolder, @NotNull VcsLogUI UI) {
myDataHolder = dataHolder;
myUI = UI;
myRefs = getRefsToDisplayOnPanel();
myRefGroups = getRefsToDisplayOnPanel();
myRefPainter = new RefPainter(myUI.getColorManager(), true);
setPreferredSize(new Dimension(-1, PrintParameters.HEIGHT_CELL + UIUtil.DEFAULT_VGAP));
setPreferredSize(new Dimension(-1, HEIGHT_CELL + UIUtil.DEFAULT_VGAP));
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
VcsRef ref = findRef(e);
if (ref != null) {
final RefGroup group = findRef(e);
if (group == null) {
return;
}
if (group.getRefs().size() == 1) {
VcsRef ref = group.getRefs().iterator().next();
myUI.jumpToCommit(ref.getCommitHash());
}
else {
final RefPopupComponent view = new RefPopupComponent(group, myUI, myRefPainter);
JBPopup popup = view.getPopup();
popup.show(new RelativePoint(BranchesPanel.this, new Point(e.getX(), BranchesPanel.this.getHeight())));
}
}
});
}
@Nullable
private VcsRef findRef(MouseEvent e) {
private RefGroup findRef(MouseEvent e) {
List<Integer> sortedPositions = Ordering.natural().sortedCopy(myRefPositions.keySet());
int index = Ordering.natural().binarySearch(sortedPositions, e.getX());
if (index < 0) {
@@ -72,35 +92,190 @@ public class BranchesPanel extends JPanel {
@Override
protected void paintComponent(Graphics g) {
myRefPositions = myRefPainter.draw((Graphics2D)g, myRefs, 0, getWidth());
myRefPositions = ContainerUtil.newHashMap();
int paddingX = 0;
for (RefGroup group : myRefGroups) {
Rectangle rectangle = myRefPainter.drawLabel((Graphics2D)g, group.getName(), paddingX, group.getBgColor());
paddingX += rectangle.width + UIUtil.DEFAULT_HGAP;
myRefPositions.put(rectangle.x, group);
}
}
public void rebuild() {
myRefs = getRefsToDisplayOnPanel();
myRefGroups = getRefsToDisplayOnPanel();
getParent().repaint();
}
@NotNull
private List<VcsRef> getRefsToDisplayOnPanel() {
private List<RefGroup> getRefsToDisplayOnPanel() {
Collection<VcsRef> allRefs = myDataHolder.getDataPack().getRefsModel().getBranches();
List<VcsRef> refsToShow = new ArrayList<VcsRef>();
List<RefGroup> groups = ContainerUtil.newArrayList();
for (Map.Entry<VirtualFile, Collection<VcsRef>> entry : VcsLogUtil.groupRefsByRoot(allRefs).entrySet()) {
VirtualFile root = entry.getKey();
Collection<VcsRef> refs = entry.getValue();
VcsLogProvider provider = myDataHolder.getLogProvider(root);
VcsLogRefManager refManager = provider.getReferenceManager();
List<RefGroup> groups = refManager.group(refs);
groups.addAll(expandExpandableGroups(refManager.group(refs)));
}
// TODO draw groups
for (RefGroup group : groups) {
if (group.getRefs().size() == 1) {
refsToShow.add(group.getRefs().iterator().next());
}
return groups;
}
private static Collection<RefGroup> expandExpandableGroups(List<RefGroup> refGroups) {
Collection<RefGroup> groups = ContainerUtil.newArrayList();
for (RefGroup group : refGroups) {
if (group.isExpanded()) {
groups.addAll(ContainerUtil.map(group.getRefs(), new Function<VcsRef, RefGroup>() {
@Override
public RefGroup fun(VcsRef ref) {
return new SingletonRefGroup(ref);
}
}));
}
else {
groups.add(group);
}
}
// TODO improve UI for multiple roots case
return refsToShow;
return groups;
}
private static class RefPopupComponent extends JPanel {
private final JBPopup myPopup;
private final JBList myList;
private final VcsLogUI myUi;
private final RefPainter myRefPainter;
private final SingleRefComponent myRendererComponent;
private final ListCellRenderer myCellRenderer;
RefPopupComponent(RefGroup group, VcsLogUI ui, RefPainter refPainter) {
super(new BorderLayout());
myUi = ui;
myRefPainter = refPainter;
myRendererComponent = new SingleRefComponent(myRefPainter);
myCellRenderer = new ListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
myRendererComponent.setRef((VcsRef)value);
myRendererComponent.setSelected(isSelected);
return myRendererComponent;
}
};
myList = createList(group);
myPopup = createPopup();
add(new JBScrollPane(myList));
}
private JBList createList(RefGroup group) {
JBList list = new JBList(createListModel(group));
list.setCellRenderer(myCellRenderer);
ListUtil.installAutoSelectOnMouseMove(list);
list.setSelectedIndex(0);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
jumpOnMouseClick(list);
jumpOnEnter(list);
return list;
}
private void jumpOnMouseClick(JBList list) {
list.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
jumpToSelectedRef();
}
});
}
private void jumpOnEnter(JBList list) {
list.addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
jumpToSelectedRef();
}
}
});
}
private JBPopup createPopup() {
return JBPopupFactory.getInstance().
createComponentPopupBuilder(this, myList).
setCancelOnClickOutside(true).
setCancelOnWindowDeactivation(true).
setFocusable(true).
setRequestFocus(true).
setResizable(true).
setDimensionServiceKey(myUi.getProject(), "Vcs.Log.Branch.Panel.RefGroup.Popup", false).
createPopup();
}
private static DefaultListModel createListModel(RefGroup group) {
DefaultListModel model = new DefaultListModel();
for (final VcsRef vcsRef : group.getRefs()) {
model.addElement(vcsRef);
}
return model;
}
@NotNull
JBPopup getPopup() {
return myPopup;
}
private void jumpToSelectedRef() {
VcsRef selectedRef = (VcsRef)myList.getSelectedValue();
if (selectedRef != null) {
myUi.jumpToCommit(selectedRef.getCommitHash());
myPopup.cancel();
}
}
}
private static class SingleRefComponent extends JPanel {
private final RefPainter myRefPainter;
private VcsRef myRef;
public boolean mySelected;
public SingleRefComponent(RefPainter refPainter) {
myRefPainter = refPainter;
}
@Override
protected void paintComponent(Graphics g) {
g.setColor(mySelected ? UIUtil.getListSelectionBackground() : UIUtil.getListBackground());
g.fillRect(0, 0, getWidth(), getHeight());
if (myRef != null) {
myRefPainter.draw((Graphics2D)g, Collections.singletonList(myRef), 0, calcWidth());
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(calcWidth(), HEIGHT_CELL);
}
private int calcWidth() {
FontMetrics metrics = getFontMetrics(getFont());
return metrics.stringWidth(myRef.getName());
}
public void setRef(@NotNull VcsRef ref) {
myRef = ref;
}
public void setSelected(boolean selected) {
mySelected = selected;
}
}
}
@@ -4,15 +4,22 @@ import com.intellij.dvcs.repo.RepositoryManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
import com.intellij.ui.JBColor;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.vcs.log.RefGroup;
import com.intellij.vcs.log.VcsLogRefManager;
import com.intellij.vcs.log.VcsRef;
import com.intellij.vcs.log.VcsRefType;
import com.intellij.vcs.log.impl.SingletonRefGroup;
import git4idea.GitBranch;
import git4idea.GitLocalBranch;
import git4idea.GitRemoteBranch;
import git4idea.branch.GitBranchesCollection;
import git4idea.repo.GitBranchTrackInfo;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.*;
@@ -124,27 +131,76 @@ public class GitRefManager implements VcsLogRefManager {
@NotNull
@Override
public List<RefGroup> group(Collection<VcsRef> refs) {
// TODO group non-tracking refs into remotes
return ContainerUtil.map(sort(refs), new Function<VcsRef, RefGroup>() {
@Override
public RefGroup fun(final VcsRef ref) {
return new RefGroup() {
@NotNull
@Override
public String getName() {
return ref.getName();
}
List<RefGroup> simpleGroups = ContainerUtil.newArrayList();
List<VcsRef> localBranches = ContainerUtil.newArrayList();
List<VcsRef> trackedBranches = ContainerUtil.newArrayList();
MultiMap<GitRemote, VcsRef> remoteRefGroups = MultiMap.create();
@NotNull
@Override
public List<VcsRef> getRefs() {
return Collections.singletonList(ref);
for (VcsRef ref : refs) {
if (ref.getType() == HEAD) {
simpleGroups.add(new SingletonRefGroup(ref));
}
else {
GitRepository repository = myRepositoryManager.getRepositoryForRoot(ref.getRoot());
if (repository == null) {
LOG.warn("No repository for root: " + ref.getRoot());
continue;
}
Collection<GitBranchTrackInfo> trackInfos = repository.getBranchTrackInfos();
GitBranchesCollection branches = repository.getBranches();
GitLocalBranch localBranch = findBranchByName(ref, branches.getLocalBranches());
if (localBranch != null) {
localBranches.add(ref);
}
else {
GitRemoteBranch remoteBranch = findBranchByName(ref, branches.getRemoteBranches());
if (remoteBranch != null) {
if (isTracked(trackInfos, remoteBranch)) {
trackedBranches.add(ref);
}
else {
remoteRefGroups.putValue(remoteBranch.getRemote(), ref);
}
}
};
else {
LOG.warn("Didn't find ref neither in local nor in remote branches: " + ref);
}
}
}
}
List<RefGroup> result = ContainerUtil.newArrayList();
result.addAll(simpleGroups);
result.add(new LogicalRefGroup("Local", localBranches));
result.add(new LogicalRefGroup("Tracked", trackedBranches));
for (Map.Entry<GitRemote, Collection<VcsRef>> entry : remoteRefGroups.entrySet()) {
final GitRemote remote = entry.getKey();
final Collection<VcsRef> branches = entry.getValue();
result.add(new RemoteRefGroup(remote, branches));
}
return result;
}
@Nullable
private static <T extends GitBranch> T findBranchByName(final VcsRef ref, Collection<T> branches) {
return ContainerUtil.find(branches, new Condition<T>() {
@Override
public boolean value(T branch) {
return branch.getName().equals(ref.getName());
}
});
}
private static boolean isTracked(Collection<GitBranchTrackInfo> trackInfos, final GitRemoteBranch remoteBranch) {
return ContainerUtil.find(trackInfos, new Condition<GitBranchTrackInfo>() {
@Override
public boolean value(GitBranchTrackInfo info) {
return info.getRemoteBranch().equals(remoteBranch);
}
}) != null;
}
private static class SimpleRefType implements VcsRefType {
private final boolean myIsBranch;
@NotNull private final Color myColor;
@@ -166,4 +222,70 @@ public class GitRefManager implements VcsLogRefManager {
}
}
private static class LogicalRefGroup implements RefGroup {
private final String myGroupName;
private final List<VcsRef> myRefs;
private LogicalRefGroup(String groupName, List<VcsRef> refs) {
myGroupName = groupName;
myRefs = refs;
}
@Override
public boolean isExpanded() {
return true;
}
@NotNull
@Override
public String getName() {
return myGroupName;
}
@NotNull
@Override
public List<VcsRef> getRefs() {
return myRefs;
}
@NotNull
@Override
public Color getBgColor() {
return HEAD_COLOR;
}
}
private class RemoteRefGroup implements RefGroup {
private final GitRemote myRemote;
private final Collection<VcsRef> myBranches;
public RemoteRefGroup(GitRemote remote, Collection<VcsRef> branches) {
myRemote = remote;
myBranches = branches;
}
@Override
public boolean isExpanded() {
return false;
}
@NotNull
@Override
public String getName() {
return myRemote.getName() + "/...";
}
@NotNull
@Override
public List<VcsRef> getRefs() {
return sort(myBranches);
}
@NotNull
@Override
public Color getBgColor() {
return REMOTE_BRANCH_COLOR;
}
}
}