6d5d990c30f35d0132ee820d549617e9c34bcd32
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / popup / WizardPopup.java
1 /*
2  * Copyright 2000-2009 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.ui.popup;
17
18 import com.intellij.ide.DataManager;
19 import com.intellij.openapi.actionSystem.DataProvider;
20 import com.intellij.openapi.actionSystem.PlatformDataKeys;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.ui.popup.*;
24 import com.intellij.openapi.util.Disposer;
25 import com.intellij.openapi.util.Pair;
26 import com.intellij.ui.PopupBorder;
27 import com.intellij.ui.ScreenUtil;
28 import com.intellij.ui.popup.list.ListPopupImpl;
29 import com.intellij.ui.popup.tree.TreePopupImpl;
30 import com.intellij.ui.popup.util.MnemonicsSearch;
31 import com.intellij.ui.speedSearch.ElementFilter;
32 import com.intellij.ui.speedSearch.SpeedSearch;
33 import org.jetbrains.annotations.NonNls;
34 import org.jetbrains.annotations.NotNull;
35
36 import javax.swing.*;
37 import java.awt.*;
38 import java.awt.event.*;
39 import java.util.Collections;
40
41 public abstract class WizardPopup extends AbstractPopup implements ActionListener, ElementFilter {
42   private static final Logger LOG = Logger.getInstance("#com.intellij.ui.popup.WizardPopup");
43
44   private static final int AUTO_POPUP_DELAY = 750;
45   private static final Dimension MAX_SIZE = new Dimension(Integer.MAX_VALUE, 600);
46
47   protected static final int STEP_X_PADDING = 2;
48
49   private final WizardPopup myParent;
50
51   protected final PopupStep<Object> myStep;
52   protected WizardPopup myChild;
53
54   private final Timer myAutoSelectionTimer = new Timer(AUTO_POPUP_DELAY, this);
55
56   private MnemonicsSearch myMnemonicsSearch;
57   private Object myParentValue;
58
59   private Point myLastOwnerPoint;
60   private Window myOwnerWindow;
61   private MyComponentAdapter myOwnerListener;
62
63   private final ActionMap myActionMap = new ActionMap();
64   private final InputMap myInputMap = new InputMap();
65
66   public WizardPopup(PopupStep<Object> aStep) {
67     this(null, aStep);
68   }
69
70   public WizardPopup(JBPopup aParent, PopupStep<Object> aStep) {
71     myParent = (WizardPopup) aParent;
72     myStep = aStep;
73
74     mySpeedSearch.setEnabled(myStep.isSpeedSearchEnabled());
75
76     final JComponent content = createContent();
77
78     JScrollPane scrollPane = new JScrollPane(content);
79     scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
80     scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
81     scrollPane.getHorizontalScrollBar().setBorder(null);
82
83     scrollPane.getActionMap().get("unitScrollLeft").setEnabled(false);
84     scrollPane.getActionMap().get("unitScrollRight").setEnabled(false);
85
86     scrollPane.setBorder(null);
87
88     final Project project = PlatformDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext());
89     init(project, scrollPane, getPreferredFocusableComponent(), true, true, true, true, null,
90          false, aStep.getTitle(), null, true, null, false, null, null, null, false, null, true, false, true, null, 0f,
91          null, true, false, new Component[0], null, true, Collections.<Pair<ActionListener, KeyStroke>>emptyList(), null, null, false);
92
93     registerAction("disposeAll", KeyEvent.VK_ESCAPE, InputEvent.SHIFT_MASK, new AbstractAction() {
94       public void actionPerformed(ActionEvent e) {
95         if (mySpeedSearch.isHoldingFilter()) {
96           mySpeedSearch.reset();
97         }
98         else {
99           disposeAll();
100         }
101       }
102     });
103
104     AbstractAction goBackAction = new AbstractAction() {
105       public void actionPerformed(ActionEvent e) {
106         goBack();
107       }
108     };
109
110     registerAction("goBack3", KeyEvent.VK_ESCAPE, 0, goBackAction);
111
112     myMnemonicsSearch = new MnemonicsSearch(this) {
113       protected void select(Object value) {
114         onSelectByMnemonic(value);
115       }
116     };
117
118
119
120   }
121
122   private void disposeAll() {
123     WizardPopup root = PopupDispatcher.getActiveRoot();
124     disposeAllParents(null);
125     root.getStep().canceled();
126   }
127
128   public void goBack() {
129     if (mySpeedSearch.isHoldingFilter()) {
130       mySpeedSearch.reset();
131       return;
132     }
133
134     if (myParent != null) {
135       myParent.disposeChildren();
136     }
137     else {
138       disposeAll();
139     }
140   }
141
142   protected abstract JComponent createContent();
143
144   public void dispose() {
145     super.dispose();
146
147     myAutoSelectionTimer.stop();
148
149     PopupDispatcher.unsetShowing(this);
150     PopupDispatcher.clearRootIfNeeded(this);
151
152
153     if (myOwnerWindow != null && myOwnerListener != null) {
154       myOwnerWindow.removeComponentListener(myOwnerListener);
155     }
156   }
157
158
159   public void disposeChildren() {
160     if (myChild != null) {
161       myChild.disposeChildren();
162       Disposer.dispose(myChild);
163       myChild = null;
164     }
165   }
166
167   public void show(final Component owner, final int aScreenX, final int aScreenY, final boolean considerForcedXY) {
168     LOG.assertTrue (!isDisposed());
169
170     Rectangle targetBounds = new Rectangle(new Point(aScreenX, aScreenY), getContent().getPreferredSize());
171     ScreenUtil.moveRectangleToFitTheScreen(targetBounds);
172
173     if (getParent() != null) {
174       if (getParent().getBounds().intersects(targetBounds)) {
175         targetBounds.x = getParent().getBounds().x - targetBounds.width - STEP_X_PADDING;
176       }
177     }
178
179     if (getParent() == null) {
180       PopupDispatcher.setActiveRoot(this);
181     }
182     else {
183       PopupDispatcher.setShowing(this);
184     }
185
186     LOG.assertTrue (!isDisposed(), "Disposed popup, parent="+getParent());
187     super.show(owner, targetBounds.x, targetBounds.y, true);
188   }
189
190   protected void afterShow() {
191     super.afterShow();
192     registerAutoMove();
193
194     if (!myFocusTrackback.isMustBeShown()) {
195       cancel();
196     }
197   }
198
199   private void registerAutoMove() {
200     if (myOwner != null) {
201       myOwnerWindow = SwingUtilities.getWindowAncestor(myOwner);
202       if (myOwnerWindow != null) {
203         myLastOwnerPoint = myOwnerWindow.getLocationOnScreen();
204         myOwnerListener = new MyComponentAdapter();
205         myOwnerWindow.addComponentListener(myOwnerListener);
206       }
207     }
208   }
209
210   private void processParentWindowMoved() {
211     if (isDisposed()) return;
212
213     final Point newOwnerPoint = myOwnerWindow.getLocationOnScreen();
214
215     int deltaX = myLastOwnerPoint.x - newOwnerPoint.x;
216     int deltaY = myLastOwnerPoint.y - newOwnerPoint.y;
217
218     myLastOwnerPoint = newOwnerPoint;
219
220     final Window wnd = SwingUtilities.getWindowAncestor(getContent());
221     final Point current = wnd.getLocationOnScreen();
222
223     setLocation(new Point(current.x - deltaX, current.y - deltaY));
224   }
225
226   protected abstract JComponent getPreferredFocusableComponent();
227
228   public void cancel(InputEvent e) {
229     super.cancel(e);
230     disposeChildren();
231     Disposer.dispose(this);
232     getStep().canceled();
233   }
234
235   @Override
236   public boolean isCancelKeyEnabled() {
237     return super.isCancelKeyEnabled() && !mySpeedSearch.isHoldingFilter();
238   }
239
240   protected void disposeAllParents(InputEvent e) {
241     myDisposeEvent = e;
242     dispose();
243     if (myParent != null) {
244       myParent.disposeAllParents(null);
245     }
246   }
247
248   public final void registerAction(@NonNls String aActionName, int aKeyCode, int aModifier, Action aAction) {
249     myInputMap.put(KeyStroke.getKeyStroke(aKeyCode, aModifier), aActionName);
250     myActionMap.put(aActionName, aAction);
251   }
252
253   public final void registerAction(@NonNls String aActionName, KeyStroke keyStroke, Action aAction) {
254     myInputMap.put(keyStroke, aActionName);
255     myActionMap.put(aActionName, aAction);
256   }
257
258   protected abstract InputMap getInputMap();
259
260   protected abstract ActionMap getActionMap();
261
262   protected final void setParentValue(Object parentValue) {
263     myParentValue = parentValue;
264   }
265
266   @NotNull
267   protected MyContentPanel createContentPanel(final boolean resizable, final PopupBorder border, final boolean isToDrawMacCorner) {
268     return new MyContainer(resizable, border, isToDrawMacCorner);
269   }
270
271   private static class MyContainer extends MyContentPanel implements DataProvider {
272
273     private MyContainer(final boolean resizable, final PopupBorder border, final boolean drawMacCorner) {
274       super(resizable, border, drawMacCorner);
275       setOpaque(true);
276       setFocusCycleRoot(true);
277     }
278
279     public Object getData(String dataId) {
280       return null;
281     }
282
283     public Dimension getPreferredSize() {
284       return computeNotBiggerDimension(super.getPreferredSize());
285     }
286
287     private static Dimension computeNotBiggerDimension(Dimension ofContent) {
288       int resultWidth = ofContent.width > MAX_SIZE.width ? MAX_SIZE.width : ofContent.width;
289       int resultHeight = ofContent.height > MAX_SIZE.height ? MAX_SIZE.height : ofContent.height;
290
291       if (ofContent.height > MAX_SIZE.height) {
292         resultWidth += new JScrollPane().getVerticalScrollBar().getPreferredSize().getWidth();
293       }
294
295       return new Dimension(resultWidth, resultHeight);
296     }
297   }
298
299   public WizardPopup getParent() {
300     return myParent;
301   }
302
303   public PopupStep getStep() {
304     return myStep;
305   }
306
307   public final void dispatch(KeyEvent event) {
308     if (event.getID() != KeyEvent.KEY_PRESSED && event.getID() != KeyEvent.KEY_RELEASED) {
309       return;
310     }
311
312     if (event.getID() == KeyEvent.KEY_PRESSED) {
313       final KeyStroke stroke = KeyStroke.getKeyStroke(event.getKeyCode(), event.getModifiers(), false);
314       if (proceedKeyEvent(event, stroke)) return;
315     }
316
317     if (event.getID() == KeyEvent.KEY_RELEASED) {
318       final KeyStroke stroke = KeyStroke.getKeyStroke(event.getKeyCode(), event.getModifiers(), true);
319       proceedKeyEvent(event, stroke);
320       return;
321     }
322
323     myMnemonicsSearch.process(event);
324     mySpeedSearch.process(event);
325
326     if (event.isConsumed()) return;
327     process(event);
328   }
329
330   private boolean proceedKeyEvent(KeyEvent event, KeyStroke stroke) {
331     if (myInputMap.get(stroke) != null) {
332       final Action action = myActionMap.get(myInputMap.get(stroke));
333       if (action != null && action.isEnabled()) {
334         action.actionPerformed(new ActionEvent(getContent(), event.getID(), "", event.getWhen(), event.getModifiers()));
335         return true;
336       }
337     }
338     return false;
339   }
340
341   protected void process(KeyEvent aEvent) {
342
343   }
344
345   public Rectangle getBounds() {
346     return new Rectangle(getContent().getLocationOnScreen(), getContent().getSize());
347   }
348
349   protected static WizardPopup createPopup(WizardPopup parent, PopupStep step, Object parentValue) {
350     if (step instanceof ListPopupStep) {
351       return new ListPopupImpl(parent, (ListPopupStep)step, parentValue);
352     }
353     else if (step instanceof TreePopupStep) {
354       return new TreePopupImpl(parent, (TreePopupStep)step, parentValue);
355     }
356     else {
357       throw new IllegalArgumentException(step.getClass().toString());
358     }
359   }
360
361   public final void actionPerformed(ActionEvent e) {
362     myAutoSelectionTimer.stop();
363     if (getStep().isAutoSelectionEnabled()) {
364       onAutoSelectionTimer();
365     }
366   }
367
368   protected final void restartTimer() {
369     if (!myAutoSelectionTimer.isRunning()) {
370       myAutoSelectionTimer.start();
371     }
372     else {
373       myAutoSelectionTimer.restart();
374     }
375   }
376
377   protected final void stopTimer() {
378     myAutoSelectionTimer.stop();
379   }
380
381   protected void onAutoSelectionTimer() {
382
383   }
384
385   public boolean shouldBeShowing(Object value) {
386     if (!myStep.isSpeedSearchEnabled()) return true;
387     SpeedSearchFilter<Object> filter = myStep.getSpeedSearchFilter();
388     if (!filter.canBeHidden(value)) return true;
389     String text = filter.getIndexedString(value);
390     return mySpeedSearch.shouldBeShowing(text);
391   }
392
393   public SpeedSearch getSpeedSearch() {
394     return mySpeedSearch;
395   }
396
397
398   protected void onSelectByMnemonic(Object value) {
399
400   }
401
402   protected abstract void onChildSelectedFor(Object value);
403
404   protected final void notifyParentOnChildSelection() {
405     if (myParent == null || myParentValue == null) return;
406     myParent.onChildSelectedFor(myParentValue);
407   }
408
409
410   private class MyComponentAdapter extends ComponentAdapter {
411     public void componentMoved(final ComponentEvent e) {
412       processParentWindowMoved();
413     }
414   }
415
416   public final void setFinalRunnable(Runnable runnable) {
417     if (getParent() == null) {
418       super.setFinalRunnable(runnable);
419     } else {
420       getParent().setFinalRunnable(runnable);
421     }
422   }
423 }