[vcs-log] scale checkbox icon size
[idea/community.git] / platform / vcs-log / impl / src / com / intellij / vcs / log / ui / filter / StructureFilterPopupComponent.java
1 /*
2  * Copyright 2000-2013 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.vcs.log.ui.filter;
17
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;
45
46 import javax.swing.*;
47 import java.awt.*;
48 import java.awt.event.InputEvent;
49 import java.awt.event.KeyEvent;
50 import java.util.*;
51 import java.util.List;
52
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();
58
59   @NotNull private final VcsLogColorManager myColorManager;
60   @NotNull private final FixedSizeQueue<VcsLogStructureFilter> myHistory = new FixedSizeQueue<>(5);
61
62   public StructureFilterPopupComponent(@NotNull FilterModel<VcsLogFileFilter> filterModel, @NotNull VcsLogColorManager colorManager) {
63     super("Paths", filterModel);
64     myColorManager = colorManager;
65   }
66
67   @NotNull
68   @Override
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());
75
76     if (files.isEmpty()) {
77       return getTextFromRoots(roots, visibleRoots.size() == getAllRoots().size());
78     }
79     else {
80       return getTextFromFilePaths(files, "folders", files.isEmpty());
81     }
82   }
83
84   @NotNull
85   private static String getTextFromRoots(@NotNull Collection<VirtualFile> files,
86                                          boolean full) {
87     return getText(files, "roots", FILE_BY_NAME_COMPARATOR, VirtualFile::getName, full);
88   }
89
90   @NotNull
91   private static String getTextFromFilePaths(@NotNull Collection<FilePath> files,
92                                              @NotNull String category,
93                                              boolean full) {
94     return getText(files, category, FILE_PATH_BY_PATH_COMPARATOR,
95                    file -> StringUtil.shortenPathWithEllipsis(file.getPresentableUrl(), FILTER_LABEL_LENGTH), full);
96   }
97
98   @NotNull
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,
103                                     boolean full) {
104     if (full) {
105       return ALL;
106     }
107     else if (files.isEmpty()) {
108       return "No " + category;
109     }
110     else {
111       F firstFile = Collections.min(files, comparator);
112       String firstFileName = getText.fun(firstFile);
113       if (files.size() == 1) {
114         return firstFileName;
115       }
116       else {
117         return firstFileName + " + " + (files.size() - 1);
118       }
119     }
120   }
121
122   @Nullable
123   @Override
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());
127   }
128
129   @NotNull
130   private String getToolTip(@NotNull Collection<VirtualFile> roots, @NotNull Collection<FilePath> files) {
131     String tooltip = "";
132     if (roots.isEmpty()) {
133       tooltip += "No Roots Selected";
134     }
135     else if (roots.size() != getAllRoots().size()) {
136       tooltip += "Roots:\n" + getTooltipTextForRoots(roots);
137     }
138     if (!files.isEmpty()) {
139       if (!tooltip.isEmpty()) tooltip += "\n";
140       tooltip += "Folders:\n" + getTooltipTextForFilePaths(files);
141     }
142     return tooltip;
143   }
144
145   @NotNull
146   private static String getTooltipTextForRoots(@NotNull Collection<VirtualFile> files) {
147     return getTooltipTextForFiles(files, FILE_BY_NAME_COMPARATOR, VirtualFile::getName);
148   }
149
150   @NotNull
151   private static String getTooltipTextForFilePaths(@NotNull Collection<FilePath> files) {
152     return getTooltipTextForFiles(files, FILE_PATH_BY_PATH_COMPARATOR, FilePath::getPresentableUrl);
153   }
154
155   @NotNull
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);
162     }
163     String tooltip = StringUtil.join(filesToDisplay, getText, "\n");
164     if (files.size() > 10) {
165       tooltip += "\n...";
166     }
167     return tooltip;
168   }
169
170   @Override
171   protected ActionGroup createActionGroup() {
172     Set<VirtualFile> roots = getAllRoots();
173
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));
178       }
179     }
180     List<AnAction> structureActions = new ArrayList<>();
181     for (VcsLogStructureFilter filter : myHistory) {
182       structureActions.add(new SelectFromHistoryAction(filter));
183     }
184
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));
189     }
190     else {
191       return new DefaultActionGroup(createAllAction(), new SelectFoldersAction(),
192                                     new Separator("Roots"), new DefaultActionGroup(rootActions),
193                                     new Separator("Recent"), new DefaultActionGroup(structureActions));
194     }
195   }
196
197   private Set<VirtualFile> getAllRoots() {
198     return myFilterModel.getDataPack().getLogProviders().keySet();
199   }
200
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);
205     }
206     else {
207       return true;
208     }
209   }
210
211   private void setVisible(@NotNull VirtualFile root, boolean visible) {
212     Set<VirtualFile> roots = getAllRoots();
213
214     VcsLogFileFilter previousFilter = myFilterModel.getFilter();
215     VcsLogRootFilter rootFilter = previousFilter != null ? previousFilter.getRootFilter() : null;
216
217     Collection<VirtualFile> visibleRoots;
218     if (rootFilter == null) {
219       if (visible) {
220         visibleRoots = roots;
221       }
222       else {
223         visibleRoots = ContainerUtil.subtract(roots, Collections.singleton(root));
224       }
225     }
226     else {
227       if (visible) {
228         visibleRoots = ContainerUtil.union(new HashSet<>(rootFilter.getRoots()), Collections.singleton(root));
229       }
230       else {
231         visibleRoots = ContainerUtil.subtract(rootFilter.getRoots(), Collections.singleton(root));
232       }
233     }
234     myFilterModel.setFilter(new VcsLogFileFilter(null, new VcsLogRootFilterImpl(visibleRoots)));
235   }
236
237   private void setVisibleOnly(@NotNull VirtualFile root) {
238     myFilterModel.setFilter(new VcsLogFileFilter(null, new VcsLogRootFilterImpl(Collections.singleton(root))));
239   }
240
241   @NotNull
242   private static String getStructureActionText(@NotNull VcsLogStructureFilter filter) {
243     return getTextFromFilePaths(filter.getFiles(), "items", filter.getFiles().isEmpty());
244   }
245
246   private static class FileByNameComparator implements Comparator<VirtualFile> {
247     @Override
248     public int compare(VirtualFile o1, VirtualFile o2) {
249       return o1.getName().compareTo(o2.getName());
250     }
251   }
252
253   private static class FilePathByPathComparator implements Comparator<FilePath> {
254     @Override
255     public int compare(FilePath o1, FilePath o2) {
256       return o1.getPresentableUrl().compareTo(o2.getPresentableUrl());
257     }
258   }
259
260   private class SelectVisibleRootAction extends ToggleAction implements DumbAware, KeepingPopupOpenAction {
261     @NotNull private final CheckboxColorIcon myIcon;
262     @NotNull private final VirtualFile myRoot;
263
264     private SelectVisibleRootAction(@NotNull VirtualFile root) {
265       super(root.getName(), root.getPresentableUrl(), null);
266       myRoot = root;
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
269     }
270
271     @Override
272     public boolean isSelected(AnActionEvent e) {
273       return isVisible(myRoot);
274     }
275
276     @Override
277     public void setSelected(AnActionEvent e, boolean state) {
278       if (!isEnabled()) {
279         setVisibleOnly(myRoot);
280       }
281       else {
282         if ((e.getModifiers() & getMask()) != 0) {
283           setVisibleOnly(myRoot);
284         }
285         else {
286           setVisible(myRoot, state);
287         }
288       }
289     }
290
291     @JdkConstants.InputEventMask
292     private int getMask() {
293       return SystemInfo.isMac ? InputEvent.META_MASK : InputEvent.CTRL_MASK;
294     }
295
296     @Override
297     public void update(@NotNull AnActionEvent e) {
298       super.update(e);
299
300       updateIcon();
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() +
305                                                                "\"");
306     }
307
308     private void updateIcon() {
309       myIcon.prepare(isVisible(myRoot) && isEnabled());
310     }
311
312     private boolean isEnabled() {
313       return myFilterModel.getFilter() == null || (myFilterModel.getFilter().getStructureFilter() == null);
314     }
315   }
316
317   private static class CheckboxColorIcon extends ColorIcon {
318     private final int mySize;
319     private boolean mySelected = false;
320     private SizedIcon mySizedIcon;
321
322     public CheckboxColorIcon(int size, @NotNull Color color) {
323       super(size, color);
324       mySize = size;
325       mySizedIcon = new SizedIcon(PlatformIcons.CHECK_ICON_SMALL, mySize, mySize);
326     }
327
328     public void prepare(boolean selected) {
329       mySelected = selected;
330     }
331
332     @Override
333     public void paintIcon(Component component, Graphics g, int i, int j) {
334       super.paintIcon(component, g, i, j);
335       if (mySelected) {
336         mySizedIcon.paintIcon(component, g, i, j);
337       }
338     }
339   }
340
341   private class SelectFoldersAction extends DumbAwareAction {
342     public static final String STRUCTURE_FILTER_TEXT = "Select Folders...";
343
344     SelectFoldersAction() {
345       super(STRUCTURE_FILTER_TEXT);
346     }
347
348     @Override
349     public void actionPerformed(@NotNull AnActionEvent e) {
350       Project project = e.getRequiredData(CommonDataKeys.PROJECT);
351       VcsLogDataPack dataPack = myFilterModel.getDataPack();
352       VcsLogFileFilter filter = myFilterModel.getFilter();
353
354       Collection<VirtualFile> files;
355       if (filter == null || filter.getStructureFilter() == null) {
356         files = Collections.emptySet();
357       }
358       else {
359         // for now, ignoring non-existing paths
360         files = ContainerUtil.mapNotNull(filter.getStructureFilter().getFiles(), FilePath::getVirtualFile);
361       }
362
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);
369       }
370     }
371
372     @Override
373     public void update(AnActionEvent e) {
374       e.getPresentation().setEnabledAndVisible(e.getProject() != null);
375     }
376   }
377
378   private class SelectFromHistoryAction extends ToggleAction {
379     @NotNull private final VcsLogStructureFilter myFilter;
380     @NotNull private final Icon myIcon;
381     @NotNull private final Icon myEmptyIcon;
382
383     private SelectFromHistoryAction(@NotNull VcsLogStructureFilter filter) {
384       super(getStructureActionText(filter), getTooltipTextForFilePaths(filter.getFiles()).replace("\n", " "), null);
385       myFilter = filter;
386       myIcon = new SizedIcon(PlatformIcons.CHECK_ICON_SMALL, CHECKBOX_ICON_SIZE, CHECKBOX_ICON_SIZE);
387       myEmptyIcon = EmptyIcon.create(CHECKBOX_ICON_SIZE);
388     }
389
390     @Override
391     public boolean isSelected(AnActionEvent e) {
392       return myFilterModel.getFilter() != null && myFilterModel.getFilter().getStructureFilter() == myFilter;
393     }
394
395     @Override
396     public void setSelected(AnActionEvent e, boolean state) {
397       myFilterModel.setFilter(new VcsLogFileFilter(myFilter, null));
398     }
399
400     @Override
401     public void update(@NotNull AnActionEvent e) {
402       super.update(e);
403
404       Presentation presentation = e.getPresentation();
405       if (isSelected(e)) {
406         presentation.setIcon(myIcon);
407       }
408       else {
409         presentation.setIcon(myEmptyIcon);
410       }
411     }
412   }
413
414   private static class FixedSizeQueue<T> implements Iterable<T> {
415     @NotNull private final LinkedList<T> myQueue = new LinkedList<>();
416     private final int maxSize;
417
418     public FixedSizeQueue(int maxSize) {
419       this.maxSize = maxSize;
420     }
421
422     @NotNull
423     @Override
424     public Iterator<T> iterator() {
425       return ContainerUtil.reverse(myQueue).iterator();
426     }
427
428     public void add(T t) {
429       myQueue.add(t);
430       if (myQueue.size() > maxSize) {
431         myQueue.poll();
432       }
433     }
434   }
435 }