Greg Sheremeta has uploaded a new change for review. Change subject: userportal, webadmin: new tooltip infrastructure ......................................................................
userportal, webadmin: new tooltip infrastructure New tooltip infrastructure that works with Element, Cells, and Widgets. Uses PatternFly tooltips for rendering. Change-Id: Ief7f8524a3dd69c983ace95206379df462bc7daf Bug-Url: Bug-Url: https://bugzilla.redhat.com/1067318 Signed-off-by: Greg Sheremeta <[email protected]> --- M backend/manager/modules/branding/src/main/resources/META-INF/tags/obrand/javascripts.tag M frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/utils/ElementUtils.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/utils/JqueryUtils.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/cell/AbstractTooltipCell.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/cell/TooltipCell.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/column/AbstractColumn.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/column/ImageResourceCell.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/ElementTooltip.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/ElementTooltipDetails.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/Tooltip.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/TooltipConfig.java A frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/TooltipMixin.java M frontend/webadmin/modules/userportal-gwtp/src/main/java/org/ovirt/engine/ui/userportal/widget/extended/vm/TooltipCell.java M frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/section/main/view/tab/MainTabClusterView.java A frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/widget/table/column/CommentColumn2.java A packaging/branding/ovirt.brand/mousetrack.js M packaging/branding/ovirt.brand/ovirt.css 17 files changed, 2,022 insertions(+), 3 deletions(-) git pull ssh://gerrit.ovirt.org:29418/ovirt-engine refs/changes/55/38555/1 diff --git a/backend/manager/modules/branding/src/main/resources/META-INF/tags/obrand/javascripts.tag b/backend/manager/modules/branding/src/main/resources/META-INF/tags/obrand/javascripts.tag index 0310ee4..e186e91 100644 --- a/backend/manager/modules/branding/src/main/resources/META-INF/tags/obrand/javascripts.tag +++ b/backend/manager/modules/branding/src/main/resources/META-INF/tags/obrand/javascripts.tag @@ -7,6 +7,7 @@ <script type="text/javascript" src="${pageContext.request.contextPath}${initParam['obrandThemePath']}${baseTheme.path}/patternfly/components/jquery/jquery.min.js"></script> <script type="text/javascript" src="${pageContext.request.contextPath}${initParam['obrandThemePath']}${baseTheme.path}/patternfly/components/bootstrap/dist/js/bootstrap.min.js"></script> <script type="text/javascript" src="${pageContext.request.contextPath}${initParam['obrandThemePath']}${baseTheme.path}/patternfly/js/patternfly.min.js"></script> +<script type="text/javascript" src="${pageContext.request.contextPath}${initParam['obrandThemePath']}${baseTheme.path}/mousetrack.js"></script> <c:choose> <c:when test="${fn:containsIgnoreCase(header['User-Agent'],'MSIE 8.0')}"> <script type="text/javascript" src="${pageContext.request.contextPath}${initParam['obrandThemePath']}${baseTheme.path}/patternfly/components/respond/dest/respond.min.js"></script> diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/utils/ElementUtils.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/utils/ElementUtils.java index 484014b..2339472 100644 --- a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/utils/ElementUtils.java +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/utils/ElementUtils.java @@ -36,4 +36,51 @@ return clientHeightAfter > clientHeightBefore || clientHeightAfter < scrollHeightAfter; } + /** + * Check up this Element's ancestor tree, and return true if we find a null parent. + * @return true if a null parent is found, false if this Element has body as an ancestor + * (meaning it's still attached). + */ + public static boolean hasNullAncestor(Element element) { + Element parent = element.getParentElement(); + while (parent != null) { + if (parent.getTagName().equalsIgnoreCase("body")) { //$NON-NLS-1$ + return false; + } + parent = parent.getParentElement(); + } + return true; + } + + /** + * Is an Element an ancestor of another Element? + */ + public static boolean hasAncestor(Element element, Element ancestor) { + + if (ancestor == null || element == null) { + return false; + } + + Element parent = element.getParentElement(); + while (parent != null) { + if (parent.getInnerHTML().equals(ancestor.getInnerHTML())) { + return true; + } + parent = parent.getParentElement(); + } + return false; + } + + /** + * Return any element found at point x, y. + */ + public static native Element getElementFromPoint(int clientX, int clientY) + /*-{ + var el = $wnd.document.elementFromPoint(clientX, clientY); + if(el != null && el.nodeType == 3) { + el = el.parentNode; + } + return el; + }-*/; + } diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/utils/JqueryUtils.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/utils/JqueryUtils.java new file mode 100644 index 0000000..8f6487b --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/utils/JqueryUtils.java @@ -0,0 +1,48 @@ +package org.ovirt.engine.ui.common.utils; + + +/** + * A collection of native functions for executing jquery code. + * + * Please have a *really* good reason to use jquery. + * + */ +public class JqueryUtils { + + /** + * Fire a mouseenter event at an element found at x,y. Must use jquery because + * GWT doesn't support 'mouseenter.' + * + * @param clientX + * @param clientY + * @return + */ + public static native void fireMouseEnter(int clientX, int clientY) + /*-{ + var el = $wnd.document.elementFromPoint(clientX, clientY); + if(el != null && el.nodeType == 3) { + el = el.parentNode; + console.log(el.parentNode); + } + $wnd.jQuery(el).trigger("mouseenter"); + }-*/; + + /** + * Is there any open tooltip visible? + * + * @return + */ + public static native boolean anyTooltipVisible() /*-{ + var $tooltip = $wnd.jQuery("div.tooltip:visible"); + if ($tooltip.length == 0) return false; + return true; + }-*/; + + /** + * Get mouse position + */ + public static native String getMousePosition() /*-{ + return "" + $wnd.mouseX + "," + $wnd.mouseY; + }-*/; + +} diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/cell/AbstractTooltipCell.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/cell/AbstractTooltipCell.java new file mode 100644 index 0000000..726e343 --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/cell/AbstractTooltipCell.java @@ -0,0 +1,133 @@ +package org.ovirt.engine.ui.common.widget.table.cell; + +import java.util.HashSet; +import java.util.Set; + +import org.ovirt.engine.ui.common.utils.ElementIdUtils; +import org.ovirt.engine.ui.common.widget.tooltip.TooltipMixin; + +import com.google.gwt.cell.client.AbstractCell; +import com.google.gwt.cell.client.ValueUpdater; +import com.google.gwt.dom.client.BrowserEvents; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.user.client.DOM; + +/** + * <p> + * This should be the base class of every oVirt custom Cell that would otherwise extend AbstractCell. + * </p> + * <p> + * Cell that displays a tooltip when hovered over. Uses the jQuery-based Bootstrap / PatternFly + * tooltip widget, ElementTooltip. While this cell doesn't have state, it + * relies on state saved in ElementTooltip. + * </p> + * <p> + * Supports rendering Element ids via the oVirt Element-ID framework. + * </p> + */ +public abstract class AbstractTooltipCell<C> extends AbstractCell<C> implements TooltipCell<C>, CellWithElementId<C> { + + private String elementIdPrefix = DOM.createUniqueId(); // default + private String columnId; + + /** + * Events to sink. By default, we only sink mouse events that tooltips need. Override this + * (and include addAll(super.getConsumedEvents())'s events!) if your cell needs to respond + * to additional events. + */ + @Override + public Set<String> getConsumedEvents() { + Set<String> set = new HashSet<String>(); + TooltipMixin.addTooltipsEvents(set); + return set; + } + + /** + * Don't call this from a custom Column. This is only used in the case this Cell is used by + * a CompositeCell. In that case, we give each component Cell of the Composite a chance to render its own + * tooltip. + * + * See userportal's composite action button cell as an example (SideTabExtendedVirtualMachineView). + * + * @see com.google.gwt.cell.client.AbstractCell#onBrowserEvent(com.google.gwt.cell.client.Cell.Context, com.google.gwt.dom.client.Element, java.lang.Object, com.google.gwt.dom.client.NativeEvent, com.google.gwt.cell.client.ValueUpdater) + */ + @Override + public void onBrowserEvent(Context context, Element parent, C value, NativeEvent event, ValueUpdater<C> valueUpdater) { + onBrowserEvent(context, parent, value, null, event, valueUpdater); + } + + /** + * Handle events for this cell. + * + * @see org.ovirt.engine.ui.common.widget.table.cell.TooltipCell#onBrowserEvent(com.google.gwt.cell.client.Cell.Context, com.google.gwt.dom.client.Element, java.lang.Object, com.google.gwt.safehtml.shared.SafeHtml, com.google.gwt.dom.client.NativeEvent, com.google.gwt.cell.client.ValueUpdater) + */ + public void onBrowserEvent(Context context, Element parent, C value, + SafeHtml tooltipContent, NativeEvent event, ValueUpdater<C> valueUpdater) { + + // if the Column did not provide a tooltip, give the Cell a chance to render one using the cell value C + if (tooltipContent == null) { + tooltipContent = getTooltip(value); + } + + if (BrowserEvents.MOUSEOVER.equals(event.getType())) { + TooltipMixin.configureTooltip(parent, tooltipContent, event); + } + + if (BrowserEvents.MOUSEOUT.equals(event.getType())) { + TooltipMixin.reapAllTooltips(); + } + + if (BrowserEvents.MOUSEDOWN.equals(event.getType())) { + TooltipMixin.hideAllTooltips(); + } + } + + /** + * Let the Cell render the tooltip using C value. This is only attempted if the Column itself + * did not provide a tooltip. This is usually only used when there is a Composite Column that + * contains multiple Cells, but each Cell needs its own tooltip. + */ + public SafeHtml getTooltip(C value) { + return null; + } + + /** + * Override the normal render to pass along an id. + * + * @see com.google.gwt.cell.client.AbstractCell#render(com.google.gwt.cell.client.Cell.Context, java.lang.Object, com.google.gwt.safehtml.shared.SafeHtmlBuilder) + */ + public void render(Context context, C value, SafeHtmlBuilder sb) { + String id = ElementIdUtils.createTableCellElementId(getElementIdPrefix(), getColumnId(), context); + render(context, value, sb, id); + } + + /** + * Render the cell. Using the value, the id, and the context, append some HTML to the + * SafeHtmlBuilder that will show in the cell when it is rendered. + * + * Override this and use the id in your render. + * + * @see org.ovirt.engine.ui.common.widget.table.cell.TooltipCell#render(com.google.gwt.cell.client.Cell.Context, java.lang.Object, com.google.gwt.safehtml.shared.SafeHtmlBuilder, java.lang.String) + */ + public abstract void render(Context context, C value, SafeHtmlBuilder sb, String id); + + public void setElementIdPrefix(String elementIdPrefix) { + this.elementIdPrefix = elementIdPrefix; + } + + public void setColumnId(String columnId) { + this.columnId = columnId; + } + + public String getElementIdPrefix() { + return elementIdPrefix; + } + + public String getColumnId() { + return columnId; + } + +} diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/cell/TooltipCell.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/cell/TooltipCell.java new file mode 100644 index 0000000..85f3fa8 --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/cell/TooltipCell.java @@ -0,0 +1,30 @@ +package org.ovirt.engine.ui.common.widget.table.cell; + +import com.google.gwt.cell.client.Cell; +import com.google.gwt.cell.client.ValueUpdater; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; + +/** + * A Cell with a tooltip. All Cells should implement this by extending ovirt's AbstractCell. + * + * @param <C> cell render type + */ +public interface TooltipCell<C> extends Cell<C>, CellWithElementId<C> { + + /** + * Called by AbstractColumn when an event occurs in a Cell. The only difference from GWT's native + * Column is that here we ask the column to provide us a tooltip value in addition to the cell's + * C value. + */ + public void onBrowserEvent(Context context, final Element parent, C value, final SafeHtml tooltipContent, + final NativeEvent event, ValueUpdater<C> valueUpdater); + + /** + * Called by AbstractColumn to render a cell. Sends the cell id so your template can include it + * in the render. + */ + public abstract void render(Context context, C value, SafeHtmlBuilder sb, String id); +} diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/column/AbstractColumn.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/column/AbstractColumn.java new file mode 100644 index 0000000..0279696 --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/column/AbstractColumn.java @@ -0,0 +1,67 @@ +package org.ovirt.engine.ui.common.widget.table.column; + +import org.ovirt.engine.ui.common.widget.table.cell.TooltipCell; + +import com.google.gwt.cell.client.Cell.Context; +import com.google.gwt.cell.client.ValueUpdater; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.user.cellview.client.Column; + +/** + * A {@link Column}. Supports tooltips. + * TODO: add sorting support. Add element-id support. + * + * @param <T> + * Table row data type. + * @param <C> + * Cell data type. + */ +public abstract class AbstractColumn<T, C> extends Column<T, C> implements ColumnWithElementId { + + public AbstractColumn(TooltipCell<C> cell) { + super(cell); + } + + public TooltipCell<C> getCell() { + return (TooltipCell<C>) super.getCell(); + } + + /** + * <p> + * Implement this to return tooltip content for T object. You'll likely use some member(s) + * of T to build a tooltip. You could also use a constant if the tooltip is always the same + * for this column. + * </p> + * <p> + * The tooltip cell will then use this value when rendering the cell. + * </p> + * + * @param object + * @return tooltip content + */ + public abstract SafeHtml getTooltip(T object); + + /** + * This is copied from GWT's Column, but we also inject the tooltip content into the cell. + * TODO-GWT: make sure that this method is in sync with Column::onBroswerEvent. + */ + public void onBrowserEvent(Context context, Element elem, final T object, NativeEvent event) { + final int index = context.getIndex(); + ValueUpdater<C> valueUpdater = (getFieldUpdater() == null) ? null : new ValueUpdater<C>() { + @Override + public void update(C value) { + getFieldUpdater().update(index, object, value); + } + }; + getCell().onBrowserEvent(context, elem, getValue(object), /***/ getTooltip(object) /***/, event, valueUpdater); + } + + @Override + public void configureElementId(String elementIdPrefix, String columnId) { + getCell().setElementIdPrefix(elementIdPrefix); + getCell().setColumnId(columnId); + } + +} diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/column/ImageResourceCell.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/column/ImageResourceCell.java new file mode 100644 index 0000000..ae7498c --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/table/column/ImageResourceCell.java @@ -0,0 +1,59 @@ +package org.ovirt.engine.ui.common.widget.table.column; + +import org.ovirt.engine.ui.common.widget.table.HasStyleClass; +import org.ovirt.engine.ui.common.widget.table.cell.AbstractTooltipCell; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.safehtml.client.SafeHtmlTemplates; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlBuilder; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.google.gwt.user.client.ui.AbstractImagePrototype; + +/** + * Cell that renders and ImageResource. Supports setting a style / class. Supports tooltips. + * TODO: this will replace StyledImageResourceCell. Delete it and change references. + */ +public class ImageResourceCell extends AbstractTooltipCell<ImageResource> implements HasStyleClass { + + interface CellTemplate extends SafeHtmlTemplates { + @Template("<div id=\"{0}\" style=\"{1}\" class=\"{2}\">{3}</div>") + SafeHtml imageContainerWithStyleClass(String id, String style, String styleClass, SafeHtml imageHtml); + } + + private String style = "line-height: 100%; text-align: center; vertical-align: middle;"; //$NON-NLS-1$ + private String styleClass = ""; //$NON-NLS-1$ + + private CellTemplate template; + + public ImageResourceCell() { + super(); + + // Delay cell template creation until the first time it's needed + if (template == null) { + template = GWT.create(CellTemplate.class); + } + } + + @Override + public void render(Context context, ImageResource value, SafeHtmlBuilder sb, String id) { + if (value != null) { + sb.append(template.imageContainerWithStyleClass( + id, + style, + styleClass, + SafeHtmlUtils.fromTrustedString(AbstractImagePrototype.create(value).getHTML()))); + } + } + + public void setStyle(String style) { + this.style = style; + } + + @Override + public void setStyleClass(String styleClass) { + this.styleClass = styleClass == null ? "" : styleClass; //$NON-NLS-1$ + } + +} diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/ElementTooltip.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/ElementTooltip.java new file mode 100644 index 0000000..2a391a0 --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/ElementTooltip.java @@ -0,0 +1,615 @@ +package org.ovirt.engine.ui.common.widget.tooltip; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Logger; + +import org.gwtbootstrap3.client.ui.base.HasHover; +import org.gwtbootstrap3.client.ui.base.HasId; +import org.gwtbootstrap3.client.ui.constants.Placement; +import org.gwtbootstrap3.client.ui.constants.Trigger; +import org.ovirt.engine.ui.common.utils.ElementUtils; +import org.ovirt.engine.ui.common.utils.JqueryUtils; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Timer; + +/** + * <p> + * Implementation of Bootstrap tooltips that are capable of wrapping non-GWT elements. + * This is designed primarily for use in grids, but could be used in any Cell. Since + * Cells don't support Widget's event system, we must use a reaper timer to check for + * when a tooltip's Element has been detached from the document. + * </p> + * <p> + * Bootstrap tooltips use jQuery under the hood. jQuery must be present for this widget + * to function. + * </p> + * <p> + * <br/> + * See also: <br/> + * <a href="http://getbootstrap.com/javascript/#tooltips">Bootstrap Documentation</a> + * <br/> + * See also: <br/> + * {@link Tooltip} + * </p> + * <p> + * ** Must call reconfigure() after altering any/all Tooltips! + * </p> + * + * @author Joshua Godi + * @author Pontus Enmark + * @author Greg Sheremeta + */ +public class ElementTooltip implements HasId, HasHover { + private static final String TOGGLE = "toggle"; //$NON-NLS-1$ + private static final String SHOW = "show"; //$NON-NLS-1$ + private static final String HIDE = "hide"; //$NON-NLS-1$ + private static final String DESTROY = "destroy"; //$NON-NLS-1$ + + private boolean isAnimated = TooltipConfig.IS_ANIMATED; + private boolean isHTML = TooltipConfig.IS_HTML; + private Placement placement = TooltipConfig.PLACEMENT; + private Trigger trigger = TooltipConfig.TRIGGER; + private SafeHtml content = null; + private String container = TooltipConfig.CONTAINER; + private final String selector = null; + + private int hideDelayMs = TooltipConfig.HIDE_DELAY_MS; + private int showDelayMs = TooltipConfig.SHOW_DELAY_MS; + private int reaperInterval = 200; + private Timer reaperTimer = null; + + private Element element; + private String id; + + private static final Logger logger = Logger.getLogger(ElementTooltip.class.getName()); + + /** + * A static registry of all tooltips in existence. Keyed by the id of the element the + * tooltip is bound to. + */ + private static Map<String, ElementTooltipDetails> tooltipRegistry = new HashMap<String, ElementTooltipDetails>(); + + /** + * Creates an empty ElementTooltip + */ + public ElementTooltip() { + } + + /** + * Creates the tooltip around this element + * + * @param e Element for the tooltip + */ + public ElementTooltip(final Element e) { + setElement(e); + } + + /** + * Sets the Element that this tooltip hovers over. + * @param e Element for the tooltip + */ + public void setElement(final Element e) { + element = e; + bindJavaScriptEvents(element); + } + + /** + * Return the Element that this tooltip hovers over. + */ + public Element getElement() { + return element; + } + + /** + * Return the tooltip registry. + * @return + */ + public static Map<String, ElementTooltipDetails> getRegistry() { + return tooltipRegistry; + } + + /** + * Return a tooltip. + * @return + */ + public static ElementTooltip getTooltip(String id) { + if (isTooltipConfigured(id)) { + return tooltipRegistry.get(id).getTooltip(); + } + return null; + } + + /** + * Is a tooltip in the registry for this id? + */ + public static boolean isTooltipConfigured(String id) { + logger.finer("checking tooltip registry for " + id); //$NON-NLS-1$ + + if (id == null || id.isEmpty()) { + return false; + } + return ElementTooltip.getRegistry().containsKey(id); + } + + /** + * Return the reaper interval. + */ + public int getReaperInterval() { + return reaperInterval; + } + + /** + * Sets the reaper interval. + */ + public void setReaperInterval(int reaperInterval) { + this.reaperInterval = reaperInterval; + } + + /** + * <p> + * Starts a timer that checks for this tooltip to be hanging open. + * This can happen when the Element or one of its ancestors is detached from the document. + * Such detaching happens frequently when Grids are refreshed -- GWT replaces an entire + * grid row, but doesn't actually delete the row. The row just gets removed from its parent + * table. To detect this, we search up the ancestor tree for a null ancestor. + * </p> + * <p> + * This should only be called for a tooltip when it is shown. It will be reaped very quickly, + * within 5 seconds. And since only visible tooltips start the timer, the timer won't run that + * much. In other words, this should not be a performance concern. + * </p> + */ + public void startHangingTooltipReaper() { + if (reaperTimer != null && reaperTimer.isRunning()) { + return; + } + + reaperTimer = new Timer() { + + @Override + public void run() { + logger.finer("reaper timer"); //$NON-NLS-1$ + if (hasNullAncestor()) { + reap(); + cancel(); // cancel this timer since this tooltip is dead + } + } + }; + + reaperTimer.scheduleRepeating(getReaperInterval()); + } + + /** + * Check up this Elements ancestor tree, and return true if we find a null parent. + * @return true if a null parent is found, false if this Element has body as an ancestor + * (meaning it's still attached). + */ + public boolean hasNullAncestor() { + return ElementUtils.hasNullAncestor(element); + } + + /** + * Walk through the tooltip registry and hide and delete any tooltips who are orphaned. + */ + public static void reapAll() { + for (Iterator<Entry<String, ElementTooltipDetails>> i = tooltipRegistry.entrySet().iterator(); i.hasNext(); ) { + Entry<String, ElementTooltipDetails> entry = i.next(); + ElementTooltip tooltip = entry.getValue().getTooltip(); + if (tooltip.hasNullAncestor()) { + tooltip.hide(); + i.remove(); + } + } + } + + /** + * Hide all tooltips. + */ + public static void hideAll() { + for (Iterator<Entry<String, ElementTooltipDetails>> i = tooltipRegistry.entrySet().iterator(); i.hasNext(); ) { + Entry<String, ElementTooltipDetails> entry = i.next(); + ElementTooltip tooltip = entry.getValue().getTooltip(); + tooltip.hide(); + } + } + + /** + * <p> + * Reap this tooltip. That is, hide it and remove it from the registry. + * </p> + * <p> + * Tooltips get reaped primarily when GWT removes their elements from the Document + * (happens every 5 seconds in grid refreshes, for example). They are watching for mouseover + * on an element that will never be visible again. So we make sure they are hidden and deleted + * from the registry. + * </p> + * <p> + * The reap *won't* happen if the mouse cursor is currently over an element that is + * identical to this tooltip's element. In other words, if I'm hovered over an icon in + * a grid, and GWT redraws the grid and puts an identical icon where the old one was, + * leave this open tooltip showing. (It'll end up getting reaped when that cell is + * moused-out of via reapAll(). + */ + public void reap() { + for (Iterator<Entry<String, ElementTooltipDetails>> i = tooltipRegistry.entrySet().iterator(); i.hasNext(); ) { + Entry<String, ElementTooltipDetails> entry = i.next(); + ElementTooltip tooltip = entry.getValue().getTooltip(); + if (this.equals(tooltip)) { + + String[] pos = JqueryUtils.getMousePosition().split(","); //$NON-NLS-1$ + int x = Integer.parseInt(pos[0]); + int y = Integer.parseInt(pos[1]); + + // can we find a replacement element using mouse coordinates? + Element replacement = ElementUtils.getElementFromPoint(x, y); + if (replacement == null) { + logger.finer("can't detect potential replacement element. reaping"); //$NON-NLS-1$ + hide(); + i.remove(); + return; + } + + // we found a potential replacement element. compare the html to see if this + // element is identical to the old one. + String html = entry.getValue().getInnerHTML(); + if (html.contains(replacement.getInnerHTML())) { + // yep, element was replaced with an identical element. DON'T reap. + logger.finer("mouse is hovering over my replacement. not reaping"); //$NON-NLS-1$ + } + else { + logger.finer("mouse is hovering over an element, but it's not identical to previous " //$NON-NLS-1$ + + "element. reaping"); //$NON-NLS-1$ + hide(); + i.remove(); + return; + } + + return; + } + } + } + + /** + * Called when the tooltip is shown. Starts the hanging tooltip reaper timer. + * + * This method also does two very important checks. + * + * First, it checks for "orphaned tooltips." Because of the high-refresh nature of our application, + * tooltipped Elements are often deleted before the tooltip can finish its render. So we check for + * this condition, and simply cancel the render if it is true. Then we fire another mouseover event + * at the current mouse coordinates, assuming the mouse is over the refreshed Element. No harm done + * if the mouse has moved away. + * + * Second, it checks for "abandoned tooltips." mouseover and mouseout are not perfect in any browser. + * Occasionally a mouseover will trigger a tooltip render, and then the user will quickly move the + * mouse away -- but for some reason the mouseout doesn't fire. In this case, a tooltip will render + * over an Element the mouse is no longer over. So we check for this condition, and simply cancel + * the render if it is true. + * + * If any issues with hanging tooltips creep up, start debugging here :) + * + * @param event Event + */ + protected void onShow(final Event event) { + + // handle the case where the element i'm attached to was just removed from the document ("orphaned tooltip") + if (hasNullAncestor()) { + event.preventDefault(); + logger.finer("orphaned tooltip. canceling render, re-firing mouseover"); //$NON-NLS-1$ + + // trigger another mouseover on the new element + String[] pos = JqueryUtils.getMousePosition().split(","); //$NON-NLS-1$ + int x = Integer.parseInt(pos[0]); + int y = Integer.parseInt(pos[1]); + Element replacement = ElementUtils.getElementFromPoint(x, y); + if (replacement != null) { + + // TODO-GWT use mouseenter if it ever becomes supported + NativeEvent newEvent = Document.get().createMouseOverEvent(0, event.getScreenX(), + event.getScreenY(), event.getClientX(), event.getClientY(), event.getCtrlKey(), + event.getAltKey(), event.getShiftKey(), event.getMetaKey(), event.getButton(), replacement); + + replacement.dispatchEvent(newEvent); + return; + } + } + // handle the case where the mouse is not over me anymore ("abandoned tooltip") + else { + String[] pos = JqueryUtils.getMousePosition().split(","); //$NON-NLS-1$ + int x = Integer.parseInt(pos[0]); + int y = Integer.parseInt(pos[1]); + + Element currentElement = ElementUtils.getElementFromPoint(x, y); + Element target = Element.as(event.getEventTarget()); + + if (!ElementUtils.hasAncestor(currentElement, target) && !currentElement.equals(target)) { + logger.finer("abandoned tooltip. canceling render."); //$NON-NLS-1$ + event.preventDefault(); + } + } + + logger.finer("starting tooltip reaper"); //$NON-NLS-1$ + this.startHangingTooltipReaper(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setId(final String id) { + this.id = id; + if (element != null) { + element.setId(id); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getId() { + return (element == null) ? id : element.getId(); + } + + @Override + public void setIsAnimated(final boolean isAnimated) { + this.isAnimated = isAnimated; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isAnimated() { + return isAnimated; + } + + /** + * {@inheritDoc} + */ + @Override + public void setIsHtml(final boolean isHTML) { + this.isHTML = isHTML; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isHtml() { + return isHTML; + } + + /** + * {@inheritDoc} + */ + @Override + public void setPlacement(final Placement placement) { + this.placement = placement; + } + + /** + * {@inheritDoc} + */ + @Override + public Placement getPlacement() { + return placement; + } + + /** + * {@inheritDoc} + */ + @Override + public void setTrigger(final Trigger trigger) { + this.trigger = trigger; + } + + /** + * {@inheritDoc} + */ + @Override + public Trigger getTrigger() { + return trigger; + } + + /** + * {@inheritDoc} + */ + @Override + public void setShowDelayMs(final int showDelayMs) { + this.showDelayMs = showDelayMs; + } + + /** + * {@inheritDoc} + */ + @Override + public int getShowDelayMs() { + return showDelayMs; + } + + /** + * {@inheritDoc} + */ + @Override + public void setHideDelayMs(final int hideDelayMs) { + this.hideDelayMs = hideDelayMs; + } + + /** + * {@inheritDoc} + */ + @Override + public int getHideDelayMs() { + return hideDelayMs; + } + + /** + * {@inheritDoc} + */ + @Override + public void setContainer(final String container) { + this.container = container; + } + + /** + * {@inheritDoc} + */ + @Override + public String getContainer() { + return container; + } + + /** + * Sets the tooltip's HTML content + */ + public void setContent(final SafeHtml content) { + this.isHTML = true; + this.content = content; + } + + /** + * Reconfigures the tooltip, must be called when altering any tooltip after it has already been shown + */ + public void reconfigure() { + // First destroy the old tooltip + destroy(); + + // Setup the new tooltip + if (container != null && selector != null) { + tooltip(element, isAnimated, isHTML, placement.getCssName(), selector, content.asString(), + trigger.getCssName(), showDelayMs, hideDelayMs, container); + } else if (container != null) { + tooltip(element, isAnimated, isHTML, placement.getCssName(), content.asString(), + trigger.getCssName(), showDelayMs, hideDelayMs, container); + } else if (selector != null) { + tooltip(element, isAnimated, isHTML, placement.getCssName(), selector, content.asString(), + trigger.getCssName(), showDelayMs, hideDelayMs); + } else { + tooltip(element, isAnimated, isHTML, placement.getCssName(), content.asString(), + trigger.getCssName(), showDelayMs, hideDelayMs); + } + } + + /** + * Toggle the Tooltip to either show/hide + */ + public void toggle() { + call(element, TOGGLE); + } + + /** + * <p> + * Force show the Tooltip. If you must, you should probably wrap this call in a Timer delay + * of getShowDelayMs() ms. + * </p> + * <p> + * This is generally flaky though. Better to fire a mouseover (or mouseenter) event at the tooltip's element. + * </p> + */ + public void show() { + logger.finer("tooltip show on element id " + element.getId()); //$NON-NLS-1$ + call(element, SHOW); + } + + /** + * Force hide the Tooltip + */ + public void hide() { + call(element, HIDE); + } + + /** + * Force the Tooltip to be destroyed + */ + public void destroy() { + call(element, DESTROY); + } + + private native void call(final Element e, final String arg) /*-{ + $wnd.jQuery(e).tooltip(arg); + }-*/; + + // @formatter:off + private native void bindJavaScriptEvents(final Element e) /*-{ + var target = this; + var $tooltip = $wnd.jQuery(e); + + $tooltip.on('show.bs.tooltip', function (evt) { + [email protected]::onShow(Lcom/google/gwt/user/client/Event;)(evt); + }); + }-*/; + + private native void tooltip(Element e, boolean animation, boolean html, String placement, String selector, + String content, String trigger, int showDelay, int hideDelay, String container) /*-{ + $wnd.jQuery(e).tooltip({ + animation: animation, + html: html, + placement: placement, + selector: selector, + title: content, + trigger: trigger, + delay: { + show: showDelay, + hide: hideDelay + }, + container: container + }); + }-*/; + + private native void tooltip(Element e, boolean animation, boolean html, String placement, + String content, String trigger, int showDelay, int hideDelay, String container) /*-{ + $wnd.jQuery(e).tooltip({ + animation: animation, + html: html, + placement: placement, + title: content, + trigger: trigger, + delay: { + show: showDelay, + hide: hideDelay + }, + container: container + }); + }-*/; + + private native void tooltip(Element e, boolean animation, boolean html, String placement, String selector, + String content, String trigger, int showDelay, int hideDelay) /*-{ + $wnd.jQuery(e).tooltip({ + animation: animation, + html: html, + placement: placement, + selector: selector, + title: content, + trigger: trigger, + delay: { + show: showDelay, + hide: hideDelay + } + }); + }-*/; + + private native void tooltip(Element e, boolean animation, boolean html, String placement, + String content, String trigger, int showDelay, int hideDelay) /*-{ + console.log($wnd.jQuery(e).tooltip({ + animation: animation, + html: html, + placement: placement, + title: content, + trigger: trigger, + delay: { + show: showDelay, + hide: hideDelay + } + })); + }-*/; + +} diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/ElementTooltipDetails.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/ElementTooltipDetails.java new file mode 100644 index 0000000..6c541c5 --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/ElementTooltipDetails.java @@ -0,0 +1,27 @@ +package org.ovirt.engine.ui.common.widget.tooltip; + +/** + * Wrapper for storing tooltip + its element's html in a map. + */ +public class ElementTooltipDetails { + + private ElementTooltip tooltip; + private String innerHTML; + + public ElementTooltip getTooltip() { + return tooltip; + } + + public void setTooltip(ElementTooltip tooltip) { + this.tooltip = tooltip; + } + + public String getInnerHTML() { + return innerHTML; + } + + public void setInnerHTML(String innerHTML) { + this.innerHTML = innerHTML; + } + +} diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/Tooltip.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/Tooltip.java new file mode 100644 index 0000000..759c1da --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/Tooltip.java @@ -0,0 +1,723 @@ +package org.ovirt.engine.ui.common.widget.tooltip; + +/* + * #%L + * GwtBootstrap3 + * %% + * Copyright (C) 2013 GwtBootstrap3 + * %% + * 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. + * #L% + */ + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.gwtbootstrap3.client.shared.event.HiddenEvent; +import org.gwtbootstrap3.client.shared.event.HiddenHandler; +import org.gwtbootstrap3.client.shared.event.HideEvent; +import org.gwtbootstrap3.client.shared.event.HideHandler; +import org.gwtbootstrap3.client.shared.event.ShowEvent; +import org.gwtbootstrap3.client.shared.event.ShowHandler; +import org.gwtbootstrap3.client.shared.event.ShownEvent; +import org.gwtbootstrap3.client.shared.event.ShownHandler; +import org.gwtbootstrap3.client.ui.base.HasHover; +import org.gwtbootstrap3.client.ui.base.HasId; +import org.gwtbootstrap3.client.ui.constants.Placement; +import org.gwtbootstrap3.client.ui.constants.Trigger; + +import com.google.gwt.dom.client.Element; +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.ui.HasOneWidget; +import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.IsWidget; +import com.google.gwt.user.client.ui.Widget; +import com.google.web.bindery.event.shared.HandlerRegistration; + +/** + * ======================================================================================== + * oVirt customization of gwtbootstrap3 tooltip + * ======================================================================================== + * TODO-GWT switch back to using native gwtbootstrap3 tooltip when upstream patch + * xxx is merged and released. + * ======================================================================================== + * ======================================================================================== + * + * Basic implementation for the Bootstrap tooltip + * <p/> + * <a href="http://getbootstrap.com/javascript/#tooltips">Bootstrap Documentation</a> + * <p/> + * <p/> + * <h3>UiBinder example</h3> + * <p/> + * <pre> + * {@code + * <t:Tooltip text="..."> + * ... + * </t:Tooltip> + * } + * </pre> + * <p/> + * ** Must call reconfigure() after altering any/all Tooltips! + * + * @author Joshua Godi + * @author Pontus Enmark + * @author Greg Sheremeta + */ +public class Tooltip implements IsWidget, HasWidgets, HasOneWidget, HasId, HasHover { + private static final String TOGGLE = "toggle"; //$NON-NLS-1$ + private static final String SHOW = "show"; //$NON-NLS-1$ + private static final String HIDE = "hide"; //$NON-NLS-1$ + private static final String DESTROY = "destroy"; //$NON-NLS-1$ + + // Defaults from http://getbootstrap.com/javascript/#tooltips + private boolean isAnimated = true; + private boolean isHTML = false; + private Placement placement = Placement.TOP; + private Trigger trigger = Trigger.HOVER; + private String title = ""; //$NON-NLS-1$ + private int hideDelayMs = 0; + private int showDelayMs = 0; + private String container = null; + private final String selector = null; + + private String tooltipClassNames = "tooltip"; //$NON-NLS-1$ + private String tooltipArrowClassNames = "tooltip-arrow"; //$NON-NLS-1$ + private String tooltipInnerClassNames = "tooltip-inner"; //$NON-NLS-1$ + + private final static String DEFAULT_TEMPLATE = "<div class=\"{0}\"><div class=\"{1}\"></div><div class=\"{2}\"></div></div>"; //$NON-NLS-1$ + private String alternateTemplate = null; + + private Widget widget; + private String id; + + /** + * Creates the empty Tooltip + */ + public Tooltip() { + } + + /** + * Creates the tooltip around this widget + * + * @param w widget for the tooltip + */ + public Tooltip(final Widget w) { + setWidget(w); + } + + /** + * {@inheritDoc} + */ + @Override + public void setWidget(final Widget w) { + // Validate + if (w == widget) { + return; + } + + // Detach new child + if (w != null) { + w.removeFromParent(); + } + + // Remove old child + if (widget != null) { + remove(widget); + } + + // Logical attach, but don't physical attach; done by jquery. + widget = w; + if (widget == null) { + return; + } + + // Bind jquery events + bindJavaScriptEvents(widget.getElement()); + + // When we attach it, configure the tooltip + widget.addAttachHandler(new AttachEvent.Handler() { + @Override + public void onAttachOrDetach(final AttachEvent event) { + reconfigure(); + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + public void add(final Widget child) { + if (getWidget() != null) { + throw new IllegalStateException("Can only contain one child widget"); //$NON-NLS-1$ + } + setWidget(child); + } + + /** + * {@inheritDoc} + */ + @Override + public void setWidget(final IsWidget w) { + widget = (w == null) ? null : w.asWidget(); + } + + /** + * {@inheritDoc} + */ + @Override + public Widget getWidget() { + return widget; + } + + /** + * {@inheritDoc} + */ + @Override + public void setId(final String id) { + this.id = id; + if (widget != null) { + widget.getElement().setId(id); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getId() { + return (widget == null) ? id : widget.getElement().getId(); + } + + @Override + public void setIsAnimated(final boolean isAnimated) { + this.isAnimated = isAnimated; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isAnimated() { + return isAnimated; + } + + /** + * {@inheritDoc} + */ + @Override + public void setIsHtml(final boolean isHTML) { + this.isHTML = isHTML; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isHtml() { + return isHTML; + } + + /** + * {@inheritDoc} + */ + @Override + public void setPlacement(final Placement placement) { + this.placement = placement; + } + + /** + * {@inheritDoc} + */ + @Override + public Placement getPlacement() { + return placement; + } + + /** + * {@inheritDoc} + */ + @Override + public void setTrigger(final Trigger trigger) { + this.trigger = trigger; + } + + /** + * {@inheritDoc} + */ + @Override + public Trigger getTrigger() { + return trigger; + } + + @Override + public void setShowDelayMs(final int showDelayMs) { + this.showDelayMs = showDelayMs; + } + + /** + * {@inheritDoc} + */ + @Override + public int getShowDelayMs() { + return showDelayMs; + } + + /** + * {@inheritDoc} + */ + @Override + public void setHideDelayMs(final int hideDelayMs) { + this.hideDelayMs = hideDelayMs; + } + + /** + * {@inheritDoc} + */ + @Override + public int getHideDelayMs() { + return hideDelayMs; + } + + /** + * {@inheritDoc} + */ + @Override + public void setContainer(final String container) { + this.container = container; + } + + /** + * {@inheritDoc} + */ + @Override + public String getContainer() { + return container; + } + + /** + * Gets the tooltip's display string + * + * @return String tooltip display string + */ + public String getTitle() { + return title; + } + + /** + * Sets the tooltip's display string + * + * @param text String display string + */ + public void setText(final String text) { + setTitle(text); + } + + /** + * Sets the tooltip's display string in HTML format + * + * @param text String display string in HTML format + */ + public void setHtml(final SafeHtml html) { + setHTML(true); + if (html != null) { + setTitle(html.asString()); + } + } + + /** + * Sets the tooltip's display string + * + * @param title String display string + */ + public void setTitle(final String title) { + this.title = title; + } + + public boolean isHTML() { + return isHTML; + } + + public void setHTML(boolean isHTML) { + this.isHTML = isHTML; + } + + public String getSelector() { + return selector; + } + + public void setAnimated(boolean isAnimated) { + this.isAnimated = isAnimated; + } + + public String getTooltipClassNames() { + return tooltipClassNames; + } + + public void setTooltipClassNames(String tooltipClassNames) { + this.tooltipClassNames = tooltipClassNames; + } + + public void addTooltipClassName(String tooltipClassName) { + this.tooltipClassNames += " " + tooltipClassName; //$NON-NLS-1$ + } + + public String getTooltipArrowClassNames() { + return tooltipArrowClassNames; + } + + public void setTooltipArrowClassNames(String tooltipArrowClassNames) { + this.tooltipArrowClassNames = tooltipArrowClassNames; + } + + public void addTooltipArrowClassName(String tooltipArrowClassName) { + this.tooltipArrowClassNames += " " + tooltipArrowClassName; //$NON-NLS-1$ + } + + public String getTooltipInnerClassNames() { + return tooltipInnerClassNames; + } + + public void setTooltipInnerClassNames(String tooltipInnerClassNames) { + this.tooltipInnerClassNames = tooltipInnerClassNames; + } + + public void addTooltipInnerClassName(String tooltipInnerClassName) { + this.tooltipInnerClassNames += " " + tooltipInnerClassName; //$NON-NLS-1$ + } + + public String getTemplate() { + return alternateTemplate; + } + + public void setTemplate(String alternateTemplate) { + this.alternateTemplate = alternateTemplate; + } + + /** + * Reconfigures the tooltip, must be called when altering any tooltip after it has already been shown + */ + public void reconfigure() { + // First destroy the old tooltip + destroy(); + + // prepare template + String template = null; + if (alternateTemplate == null) { + template = DEFAULT_TEMPLATE.replace("{0}", getTooltipClassNames()); //$NON-NLS-1$ + template = template.replace("{1}", getTooltipArrowClassNames()); //$NON-NLS-1$ + template = template.replace("{2}", getTooltipInnerClassNames()); //$NON-NLS-1$ + } + else { + template = alternateTemplate; + } + + // TODO clean this up + + // Setup the new tooltip + if (container != null && selector != null) { + tooltip(widget.getElement(), isAnimated, isHTML, placement.getCssName(), selector, title, + trigger.getCssName(), showDelayMs, hideDelayMs, container, template); + } else if (container != null) { + tooltip(widget.getElement(), isAnimated, isHTML, placement.getCssName(), title, + trigger.getCssName(), showDelayMs, hideDelayMs, container, template); + } else if (selector != null) { + tooltip(widget.getElement(), isAnimated, isHTML, placement.getCssName(), selector, title, + trigger.getCssName(), showDelayMs, hideDelayMs, template); + } else { + tooltip(widget.getElement(), isAnimated, isHTML, placement.getCssName(), title, + trigger.getCssName(), showDelayMs, hideDelayMs, template); + } + } + + /** + * Toggle the Tooltip to either show/hide + */ + public void toggle() { + call(widget.getElement(), TOGGLE); + } + + /** + * Force show the Tooltip + */ + public void show() { + call(widget.getElement(), SHOW); + } + + /** + * Force hide the Tooltip + */ + public void hide() { + call(widget.getElement(), HIDE); + } + + /** + * Force the Tooltip to be destroyed + */ + public void destroy() { + call(widget.getElement(), DESTROY); + } + + /** + * Can be override by subclasses to handle Tooltip's "show" event however + * it's recommended to add an event handler to the tooltip. + * + * @param evt Event + * @see org.gwtbootstrap3.client.shared.event.ShowEvent + */ + protected void onShow(final Event evt) { + widget.fireEvent(new ShowEvent(evt)); + } + + /** + * Can be override by subclasses to handle Tooltip's "shown" event however + * it's recommended to add an event handler to the tooltip. + * + * @param evt Event + * @see ShownEvent + */ + protected void onShown(final Event evt) { + widget.fireEvent(new ShownEvent(evt)); + } + + /** + * Can be override by subclasses to handle Tooltip's "hide" event however + * it's recommended to add an event handler to the tooltip. + * + * @param evt Event + * @see org.gwtbootstrap3.client.shared.event.HideEvent + */ + protected void onHide(final Event evt) { + widget.fireEvent(new HideEvent(evt)); + } + + /** + * Can be override by subclasses to handle Tooltip's "hidden" event however + * it's recommended to add an event handler to the tooltip. + * + * @param evt Event + * @see org.gwtbootstrap3.client.shared.event.HiddenEvent + */ + protected void onHidden(final Event evt) { + widget.fireEvent(new HiddenEvent(evt)); + } + + /** + * Adds a show handler to the Tooltip that will be fired when the Tooltip's show event is fired + * + * @param showHandler ShowHandler to handle the show event + * @return HandlerRegistration of the handler + */ + public HandlerRegistration addShowHandler(final ShowHandler showHandler) { + return widget.addHandler(showHandler, ShowEvent.getType()); + } + + /** + * Adds a shown handler to the Tooltip that will be fired when the Tooltip's shown event is fired + * + * @param shownHandler ShownHandler to handle the shown event + * @return HandlerRegistration of the handler + */ + public HandlerRegistration addShownHandler(final ShownHandler shownHandler) { + return widget.addHandler(shownHandler, ShownEvent.getType()); + } + + /** + * Adds a hide handler to the Tooltip that will be fired when the Tooltip's hide event is fired + * + * @param hideHandler HideHandler to handle the hide event + * @return HandlerRegistration of the handler + */ + public HandlerRegistration addHideHandler(final HideHandler hideHandler) { + return widget.addHandler(hideHandler, HideEvent.getType()); + } + + /** + * Adds a hidden handler to the Tooltip that will be fired when the Tooltip's hidden event is fired + * + * @param hiddenHandler HiddenHandler to handle the hidden event + * @return HandlerRegistration of the handler + */ + public HandlerRegistration addHiddenHandler(final HiddenHandler hiddenHandler) { + return widget.addHandler(hiddenHandler, HiddenEvent.getType()); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + widget = null; + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator<Widget> iterator() { + // Simple iterator for the widget + return new Iterator<Widget>() { + boolean hasElement = widget != null; + Widget returned = null; + + @Override + public boolean hasNext() { + return hasElement; + } + + @Override + public Widget next() { + if (!hasElement || (widget == null)) { + throw new NoSuchElementException(); + } + hasElement = false; + return (returned = widget); + } + + @Override + public void remove() { + if (returned != null) { + Tooltip.this.remove(returned); + } + } + }; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean remove(final Widget w) { + // Validate. + if (widget != w) { + return false; + } + + // Logical detach. + clear(); + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public Widget asWidget() { + return widget; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return asWidget().toString(); + } + + // @formatter:off + private native void bindJavaScriptEvents(final Element e) /*-{ + var target = this; + var $tooltip = $wnd.jQuery(e); + + $tooltip.on('show.bs.tooltip', function (evt) { + [email protected]::onShow(Lcom/google/gwt/user/client/Event;)(evt); + }); + + $tooltip.on('shown.bs.tooltip', function (evt) { + [email protected]::onShown(Lcom/google/gwt/user/client/Event;)(evt); + }); + + $tooltip.on('hide.bs.tooltip', function (evt) { + [email protected]::onHide(Lcom/google/gwt/user/client/Event;)(evt); + }); + + $tooltip.on('hidden.bs.tooltip', function (evt) { + [email protected]::onHidden(Lcom/google/gwt/user/client/Event;)(evt); + }); + }-*/; + + private native void call(final Element e, final String arg) /*-{ + $wnd.jQuery(e).tooltip(arg); + }-*/; + + private native void tooltip(Element e, boolean animation, boolean html, String placement, String selector, + String title, String trigger, int showDelay, int hideDelay, String container, String template) /*-{ + $wnd.jQuery(e).tooltip({ + animation: animation, + html: html, + placement: placement, + selector: selector, + title: title, + trigger: trigger, + delay: { + show: showDelay, + hide: hideDelay + }, + container: container, + template: template + }); + }-*/; + + private native void tooltip(Element e, boolean animation, boolean html, String placement, + String title, String trigger, int showDelay, int hideDelay, String container, String template) /*-{ + $wnd.jQuery(e).tooltip({ + animation: animation, + html: html, + placement: placement, + title: title, + trigger: trigger, + delay: { + show: showDelay, + hide: hideDelay + }, + container: container, + template: template + }); + }-*/; + + private native void tooltip(Element e, boolean animation, boolean html, String placement, String selector, + String title, String trigger, int showDelay, int hideDelay, String template) /*-{ + $wnd.jQuery(e).tooltip({ + animation: animation, + html: html, + placement: placement, + selector: selector, + title: title, + trigger: trigger, + delay: { + show: showDelay, + hide: hideDelay + }, + template: template + }); + }-*/; + + private native void tooltip(Element e, boolean animation, boolean html, String placement, + String title, String trigger, int showDelay, int hideDelay, String template) /*-{ + $wnd.jQuery(e).tooltip({ + animation: animation, + html: html, + placement: placement, + title: title, + trigger: trigger, + delay: { + show: showDelay, + hide: hideDelay + }, + template: template + }); + }-*/; +} diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/TooltipConfig.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/TooltipConfig.java new file mode 100644 index 0000000..f41b614 --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/TooltipConfig.java @@ -0,0 +1,37 @@ +package org.ovirt.engine.ui.common.widget.tooltip; + +import org.gwtbootstrap3.client.ui.constants.Placement; +import org.gwtbootstrap3.client.ui.constants.Trigger; + +/** + * Constant configuration values shared across all tooltips. + */ +public class TooltipConfig { + + public enum Width { + W220 ("tooltip-w220"), //$NON-NLS-1$ + W320 ("tooltip-w320"), //$NON-NLS-1$ + W420 ("tooltip-w420"), //$NON-NLS-1$ + W520 ("tooltip-w520"), //$NON-NLS-1$ + W620 ("tooltip-w620"); //$NON-NLS-1$ + + private final String widthClass; // in px + + Width(String widthClass) { + this.widthClass = widthClass; + } + + public String getWidthClass() { + return widthClass; + } + } + + public final static boolean IS_ANIMATED = true; + public final static boolean IS_HTML = true; + public final static Placement PLACEMENT = Placement.TOP; + public final static Trigger TRIGGER = Trigger.HOVER; + public final static String CONTAINER = "body"; //$NON-NLS-1$ + public final static int HIDE_DELAY_MS = 0; + public final static int SHOW_DELAY_MS = 500; + +} diff --git a/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/TooltipMixin.java b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/TooltipMixin.java new file mode 100644 index 0000000..b31497d --- /dev/null +++ b/frontend/webadmin/modules/gwt-common/src/main/java/org/ovirt/engine/ui/common/widget/tooltip/TooltipMixin.java @@ -0,0 +1,140 @@ +package org.ovirt.engine.ui.common.widget.tooltip; + +import java.util.Set; +import java.util.logging.Logger; + +import org.ovirt.engine.ui.common.utils.JqueryUtils; + +import com.google.gwt.dom.client.BrowserEvents; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Timer; + +/** + * + * Set of static methods used by tooltip-capable Cells to group the tooltip logic in one place. + * + * TODO When we have Java 8 mixins, consider using those in place of this class. + * + * There is currently some GWT or ovirt bug causing duplicate mouseover and mouseout events. + * It doesn't affect the logic, but be aware of it when working on this code. + * + */ +public class TooltipMixin { + + private static final Logger logger = Logger.getLogger(TooltipMixin.class.getName()); + + /** + * Add events that tooltips care about (over, down, out) to a cell's Set of sunk events. + */ + public static void addTooltipsEvents(Set<String> set) { + set.add(BrowserEvents.MOUSEOVER); + set.add(BrowserEvents.MOUSEOUT); + set.add(BrowserEvents.MOUSEDOWN); + } + + /** + * Hijack mouseover event and use it to create the tooltip. This is done the first time a Cell is moused-over + * because that's the only place GWT gives us access to the actual Element. + * + * Once the tooltip is configured, we need to fire a new mouseenter event at the cell so jQuery can pick it + * up and show the tooltip. + */ + public static void configureTooltip(final Element parent, SafeHtml tooltipContent, final NativeEvent event) { + if (tooltipContent == null || tooltipContent.asString().trim().isEmpty()) { + // there is no tooltip for this render. no-op. + logger.finer("null or empty tooltip content"); //$NON-NLS-1$ + } + else if (isTooltipConfigured(parent)) { + logger.finer("tooltip already configured"); //$NON-NLS-1$ + + // should this bad tooltip be showing and it's not? + checkForceShow(event); + } + else { + + logger.finer("tooltip not configured yet -- adding"); //$NON-NLS-1$ + addTooltipToElement(tooltipContent, parent); + + logger.finer("firing native event to jquery tooltip"); //$NON-NLS-1$ + + // kill this event -- we abused it to configure the tooltip + event.stopPropagation(); + event.preventDefault(); + + // and fire another event for jquery to handle + Node node = parent.getChild(0); + if (node instanceof Element) { + Element e = (Element) node; + + NativeEvent newEvent = Document.get().createMouseOverEvent(0, event.getScreenX(), + event.getScreenY(), event.getClientX(), event.getClientY(), event.getCtrlKey(), + event.getAltKey(), event.getShiftKey(), event.getMetaKey(), event.getButton(), e); + e.dispatchEvent(newEvent); + } + } + } + + public static void reapAllTooltips() { + // all tooltips should be reaped + ElementTooltip.reapAll(); + } + + public static void hideAllTooltips() { + // all tooltips should be hidden + ElementTooltip.hideAll(); + } + + public static ElementTooltip addTooltipToElement(SafeHtml tooltipContent, Element element) { + ElementTooltip tooltip = new ElementTooltip(element); + + tooltip.setContent(tooltipContent); + tooltip.reconfigure(); + + String cellId = element.getId(); + if (cellId == null || cellId.isEmpty()) { + cellId = DOM.createUniqueId(); + element.setId(cellId); + } + + // add tooltip to registry -- key by element-id, save the tooltip and the html of the element + ElementTooltipDetails details = new ElementTooltipDetails(); + details.setTooltip(tooltip); + details.setInnerHTML(element.getInnerHTML()); + ElementTooltip.getRegistry().put(cellId, details); + + return tooltip; + } + + public static boolean isTooltipConfigured(Element parent) { + return ElementTooltip.isTooltipConfigured(parent.getId()); + } + + /** + * mouseover and mouseout aren't perfect + * so give tooltip some time (50ms) to show, and then check to see if we should force show it + * TODO-GWT try using mouseenter and mouseleave, if GWT adds support for these. + */ + public static void checkForceShow(final NativeEvent event) { + Timer timer = new Timer() { + @Override + public void run() { + String[] pos = JqueryUtils.getMousePosition().split(","); //$NON-NLS-1$ + int x = Integer.parseInt(pos[0]); + int y = Integer.parseInt(pos[1]); + + logger.finer("checking for force show. any tooltip visible? " + JqueryUtils.anyTooltipVisible()); //$NON-NLS-1$ + if (!JqueryUtils.anyTooltipVisible()) { + logger.finer("force showing closed tooltip"); //$NON-NLS-1$ + JqueryUtils.fireMouseEnter(x, y); + } + } + }; + timer.schedule(50); + } + +} diff --git a/frontend/webadmin/modules/userportal-gwtp/src/main/java/org/ovirt/engine/ui/userportal/widget/extended/vm/TooltipCell.java b/frontend/webadmin/modules/userportal-gwtp/src/main/java/org/ovirt/engine/ui/userportal/widget/extended/vm/TooltipCell.java index 326d0ce..e8c1ef5 100644 --- a/frontend/webadmin/modules/userportal-gwtp/src/main/java/org/ovirt/engine/ui/userportal/widget/extended/vm/TooltipCell.java +++ b/frontend/webadmin/modules/userportal-gwtp/src/main/java/org/ovirt/engine/ui/userportal/widget/extended/vm/TooltipCell.java @@ -13,11 +13,13 @@ import com.google.gwt.user.client.DOM; /** + * TODO remove * Decorates a cell with a tooltip which is given from a tooltip provider * * @param <C> * the type that this Cell represents */ +@Deprecated public class TooltipCell<T> extends CompositeCell<T> { private final TooltipProvider<T> provider; diff --git a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/section/main/view/tab/MainTabClusterView.java b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/section/main/view/tab/MainTabClusterView.java index c1b0623..2886b07 100644 --- a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/section/main/view/tab/MainTabClusterView.java +++ b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/section/main/view/tab/MainTabClusterView.java @@ -24,7 +24,7 @@ import org.ovirt.engine.ui.webadmin.widget.action.WebAdminButtonDefinition; import org.ovirt.engine.ui.webadmin.widget.action.WebAdminImageButtonDefinition; import org.ovirt.engine.ui.webadmin.widget.action.WebAdminMenuBarButtonDefinition; -import org.ovirt.engine.ui.webadmin.widget.table.column.CommentColumn; +import org.ovirt.engine.ui.webadmin.widget.table.column.CommentColumn2; import com.google.gwt.core.client.GWT; import com.google.inject.Inject; @@ -57,8 +57,11 @@ nameColumn.makeSortable(ClusterConditionFieldAutoCompleter.NAME); getTable().addColumn(nameColumn, constants.nameCluster(), "150px"); //$NON-NLS-1$ - CommentColumn<VDSGroup> commentColumn = new CommentColumn<VDSGroup>(); - getTable().addColumnWithHtmlHeader(commentColumn, commentColumn.getHeaderHtml(), "30px"); //$NON-NLS-1$ + CommentColumn2<VDSGroup> commentColumn = new CommentColumn2<VDSGroup>(); + // TODO: add support for tooltips on headers + // TODO: don't hardcode "Comment" -- use image + // getTable().addColumnWithHtmlHeader(commentColumn, commentColumn.getHeaderHtml(), "30px"); //$NON-NLS-1$ + getTable().addColumn(commentColumn, "Comment", "50px"); //$NON-NLS-1$ //$NON-NLS-2$ if (ApplicationModeHelper.getUiMode() != ApplicationMode.GlusterOnly) { AbstractTextColumnWithTooltip<VDSGroup> dataCenterColumn = new AbstractTextColumnWithTooltip<VDSGroup>() { diff --git a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/widget/table/column/CommentColumn2.java b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/widget/table/column/CommentColumn2.java new file mode 100644 index 0000000..50eeec6 --- /dev/null +++ b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/widget/table/column/CommentColumn2.java @@ -0,0 +1,55 @@ +package org.ovirt.engine.ui.webadmin.widget.table.column; + +import org.ovirt.engine.core.common.businessentities.Commented; +import org.ovirt.engine.ui.common.CommonApplicationResources; +import org.ovirt.engine.ui.common.widget.table.cell.TooltipCell; +import org.ovirt.engine.ui.common.widget.table.column.AbstractColumn; +import org.ovirt.engine.ui.common.widget.table.column.ImageResourceCell; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; + +/** + * Column that render a comment image (yellow paper icon) that, when hovered, shows the + * actual comment in a tooltip. + * + * @param <T> row type + */ +public class CommentColumn2<T extends Commented> extends AbstractColumn<T, ImageResource> { + + private static final CommonApplicationResources RESOURCES = GWT.create(CommonApplicationResources.class); + + public CommentColumn2() { + super(new ImageResourceCell()); + } + + public CommentColumn2(TooltipCell<ImageResource> cell) { + super(cell); + } + + /** + * Using some row value of type T, build an ImageResource to render in this column. + * + * @see com.google.gwt.user.cellview.client.Column#getValue(java.lang.Object) + */ + @Override + public ImageResource getValue(T value) { + if (value.getComment() != null && !value.getComment().isEmpty()) { + return RESOURCES.commentImage(); + } + return null; + } + + /** + * Using some row value of type T, build a SafeHtml tooltip to render when this column is moused over. + * + * @see org.ovirt.engine.ui.common.widget.table.column.AbstractColumn#getTooltip(java.lang.Object) + */ + @Override + public SafeHtml getTooltip(T value) { + return SafeHtmlUtils.fromString(value.getComment()); + } + +} diff --git a/packaging/branding/ovirt.brand/mousetrack.js b/packaging/branding/ovirt.brand/mousetrack.js new file mode 100644 index 0000000..7a67cd3 --- /dev/null +++ b/packaging/branding/ovirt.brand/mousetrack.js @@ -0,0 +1,7 @@ +jQuery(function() { + jQuery(document).mousemove(function(e) { + window.mouseX = e.pageX; + window.mouseY = e.pageY; + }); +}); + diff --git a/packaging/branding/ovirt.brand/ovirt.css b/packaging/branding/ovirt.brand/ovirt.css index 49dc1fc..3383d5c 100644 --- a/packaging/branding/ovirt.brand/ovirt.css +++ b/packaging/branding/ovirt.brand/ovirt.css @@ -22,3 +22,28 @@ .labelDisabled { color: gray; } + +/*************************************** +Tooltips +TODO: use SASS, break into tooltips.sass +****************************************/ + +.tooltip-w220 .tooltip-inner { + max-width: 220px !important; +} + +.tooltip-w320 .tooltip-inner { + max-width: 320px !important; +} + +.tooltip-w420 .tooltip-inner { + max-width: 420px !important; +} + +.tooltip-w520 .tooltip-inner { + max-width: 520px !important; +} + +.tooltip-w620 .tooltip-inner { + max-width: 620px !important; +} -- To view, visit https://gerrit.ovirt.org/38555 To unsubscribe, visit https://gerrit.ovirt.org/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ief7f8524a3dd69c983ace95206379df462bc7daf Gerrit-PatchSet: 1 Gerrit-Project: ovirt-engine Gerrit-Branch: master Gerrit-Owner: Greg Sheremeta <[email protected]> _______________________________________________ Engine-patches mailing list [email protected] http://lists.ovirt.org/mailman/listinfo/engine-patches
