2 * Copyright 2000-2013 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.ui;
18 import com.intellij.codeInsight.hint.TooltipController;
19 import com.intellij.ide.IdeTooltip;
20 import com.intellij.ide.IdeTooltipManager;
21 import com.intellij.ide.TooltipEvent;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.ui.popup.Balloon;
24 import com.intellij.openapi.ui.popup.JBPopup;
25 import com.intellij.openapi.ui.popup.JBPopupFactory;
26 import com.intellij.openapi.util.Computable;
27 import com.intellij.openapi.util.UserDataHolderBase;
28 import com.intellij.openapi.wm.ex.LayoutFocusTraversalPolicyExt;
29 import com.intellij.ui.awt.RelativePoint;
30 import com.intellij.ui.components.panels.OpaquePanel;
31 import org.jetbrains.annotations.NotNull;
34 import javax.swing.border.Border;
35 import javax.swing.border.LineBorder;
36 import javax.swing.event.EventListenerList;
38 import java.awt.event.ActionEvent;
39 import java.awt.event.ActionListener;
40 import java.awt.event.KeyEvent;
41 import java.awt.event.MouseEvent;
42 import java.util.EventListener;
43 import java.util.EventObject;
45 public class LightweightHint extends UserDataHolderBase implements Hint {
46 private static final Logger LOG = Logger.getInstance("#com.intellij.ui.LightweightHint");
48 private final JComponent myComponent;
49 private JComponent myFocusBackComponent;
50 private final EventListenerList myListenerList = new EventListenerList();
51 private MyEscListener myEscListener;
52 private JBPopup myPopup;
53 private JComponent myParentComponent;
54 private boolean myIsRealPopup = false;
55 private boolean myForceLightweightPopup = false;
56 private boolean mySelectingHint;
58 private boolean myForceShowAsPopup = false;
59 private String myTitle = null;
60 private boolean myCancelOnClickOutside = true;
61 private boolean myCancelOnOtherWindowOpen = true;
62 private boolean myResizable;
64 private IdeTooltip myCurrentIdeTooltip;
65 private HintHint myHintHint;
66 private JComponent myFocusRequestor;
68 private boolean myForceHideShadow = false;
70 public LightweightHint(@NotNull final JComponent component) {
71 myComponent = component;
74 public void setForceLightweightPopup(final boolean forceLightweightPopup) {
75 myForceLightweightPopup = forceLightweightPopup;
79 public void setForceShowAsPopup(final boolean forceShowAsPopup) {
80 myForceShowAsPopup = forceShowAsPopup;
83 public void setFocusRequestor(JComponent c) {
87 public void setTitle(final String title) {
91 public boolean isSelectingHint() {
92 return mySelectingHint;
95 public void setSelectingHint(final boolean selectingHint) {
96 mySelectingHint = selectingHint;
99 public void setCancelOnClickOutside(final boolean b) {
100 myCancelOnClickOutside = b;
103 public void setCancelOnOtherWindowOpen(final boolean b) {
104 myCancelOnOtherWindowOpen = b;
107 public void setResizable(final boolean b) {
112 * Shows the hint in the layered pane. Coordinates <code>x</code> and <code>y</code>
113 * are in <code>parentComponent</code> coordinate system. Note that the component
114 * appears on 250 layer.
117 public void show(@NotNull final JComponent parentComponent,
120 final JComponent focusBackComponent,
121 @NotNull final HintHint hintHint) {
122 myParentComponent = parentComponent;
123 myHintHint = hintHint;
125 myFocusBackComponent = focusBackComponent;
127 LOG.assertTrue(myParentComponent.isShowing());
128 myEscListener = new MyEscListener();
129 myComponent.registerKeyboardAction(myEscListener, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
130 myComponent.registerKeyboardAction(myEscListener, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_FOCUSED);
131 final JLayeredPane layeredPane = parentComponent.getRootPane().getLayeredPane();
133 myComponent.validate();
135 if (!myForceShowAsPopup &&
136 (myForceLightweightPopup ||
137 fitsLayeredPane(layeredPane, myComponent, new RelativePoint(parentComponent, new Point(x, y)), hintHint))) {
139 final Dimension preferredSize = myComponent.getPreferredSize();
142 if (hintHint.isAwtTooltip()) {
144 new IdeTooltip(hintHint.getOriginalComponent(), hintHint.getOriginalPoint(), myComponent, hintHint, myComponent) {
146 protected boolean canAutohideOn(TooltipEvent event) {
147 if (event.getInputEvent() instanceof MouseEvent) {
148 return !(hintHint.isContentActive() && event.isIsEventInsideBalloon());
150 else if (event.getAction() != null) {
159 protected void onHidden() {
161 TooltipController.getInstance().resetCurrent();
165 public boolean canBeDismissedOnTimeout() {
168 }.setToCenterIfSmall(hintHint.isMayCenterTooltip())
169 .setPreferredPosition(hintHint.getPreferredPosition())
170 .setHighlighterType(hintHint.isHighlighterType())
171 .setTextForeground(hintHint.getTextForeground())
172 .setTextBackground(hintHint.getTextBackground())
173 .setBorderColor(hintHint.getBorderColor())
174 .setBorderInsets(hintHint.getBorderInsets())
175 .setFont(hintHint.getTextFont())
176 .setCalloutShift(hintHint.getCalloutShift())
177 .setPositionChangeShift(hintHint.getPositionChangeX(), hintHint.getPositionChangeY())
178 .setExplicitClose(hintHint.isExplicitClose())
180 myComponent.validate();
181 myCurrentIdeTooltip = IdeTooltipManager.getInstance().show(tooltip, hintHint.isShowImmediately(), hintHint.isAnimationEnabled());
184 final Point layeredPanePoint = SwingUtilities.convertPoint(parentComponent, x, y, layeredPane);
185 myComponent.setBounds(layeredPanePoint.x, layeredPanePoint.y, preferredSize.width, preferredSize.height);
186 layeredPane.add(myComponent, JLayeredPane.POPUP_LAYER);
188 myComponent.validate();
189 myComponent.repaint();
193 myIsRealPopup = true;
194 Point actualPoint = new Point(x, y);
195 JComponent actualComponent = new OpaquePanel(new BorderLayout());
196 actualComponent.add(myComponent, BorderLayout.CENTER);
197 if (isAwtTooltip()) {
198 int inset = BalloonImpl.getNormalInset();
199 actualComponent.setBorder(new LineBorder(hintHint.getTextBackground(), inset));
200 actualComponent.setBackground(hintHint.getTextBackground());
201 actualComponent.validate();
204 myPopup = JBPopupFactory.getInstance().createComponentPopupBuilder(actualComponent, myFocusRequestor)
205 .setRequestFocus(myFocusRequestor != null)
206 .setFocusable(myFocusRequestor != null)
207 .setResizable(myResizable)
208 .setMovable(myTitle != null)
210 .setModalContext(false)
211 .setShowShadow(isRealPopup() && !isForceHideShadow())
212 .setCancelKeyEnabled(false)
213 .setCancelOnClickOutside(myCancelOnClickOutside)
214 .setCancelCallback(new Computable<Boolean>() {
216 public Boolean compute() {
221 .setCancelOnOtherWindowOpen(myCancelOnOtherWindowOpen)
225 myPopup.show(new RelativePoint(myParentComponent, new Point(actualPoint.x, actualPoint.y)));
229 protected void onPopupCancel() {
232 private void fixActualPoint(Point actualPoint) {
233 if (!isAwtTooltip()) return;
234 if (!myIsRealPopup) return;
236 Dimension size = myComponent.getPreferredSize();
237 Balloon.Position position = myHintHint.getPreferredPosition();
238 int shift = BalloonImpl.getPointerLength(position, false);
241 actualPoint.y += shift;
244 actualPoint.y -= (shift + size.height);
247 actualPoint.x -= (shift + size.width);
250 actualPoint.y += shift;
255 protected void beforeShow() {
259 public boolean vetoesHiding() {
263 public boolean isForceHideShadow() {
264 return myForceHideShadow;
267 public void setForceHideShadow(boolean forceHideShadow) {
268 myForceHideShadow = forceHideShadow;
271 private static boolean fitsLayeredPane(JLayeredPane pane, JComponent component, RelativePoint desiredLocation, HintHint hintHint) {
272 if (hintHint.isAwtTooltip()) {
273 Dimension size = component.getPreferredSize();
274 Dimension paneSize = pane.getSize();
276 Point target = desiredLocation.getPointOn(pane).getPoint();
277 Balloon.Position pos = hintHint.getPreferredPosition();
278 int pointer = BalloonImpl.getPointerLength(pos, false) + BalloonImpl.getNormalInset();
279 if (pos == Balloon.Position.above || pos == Balloon.Position.below) {
280 boolean heightFit = target.y - size.height - pointer > 0 || target.y + size.height + pointer < paneSize.height;
281 return heightFit && size.width + pointer < paneSize.width;
284 boolean widthFit = target.x - size.width - pointer > 0 || target.x + size.width + pointer < paneSize.width;
285 return widthFit && size.height + pointer < paneSize.height;
289 final Rectangle lpRect = new Rectangle(pane.getLocationOnScreen().x, pane.getLocationOnScreen().y, pane.getWidth(), pane.getHeight());
290 Rectangle componentRect = new Rectangle(desiredLocation.getScreenPoint().x,
291 desiredLocation.getScreenPoint().y,
292 component.getPreferredSize().width,
293 component.getPreferredSize().height);
294 return lpRect.contains(componentRect);
298 private void fireHintHidden() {
299 final EventListener[] listeners = myListenerList.getListeners(HintListener.class);
300 for (EventListener listener : listeners) {
301 ((HintListener)listener).hintHidden(new EventObject(this));
306 * @return bounds of hint component in the layered pane.
308 public final Rectangle getBounds() {
309 return myComponent.getBounds();
313 public boolean isVisible() {
315 return myPopup != null && myPopup.isVisible();
317 if (myCurrentIdeTooltip != null) {
318 return myComponent.isShowing() || IdeTooltipManager.getInstance().isQueuedToShow(myCurrentIdeTooltip);
320 return myComponent.isShowing();
323 public final boolean isRealPopup() {
324 return myIsRealPopup || myForceShowAsPopup;
332 public void hide(boolean ok) {
336 myPopup.closeOk(null);
344 if (myCurrentIdeTooltip != null) {
345 IdeTooltip tooltip = myCurrentIdeTooltip;
346 myCurrentIdeTooltip = null;
350 final JRootPane rootPane = myComponent.getRootPane();
351 if (rootPane != null) {
352 final Rectangle bounds = myComponent.getBounds();
353 final JLayeredPane layeredPane = rootPane.getLayeredPane();
356 if (myFocusBackComponent != null) {
357 LayoutFocusTraversalPolicyExt.setOverridenDefaultComponent(myFocusBackComponent);
359 layeredPane.remove(myComponent);
362 LayoutFocusTraversalPolicyExt.setOverridenDefaultComponent(null);
365 layeredPane.paintImmediately(bounds.x, bounds.y, bounds.width, bounds.height);
370 if (myEscListener != null) {
371 myComponent.unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
374 TooltipController.getInstance().hide(this);
381 setSize(myComponent.getPreferredSize());
385 public void updateBounds(int x, int y) {
386 setSize(myComponent.getPreferredSize());
387 updateLocation(x, y);
390 public void updateLocation(int x, int y) {
391 Point point = new Point(x, y);
392 fixActualPoint(point);
393 setLocation(new RelativePoint(myParentComponent, point));
396 public final JComponent getComponent() {
401 public final void addHintListener(@NotNull final HintListener listener) {
402 myListenerList.add(HintListener.class, listener);
406 public final void removeHintListener(@NotNull final HintListener listener) {
407 myListenerList.remove(HintListener.class, listener);
410 public Point getLocationOn(JComponent c) {
413 location = myPopup.getLocationOnScreen();
414 SwingUtilities.convertPointFromScreen(location, c);
417 if (myCurrentIdeTooltip != null) {
418 Point tipPoint = myCurrentIdeTooltip.getPoint();
419 Component tipComponent = myCurrentIdeTooltip.getComponent();
420 return SwingUtilities.convertPoint(tipComponent, tipPoint, c);
423 location = SwingUtilities.convertPoint(
424 myComponent.getParent(),
425 myComponent.getLocation(),
435 public void setLocation(@NotNull RelativePoint point) {
437 myPopup.setLocation(point.getScreenPoint());
440 if (myCurrentIdeTooltip != null) {
441 Point screenPoint = point.getScreenPoint();
442 if (!screenPoint.equals(new RelativePoint(myCurrentIdeTooltip.getComponent(), myCurrentIdeTooltip.getPoint()).getScreenPoint())) {
443 myCurrentIdeTooltip.setPoint(point.getPoint());
444 myCurrentIdeTooltip.setComponent(point.getComponent());
445 IdeTooltipManager.getInstance().show(myCurrentIdeTooltip, true, false);
449 Point targetPoint = point.getPoint(myComponent.getParent());
450 myComponent.setLocation(targetPoint);
452 myComponent.revalidate();
453 myComponent.repaint();
458 public void setSize(final Dimension size) {
459 if (myIsRealPopup && myPopup != null) {
460 // There is a possible case that a popup wraps target content component into other components which might have borders.
461 // That's why we can't just apply component's size to the whole popup. It needs to be adjusted before that.
462 JComponent popupContent = myPopup.getContent();
464 int heightExpand = 0;
465 boolean adjustSize = false;
466 JComponent prev = myComponent;
467 for (Container c = myComponent.getParent(); c != null; c = c.getParent()) {
468 if (c == popupContent) {
472 if (c instanceof JComponent) {
473 Border border = ((JComponent)c).getBorder();
474 if (prev != null && border != null) {
475 Insets insets = border.getBorderInsets(prev);
476 widthExpand += insets.left + insets.right;
477 heightExpand += insets.top + insets.bottom;
479 prev = (JComponent)c;
485 Dimension sizeToUse = size;
486 if (adjustSize && (widthExpand != 0 || heightExpand != 0)) {
487 sizeToUse = new Dimension(size.width + widthExpand, size.height + heightExpand);
489 myPopup.setSize(sizeToUse);
491 else if (!isAwtTooltip()) {
492 myComponent.setSize(size);
494 myComponent.revalidate();
495 myComponent.repaint();
499 public boolean isAwtTooltip() {
500 return myHintHint != null && myHintHint.isAwtTooltip();
503 public Dimension getSize() {
504 return myComponent.getSize();
507 public boolean isInsideHint(RelativePoint target) {
508 if (myComponent == null || !myComponent.isShowing()) return false;
511 Window wnd = SwingUtilities.getWindowAncestor(myComponent);
512 return wnd.getBounds().contains(target.getScreenPoint());
514 else if (myCurrentIdeTooltip != null) {
515 return myCurrentIdeTooltip.isInside(target);
518 return new Rectangle(myComponent.getLocationOnScreen(), myComponent.getSize()).contains(target.getScreenPoint());
522 private final class MyEscListener implements ActionListener {
524 public final void actionPerformed(final ActionEvent e) {
530 public String toString() {
531 return getComponent().toString();
534 public boolean canControlAutoHide() {
535 return myCurrentIdeTooltip != null && myCurrentIdeTooltip.getTipComponent().isShowing();
538 public IdeTooltip getCurrentIdeTooltip() {
539 return myCurrentIdeTooltip;