Reliable tool window activation from Switcher
[idea/community.git] / platform / platform-impl / src / com / intellij / ide / actions / Switcher.java
1 /*
2  * Copyright 2000-2015 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.ide.actions;
17
18 import com.intellij.featureStatistics.FeatureUsageTracker;
19 import com.intellij.ide.DataManager;
20 import com.intellij.ide.IdeEventQueue;
21 import com.intellij.ide.ui.UISettings;
22 import com.intellij.openapi.actionSystem.*;
23 import com.intellij.openapi.actionSystem.impl.PresentationFactory;
24 import com.intellij.openapi.application.ApplicationManager;
25 import com.intellij.openapi.editor.markup.EffectType;
26 import com.intellij.openapi.editor.markup.TextAttributes;
27 import com.intellij.openapi.fileEditor.FileEditorManager;
28 import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
29 import com.intellij.openapi.fileEditor.impl.EditorHistoryManager;
30 import com.intellij.openapi.fileEditor.impl.EditorTabbedContainer;
31 import com.intellij.openapi.fileEditor.impl.EditorWindow;
32 import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl;
33 import com.intellij.openapi.keymap.KeymapManager;
34 import com.intellij.openapi.project.DumbAware;
35 import com.intellij.openapi.project.Project;
36 import com.intellij.openapi.ui.GraphicsConfig;
37 import com.intellij.openapi.ui.popup.JBPopup;
38 import com.intellij.openapi.ui.popup.JBPopupFactory;
39 import com.intellij.openapi.util.*;
40 import com.intellij.openapi.util.io.FileUtil;
41 import com.intellij.openapi.util.text.StringUtil;
42 import com.intellij.openapi.vcs.FileStatus;
43 import com.intellij.openapi.vcs.FileStatusManager;
44 import com.intellij.openapi.vfs.VirtualFile;
45 import com.intellij.openapi.wm.IdeFocusManager;
46 import com.intellij.openapi.wm.ToolWindow;
47 import com.intellij.openapi.wm.ToolWindowManager;
48 import com.intellij.openapi.wm.WindowManager;
49 import com.intellij.openapi.wm.impl.ToolWindowImpl;
50 import com.intellij.openapi.wm.impl.ToolWindowManagerImpl;
51 import com.intellij.ui.*;
52 import com.intellij.ui.components.JBList;
53 import com.intellij.ui.speedSearch.NameFilteringListModel;
54 import com.intellij.ui.speedSearch.SpeedSearchUtil;
55 import com.intellij.util.*;
56 import com.intellij.util.containers.ContainerUtil;
57 import com.intellij.util.ui.JBUI;
58 import com.intellij.util.ui.StatusText;
59 import com.intellij.util.ui.UIUtil;
60 import org.jetbrains.annotations.NonNls;
61 import org.jetbrains.annotations.NotNull;
62 import org.jetbrains.annotations.Nullable;
63
64 import javax.swing.*;
65 import javax.swing.event.ListSelectionEvent;
66 import javax.swing.event.ListSelectionListener;
67 import java.awt.*;
68 import java.awt.event.*;
69 import java.beans.PropertyChangeEvent;
70 import java.beans.PropertyChangeListener;
71 import java.io.File;
72 import java.util.*;
73 import java.util.List;
74
75 import static java.awt.event.KeyEvent.*;
76
77 /**
78  * @author Konstantin Bulenkov
79  */
80 @SuppressWarnings({"AssignmentToStaticFieldFromInstanceMethod", "SSBasedInspection"})
81 public class Switcher extends AnAction implements DumbAware {
82   private static volatile SwitcherPanel SWITCHER = null;
83   private static final Color BORDER_COLOR = Gray._135;
84   private static final Color SEPARATOR_COLOR = new JBColor(BORDER_COLOR.brighter(), Gray._75);
85   @NonNls private static final String SWITCHER_FEATURE_ID = "switcher";
86   private static final Color ON_MOUSE_OVER_BG_COLOR = new JBColor(new Color(231, 242, 249), new Color(77, 80, 84));
87   private static int CTRL_KEY;
88   private static int ALT_KEY;
89   @Nullable public static final Runnable CHECKER = new Runnable() {
90     @Override
91     public void run() {
92       synchronized (Switcher.class) {
93         if (SWITCHER != null) {
94           SWITCHER.navigate(false);
95         }
96       }
97     }
98   };
99   @NotNull private static final CustomShortcutSet TW_SHORTCUT;
100
101   static {
102     Shortcut recentFiles = ArrayUtil.getFirstElement(KeymapManager.getInstance().getActiveKeymap().getShortcuts("RecentFiles"));
103     List<Shortcut> shortcuts = ContainerUtil.newArrayList();
104     for (char ch = '0'; ch <= '9'; ch++) {
105       shortcuts.add(CustomShortcutSet.fromString("control " + ch).getShortcuts()[0]);
106     }
107     for (char ch = 'A'; ch <= 'Z'; ch++) {
108       Shortcut shortcut = CustomShortcutSet.fromString("control " + ch).getShortcuts()[0];
109       if (shortcut.equals(recentFiles)) continue;
110       shortcuts.add(shortcut);
111     }
112     TW_SHORTCUT = new CustomShortcutSet(shortcuts.toArray(new Shortcut[shortcuts.size()]));
113
114     IdeEventQueue.getInstance().addPostprocessor(new IdeEventQueue.EventDispatcher() {
115       @Override
116       public boolean dispatch(AWTEvent event) {
117         ToolWindow tw;
118         if (SWITCHER != null && event instanceof KeyEvent && !SWITCHER.isPinnedMode()) {
119           final KeyEvent keyEvent = (KeyEvent)event;
120           if (event.getID() == KEY_RELEASED && keyEvent.getKeyCode() == CTRL_KEY) {
121             SwingUtilities.invokeLater(CHECKER);
122           }
123           else if (event.getID() == KEY_PRESSED && event != INIT_EVENT
124                    && (tw = SWITCHER.twShortcuts.get(String.valueOf((char)keyEvent.getKeyCode()))) != null) {
125             SWITCHER.myPopup.closeOk(null);
126             tw.activate(null, true, true);
127           }
128         }
129         return false;
130       }
131     }, null);
132   }
133
134   @NonNls private static final String SWITCHER_TITLE = "Switcher";
135   @NonNls private static InputEvent INIT_EVENT;
136
137   @Override
138   public void update(@NotNull AnActionEvent e) {
139     e.getPresentation().setEnabled(e.getProject() != null);
140   }
141
142   public void actionPerformed(@NotNull AnActionEvent e) {
143     final Project project = CommonDataKeys.PROJECT.getData(e.getDataContext());
144     if (project == null) return;
145
146     boolean isNewSwitcher = false;
147     synchronized (Switcher.class) {
148       INIT_EVENT = e.getInputEvent();
149       if (SWITCHER != null && SWITCHER.isPinnedMode()) {
150         SWITCHER.cancel();
151         SWITCHER = null;
152       }
153       if (SWITCHER == null) {
154         isNewSwitcher = true;
155         SWITCHER = createAndShowSwitcher(e, SWITCHER_TITLE, false, null);
156         FeatureUsageTracker.getInstance().triggerFeatureUsed(SWITCHER_FEATURE_ID);
157       }
158     }
159
160     assert SWITCHER != null;
161     if (!SWITCHER.isPinnedMode()) {
162       if (e.getInputEvent().isShiftDown()) {
163         SWITCHER.goBack();
164       }
165       else {
166         if (isNewSwitcher && !FileEditorManagerEx.getInstanceEx(project).hasOpenedFile()) {
167           SWITCHER.files.setSelectedIndex(0);
168         }
169         else {
170           SWITCHER.goForward();
171         }
172       }
173     }
174   }
175
176   @Nullable
177   public static SwitcherPanel createAndShowSwitcher(@NotNull AnActionEvent e, @NotNull String title, boolean pinned, @Nullable final VirtualFile[] vFiles) {
178     Project project = getEventProject(e);
179     if (SWITCHER != null && Comparing.equal(SWITCHER.myTitle, title)) {
180       SWITCHER.goForward();
181       return null;
182     }
183     return project == null ? null : createAndShowSwitcher(project, title, pinned, vFiles);
184   }
185
186   @Nullable
187   private static SwitcherPanel createAndShowSwitcher(@NotNull Project project,
188                                                      @NotNull String title,
189                                                      boolean pinned,
190                                                      @Nullable final VirtualFile[] vFiles) {
191     synchronized (Switcher.class) {
192       if (SWITCHER != null) {
193         SWITCHER.cancel();
194       }
195       SWITCHER = new SwitcherPanel(project, title, pinned) {
196         @NotNull
197         @Override
198         protected VirtualFile[] getFiles(@NotNull Project project) {
199           return vFiles != null ? vFiles : super.getFiles(project);
200         }
201       };
202       return SWITCHER;
203     }
204   }
205
206   public static class SwitcherPanel extends JPanel implements KeyListener, MouseListener, MouseMotionListener {
207     final JBPopup myPopup;
208     final MyList toolWindows;
209     final MyList files;
210     final JPanel separator;
211     final ToolWindowManager twManager;
212     final JLabel pathLabel = new JLabel(" ");
213     final JPanel descriptions;
214     final Project project;
215     private final boolean myPinned;
216     final Map<String, ToolWindow> twShortcuts;
217     final Alarm myAlarm;
218     final SwitcherSpeedSearch mySpeedSearch;
219     final String myTitle;
220     final ClickListener myClickListener = new ClickListener() {
221       @Override
222       public boolean onClick(@NotNull MouseEvent e, int clickCount) {
223         if (myPinned && (e.isControlDown() || e.isMetaDown() || e.isShiftDown())) return false;
224         final Object source = e.getSource();
225         if (source instanceof JList) {
226           JList jList = (JList)source;
227           if (jList.getSelectedIndex() == -1 && jList.getAnchorSelectionIndex() != -1) {
228             jList.setSelectedIndex(jList.getAnchorSelectionIndex());
229           }
230           if (jList.getSelectedIndex() != -1) {
231             navigate(false);
232           }
233         }
234         return true;
235       }
236     };
237
238     @SuppressWarnings({"ManualArrayToCollectionCopy", "ConstantConditions"})
239     SwitcherPanel(@NotNull final Project project, @NotNull String title, boolean pinned) {
240       setLayout(new SwitcherLayouter());
241       this.project = project;
242       myTitle = title;
243       myPinned = pinned;
244       mySpeedSearch = pinned ? new SwitcherSpeedSearch() : null;
245
246       setFocusable(true);
247       addKeyListener(this);
248       setBorder(JBUI.Borders.empty());
249       setBackground(JBColor.background());
250       pathLabel.setHorizontalAlignment(SwingConstants.LEFT);
251
252       final Font font = pathLabel.getFont();
253       pathLabel.setFont(font.deriveFont(Math.max(10f, font.getSize() - 4f)));
254
255       descriptions = new JPanel(new BorderLayout()) {
256         @Override
257         protected void paintComponent(@NotNull Graphics g) {
258           super.paintComponent(g);
259           g.setColor(UIUtil.isUnderDarcula() ? SEPARATOR_COLOR : BORDER_COLOR);
260           g.drawLine(0, 0, getWidth(), 0);
261         }
262       };
263
264       descriptions.setBorder(BorderFactory.createEmptyBorder(1, 4, 1, 4));
265       descriptions.add(pathLabel, BorderLayout.CENTER);
266       twManager = ToolWindowManager.getInstance(project);
267       DefaultListModel twModel = new DefaultListModel();
268       List<ActivateToolWindowAction> actions = ToolWindowsGroup.getToolWindowActions(project);
269       List<ToolWindow> windows = ContainerUtil.newArrayList();
270       for (ActivateToolWindowAction action : actions) {
271         ToolWindow tw = twManager.getToolWindow(action.getToolWindowId());
272         if (tw.isAvailable()) {
273           windows.add(tw);
274         }
275       }
276       twShortcuts = createShortcuts(windows);
277       final Map<ToolWindow, String> map = ContainerUtil.reverseMap(twShortcuts);
278       Collections.sort(windows, new Comparator<ToolWindow>() {
279         @Override
280         public int compare(@NotNull ToolWindow o1, @NotNull ToolWindow o2) {
281           return StringUtil.compare(map.get(o1), map.get(o2), false);
282         }
283       });
284       for (ToolWindow window : windows) {
285         twModel.addElement(window);
286       }
287
288       toolWindows = new MyList(twModel);
289       if (pinned) {
290         new NameFilteringListModel<ToolWindow>(toolWindows, new Function<ToolWindow, String>() {
291           @NotNull
292           @Override
293           public String fun(@NotNull ToolWindow window) {
294             return window.getStripeTitle();
295           }
296         }, new Condition<String>() {
297           @Override
298           public boolean value(@NotNull String s) {
299             return !mySpeedSearch.isPopupActive()
300                    || StringUtil.isEmpty(mySpeedSearch.getEnteredPrefix())
301                    || mySpeedSearch.getComparator().matchingFragments(mySpeedSearch.getEnteredPrefix(), s) != null;
302           }
303         }, mySpeedSearch);
304       }
305
306       toolWindows.setBorder(IdeBorderFactory.createEmptyBorder(5, 5, 5, 20));
307       toolWindows.setSelectionMode(pinned ? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION : ListSelectionModel.SINGLE_SELECTION);
308       toolWindows.setCellRenderer(new SwitcherToolWindowsListRenderer(mySpeedSearch, map, myPinned) {
309         @NotNull
310         @Override
311         public Component getListCellRendererComponent(@NotNull JList list,
312                                                       Object value,
313                                                       int index,
314                                                       boolean selected,
315                                                       boolean hasFocus) {
316           final JComponent renderer = (JComponent)super.getListCellRendererComponent(list, value, index, selected, selected);
317           if (selected) {
318             return renderer;
319           }
320           final Color bgColor = list == mouseMoveSrc && index == mouseMoveListIndex ? ON_MOUSE_OVER_BG_COLOR : list.getBackground();
321           UIUtil.changeBackGround(renderer, bgColor);
322           return renderer;
323         }
324       });
325       toolWindows.addKeyListener(this);
326       toolWindows.addMouseListener(this);
327       toolWindows.addMouseMotionListener(this);
328       myClickListener.installOn(toolWindows);
329       toolWindows.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
330         public void valueChanged(@NotNull ListSelectionEvent e) {
331           if (!toolWindows.isSelectionEmpty() && !files.isSelectionEmpty()) {
332             files.clearSelection();
333           }
334         }
335       });
336
337       separator = new JPanel() {
338         @Override
339         protected void paintComponent(@NotNull Graphics g) {
340           super.paintComponent(g);
341           g.setColor(SEPARATOR_COLOR);
342           g.drawLine(0, 0, 0, getHeight());
343         }
344       };
345       separator.setBackground(toolWindows.getBackground());
346
347       int selectionIndex = -1;
348       final FileEditorManagerImpl editorManager = (FileEditorManagerImpl)FileEditorManager.getInstance(project);
349       final ArrayList<FileInfo> filesData = new ArrayList<FileInfo>();
350       final ArrayList<FileInfo> editors = new ArrayList<FileInfo>();
351       if (!pinned) {
352         if (UISettings.getInstance().EDITOR_TAB_PLACEMENT != UISettings.TABS_NONE) {
353           for (Pair<VirtualFile, EditorWindow> pair : editorManager.getSelectionHistory()) {
354             editors.add(new FileInfo(pair.first, pair.second, project));
355           }
356         }
357       }
358       if (editors.size() < 2 || isPinnedMode()) {
359         if (isPinnedMode() && editors.size() > 1) {
360           filesData.addAll(editors);
361         }
362         final VirtualFile[] recentFiles = ArrayUtil.reverseArray(getFiles(project));
363         final int maxFiles = Math.max(editors.size(), recentFiles.length);
364         final int len = isPinnedMode() ? recentFiles.length : Math.min(toolWindows.getModel().getSize(), maxFiles);
365         boolean firstRecentMarked = false;
366         final List<VirtualFile> selectedFiles = Arrays.asList(editorManager.getSelectedFiles());
367         for (int i = 0; i < len; i++) {
368           if (isPinnedMode() && selectedFiles.contains(recentFiles[i])) {
369             continue;
370           }
371
372           final FileInfo info = new FileInfo(recentFiles[i], null, project);
373           boolean add = true;
374           if (isPinnedMode()) {
375             for (FileInfo fileInfo : filesData) {
376               if (fileInfo.first.equals(info.first)) {
377                 add = false;
378                 break;
379               }
380             }
381           }
382           if (add) {
383             filesData.add(info);
384             if (!firstRecentMarked) {
385               firstRecentMarked = true;
386               selectionIndex = filesData.size() - 1;
387             }
388           }
389         }
390         //if (editors.size() == 1) selectionIndex++;
391         if (editors.size() == 1 && (filesData.isEmpty() || !editors.get(0).getFirst().equals(filesData.get(0).getFirst()))) {
392           filesData.add(0, editors.get(0));
393         }
394       } else {
395         for (int i = 0; i < Math.min(30, editors.size()); i++) {
396           filesData.add(editors.get(i));
397         }
398       }
399
400       final DefaultListModel filesModel = new DefaultListModel();
401       for (FileInfo editor : filesData) {
402         filesModel.addElement(editor);
403       }
404
405       final VirtualFilesRenderer filesRenderer = new VirtualFilesRenderer(this) {
406         JPanel myPanel = new JPanel(new BorderLayout());
407         JLabel myLabel = new JLabel() {
408           @Override
409           protected void paintComponent(@NotNull Graphics g) {
410             GraphicsConfig config = new GraphicsConfig(g);
411             ((Graphics2D)g).setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f));
412             super.paintComponent(g);
413             config.restore();
414           }
415         };
416
417         {
418           myPanel.setOpaque(false);
419           myPanel.setBackground(UIUtil.getListBackground());
420           myLabel.setText("* ");
421         }
422
423         @NotNull
424         @Override
425         public Component getListCellRendererComponent(@NotNull JList list,
426                                                       Object value,
427                                                       int index,
428                                                       boolean selected,
429                                                       boolean hasFocus) {
430           final Component c = super.getListCellRendererComponent(list, value, index, selected, selected);
431           final Color bg = UIUtil.getListBackground();
432           final Color fg = UIUtil.getListForeground();
433           myLabel.setFont(list.getFont());
434           myLabel.setForeground(open ? fg : bg);
435
436           myPanel.removeAll();
437           myPanel.add(myLabel, BorderLayout.WEST);
438           myPanel.add(c, BorderLayout.CENTER);
439           return myPanel;
440         }
441
442         @Override
443         protected void customizeCellRenderer(JList list, Object value, int index, boolean selected, boolean hasFocus) {
444           setPaintFocusBorder(false);
445           super.customizeCellRenderer(list, value, index, selected, hasFocus);
446         }
447       };
448
449       final ListSelectionListener filesSelectionListener = new ListSelectionListener() {
450         @Nullable
451         private String getTitle2Text(@Nullable String fullText) {
452           int labelWidth = pathLabel.getWidth();
453           if (fullText == null || fullText.length() == 0) return " ";
454           while (pathLabel.getFontMetrics(pathLabel.getFont()).stringWidth(fullText) > labelWidth) {
455             int sep = fullText.indexOf(File.separatorChar, 4);
456             if (sep < 0) return fullText;
457             fullText = "..." + fullText.substring(sep);
458           }
459
460           return fullText;
461         }
462
463         public void valueChanged(@NotNull final ListSelectionEvent e) {
464           ApplicationManager.getApplication().invokeLater(new Runnable() {
465             public void run() {
466               updatePathLabel();
467             }
468           });
469         }
470
471         private void updatePathLabel() {
472           Object[] values = files.getSelectedValues();
473           if (values != null && values.length == 1) {
474             VirtualFile file = ((FileInfo)values[0]).first;
475             String presentableUrl = ObjectUtils.notNull(file.getParent(), file).getPresentableUrl();
476             pathLabel.setText(getTitle2Text(FileUtil.getLocationRelativeToUserHome(presentableUrl)));
477           }
478           else {
479             pathLabel.setText(" ");
480           }
481         }
482       };
483
484       files = new MyList(filesModel);
485       if (pinned) {
486         new NameFilteringListModel<FileInfo>(files, new Function<FileInfo, String>() {
487           @Override
488           public String fun(@NotNull FileInfo info) {
489             return info.getNameForRendering();
490           }
491         }, new Condition<String>() {
492           @Override
493           public boolean value(@NotNull String s) {
494             return !mySpeedSearch.isPopupActive()
495                    || StringUtil.isEmpty(mySpeedSearch.getEnteredPrefix())
496                    || mySpeedSearch.getComparator().matchingFragments(mySpeedSearch.getEnteredPrefix(), s) != null;
497           }
498         }, mySpeedSearch);
499       }
500
501       files.setSelectionMode(pinned ? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION : ListSelectionModel.SINGLE_SELECTION);
502       files.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
503         public void valueChanged(@NotNull ListSelectionEvent e) {
504           if (!files.isSelectionEmpty() && !toolWindows.isSelectionEmpty()) {
505             toolWindows.getSelectionModel().clearSelection();
506           }
507         }
508       });
509
510       files.getSelectionModel().addListSelectionListener(filesSelectionListener);
511
512       files.setCellRenderer(filesRenderer);
513       files.setBorder(IdeBorderFactory.createEmptyBorder(5, 5, 5, 20));
514       files.addKeyListener(this);
515       files.addMouseListener(this);
516       files.addMouseMotionListener(this);
517       myClickListener.installOn(files);
518
519       this.add(toolWindows, BorderLayout.WEST);
520       if (filesModel.size() > 0) {
521         files.setAlignmentY(1f);
522         if (files.getModel().getSize() > 20) {
523           final JScrollPane pane = ScrollPaneFactory.createScrollPane(files, true);
524           pane.setPreferredSize(new Dimension(files.getPreferredSize().width + 10, 20 * 20));
525           pane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
526           this.add(pane, BorderLayout.EAST);
527         }
528         else {
529           this.add(files, BorderLayout.EAST);
530         }
531         if (selectionIndex > -1) {
532           files.setSelectedIndex(selectionIndex);
533         }
534         this.add(separator, BorderLayout.CENTER);
535       }
536       this.add(descriptions, BorderLayout.SOUTH);
537
538       final ShortcutSet shortcutSet = ActionManager.getInstance().getAction(IdeActions.ACTION_SWITCHER).getShortcutSet();
539       final int modifiers = getModifiers(shortcutSet);
540       final boolean isAlt = (modifiers & Event.ALT_MASK) != 0;
541       ALT_KEY = isAlt ? VK_CONTROL : VK_ALT;
542       CTRL_KEY = isAlt ? VK_ALT : VK_CONTROL;
543
544       myPopup = JBPopupFactory.getInstance().createComponentPopupBuilder(this, this)
545         .setResizable(pinned)
546         .setModalContext(false)
547         .setFocusable(true)
548         .setRequestFocus(true)
549         .setTitle(title)
550         .setCancelOnWindowDeactivation(true)
551         .setCancelOnOtherWindowOpen(true)
552         .setMovable(pinned)
553         .setCancelKeyEnabled(false)
554         .setCancelCallback(new Computable<Boolean>() {
555           @NotNull
556           public Boolean compute() {
557             SWITCHER = null;
558             return true;
559           }
560         }).createPopup();
561
562       if (isPinnedMode()) {
563         new AnAction(null, null, null) {
564           @Override
565           public void actionPerformed(@NotNull AnActionEvent e) {
566             changeSelection();
567           }
568         }.registerCustomShortcutSet(CustomShortcutSet.fromString("TAB"), this, myPopup);
569         new AnAction(null, null, null) {
570           @Override
571           public void actionPerformed(@NotNull AnActionEvent e) {
572             if (mySpeedSearch != null && mySpeedSearch.isPopupActive()) {
573               mySpeedSearch.hidePopup();
574             }
575             else {
576               myPopup.cancel();
577             }
578           }
579         }.registerCustomShortcutSet(CustomShortcutSet.fromString("ESCAPE"), this, myPopup);
580       }
581       new AnAction(null, null, null) {
582         @Override
583         public void actionPerformed(@NotNull AnActionEvent e) {
584           //suppress all actions to activate a toolwindow : IDEA-71277
585         }
586       }.registerCustomShortcutSet(TW_SHORTCUT, this, myPopup);
587
588       Window window = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
589       if (window == null) {
590         window = WindowManager.getInstance().getFrame(project);
591       }
592       myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, myPopup);
593       myPopup.showInCenterOf(window);
594     }
595
596     @NotNull
597     protected VirtualFile[] getFiles(@NotNull Project project) {
598       return EditorHistoryManager.getInstance(project).getFiles();
599     }
600
601     @NotNull
602     private static Map<String, ToolWindow> createShortcuts(@NotNull List<ToolWindow> windows) {
603       final Map<String, ToolWindow> keymap = new HashMap<String, ToolWindow>(windows.size());
604       final List<ToolWindow> otherTW = new ArrayList<ToolWindow>();
605       for (ToolWindow window : windows) {
606         int index = ActivateToolWindowAction.getMnemonicForToolWindow(((ToolWindowImpl)window).getId());
607         if (index >= '0' && index <= '9') {
608           keymap.put(getIndexShortcut(index - '0'), window);
609         }
610         else {
611           otherTW.add(window);
612         }
613       }
614       int i = 0;
615       for (ToolWindow window : otherTW) {
616         while (keymap.get(getIndexShortcut(i)) != null) {
617           i++;
618         }
619         keymap.put(getIndexShortcut(i), window);
620         i++;
621       }
622       return keymap;
623     }
624
625     private static String getIndexShortcut(int index) {
626       return StringUtil.toUpperCase(Integer.toString(index, index + 1));
627     }
628
629     private static int getModifiers(@Nullable ShortcutSet shortcutSet) {
630       if (shortcutSet == null
631           || shortcutSet.getShortcuts().length == 0
632           || !(shortcutSet.getShortcuts()[0] instanceof KeyboardShortcut)) {
633         return Event.CTRL_MASK;
634       }
635       return ((KeyboardShortcut)shortcutSet.getShortcuts()[0]).getFirstKeyStroke().getModifiers();
636     }
637
638     public void keyTyped(@NotNull KeyEvent e) {
639     }
640
641     public void keyReleased(@NotNull KeyEvent e) {
642       boolean ctrl = e.getKeyCode() == CTRL_KEY;
643       boolean enter = e.getKeyCode() == VK_ENTER;
644       if (ctrl && isAutoHide() || enter) {
645         navigate(e.isShiftDown() && enter);
646       }
647       else if (e.getKeyCode() == VK_LEFT) {
648         goLeft();
649       }
650       else if (e.getKeyCode() == VK_RIGHT) {
651         goRight();
652       }
653     }
654
655     KeyEvent lastEvent;
656
657     public void keyPressed(@NotNull KeyEvent e) {
658       if (mySpeedSearch != null && mySpeedSearch.isPopupActive() || lastEvent == e) return;
659       lastEvent = e;
660       switch (e.getKeyCode()) {
661         case VK_UP:
662           if (!isPinnedMode()) {
663             goBack();
664           }
665           else {
666             getSelectedList().processKeyEvent(e);
667           }
668           break;
669         case VK_DOWN:
670           if (!isPinnedMode()) {
671             goForward();
672           }
673           else {
674             getSelectedList().processKeyEvent(e);
675           }
676           break;
677         case VK_ESCAPE:
678           cancel();
679           break;
680         case VK_END:
681           ListScrollingUtil.moveEnd(getSelectedList());
682           break;
683         case VK_PAGE_DOWN:
684           ListScrollingUtil.movePageDown(getSelectedList());
685           break;
686         case VK_HOME:
687           ListScrollingUtil.moveHome(getSelectedList());
688           break;
689         case VK_PAGE_UP:
690           ListScrollingUtil.movePageUp(getSelectedList());
691           break;
692         case VK_DELETE:
693         case VK_BACK_SPACE: // Mac users
694         case VK_Q:
695           closeTabOrToolWindow();
696           break;
697       }
698       if (e.getKeyCode() == ALT_KEY) {
699         changeSelection();
700       }
701     }
702
703     private void changeSelection() {
704       if (isFilesSelected()) {
705         goLeft();
706       }
707       else {
708         goRight();
709       }
710     }
711
712     private void closeTabOrToolWindow() {
713       final int[] selected = getSelectedList().getSelectedIndices();
714       Arrays.sort(selected);
715       int selectedIndex = 0;
716       for (int i = selected.length - 1; i >= 0; i--) {
717         selectedIndex = selected[i];
718         Object value = getSelectedList().getModel().getElementAt(selectedIndex);
719         if (value instanceof FileInfo) {
720           final FileInfo info = (FileInfo)value;
721           final VirtualFile virtualFile = info.first;
722           final FileEditorManagerImpl editorManager = (FileEditorManagerImpl)FileEditorManager.getInstance(project);
723           final JList jList = getSelectedList();
724           final EditorWindow wnd = findAppropriateWindow(info);
725           if (wnd == null) {
726             editorManager.closeFile(virtualFile, false, false);
727           }
728           else {
729             editorManager.closeFile(virtualFile, wnd, false);
730           }
731
732           final IdeFocusManager focusManager = IdeFocusManager.getInstance(project);
733           myAlarm.cancelAllRequests();
734           myAlarm.addRequest(new Runnable() {
735             @Override
736             public void run() {
737               focusManager.requestFocus(SwitcherPanel.this, true);
738             }
739           }, 300);
740           if (jList.getModel().getSize() == 1) {
741             goLeft();
742             removeElementAt(jList, selectedIndex);
743             this.remove(jList);
744             this.remove(separator);
745             final Dimension size = toolWindows.getSize();
746             myPopup.setSize(new Dimension(size.width, myPopup.getSize().height));
747           }
748           else {
749             removeElementAt(jList, selectedIndex);
750             jList.setSize(jList.getPreferredSize());
751           }
752           if (isPinnedMode()) {
753             EditorHistoryManager.getInstance(project).removeFile(virtualFile);
754           }
755         }
756         else if (value instanceof ToolWindow) {
757           final ToolWindow toolWindow = (ToolWindow)value;
758           if (twManager instanceof ToolWindowManagerImpl) {
759             ToolWindowManagerImpl manager = (ToolWindowManagerImpl)twManager;
760             manager.hideToolWindow(((ToolWindowImpl)toolWindow).getId(), false, false);
761           }
762           else {
763             toolWindow.hide(null);
764           }
765         }
766       }
767       pack();
768       myPopup.getContent().revalidate();
769       myPopup.getContent().repaint();
770       if (getSelectedList().getModel().getSize() > selectedIndex) {
771         getSelectedList().setSelectedIndex(selectedIndex);
772         getSelectedList().ensureIndexIsVisible(selectedIndex);
773       }
774     }
775
776     private static void removeElementAt(@NotNull JList jList, int index) {
777       final ListModel model = jList.getModel();
778       if (model instanceof DefaultListModel) {
779         ((DefaultListModel)model).removeElementAt(index);
780       }
781       else if (model instanceof NameFilteringListModel) {
782         ((NameFilteringListModel)model).remove(index);
783       }
784       else {
785         throw new IllegalArgumentException("Wrong list model " + model.getClass());
786       }
787     }
788
789     private void pack() {
790       this.setSize(this.getPreferredSize());
791       final JRootPane rootPane = SwingUtilities.getRootPane(this);
792       Container container = this;
793       do {
794         container = container.getParent();
795         container.setSize(container.getPreferredSize());
796       }
797       while (container != rootPane);
798       container.getParent().setSize(container.getPreferredSize());
799     }
800
801     private boolean isFilesSelected() {
802       return getSelectedList() == files;
803     }
804
805     private boolean isFilesVisible() {
806       return files.getModel().getSize() > 0;
807     }
808
809     private boolean isToolWindowsSelected() {
810       return getSelectedList() == toolWindows;
811     }
812
813     private void goRight() {
814       if ((isFilesSelected() || !isFilesVisible()) && isAutoHide()) {
815         cancel();
816       }
817       else {
818         if (files.getModel().getSize() > 0) {
819           final int index = Math.min(toolWindows.getSelectedIndex(), files.getModel().getSize() - 1);
820           files.setSelectedIndex(index);
821           files.ensureIndexIsVisible(index);
822           toolWindows.getSelectionModel().clearSelection();
823         }
824       }
825     }
826
827     private void cancel() {
828       myPopup.cancel();
829     }
830
831     private void goLeft() {
832       if (isToolWindowsSelected() && isAutoHide()) {
833         cancel();
834       }
835       else {
836         if (toolWindows.getModel().getSize() > 0) {
837           toolWindows.setSelectedIndex(Math.min(files.getSelectedIndex(), toolWindows.getModel().getSize() - 1));
838           files.getSelectionModel().clearSelection();
839         }
840       }
841     }
842
843     public void goForward() {
844       JList list = getSelectedList();
845       int index = list.getSelectedIndex() + 1;
846       if (index >= list.getModel().getSize()) {
847         index = 0;
848         if (isFilesVisible()) {
849           list = isFilesSelected() ? toolWindows : files;
850         }
851       }
852       list.setSelectedIndex(index);
853       list.ensureIndexIsVisible(index);
854     }
855
856     public void goBack() {
857       JList list = getSelectedList();
858       int index = list.getSelectedIndex() - 1;
859       if (index < 0) {
860         if (isFilesVisible()) {
861           list = isFilesSelected() ? toolWindows : files;
862         }
863         index = list.getModel().getSize() - 1;
864       }
865       list.setSelectedIndex(index);
866       list.ensureIndexIsVisible(index);
867     }
868
869     @Nullable
870     public MyList getSelectedList() {
871       return getSelectedList(files);
872     }
873
874     @Nullable
875     MyList getSelectedList(@Nullable MyList preferable) {
876       if (toolWindows.isSelectionEmpty() && files.isSelectionEmpty()) {
877         if (preferable != null && preferable.getModel().getSize() > 0) {
878           preferable.setSelectedIndex(0);
879           return preferable;
880         }
881         else if (files.getModel().getSize() > 0) {
882           files.setSelectedIndex(0);
883           return files;
884         }
885         else {
886           toolWindows.setSelectedIndex(0);
887           return toolWindows;
888         }
889       }
890       else {
891         return toolWindows.isSelectionEmpty() ? files : toolWindows;
892       }
893     }
894
895     void navigate(final boolean openInNewWindow) {
896       final Object[] values = getSelectedList().getSelectedValues();
897       myPopup.cancel(null);
898       if (values.length > 0 && values[0] instanceof ToolWindow) {
899         final ToolWindow toolWindow = (ToolWindow)values[0];
900         IdeFocusManager.getInstance(project).doWhenFocusSettlesDown(new Runnable() {
901           @Override
902           public void run() {
903             toolWindow.activate(null, true, true);
904           }
905         });
906       }
907       else {
908         IdeFocusManager.getInstance(project).doWhenFocusSettlesDown(new Runnable() {
909           @Override
910           public void run() {
911             final FileEditorManagerImpl manager = (FileEditorManagerImpl)FileEditorManager.getInstance(project);
912             for (Object value : values) {
913               if (value instanceof FileInfo) {
914                 final FileInfo info = (FileInfo)value;
915
916                 VirtualFile file = info.first;
917                 if (openInNewWindow) {
918                   manager.openFileInNewWindow(file);
919                 }
920                 else if (info.second != null) {
921                   EditorWindow wnd = findAppropriateWindow(info);
922                   if (wnd != null) {
923                     manager.openFileImpl2(wnd, file, true);
924                     manager.addSelectionRecord(file, wnd);
925                   }
926                 }
927                 else {
928                   manager.openFile(file, true, true);
929                 }
930               }
931             }
932           }
933         });
934       }
935     }
936
937     @Nullable
938     private static EditorWindow findAppropriateWindow(@NotNull FileInfo info) {
939       if (info.second == null) return null;
940       final EditorWindow[] windows = info.second.getOwner().getWindows();
941       return ArrayUtil.contains(info.second, windows) ? info.second : windows.length > 0 ? windows[0] : null;
942     }
943
944     public void mouseClicked(@NotNull MouseEvent e) {
945     }
946
947     private boolean mouseMovedFirstTime = true;
948     private JList mouseMoveSrc = null;
949     private int mouseMoveListIndex = -1;
950
951     public void mouseMoved(@NotNull MouseEvent e) {
952       if (mouseMovedFirstTime) {
953         mouseMovedFirstTime = false;
954         return;
955       }
956       final Object source = e.getSource();
957       boolean changed = false;
958       if (source instanceof JList) {
959         JList list = (JList)source;
960         int index = list.locationToIndex(e.getPoint());
961         if (0 <= index && index < list.getModel().getSize()) {
962           mouseMoveSrc = list;
963           mouseMoveListIndex = index;
964           changed = true;
965         }
966       }
967       if (!changed) {
968         mouseMoveSrc = null;
969         mouseMoveListIndex = -1;
970       }
971
972       repaintLists();
973     }
974
975     private void repaintLists() {
976       toolWindows.repaint();
977       files.repaint();
978     }
979
980     public void mousePressed(@NotNull MouseEvent e) {
981     }
982
983     public void mouseReleased(@NotNull MouseEvent e) {
984     }
985
986     public void mouseEntered(@NotNull MouseEvent e) {
987     }
988
989     public void mouseExited(@NotNull MouseEvent e) {
990       mouseMoveSrc = null;
991       mouseMoveListIndex = -1;
992       repaintLists();
993     }
994
995     public void mouseDragged(@NotNull MouseEvent e) {
996     }
997
998     private class SwitcherSpeedSearch extends SpeedSearchBase<SwitcherPanel> implements PropertyChangeListener {
999       private Object[] myElements;
1000
1001       public SwitcherSpeedSearch() {
1002         super(SwitcherPanel.this);
1003         addChangeListener(this);
1004         setComparator(new SpeedSearchComparator(false, true));
1005       }
1006
1007       @Override
1008       protected void processKeyEvent(@NotNull final KeyEvent e) {
1009         final int keyCode = e.getKeyCode();
1010         if (keyCode == VK_LEFT || keyCode == VK_RIGHT) {
1011           return;
1012         }
1013         if (keyCode == VK_ENTER && files.getModel().getSize() + toolWindows.getModel().getSize() == 0) {
1014           AnAction gotoFile = ActionManager.getInstance().getAction("GotoFile");
1015           if (gotoFile != null) {
1016             final String search = mySpeedSearch.getEnteredPrefix();
1017             myPopup.cancel();
1018             final AnAction action = gotoFile;
1019             SwingUtilities.invokeLater(new Runnable() {
1020               @Override
1021               public void run() {
1022
1023                 DataManager.getInstance().getDataContextFromFocus().doWhenDone(new Consumer<DataContext>() {
1024                   @Override
1025                   public void consume(@NotNull final DataContext context) {
1026                     final DataContext dataContext = new DataContext() {
1027                       @Nullable
1028                       @Override
1029                       public Object getData(@NonNls String dataId) {
1030                         if (PlatformDataKeys.PREDEFINED_TEXT.is(dataId)) {
1031                           return search;
1032                         }
1033                         return context.getData(dataId);
1034                       }
1035                     };
1036                     final AnActionEvent event =
1037                       new AnActionEvent(e, dataContext, ActionPlaces.EDITOR_POPUP, new PresentationFactory().getPresentation(action),
1038                                         ActionManager.getInstance(), 0);
1039                     action.actionPerformed(event);
1040                   }
1041                 });
1042               }
1043             });
1044             return;
1045           }
1046         }
1047         super.processKeyEvent(e);
1048       }
1049
1050       @Override
1051       protected int getSelectedIndex() {
1052         return isFilesSelected()
1053                ? files.getSelectedIndex()
1054                : files.getModel().getSize() + toolWindows.getSelectedIndex();
1055       }
1056
1057       @Override
1058       protected Object[] getAllElements() {
1059
1060         final SwitcherPanel switcher = SwitcherPanel.this;
1061
1062         ListModel filesModel = switcher.files.getModel();
1063         final Object[] files = new Object[filesModel.getSize()];
1064         for (int i = 0; i < files.length; i++) {
1065           files[i] = filesModel.getElementAt(i);
1066         }
1067
1068         ListModel twModel = switcher.toolWindows.getModel();
1069         final Object[] toolWindows = new Object[twModel.getSize()];
1070         for (int i = 0; i < toolWindows.length; i++) {
1071           toolWindows[i] = twModel.getElementAt(i);
1072         }
1073
1074         myElements = new Object[files.length + toolWindows.length];
1075         System.arraycopy(files, 0, myElements, 0, files.length);
1076         System.arraycopy(toolWindows, 0, myElements, files.length, toolWindows.length);
1077
1078         return myElements;
1079       }
1080
1081
1082       @Override
1083       protected String getElementText(Object element) {
1084         if (element instanceof ToolWindow) {
1085           return ((ToolWindow)element).getStripeTitle();
1086         }
1087         else if (element instanceof FileInfo) {
1088           return ((FileInfo)element).getNameForRendering();
1089         }
1090         return "";
1091       }
1092
1093       @Override
1094       protected void selectElement(Object element, String selectedText) {
1095         if (element instanceof FileInfo) {
1096           if (!toolWindows.isSelectionEmpty()) toolWindows.clearSelection();
1097           files.setSelectedValue(element, false);
1098         }
1099         else {
1100           if (!files.isSelectionEmpty()) files.clearSelection();
1101           toolWindows.setSelectedValue(element, false);
1102         }
1103       }
1104
1105       @Nullable
1106       @Override
1107       protected Object findElement(String s) {
1108         final List<SpeedSearchObjectWithWeight> elements = SpeedSearchObjectWithWeight.findElement(s, this);
1109         return elements.isEmpty() ? null : elements.get(0).node;
1110       }
1111
1112       @Override
1113       public void propertyChange(@NotNull PropertyChangeEvent evt) {
1114         final MyList list = getSelectedList();
1115         final Object value = list.getSelectedValue();
1116         if (project.isDisposed()) {
1117           myPopup.cancel();
1118           return;
1119         }
1120         ((NameFilteringListModel)files.getModel()).refilter();
1121         ((NameFilteringListModel)toolWindows.getModel()).refilter();
1122         if (files.getModel().getSize() + toolWindows.getModel().getSize() == 0) {
1123           toolWindows.getEmptyText().setText("");
1124           files.getEmptyText().setText("Press 'Enter' to search in Project");
1125         }
1126         else {
1127           files.getEmptyText().setText(StatusText.DEFAULT_EMPTY_TEXT);
1128           toolWindows.getEmptyText().setText(StatusText.DEFAULT_EMPTY_TEXT);
1129         }
1130         files.repaint();
1131         toolWindows.repaint();
1132         getSelectedList(list).setSelectedValue(value, true);
1133       }
1134     }
1135
1136     public boolean isAutoHide() {
1137       return !myPinned;
1138     }
1139
1140     public boolean isPinnedMode() {
1141       return myPinned;
1142     }
1143
1144     private class SwitcherLayouter extends BorderLayout {
1145       private Rectangle sBounds;
1146       private Rectangle tBounds;
1147       private Rectangle fBounds;
1148       private Rectangle dBounds;
1149
1150       @Override
1151       public void layoutContainer(@NotNull Container target) {
1152         final JScrollPane scrollPane = UIUtil.getParentOfType(JScrollPane.class, files);
1153         JComponent filesPane = scrollPane != null ? scrollPane : files;
1154         if (sBounds == null || !target.isShowing()) {
1155           super.layoutContainer(target);
1156           sBounds = separator.getBounds();
1157           tBounds = toolWindows.getBounds();
1158           fBounds = filesPane.getBounds();
1159           dBounds = descriptions.getBounds();
1160         }
1161         else {
1162           final int h = target.getHeight();
1163           final int w = target.getWidth();
1164           sBounds.height = h - dBounds.height;
1165           tBounds.height = h - dBounds.height;
1166           fBounds.height = h - dBounds.height;
1167           fBounds.width = w - sBounds.width - tBounds.width;
1168           dBounds.width = w;
1169           dBounds.y = h - dBounds.height;
1170           separator.setBounds(sBounds);
1171           toolWindows.setBounds(tBounds);
1172           filesPane.setBounds(fBounds);
1173           descriptions.setBounds(dBounds);
1174         }
1175       }
1176     }
1177   }
1178
1179   private static class VirtualFilesRenderer extends ColoredListCellRenderer {
1180     private final SwitcherPanel mySwitcherPanel;
1181     boolean open;
1182
1183     public VirtualFilesRenderer(@NotNull SwitcherPanel switcherPanel) {
1184       mySwitcherPanel = switcherPanel;
1185     }
1186
1187     protected void customizeCellRenderer(JList list, Object value, int index, boolean selected, boolean hasFocus) {
1188       if (value instanceof FileInfo) {
1189         Project project = mySwitcherPanel.project;
1190         VirtualFile virtualFile = ((FileInfo)value).getFirst();
1191         String renderedName = ((FileInfo)value).getNameForRendering();
1192         setIcon(IconUtil.getIcon(virtualFile, Iconable.ICON_FLAG_READ_STATUS, project));
1193
1194         FileStatus fileStatus = FileStatusManager.getInstance(project).getStatus(virtualFile);
1195         open = FileEditorManager.getInstance(project).isFileOpen(virtualFile);
1196         TextAttributes attributes = new TextAttributes(fileStatus.getColor(), null, null, EffectType.LINE_UNDERSCORE, Font.PLAIN);
1197         append(renderedName, SimpleTextAttributes.fromTextAttributes(attributes));
1198
1199         // calc color the same way editor tabs do this, i.e. including EPs
1200         Color color = EditorTabbedContainer.calcTabColor(project, virtualFile);
1201
1202         if (!selected && color != null) {
1203           setBackground(color);
1204         }
1205         SpeedSearchUtil.applySpeedSearchHighlighting(mySwitcherPanel, this, false, selected);
1206       }
1207     }
1208   }
1209
1210   private static class FileInfo extends Pair<VirtualFile, EditorWindow> {
1211     private final Project myProject;
1212     private String myNameForRendering;
1213
1214     public FileInfo(VirtualFile first, EditorWindow second, Project project) {
1215       super(first, second);
1216       myProject = project;
1217     }
1218
1219     String getNameForRendering() {
1220       if (myNameForRendering == null) {
1221         // calc name the same way editor tabs do this, i.e. including EPs
1222         myNameForRendering = EditorTabbedContainer.calcTabTitle(myProject, first);
1223       }
1224       return myNameForRendering;
1225     }
1226   }
1227
1228   private static class MyList extends JBList {
1229     public MyList(@NotNull DefaultListModel model) {
1230       super(model);
1231     }
1232
1233     @Override
1234     public void processKeyEvent(@NotNull KeyEvent e) {
1235       super.processKeyEvent(e);
1236     }
1237   }
1238 }