/*
* Copyright (c) 2005-2010 Flamingo Kirill Grouchnikov. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of Flamingo Kirill Grouchnikov nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.pushingpixels.flamingo.api.common;
import java.awt.*;
import java.awt.event.*;
import java.util.List;
import javax.swing.*;
import org.pushingpixels.flamingo.api.common.popup.JPopupPanel;
import org.pushingpixels.flamingo.api.common.popup.PopupPanelManager;
import org.pushingpixels.flamingo.api.ribbon.AbstractRibbonBand;
import org.pushingpixels.flamingo.internal.ui.common.JRichTooltipPanel;
public class RichToolTipManager extends MouseAdapter implements
MouseMotionListener {
private Timer initialDelayTimer;
private Timer dismissTimer;
private RichTooltip richTooltip;
private JTrackableComponent insideComponent;
private MouseEvent mouseEvent;
final static RichToolTipManager sharedInstance = new RichToolTipManager();
private Popup tipWindow;
private JRichTooltipPanel tip;
private boolean tipShowing = false;
private static final String TRACKED_FOR_RICH_TOOLTIP = "flamingo.internal.trackedForRichTooltip";
public static abstract class JTrackableComponent extends JComponent {
public abstract RichTooltip getRichTooltip(MouseEvent mouseEvent);
}
RichToolTipManager() {
initialDelayTimer = new Timer(750, new InitialDelayTimerAction());
initialDelayTimer.setRepeats(false);
dismissTimer = new Timer(20000, new DismissTimerAction());
dismissTimer.setRepeats(false);
}
/**
* Specifies the initial delay value.
*
* @param milliseconds
* the number of milliseconds to delay (after the cursor has
* paused) before displaying the tooltip
* @see #getInitialDelay
*/
public void setInitialDelay(int milliseconds) {
initialDelayTimer.setInitialDelay(milliseconds);
}
/**
* Returns the initial delay value.
*
* @return an integer representing the initial delay value, in milliseconds
* @see #setInitialDelay
*/
public int getInitialDelay() {
return initialDelayTimer.getInitialDelay();
}
/**
* Specifies the dismissal delay value.
*
* @param milliseconds
* the number of milliseconds to delay before taking away the
* tooltip
* @see #getDismissDelay
*/
public void setDismissDelay(int milliseconds) {
dismissTimer.setInitialDelay(milliseconds);
}
/**
* Returns the dismissal delay value.
*
* @return an integer representing the dismissal delay value, in
* milliseconds
* @see #setDismissDelay
*/
public int getDismissDelay() {
return dismissTimer.getInitialDelay();
}
void showTipWindow(MouseEvent mouseEvent) {
if (insideComponent == null || !insideComponent.isShowing())
return;
Dimension size;
Point screenLocation = insideComponent.getLocationOnScreen();
Point location = new Point();
GraphicsConfiguration gc;
gc = insideComponent.getGraphicsConfiguration();
Rectangle sBounds = gc.getBounds();
Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc);
// Take into account screen insets, decrease viewport
sBounds.x += screenInsets.left;
sBounds.y += screenInsets.top;
sBounds.width -= (screenInsets.left + screenInsets.right);
sBounds.height -= (screenInsets.top + screenInsets.bottom);
// Just to be paranoid
hideTipWindow();
tip = new JRichTooltipPanel(insideComponent.getRichTooltip(mouseEvent));
tip
.applyComponentOrientation(insideComponent
.getComponentOrientation());
size = tip.getPreferredSize();
AbstractRibbonBand<?> ribbonBand = (AbstractRibbonBand<?>) SwingUtilities
.getAncestorOfClass(AbstractRibbonBand.class, insideComponent);
boolean ltr = tip.getComponentOrientation().isLeftToRight();
boolean isInRibbonBand = (ribbonBand != null);
if (isInRibbonBand) {
// display directly below or above ribbon band
location.x = ltr ? screenLocation.x : screenLocation.x
+ insideComponent.getWidth() - size.width;
Point bandLocationOnScreen = ribbonBand.getLocationOnScreen();
location.y = bandLocationOnScreen.y + ribbonBand.getHeight() + 4;
if ((location.y + size.height) > (sBounds.y + sBounds.height)) {
location.y = bandLocationOnScreen.y - size.height;
}
} else {
// display directly below or above it
location.x = ltr ? screenLocation.x : screenLocation.x
+ insideComponent.getWidth() - size.width;
location.y = screenLocation.y + insideComponent.getHeight();
if ((location.y + size.height) > (sBounds.y + sBounds.height)) {
location.y = screenLocation.y - size.height;
}
}
// Tweak the X location to not overflow the screen
if (location.x < sBounds.x) {
location.x = sBounds.x;
} else if (location.x - sBounds.x + size.width > sBounds.width) {
location.x = sBounds.x + Math.max(0, sBounds.width - size.width);
}
PopupFactory popupFactory = PopupFactory.getSharedInstance();
tipWindow = popupFactory.getPopup(insideComponent, tip, location.x,
location.y);
tipWindow.show();
dismissTimer.start();
tipShowing = true;
}
void hideTipWindow() {
if (tipWindow != null) {
tipWindow.hide();
tipWindow = null;
tipShowing = false;
tip = null;
dismissTimer.stop();
}
}
/**
* Returns a shared <code>ToolTipManager</code> instance.
*
* @return a shared <code>ToolTipManager</code> object
*/
public static RichToolTipManager sharedInstance() {
return sharedInstance;
}
/**
* Registers a component for tooltip management.
* <p>
* This will register key bindings to show and hide the tooltip text only if
* <code>component</code> has focus bindings. This is done so that
* components that are not normally focus traversable, such as
* <code>JLabel</code>, are not made focus traversable as a result of
* invoking this method.
*
* @param comp
* a <code>JComponent</code> object to add
* @see JComponent#isFocusTraversable
*/
public void registerComponent(JTrackableComponent comp) {
if (Boolean.TRUE.equals(comp
.getClientProperty(TRACKED_FOR_RICH_TOOLTIP)))
return;
comp.addMouseListener(this);
// commandButton.addMouseMotionListener(moveBeforeEnterListener);
comp.putClientProperty(TRACKED_FOR_RICH_TOOLTIP, Boolean.TRUE);
}
/**
* Removes a component from tooltip control.
*
* @param comp
* a <code>JComponent</code> object to remove
*/
public void unregisterComponent(JTrackableComponent comp) {
comp.removeMouseListener(this);
comp.putClientProperty(TRACKED_FOR_RICH_TOOLTIP, null);
}
@Override
public void mouseEntered(MouseEvent event) {
initiateToolTip(event);
}
private void initiateToolTip(MouseEvent event) {
JTrackableComponent component = (JTrackableComponent) event.getSource();
// component.removeMouseMotionListener(moveBeforeEnterListener);
Point location = event.getPoint();
// ensure tooltip shows only in proper place
if (location.x < 0 || location.x >= component.getWidth()
|| location.y < 0 || location.y >= component.getHeight()) {
return;
}
// do not show tooltips on components in popup panels that are not
// in the last shown one
List<PopupPanelManager.PopupInfo> popups = PopupPanelManager
.defaultManager().getShownPath();
if (popups.size() > 0) {
JPopupPanel popupPanel = popups.get(popups.size() - 1)
.getPopupPanel();
boolean ignore = true;
Component c = component;
while (c != null) {
if (c == popupPanel) {
ignore = false;
break;
}
c = c.getParent();
}
if (ignore)
return;
}
if (insideComponent != null) {
initialDelayTimer.stop();
}
// A component in an unactive internal frame is sent two
// mouseEntered events, make sure we don't end up adding
// ourselves an extra time.
component.removeMouseMotionListener(this);
component.addMouseMotionListener(this);
insideComponent = component;
mouseEvent = event;
initialDelayTimer.start();
}
@Override
public void mouseExited(MouseEvent event) {
initialDelayTimer.stop();
if (insideComponent != null) {
insideComponent.removeMouseMotionListener(this);
}
insideComponent = null;
richTooltip = null;
mouseEvent = null;
hideTipWindow();
}
@Override
public void mousePressed(MouseEvent event) {
hideTipWindow();
initialDelayTimer.stop();
insideComponent = null;
mouseEvent = null;
}
@Override
public void mouseDragged(MouseEvent event) {
}
@Override
public void mouseMoved(MouseEvent event) {
if (tipShowing) {
checkForTipChange(event);
} else {
// Lazily lookup the values from within insideTimerAction
insideComponent = (JTrackableComponent) event.getSource();
mouseEvent = event;
richTooltip = null;
initialDelayTimer.restart();
}
}
private void checkForTipChange(MouseEvent event) {
JTrackableComponent component = (JTrackableComponent) event.getSource();
RichTooltip newTooltip = component.getRichTooltip(event);
// is it different?
boolean isDifferent = (richTooltip != newTooltip);
if (isDifferent) {
hideTipWindow();
if (newTooltip != null) {
richTooltip = newTooltip;
initialDelayTimer.restart();
}
}
}
protected class InitialDelayTimerAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (insideComponent != null && insideComponent.isShowing()) {
// Lazy lookup
if (richTooltip == null && mouseEvent != null) {
richTooltip = insideComponent.getRichTooltip(mouseEvent);
}
if (richTooltip != null) {
boolean showRichTooltip = true;
// check that no visible popup is originating in this
// component
for (PopupPanelManager.PopupInfo pi : PopupPanelManager
.defaultManager().getShownPath()) {
if (pi.getPopupOriginator() == insideComponent) {
showRichTooltip = false;
break;
}
}
if (showRichTooltip) {
showTipWindow(mouseEvent);
}
} else {
insideComponent = null;
richTooltip = null;
mouseEvent = null;
hideTipWindow();
}
}
}
}
protected class DismissTimerAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
hideTipWindow();
initialDelayTimer.stop();
insideComponent = null;
mouseEvent = null;
}
}
}