/**
* Sencha GXT 3.1.0-beta - Sencha for GWT
* Copyright(c) 2007-2014, Sencha, Inc.
* licensing@sencha.com
*
* http://www.sencha.com/products/gxt/license/
*/
package com.sencha.gxt.widget.core.client.tips;
import java.util.Date;
import java.util.logging.Logger;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.logical.shared.AttachEvent;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Widget;
import com.sencha.gxt.core.client.GXTLogConfiguration;
import com.sencha.gxt.core.client.Style.Anchor;
import com.sencha.gxt.core.client.Style.AnchorAlignment;
import com.sencha.gxt.core.client.Style.Side;
import com.sencha.gxt.core.client.dom.XDOM;
import com.sencha.gxt.core.client.dom.XElement;
import com.sencha.gxt.core.client.util.Point;
import com.sencha.gxt.core.client.util.Region;
import com.sencha.gxt.core.client.util.Size;
import com.sencha.gxt.core.client.util.Util;
import com.sencha.gxt.core.shared.event.GroupingHandlerRegistration;
import com.sencha.gxt.widget.core.client.event.HideEvent;
import com.sencha.gxt.widget.core.client.event.HideEvent.HideHandler;
import com.sencha.gxt.widget.core.client.event.XEvent;
import com.sencha.gxt.widget.core.client.tips.ToolTipConfig.ToolTipRenderer;
/**
* A standard tooltip implementation for providing additional information when hovering over a target element.
*/
public class ToolTip extends Tip {
private static Logger logger = Logger.getLogger(ToolTip.class.getName());
private class Handler implements MouseOverHandler, MouseOutHandler, MouseMoveHandler, HideHandler,
AttachEvent.Handler, FocusHandler, BlurHandler, KeyDownHandler {
@Override
public void onAttachOrDetach(AttachEvent event) {
if (!event.isAttached()) {
hide();
}
}
@Override
public void onBlur(BlurEvent event) {
}
@Override
public void onFocus(FocusEvent event) {
}
@Override
public void onHide(HideEvent event) {
hide();
}
@Override
public void onKeyDown(KeyDownEvent event) {
}
@Override
public void onMouseMove(MouseMoveEvent event) {
onTargetMouseMove(event);
}
@Override
public void onMouseOut(MouseOutEvent event) {
onTargetMouseOut(event);
}
@Override
public void onMouseOver(MouseOverEvent event) {
onTargetMouseOver(event);
}
}
protected XElement anchorEl;
protected Timer dismissTimer;
protected Timer hideTimer;
protected Timer showTimer;
protected Element target;
protected Point targetXY = new Point(0, 0);
protected String titleHtml, bodyHtml;
private GroupingHandlerRegistration handlerRegistration;
private Date lastActive;
/**
* Creates a new tool tip.
*
* @param target the target widget
*/
public ToolTip(Widget target) {
init();
initTarget(target);
}
/**
* Creates a new tool tip.
*
* @param appearance the appearance
*/
public ToolTip(TipAppearance appearance) {
super(appearance);
init();
initTarget(null);
}
/**
* Creates a new tool tip.
*
* @param target the target widget
* @param appearance the appearance
*/
public ToolTip(Widget target, TipAppearance appearance) {
super(appearance);
init();
initTarget(target);
}
/**
* Creates a new tool tip for the given target.
*
* @param target the target widget
* @param config the tool tip config
*/
public ToolTip(Widget target, ToolTipConfig config) {
init();
updateConfig(config);
initTarget(target);
}
/**
* Creates a new tool tip.
*
* @param config the tool tip config
*/
public ToolTip(ToolTipConfig config) {
init();
updateConfig(config);
initTarget(null);
}
/**
* Returns the quick show interval.
*
* @return the quick show interval
*/
public int getQuickShowInterval() {
return quickShowInterval;
}
/**
* Returns the current tool tip config.
*
* @return the tool tip config
*/
public ToolTipConfig getToolTipConfig() {
return toolTipConfig;
}
@Override
public void hide() {
clearTimers();
lastActive = new Date();
super.hide();
}
/**
* Binds the tool tip to the target widget. Allows a tool tip to switch the target widget.
*
* @param widget the target widget
*/
public void initTarget(final Widget widget) {
if (handlerRegistration != null) {
handlerRegistration.removeHandler();
}
if (widget != null) {
this.target = widget.getElement();
Handler handler = new Handler();
handlerRegistration = new GroupingHandlerRegistration();
handlerRegistration.add(widget.addDomHandler(handler, MouseOverEvent.getType()));
handlerRegistration.add(widget.addDomHandler(handler, MouseOutEvent.getType()));
handlerRegistration.add(widget.addDomHandler(handler, MouseMoveEvent.getType()));
handlerRegistration.add(widget.addHandler(handler, HideEvent.getType()));
handlerRegistration.add(widget.addHandler(handler, AttachEvent.getType()));
}
}
/**
* Sets the quick show interval (defaults to 250).
*
* @param quickShowInterval the quick show interval
*/
public void setQuickShowInterval(int quickShowInterval) {
this.quickShowInterval = quickShowInterval;
}
@Override
public void show() {
if (disabled) return;
Side origAnchor = null;
boolean origConstrainPosition = false;
if (toolTipConfig.getAnchor() != null) {
origAnchor = toolTipConfig.getAnchor();
// attach for good measure
// getTarget, so the elements can be properly measured/sized.
showAt(getTargetXY(0));
origConstrainPosition = this.constrainPosition;
constrainPosition = false;
}
// go to the method below, it has some added benefits before going to super
Point xy = getTargetXY(0);
showAt(xy.getX(), xy.getY());
if (toolTipConfig.getAnchor() != null) {
anchorEl.show();
constrainPosition = origConstrainPosition;
toolTipConfig.setAnchor(origAnchor);
} else {
anchorEl.hide();
}
}
@Override
public void showAt(int x, int y) {
if (disabled) return;
lastActive = new Date();
clearTimers();
// retain the position set so that show() can be used to reposition the tip
// subtract mouse offset b/c getTarget(xy) sums to to targetXY
if (!toolTipConfig.isTrackMouse()) {
targetXY.setX(x - toolTipConfig.getMouseOffsetX());
targetXY.setY(y - toolTipConfig.getMouseOffsetY());
}
super.showAt(x, y);
if (toolTipConfig.getAnchor() != null) {
anchorEl.show();
syncAnchor();
} else {
anchorEl.hide();
}
if (toolTipConfig.getDismissDelay() > 0 && toolTipConfig.isAutoHide() && !toolTipConfig.isCloseable()) {
dismissTimer = new Timer() {
@Override
public void run() {
hide();
}
};
dismissTimer.schedule(toolTipConfig.getDismissDelay());
}
}
/**
* Updates the tool tip with the given config.
*
* @param config the tool tip config
*/
public void update(ToolTipConfig config) {
updateConfig(config);
if (isAttached()) {
updateContent();
}
doAutoWidth();
// When the tooltip text is updated reposition if the text runs the tooltip out of range/screen
if (!config.isTrackMouse() && isAttached()) {
show();
}
}
protected void clearTimer(String timer) {
if (timer.equals("hide")) {
if (hideTimer != null) {
hideTimer.cancel();
hideTimer = null;
}
} else if (timer.equals("dismiss")) {
if (dismissTimer != null) {
dismissTimer.cancel();
dismissTimer = null;
}
} else if (timer.equals("show")) {
if (showTimer != null) {
showTimer.cancel();
showTimer = null;
}
}
}
protected void clearTimers() {
clearTimer("show");
clearTimer("dismiss");
clearTimer("hide");
}
protected void delayHide() {
if (isAttached() && hideTimer == null && toolTipConfig.isAutoHide() && !toolTipConfig.isCloseable()) {
if (toolTipConfig.getHideDelay() == 0) {
hide();
return;
}
hideTimer = new Timer() {
@Override
public void run() {
hide();
}
};
hideTimer.schedule(toolTipConfig.getHideDelay());
}
}
protected void delayShow() {
if (!isAttached() && showTimer == null) {
if ((new Date().getTime() - lastActive.getTime()) < quickShowInterval) {
show();
} else {
if (toolTipConfig.getShowDelay() > 0) {
showTimer = new Timer() {
@Override
public void run() {
show();
}
};
showTimer.schedule(toolTipConfig.getShowDelay());
} else {
show();
}
}
} else if (isAttached()) {
show();
}
}
protected AnchorAlignment getAnchorAlign() {
switch (toolTipConfig.getAnchor()) {
case TOP:
return new AnchorAlignment(Anchor.TOP_LEFT, Anchor.BOTTOM_LEFT);
case LEFT:
return new AnchorAlignment(Anchor.TOP_LEFT, Anchor.TOP_RIGHT);
case RIGHT:
return new AnchorAlignment(Anchor.TOP_RIGHT, Anchor.TOP_LEFT);
default:
return new AnchorAlignment(Anchor.BOTTOM_LEFT, Anchor.TOP_LEFT);
}
}
protected int[] getOffsets() {
int[] offsets;
if (toolTipConfig.isAnchorToTarget() && !toolTipConfig.isTrackMouse()) {
switch (toolTipConfig.getAnchor()) {
case TOP:
offsets = new int[] {0, 9};
break;
case BOTTOM:
offsets = new int[] {0, -9};
break;
case RIGHT:
offsets = new int[] {-9, 0};
break;
default:
offsets = new int[] {9, 0};
break;
}
} else {
int anchorOffset = toolTipConfig.getAnchorOffset();
switch (toolTipConfig.getAnchor()) {
case TOP:
offsets = new int[] {-15 - anchorOffset, 30};
break;
case BOTTOM:
offsets = new int[] {-19 - anchorOffset, -13 - getElement().getOffsetHeight()};
break;
case RIGHT:
offsets = new int[] {-15 - getElement().getOffsetWidth(), -13 - anchorOffset};
break;
default:
offsets = new int[] {25, -13 - anchorOffset};
break;
}
}
offsets[0] += toolTipConfig.getMouseOffsetX();
offsets[1] += toolTipConfig.getMouseOffsetY();
return offsets;
}
/**
* Creates a new tool tip.
*/
protected void init() {
toolTipConfig = new ToolTipConfig();
lastActive = new Date();
monitorWindowResize = true;
anchorEl = Document.get().createDivElement().cast();
getAppearance().applyAnchorStyle(anchorEl);
getElement().appendChild(anchorEl);
}
@Override
protected void onAfterFirstAttach() {
super.onAfterFirstAttach();
anchorEl.setZIndex(getElement().getZIndex() + 1);
}
protected void onMouseMove(Event event) {
targetXY = event.<XEvent> cast().getXY();
if (isAttached() && toolTipConfig.isTrackMouse()) {
Side origAnchor = toolTipConfig.getAnchor();
Point p = getTargetXY(0);
toolTipConfig.setAnchor(origAnchor);
if (constrainPosition) {
p = getElement().adjustForConstraints(p);
}
super.showAt(p.getX(), p.getY());
}
}
protected void onTargetMouseMove(MouseMoveEvent event) {
onMouseMove(event.getNativeEvent().<Event> cast());
}
protected void onTargetMouseOut(MouseOutEvent event) {
Element source = event.getNativeEvent().getEventTarget().cast();
Element to = event.getNativeEvent().getRelatedEventTarget().cast();
if (source != null && (to == null || !source.isOrHasChild(to.<Element> cast()))) {
onTargetOut(event.getNativeEvent().<Event> cast());
}
}
protected void onTargetMouseOver(MouseOverEvent event) {
Element source = event.getNativeEvent().getEventTarget().cast();
EventTarget from = event.getNativeEvent().getRelatedEventTarget();
if (source != null && (from == null || !source.isOrHasChild(from.<Element> cast()))) {
onTargetOver(event.getNativeEvent().<Event> cast());
}
}
protected void onTargetOut(Event ce) {
if (disabled) {
return;
}
clearTimer("show");
delayHide();
}
protected void onTargetOver(Event ce) {
if (disabled || !target.isOrHasChild(ce.getEventTarget().<Element> cast())) {
return;
}
clearTimer("hide");
targetXY = new Point(ce.getClientX(), ce.getClientY());
delayShow();
}
@Override
protected void onWindowResize(int width, int height) {
super.onWindowResize(width, height);
// this can only be reached if the tooltip is already visible, show it again
// to sync anchor
show();
}
protected void syncAnchor() {
Anchor anchorPos, targetPos;
final int offsetX, offsetY;
int anchorOffset = toolTipConfig.getAnchorOffset();
switch (toolTipConfig.getAnchor()) {
case TOP:
anchorPos = Anchor.BOTTOM;
targetPos = Anchor.TOP_LEFT;
offsetX = 20 + anchorOffset;
offsetY = 2;
break;
case RIGHT:
anchorPos = Anchor.LEFT;
targetPos = Anchor.TOP_RIGHT;
offsetX = -2;
offsetY = 11 + anchorOffset;
break;
case BOTTOM:
anchorPos = Anchor.TOP;
targetPos = Anchor.BOTTOM_LEFT;
offsetX = 20 + anchorOffset;
offsetY = -2;
break;
default:
anchorPos = Anchor.RIGHT;
targetPos = Anchor.TOP_LEFT;
offsetX = 2;
offsetY = 11 + anchorOffset;
break;
}
anchorEl.alignTo(getElement(), new AnchorAlignment(anchorPos, targetPos, false), offsetX, offsetY);
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
protected void updateContent() {
String textHtml = "";
if (toolTipConfig.getRenderer() != null) {
Object data = toolTipConfig.getData();
ToolTipRenderer r = toolTipConfig.getRenderer();
SafeHtml html = r.renderToolTip(data);
textHtml = html.asString();
} else {
textHtml = Util.isEmptyString(bodyHtml) ? " " : bodyHtml;
}
getAppearance().updateContent(getElement(), titleHtml, textHtml);
}
/**
* Add to tooltip anchor to the range for overall measurement calculations.
*
* @param offsets return what was given with added anchor amount depending on side. Adds a couple pixels for spacing.
* @return offsets
*/
private int[] getAddAnchorToRange(int[] offsets) {
Region ar = XElement.as(anchorEl).getRegion();
// Note things are inverted from screen to code
switch (toolTipConfig.getAnchor()) {
case TOP:
offsets[0] = 0;
offsets[1] += ar.getBottom() - ar.getTop() + 2;
break;
case BOTTOM:
offsets[0] = 0;
offsets[1] += ar.getBottom() - ar.getTop() + 2;
break;
case RIGHT:
offsets[0] = ar.getRight() - ar.getLeft() + 2;
offsets[1] = 0;
break;
case LEFT:
offsets[0] = ar.getRight() - ar.getLeft() + 2;
offsets[1] = 0;
break;
}
return offsets;
}
protected Point getTargetXY(int targetCounter) {
if (toolTipConfig.getAnchor() != null) {
targetCounter++;
int[] offsets = getOffsets();
// base pin point, depending on anchor side designation
Point xy = targetXY;
if (toolTipConfig.isAnchorToTarget() && !toolTipConfig.isTrackMouse()) {
// Note: Don't set the scroll offset in x and y
xy = getElement().getAlignToXY(target, getAnchorAlign(), 0, 0);
}
int dw = XDOM.getViewWidth(false);
int dh = XDOM.getViewHeight(false);
int scrollX = XDOM.getBodyScrollLeft();
int scrollY = XDOM.getBodyScrollTop();
int[] axy = new int[] {xy.getX() + offsets[0], xy.getY() + offsets[1]};
// getElement().getAlignToXY adds scroll offset, this takes it away, b/c Tip showAt translates/adds it again
// This takes out the scroll offset given by 'getElement().getAlignToXY'
if (toolTipConfig.isAnchorToTarget() && !toolTipConfig.isTrackMouse()) {
axy[0] -= XDOM.getBodyScrollLeft();
axy[1] -= XDOM.getBodyScrollTop();
if (scrollY > 0) {
// bottom offset correct is double
scrollY -= XDOM.getBodyScrollTop() * 2;
}
}
Size sz = getElement().getSize();
Region r = XElement.as(target).getRegion();
// Anchor range is not factored in on overall size, which overall size has to be used to flip to another side
// Note, things are inverted from display to programattical code like RIGHT means screen display LEFT
offsets = getAddAnchorToRange(offsets);
// When tip is screen top hits the ceiling out of range/site, this is the offset that fixes it.
if (toolTipConfig.isTrackMouse() && toolTipConfig.getAnchor() == Side.BOTTOM) {
offsets[1] = XDOM.getBodyScrollTop() * 2;
}
// When tip is screen bottom during a scroll needs an offset correction so not to flip to early
if (toolTipConfig.isTrackMouse() && toolTipConfig.getAnchor() == Side.TOP) {
offsets[1] -= XDOM.getBodyScrollTop() * 2;
}
if (GXTLogConfiguration.loggingIsEnabled()) {
String s = "dw=" + dw + ",dh=" + dh + " ";
s += "scroll=" + scrollX + ",=" + scrollY + " ";
s += "offsets=" + offsets[0] + "," + offsets[1] + " ";
s += "axy=" + axy[0] + "," + axy[1] + " ";
s += "sz=" + sz + " ";
s += "r=" + r + " ";
s += "isTrackMouse=" + toolTipConfig.isTrackMouse() + " ";
s += "targetXY=" + targetXY + " ";
logger.finest(s);
if (toolTipConfig.getAnchor() == Side.TOP) {
String s1 = "TOP CALC: " + "sz.getHeight()=" + sz.getHeight() + " " + "+ offsets[1]=" + offsets[1] + " "
+ "+ scrollY " + scrollY + " " + "< dh=" + dh + " " + "- r.getBottom()=" + r.getBottom();
String s2 = "TOP CALC: " + (sz.getHeight() + offsets[1] + scrollY) + " < " + (dh - r.getBottom()) + " ";
String s3 = "TOP CALC: " + (sz.getHeight() + offsets[1] + scrollY < dh - r.getBottom());
logger.finest(s1);
logger.finest(s2);
logger.finest(s3);
}
if (toolTipConfig.getAnchor() == Side.BOTTOM) {
String s1 = "BOTTOM CALC: sz.getHeight()=" + sz.getHeight() + " + offsets[1]=" + offsets[1] + " - scrollY="
+ scrollY + " < r.getTop()=" + r.getTop();
String s2 = "BOTTOM CALC: " + (sz.getHeight() + offsets[1] - scrollY) + " < " + r.getTop();
String s3 = "BOTTOM CALC: " + (sz.getHeight() + offsets[1] - scrollY < r.getTop());
logger.finest(s1);
logger.finest(s2);
logger.finest(s3);
}
if (toolTipConfig.getAnchor() == Side.LEFT) {
String s1 = "LEFT CALC: sz.getWidth()=" + sz.getWidth() + " + offsets[0]=" + offsets[0] + " + scrollX="
+ scrollX + " < dw=" + dw + " - r.getRight()=" + r.getRight();
String s2 = "LEFT CALC: " + (sz.getWidth() + offsets[0] + scrollX) + " < " + (dw - r.getRight());
String s3 = "LEFT CALC: " + (sz.getWidth() + offsets[0] + scrollX < dw - r.getRight());
logger.finest(s1);
logger.finest(s2);
logger.finest(s3);
}
if (toolTipConfig.getAnchor() == Side.RIGHT) {
String s1 = "RIGHT CALC: sz.getWidth()=" + sz.getWidth() + " + offsets[0]=" + offsets[0] + " + scrollX="
+ scrollX + " < r.getLeft()=" + r.getLeft();
String s2 = "RIGHT CALC: " + (sz.getWidth() + offsets[0] + scrollX) + " < " + r.getLeft();
String s3 = "RIGHT CALC: " + (sz.getWidth() + offsets[0] + scrollX < r.getLeft());
logger.finest(s1);
logger.finest(s2);
logger.finest(s3);
}
}
// if we are not inside valid ranges we try to switch the anchor
if (!((toolTipConfig.getAnchor() == Side.TOP && (sz.getHeight() + offsets[1] + scrollY < dh - r.getBottom()))
|| (toolTipConfig.getAnchor() == Side.RIGHT && (sz.getWidth() + offsets[0] + scrollX < r.getLeft()))
|| (toolTipConfig.getAnchor() == Side.BOTTOM && (sz.getHeight() + offsets[1] - scrollY < r.getTop()))
|| (toolTipConfig.getAnchor() == Side.LEFT && (sz.getWidth() + offsets[0] - scrollX < dw - r.getRight())))
&& targetCounter < 4) {
targetCounter++;
if (sz.getWidth() + offsets[0] + scrollX < r.getLeft()) {
toolTipConfig.setAnchor(Side.RIGHT);
return getTargetXY(targetCounter);
}
if (sz.getWidth() + offsets[0] + scrollX < r.getLeft()) {
toolTipConfig.setAnchor(Side.LEFT);
return getTargetXY(targetCounter);
}
if (sz.getHeight() + offsets[1] + scrollY < dh - r.getBottom()) {
toolTipConfig.setAnchor(Side.TOP);
return getTargetXY(targetCounter);
}
if (sz.getHeight() + offsets[1] + scrollY < r.getTop()) {
toolTipConfig.setAnchor(Side.BOTTOM);
return getTargetXY(targetCounter);
}
}
// sets the direction of the anchor <^>
if (toolTipConfig.isAnchorArrow()) {
getAppearance().applyAnchorDirectionStyle(anchorEl, toolTipConfig.getAnchor());
}
// reset recursion check counter
targetCounter = 0;
return new Point(axy[0], axy[1]);
} else {
int x = targetXY.getX() + toolTipConfig.getMouseOffsetX();
int y = targetXY.getY() + toolTipConfig.getMouseOffsetY();
return new Point(x, y);
}
}
private void updateConfig(ToolTipConfig config) {
this.toolTipConfig = config;
if (!config.isEnabled()) {
clearTimers();
hide();
}
setMinWidth(config.getMinWidth());
setMaxWidth(config.getMaxWidth());
setClosable(config.isCloseable());
bodyHtml = config.getBodyHtml();
titleHtml = config.getTitleHtml();
}
}