/* * Copyright 2000-2014 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.debugger.ui.impl; import com.intellij.ide.FrameStateListener; import com.intellij.ide.FrameStateManager; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonShortcuts; import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Weighted; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.wm.IdeGlassPane; import com.intellij.openapi.wm.IdeGlassPaneUtil; import com.intellij.util.Alarm; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.update.Activatable; import com.intellij.util.ui.update.UiNotifyConnector; import com.intellij.xdebugger.settings.XDebuggerSettingsManager; import javax.swing.*; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import java.awt.*; import java.awt.event.*; /** * @author lex */ public class TipManager implements Disposable, PopupMenuListener { private volatile boolean myIsDisposed = false; private boolean myPopupShown; private MyAwtPreprocessor myHideCanceller; private MouseEvent myLastMouseEvent; public interface TipFactory { JComponent createToolTip (MouseEvent e); MouseEvent createTooltipEvent(MouseEvent candidateEvent); boolean isFocusOwner(); } private boolean isOverTip(MouseEvent e) { if (myCurrentTooltip != null) { if(!myCurrentTooltip.isShowing()) { hideTooltip(true); return false; } final Component eventOriginator = e.getComponent(); if (eventOriginator == null) { return false; } final Point point = e.getPoint(); SwingUtilities.convertPointToScreen(point, eventOriginator); final Rectangle bounds = myCurrentTooltip.getBounds(); final Point tooltipLocationOnScreen = myCurrentTooltip.getLocationOnScreen(); bounds.setLocation(tooltipLocationOnScreen.x, tooltipLocationOnScreen.y); return bounds.contains(point); } return false; } boolean myInsideComponent; private class MyMouseListener extends MouseAdapter implements Weighted{ @Override public void mouseExited(final MouseEvent e) { myInsideComponent = false; } @Override public void mousePressed(final MouseEvent e) { if (myInsideComponent) { hideTooltip(true); } } @Override public double getWeight() { return 0; } @Override public void mouseEntered(final MouseEvent e) { myInsideComponent = true; } } private class MyFrameStateListener extends FrameStateListener.Adapter { @Override public void onFrameDeactivated() { hideTooltip(true); } } public JPopupMenu registerPopup(JPopupMenu menu) { menu.addPopupMenuListener(this); return menu; } @Override public void popupMenuWillBecomeVisible(final PopupMenuEvent e) { myPopupShown = true; } @Override public void popupMenuWillBecomeInvisible(final PopupMenuEvent e) { onPopupClosed(e); } @Override public void popupMenuCanceled(final PopupMenuEvent e) { onPopupClosed(e); } private void onPopupClosed(final PopupMenuEvent e) { myPopupShown = false; if (e.getSource() instanceof JPopupMenu) { ((JPopupMenu)e.getSource()).removePopupMenuListener(this); } } private class MyMouseMotionListener extends MouseMotionAdapter implements Weighted{ @Override public void mouseMoved(final MouseEvent e) { myLastMouseEvent = e; if (!myComponent.isShowing()) return; myInsideComponent = true; if (myCurrentTooltip == null) { if (isInsideComponent(e)) { tryTooltip(e, true); } } else { if (!isOverTip(e)) { tryTooltip(e, true); } } } @Override public double getWeight() { return 0; } } private boolean isInsideComponent(final MouseEvent e) { final Rectangle compBounds = myComponent.getVisibleRect(); final Point compPoint = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), myComponent); return compBounds.contains(compPoint); } private void tryTooltip(final InputEvent e, final boolean auto) { myShowAlarm.cancelAllRequests(); myHideAlarm.cancelAllRequests(); myShowAlarm.addRequest(() -> { if (!myIsDisposed && !myPopupShown) { showTooltip(e, auto); } }, auto ? XDebuggerSettingsManager.getInstance().getDataViewSettings().getValueLookupDelay() : 10); } private void showTooltip(InputEvent e, boolean auto) { if (auto && !Registry.is("debugger.valueTooltipAutoShow")) return; MouseEvent sourceEvent = null; JComponent newTip = null; if (e instanceof MouseEvent) { sourceEvent = (MouseEvent)e; } else if (e instanceof KeyEvent) { sourceEvent = myTipFactory.createTooltipEvent(myLastMouseEvent); } MouseEvent convertedEvent = null; if (sourceEvent != null) { convertedEvent = SwingUtilities.convertMouseEvent(sourceEvent.getComponent(), sourceEvent, myComponent); newTip = myTipFactory.createToolTip(convertedEvent); } if (newTip == null || (auto && !myTipFactory.isFocusOwner())) { hideTooltip(false); return; } if(newTip == myCurrentTooltip) { if (!auto) { hideTooltip(true); return; } return; } hideTooltip(true); if(myComponent.isShowing()) { PopupFactory popupFactory = PopupFactory.getSharedInstance(); final Point location = convertedEvent.getPoint(); final Component sourceComponent = convertedEvent.getComponent(); if (sourceComponent != null) { SwingUtilities.convertPointToScreen(location, sourceComponent); } myTipPopup = popupFactory.getPopup(myComponent, newTip, location.x, location.y); myInsideComponent = false; myTipPopup.show(); myCurrentTooltip = newTip; } } public void hideTooltip() { hideTooltip(true); } public void hideTooltip(boolean now) { if (myTipPopup == null) return; if (now) { myHideAlarm.cancelAllRequests(); myTipPopup.hide(); myTipPopup = null; myCurrentTooltip = null; } else { myHideAlarm.addRequest(() -> { if (myInsideComponent) { hideTooltip(true); } }, 100); } } private JComponent myCurrentTooltip; private Popup myTipPopup; private final TipFactory myTipFactory; private final JComponent myComponent; private MouseListener myMouseListener = new MyMouseListener(); private MouseMotionListener myMouseMotionListener = new MyMouseMotionListener(); private FrameStateListener myFrameStateListener = new MyFrameStateListener(); private final Alarm myShowAlarm = new Alarm(); private final Alarm myHideAlarm = new Alarm(); private IdeGlassPane myGP; public TipManager(final JComponent component, TipFactory factory) { myTipFactory = factory; myComponent = component; new UiNotifyConnector.Once(component, new Activatable() { @Override public void showNotify() { installListeners(); } @Override public void hideNotify() { } }); final HideTooltipAction hide = new HideTooltipAction(); hide.registerCustomShortcutSet(CommonShortcuts.ESCAPE, myComponent); Disposer.register(this, new Disposable() { @Override public void dispose() { hide.unregisterCustomShortcutSet(myComponent); } }); } private class HideTooltipAction extends AnAction { @Override public void actionPerformed(AnActionEvent e) { hideTooltip(true); } @Override public void update(AnActionEvent e) { e.getPresentation().setEnabled(myTipPopup != null); } } private void installListeners() { if (myIsDisposed) return; myGP = IdeGlassPaneUtil.find(myComponent); assert myGP != null; myGP.addMousePreprocessor(myMouseListener, this); myGP.addMouseMotionPreprocessor(myMouseMotionListener, this); myHideCanceller = new MyAwtPreprocessor(); Toolkit.getDefaultToolkit().addAWTEventListener(myHideCanceller, AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK); FrameStateManager.getInstance().addListener(myFrameStateListener); } @Override public void dispose() { Disposer.dispose(this); hideTooltip(true); Toolkit.getDefaultToolkit().removeAWTEventListener(myHideCanceller); myIsDisposed = true; myShowAlarm.cancelAllRequests(); myMouseListener = null; myMouseMotionListener = null; FrameStateManager.getInstance().removeListener(myFrameStateListener); myFrameStateListener = null; } private class MyAwtPreprocessor implements AWTEventListener { @Override public void eventDispatched(AWTEvent event) { if (event.getID() == MouseEvent.MOUSE_MOVED) { preventFromHideIfInsideTooltip(event); } else if (event.getID() == MouseEvent.MOUSE_PRESSED || event.getID() == MouseEvent.MOUSE_RELEASED) { hideTooltipIfCloseClick((MouseEvent)event); } else if (event instanceof KeyEvent) { tryToShowTooltipIfRequested((KeyEvent)event); } } private void hideTooltipIfCloseClick(MouseEvent me) { if (myCurrentTooltip == null) return; if (isInsideTooltip(me) && UIUtil.isCloseClick(me)) { hideTooltip(true); } } private void tryToShowTooltipIfRequested(KeyEvent event) { if (KeymapUtil.isTooltipRequest(event)) { tryTooltip(event, false); } else { if (event.getID() == KeyEvent.KEY_PRESSED) { myLastMouseEvent = null; } } } private void preventFromHideIfInsideTooltip(AWTEvent event) { if (myCurrentTooltip == null) return; if (event.getID() == MouseEvent.MOUSE_MOVED) { final MouseEvent me = (MouseEvent)event; if (isInsideTooltip(me)) { myHideAlarm.cancelAllRequests(); } } } private boolean isInsideTooltip(MouseEvent me) { return myCurrentTooltip == me.getComponent() || SwingUtilities.isDescendingFrom(me.getComponent(), myCurrentTooltip); } } }