diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogBranchFilter.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogBranchFilter.java new file mode 100644 index 000000000000..fc7e6a3a6630 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogBranchFilter.java @@ -0,0 +1,43 @@ +package com.intellij.vcs.log.data; + +import com.intellij.util.Function; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.vcs.log.Hash; +import com.intellij.vcs.log.VcsRef; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * TODO use VcsRef instead of String + */ +public class VcsLogBranchFilter implements VcsLogGraphFilter { + + @NotNull private final Collection myMatchingHeads; + private final String myBranchName; + + public VcsLogBranchFilter(Collection allRefs, final String branchName) { + myBranchName = branchName; + myMatchingHeads = ContainerUtil.mapNotNull(allRefs, new Function() { + @Override + public Hash fun(VcsRef ref) { + if (ref.getName().equals(branchName)) { + return ref.getCommitHash(); + } + return null; + } + }); + + } + + @Override + public boolean matches(@NotNull Hash hash) { + return myMatchingHeads.contains(hash); + } + + @Override + public String toString() { + return "on: " + myBranchName; + } + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogDataHolder.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogDataHolder.java index a4e95467ce58..203fa542d608 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogDataHolder.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogDataHolder.java @@ -16,6 +16,8 @@ package com.intellij.vcs.log.data; import com.intellij.openapi.Disposable; +import com.intellij.openapi.diagnostic.Attachment; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.BackgroundTaskQueue; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.Task; @@ -85,6 +87,8 @@ public class VcsLogDataHolder implements Disposable { public static final Topic REFRESH_COMPLETED = Topic.create("Vcs.Log.Completed", Runnable.class); + private static final Logger LOG = Logger.getInstance(VcsLogDataHolder.class); + @NotNull private final Project myProject; @NotNull private final VcsLogObjectsFactory myFactory; @NotNull private final Map myLogProviders; @@ -386,6 +390,39 @@ public class VcsLogDataHolder implements Disposable { return myLogProviders.get(root); } + /** + * Returns the ordered part of the recent commits, details of which are always stored in the cache. + */ + @NotNull + public Collection getTopCommits() { + return myLogData.getTopCommits(); + } + + /** + * Returns details of recent commits ordered in the "right way" (as in the log). + */ + @NotNull + public Collection getTopCommitDetails() { + final Collection topCommits = getTopCommits(); + return ContainerUtil.mapNotNull(topCommits, new Function() { + @Nullable + @Override + public VcsFullCommitDetails fun(TimedVcsCommit commit) { + Hash hash = commit.getHash(); + VcsFullCommitDetails details = myTopCommitsDetailsCache.get(hash); + if (details != null) { + return details; + } + + // shouldn't happen + LOG.error("No details were stored for commit " + hash, + new Attachment("details_cache.txt", myTopCommitsDetailsCache.toString()), + new Attachment("top_commits.txt", topCommits.toString())); + return null; + } + }); + } + /** * Contains full logs per repository root & references per root. * diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogDetailsFilter.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogDetailsFilter.java new file mode 100644 index 000000000000..a88df5fc2162 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogDetailsFilter.java @@ -0,0 +1,15 @@ +package com.intellij.vcs.log.data; + +import com.intellij.vcs.log.VcsFullCommitDetails; +import org.jetbrains.annotations.NotNull; + +/** + * Filter which needs {@link VcsFullCommitDetails} to work. + * + * @see VcsLogGraphFilter + */ +public interface VcsLogDetailsFilter extends VcsLogFilter { + + boolean matches(@NotNull VcsFullCommitDetails details); + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogFilter.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogFilter.java new file mode 100644 index 000000000000..7c185c95c096 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogFilter.java @@ -0,0 +1,8 @@ +package com.intellij.vcs.log.data; + +/** + * Marker interface indicating that this is a filter for the VCS log. + */ +public interface VcsLogFilter { + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogFilterer.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogFilterer.java new file mode 100644 index 000000000000..8621c48aff38 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogFilterer.java @@ -0,0 +1,100 @@ +package com.intellij.vcs.log.data; + +import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.Function; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.vcs.log.VcsFullCommitDetails; +import com.intellij.vcs.log.graph.elements.Node; +import com.intellij.vcs.log.graphmodel.GraphModel; +import com.intellij.vcs.log.ui.VcsLogUI; +import com.intellij.vcs.log.ui.tables.GraphTableModel; +import com.intellij.vcs.log.ui.tables.NoGraphTableModel; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.List; + +public class VcsLogFilterer { + + private static final Function ALL_NODES_VISIBLE = new Function() { + @Override + public Boolean fun(Node node) { + return true; + } + }; + + @NotNull private final VcsLogDataHolder myLogDataHolder; + @NotNull private final VcsLogUI myUI; + + public VcsLogFilterer(@NotNull VcsLogDataHolder logDataHolder, @NotNull VcsLogUI ui) { + myLogDataHolder = logDataHolder; + myUI = ui; + } + + public void applyFiltersAndUpdateUi(@NotNull Collection filters) { + GraphModel graphModel = myLogDataHolder.getDataPack().getGraphModel(); + List graphFilters = ContainerUtil.findAll(filters, VcsLogGraphFilter.class); + List detailsFilters = ContainerUtil.findAll(filters, VcsLogDetailsFilter.class); + + // it is important to apply graph filters first: + // if we apply other filters first, we loose the graph and won't be able to apple graph filters in that case + // (e.g. won't be able to find out if a commit belongs to the branch selected by user). + + // hide invisible nodes from the graph + if (!graphFilters.isEmpty()) { + applyGraphFilters(graphModel, graphFilters); + } + else { + graphModel.setVisibleBranchesNodes(ALL_NODES_VISIBLE); + } + + // apply details filters, and use simple table without graph (we can't filter by details and keep the graph yet). + if (!detailsFilters.isEmpty()) { + List> filteredCommits = filterByDetails(graphModel, detailsFilters); + myUI.setModel(new NoGraphTableModel(filteredCommits, myLogDataHolder.getDataPack().getRefsModel())); + } + else { + myUI.setModel(new GraphTableModel(myLogDataHolder)); + } + + myUI.updateUI(); + } + + private static void applyGraphFilters(GraphModel graphModel, final List onGraphFilters) { + graphModel.setVisibleBranchesNodes(new Function() { + @Override + public Boolean fun(final Node node) { + return !ContainerUtil.exists(onGraphFilters, new Condition() { + @Override + public boolean value(VcsLogGraphFilter filter) { + return !filter.matches(node.getCommitHash()); + } + }); + } + }); + } + + private List> filterByDetails(final GraphModel graphModel, + final List detailsFilters) { + return ContainerUtil.mapNotNull(myLogDataHolder.getTopCommitDetails(), + new Function>() { + @Override + public Pair fun(final VcsFullCommitDetails details) { + boolean allFilterMatch = !ContainerUtil.exists(detailsFilters, new Condition() { + @Override + public boolean value(VcsLogDetailsFilter filter) { + return !filter.matches(details); + } + }); + Node node = graphModel.getNodeIfVisible(details.getHash()); + if (node == null || !allFilterMatch) { + return null; + } + return Pair.create(details, node.getBranch().getRepositoryRoot()); + } + }); + } + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogGraphFilter.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogGraphFilter.java new file mode 100644 index 000000000000..439bea9e5aa8 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogGraphFilter.java @@ -0,0 +1,13 @@ +package com.intellij.vcs.log.data; + +import com.intellij.vcs.log.Hash; +import org.jetbrains.annotations.NotNull; + +/** + * Filter that is able to work on graph. + */ +public interface VcsLogGraphFilter extends VcsLogFilter { + + boolean matches(@NotNull Hash hash); + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogUserFilter.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogUserFilter.java new file mode 100644 index 000000000000..11c051adef1b --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/data/VcsLogUserFilter.java @@ -0,0 +1,22 @@ +package com.intellij.vcs.log.data; + +import com.intellij.vcs.log.VcsFullCommitDetails; +import org.jetbrains.annotations.NotNull; + +/** + * TODO use some special User class instead of String + */ +public class VcsLogUserFilter implements VcsLogDetailsFilter { + + @NotNull private final String myUser; + + public VcsLogUserFilter(@NotNull String user) { + myUser = user; + } + + @Override + public boolean matches(@NotNull VcsFullCommitDetails detail) { + return detail.getAuthorName().toLowerCase().contains(myUser) || detail.getAuthorEmail().toLowerCase().contains(myUser); + } + +} 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 43d383e80057..6f7f5359c0dd 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 @@ -8,17 +8,19 @@ import com.intellij.vcs.log.Hash; import com.intellij.vcs.log.compressedlist.UpdateRequest; import com.intellij.vcs.log.data.DataPack; import com.intellij.vcs.log.data.VcsLogDataHolder; +import com.intellij.vcs.log.data.VcsLogFilter; +import com.intellij.vcs.log.data.VcsLogFilterer; import com.intellij.vcs.log.graph.elements.GraphElement; import com.intellij.vcs.log.graph.elements.Node; import com.intellij.vcs.log.graphmodel.FragmentManager; import com.intellij.vcs.log.graphmodel.GraphFragment; import com.intellij.vcs.log.printmodel.SelectController; import com.intellij.vcs.log.ui.frame.MainFrame; -import com.intellij.vcs.log.ui.tables.GraphTableModel; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.table.TableModel; +import java.util.Collection; /** * @author erokhins @@ -30,23 +32,22 @@ public class VcsLogUI { @NotNull private final VcsLogDataHolder myLogDataHolder; @NotNull private final MainFrame myMainFrame; @NotNull private final VcsLogColorManager myColorManager; + @NotNull private final VcsLogFilterer myFilterer; @Nullable private GraphElement prevGraphElement; - @NotNull private TableModel myGraphModel; public VcsLogUI(@NotNull VcsLogDataHolder logDataHolder, @NotNull Project project, @NotNull VcsLogColorManager manager) { myLogDataHolder = logDataHolder; myColorManager = manager; + myFilterer = new VcsLogFilterer(logDataHolder, this); myMainFrame = new MainFrame(myLogDataHolder, this, project); project.getMessageBus().connect(project).subscribe(VcsLogDataHolder.REFRESH_COMPLETED, new Runnable() { @Override public void run() { - reloadModel(); - updateUI(); + applyFilters(); } }); - reloadModel(); - updateUI(); + applyFilters(); } @NotNull @@ -64,15 +65,14 @@ public class VcsLogUI { }); } - public void reloadModel() { - myGraphModel = new GraphTableModel(myLogDataHolder); + public void setModel(@NotNull TableModel model) { + myMainFrame.getGraphTable().setModel(model); } public void updateUI() { UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { - myMainFrame.getGraphTable().setModel(myGraphModel); myMainFrame.getGraphTable().setPreferredColumnWidths(); myMainFrame.getGraphTable().repaint(); myMainFrame.refresh(); @@ -180,4 +180,24 @@ public class VcsLogUI { public VcsLogColorManager getColorManager() { return myColorManager; } + + @NotNull + public VcsLogFilterer getFilterer() { + return myFilterer; + } + + @NotNull + public VcsLogDataHolder getLogDataHolder() { + return myLogDataHolder; + } + + private void applyFilters() { + myFilterer.applyFiltersAndUpdateUi(collectFilters()); + } + + @NotNull + private Collection collectFilters() { + return myMainFrame.getFilterUi().getFilters(); + } + } 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 new file mode 100644 index 000000000000..0bbca5ff99cf --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/BranchFilterPopupComponent.java @@ -0,0 +1,84 @@ +/* + * 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.ui.filter; + +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +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.data.VcsLogBranchFilter; +import com.intellij.vcs.log.data.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.Collection; +import java.util.List; +import java.util.Map; + +class BranchFilterPopupComponent extends FilterPopupComponent { + + @NotNull private final VcsLogUI myUi; + + BranchFilterPopupComponent(@NotNull VcsLogClassicFilterUi filterUi, @NotNull VcsLogUI ui) { + super(filterUi, "Branch"); + myUi = ui; + } + + @Override + protected ActionGroup createActionGroup() { + DefaultActionGroup actionGroup = new DefaultActionGroup(); + + actionGroup.add(createAllAction()); + + Collection allRefs = myUi.getLogDataHolder().getDataPack().getRefsModel().getBranches(); + for (Map.Entry> entry : VcsLogUtil.groupRefsByRoot(allRefs).entrySet()) { + VirtualFile root = entry.getKey(); + Collection refs = entry.getValue(); + VcsLogProvider provider = myUi.getLogDataHolder().getLogProvider(root); + 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); + } + } + } + return actionGroup; + } + + @Nullable + @Override + protected VcsLogFilter getFilter() { + String value = getValue(); + return value == ALL + ? null + : new VcsLogBranchFilter(myUi.getLogDataHolder().getDataPack().getRefsModel().getBranches(), value); + } + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/FilterPopupComponent.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/FilterPopupComponent.java new file mode 100644 index 000000000000..b724f6a1659d --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/FilterPopupComponent.java @@ -0,0 +1,209 @@ +/* + * 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.ui.filter; + +import com.intellij.icons.AllIcons; +import com.intellij.ide.DataManager; +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.ui.popup.ListPopup; +import com.intellij.ui.ClickListener; +import com.intellij.ui.RoundedLineBorder; +import com.intellij.util.ui.UIUtil; +import com.intellij.vcs.log.data.VcsLogFilter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.border.Border; +import java.awt.event.*; + +/** + * Base class for components which allow to set up filter for the VCS Log, by displaying a popup with available choices. + */ +abstract class FilterPopupComponent extends JPanel { + + /** + * Special value that indicates that no filtering is on. + */ + protected static final String ALL = "All"; + + private static final int GAP_BEFORE_ARROW = 3; + private static final int BORDER_SIZE = 2; + private static final Border INNER_MARGIN_BORDER = BorderFactory.createEmptyBorder(2, 2, 2, 2); + private static final Border FOCUSED_BORDER = createFocusedBorder(); + private static final Border UNFOCUSED_BORDER = createUnfocusedBorder(); + + @NotNull private final JLabel myFilterNameLabel; + @NotNull private final JLabel myFilterValueLabel; + @NotNull private final VcsLogClassicFilterUi myFilterUi; + + FilterPopupComponent(@NotNull VcsLogClassicFilterUi filterUi, @NotNull String filterName) { + myFilterUi = filterUi; + myFilterNameLabel = new JLabel(filterName + ": "); + myFilterValueLabel = new JLabel(ALL); + setDefaultForeground(); + setFocusable(true); + setBorder(UNFOCUSED_BORDER); + + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + add(myFilterNameLabel); + add(myFilterValueLabel); + add(Box.createHorizontalStrut(GAP_BEFORE_ARROW)); + add(new JLabel(AllIcons.Ide.Statusbar_arrows)); + + showPopupMenuOnClick(); + showPopupMenuFromKeyboard(); + indicateHovering(); + indicateFocusing(); + } + + private static Border createFocusedBorder() { + return BorderFactory.createCompoundBorder(new RoundedLineBorder(UIUtil.getHeaderActiveColor(), 10, BORDER_SIZE), + INNER_MARGIN_BORDER); + } + + private static Border createUnfocusedBorder() { + return BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, BORDER_SIZE), + INNER_MARGIN_BORDER); + } + + /** + * Create popup actions available under this filter. + */ + protected abstract ActionGroup createActionGroup(); + + /** + * Return the filter currently selected by this component. + */ + @Nullable + protected abstract VcsLogFilter getFilter(); + + protected void setValue(@NotNull String newValue) { + myFilterValueLabel.setText(newValue); + } + + @NotNull + public String getValue() { + return myFilterValueLabel.getText(); + } + + protected void applyFilters() { + myFilterUi.applyFilters(); + } + + /** + * Returns the special action that indicates that no filtering is selected in this component. + */ + protected AnAction createAllAction() { + return new AllAction(this); + } + + private void indicateFocusing() { + addFocusListener(new FocusAdapter() { + @Override + public void focusGained(FocusEvent e) { + setBorder(FOCUSED_BORDER); + } + + @Override + public void focusLost(FocusEvent e) { + setBorder(UNFOCUSED_BORDER); + } + }); + } + + private void showPopupMenuFromKeyboard() { + addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_DOWN) { + showPopupMenu(); + } + } + }); + } + + private void showPopupMenuOnClick() { + new ClickListener() { + @Override + public boolean onClick(MouseEvent event, int clickCount) { + showPopupMenu(); + return true; + } + }.installOn(this); + } + + private void indicateHovering() { + addMouseListener(new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + setOnHoverForeground(); + } + + @Override + public void mouseExited(MouseEvent e) { + setDefaultForeground(); + } + }); + } + + private void setDefaultForeground() { + myFilterNameLabel.setForeground(UIUtil.isUnderDarcula() ? UIUtil.getLabelForeground() : UIUtil.getInactiveTextColor()); + myFilterValueLabel.setForeground(UIUtil.isUnderDarcula() ? UIUtil.getLabelForeground() : + UIUtil.getInactiveTextColor().darker().darker()); + } + + private void setOnHoverForeground() { + myFilterNameLabel.setForeground(UIUtil.isUnderDarcula() ? UIUtil.getLabelForeground() : UIUtil.getTextAreaForeground()); + myFilterValueLabel.setForeground(UIUtil.isUnderDarcula() ? UIUtil.getLabelForeground() : UIUtil.getTextFieldForeground()); + } + + private void showPopupMenu() { + ListPopup popup = JBPopupFactory.getInstance().createActionGroupPopup(null, createActionGroup(), + DataManager.getInstance().getDataContext(this), + JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, false); + popup.showUnderneathOf(this); + } + + private static class AllAction extends SetValueAction { + AllAction(@NotNull FilterPopupComponent component) { + super(ALL, component); + } + } + + protected static class SetValueAction extends DumbAwareAction { + + private final String myValue; + private final FilterPopupComponent myFilterComponent; + + SetValueAction(String value, FilterPopupComponent filterComponent) { + super(value); + myValue = value; + myFilterComponent = filterComponent; + } + + @Override + public void actionPerformed(AnActionEvent e) { + myFilterComponent.setValue(myValue); + myFilterComponent.applyFilters(); + } + } + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/UserFilterPopupComponent.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/UserFilterPopupComponent.java new file mode 100644 index 000000000000..0bbd6f92ba88 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/UserFilterPopupComponent.java @@ -0,0 +1,106 @@ +/* + * 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.ui.filter; + +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.ui.popup.JBPopup; +import com.intellij.openapi.ui.popup.JBPopupAdapter; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.ui.popup.LightweightWindowEvent; +import com.intellij.ui.components.JBTextField; +import com.intellij.vcs.log.data.VcsLogFilter; +import com.intellij.vcs.log.data.VcsLogUserFilter; +import org.jetbrains.annotations.Nullable; + +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +/** + * Show a popup to select a user or enter the user name. + */ +class UserFilterPopupComponent extends FilterPopupComponent { + + UserFilterPopupComponent(VcsLogClassicFilterUi filterUi) { + super(filterUi, "User"); + } + + @Override + protected ActionGroup createActionGroup() { + DefaultActionGroup group = new DefaultActionGroup(); + group.add(createAllAction()); + // TODO show recently selected users + group.add(new SelectUserAction()); + return group; + } + + @Nullable + @Override + protected VcsLogFilter getFilter() { + String value = getValue(); + if (value == ALL) { + return null; + } + return new VcsLogUserFilter(value); + } + + private class SelectUserAction extends DumbAwareAction { + + SelectUserAction() { + super("Select..."); + } + + @Override + public void actionPerformed(AnActionEvent e) { + final JBTextField textField = new JBTextField(10); + + final JBPopup popup = JBPopupFactory.getInstance().createComponentPopupBuilder(textField, textField) + .setCancelOnClickOutside(true) + .setCancelOnWindowDeactivation(true) + .setCancelKeyEnabled(true) + .setRequestFocus(true) + .createPopup(); + + textField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + popup.closeOk(e); + } + else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + popup.cancel(e); + } + } + }); + + popup.addListener(new JBPopupAdapter() { + @Override + public void onClosed(LightweightWindowEvent event) { + if (event.isOk()) { + String user = textField.getText(); + setValue(user); + applyFilters(); + } + } + }); + popup.showUnderneathOf(UserFilterPopupComponent.this); + textField.requestFocus(); + } + } + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogClassicFilterUi.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogClassicFilterUi.java new file mode 100644 index 000000000000..e8db11587089 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogClassicFilterUi.java @@ -0,0 +1,101 @@ +/* + * 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.ui.filter; + +import com.intellij.ui.SearchTextField; +import com.intellij.util.Function; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.ui.UIUtil; +import com.intellij.vcs.log.data.VcsLogFilter; +import com.intellij.vcs.log.data.VcsLogFilterer; +import com.intellij.vcs.log.ui.VcsLogUI; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + */ +public class VcsLogClassicFilterUi implements VcsLogFilterUi { + + @NotNull private final VcsLogFilterer myFilterer; + @NotNull private final JComponent myRootPanel; + @NotNull private final List myFilterPopupComponents; + @NotNull private final SearchTextField myTextFilter; + + public VcsLogClassicFilterUi(@NotNull VcsLogUI ui) { + myFilterer = ui.getFilterer(); + + JLabel filterCaption = new JLabel("Filter:"); + filterCaption.setForeground(UIUtil.isUnderDarcula() ? UIUtil.getLabelForeground() : UIUtil.getInactiveTextColor()); + + myTextFilter = new SearchTextField(); + myTextFilter.getTextEditor().addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + applyFilters(); + } + }); + FilterPopupComponent branchFilter = new BranchFilterPopupComponent(this, ui); + FilterPopupComponent userFilter = new UserFilterPopupComponent(this); + + myFilterPopupComponents = ContainerUtil.newArrayList(); + myFilterPopupComponents.add(branchFilter); + myFilterPopupComponents.add(userFilter); + + myRootPanel = new JPanel(); + myRootPanel.add(filterCaption); + myRootPanel.add(myTextFilter); + myRootPanel.add(branchFilter); + myRootPanel.add(userFilter); + } + + @NotNull + @Override + public JComponent getRootComponent() { + return myRootPanel; + } + + @NotNull + @Override + public Collection getFilters() { + List filters = getPopupFilters(); + if (!myTextFilter.getText().isEmpty()) { + filters.add(new VcsLogTextFilter(myTextFilter.getText())); + } + return filters; + } + + @NotNull + private List getPopupFilters() { + return new ArrayList(ContainerUtil.mapNotNull(myFilterPopupComponents, + new Function() { + @Override + public VcsLogFilter fun(FilterPopupComponent filterComponent) { + return filterComponent.getFilter(); + } + })); + } + + void applyFilters() { + myFilterer.applyFiltersAndUpdateUi(getFilters()); + } + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogFilterUi.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogFilterUi.java new file mode 100644 index 000000000000..8718a7f6bc54 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogFilterUi.java @@ -0,0 +1,28 @@ +package com.intellij.vcs.log.ui.filter; + +import com.intellij.vcs.log.data.VcsLogFilter; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.util.Collection; + +/** + * Graphical UI for filtering commits in the log. + * + * @author Kirill Likhodedov + */ +public interface VcsLogFilterUi { + + /** + * Returns the component which will be added to the Log toolbar. + */ + @NotNull + JComponent getRootComponent(); + + /** + * Returns the filters currently active, i.e. switched on by user. + */ + @NotNull + Collection getFilters(); + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogTextFilter.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogTextFilter.java new file mode 100644 index 000000000000..eebabd112215 --- /dev/null +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/filter/VcsLogTextFilter.java @@ -0,0 +1,35 @@ +/* + * 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.ui.filter; + +import com.intellij.vcs.log.VcsFullCommitDetails; +import com.intellij.vcs.log.data.VcsLogDetailsFilter; +import org.jetbrains.annotations.NotNull; + +class VcsLogTextFilter implements VcsLogDetailsFilter { + + @NotNull private final String myText; + + public VcsLogTextFilter(@NotNull String text) { + myText = text; + } + + @Override + public boolean matches(@NotNull VcsFullCommitDetails details) { + return details.getFullMessage().toLowerCase().contains(myText); + } + +} diff --git a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/MainFrame.java b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/MainFrame.java index d8dbbc985815..11f6f353bf7e 100644 --- a/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/MainFrame.java +++ b/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/frame/MainFrame.java @@ -6,10 +6,15 @@ import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; +import com.intellij.ui.JBColor; +import com.intellij.ui.SeparatorComponent; +import com.intellij.ui.SeparatorOrientation; import com.intellij.vcs.log.VcsLogSettings; import com.intellij.vcs.log.data.VcsLogDataHolder; import com.intellij.vcs.log.ui.GitLogIcons; +import com.intellij.vcs.log.ui.filter.VcsLogClassicFilterUi; import com.intellij.vcs.log.ui.VcsLogUI; +import com.intellij.vcs.log.ui.filter.VcsLogFilterUi; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -26,6 +31,7 @@ public class MainFrame { @NotNull private final JPanel myMainPanel; @NotNull private final ActiveSurface myActiveSurface; @NotNull private final VcsLogSettings mySettings; + @NotNull private final VcsLogFilterUi myFilterUi; public MainFrame(@NotNull VcsLogDataHolder logDataHolder, @NotNull VcsLogUI vcsLogUI, @NotNull Project project) { myLogDataHolder = logDataHolder; @@ -36,9 +42,15 @@ public class MainFrame { myActiveSurface = new ActiveSurface(logDataHolder, vcsLogUI, project); myActiveSurface.setupDetailsSplitter(mySettings.isShowDetails()); + JComponent toolbar = Box.createHorizontalBox(); + myFilterUi = new VcsLogClassicFilterUi(myUI); + toolbar.add(myFilterUi.getRootComponent()); + toolbar.add(new SeparatorComponent(JBColor.LIGHT_GRAY, SeparatorOrientation.VERTICAL)); + toolbar.add(createActionsToolbar()); + myMainPanel = new JPanel(); myMainPanel.setLayout(new BorderLayout()); - myMainPanel.add(createToolbar(), BorderLayout.NORTH); + myMainPanel.add(toolbar, BorderLayout.NORTH); myMainPanel.add(myActiveSurface, BorderLayout.CENTER); } @@ -46,7 +58,12 @@ public class MainFrame { return myActiveSurface.getGraphTable(); } - private JComponent createToolbar() { + @NotNull + public VcsLogFilterUi getFilterUi() { + return myFilterUi; + } + + private JComponent createActionsToolbar() { AnAction hideBranchesAction = new DumbAwareAction("Collapse linear branches", "Collapse linear branches", GitLogIcons.SPIDER) { @Override public void actionPerformed(AnActionEvent e) {