[log] IDEA-116242 Allow multiple users selection in the log filter

* Let FilterPopupComponent return several filters instead of one.
  Filter results will be united with OR.
  Clarify this fact in javadocs.

* UserFilterPopupComponent: open a multi-line editor popup to enter
  user names.
* Names can be separated with comma, | and \n.
* Since there can be several user names, persist not single users,
  but user groups. Use a custom class, because List<List<String>>
  seems to be not possible in PersistentStateComponent by default.
* Rename the State property to avoid conflicts when upgrading.
* Display user names like this: "John|Bob". If the name is too long,
  shorten with ellipsis; still display full names in tooltips.
This commit is contained in:
Kirill Likhodedov
2014-01-08 14:41:08 +04:00
parent be0561a3e0
commit ee40019912
8 changed files with 245 additions and 60 deletions
@@ -74,7 +74,11 @@ public interface VcsLogProvider {
void subscribeToRootRefreshEvents(@NotNull Collection<VirtualFile> roots, @NotNull VcsLogRefresher refresher);
/**
* Return commits with full details, which correspond to the given filters.
* <p>Return commits with full details, which correspond to the given filters.</p>
*
* <p>There can be several filters of a single type (e.g. several filters by users).<br/>
* Filters of different types are concatenated with {@code AND}, while filters of a single type are concatenated with {@code OR}.
* E.g.: (branch1 OR branch2) AND (user1 OR user2).</p>
*
* @param maxCount maximum number of commits to request from the VCS, or -1 for unlimited.
*/
@@ -19,14 +19,13 @@ import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.components.StoragePathMacros;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcs.log.VcsLogSettings;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.*;
/**
* Stores UI configuration based on user activity and preferences.
@@ -42,7 +41,7 @@ public class VcsLogUiProperties implements PersistentStateComponent<VcsLogUiProp
public static class State {
public boolean SHOW_DETAILS = true;
public Deque<String> RECENTLY_FILTERED_USERS = new ArrayDeque<String>();
public Deque<UserGroup> RECENTLY_FILTERED_USER_GROUPS = new ArrayDeque<UserGroup>();
}
@Nullable
@@ -68,19 +67,44 @@ public class VcsLogUiProperties implements PersistentStateComponent<VcsLogUiProp
myState.SHOW_DETAILS = showDetails;
}
public void addRecentlyFilteredUser(@NotNull String username) {
if (myState.RECENTLY_FILTERED_USERS.contains(username)) {
public void addRecentlyFilteredUserGroup(@NotNull List<String> usersInGroup) {
UserGroup group = new UserGroup();
group.users = usersInGroup;
if (myState.RECENTLY_FILTERED_USER_GROUPS.contains(group)) {
return;
}
myState.RECENTLY_FILTERED_USERS.addFirst(username);
if (myState.RECENTLY_FILTERED_USERS.size() > RECENTLY_FILTERED_USERS_AMOUNT) {
myState.RECENTLY_FILTERED_USERS.removeLast();
myState.RECENTLY_FILTERED_USER_GROUPS.addFirst(group);
if (myState.RECENTLY_FILTERED_USER_GROUPS.size() > RECENTLY_FILTERED_USERS_AMOUNT) {
myState.RECENTLY_FILTERED_USER_GROUPS.removeLast();
}
}
@NotNull
public List<String> getRecentlyFilteredUsers() {
return new ArrayList<String>(myState.RECENTLY_FILTERED_USERS);
public List<List<String>> getRecentlyFilteredUserGroups() {
return ContainerUtil.map2List(myState.RECENTLY_FILTERED_USER_GROUPS, new Function<UserGroup, List<String>>() {
@Override
public List<String> fun(UserGroup group) {
return group.users;
}
});
}
public static class UserGroup {
public List<String> users = new ArrayList<String>();
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserGroup group = (UserGroup)o;
if (!users.equals(group.users)) return false;
return true;
}
@Override
public int hashCode() {
return users.hashCode();
}
}
}
@@ -124,11 +124,10 @@ class BranchFilterPopupComponent extends FilterPopupComponent {
@Nullable
@Override
protected VcsLogFilter getFilter() {
protected Collection<VcsLogFilter> getFilters() {
String value = getValue();
return value == ALL
? null
: new VcsLogBranchFilter(myUi.getLogDataHolder().getDataPack().getRefsModel().getBranches(), value);
Collection<VcsRef> allBranches = myUi.getLogDataHolder().getDataPack().getRefsModel().getBranches();
return value == ALL ? null : Collections.<VcsLogFilter>singleton(new VcsLogBranchFilter(allBranches, value));
}
}
@@ -29,6 +29,8 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
class DateFilterPopupComponent extends FilterPopupComponent {
@@ -66,8 +68,8 @@ class DateFilterPopupComponent extends FilterPopupComponent {
@Nullable
@Override
protected VcsLogFilter getFilter() {
return myAfter == null && myBefore == null ? null : new VcsLogDateFilter(myAfter, myBefore);
protected Collection<VcsLogFilter> getFilters() {
return myAfter == null && myBefore == null ? null : Collections.<VcsLogFilter>singleton(new VcsLogDateFilter(myAfter, myBefore));
}
private void setOnlyAfter(Date after) {
@@ -33,6 +33,7 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.event.*;
import java.util.Collection;
/**
* Base class for components which allow to set up filter for the VCS Log, by displaying a popup with available choices.
@@ -90,13 +91,19 @@ abstract class FilterPopupComponent extends JPanel {
protected abstract ActionGroup createActionGroup();
/**
* Return the filter currently selected by this component.
* Return the filters (which would be concatenated with OR) currently selected via this component.</br>
* Or return null if no filters are selected via this component.
*/
@Nullable
protected abstract VcsLogFilter getFilter();
protected abstract Collection<VcsLogFilter> getFilters();
protected void setValue(@NotNull String newValue) {
myFilterValueLabel.setText(newValue);
setValue(newValue, newValue);
}
protected void setValue(@NotNull String value, @NotNull String tooltip) {
myFilterValueLabel.setText(value);
setToolTipText(tooltip);
}
@NotNull
@@ -52,8 +52,8 @@ class StructureFilterPopupComponent extends FilterPopupComponent {
@Nullable
@Override
protected VcsLogFilter getFilter() {
return getValue() == ALL || myFiles.isEmpty() ? null : new VcsLogStructureFilter(myFiles, myRoots);
protected Collection<VcsLogFilter> getFilters() {
return getValue() == ALL || myFiles.isEmpty() ? null : Collections.<VcsLogFilter>singleton(new VcsLogStructureFilter(myFiles, myRoots));
}
private void setValue(@NotNull Collection<VirtualFile> files) {
@@ -15,24 +15,36 @@
*/
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.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.fileTypes.FileTypes;
import com.intellij.openapi.keymap.KeymapUtil;
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.LightweightWindowEvent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.spellchecker.ui.SpellCheckingEditorCustomization;
import com.intellij.ui.EditorCustomization;
import com.intellij.ui.EditorTextField;
import com.intellij.ui.EditorTextFieldProvider;
import com.intellij.ui.SoftWrapsEditorCustomization;
import com.intellij.util.Function;
import com.intellij.util.TextFieldCompletionProviderDumbAware;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcs.log.VcsLogFilter;
import com.intellij.vcs.log.VcsUser;
import com.intellij.vcs.log.data.VcsLogDataHolder;
import com.intellij.vcs.log.data.VcsLogUiProperties;
import com.intellij.vcs.log.data.VcsLogUserFilter;
import com.intellij.vcs.log.ui.PopupWithTextFieldWithAutoCompletion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import java.awt.*;
import java.util.*;
import java.util.List;
/**
@@ -41,9 +53,13 @@ import java.util.List;
class UserFilterPopupComponent extends FilterPopupComponent {
private static final String ME = "me";
private static final char[] USERS_SEPARATORS = { ',', '|', '\n' };
private final VcsLogDataHolder myDataHolder;
private final VcsLogUiProperties myUiProperties;
@Nullable private Collection<String> mySelectedUsers;
UserFilterPopupComponent(VcsLogClassicFilterUi filterUi, VcsLogDataHolder dataHolder, VcsLogUiProperties uiProperties) {
super(filterUi, "User");
myDataHolder = dataHolder;
@@ -52,15 +68,22 @@ class UserFilterPopupComponent extends FilterPopupComponent {
@Override
protected ActionGroup createActionGroup() {
DefaultActionGroup group = new DefaultActionGroup();
group.add(createAllAction());
group.add(new SetValueAction(ME, this));
AnAction allAction = new DumbAwareAction(ALL) {
@Override
public void actionPerformed(AnActionEvent e) {
apply(null, ALL, ALL);
}
};
List<String> recentlyFilteredUsers = myUiProperties.getRecentlyFilteredUsers();
DefaultActionGroup group = new DefaultActionGroup();
group.add(allAction);
group.add(new UserAction(Collections.singleton(ME)));
List<List<String>> recentlyFilteredUsers = myUiProperties.getRecentlyFilteredUserGroups();
if (!recentlyFilteredUsers.isEmpty()) {
group.addSeparator("Recently searched");
for (String recentUser : recentlyFilteredUsers) {
group.add(new SetValueAction(recentUser, this));
for (List<String> recentGroup : recentlyFilteredUsers) {
group.add(new UserAction(recentGroup));
}
}
group.addSeparator();
@@ -70,16 +93,50 @@ class UserFilterPopupComponent extends FilterPopupComponent {
@Nullable
@Override
protected VcsLogFilter getFilter() {
String value = getValue();
if (value == ALL) {
protected Collection<VcsLogFilter> getFilters() {
if (mySelectedUsers == null) {
return null;
}
if (value == ME) {
return new VcsLogUserFilter.Me(myDataHolder.getCurrentUser());
myUiProperties.addRecentlyFilteredUserGroup(new ArrayList<String>(mySelectedUsers));
return ContainerUtil.map(mySelectedUsers, new Function<String, VcsLogFilter>() {
@Override
public VcsLogFilter fun(String name) {
return name == ME ? new VcsLogUserFilter.Me(myDataHolder.getCurrentUser()) : new VcsLogUserFilter.ByName(name);
}
});
}
private void apply(Collection<String> users, String text, String tooltip) {
mySelectedUsers = users;
applyFilters();
setValue(text, tooltip);
}
@NotNull
private static String displayableText(@NotNull Collection<String> users) {
if (users.size() == 1) {
return users.iterator().next();
}
return StringUtil.shortenTextWithEllipsis(StringUtil.join(users, "|"), 30, 0, true);
}
@NotNull
private static String tooltip(@NotNull Collection<String> users) {
return StringUtil.join(users, ", ");
}
private class UserAction extends DumbAwareAction {
@NotNull private final Collection<String> myUsers;
UserAction(@NotNull Collection<String> users) {
super(displayableText(users), tooltip(users), null);
myUsers = users;
}
@Override
public void actionPerformed(AnActionEvent e) {
apply(myUsers, displayableText(myUsers), tooltip(myUsers));
}
myUiProperties.addRecentlyFilteredUser(value);
return new VcsLogUserFilter.ByName(value);
}
private class SelectUserAction extends DumbAwareAction {
@@ -90,6 +147,11 @@ class UserFilterPopupComponent extends FilterPopupComponent {
@Override
public void actionPerformed(AnActionEvent e) {
Project project = e.getProject();
if (project == null) {
return;
}
Collection<String> users = ContainerUtil.map(myDataHolder.getAllUsers(), new Function<VcsUser, String>() {
@Override
public String fun(VcsUser user) {
@@ -97,21 +159,105 @@ class UserFilterPopupComponent extends FilterPopupComponent {
}
});
final PopupWithTextFieldWithAutoCompletion textField = new PopupWithTextFieldWithAutoCompletion(e.getProject(), users);
JBPopup popup = textField.createPopup();
final MultilinePopupBuilder popupBuilder = new MultilinePopupBuilder(project, users);
JBPopup popup = popupBuilder.createPopup();
popup.addListener(new JBPopupAdapter() {
@Override
public void onClosed(LightweightWindowEvent event) {
if (event.isOk()) {
String user = textField.getText();
setValue(user);
applyFilters();
final String userText = popupBuilder.getText().trim();
Collection<String> selectedUsers = ContainerUtil.toCollection(StringUtil.tokenize(userText, new String(USERS_SEPARATORS)));
apply(selectedUsers, displayableText(selectedUsers), tooltip(selectedUsers));
}
}
});
popup.showUnderneathOf(UserFilterPopupComponent.this);
}
}
private static class MultilinePopupBuilder {
private final EditorTextField myTextField;
MultilinePopupBuilder(@NotNull Project project, @NotNull final Collection<String> users) {
myTextField = createTextField(project);
new UsersCompletionProvider(users).apply(myTextField);
}
@NotNull
private static EditorTextField createTextField(@NotNull Project project) {
final EditorTextFieldProvider service = ServiceManager.getService(project, EditorTextFieldProvider.class);
List<EditorCustomization> features = Arrays.<EditorCustomization>asList(SoftWrapsEditorCustomization.ENABLED,
SpellCheckingEditorCustomization.DISABLED);
EditorTextField textField = service.getEditorField(FileTypes.PLAIN_TEXT.getLanguage(), project, features);
textField.setBorder(new CompoundBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2), textField.getBorder()));
textField.setOneLineMode(false);
return textField;
}
@NotNull
JBPopup createPopup() {
JPanel panel = new JPanel(new BorderLayout());
panel.add(myTextField, BorderLayout.CENTER);
ComponentPopupBuilder builder = JBPopupFactory.getInstance().createComponentPopupBuilder(panel, myTextField)
.setCancelOnClickOutside(true)
.setAdText(KeymapUtil.getShortcutsText(CommonShortcuts.CTRL_ENTER.getShortcuts()) + " to finish")
.setMovable(true)
.setRequestFocus(true)
.setResizable(true)
.setMayBeParent(true);
final JBPopup popup = builder.createPopup();
popup.setMinimumSize(new Dimension(200, 90));
AnAction okAction = new DumbAwareAction() {
@Override
public void actionPerformed(AnActionEvent e) {
unregisterCustomShortcutSet(popup.getContent());
popup.closeOk(e.getInputEvent());
}
};
okAction.registerCustomShortcutSet(CommonShortcuts.CTRL_ENTER, popup.getContent());
return popup;
}
String getText() {
return myTextField.getText();
}
private static class UsersCompletionProvider extends TextFieldCompletionProviderDumbAware {
@NotNull private final Collection<String> myUsers;
UsersCompletionProvider(@NotNull Collection<String> users) {
super(true);
myUsers = users;
}
@NotNull
@Override
protected String getPrefix(@NotNull String currentTextPrefix) {
final int separatorPosition = lastSeparatorPosition(currentTextPrefix);
return separatorPosition == -1 ? currentTextPrefix : currentTextPrefix.substring(separatorPosition + 1).trim();
}
private static int lastSeparatorPosition(@NotNull String text) {
int lastPosition = -1;
for (char separator : USERS_SEPARATORS) {
int lio = text.lastIndexOf(separator);
if (lio > lastPosition) {
lastPosition = lio;
}
}
return lastPosition;
}
@Override
protected void addCompletionVariants(@NotNull String text, int offset, @NotNull String prefix,
@NotNull CompletionResultSet result) {
result.addLookupAdvertisement("Select one or more users separated with comma, | or new lines");
for (String completionVariant : myUsers) {
final LookupElementBuilder element = LookupElementBuilder.create(completionVariant);
result.addElement(element.withLookupString(completionVariant.toLowerCase()));
}
}
}
}
}
@@ -15,12 +15,14 @@
*/
package com.intellij.vcs.log.ui.filter;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.actionSystem.ex.CustomComponentAction;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.ui.SearchTextField;
import com.intellij.ui.SearchTextFieldWithStoredHistory;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcs.log.VcsLogFilter;
@@ -91,13 +93,14 @@ public class VcsLogClassicFilterUi implements VcsLogFilterUi {
@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();
}
}));
List<VcsLogFilter> filters = new ArrayList<VcsLogFilter>();
for (FilterPopupComponent popupComponent : myFilterPopupComponents) {
Collection<VcsLogFilter> popupFilters = popupComponent.getFilters();
if (popupFilters != null) {
filters.addAll(popupFilters);
}
}
return filters;
}
void applyFilters() {