2 * Copyright 2000-2013 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.vcs.log.ui.filter;
18 import com.intellij.openapi.actionSystem.*;
19 import com.intellij.openapi.project.DumbAware;
20 import com.intellij.openapi.project.DumbAwareAction;
21 import com.intellij.openapi.project.Project;
22 import com.intellij.openapi.util.SystemInfo;
23 import com.intellij.openapi.util.text.StringUtil;
24 import com.intellij.openapi.vcs.FilePath;
25 import com.intellij.openapi.vfs.VirtualFile;
26 import com.intellij.ui.SizedIcon;
27 import com.intellij.ui.popup.KeepingPopupOpenAction;
28 import com.intellij.util.NotNullFunction;
29 import com.intellij.util.PlatformIcons;
30 import com.intellij.util.containers.ContainerUtil;
31 import com.intellij.util.ui.ColorIcon;
32 import com.intellij.util.ui.EmptyIcon;
33 import com.intellij.util.ui.JBUI;
34 import com.intellij.vcs.log.VcsLogDataPack;
35 import com.intellij.vcs.log.VcsLogRootFilter;
36 import com.intellij.vcs.log.VcsLogRootFilterImpl;
37 import com.intellij.vcs.log.VcsLogStructureFilter;
38 import com.intellij.vcs.log.data.VcsLogStructureFilterImpl;
39 import com.intellij.vcs.log.impl.VcsLogUtil;
40 import com.intellij.vcs.log.ui.VcsLogColorManager;
41 import com.intellij.vcs.log.ui.frame.VcsLogGraphTable;
42 import org.intellij.lang.annotations.JdkConstants;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
48 import java.awt.event.InputEvent;
49 import java.awt.event.KeyEvent;
51 import java.util.List;
53 class StructureFilterPopupComponent extends FilterPopupComponent<VcsLogFileFilter> {
54 private static final int FILTER_LABEL_LENGTH = 30;
55 private static final int CHECKBOX_ICON_SIZE = 15;
56 public static final FileByNameComparator FILE_BY_NAME_COMPARATOR = new FileByNameComparator();
57 public static final FilePathByPathComparator FILE_PATH_BY_PATH_COMPARATOR = new FilePathByPathComparator();
59 @NotNull private final VcsLogColorManager myColorManager;
60 @NotNull private final FixedSizeQueue<VcsLogStructureFilter> myHistory = new FixedSizeQueue<>(5);
62 public StructureFilterPopupComponent(@NotNull FilterModel<VcsLogFileFilter> filterModel, @NotNull VcsLogColorManager colorManager) {
63 super("Paths", filterModel);
64 myColorManager = colorManager;
69 protected String getText(@NotNull VcsLogFileFilter filter) {
70 Collection<VirtualFile> roots = filter.getRootFilter() == null ? getAllRoots() : filter.getRootFilter().getRoots();
71 Collection<FilePath> files =
72 filter.getStructureFilter() == null ? Collections.<FilePath>emptySet() : filter.getStructureFilter().getFiles();
73 Collection<VirtualFile> visibleRoots =
74 VcsLogUtil.getAllVisibleRoots(getAllRoots(), filter.getRootFilter(), filter.getStructureFilter());
76 if (files.isEmpty()) {
77 return getTextFromRoots(roots, visibleRoots.size() == getAllRoots().size());
80 return getTextFromFilePaths(files, "folders", files.isEmpty());
85 private static String getTextFromRoots(@NotNull Collection<VirtualFile> files,
87 return getText(files, "roots", FILE_BY_NAME_COMPARATOR, VirtualFile::getName, full);
91 private static String getTextFromFilePaths(@NotNull Collection<FilePath> files,
92 @NotNull String category,
94 return getText(files, category, FILE_PATH_BY_PATH_COMPARATOR,
95 file -> StringUtil.shortenPathWithEllipsis(file.getPresentableUrl(), FILTER_LABEL_LENGTH), full);
99 private static <F> String getText(@NotNull Collection<F> files,
100 @NotNull String category,
101 @NotNull Comparator<F> comparator,
102 @NotNull NotNullFunction<F, String> getText,
107 else if (files.isEmpty()) {
108 return "No " + category;
111 F firstFile = Collections.min(files, comparator);
112 String firstFileName = getText.fun(firstFile);
113 if (files.size() == 1) {
114 return firstFileName;
117 return firstFileName + " + " + (files.size() - 1);
124 protected String getToolTip(@NotNull VcsLogFileFilter filter) {
125 return getToolTip(filter.getRootFilter() == null ? getAllRoots() : filter.getRootFilter().getRoots(),
126 filter.getStructureFilter() == null ? Collections.emptySet() : filter.getStructureFilter().getFiles());
130 private String getToolTip(@NotNull Collection<VirtualFile> roots, @NotNull Collection<FilePath> files) {
132 if (roots.isEmpty()) {
133 tooltip += "No Roots Selected";
135 else if (roots.size() != getAllRoots().size()) {
136 tooltip += "Roots:\n" + getTooltipTextForRoots(roots);
138 if (!files.isEmpty()) {
139 if (!tooltip.isEmpty()) tooltip += "\n";
140 tooltip += "Folders:\n" + getTooltipTextForFilePaths(files);
146 private static String getTooltipTextForRoots(@NotNull Collection<VirtualFile> files) {
147 return getTooltipTextForFiles(files, FILE_BY_NAME_COMPARATOR, VirtualFile::getName);
151 private static String getTooltipTextForFilePaths(@NotNull Collection<FilePath> files) {
152 return getTooltipTextForFiles(files, FILE_PATH_BY_PATH_COMPARATOR, FilePath::getPresentableUrl);
156 private static <F> String getTooltipTextForFiles(@NotNull Collection<F> files,
157 @NotNull Comparator<F> comparator,
158 @NotNull NotNullFunction<F, String> getText) {
159 List<F> filesToDisplay = ContainerUtil.sorted(files, comparator);
160 if (files.size() > 10) {
161 filesToDisplay = filesToDisplay.subList(0, 10);
163 String tooltip = StringUtil.join(filesToDisplay, getText, "\n");
164 if (files.size() > 10) {
171 protected ActionGroup createActionGroup() {
172 Set<VirtualFile> roots = getAllRoots();
174 List<AnAction> rootActions = new ArrayList<>();
175 if (myColorManager.isMultipleRoots()) {
176 for (VirtualFile root : ContainerUtil.sorted(roots, FILE_BY_NAME_COMPARATOR)) {
177 rootActions.add(new SelectVisibleRootAction(root));
180 List<AnAction> structureActions = new ArrayList<>();
181 for (VcsLogStructureFilter filter : myHistory) {
182 structureActions.add(new SelectFromHistoryAction(filter));
185 if (roots.size() > 15) {
186 return new DefaultActionGroup(createAllAction(), new SelectFoldersAction(),
187 new Separator("Recent"), new DefaultActionGroup(structureActions),
188 new Separator("Roots"), new DefaultActionGroup(rootActions));
191 return new DefaultActionGroup(createAllAction(), new SelectFoldersAction(),
192 new Separator("Roots"), new DefaultActionGroup(rootActions),
193 new Separator("Recent"), new DefaultActionGroup(structureActions));
197 private Set<VirtualFile> getAllRoots() {
198 return myFilterModel.getDataPack().getLogProviders().keySet();
201 private boolean isVisible(@NotNull VirtualFile root) {
202 VcsLogFileFilter filter = myFilterModel.getFilter();
203 if (filter != null && filter.getRootFilter() != null) {
204 return filter.getRootFilter().getRoots().contains(root);
211 private void setVisible(@NotNull VirtualFile root, boolean visible) {
212 Set<VirtualFile> roots = getAllRoots();
214 VcsLogFileFilter previousFilter = myFilterModel.getFilter();
215 VcsLogRootFilter rootFilter = previousFilter != null ? previousFilter.getRootFilter() : null;
217 Collection<VirtualFile> visibleRoots;
218 if (rootFilter == null) {
220 visibleRoots = roots;
223 visibleRoots = ContainerUtil.subtract(roots, Collections.singleton(root));
228 visibleRoots = ContainerUtil.union(new HashSet<>(rootFilter.getRoots()), Collections.singleton(root));
231 visibleRoots = ContainerUtil.subtract(rootFilter.getRoots(), Collections.singleton(root));
234 myFilterModel.setFilter(new VcsLogFileFilter(null, new VcsLogRootFilterImpl(visibleRoots)));
237 private void setVisibleOnly(@NotNull VirtualFile root) {
238 myFilterModel.setFilter(new VcsLogFileFilter(null, new VcsLogRootFilterImpl(Collections.singleton(root))));
242 private static String getStructureActionText(@NotNull VcsLogStructureFilter filter) {
243 return getTextFromFilePaths(filter.getFiles(), "items", filter.getFiles().isEmpty());
246 private static class FileByNameComparator implements Comparator<VirtualFile> {
248 public int compare(VirtualFile o1, VirtualFile o2) {
249 return o1.getName().compareTo(o2.getName());
253 private static class FilePathByPathComparator implements Comparator<FilePath> {
255 public int compare(FilePath o1, FilePath o2) {
256 return o1.getPresentableUrl().compareTo(o2.getPresentableUrl());
260 private class SelectVisibleRootAction extends ToggleAction implements DumbAware, KeepingPopupOpenAction {
261 @NotNull private final CheckboxColorIcon myIcon;
262 @NotNull private final VirtualFile myRoot;
264 private SelectVisibleRootAction(@NotNull VirtualFile root) {
265 super(root.getName(), root.getPresentableUrl(), null);
267 myIcon = new CheckboxColorIcon(JBUI.scale(CHECKBOX_ICON_SIZE), VcsLogGraphTable.getRootBackgroundColor(myRoot, myColorManager));
268 getTemplatePresentation().setIcon(EmptyIcon.create(JBUI.scale(CHECKBOX_ICON_SIZE))); // see PopupFactoryImpl.calcMaxIconSize
272 public boolean isSelected(AnActionEvent e) {
273 return isVisible(myRoot);
277 public void setSelected(AnActionEvent e, boolean state) {
279 setVisibleOnly(myRoot);
282 if ((e.getModifiers() & getMask()) != 0) {
283 setVisibleOnly(myRoot);
286 setVisible(myRoot, state);
291 @JdkConstants.InputEventMask
292 private int getMask() {
293 return SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK;
297 public void update(@NotNull AnActionEvent e) {
301 e.getPresentation().setIcon(myIcon);
302 e.getPresentation().putClientProperty(TOOL_TIP_TEXT_KEY, KeyEvent.getKeyModifiersText(getMask()) +
303 "+Click to see only \"" +
304 e.getPresentation().getText() +
308 private void updateIcon() {
309 myIcon.prepare(isVisible(myRoot) && isEnabled());
312 private boolean isEnabled() {
313 return myFilterModel.getFilter() == null || (myFilterModel.getFilter().getStructureFilter() == null);
317 private static class CheckboxColorIcon extends ColorIcon {
318 private final int mySize;
319 private boolean mySelected = false;
320 private SizedIcon mySizedIcon;
322 public CheckboxColorIcon(int size, @NotNull Color color) {
325 mySizedIcon = new SizedIcon(PlatformIcons.CHECK_ICON_SMALL, mySize, mySize);
328 public void prepare(boolean selected) {
329 mySelected = selected;
333 public void paintIcon(Component component, Graphics g, int i, int j) {
334 super.paintIcon(component, g, i, j);
336 mySizedIcon.paintIcon(component, g, i, j);
341 private class SelectFoldersAction extends DumbAwareAction {
342 public static final String STRUCTURE_FILTER_TEXT = "Select Folders...";
344 SelectFoldersAction() {
345 super(STRUCTURE_FILTER_TEXT);
349 public void actionPerformed(@NotNull AnActionEvent e) {
350 Project project = e.getRequiredData(CommonDataKeys.PROJECT);
351 VcsLogDataPack dataPack = myFilterModel.getDataPack();
352 VcsLogFileFilter filter = myFilterModel.getFilter();
354 Collection<VirtualFile> files;
355 if (filter == null || filter.getStructureFilter() == null) {
356 files = Collections.emptySet();
359 // for now, ignoring non-existing paths
360 files = ContainerUtil.mapNotNull(filter.getStructureFilter().getFiles(), FilePath::getVirtualFile);
363 VcsStructureChooser chooser = new VcsStructureChooser(project, "Select Files or Folders to Filter by", files,
364 new ArrayList<>(dataPack.getLogProviders().keySet()));
365 if (chooser.showAndGet()) {
366 VcsLogStructureFilterImpl structureFilter = new VcsLogStructureFilterImpl(new HashSet<VirtualFile>(chooser.getSelectedFiles()));
367 myFilterModel.setFilter(new VcsLogFileFilter(structureFilter, null));
368 myHistory.add(structureFilter);
373 public void update(AnActionEvent e) {
374 e.getPresentation().setEnabledAndVisible(e.getProject() != null);
378 private class SelectFromHistoryAction extends ToggleAction {
379 @NotNull private final VcsLogStructureFilter myFilter;
380 @NotNull private final Icon myIcon;
381 @NotNull private final Icon myEmptyIcon;
383 private SelectFromHistoryAction(@NotNull VcsLogStructureFilter filter) {
384 super(getStructureActionText(filter), getTooltipTextForFilePaths(filter.getFiles()).replace("\n", " "), null);
386 myIcon = new SizedIcon(PlatformIcons.CHECK_ICON_SMALL, CHECKBOX_ICON_SIZE, CHECKBOX_ICON_SIZE);
387 myEmptyIcon = EmptyIcon.create(CHECKBOX_ICON_SIZE);
391 public boolean isSelected(AnActionEvent e) {
392 return myFilterModel.getFilter() != null && myFilterModel.getFilter().getStructureFilter() == myFilter;
396 public void setSelected(AnActionEvent e, boolean state) {
397 myFilterModel.setFilter(new VcsLogFileFilter(myFilter, null));
401 public void update(@NotNull AnActionEvent e) {
404 Presentation presentation = e.getPresentation();
406 presentation.setIcon(myIcon);
409 presentation.setIcon(myEmptyIcon);
414 private static class FixedSizeQueue<T> implements Iterable<T> {
415 @NotNull private final LinkedList<T> myQueue = new LinkedList<>();
416 private final int maxSize;
418 public FixedSizeQueue(int maxSize) {
419 this.maxSize = maxSize;
424 public Iterator<T> iterator() {
425 return ContainerUtil.reverse(myQueue).iterator();
428 public void add(T t) {
430 if (myQueue.size() > maxSize) {