/* * Copyright 2000-2015 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.ui; import com.intellij.codeInsight.hint.TooltipController; import com.intellij.ide.IdeTooltip; import com.intellij.ide.IdeTooltipManager; import com.intellij.ide.TooltipEvent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.util.UserDataHolderBase; import com.intellij.openapi.wm.ex.LayoutFocusTraversalPolicyExt; import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.components.panels.OpaquePanel; import org.jetbrains.annotations.NotNull; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.LineBorder; import javax.swing.event.EventListenerList; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.EventListener; import java.util.EventObject; public class LightweightHint extends UserDataHolderBase implements Hint { private static final Logger LOG = Logger.getInstance("#com.intellij.ui.LightweightHint"); private final JComponent myComponent; private JComponent myFocusBackComponent; private final EventListenerList myListenerList = new EventListenerList(); private MyEscListener myEscListener; private JBPopup myPopup; private JComponent myParentComponent; private boolean myIsRealPopup = false; private boolean myForceLightweightPopup = false; private boolean mySelectingHint; private boolean myForceShowAsPopup = false; private String myTitle = null; private boolean myCancelOnClickOutside = true; private boolean myCancelOnOtherWindowOpen = true; private boolean myResizable; private IdeTooltip myCurrentIdeTooltip; private HintHint myHintHint; private JComponent myFocusRequestor; private boolean myForceHideShadow = false; public LightweightHint(@NotNull final JComponent component) { myComponent = component; } public void setForceLightweightPopup(final boolean forceLightweightPopup) { myForceLightweightPopup = forceLightweightPopup; } public void setForceShowAsPopup(final boolean forceShowAsPopup) { myForceShowAsPopup = forceShowAsPopup; } public void setFocusRequestor(JComponent c) { myFocusRequestor = c; } public void setTitle(final String title) { myTitle = title; } public boolean isSelectingHint() { return mySelectingHint; } public void setSelectingHint(final boolean selectingHint) { mySelectingHint = selectingHint; } public void setCancelOnClickOutside(final boolean b) { myCancelOnClickOutside = b; } public void setCancelOnOtherWindowOpen(final boolean b) { myCancelOnOtherWindowOpen = b; } public void setResizable(final boolean b) { myResizable = b; } protected boolean canAutoHideOn(TooltipEvent event) { return true; } /** * Shows the hint in the layered pane. Coordinates <code>x</code> and <code>y</code> * are in <code>parentComponent</code> coordinate system. Note that the component * appears on 250 layer. */ @Override public void show(@NotNull final JComponent parentComponent, final int x, final int y, final JComponent focusBackComponent, @NotNull final HintHint hintHint) { myParentComponent = parentComponent; myHintHint = hintHint; myFocusBackComponent = focusBackComponent; LOG.assertTrue(myParentComponent.isShowing()); myEscListener = new MyEscListener(); myComponent.registerKeyboardAction(myEscListener, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); myComponent.registerKeyboardAction(myEscListener, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_FOCUSED); final JLayeredPane layeredPane = parentComponent.getRootPane().getLayeredPane(); myComponent.validate(); if (!myForceShowAsPopup && (myForceLightweightPopup || fitsLayeredPane(layeredPane, myComponent, new RelativePoint(parentComponent, new Point(x, y)), hintHint))) { beforeShow(); final Dimension preferredSize = myComponent.getPreferredSize(); if (hintHint.isAwtTooltip()) { IdeTooltip tooltip = new IdeTooltip(hintHint.getOriginalComponent(), hintHint.getOriginalPoint(), myComponent, hintHint, myComponent) { @Override protected boolean canAutohideOn(TooltipEvent event) { if (!LightweightHint.this.canAutoHideOn(event)) { return false; } else if (event.getInputEvent() instanceof MouseEvent) { return !(hintHint.isContentActive() && event.isIsEventInsideBalloon()); } else if (event.getAction() != null) { return false; } else { return true; } } @Override protected void onHidden() { fireHintHidden(); TooltipController.getInstance().resetCurrent(); } @Override public boolean canBeDismissedOnTimeout() { return false; } }.setToCenterIfSmall(hintHint.isMayCenterTooltip()) .setPreferredPosition(hintHint.getPreferredPosition()) .setHighlighterType(hintHint.isHighlighterType()) .setTextForeground(hintHint.getTextForeground()) .setTextBackground(hintHint.getTextBackground()) .setBorderColor(hintHint.getBorderColor()) .setBorderInsets(hintHint.getBorderInsets()) .setFont(hintHint.getTextFont()) .setCalloutShift(hintHint.getCalloutShift()) .setPositionChangeShift(hintHint.getPositionChangeX(), hintHint.getPositionChangeY()) .setExplicitClose(hintHint.isExplicitClose()) .setRequestFocus(hintHint.isRequestFocus()) .setHint(true); myComponent.validate(); myCurrentIdeTooltip = IdeTooltipManager.getInstance().show(tooltip, hintHint.isShowImmediately(), hintHint.isAnimationEnabled()); } else { final Point layeredPanePoint = SwingUtilities.convertPoint(parentComponent, x, y, layeredPane); myComponent.setBounds(layeredPanePoint.x, layeredPanePoint.y, preferredSize.width, preferredSize.height); layeredPane.add(myComponent, JLayeredPane.POPUP_LAYER); myComponent.validate(); myComponent.repaint(); } } else { myIsRealPopup = true; Point actualPoint = new Point(x, y); JComponent actualComponent = new OpaquePanel(new BorderLayout()); actualComponent.add(myComponent, BorderLayout.CENTER); if (isAwtTooltip()) { int inset = BalloonImpl.getNormalInset(); actualComponent.setBorder(new LineBorder(hintHint.getTextBackground(), inset)); actualComponent.setBackground(hintHint.getTextBackground()); actualComponent.validate(); } myPopup = JBPopupFactory.getInstance().createComponentPopupBuilder(actualComponent, myFocusRequestor) .setRequestFocus(myFocusRequestor != null) .setFocusable(myFocusRequestor != null) .setResizable(myResizable) .setMovable(myTitle != null) .setTitle(myTitle) .setModalContext(false) .setShowShadow(isRealPopup() && !isForceHideShadow()) .setCancelKeyEnabled(false) .setCancelOnClickOutside(myCancelOnClickOutside) .setCancelCallback(() -> { onPopupCancel(); return true; }) .setCancelOnOtherWindowOpen(myCancelOnOtherWindowOpen) .createPopup(); beforeShow(); myPopup.show(new RelativePoint(myParentComponent, new Point(actualPoint.x, actualPoint.y))); } } protected void onPopupCancel() { } private void fixActualPoint(Point actualPoint) { if (!isAwtTooltip()) return; if (!myIsRealPopup) return; Dimension size = myComponent.getPreferredSize(); Balloon.Position position = myHintHint.getPreferredPosition(); int shift = BalloonImpl.getPointerLength(position, false); switch (position) { case below: actualPoint.y += shift; break; case above: actualPoint.y -= (shift + size.height); break; case atLeft: actualPoint.x -= (shift + size.width); break; case atRight: actualPoint.y += shift; break; } } protected void beforeShow() { } public boolean vetoesHiding() { return false; } public boolean isForceHideShadow() { return myForceHideShadow; } public void setForceHideShadow(boolean forceHideShadow) { myForceHideShadow = forceHideShadow; } private static boolean fitsLayeredPane(JLayeredPane pane, JComponent component, RelativePoint desiredLocation, HintHint hintHint) { if (hintHint.isAwtTooltip()) { Dimension size = component.getPreferredSize(); Dimension paneSize = pane.getSize(); Point target = desiredLocation.getPointOn(pane).getPoint(); Balloon.Position pos = hintHint.getPreferredPosition(); int pointer = BalloonImpl.getPointerLength(pos, false) + BalloonImpl.getNormalInset(); if (pos == Balloon.Position.above || pos == Balloon.Position.below) { boolean heightFit = target.y - size.height - pointer > 0 || target.y + size.height + pointer < paneSize.height; return heightFit && size.width + pointer < paneSize.width; } else { boolean widthFit = target.x - size.width - pointer > 0 || target.x + size.width + pointer < paneSize.width; return widthFit && size.height + pointer < paneSize.height; } } else { final Rectangle lpRect = new Rectangle(pane.getLocationOnScreen().x, pane.getLocationOnScreen().y, pane.getWidth(), pane.getHeight()); Rectangle componentRect = new Rectangle(desiredLocation.getScreenPoint().x, desiredLocation.getScreenPoint().y, component.getPreferredSize().width, component.getPreferredSize().height); return lpRect.contains(componentRect); } } private void fireHintHidden() { final EventListener[] listeners = myListenerList.getListeners(HintListener.class); for (EventListener listener : listeners) { ((HintListener)listener).hintHidden(new EventObject(this)); } } /** * @return bounds of hint component in the parent component's layered pane coordinate system. */ public final Rectangle getBounds() { Rectangle bounds = new Rectangle(myComponent.getBounds()); final JLayeredPane layeredPane = myParentComponent.getRootPane().getLayeredPane(); return SwingUtilities.convertRectangle(myComponent, bounds, layeredPane); } @Override public boolean isVisible() { if (myIsRealPopup) { return myPopup != null && myPopup.isVisible(); } if (myCurrentIdeTooltip != null) { return myComponent.isShowing() || IdeTooltipManager.getInstance().isQueuedToShow(myCurrentIdeTooltip); } return myComponent.isShowing(); } public final boolean isRealPopup() { return myIsRealPopup || myForceShowAsPopup; } @Override public void hide() { hide(false); } public void hide(boolean ok) { if (isVisible()) { if (myIsRealPopup) { if (ok) { myPopup.closeOk(null); } else { myPopup.cancel(); } myPopup = null; } else { if (myCurrentIdeTooltip != null) { IdeTooltip tooltip = myCurrentIdeTooltip; myCurrentIdeTooltip = null; tooltip.hide(); } else { JRootPane rootPane = myComponent.getRootPane(); JLayeredPane layeredPane = rootPane == null ? null : rootPane.getLayeredPane(); if (layeredPane != null) { Rectangle bounds = myComponent.getBounds(); try { if (myFocusBackComponent != null) { LayoutFocusTraversalPolicyExt.setOverridenDefaultComponent(myFocusBackComponent); } layeredPane.remove(myComponent); } finally { LayoutFocusTraversalPolicyExt.setOverridenDefaultComponent(null); } layeredPane.paintImmediately(bounds.x, bounds.y, bounds.width, bounds.height); } } } } if (myEscListener != null) { myComponent.unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0)); } TooltipController.getInstance().hide(this); fireHintHidden(); } @Override public void pack() { setSize(myComponent.getPreferredSize()); } public void updateLocation(int x, int y) { Point point = new Point(x, y); fixActualPoint(point); setLocation(new RelativePoint(myParentComponent, point)); } public void updatePosition(Balloon.Position position) { if (myHintHint != null) { myHintHint.setPreferredPosition(position); } if (myCurrentIdeTooltip != null) { myCurrentIdeTooltip.setPreferredPosition(position); } } public final JComponent getComponent() { return myComponent; } @Override public final void addHintListener(@NotNull final HintListener listener) { myListenerList.add(HintListener.class, listener); } @Override public final void removeHintListener(@NotNull final HintListener listener) { myListenerList.remove(HintListener.class, listener); } public Point getLocationOn(JComponent c) { Point location; if (isRealPopup() && !myPopup.isDisposed()) { location = myPopup.getLocationOnScreen(); SwingUtilities.convertPointFromScreen(location, c); } else { if (myCurrentIdeTooltip != null) { Point tipPoint = myCurrentIdeTooltip.getPoint(); Component tipComponent = myCurrentIdeTooltip.getComponent(); return SwingUtilities.convertPoint(tipComponent, tipPoint, c); } else { location = SwingUtilities.convertPoint( myComponent.getParent(), myComponent.getLocation(), c ); } } return location; } @Override public void setLocation(@NotNull RelativePoint point) { if (isRealPopup()) { myPopup.setLocation(point.getScreenPoint()); } else { if (myCurrentIdeTooltip != null) { Point screenPoint = point.getScreenPoint(); if (!screenPoint.equals(new RelativePoint(myCurrentIdeTooltip.getComponent(), myCurrentIdeTooltip.getPoint()).getScreenPoint())) { myCurrentIdeTooltip.setPoint(point.getPoint()); myCurrentIdeTooltip.setComponent(point.getComponent()); IdeTooltipManager.getInstance().show(myCurrentIdeTooltip, true, false); } } else { Point targetPoint = point.getPoint(myComponent.getParent()); myComponent.setLocation(targetPoint); myComponent.revalidate(); myComponent.repaint(); } } } public void setSize(final Dimension size) { if (myIsRealPopup && myPopup != null) { // There is a possible case that a popup wraps target content component into other components which might have borders. // That's why we can't just apply component's size to the whole popup. It needs to be adjusted before that. JComponent popupContent = myPopup.getContent(); int widthExpand = 0; int heightExpand = 0; boolean adjustSize = false; JComponent prev = myComponent; for (Container c = myComponent.getParent(); c != null; c = c.getParent()) { if (c == popupContent) { adjustSize = true; break; } if (c instanceof JComponent) { Border border = ((JComponent)c).getBorder(); if (prev != null && border != null) { Insets insets = border.getBorderInsets(prev); widthExpand += insets.left + insets.right; heightExpand += insets.top + insets.bottom; } prev = (JComponent)c; } else { prev = null; } } Dimension sizeToUse = size; if (adjustSize && (widthExpand != 0 || heightExpand != 0)) { sizeToUse = new Dimension(size.width + widthExpand, size.height + heightExpand); } myPopup.setSize(sizeToUse); } else if (!isAwtTooltip()) { myComponent.setSize(size); myComponent.revalidate(); myComponent.repaint(); } } public boolean isAwtTooltip() { return myHintHint != null && myHintHint.isAwtTooltip(); } public Dimension getSize() { return myComponent.getSize(); } public boolean isInsideHint(RelativePoint target) { if (myComponent == null || !myComponent.isShowing()) return false; if (myIsRealPopup) { Window wnd = SwingUtilities.getWindowAncestor(myComponent); return wnd.getBounds().contains(target.getScreenPoint()); } else if (myCurrentIdeTooltip != null) { return myCurrentIdeTooltip.isInside(target); } else { return new Rectangle(myComponent.getLocationOnScreen(), myComponent.getSize()).contains(target.getScreenPoint()); } } private final class MyEscListener implements ActionListener { @Override public final void actionPerformed(final ActionEvent e) { hide(); } } @Override public String toString() { return getComponent().toString(); } public boolean canControlAutoHide() { return myCurrentIdeTooltip != null && myCurrentIdeTooltip.getTipComponent().isShowing(); } public IdeTooltip getCurrentIdeTooltip() { return myCurrentIdeTooltip; } }