| /* |
| * Copyright 2000-2013 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.Computable; |
| 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; |
| } |
| |
| /** |
| * 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 (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()) |
| .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()) { |
| fixActualPoint(actualPoint); |
| |
| |
| 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(new Computable<Boolean>() { |
| @Override |
| public Boolean compute() { |
| 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 layered pane. |
| */ |
| public final Rectangle getBounds() { |
| return myComponent.getBounds(); |
| } |
| |
| @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 { |
| final JRootPane rootPane = myComponent.getRootPane(); |
| if (rootPane != null) { |
| final Rectangle bounds = myComponent.getBounds(); |
| final JLayeredPane layeredPane = rootPane.getLayeredPane(); |
| |
| 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()); |
| } |
| |
| @Override |
| public void updateBounds(int x, int y) { |
| setSize(myComponent.getPreferredSize()); |
| updateLocation(x, y); |
| } |
| |
| public void updateLocation(int x, int y) { |
| Point point = new Point(x, y); |
| fixActualPoint(point); |
| setLocation(new RelativePoint(myParentComponent, point)); |
| } |
| |
| 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()) { |
| 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; |
| } |
| } |