package org.geogebra.web.html5.gui.tooltip;
import org.geogebra.common.euclidian.event.PointerEventType;
import org.geogebra.common.javax.swing.SwingConstants;
import org.geogebra.common.util.StringUtil;
import org.geogebra.web.html5.css.GuiResourcesSimple;
import org.geogebra.web.html5.gui.util.CancelEventTimer;
import org.geogebra.web.html5.gui.util.ClickEndHandler;
import org.geogebra.web.html5.main.AppW;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.SimplePanel;
/**
* <p>
* Singleton class that maintains a GWT panel for displaying toolTips.
* </p>
*
* <p>
* Design is adapted from Java's ToolTipManager. ToolTip behavior should follow
* this description from the Java source code:
* </p>
*
* <p>
* "ToolTipManager contains numerous properties for configuring how long it will
* take for the tooltips to become visible, and how long till they hide.
* Consider a component that has a different tooltip based on where the mouse
* is, such as JTree. When the mouse moves into the JTree and over a region that
* has a valid tooltip, the tooltip will become visible after initialDelay
* milliseconds. After dismissDelay milliseconds the tooltip will be hidden. If
* the mouse is over a region that has a valid tooltip, and the tooltip is
* currently visible, when the mouse moves to a region that doesn't have a valid
* tooltip the tooltip will be hidden. If the mouse then moves back into a
* region that has a valid tooltip within reshowDelay milliseconds, the tooltip
* will immediately be shown, otherwise the tooltip will be shown again after
* initialDelay milliseconds."
* </p>
*
* @author G. Sturr
*/
public final class ToolTipManagerW {
/**
* The toolTip can include a link. depending on the type of the link,
* another picture has to be added.
*/
public enum ToolTipLinkType {
/**
* question mark
*/
Help,
/**
* TODO another picture is needed
*/
ViewSavedFile;
}
private AppW app;
private SimplePanel tipPanel;
private HTML tipHTML;
private TooltipPanel bottomInfoTipPanel;
private HTML bottomInfoTipHTML;
private String questionMark;
private String viewSavedFile;
private Label helpLabel;
private String oldText = "";
/** last mouse x coord */
int mouseX = 0;
/** last mouse y coord */
int mouseY = 0;
private Timer timer;
private boolean blockToolTip = true;
/**
* Time, in milliseconds, to delay showing a toolTip.
*
* Java default = 1750, // maybe we use a quicker 1000?
*/
private int initialDelay = 1750;
/**
* Time, in milliseconds, to allow the toolTip to remain visible.
*
* Java default = 4000.
*/
private int dismissDelay = 4000;
/**
* Time, in milliseconds, to allow showing a new toolTip immediately, with
* no delay. After this delay has expired, toolTips are shown with an
* initial delay.
*
* Java default = 500;
*/
private int reshowDelay = 500;
/**
* Flag to enable/disable a delay time before showing a toolTip.
* */
boolean enableDelay = true;
/**
* Flag to prevent a toolTip delay, even if an initial delay has been
* enabled. This is helpful when the mouse is moved around nearby objects
* and the initial delay is annoying to the user.
*/
boolean showImmediately = false;
/**
* HTML element associated with the toolTip. The toolTip will be positioned
* relative to this element.
*/
private Element tipElement;
private static boolean enabled = true;
private String helpURL;
/** Singleton instance of ToolTipManager. */
final static ToolTipManagerW sharedInstance = new ToolTipManagerW();
/*****************************************************
* Constructor
*/
private ToolTipManagerW() {
initTooltipManagerW();
}
/**
* All methods are accessed from this instance.
*
* @return Singleton instance of this class
*/
public static ToolTipManagerW sharedInstance() {
return sharedInstance;
}
private void initTooltipManagerW() {
if (tipPanel != null || !enabled) {
return;
}
createTipElements();
createBottomInfoTipElements();
registerMouseListeners();
}
private void createTipElements() {
tipHTML = new HTML();
tipHTML.setStyleName("toolTipHTML");
tipPanel = new SimplePanel();
tipPanel.setStyleName("ToolTip");
tipPanel.add(tipHTML);
tipPanel.setVisible(false);
RootPanel.get().add(tipPanel);
}
private void createBottomInfoTipElements() {
bottomInfoTipHTML = new HTML();
bottomInfoTipHTML.setStyleName("infoText");
questionMark = GuiResourcesSimple.INSTANCE.questionMark().getSafeUri()
.asString();
viewSavedFile = GuiResourcesSimple.INSTANCE.viewSaved().getSafeUri()
.asString();
bottomInfoTipPanel = new TooltipPanel();
bottomInfoTipPanel.setStyleName("infoTooltip");
bottomInfoTipPanel.add(bottomInfoTipHTML);
bottomInfoTipPanel.setVisible(false);
RootPanel.get().add(bottomInfoTipPanel);
ClickEndHandler.init(bottomInfoTipPanel, new ClickEndHandler() {
@Override
public void onClickEnd(int x, int y, PointerEventType type) {
openHelp();
}
});
}
/**
* Open current help URL in browser / webview
*/
void openHelp() {
if (helpURL != null && app != null) {
app.getFileManager().open(helpURL);
hideAllToolTips();
}
}
/**
* @return whether tooltips are blocked
*/
public boolean isToolTipBlocked() {
return blockToolTip;
}
/**
* @param blockToolTip
* whether to block tooltips
*/
public void setBlockToolTip(boolean blockToolTip) {
this.blockToolTip = blockToolTip;
}
// =====================================
// BottomInfoToolTip
// =====================================
/**
* @param text
* String
* @param helpLinkURL
* String
* @param link
* {@link ToolTipLinkType}
* @param app
* app for positioning
* @param kb
* whether keyboard is open
*/
public void showBottomInfoToolTip(String text, final String helpLinkURL,
ToolTipLinkType link, AppW app, boolean kb) {
if (blockToolTip || app == null) {
return;
}
this.app = app;
bottomInfoTipPanel.removeFromParent();
app.getPanel().add(bottomInfoTipPanel);
bottomInfoTipHTML.setHTML(text);
if (helpLabel != null) {
bottomInfoTipPanel.remove(helpLabel);
}
boolean online = app.getNetworkOperation() == null
|| app.getNetworkOperation().isOnline();
this.helpURL = helpLinkURL;
if (app.isExam() && app.getExam().getStart() >= 0) {
this.helpURL = null;
}
if (helpURL != null && helpURL.length() > 0 && link != null
&& online) {
helpLabel = new Label();
if (link.equals(ToolTipLinkType.Help)) {
helpLabel.getElement().getStyle()
.setBackgroundImage("url(" + this.questionMark + ")");
} else if (link.equals(ToolTipLinkType.ViewSavedFile)) {
helpLabel.getElement().getStyle()
.setBackgroundImage("url(" + this.viewSavedFile + ")");
}
// IE and FF block popups if they are comming from mousedown, so use
// mouseup instead
helpLabel.addStyleName("manualLink");
/*
* In "exam" mode the question mark is not shown
*/
if (!(app.isExam() && app.getExam().getStart() >= 0)) {
bottomInfoTipPanel.add(helpLabel);
}
}
bottomInfoTipPanel.setVisible(true);
// Helps to align the InfoTooltip in the center of the screen:
Style style = bottomInfoTipPanel.getElement().getStyle();
style.setLeft(0, Unit.PX);
double left = (app.getWidth() - bottomInfoTipPanel.getOffsetWidth()) / 2;
if (left < 0) {
left = 0;
}
// Toolbar on bottom - tooltip needs to be positioned higher so it
// doesn't overlap with the toolbar
if (app.getToolbarPosition() == SwingConstants.SOUTH) {
style.setLeft(left * 1.5, Unit.PX);
style.setTop(
(app.getHeight() - (kb ? 250 : 70) - 50) - 20 * lines(text),
Unit.PX);
// Toolbar on top
} else {
style.setLeft(left, Unit.PX);
style.setTop((app.getHeight() - (kb ? 250 : 70)) - 20 * lines(text),
Unit.PX);
}
if (link == ToolTipLinkType.Help && helpURL != null
&& helpURL.length() > 0) {
scheduleHideBottom();
}
}
private static int lines(String text) {
int lines = 0;
for (int i = 0; i < text.length(); i++) {
if ('\n' == text.charAt(i)) {
lines++;
}
}
return lines;
}
/**
* displays the given message
*
* @param text
* String
* @param closeAutomatic
* whether the message should be closed automatically after
* dismissDelay milliseconds
* @param app
* application
*/
public void showBottomMessage(String text, boolean closeAutomatic, AppW app) {
if (text == null || "".equals(text)) {
hideBottomInfoToolTip();
return;
}
blockToolTip = false;
showBottomInfoToolTip(StringUtil.toHTMLString(text), "", null, app, app
.getAppletFrame()
.isKeyboardShowing());
blockToolTip = true;
if (closeAutomatic) {
scheduleHideBottom();
}
}
private void scheduleHideBottom() {
cancelTimer();
timer = new Timer() {
@Override
public void run() {
hideBottomInfoToolTip();
}
};
timer.schedule(dismissDelay);
}
/**
* Hide the bottom tooltip
*/
public void hideBottomInfoToolTip() {
cancelTimer();
bottomInfoTipPanel.removeFromParent();
}
// =====================================
// Getters/Setters
// =====================================
/**
* @return time, in milliseconds, to wait before showing toolTip
*/
public int getInitialDelay() {
return initialDelay;
}
/**
* Set initial delay time.
*
* @param initialDelay
* time, in milliseconds, to wait before showing toolTip
*/
public void setInitialDelay(int initialDelay) {
this.initialDelay = initialDelay;
}
/**
* @return time, in milliseconds, to wait before hiding toolTip
* */
public int getDismissDelay() {
return dismissDelay;
}
/**
* Set dismissDelay time
*
* @param dismissDelay
* time, in milliseconds, to wait before hiding toolTip
*/
public void setDismissDelay(int dismissDelay) {
this.dismissDelay = dismissDelay;
}
/**
* Set flag to enable/disable delay timers
*
* @param enableDelay
* If true, timers manage toolTip visibility. If false, the
* toolTip is shown immediately without automatic hiding.
*/
public void setEnableDelay(boolean enableDelay) {
this.enableDelay = enableDelay;
}
// =====================================
// Mouse Listeners
// =====================================
/**
* Register mouse listeners to keep track of the mouse position and hide the
* toolTip on a mouseDown event.
*/
private void registerMouseListeners() {
if (!enabled) {
return;
}
// Closing tooltips is done in AppW.closePopups
Event.addNativePreviewHandler(new NativePreviewHandler() {
@Override
public void onPreviewNativeEvent(NativePreviewEvent event) {
NativeEvent e = event.getNativeEvent();
if (event.getTypeInt() == Event.ONTOUCHSTART) {
CancelEventTimer.touchEventOccured();
}
mouseX = e.getClientX();
mouseY = e.getClientY();
}
});
}
// ======================================
// ToolTip location
// ======================================
/**
* Set the toolTip widget location using the tipElement location or, if this
* is null, use current mouse coordinates.
*/
void setToolTipLocation() {
int left, top, topAbove;
// get initial position from associated tip element or,
// if this is null, from mouse coordinates
if (tipElement == null) {
left = Window.getScrollLeft() + mouseX;
topAbove = top = Window.getScrollTop() + mouseY + 18;
} else {
left = tipElement.getAbsoluteLeft();
top = tipElement.getAbsoluteBottom();
topAbove = tipElement.getAbsoluteTop();
}
// handle toolTip overflow at left and bottom edge
int w = tipPanel.getOffsetWidth();
int windowLeft = RootPanel.get().getAbsoluteLeft()
+ RootPanel.get().getOffsetWidth();
if (left + w > windowLeft) {
left = left - w;
}
int h = tipPanel.getOffsetHeight();
int windowBottom = RootPanel.get().getAbsoluteTop()
+ RootPanel.get().getOffsetHeight();
if (top + h > windowBottom) {
top = topAbove - h;
}
// set the toolTip location
RootPanel.get().setWidgetPosition(tipPanel, left, top);
}
// ======================================
// Show/Hide ToolTip
// ======================================
/**
* Show toolTip relative to a given element
*
* @param element
* element associated with tooltip
* @param toolTipText
* text to be displayed
*/
public void showToolTip(Element element, String toolTipText) {
if (!enabled) {
return;
}
tipElement = element;
if (tipElement == null) {
hideToolTip();
return;
}
showToolTipWithDelay(toolTipText);
}
/**
* Show toolTip using mouse coordinates.
*
* @param toolTipText
* text to be displayed
*/
public void showToolTip(String toolTipText) {
if (!enabled) {
return;
}
tipElement = null;
showToolTipWithDelay(toolTipText);
}
private void showToolTipWithDelay(String toolTipText) {
if (oldText.equals(toolTipText)) {
return;
}
if (toolTipText == null) {
hideToolTip();
return;
}
oldText = toolTipText;
tipHTML.setHTML(toolTipText);
if (enableDelay && !showImmediately) {
setInitialDelayTimer();
} else {
show();
}
}
/**
* Show the toolTip.
*/
void show() {
if (!enabled) {
return;
}
// tipPanel.getElement().getStyle().setProperty("visibility",
// "visible");
tipPanel.setVisible(true);
// locate and show the toolTip
setToolTipLocation();
// set to immediate mode so that toolTips for nearby elements will not
// be delayed
showImmediately = true;
// set the dismiss timer
if (enableDelay) {
setDismissDelayTimer();
}
}
/**
* Hide the toolTip.
*/
public void hideToolTip() {
if (!enabled) {
return;
}
// exit if toolTip is already hidden
if (!tipPanel.isVisible() && "".equals(oldText)) {
return;
}
tipHTML.setHTML("");
oldText = "";
// tipPanel.getElement().getStyle().setProperty("visibility", "hidden");
tipPanel.setVisible(false);
// cancel the timer in case of a delayed call to show()
cancelTimer();
// but, if in immediate mode, reset the reshow timer
if (showImmediately) {
setReshowTimer();
}
}
// ======================================
// Timers
// ======================================
private void setInitialDelayTimer() {
cancelTimer();
timer = new Timer() {
@Override
public void run() {
show();
// App.debug("initialDelay timer done, toolTip shown");
}
};
// App.debug("start initialDelay timer");
timer.schedule(initialDelay);
}
private void setDismissDelayTimer() {
cancelTimer();
timer = new Timer() {
@Override
public void run() {
hideToolTip();
// App.debug("dismissDelay timer done, toolTip hidden");
}
};
// App.debug("start dismissDelay timer");
timer.schedule(dismissDelay);
}
private void setReshowTimer() {
cancelTimer();
timer = new Timer() {
@Override
public void run() {
showImmediately = false;
// App.debug("reshow timer done, showImmediately = false");
}
};
// App.debug("start reshowDelay timer");
timer.schedule(reshowDelay);
}
private void cancelTimer() {
if (timer != null) {
timer.cancel();
timer = null;
}
}
/**
* @param allowToolTips
* global tooltips flag
*/
public static void setEnabled(boolean allowToolTips) {
enabled = allowToolTips;
}
/**
* Hide all tooltips
*/
public static void hideAllToolTips() {
sharedInstance().showImmediately = false;
sharedInstance().hideToolTip();
sharedInstance().hideBottomInfoToolTip();
}
}