blob: 2f4841f1323696f285ba0c2b5c7b140a640db949 [file] [log] [blame]
/*
* 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;
}
}