diff --git a/platform/vcs-log/api/src/com/intellij/vcs/log/RefGroup.java b/platform/vcs-log/api/src/com/intellij/vcs/log/RefGroup.java index ff5f26252481..fbc643235988 100644 --- a/platform/vcs-log/api/src/com/intellij/vcs/log/RefGroup.java +++ b/platform/vcs-log/api/src/com/intellij/vcs/log/RefGroup.java @@ -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 getRefs(); + /** + * Returns the background color of this ref group, which will be used to paint it on the Branches panel. + */ + @NotNull + Color getBgColor(); + } diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/SingletonRefGroup.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/SingletonRefGroup.java new file mode 100644 index 000000000000..9a7738aa6829 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/impl/SingletonRefGroup.java @@ -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 getRefs() { + return Collections.singletonList(myRef); + } + + @NotNull + @Override + public Color getBgColor() { + return myRef.getType().getBackgroundColor(); + } +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/VcsLogUI.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/VcsLogUI.java index c82cbfb47015..05de16622caa 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/VcsLogUI.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/VcsLogUI.java @@ -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; + } } diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/BranchFilterPopupComponent.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/BranchFilterPopupComponent.java index d49e597eabe7..9f30d6209830 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/BranchFilterPopupComponent.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/BranchFilterPopupComponent.java @@ -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 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 orderedGroups = orderRefGroups(groups); + actionGroup.addAll(orderedGroups); } return actionGroup; } + private List orderRefGroups(List 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() { diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/BranchesPanel.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/BranchesPanel.java index 9870277f65f4..5e49860463b1 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/BranchesPanel.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/BranchesPanel.java @@ -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 myRefs; + private List myRefGroups; private final RefPainter myRefPainter; - private Map myRefPositions = new HashMap(); + private Map 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 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 getRefsToDisplayOnPanel() { + private List getRefsToDisplayOnPanel() { Collection allRefs = myDataHolder.getDataPack().getRefsModel().getBranches(); - List refsToShow = new ArrayList(); + List groups = ContainerUtil.newArrayList(); for (Map.Entry> entry : VcsLogUtil.groupRefsByRoot(allRefs).entrySet()) { VirtualFile root = entry.getKey(); Collection refs = entry.getValue(); VcsLogProvider provider = myDataHolder.getLogProvider(root); VcsLogRefManager refManager = provider.getReferenceManager(); - List 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 expandExpandableGroups(List refGroups) { + Collection groups = ContainerUtil.newArrayList(); + for (RefGroup group : refGroups) { + if (group.isExpanded()) { + groups.addAll(ContainerUtil.map(group.getRefs(), new Function() { + @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; + } } } diff --git a/plugins/git4idea/src/git4idea/log/GitRefManager.java b/plugins/git4idea/src/git4idea/log/GitRefManager.java index e27f4d25ce69..867b8258df97 100644 --- a/plugins/git4idea/src/git4idea/log/GitRefManager.java +++ b/plugins/git4idea/src/git4idea/log/GitRefManager.java @@ -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 group(Collection refs) { - // TODO group non-tracking refs into remotes - return ContainerUtil.map(sort(refs), new Function() { - @Override - public RefGroup fun(final VcsRef ref) { - return new RefGroup() { - @NotNull - @Override - public String getName() { - return ref.getName(); - } + List simpleGroups = ContainerUtil.newArrayList(); + List localBranches = ContainerUtil.newArrayList(); + List trackedBranches = ContainerUtil.newArrayList(); + MultiMap remoteRefGroups = MultiMap.create(); - @NotNull - @Override - public List 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 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 result = ContainerUtil.newArrayList(); + result.addAll(simpleGroups); + result.add(new LogicalRefGroup("Local", localBranches)); + result.add(new LogicalRefGroup("Tracked", trackedBranches)); + for (Map.Entry> entry : remoteRefGroups.entrySet()) { + final GitRemote remote = entry.getKey(); + final Collection branches = entry.getValue(); + result.add(new RemoteRefGroup(remote, branches)); + } + return result; + } + + @Nullable + private static T findBranchByName(final VcsRef ref, Collection branches) { + return ContainerUtil.find(branches, new Condition() { + @Override + public boolean value(T branch) { + return branch.getName().equals(ref.getName()); } }); } + private static boolean isTracked(Collection trackInfos, final GitRemoteBranch remoteBranch) { + return ContainerUtil.find(trackInfos, new Condition() { + @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 myRefs; + + private LogicalRefGroup(String groupName, List refs) { + myGroupName = groupName; + myRefs = refs; + } + + @Override + public boolean isExpanded() { + return true; + } + + @NotNull + @Override + public String getName() { + return myGroupName; + } + + @NotNull + @Override + public List getRefs() { + return myRefs; + } + + @NotNull + @Override + public Color getBgColor() { + return HEAD_COLOR; + } + } + + private class RemoteRefGroup implements RefGroup { + private final GitRemote myRemote; + private final Collection myBranches; + + public RemoteRefGroup(GitRemote remote, Collection branches) { + myRemote = remote; + myBranches = branches; + } + + @Override + public boolean isExpanded() { + return false; + } + + @NotNull + @Override + public String getName() { + return myRemote.getName() + "/..."; + } + + @NotNull + @Override + public List getRefs() { + return sort(myBranches); + } + + @NotNull + @Override + public Color getBgColor() { + return REMOTE_BRANCH_COLOR; + } + + } }