[log] Introduce filtering

I. Summary

We are able to filter graph by branches: not-wanted branches will be
just hidden with all their commits (unless they belong to a branch
that is to be shown).

We are not able to filter graph by other details yet: filtering, say,
by user can make "holes" in the graph, and we are not ready to
support this now.

Therefore, if user filters just by branch, we continue to show the
graph to him, but if he switches on a filter which is not supported
by graph (user, full-text), we turn the graph off by substituting the
TableModel with NoGraphTableModel which just shows commit subject in
the first column.

II. UI

Copy the same UI as it were in the old Git Log: a text field for
full-text search, popup menus for filtering by branch and by user.
Structure filter is not implemented yet.

The code of these classic-style filters seems to be tighly connected
with the old Log infrastructure => they are reimplemented from scratch.

Add a few niceties to popup controls: let them be focusable,
draw the focus border around them, make them keyboard friendly.

III. Details

VcsLogFilterer:
* VcsLogFilterer applies filters to the log. At first applies to the
  graph (to be able to filter by branch), then it applies other
  filters to details.
* The whole graph which was built can be filtered by branch. However
  details only of the recent commits are filtered by text/user, because
  details of older commits are not available.
  Get these details via VcsLogDataHolder#getTopCommitDetails().
* TODO: If there will be no or little results, we should load more
  commit details from the VCS. This will be implemented later.

VcsLogUI:
* Don't store the table model in VcsLogUI: it can be applied to the
  table at once.
* To keep filtering after refresh, apply filters instead of just
  creating new model.
This commit is contained in:
Kirill Likhodedov
2013-10-21 17:40:10 +04:00
parent 35879f85ec
commit 582fce20ba
15 changed files with 849 additions and 11 deletions
@@ -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<Hash> myMatchingHeads;
private final String myBranchName;
public VcsLogBranchFilter(Collection<VcsRef> allRefs, final String branchName) {
myBranchName = branchName;
myMatchingHeads = ContainerUtil.mapNotNull(allRefs, new Function<VcsRef, Hash>() {
@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;
}
}
@@ -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<Runnable> 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<VirtualFile, VcsLogProvider> 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<TimedVcsCommit> getTopCommits() {
return myLogData.getTopCommits();
}
/**
* Returns details of recent commits ordered in the "right way" (as in the log).
*/
@NotNull
public Collection<VcsFullCommitDetails> getTopCommitDetails() {
final Collection<TimedVcsCommit> topCommits = getTopCommits();
return ContainerUtil.mapNotNull(topCommits, new Function<TimedVcsCommit, VcsFullCommitDetails>() {
@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.
*
@@ -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);
}
@@ -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 {
}
@@ -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<Node,Boolean> ALL_NODES_VISIBLE = new Function<Node, Boolean>() {
@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<VcsLogFilter> filters) {
GraphModel graphModel = myLogDataHolder.getDataPack().getGraphModel();
List<VcsLogGraphFilter> graphFilters = ContainerUtil.findAll(filters, VcsLogGraphFilter.class);
List<VcsLogDetailsFilter> 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<Pair<VcsFullCommitDetails, VirtualFile>> 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<VcsLogGraphFilter> onGraphFilters) {
graphModel.setVisibleBranchesNodes(new Function<Node, Boolean>() {
@Override
public Boolean fun(final Node node) {
return !ContainerUtil.exists(onGraphFilters, new Condition<VcsLogGraphFilter>() {
@Override
public boolean value(VcsLogGraphFilter filter) {
return !filter.matches(node.getCommitHash());
}
});
}
});
}
private List<Pair<VcsFullCommitDetails, VirtualFile>> filterByDetails(final GraphModel graphModel,
final List<VcsLogDetailsFilter> detailsFilters) {
return ContainerUtil.mapNotNull(myLogDataHolder.getTopCommitDetails(),
new Function<VcsFullCommitDetails, Pair<VcsFullCommitDetails,VirtualFile>>() {
@Override
public Pair<VcsFullCommitDetails, VirtualFile> fun(final VcsFullCommitDetails details) {
boolean allFilterMatch = !ContainerUtil.exists(detailsFilters, new Condition<VcsLogDetailsFilter>() {
@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());
}
});
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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<VcsLogFilter> collectFilters() {
return myMainFrame.getFilterUi().getFilters();
}
}
@@ -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<VcsRef> allRefs = myUi.getLogDataHolder().getDataPack().getRefsModel().getBranches();
for (Map.Entry<VirtualFile, Collection<VcsRef>> entry : VcsLogUtil.groupRefsByRoot(allRefs).entrySet()) {
VirtualFile root = entry.getKey();
Collection<VcsRef> refs = entry.getValue();
VcsLogProvider provider = myUi.getLogDataHolder().getLogProvider(root);
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);
}
}
}
return actionGroup;
}
@Nullable
@Override
protected VcsLogFilter getFilter() {
String value = getValue();
return value == ALL
? null
: new VcsLogBranchFilter(myUi.getLogDataHolder().getDataPack().getRefsModel().getBranches(), value);
}
}
@@ -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();
}
}
}
@@ -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();
}
}
}
@@ -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<FilterPopupComponent> 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<VcsLogFilter> getFilters() {
List<VcsLogFilter> filters = getPopupFilters();
if (!myTextFilter.getText().isEmpty()) {
filters.add(new VcsLogTextFilter(myTextFilter.getText()));
}
return filters;
}
@NotNull
private List<VcsLogFilter> getPopupFilters() {
return new ArrayList<VcsLogFilter>(ContainerUtil.mapNotNull(myFilterPopupComponents,
new Function<FilterPopupComponent, VcsLogFilter>() {
@Override
public VcsLogFilter fun(FilterPopupComponent filterComponent) {
return filterComponent.getFilter();
}
}));
}
void applyFilters() {
myFilterer.applyFiltersAndUpdateUi(getFilters());
}
}
@@ -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<VcsLogFilter> getFilters();
}
@@ -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);
}
}
@@ -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) {