Merge remote-tracking branch 'origin/master'
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / AbstractExpandableItemsHandler.java
1 /*
2  * Copyright 2000-2014 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;
17
18 import com.intellij.openapi.application.ApplicationManager;
19 import com.intellij.openapi.util.Comparing;
20 import com.intellij.openapi.util.Pair;
21 import com.intellij.openapi.util.registry.Registry;
22 import com.intellij.ui.border.CustomLineBorder;
23 import com.intellij.ui.popup.AbstractPopup;
24 import com.intellij.ui.popup.MovablePopup;
25 import com.intellij.util.Alarm;
26 import com.intellij.util.ui.UIUtil;
27 import org.jetbrains.annotations.NotNull;
28 import org.jetbrains.annotations.Nullable;
29
30 import javax.swing.*;
31 import java.awt.*;
32 import java.awt.event.*;
33 import java.awt.image.BufferedImage;
34 import java.util.Collection;
35 import java.util.Collections;
36
37 public abstract class AbstractExpandableItemsHandler<KeyType, ComponentType extends JComponent> implements ExpandableItemsHandler<KeyType> {
38   protected final ComponentType myComponent;
39
40   private final Alarm myUpdateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
41   private final CellRendererPane myRendererPane = new CellRendererPane();
42   private final JComponent myTipComponent = new JComponent() {
43     @Override
44     protected void paintComponent(Graphics g) {
45       Insets insets = getInsets();
46       UIUtil.drawImage(g, myImage, insets.left, insets.top, null);
47     }
48   };
49
50   private boolean myEnabled = Registry.is("ide.expansion.hints.enabled");
51   private final MovablePopup myPopup;
52   private KeyType myKey;
53   private Rectangle myKeyItemBounds;
54   private BufferedImage myImage;
55
56   protected AbstractExpandableItemsHandler(@NotNull final ComponentType component) {
57     myComponent = component;
58     myComponent.add(myRendererPane);
59     myComponent.validate();
60     myPopup = new MovablePopup(myComponent, myTipComponent);
61
62     MouseAdapter tipMouseAdapter = new MouseAdapter() {
63       @Override
64       public void mouseExited(MouseEvent e) {
65         // don't hide the hint if mouse exited to myComponent
66         if (myComponent.getMousePosition() == null) {
67           hideHint();
68         }
69       }
70
71       @Override
72       public void mouseWheelMoved(MouseWheelEvent e) {
73         Point p = e.getLocationOnScreen();
74         SwingUtilities.convertPointFromScreen(p, myComponent);
75         myComponent.dispatchEvent(new MouseWheelEvent(myComponent,
76                                                         e.getID(),
77                                                         e.getWhen(),
78                                                         e.getModifiers(),
79                                                         p.x, p.y,
80                                                         e.getClickCount(),
81                                                         e.isPopupTrigger(),
82                                                         e.getScrollType(),
83                                                         e.getScrollAmount(),
84                                                         e.getWheelRotation()));
85       }
86
87       @Override
88       public void mouseClicked(MouseEvent e) {
89         Point p = e.getLocationOnScreen();
90         SwingUtilities.convertPointFromScreen(p, myComponent);
91         myComponent.dispatchEvent(new MouseEvent(myComponent,
92                                                       e.getID(),
93                                                       e.getWhen(),
94                                                       e.getModifiers(),
95                                                       p.x, p.y,
96                                                       e.getClickCount(),
97                                                       e.isPopupTrigger(),
98                                                       e.getButton()));
99       }
100
101       @Override
102       public void mousePressed(MouseEvent e) {
103         mouseClicked(e);
104       }
105
106       @Override
107       public void mouseReleased(MouseEvent e) {
108         mouseClicked(e);
109       }
110
111       @Override
112       public void mouseMoved(MouseEvent e) {
113         mouseClicked(e);
114       }
115
116       @Override
117       public void mouseDragged(MouseEvent e) {
118         mouseClicked(e);
119       }
120     };
121     myTipComponent.addMouseListener(tipMouseAdapter);
122     myTipComponent.addMouseWheelListener(tipMouseAdapter);
123     myTipComponent.addMouseMotionListener(tipMouseAdapter);
124
125     myComponent.addMouseListener(
126       new MouseListener() {
127         @Override
128         public void mouseEntered(MouseEvent e) {
129           handleMouseEvent(e);
130         }
131
132         @Override
133         public void mouseExited(MouseEvent e) {
134           // don't hide the hint if mouse exited to it
135           if (myTipComponent.getMousePosition() == null) {
136             hideHint();
137           }
138         }
139
140         @Override
141         public void mouseClicked(MouseEvent e) {
142         }
143
144         @Override
145         public void mousePressed(MouseEvent e) {
146           handleMouseEvent(e);
147         }
148
149         @Override
150         public void mouseReleased(MouseEvent e) {
151           handleMouseEvent(e);
152         }
153       }
154     );
155
156     myComponent.addMouseMotionListener(
157       new MouseMotionListener() {
158         @Override
159         public void mouseDragged(MouseEvent e) {
160           handleMouseEvent(e);
161         }
162
163         @Override
164         public void mouseMoved(MouseEvent e) {
165           handleMouseEvent(e, false);
166         }
167       }
168     );
169
170     myComponent.addFocusListener(
171       new FocusAdapter() {
172         @Override
173         public void focusLost(FocusEvent e) {
174           onFocusLost();
175         }
176
177         @Override
178         public void focusGained(FocusEvent e) {
179           updateCurrentSelection();
180         }
181       }
182     );
183
184     myComponent.addComponentListener(
185       new ComponentAdapter() {
186         @Override
187         public void componentHidden(ComponentEvent e) {
188           hideHint();
189         }
190
191         @Override
192         public void componentMoved(ComponentEvent e) {
193           updateCurrentSelection();
194         }
195
196         @Override
197         public void componentResized(ComponentEvent e) {
198           updateCurrentSelection();
199         }
200       }
201     );
202
203     myComponent.addHierarchyBoundsListener(new HierarchyBoundsAdapter() {
204       @Override
205       public void ancestorMoved(HierarchyEvent e) {
206         updateCurrentSelection();
207       }
208
209       @Override
210       public void ancestorResized(HierarchyEvent e) {
211         updateCurrentSelection();
212       }
213     });
214
215     myComponent.addHierarchyListener(
216       new HierarchyListener() {
217         @Override
218         public void hierarchyChanged(HierarchyEvent e) {
219           hideHint();
220         }
221       }
222     );
223   }
224
225   protected void onFocusLost() {
226     hideHint();
227   }
228
229   @Override
230   public void setEnabled(boolean enabled) {
231     myEnabled = enabled;
232     if (!myEnabled) hideHint();
233   }
234
235   @Override
236   public boolean isEnabled() {
237     return myEnabled;
238   }
239
240   @NotNull
241   @Override
242   public Collection<KeyType> getExpandedItems() {
243     return myKey == null ? Collections.<KeyType>emptyList() : Collections.singleton(myKey);
244   }
245
246   protected void updateCurrentSelection() {
247     handleSelectionChange(myKey, true);
248   }
249
250   private void handleMouseEvent(MouseEvent e) {
251     handleMouseEvent(e, true);
252   }
253
254   protected void handleMouseEvent(MouseEvent e, boolean forceUpdate) {
255     KeyType selected = getCellKeyForPoint(e.getPoint());
256     if (forceUpdate || !Comparing.equal(myKey, selected)) {
257       handleSelectionChange(selected, true);
258     }
259   }
260
261   protected void handleSelectionChange(KeyType selected) {
262     handleSelectionChange(selected, false);
263   }
264
265   protected void handleSelectionChange(final KeyType selected, final boolean processIfUnfocused) {
266     if (!ApplicationManager.getApplication().isDispatchThread()) {
267       return;
268     }
269     myUpdateAlarm.cancelAllRequests();
270     if (selected == null) {
271       hideHint();
272       return;
273     }
274     if (!selected.equals(myKey)) {
275       hideHint();
276     }
277     myUpdateAlarm.addRequest(new Runnable() {
278       @Override
279       public void run() {
280         doHandleSelectionChange(selected, processIfUnfocused);
281       }
282     }, 10);
283   }
284
285   private void doHandleSelectionChange(@NotNull KeyType selected, boolean processIfUnfocused) {
286     if (!myEnabled
287         || !myComponent.isEnabled()
288         || !myComponent.isShowing()
289         || !myComponent.getVisibleRect().intersects(getVisibleRect(selected))
290         || !myComponent.isFocusOwner() && !processIfUnfocused
291         || isPopup()) {
292       hideHint();
293       return;
294     }
295
296     myKey = selected;
297
298     Point location = createToolTipImage(myKey);
299
300     if (location == null) {
301       hideHint();
302     }
303     else {
304       Dimension size = myTipComponent.getPreferredSize();
305       myPopup.setBounds(location.x, location.y, size.width, size.height);
306       myPopup.setHeavyWeight(hasOwnedWindows());
307       if (!myPopup.isVisible()) {
308         myPopup.setVisible(true);
309       }
310       repaintKeyItem();
311     }
312   }
313
314   protected boolean isPopup() {
315     Window window = SwingUtilities.getWindowAncestor(myComponent);
316     return window != null
317            && !(window instanceof Dialog || window instanceof Frame)
318            && !isHintsAllowed(window);
319   }
320
321   private static boolean isHintsAllowed(Window window) {
322     if (window instanceof RootPaneContainer) {
323       final JRootPane pane = ((RootPaneContainer)window).getRootPane();
324       if (pane != null) {
325         return Boolean.TRUE.equals(pane.getClientProperty(AbstractPopup.SHOW_HINTS));
326       }
327     }
328     return false;
329   }
330
331   private boolean hasOwnedWindows() {
332     Window owner = SwingUtilities.getWindowAncestor(myComponent);
333     Window popup = SwingUtilities.getWindowAncestor(myTipComponent);
334     for (Window other : owner.getOwnedWindows()) {
335       if (popup != other && other.isVisible()) {
336         return false;
337       }
338     }
339     return true;
340   }
341
342   private void hideHint() {
343     myUpdateAlarm.cancelAllRequests();
344     if (myPopup.isVisible()) {
345       myPopup.setVisible(false);
346       repaintKeyItem();
347     }
348     myKey = null;
349   }
350
351   public boolean isShowing() {
352     return myPopup.isVisible();
353   }
354
355   private void repaintKeyItem() {
356     if (myKeyItemBounds != null) {
357       myComponent.repaint(myKeyItemBounds);
358     }
359   }
360
361   @Nullable
362   private Point createToolTipImage(@NotNull KeyType key) {
363     Pair<Component, Rectangle> rendererAndBounds = getCellRendererAndBounds(key);
364     if (rendererAndBounds == null) return null;
365
366     Component renderer = rendererAndBounds.first;
367     if (!(renderer instanceof JComponent)) return null;
368
369     myKeyItemBounds = rendererAndBounds.second;
370
371     Rectangle cellBounds = myKeyItemBounds;
372     Rectangle visibleRect = getVisibleRect(key);
373
374     if (cellBounds.y < visibleRect.y) return null;
375
376     int cellMaxY = cellBounds.y + cellBounds.height;
377     int visMaxY = visibleRect.y + visibleRect.height;
378     if (cellMaxY > visMaxY) return null;
379
380     int cellMaxX = cellBounds.x + cellBounds.width;
381     int visMaxX = visibleRect.x + visibleRect.width;
382
383     Point location = new Point(visMaxX, cellBounds.y);
384     SwingUtilities.convertPointToScreen(location, myComponent);
385
386     Rectangle screen = !Registry.is("ide.expansion.hints.on.all.screens")
387                        ? ScreenUtil.getScreenRectangle(location)
388                        : ScreenUtil.getAllScreensRectangle();
389
390     int borderWidth = isPaintBorder() ? 1 : 0;
391     int width = Math.min(screen.width + screen.x - location.x - borderWidth, cellMaxX - visMaxX);
392     int height = cellBounds.height;
393
394     if (width <= 0 || height <= 0) return null;
395
396     Dimension size = getImageSize(width, height);
397     myImage = UIUtil.createImage(size.width, size.height, BufferedImage.TYPE_INT_RGB);
398
399     Graphics2D g = myImage.createGraphics();
400     g.setClip(null);
401     doFillBackground(height, width, g);
402     g.translate(cellBounds.x - visMaxX, 0);
403     doPaintTooltipImage(renderer, cellBounds, g, key);
404
405     CustomLineBorder border = null;
406     if (borderWidth > 0) {
407       border = new CustomLineBorder(getBorderColor(), borderWidth, 0, borderWidth, borderWidth);
408       location.y -= borderWidth;
409       size.width += borderWidth;
410       size.height += borderWidth + borderWidth;
411     }
412
413     g.dispose();
414     myRendererPane.remove(renderer);
415
416     myTipComponent.setBorder(border);
417     myTipComponent.setPreferredSize(size);
418     return location;
419   }
420
421   protected boolean isPaintBorder() {
422     return true;
423   }
424
425   protected Color getBorderColor() {
426     return JBColor.border();
427   }
428
429   protected Dimension getImageSize(final int width, final int height) {
430     return new Dimension(width, height);
431   }
432
433   protected void doFillBackground(int height, int width, Graphics2D g) {
434     g.setColor(myComponent.getBackground());
435     g.fillRect(0, 0, width, height);
436   }
437
438   protected void doPaintTooltipImage(Component rComponent, Rectangle cellBounds, Graphics2D g, KeyType key) {
439     myRendererPane.paintComponent(g, rComponent, myComponent, 0, 0, cellBounds.width, cellBounds.height, true);
440   }
441
442   protected Rectangle getVisibleRect(KeyType key) {
443     return myComponent.getVisibleRect();
444   }
445
446   @Nullable
447   protected abstract Pair<Component, Rectangle> getCellRendererAndBounds(KeyType key);
448
449   protected abstract KeyType getCellKeyForPoint(Point point);
450 }