diff options
Diffstat (limited to 'client')
58 files changed, 4241 insertions, 1410 deletions
diff --git a/client/src/com/vaadin/DefaultWidgetSet.gwt.xml b/client/src/com/vaadin/DefaultWidgetSet.gwt.xml index 3aba1f6fee..2719493853 100755 --- a/client/src/com/vaadin/DefaultWidgetSet.gwt.xml +++ b/client/src/com/vaadin/DefaultWidgetSet.gwt.xml @@ -10,4 +10,8 @@ <entry-point class="com.vaadin.client.ApplicationConfiguration" /> + <!-- Since 7.2. Compile all permutations (browser support) into one Javascript + file. Speeds up compilation and does not make the Javascript significantly + larger. --> + <collapse-all-properties /> </module> diff --git a/client/src/com/vaadin/Vaadin.gwt.xml b/client/src/com/vaadin/Vaadin.gwt.xml index a1dca07a1c..d4eb454e86 100644 --- a/client/src/com/vaadin/Vaadin.gwt.xml +++ b/client/src/com/vaadin/Vaadin.gwt.xml @@ -59,6 +59,7 @@ <!-- Use the new cross site linker to get a nocache.js without document.write --> <add-linker name="xsiframe" /> + <extend-property name="user.agent" values="opera" /> <!-- Remove IE6/IE7 permutation as they are not supported --> <set-property name="user.agent" value="ie8,ie9,ie10,gecko1_8,safari,opera" /> diff --git a/client/src/com/vaadin/client/ApplicationConfiguration.java b/client/src/com/vaadin/client/ApplicationConfiguration.java index 7a70080c7e..1a5b0d836f 100644 --- a/client/src/com/vaadin/client/ApplicationConfiguration.java +++ b/client/src/com/vaadin/client/ApplicationConfiguration.java @@ -41,6 +41,7 @@ import com.vaadin.client.debug.internal.LogSection; import com.vaadin.client.debug.internal.NetworkSection; import com.vaadin.client.debug.internal.ProfilerSection; import com.vaadin.client.debug.internal.Section; +import com.vaadin.client.debug.internal.TestBenchSection; import com.vaadin.client.debug.internal.VDebugWindow; import com.vaadin.client.metadata.BundleLoadCallback; import com.vaadin.client.metadata.ConnectorBundleLoader; @@ -511,6 +512,30 @@ public class ApplicationConfiguration implements EntryPoint { } } + /** + * Returns all tags for given class. Tags are used in + * {@link ApplicationConfiguration} to keep track of different classes and + * their hierarchy + * + * @since 7.2 + * @param classname + * name of class which tags we want + * @return Integer array of tags pointing to this classname + */ + public Integer[] getTagsForServerSideClassName(String classname) { + List<Integer> tags = new ArrayList<Integer>(); + + for (Map.Entry<Integer, String> entry : tagToServerSideClassName + .entrySet()) { + if (classname.equals(entry.getValue())) { + tags.add(entry.getKey()); + } + } + + Integer[] out = new Integer[tags.size()]; + return tags.toArray(out); + } + public Integer getParentTag(int tag) { return componentInheritanceMap.get(tag); } @@ -595,6 +620,7 @@ public class ApplicationConfiguration implements EntryPoint { window.addSection((Section) GWT.create(InfoSection.class)); window.addSection((Section) GWT.create(HierarchySection.class)); window.addSection((Section) GWT.create(NetworkSection.class)); + window.addSection((Section) GWT.create(TestBenchSection.class)); if (Profiler.isEnabled()) { window.addSection((Section) GWT.create(ProfilerSection.class)); } @@ -760,5 +786,4 @@ public class ApplicationConfiguration implements EntryPoint { private static final Logger getLogger() { return Logger.getLogger(ApplicationConfiguration.class.getName()); } - } diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java index da41543894..b84d8a376f 100644 --- a/client/src/com/vaadin/client/ApplicationConnection.java +++ b/client/src/com/vaadin/client/ApplicationConnection.java @@ -1,4 +1,4 @@ -/* +/* * Copyright 2000-2013 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not @@ -65,16 +65,17 @@ import com.google.gwt.user.client.Window.ClosingHandler; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConfiguration.ErrorMessage; -import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent; import com.vaadin.client.ResourceLoader.ResourceLoadEvent; import com.vaadin.client.ResourceLoader.ResourceLoadListener; import com.vaadin.client.communication.HasJavaScriptConnectorHelper; +import com.vaadin.client.communication.Heartbeat; import com.vaadin.client.communication.JavaScriptMethodInvocation; import com.vaadin.client.communication.JsonDecoder; import com.vaadin.client.communication.JsonEncoder; import com.vaadin.client.communication.PushConnection; import com.vaadin.client.communication.RpcManager; import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.componentlocator.ComponentLocator; import com.vaadin.client.extensions.AbstractExtensionConnector; import com.vaadin.client.metadata.ConnectorBundleLoader; import com.vaadin.client.metadata.Method; @@ -363,7 +364,7 @@ public class ApplicationConnection { * * To listen for the event add a {@link ApplicationStoppedHandler} by * invoking - * {@link ApplicationConnection#addHandler(ApplicationStoppedEvent.Type, ApplicationStoppedHandler)} + * {@link ApplicationConnection#addHandler(ApplicationConnection.ApplicationStoppedEvent.Type, ApplicationStoppedHandler)} * to the {@link ApplicationConnection} * * @since 7.1.8 @@ -428,6 +429,8 @@ public class ApplicationConnection { private VLoadingIndicator loadingIndicator; + private Heartbeat heartbeat = GWT.create(Heartbeat.class); + public static class MultiStepDuration extends Duration { private int previousStep = elapsedMillis(); @@ -490,7 +493,7 @@ public class ApplicationConnection { getLoadingIndicator().show(); - scheduleHeartbeat(); + heartbeat.init(this); Window.addWindowClosingHandler(new ClosingHandler() { @Override @@ -547,38 +550,47 @@ public class ApplicationConnection { private native void initializeTestbenchHooks( ComponentLocator componentLocator, String TTAppId) /*-{ - var ap = this; - var client = {}; - client.isActive = $entry(function() { - return ap.@com.vaadin.client.ApplicationConnection::hasActiveRequest()() - || ap.@com.vaadin.client.ApplicationConnection::isExecutingDeferredCommands()(); - }); - var vi = ap.@com.vaadin.client.ApplicationConnection::getVersionInfo()(); - if (vi) { - client.getVersionInfo = function() { - return vi; - } - } + var ap = this; + var client = {}; + client.isActive = $entry(function() { + return ap.@com.vaadin.client.ApplicationConnection::hasActiveRequest()() + || ap.@com.vaadin.client.ApplicationConnection::isExecutingDeferredCommands()(); + }); + var vi = ap.@com.vaadin.client.ApplicationConnection::getVersionInfo()(); + if (vi) { + client.getVersionInfo = function() { + return vi; + } + } - client.getProfilingData = $entry(function() { - var pd = [ - ap.@com.vaadin.client.ApplicationConnection::lastProcessingTime, + client.getProfilingData = $entry(function() { + var pd = [ + ap.@com.vaadin.client.ApplicationConnection::lastProcessingTime, ap.@com.vaadin.client.ApplicationConnection::totalProcessingTime - ]; - pd = pd.concat(ap.@com.vaadin.client.ApplicationConnection::serverTimingInfo); - pd[pd.length] = ap.@com.vaadin.client.ApplicationConnection::bootstrapTime; - return pd; - }); + ]; + pd = pd.concat(ap.@com.vaadin.client.ApplicationConnection::serverTimingInfo); + pd[pd.length] = ap.@com.vaadin.client.ApplicationConnection::bootstrapTime; + return pd; + }); - client.getElementByPath = $entry(function(id) { - return componentLocator.@com.vaadin.client.ComponentLocator::getElementByPath(Ljava/lang/String;)(id); - }); - client.getPathForElement = $entry(function(element) { - return componentLocator.@com.vaadin.client.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element); - }); - client.initializing = false; + client.getElementByPath = $entry(function(id) { + return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementByPath(Ljava/lang/String;)(id); + }); + client.getElementByPathStartingAt = $entry(function(id, element) { + return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementByPathStartingAt(Ljava/lang/String;Lcom/google/gwt/user/client/Element;)(id, element); + }); + client.getElementsByPath = $entry(function(id) { + return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementsByPath(Ljava/lang/String;)(id); + }); + client.getElementsByPathStartingAt = $entry(function(id, element) { + return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementsByPathStartingAt(Ljava/lang/String;Lcom/google/gwt/user/client/Element;)(id, element); + }); + client.getPathForElement = $entry(function(element) { + return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element); + }); + client.initializing = false; - $wnd.vaadin.clients[TTAppId] = client; + $wnd.vaadin.clients[TTAppId] = client; }-*/; private static native final int calculateBootstrapTime() @@ -1460,6 +1472,9 @@ public class ApplicationConnection { if (meta.containsKey("timedRedirect")) { final ValueMap timedRedirect = meta .getValueMap("timedRedirect"); + if (redirectTimer != null) { + redirectTimer.cancel(); + } redirectTimer = new Timer() { @Override public void run() { @@ -3311,20 +3326,11 @@ public class ApplicationConnection { * interval elapses if the interval is a positive number. Otherwise, does * nothing. * - * @see #sendHeartbeat() - * @see ApplicationConfiguration#getHeartbeatInterval() + * @deprecated as of 7.2, use {@link Heartbeat#schedule()} instead */ + @Deprecated protected void scheduleHeartbeat() { - final int interval = getConfiguration().getHeartbeatInterval(); - if (interval > 0) { - VConsole.log("Scheduling heartbeat in " + interval + " seconds"); - new Timer() { - @Override - public void run() { - sendHeartbeat(); - } - }.schedule(interval * 1000); - } + heartbeat.schedule(); } /** @@ -3333,51 +3339,12 @@ public class ApplicationConnection { * Heartbeat requests are used to inform the server that the client-side is * still alive. If the client page is closed or the connection lost, the * server will eventually close the inactive UI. - * <p> - * <b>TODO</b>: Improved error handling, like in doUidlRequest(). * - * @see #scheduleHeartbeat() + * @deprecated as of 7.2, use {@link Heartbeat#send()} instead */ + @Deprecated protected void sendHeartbeat() { - final String uri = addGetParameters( - translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX - + ApplicationConstants.HEARTBEAT_PATH + '/'), - UIConstants.UI_ID_PARAMETER + "=" - + getConfiguration().getUIId()); - - final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri); - - final RequestCallback callback = new RequestCallback() { - - @Override - public void onResponseReceived(Request request, Response response) { - int status = response.getStatusCode(); - if (status == Response.SC_OK) { - // TODO Permit retry in some error situations - VConsole.log("Heartbeat response OK"); - scheduleHeartbeat(); - } else if (status == Response.SC_GONE) { - showSessionExpiredError(null); - } else { - VConsole.error("Failed sending heartbeat to server. Error code: " - + status); - } - } - - @Override - public void onError(Request request, Throwable exception) { - VConsole.error("Exception sending heartbeat: " + exception); - } - }; - - rb.setCallback(callback); - - try { - VConsole.log("Sending heartbeat request..."); - rb.send(); - } catch (RequestException re) { - callback.onError(null, re); - } + heartbeat.send(); } /** diff --git a/client/src/com/vaadin/client/ComponentLocator.java b/client/src/com/vaadin/client/ComponentLocator.java index af934470c2..ef7ccc3b65 100644 --- a/client/src/com/vaadin/client/ComponentLocator.java +++ b/client/src/com/vaadin/client/ComponentLocator.java @@ -15,706 +15,20 @@ */ package com.vaadin.client; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import com.google.gwt.core.client.JavaScriptObject; -import com.google.gwt.user.client.DOM; -import com.google.gwt.user.client.Element; -import com.google.gwt.user.client.ui.HasWidgets; -import com.google.gwt.user.client.ui.RootPanel; -import com.google.gwt.user.client.ui.Widget; -import com.vaadin.client.ui.SubPartAware; -import com.vaadin.client.ui.VCssLayout; -import com.vaadin.client.ui.VGridLayout; -import com.vaadin.client.ui.VOverlay; -import com.vaadin.client.ui.VTabsheetPanel; -import com.vaadin.client.ui.VUI; -import com.vaadin.client.ui.VWindow; -import com.vaadin.client.ui.orderedlayout.Slot; -import com.vaadin.client.ui.orderedlayout.VAbstractOrderedLayout; -import com.vaadin.client.ui.window.WindowConnector; -import com.vaadin.shared.AbstractComponentState; -import com.vaadin.shared.Connector; -import com.vaadin.shared.communication.SharedState; - /** * ComponentLocator provides methods for generating a String locator for a given * DOM element and for locating a DOM element using a String locator. + * + * @since 5.4 + * @deprecated Moved to com.vaadin.client.componentlocator.ComponentLocator */ -public class ComponentLocator { - - /** - * Separator used in the String locator between a parent and a child widget. - */ - private static final String PARENTCHILD_SEPARATOR = "/"; - - /** - * Separator used in the String locator between the part identifying the - * containing widget and the part identifying the target element within the - * widget. - */ - private static final String SUBPART_SEPARATOR = "#"; - - /** - * String that identifies the root panel when appearing first in the String - * locator. - */ - private static final String ROOT_ID = "Root"; - - /** - * Reference to ApplicationConnection instance. - */ - private ApplicationConnection client; - +public class ComponentLocator extends com.vaadin.client.componentlocator.ComponentLocator { /** * Construct a ComponentLocator for the given ApplicationConnection. - * - * @param client - * ApplicationConnection instance for the application. + * + * @param client ApplicationConnection instance for the application. */ public ComponentLocator(ApplicationConnection client) { - this.client = client; - } - - /** - * Generates a String locator which uniquely identifies the target element. - * The {@link #getElementByPath(String)} method can be used for the inverse - * operation, i.e. locating an element based on the return value from this - * method. - * <p> - * Note that getElementByPath(getPathForElement(element)) == element is not - * always true as {@link #getPathForElement(Element)} can return a path to - * another element if the widget determines an action on the other element - * will give the same result as the action on the target element. - * </p> - * - * @since 5.4 - * @param targetElement - * The element to generate a path for. - * @return A String locator that identifies the target element or null if a - * String locator could not be created. - */ - public String getPathForElement(Element targetElement) { - String pid = null; - - targetElement = getElement(targetElement); - - Element e = targetElement; - - while (true) { - pid = ConnectorMap.get(client).getConnectorId(e); - if (pid != null) { - break; - } - - e = DOM.getParent(e); - if (e == null) { - break; - } - } - - Widget w = null; - if (pid != null) { - // If we found a Paintable then we use that as reference. We should - // find the Paintable for all but very special cases (like - // overlays). - w = ((ComponentConnector) ConnectorMap.get(client) - .getConnector(pid)).getWidget(); - - /* - * Still if the Paintable contains a widget that implements - * SubPartAware, we want to use that as a reference - */ - Widget targetParent = findParentWidget(targetElement, w); - while (targetParent != w && targetParent != null) { - if (targetParent instanceof SubPartAware) { - /* - * The targetParent widget is a child of the Paintable and - * the first parent (of the targetElement) that implements - * SubPartAware - */ - w = targetParent; - break; - } - targetParent = targetParent.getParent(); - } - } - if (w == null) { - // Check if the element is part of a widget that is attached - // directly to the root panel - RootPanel rootPanel = RootPanel.get(); - int rootWidgetCount = rootPanel.getWidgetCount(); - for (int i = 0; i < rootWidgetCount; i++) { - Widget rootWidget = rootPanel.getWidget(i); - if (rootWidget.getElement().isOrHasChild(targetElement)) { - // The target element is contained by this root widget - w = findParentWidget(targetElement, rootWidget); - break; - } - } - if (w != null) { - // We found a widget but we should still see if we find a - // SubPartAware implementor (we cannot find the Paintable as - // there is no link from VOverlay to its paintable/owner). - Widget subPartAwareWidget = findSubPartAwareParentWidget(w); - if (subPartAwareWidget != null) { - w = subPartAwareWidget; - } - } - } - - if (w == null) { - // Containing widget not found - return null; - } - - // Determine the path for the target widget - String path = getPathForWidget(w); - if (path == null) { - /* - * No path could be determined for the target widget. Cannot create - * a locator string. - */ - return null; - } - - // The parent check is a work around for Firefox 15 which fails to - // compare elements properly (#9534) - if (w.getElement() == targetElement) { - /* - * We are done if the target element is the root of the target - * widget. - */ - return path; - } else if (w instanceof SubPartAware) { - /* - * If the widget can provide an identifier for the targetElement we - * let it do that - */ - String elementLocator = ((SubPartAware) w) - .getSubPartName(targetElement); - if (elementLocator != null) { - return path + SUBPART_SEPARATOR + elementLocator; - } - } - /* - * If everything else fails we use the DOM path to identify the target - * element - */ - String domPath = getDOMPathForElement(targetElement, w.getElement()); - if (domPath == null) { - return path; - } else { - return path + domPath; - } - } - - /** - * Returns the element passed to the method. Or in case of Firefox 15, - * returns the real element that is in the DOM instead of the element passed - * to the method (which is the same element but not ==). - * - * @param targetElement - * the element to return - * @return the element passed to the method - */ - private Element getElement(Element targetElement) { - if (targetElement == null) { - return null; - } - - if (!BrowserInfo.get().isFirefox()) { - return targetElement; - } - - if (BrowserInfo.get().getBrowserMajorVersion() != 15) { - return targetElement; - } - - // Firefox 15, you make me sad - if (targetElement.getNextSibling() != null) { - return (Element) targetElement.getNextSibling() - .getPreviousSibling(); - } - if (targetElement.getPreviousSibling() != null) { - return (Element) targetElement.getPreviousSibling() - .getNextSibling(); - } - // No siblings so this is the only child - return (Element) targetElement.getParentNode().getChild(0); - } - - /** - * Finds the first widget in the hierarchy (moving upwards) that implements - * SubPartAware. Returns the SubPartAware implementor or null if none is - * found. - * - * @param w - * The widget to start from. This is returned if it implements - * SubPartAware. - * @return The first widget (upwards in hierarchy) that implements - * SubPartAware or null - */ - private Widget findSubPartAwareParentWidget(Widget w) { - - while (w != null) { - if (w instanceof SubPartAware) { - return w; - } - w = w.getParent(); - } - return null; - } - - /** - * Returns the first widget found when going from {@code targetElement} - * upwards in the DOM hierarchy, assuming that {@code ancestorWidget} is a - * parent of {@code targetElement}. - * - * @param targetElement - * @param ancestorWidget - * @return The widget whose root element is a parent of - * {@code targetElement}. - */ - private Widget findParentWidget(Element targetElement, Widget ancestorWidget) { - /* - * As we cannot resolve Widgets from the element we start from the - * widget and move downwards to the correct child widget, as long as we - * find one. - */ - if (ancestorWidget instanceof HasWidgets) { - for (Widget w : ((HasWidgets) ancestorWidget)) { - if (w.getElement().isOrHasChild(targetElement)) { - return findParentWidget(targetElement, w); - } - } - } - - // No children found, this is it - return ancestorWidget; - } - - /** - * Locates an element based on a DOM path and a base element. - * - * @param baseElement - * The base element which the path is relative to - * @param path - * String locator (consisting of domChild[x] parts) that - * identifies the element - * @return The element identified by path, relative to baseElement or null - * if the element could not be found. - */ - private Element getElementByDOMPath(Element baseElement, String path) { - String parts[] = path.split(PARENTCHILD_SEPARATOR); - Element element = baseElement; - - for (String part : parts) { - if (part.startsWith("domChild[")) { - String childIndexString = part.substring("domChild[".length(), - part.length() - 1); - - if (Util.findWidget(baseElement, null) instanceof VAbstractOrderedLayout) { - if (element.hasChildNodes()) { - Element e = element.getFirstChildElement().cast(); - String cn = e.getClassName(); - if (cn != null - && (cn.equals("v-expand") || cn - .contains("v-has-caption"))) { - element = e; - } - } - } - - try { - int childIndex = Integer.parseInt(childIndexString); - element = DOM.getChild(element, childIndex); - } catch (Exception e) { - return null; - } - - if (element == null) { - return null; - } - - } - } - - return element; + super(client); } - - /** - * Generates a String locator using domChild[x] parts for the element - * relative to the baseElement. - * - * @param element - * The target element - * @param baseElement - * The starting point for the locator. The generated path is - * relative to this element. - * @return A String locator that can be used to locate the target element - * using {@link #getElementByDOMPath(Element, String)} or null if - * the locator String cannot be created. - */ - private String getDOMPathForElement(Element element, Element baseElement) { - Element e = element; - String path = ""; - while (true) { - int childIndex = -1; - Element siblingIterator = e; - while (siblingIterator != null) { - childIndex++; - siblingIterator = siblingIterator.getPreviousSiblingElement() - .cast(); - } - - path = PARENTCHILD_SEPARATOR + "domChild[" + childIndex + "]" - + path; - - JavaScriptObject parent = e.getParentElement(); - if (parent == null) { - return null; - } - // The parent check is a work around for Firefox 15 which fails to - // compare elements properly (#9534) - if (parent == baseElement) { - break; - } - - e = parent.cast(); - } - - return path; - } - - /** - * Locates an element using a String locator (path) which identifies a DOM - * element. The {@link #getPathForElement(Element)} method can be used for - * the inverse operation, i.e. generating a string expression for a DOM - * element. - * - * @since 5.4 - * @param path - * The String locater which identifies the target element. - * @return The DOM element identified by {@code path} or null if the element - * could not be located. - */ - public Element getElementByPath(String path) { - /* - * Path is of type "targetWidgetPath#componentPart" or - * "targetWidgetPath". - */ - String parts[] = path.split(SUBPART_SEPARATOR, 2); - String widgetPath = parts[0]; - Widget w = getWidgetFromPath(widgetPath); - if (w == null || !Util.isAttachedAndDisplayed(w)) { - return null; - } - - if (parts.length == 1) { - int pos = widgetPath.indexOf("domChild"); - if (pos == -1) { - return w.getElement(); - } - - // Contains dom reference to a sub element of the widget - String subPath = widgetPath.substring(pos); - return getElementByDOMPath(w.getElement(), subPath); - } else if (parts.length == 2) { - if (w instanceof SubPartAware) { - return ((SubPartAware) w).getSubPartElement(parts[1]); - } - } - - return null; - } - - /** - * Creates a locator String for the given widget. The path can be used to - * locate the widget using {@link #getWidgetFromPath(String)}. - * - * Returns null if no path can be determined for the widget or if the widget - * is null. - * - * @param w - * The target widget - * @return A String locator for the widget - */ - private String getPathForWidget(Widget w) { - if (w == null) { - return null; - } - String elementId = w.getElement().getId(); - if (elementId != null && !elementId.isEmpty() - && !elementId.startsWith("gwt-uid-")) { - // Use PID_S+id if the user has set an id but do not use it for auto - // generated id:s as these might not be consistent - return "PID_S" + elementId; - } else if (w instanceof VUI) { - return ""; - } else if (w instanceof VWindow) { - Connector windowConnector = ConnectorMap.get(client) - .getConnector(w); - List<WindowConnector> subWindowList = client.getUIConnector() - .getSubWindows(); - int indexOfSubWindow = subWindowList.indexOf(windowConnector); - return PARENTCHILD_SEPARATOR + "VWindow[" + indexOfSubWindow + "]"; - } else if (w instanceof RootPanel) { - return ROOT_ID; - } - - Widget parent = w.getParent(); - - String basePath = getPathForWidget(parent); - if (basePath == null) { - return null; - } - String simpleName = Util.getSimpleName(w); - - /* - * Check if the parent implements Iterable. At least VPopupView does not - * implement HasWdgets so we cannot check for that. - */ - if (!(parent instanceof Iterable<?>)) { - // Parent does not implement Iterable so we cannot find out which - // child this is - return null; - } - - Iterator<?> i = ((Iterable<?>) parent).iterator(); - int pos = 0; - while (i.hasNext()) { - Object child = i.next(); - if (child == w) { - return basePath + PARENTCHILD_SEPARATOR + simpleName + "[" - + pos + "]"; - } - String simpleName2 = Util.getSimpleName(child); - if (simpleName.equals(simpleName2)) { - pos++; - } - } - - return null; - } - - /** - * Locates the widget based on a String locator. - * - * @param path - * The String locator that identifies the widget. - * @return The Widget identified by the String locator or null if the widget - * could not be identified. - */ - private Widget getWidgetFromPath(String path) { - Widget w = null; - String parts[] = path.split(PARENTCHILD_SEPARATOR); - - for (int i = 0; i < parts.length; i++) { - String part = parts[i]; - - if (part.equals(ROOT_ID)) { - w = RootPanel.get(); - } else if (part.equals("")) { - w = client.getUIConnector().getWidget(); - } else if (w == null) { - String id = part; - // Must be old static pid (PID_S*) - ServerConnector connector = ConnectorMap.get(client) - .getConnector(id); - if (connector == null) { - // Lookup by component id - // TODO Optimize this - connector = findConnectorById(client.getUIConnector(), - id.substring(5)); - } - - if (connector instanceof ComponentConnector) { - w = ((ComponentConnector) connector).getWidget(); - } else { - // Not found - return null; - } - } else if (part.startsWith("domChild[")) { - // The target widget has been found and the rest identifies the - // element - break; - } else if (w instanceof Iterable) { - // W identifies a widget that contains other widgets, as it - // should. Try to locate the child - Iterable<?> parent = (Iterable<?>) w; - - // Part is of type "VVerticalLayout[0]", split this into - // VVerticalLayout and 0 - String[] split = part.split("\\[", 2); - String widgetClassName = split[0]; - String indexString = split[1].substring(0, - split[1].length() - 1); - int widgetPosition = Integer.parseInt(indexString); - - // AbsolutePanel in GridLayout has been removed -> skip it - if (w instanceof VGridLayout - && "AbsolutePanel".equals(widgetClassName)) { - continue; - } - - // FlowPane in CSSLayout has been removed -> skip it - if (w instanceof VCssLayout - && "VCssLayout$FlowPane".equals(widgetClassName)) { - continue; - } - - // ChildComponentContainer and VOrderedLayout$Slot have been - // replaced with Slot - if (w instanceof VAbstractOrderedLayout - && ("ChildComponentContainer".equals(widgetClassName) || "VOrderedLayout$Slot" - .equals(widgetClassName))) { - widgetClassName = "Slot"; - } - - if (w instanceof VTabsheetPanel && widgetPosition != 0) { - // TabSheetPanel now only contains 1 connector => the index - // is always 0 which indicates the widget in the active tab - widgetPosition = 0; - } - if (w instanceof VOverlay - && "VCalendarPanel".equals(widgetClassName)) { - // Vaadin 7.1 adds a wrapper for datefield popups - parent = (Iterable<?>) ((Iterable) parent).iterator() - .next(); - } - /* - * The new grid and ordered layotus do not contain - * ChildComponentContainer widgets. This is instead simulated by - * constructing a path step that would find the desired widget - * from the layout and injecting it as the next search step - * (which would originally have found the widget inside the - * ChildComponentContainer) - */ - if ((w instanceof VGridLayout) - && "ChildComponentContainer".equals(widgetClassName) - && i + 1 < parts.length) { - - HasWidgets layout = (HasWidgets) w; - - String nextPart = parts[i + 1]; - String[] nextSplit = nextPart.split("\\[", 2); - String nextWidgetClassName = nextSplit[0]; - - // Find the n:th child and count the number of children with - // the same type before it - int nextIndex = 0; - for (Widget child : layout) { - boolean matchingType = nextWidgetClassName.equals(Util - .getSimpleName(child)); - if (matchingType && widgetPosition == 0) { - // This is the n:th child that we looked for - break; - } else if (widgetPosition < 0) { - // Error if we're past the desired position without - // a match - return null; - } else if (matchingType) { - // If this was another child of the expected type, - // increase the count for the next step - nextIndex++; - } - - // Don't count captions - if (!(child instanceof VCaption)) { - widgetPosition--; - } - } - - // Advance to the next step, this time checking for the - // actual child widget - parts[i + 1] = nextWidgetClassName + '[' + nextIndex + ']'; - continue; - } - - // Locate the child - Iterator<? extends Widget> iterator; - - /* - * VWindow and VContextMenu workarounds for backwards - * compatibility - */ - if (widgetClassName.equals("VWindow")) { - List<WindowConnector> windows = client.getUIConnector() - .getSubWindows(); - List<VWindow> windowWidgets = new ArrayList<VWindow>( - windows.size()); - for (WindowConnector wc : windows) { - windowWidgets.add(wc.getWidget()); - } - iterator = windowWidgets.iterator(); - } else if (widgetClassName.equals("VContextMenu")) { - return client.getContextMenu(); - } else { - iterator = (Iterator<? extends Widget>) parent.iterator(); - } - - boolean ok = false; - - // Find the widgetPosition:th child of type "widgetClassName" - while (iterator.hasNext()) { - - Widget child = iterator.next(); - String simpleName2 = Util.getSimpleName(child); - - if (!widgetClassName.equals(simpleName2) - && child instanceof Slot) { - /* - * Support legacy tests without any selector for the - * Slot widget (i.e. /VVerticalLayout[0]/VButton[0]) by - * directly checking the stuff inside the slot - */ - child = ((Slot) child).getWidget(); - simpleName2 = Util.getSimpleName(child); - } - - if (widgetClassName.equals(simpleName2)) { - if (widgetPosition == 0) { - w = child; - ok = true; - break; - } - widgetPosition--; - - } - } - - if (!ok) { - // Did not find the child - return null; - } - } else { - // W identifies something that is not a "HasWidgets". This - // should not happen as all widget containers should implement - // HasWidgets. - return null; - } - } - - return w; - } - - private ServerConnector findConnectorById(ServerConnector root, String id) { - SharedState state = root.getState(); - if (state instanceof AbstractComponentState - && id.equals(((AbstractComponentState) state).id)) { - return root; - } - for (ServerConnector child : root.getChildren()) { - ServerConnector found = findConnectorById(child, id); - if (found != null) { - return found; - } - } - - return null; - } - } diff --git a/client/src/com/vaadin/client/ConnectorMap.java b/client/src/com/vaadin/client/ConnectorMap.java index 810f12824a..c2f1eda21d 100644 --- a/client/src/com/vaadin/client/ConnectorMap.java +++ b/client/src/com/vaadin/client/ConnectorMap.java @@ -116,7 +116,7 @@ public class ConnectorMap { * no connector was found */ public ComponentConnector getConnector(Widget widget) { - return getConnector(widget.getElement()); + return widget == null ? null : getConnector(widget.getElement()); } public void registerConnector(String id, ServerConnector connector) { diff --git a/client/src/com/vaadin/client/Profiler.java b/client/src/com/vaadin/client/Profiler.java index 083f2559b1..cfce59b08b 100644 --- a/client/src/com/vaadin/client/Profiler.java +++ b/client/src/com/vaadin/client/Profiler.java @@ -297,10 +297,6 @@ public class Profiler { if (isEnabled()) { double now = Duration.currentTimeMillis(); - StringBuilder stringBuilder = new StringBuilder( - "Time since window.performance.timing events"); - SimpleTree tree = new SimpleTree(stringBuilder.toString()); - String[] keys = new String[] { "navigationStart", "unloadEventStart", "unloadEventEnd", "redirectStart", "redirectEnd", "fetchStart", "domainLookupStart", diff --git a/client/src/com/vaadin/client/SimpleTree.java b/client/src/com/vaadin/client/SimpleTree.java index 7370496cb8..edfa23fb13 100644 --- a/client/src/com/vaadin/client/SimpleTree.java +++ b/client/src/com/vaadin/client/SimpleTree.java @@ -116,6 +116,14 @@ public class SimpleTree extends ComplexPanel implements HasDoubleClickHandlers { } } + public boolean isOpen() { + return "-".equals(handle.getInnerHTML()); + } + + public String getCaption() { + return text.getInnerText(); + } + public SimpleTree(String caption) { this(); setText(caption); diff --git a/client/src/com/vaadin/client/Util.java b/client/src/com/vaadin/client/Util.java index 8972670232..edbb40e86c 100644 --- a/client/src/com/vaadin/client/Util.java +++ b/client/src/com/vaadin/client/Util.java @@ -855,6 +855,7 @@ public class Util { * @param class1 * the Widget type to seek for */ + @SuppressWarnings("unchecked") public static <T> T findWidget(Element element, Class<? extends Widget> class1) { if (element != null) { @@ -866,7 +867,7 @@ public class Util { element = (Element) element.getParentElement(); } } - if (eventListener != null) { + if (eventListener instanceof Widget) { /* * Then find the first widget of type class1 from widget * hierarchy diff --git a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java index c9b9e46cd5..9eb2b881bb 100644 --- a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java +++ b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java @@ -344,6 +344,14 @@ public class AtmospherePushConnection implements PushConnection { state = State.CONNECT_PENDING; } + protected void onClientTimeout(AtmosphereResponse response) { + state = State.DISCONNECTED; + errorHandler + .onError( + "Client unexpectedly disconnected. Ensure client timeout is disabled.", + -1); + } + protected void onReconnect(JavaScriptObject request, final AtmosphereResponse response) { if (state == State.CONNECTED) { @@ -438,6 +446,7 @@ public class AtmospherePushConnection implements PushConnection { fallbackTransport: 'streaming', contentType: 'application/json; charset=UTF-8', reconnectInterval: 5000, + timeout: -1, maxReconnectOnClose: 10000000, trackMessageLength: true, enableProtocol: false, @@ -472,6 +481,9 @@ public class AtmospherePushConnection implements PushConnection { config.onReconnect = $entry(function(request, response) { self.@com.vaadin.client.communication.AtmospherePushConnection::onReconnect(*)(request, response); }); + config.onClientTimeout = $entry(function(request) { + self.@com.vaadin.client.communication.AtmospherePushConnection::onClientTimeout(*)(request); + }); return $wnd.jQueryVaadin.atmosphere.subscribe(config); }-*/; diff --git a/client/src/com/vaadin/client/communication/Date_Serializer.java b/client/src/com/vaadin/client/communication/Date_Serializer.java new file mode 100644 index 0000000000..c6eb7af188 --- /dev/null +++ b/client/src/com/vaadin/client/communication/Date_Serializer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.Date; + +import com.google.gwt.json.client.JSONNumber; +import com.google.gwt.json.client.JSONValue; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.metadata.Type; + +/** + * Client side serializer/deserializer for java.util.Date + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class Date_Serializer implements JSONSerializer<Date> { + + @Override + public Date deserialize(Type type, JSONValue jsonValue, + ApplicationConnection connection) { + return new Date((long) ((JSONNumber) jsonValue).doubleValue()); + } + + @Override + public JSONValue serialize(Date value, ApplicationConnection connection) { + return new JSONNumber(value.getTime()); + } + +} diff --git a/client/src/com/vaadin/client/communication/Heartbeat.java b/client/src/com/vaadin/client/communication/Heartbeat.java new file mode 100644 index 0000000000..4b80827127 --- /dev/null +++ b/client/src/com/vaadin/client/communication/Heartbeat.java @@ -0,0 +1,171 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.logging.Logger; + +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.RequestBuilder; +import com.google.gwt.http.client.RequestCallback; +import com.google.gwt.http.client.RequestException; +import com.google.gwt.http.client.Response; +import com.google.gwt.user.client.Timer; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.ui.ui.UIConstants; + +/** + * Handles sending of heartbeats to the server and reacting to the response + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class Heartbeat { + + private int interval = -1; + private Timer timer = new Timer() { + @Override + public void run() { + send(); + } + }; + + private ApplicationConnection connection; + + private static Logger getLogger() { + return Logger.getLogger(Heartbeat.class.getName()); + } + + /** + * Initializes the heartbeat for the given application connection + * + * @param connection + * the connection + */ + public void init(ApplicationConnection connection) { + this.connection = connection; + interval = connection.getConfiguration().getHeartbeatInterval(); + setInterval(interval); + schedule(); + + connection.addHandler( + ApplicationConnection.ApplicationStoppedEvent.TYPE, + new ApplicationConnection.ApplicationStoppedHandler() { + + @Override + public void onApplicationStopped( + ApplicationStoppedEvent event) { + setInterval(-1); + schedule(); + } + }); + + } + + /** + * Sends a heartbeat to the server + */ + public void send() { + final String uri = ApplicationConnection.addGetParameters( + getConnection().translateVaadinUri( + ApplicationConstants.APP_PROTOCOL_PREFIX + + ApplicationConstants.HEARTBEAT_PATH + '/'), + UIConstants.UI_ID_PARAMETER + "=" + + getConnection().getConfiguration().getUIId()); + + final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri); + + final RequestCallback callback = new RequestCallback() { + + @Override + public void onResponseReceived(Request request, Response response) { + int status = response.getStatusCode(); + if (status == Response.SC_OK) { + // TODO Permit retry in some error situations + getLogger().fine("Heartbeat response OK"); + schedule(); + } else if (status == Response.SC_GONE) { + // FIXME This should really do something else like send an + // event + getConnection().showSessionExpiredError(null); + } else { + getLogger().warning( + "Failed sending heartbeat to server. Error code: " + + status); + } + } + + @Override + public void onError(Request request, Throwable exception) { + getLogger().severe("Exception sending heartbeat: " + exception); + } + }; + + rb.setCallback(callback); + + try { + getLogger().fine("Sending heartbeat request..."); + rb.send(); + } catch (RequestException re) { + callback.onError(null, re); + } + + } + + /** + * @return the interval at which heartbeat requests are sent + */ + public int getInterval() { + return interval; + } + + /** + * sets the interval at which heartbeat requests are sent + * + * @param interval + * the new interval + */ + public void setInterval(int interval) { + this.interval = interval; + } + + /** + * Updates the schedule of the heartbeat to match the set interval. A + * negative interval disables the heartbeat. + */ + public void schedule() { + if (getInterval() > 0) { + getLogger() + .fine("Scheduling heartbeat in " + interval + " seconds"); + timer.schedule(interval * 1000); + } else { + if (timer != null) { + getLogger().fine("Disabling heartbeat"); + timer.cancel(); + } + } + + } + + /** + * @return the application connection + */ + protected ApplicationConnection getConnection() { + return connection; + } + +} diff --git a/client/src/com/vaadin/client/communication/JSONSerializer.java b/client/src/com/vaadin/client/communication/JSONSerializer.java index e5829ece24..a4e78e503c 100644 --- a/client/src/com/vaadin/client/communication/JSONSerializer.java +++ b/client/src/com/vaadin/client/communication/JSONSerializer.java @@ -23,14 +23,17 @@ import com.vaadin.client.metadata.Type; /** * Implementors of this interface knows how to serialize an Object of a given * type to JSON and how to deserialize the JSON back into an object. - * + * <p> * The {@link #serialize(Object, ApplicationConnection)} and * {@link #deserialize(Type, JSONValue, ApplicationConnection)} methods must be * symmetric so they can be chained and produce the original result (or an equal * result). - * + * <p> * Each {@link JSONSerializer} implementation can handle an object of a single * type - see {@link Type#findSerializer()}. + * <p> + * This is the client side interface, see + * com.vaadin.server.communication.JSONSerializer for the server side interface. * * @since 7.0 */ diff --git a/client/src/com/vaadin/client/componentlocator/ComponentLocator.java b/client/src/com/vaadin/client/componentlocator/ComponentLocator.java new file mode 100644 index 0000000000..d2a89c00d5 --- /dev/null +++ b/client/src/com/vaadin/client/componentlocator/ComponentLocator.java @@ -0,0 +1,220 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.componentlocator; + +import java.util.Arrays; +import java.util.List; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArray; +import com.google.gwt.user.client.Element; +import com.vaadin.client.ApplicationConnection; + +/** + * ComponentLocator provides methods for generating a String locator for a given + * DOM element and for locating a DOM element using a String locator. + * <p> + * The main use for this class is locating components for automated testing + * purposes. + * + * @since 7.2, moved from {@link com.vaadin.client.ComponentLocator} + */ +public class ComponentLocator { + + private final List<LocatorStrategy> locatorStrategies; + + /** + * Reference to ApplicationConnection instance. + */ + + private final ApplicationConnection client; + + /** + * Construct a ComponentLocator for the given ApplicationConnection. + * + * @param client + * ApplicationConnection instance for the application. + */ + public ComponentLocator(ApplicationConnection client) { + this.client = client; + locatorStrategies = Arrays.asList(new VaadinFinderLocatorStrategy( + client), new LegacyLocatorStrategy(client)); + } + + /** + * Generates a String locator which uniquely identifies the target element. + * The {@link #getElementByPath(String)} method can be used for the inverse + * operation, i.e. locating an element based on the return value from this + * method. + * <p> + * Note that getElementByPath(getPathForElement(element)) == element is not + * always true as #getPathForElement(Element) can return a path to another + * element if the widget determines an action on the other element will give + * the same result as the action on the target element. + * </p> + * + * @since 5.4 + * @param targetElement + * The element to generate a path for. + * @return A String locator that identifies the target element or null if a + * String locator could not be created. + */ + public String getPathForElement(Element targetElement) { + for (LocatorStrategy strategy : locatorStrategies) { + String path = strategy.getPathForElement(targetElement); + if (null != path) { + return path; + } + } + return null; + } + + /** + * Locates an element using a String locator (path) which identifies a DOM + * element. The {@link #getPathForElement(Element)} method can be used for + * the inverse operation, i.e. generating a string expression for a DOM + * element. + * + * @since 5.4 + * @param path + * The String locator which identifies the target element. + * @return The DOM element identified by {@code path} or null if the element + * could not be located. + */ + public Element getElementByPath(String path) { + for (LocatorStrategy strategy : locatorStrategies) { + if (strategy.validatePath(path)) { + Element element = strategy.getElementByPath(path); + if (null != element) { + return element; + } + } + } + return null; + } + + /** + * Locates elements using a String locator (path) which identifies DOM + * elements. + * + * @since 7.2 + * @param path + * The String locator which identifies target elements. + * @return The JavaScriptArray of DOM elements identified by {@code path} or + * empty array if elements could not be located. + */ + public JsArray<Element> getElementsByPath(String path) { + JsArray<Element> jsElements = JavaScriptObject.createArray().cast(); + for (LocatorStrategy strategy : locatorStrategies) { + if (strategy.validatePath(path)) { + List<Element> elements = strategy.getElementsByPath(path); + if (elements.size() > 0) { + for (Element e : elements) { + jsElements.push(e); + } + return jsElements; + } + } + } + return jsElements; + } + + /** + * Locates elements using a String locator (path) which identifies DOM + * elements. The path starts from the specified root element. + * + * @see #getElementByPath(String) + * + * @since 7.2 + * @param path + * The path of elements to be found + * @param root + * The root element where the path is anchored + * @return The JavaScriptArray of DOM elements identified by {@code path} or + * empty array if elements could not be located. + */ + public JsArray<Element> getElementsByPathStartingAt(String path, + Element root) { + JsArray<Element> jsElements = JavaScriptObject.createArray().cast(); + for (LocatorStrategy strategy : locatorStrategies) { + if (strategy.validatePath(path)) { + List<Element> elements = strategy.getElementsByPathStartingAt( + path, root); + if (elements.size() > 0) { + for (Element e : elements) { + jsElements.push(e); + } + return jsElements; + } + } + } + return jsElements; + } + + /** + * Locates an element using a String locator (path) which identifies a DOM + * element. The path starts from the specified root element. + * + * @see #getElementByPath(String) + * + * @param path + * The path of the element to be found + * @param root + * The root element where the path is anchored + * @return The DOM element identified by {@code path} or null if the element + * could not be located. + */ + public Element getElementByPathStartingAt(String path, Element root) { + for (LocatorStrategy strategy : locatorStrategies) { + if (strategy.validatePath(path)) { + Element element = strategy.getElementByPathStartingAt(path, + root); + if (null != element) { + return element; + } + } + } + return null; + } + + /** + * Returns the {@link ApplicationConnection} used by this locator. + * <p> + * This method is primarily for internal use by the framework. + * + * @return the application connection + */ + public ApplicationConnection getClient() { + return client; + } + + /** + * Check if a given selector is valid for LegacyLocatorStrategy. + * + * @param path + * Vaadin selector path + * @return true if passes path validation with LegacyLocatorStrategy + */ + public boolean isValidForLegacyLocator(String path) { + for (LocatorStrategy ls : locatorStrategies) { + if (ls instanceof LegacyLocatorStrategy) { + return ls.validatePath(path); + } + } + return false; + } + +} diff --git a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java new file mode 100644 index 0000000000..2e9d0a16d0 --- /dev/null +++ b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java @@ -0,0 +1,719 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.componentlocator; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.regexp.shared.RegExp; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ConnectorMap; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.Util; +import com.vaadin.client.VCaption; +import com.vaadin.client.ui.SubPartAware; +import com.vaadin.client.ui.VCssLayout; +import com.vaadin.client.ui.VGridLayout; +import com.vaadin.client.ui.VOverlay; +import com.vaadin.client.ui.VTabsheetPanel; +import com.vaadin.client.ui.VUI; +import com.vaadin.client.ui.VWindow; +import com.vaadin.client.ui.orderedlayout.Slot; +import com.vaadin.client.ui.orderedlayout.VAbstractOrderedLayout; +import com.vaadin.client.ui.window.WindowConnector; +import com.vaadin.shared.AbstractComponentState; +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.SharedState; + +/** + * The LegacyLocatorStrategy class handles the legacy locator syntax that was + * introduced in version 5.4 of the framework. The legacy locator strategy is + * always used if no other strategy claims responsibility for a locator string. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class LegacyLocatorStrategy implements LocatorStrategy { + + /** + * Separator used in the String locator between a parent and a child widget. + */ + static final String PARENTCHILD_SEPARATOR = "/"; + /** + * Separator used in the String locator between the part identifying the + * containing widget and the part identifying the target element within the + * widget. + */ + static final String SUBPART_SEPARATOR = "#"; + /** + * String that identifies the root panel when appearing first in the String + * locator. + */ + static final String ROOT_ID = "Root"; + + private final ApplicationConnection client; + + private static final RegExp validSyntax = RegExp + .compile("^((\\w+::)?((PID_S)?\\w[-$_a-zA-Z0-9.' ]*)?)?(/[-$_a-zA-Z0-9]+\\[\\d+\\])*/?(#.*)?$"); + + public LegacyLocatorStrategy(ApplicationConnection clientConnection) { + client = clientConnection; + } + + @Override + public boolean validatePath(String path) { + return validSyntax.test(path); + } + + @Override + public String getPathForElement(Element targetElement) { + ComponentConnector connector = Util + .findPaintable(client, targetElement); + + Widget w = null; + if (connector != null) { + // If we found a Paintable then we use that as reference. We should + // find the Paintable for all but very special cases (like + // overlays). + w = connector.getWidget(); + + /* + * Still if the Paintable contains a widget that implements + * SubPartAware, we want to use that as a reference + */ + Widget targetParent = findParentWidget(targetElement, w); + while (targetParent != w && targetParent != null) { + if (targetParent instanceof SubPartAware) { + /* + * The targetParent widget is a child of the Paintable and + * the first parent (of the targetElement) that implements + * SubPartAware + */ + w = targetParent; + break; + } + targetParent = targetParent.getParent(); + } + } + if (w == null) { + // Check if the element is part of a widget that is attached + // directly to the root panel + RootPanel rootPanel = RootPanel.get(); + int rootWidgetCount = rootPanel.getWidgetCount(); + for (int i = 0; i < rootWidgetCount; i++) { + Widget rootWidget = rootPanel.getWidget(i); + if (rootWidget.getElement().isOrHasChild(targetElement)) { + // The target element is contained by this root widget + w = findParentWidget(targetElement, rootWidget); + break; + } + } + if (w != null) { + // We found a widget but we should still see if we find a + // SubPartAware implementor (we cannot find the Paintable as + // there is no link from VOverlay to its paintable/owner). + Widget subPartAwareWidget = findSubPartAwareParentWidget(w); + if (subPartAwareWidget != null) { + w = subPartAwareWidget; + } + } + } + + if (w == null) { + // Containing widget not found + return null; + } + + // Determine the path for the target widget + String path = getPathForWidget(w); + if (path == null) { + /* + * No path could be determined for the target widget. Cannot create + * a locator string. + */ + return null; + } + + // The parent check is a work around for Firefox 15 which fails to + // compare elements properly (#9534) + if (w.getElement() == targetElement) { + /* + * We are done if the target element is the root of the target + * widget. + */ + return path; + } else if (w instanceof SubPartAware) { + /* + * If the widget can provide an identifier for the targetElement we + * let it do that + */ + String elementLocator = ((SubPartAware) w) + .getSubPartName(targetElement); + if (elementLocator != null) { + return path + LegacyLocatorStrategy.SUBPART_SEPARATOR + + elementLocator; + } + } + /* + * If everything else fails we use the DOM path to identify the target + * element + */ + String domPath = getDOMPathForElement(targetElement, w.getElement()); + if (domPath == null) { + return path; + } else { + return path + domPath; + } + } + + /** + * {@inheritDoc} + */ + @Override + public Element getElementByPath(String path) { + return getElementByPathStartingAt(path, null); + } + + /** + * {@inheritDoc} + */ + @Override + public Element getElementByPathStartingAt(String path, Element baseElement) { + /* + * Path is of type "targetWidgetPath#componentPart" or + * "targetWidgetPath". + */ + String parts[] = path.split(LegacyLocatorStrategy.SUBPART_SEPARATOR, 2); + String widgetPath = parts[0]; + + // Note that this only works if baseElement can be mapped to a + // widget to which the path is relative. Otherwise, the current + // implementation simply interprets the path as if baseElement was + // null. + Widget baseWidget = Util.findWidget(baseElement, null); + + Widget w = getWidgetFromPath(widgetPath, baseWidget); + if (w == null || !Util.isAttachedAndDisplayed(w)) { + return null; + } + if (parts.length == 1) { + int pos = widgetPath.indexOf("domChild"); + if (pos == -1) { + return w.getElement(); + } + + // Contains dom reference to a sub element of the widget + String subPath = widgetPath.substring(pos); + return getElementByDOMPath(w.getElement(), subPath); + } else if (parts.length == 2) { + if (w instanceof SubPartAware) { + return ((SubPartAware) w).getSubPartElement(parts[1]); + } + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public List<Element> getElementsByPath(String path) { + // This type of search is not supported in LegacyLocator + List<Element> array = new ArrayList<Element>(); + Element e = getElementByPath(path); + if (e != null) { + array.add(e); + } + return array; + } + + /** + * {@inheritDoc} + */ + @Override + public List<Element> getElementsByPathStartingAt(String path, Element root) { + // This type of search is not supported in LegacyLocator + List<Element> array = new ArrayList<Element>(); + Element e = getElementByPathStartingAt(path, root); + if (e != null) { + array.add(e); + } + return array; + } + + /** + * Finds the first widget in the hierarchy (moving upwards) that implements + * SubPartAware. Returns the SubPartAware implementor or null if none is + * found. + * + * @param w + * The widget to start from. This is returned if it implements + * SubPartAware. + * @return The first widget (upwards in hierarchy) that implements + * SubPartAware or null + */ + Widget findSubPartAwareParentWidget(Widget w) { + + while (w != null) { + if (w instanceof SubPartAware) { + return w; + } + w = w.getParent(); + } + return null; + } + + /** + * Returns the first widget found when going from {@code targetElement} + * upwards in the DOM hierarchy, assuming that {@code ancestorWidget} is a + * parent of {@code targetElement}. + * + * @param targetElement + * @param ancestorWidget + * @return The widget whose root element is a parent of + * {@code targetElement}. + */ + private Widget findParentWidget(Element targetElement, Widget ancestorWidget) { + /* + * As we cannot resolve Widgets from the element we start from the + * widget and move downwards to the correct child widget, as long as we + * find one. + */ + if (ancestorWidget instanceof HasWidgets) { + for (Widget w : ((HasWidgets) ancestorWidget)) { + if (w.getElement().isOrHasChild(targetElement)) { + return findParentWidget(targetElement, w); + } + } + } + + // No children found, this is it + return ancestorWidget; + } + + /** + * Locates an element based on a DOM path and a base element. + * + * @param baseElement + * The base element which the path is relative to + * @param path + * String locator (consisting of domChild[x] parts) that + * identifies the element + * @return The element identified by path, relative to baseElement or null + * if the element could not be found. + */ + private Element getElementByDOMPath(Element baseElement, String path) { + String parts[] = path.split(PARENTCHILD_SEPARATOR); + Element element = baseElement; + + for (int i = 0, l = parts.length; i < l; ++i) { + String part = parts[i]; + if (part.startsWith("domChild[")) { + String childIndexString = part.substring("domChild[".length(), + part.length() - 1); + + if (Util.findWidget(baseElement, null) instanceof VAbstractOrderedLayout) { + if (element.hasChildNodes()) { + Element e = element.getFirstChildElement().cast(); + String cn = e.getClassName(); + if (cn != null + && (cn.equals("v-expand") || cn + .contains("v-has-caption"))) { + element = e; + } + } + } + + try { + int childIndex = Integer.parseInt(childIndexString); + element = DOM.getChild(element, childIndex); + } catch (Exception e) { + return null; + } + + if (element == null) { + return null; + } + + } else { + + path = parts[i]; + for (int j = i + 1; j < l; ++j) { + path += PARENTCHILD_SEPARATOR + parts[j]; + } + + return getElementByPathStartingAt(path, element); + } + } + + return element; + } + + /** + * Generates a String locator using domChild[x] parts for the element + * relative to the baseElement. + * + * @param element + * The target element + * @param baseElement + * The starting point for the locator. The generated path is + * relative to this element. + * @return A String locator that can be used to locate the target element + * using + * {@link #getElementByDOMPath(com.google.gwt.user.client.Element, String)} + * or null if the locator String cannot be created. + */ + private String getDOMPathForElement(Element element, Element baseElement) { + Element e = element; + String path = ""; + while (true) { + int childIndex = -1; + Element siblingIterator = e; + while (siblingIterator != null) { + childIndex++; + siblingIterator = siblingIterator.getPreviousSiblingElement() + .cast(); + } + + path = PARENTCHILD_SEPARATOR + "domChild[" + childIndex + "]" + + path; + + JavaScriptObject parent = e.getParentElement(); + if (parent == null) { + return null; + } + // The parent check is a work around for Firefox 15 which fails to + // compare elements properly (#9534) + if (parent == baseElement) { + break; + } + + e = parent.cast(); + } + + return path; + } + + /** + * Creates a locator String for the given widget. The path can be used to + * locate the widget using {@link #getWidgetFromPath(String, Widget)}. + * <p/> + * Returns null if no path can be determined for the widget or if the widget + * is null. + * + * @param w + * The target widget + * @return A String locator for the widget + */ + private String getPathForWidget(Widget w) { + if (w == null) { + return null; + } + String elementId = w.getElement().getId(); + if (elementId != null && !elementId.isEmpty() + && !elementId.startsWith("gwt-uid-")) { + // Use PID_S+id if the user has set an id but do not use it for auto + // generated id:s as these might not be consistent + return "PID_S" + elementId; + } else if (w instanceof VUI) { + return ""; + } else if (w instanceof VWindow) { + Connector windowConnector = ConnectorMap.get(client) + .getConnector(w); + List<WindowConnector> subWindowList = client.getUIConnector() + .getSubWindows(); + int indexOfSubWindow = subWindowList.indexOf(windowConnector); + return PARENTCHILD_SEPARATOR + "VWindow[" + indexOfSubWindow + "]"; + } else if (w instanceof RootPanel) { + return ROOT_ID; + } + + Widget parent = w.getParent(); + + String basePath = getPathForWidget(parent); + if (basePath == null) { + return null; + } + String simpleName = Util.getSimpleName(w); + + /* + * Check if the parent implements Iterable. At least VPopupView does not + * implement HasWdgets so we cannot check for that. + */ + if (!(parent instanceof Iterable<?>)) { + // Parent does not implement Iterable so we cannot find out which + // child this is + return null; + } + + Iterator<?> i = ((Iterable<?>) parent).iterator(); + int pos = 0; + while (i.hasNext()) { + Object child = i.next(); + if (child == w) { + return basePath + PARENTCHILD_SEPARATOR + simpleName + "[" + + pos + "]"; + } + String simpleName2 = Util.getSimpleName(child); + if (simpleName.equals(simpleName2)) { + pos++; + } + } + + return null; + } + + /** + * Locates the widget based on a String locator. + * + * @param path + * The String locator that identifies the widget. + * @param baseWidget + * the widget to which the path is relative, null if relative to + * root + * @return The Widget identified by the String locator or null if the widget + * could not be identified. + */ + @SuppressWarnings("unchecked") + private Widget getWidgetFromPath(String path, Widget baseWidget) { + Widget w = baseWidget; + String parts[] = path.split(PARENTCHILD_SEPARATOR); + + for (int i = 0; i < parts.length; i++) { + String part = parts[i]; + + if (part.equals(ROOT_ID)) { + w = RootPanel.get(); + } else if (part.equals("")) { + if (w == null) { + w = client.getUIConnector().getWidget(); + } + } else if (w == null) { + String id = part; + // Must be old static pid (PID_S*) + ServerConnector connector = ConnectorMap.get(client) + .getConnector(id); + if (connector == null) { + // Lookup by component id + // TODO Optimize this + connector = findConnectorById(client.getUIConnector(), + id.substring(5)); + } + + if (connector instanceof ComponentConnector) { + w = ((ComponentConnector) connector).getWidget(); + } else { + // Not found + return null; + } + } else if (part.startsWith("domChild[")) { + // The target widget has been found and the rest identifies the + // element + break; + } else if (w instanceof Iterable) { + // W identifies a widget that contains other widgets, as it + // should. Try to locate the child + Iterable<?> parent = (Iterable<?>) w; + + // Part is of type "VVerticalLayout[0]", split this into + // VVerticalLayout and 0 + String[] split = part.split("\\[", 2); + String widgetClassName = split[0]; + String indexString = split[1].substring(0, + split[1].length() - 1); + + int widgetPosition; + try { + widgetPosition = Integer.parseInt(indexString); + } catch (NumberFormatException e) { + // We've probably been fed a new-style Vaadin locator with a + // string-form predicate, that doesn't match anything in the + // search space. + return null; + } + + // AbsolutePanel in GridLayout has been removed -> skip it + if (w instanceof VGridLayout + && "AbsolutePanel".equals(widgetClassName)) { + continue; + } + + // FlowPane in CSSLayout has been removed -> skip it + if (w instanceof VCssLayout + && "VCssLayout$FlowPane".equals(widgetClassName)) { + continue; + } + + // ChildComponentContainer and VOrderedLayout$Slot have been + // replaced with Slot + if (w instanceof VAbstractOrderedLayout + && ("ChildComponentContainer".equals(widgetClassName) || "VOrderedLayout$Slot" + .equals(widgetClassName))) { + widgetClassName = "Slot"; + } + + if (w instanceof VTabsheetPanel && widgetPosition != 0) { + // TabSheetPanel now only contains 1 connector => the index + // is always 0 which indicates the widget in the active tab + widgetPosition = 0; + } + if (w instanceof VOverlay + && "VCalendarPanel".equals(widgetClassName)) { + // Vaadin 7.1 adds a wrapper for datefield popups + parent = (Iterable<?>) ((Iterable<?>) parent).iterator() + .next(); + } + /* + * The new grid and ordered layouts do not contain + * ChildComponentContainer widgets. This is instead simulated by + * constructing a path step that would find the desired widget + * from the layout and injecting it as the next search step + * (which would originally have found the widget inside the + * ChildComponentContainer) + */ + if ((w instanceof VGridLayout) + && "ChildComponentContainer".equals(widgetClassName) + && i + 1 < parts.length) { + + HasWidgets layout = (HasWidgets) w; + + String nextPart = parts[i + 1]; + String[] nextSplit = nextPart.split("\\[", 2); + String nextWidgetClassName = nextSplit[0]; + + // Find the n:th child and count the number of children with + // the same type before it + int nextIndex = 0; + for (Widget child : layout) { + boolean matchingType = nextWidgetClassName.equals(Util + .getSimpleName(child)); + if (matchingType && widgetPosition == 0) { + // This is the n:th child that we looked for + break; + } else if (widgetPosition < 0) { + // Error if we're past the desired position without + // a match + return null; + } else if (matchingType) { + // If this was another child of the expected type, + // increase the count for the next step + nextIndex++; + } + + // Don't count captions + if (!(child instanceof VCaption)) { + widgetPosition--; + } + } + + // Advance to the next step, this time checking for the + // actual child widget + parts[i + 1] = nextWidgetClassName + '[' + nextIndex + ']'; + continue; + } + + // Locate the child + Iterator<? extends Widget> iterator; + + /* + * VWindow and VContextMenu workarounds for backwards + * compatibility + */ + if (widgetClassName.equals("VWindow")) { + List<WindowConnector> windows = client.getUIConnector() + .getSubWindows(); + List<VWindow> windowWidgets = new ArrayList<VWindow>( + windows.size()); + for (WindowConnector wc : windows) { + windowWidgets.add(wc.getWidget()); + } + iterator = windowWidgets.iterator(); + } else if (widgetClassName.equals("VContextMenu")) { + return client.getContextMenu(); + } else { + iterator = (Iterator<? extends Widget>) parent.iterator(); + } + + boolean ok = false; + + // Find the widgetPosition:th child of type "widgetClassName" + while (iterator.hasNext()) { + + Widget child = iterator.next(); + String simpleName2 = Util.getSimpleName(child); + + if (!widgetClassName.equals(simpleName2) + && child instanceof Slot) { + /* + * Support legacy tests without any selector for the + * Slot widget (i.e. /VVerticalLayout[0]/VButton[0]) by + * directly checking the stuff inside the slot + */ + child = ((Slot) child).getWidget(); + simpleName2 = Util.getSimpleName(child); + } + + if (widgetClassName.equals(simpleName2)) { + if (widgetPosition == 0) { + w = child; + ok = true; + break; + } + widgetPosition--; + + } + } + + if (!ok) { + // Did not find the child + return null; + } + } else { + // W identifies something that is not a "HasWidgets". This + // should not happen as all widget containers should implement + // HasWidgets. + return null; + } + } + + return w; + } + + private ServerConnector findConnectorById(ServerConnector root, String id) { + SharedState state = root.getState(); + if (state instanceof AbstractComponentState + && id.equals(((AbstractComponentState) state).id)) { + return root; + } + for (ServerConnector child : root.getChildren()) { + ServerConnector found = findConnectorById(child, id); + if (found != null) { + return found; + } + } + + return null; + } + +} diff --git a/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java new file mode 100644 index 0000000000..e892f43d76 --- /dev/null +++ b/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java @@ -0,0 +1,122 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.componentlocator; + +import java.util.List; + +import com.google.gwt.user.client.Element; + +/** + * This interface should be implemented by all locator strategies. A locator + * strategy is responsible for generating and decoding a string that identifies + * an element in the DOM. A strategy can implement its own syntax for the + * locator string, which may be completely different from any other strategy's + * syntax. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface LocatorStrategy { + + /** + * Test the given input path for formatting errors. If a given path can not + * be validated, the locator strategy will not be attempted. + * + * @param path + * a locator path expression + * @return true, if the implementing class can process the given path, + * otherwise false + */ + boolean validatePath(String path); + + /** + * Generates a String locator which uniquely identifies the target element. + * The {@link #getElementByPath(String)} method can be used for the inverse + * operation, i.e. locating an element based on the return value from this + * method. + * <p> + * Note that getElementByPath(getPathForElement(element)) == element is not + * always true as #getPathForElement(Element) can return a path to another + * element if the widget determines an action on the other element will give + * the same result as the action on the target element. + * </p> + * + * @param targetElement + * The element to generate a path for. + * @return A String locator that identifies the target element or null if a + * String locator could not be created. + */ + String getPathForElement(Element targetElement); + + /** + * Locates an element using a String locator (path) which identifies a DOM + * element. The {@link #getPathForElement(Element)} method can be used for + * the inverse operation, i.e. generating a string expression for a DOM + * element. + * + * @param path + * The String locator which identifies the target element. + * @return The DOM element identified by {@code path} or null if the element + * could not be located. + */ + Element getElementByPath(String path); + + /** + * Locates an element using a String locator (path) which identifies a DOM + * element. The path starts from the specified root element. + * + * @see #getElementByPath(String) + * + * @param path + * The String locator which identifies the target element. + * @param root + * The element that is at the root of the path. + * @return The DOM element identified by {@code path} or null if the element + * could not be located. + */ + Element getElementByPathStartingAt(String path, Element root); + + /** + * Locates all elements that match a String locator (path) which identifies + * DOM elements. + * + * This functionality is limited in {@link LegacyLocatorStrategy}. + * + * @param path + * The String locator which identifies target elements. + * @return List that contains all matched elements. Empty list if none + * found. + */ + List<Element> getElementsByPath(String path); + + /** + * Locates all elements that match a String locator (path) which identifies + * DOM elements. The path starts from the specified root element. + * + * This functionality is limited in {@link LegacyLocatorStrategy}. + * + * @see #getElementsByPath(String) + * + * @param path + * The String locator which identifies target elements. + * @param root + * The element that is at the root of the path. + * @return List that contains all matched elements. Empty list if none + * found. + */ + + List<Element> getElementsByPathStartingAt(String path, Element root); +} diff --git a/client/src/com/vaadin/client/componentlocator/LocatorUtil.java b/client/src/com/vaadin/client/componentlocator/LocatorUtil.java new file mode 100644 index 0000000000..04624920a9 --- /dev/null +++ b/client/src/com/vaadin/client/componentlocator/LocatorUtil.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.componentlocator; + +/** + * Common String manipulator utilities used in VaadinFinderLocatorStrategy and + * SelectorPredicates. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class LocatorUtil { + + /** + * Find first occurrence of character that's not inside quotes starting from + * specified index. + * + * @param str + * Full string for searching + * @param find + * Character we want to find + * @param startingAt + * Index where we start + * @return Index of character. -1 if character not found + */ + protected static int indexOfIgnoringQuoted(String str, char find, + int startingAt) { + boolean quote = false; + String quoteChars = "'\""; + char currentQuote = '"'; + for (int i = startingAt; i < str.length(); ++i) { + char cur = str.charAt(i); + if (quote) { + if (cur == currentQuote) { + quote = !quote; + } + continue; + } else if (cur == find) { + return i; + } else { + if (quoteChars.indexOf(cur) >= 0) { + currentQuote = cur; + quote = !quote; + } + } + } + return -1; + } + + /** + * Find first occurrence of character that's not inside quotes starting from + * the beginning of string. + * + * @param str + * Full string for searching + * @param find + * Character we want to find + * @return Index of character. -1 if character not found + */ + protected static int indexOfIgnoringQuoted(String str, char find) { + return indexOfIgnoringQuoted(str, find, 0); + } +} diff --git a/client/src/com/vaadin/client/componentlocator/SelectorPredicate.java b/client/src/com/vaadin/client/componentlocator/SelectorPredicate.java new file mode 100644 index 0000000000..32b33005ed --- /dev/null +++ b/client/src/com/vaadin/client/componentlocator/SelectorPredicate.java @@ -0,0 +1,228 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.componentlocator; + +import java.util.ArrayList; +import java.util.List; + +/** + * SelectorPredicates are statements about the state of different components + * that VaadinFinderLocatorStrategy is finding. SelectorPredicates also provide + * useful information of said components to debug window by giving means to + * provide better variable naming. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class SelectorPredicate { + private String name = ""; + private String value = ""; + private boolean wildcard = false; + private int index = -1; + + public static List<SelectorPredicate> extractPostFilterPredicates( + String path) { + if (path.startsWith("(")) { + return extractPredicates(path.substring(path.lastIndexOf(')'))); + } + return new ArrayList<SelectorPredicate>(); + } + + /** + * Generate a list of predicates from a single predicate string + * + * @param str + * a comma separated string of predicates + * @return a List of Predicate objects + */ + public static List<SelectorPredicate> extractPredicates(String path) { + List<SelectorPredicate> predicates = new ArrayList<SelectorPredicate>(); + + String predicateStr = extractPredicateString(path); + if (null == predicateStr || predicateStr.length() == 0) { + return predicates; + } + + // Extract input strings + List<String> input = readPredicatesFromString(predicateStr); + + // Process each predicate into proper predicate descriptor + for (String s : input) { + SelectorPredicate p = new SelectorPredicate(); + s = s.trim(); + + try { + // If we can parse out the predicate as a pure index argument, + // stop processing here. + p.index = Integer.parseInt(s); + predicates.add(p); + + continue; + } catch (Exception e) { + p.index = -1; + } + + int idx = LocatorUtil.indexOfIgnoringQuoted(s, '='); + if (idx < 0) { + continue; + } + p.name = s.substring(0, idx); + p.value = s.substring(idx + 1); + + if (p.value.equals("?")) { + p.wildcard = true; + p.value = null; + } else { + // Only unquote predicate value once we're sure it's a proper + // value... + + p.value = unquote(p.value); + } + + predicates.add(p); + } + // Move any (and all) index predicates to last place in the list. + for (int i = 0, l = predicates.size(); i < l - 1; ++i) { + if (predicates.get(i).index > -1) { + predicates.add(predicates.remove(i)); + --i; + --l; + } + } + + return predicates; + } + + /** + * Splits the predicate string to list of predicate strings. + * + * @param predicateStr + * Comma separated predicate strings + * @return List of predicate strings + */ + private static List<String> readPredicatesFromString(String predicateStr) { + List<String> predicates = new ArrayList<String>(); + int prevIdx = 0; + int idx = LocatorUtil.indexOfIgnoringQuoted(predicateStr, ',', prevIdx); + + while (idx > -1) { + predicates.add(predicateStr.substring(prevIdx, idx)); + prevIdx = idx + 1; + idx = LocatorUtil.indexOfIgnoringQuoted(predicateStr, ',', prevIdx); + } + predicates.add(predicateStr.substring(prevIdx)); + + return predicates; + } + + /** + * Returns the predicate string, i.e. the string between the brackets in a + * path fragment. Examples: <code> + * VTextField[0] => 0 + * VTextField[caption='foo'] => caption='foo' + * </code> + * + * @param pathFragment + * The path fragment from which to extract the predicate string. + * @return The predicate string for the path fragment or empty string if not + * found. + */ + private static String extractPredicateString(String pathFragment) { + int ixOpenBracket = LocatorUtil + .indexOfIgnoringQuoted(pathFragment, '['); + if (ixOpenBracket >= 0) { + int ixCloseBracket = LocatorUtil.indexOfIgnoringQuoted( + pathFragment, ']', ixOpenBracket); + return pathFragment.substring(ixOpenBracket + 1, ixCloseBracket); + } + return ""; + } + + /** + * Removes the surrounding quotes from a string if it is quoted. + * + * @param str + * the possibly quoted string + * @return an unquoted version of str + */ + private static String unquote(String str) { + if ((str.startsWith("\"") && str.endsWith("\"")) + || (str.startsWith("'") && str.endsWith("'"))) { + return str.substring(1, str.length() - 1); + } + return str; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name + * the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @param value + * the value to set + */ + public void setValue(String value) { + this.value = value; + } + + /** + * @return the index + */ + public int getIndex() { + return index; + } + + /** + * @param index + * the index to set + */ + public void setIndex(int index) { + this.index = index; + } + + /** + * @return the wildcard + */ + public boolean isWildcard() { + return wildcard; + } + + /** + * @param wildcard + * the wildcard to set + */ + public void setWildcard(boolean wildcard) { + this.wildcard = wildcard; + } +} diff --git a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java new file mode 100644 index 0000000000..49090b66db --- /dev/null +++ b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java @@ -0,0 +1,748 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.componentlocator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.HasComponentsConnector; +import com.vaadin.client.Util; +import com.vaadin.client.metadata.Property; +import com.vaadin.client.metadata.TypeDataStore; +import com.vaadin.client.ui.AbstractConnector; +import com.vaadin.client.ui.SubPartAware; +import com.vaadin.client.ui.VNotification; + +/** + * The VaadinFinder locator strategy implements an XPath-like syntax for + * locating elements in Vaadin applications. This is used in the new + * VaadinFinder API in TestBench 4. + * + * Examples of the supported syntax: + * <ul> + * <li>Find the third text field in the DOM: {@code //VTextField[2]}</li> + * <li>Find the second button inside the first vertical layout: + * {@code //VVerticalLayout/VButton[1]}</li> + * <li>Find the first column on the third row of the "Accounts" table: + * {@code //VScrollTable[caption="Accounts"]#row[2]/col[0]}</li> + * </ul> + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class VaadinFinderLocatorStrategy implements LocatorStrategy { + + public static final String SUBPART_SEPARATOR = "#"; + + private final ApplicationConnection client; + + /** + * Internal descriptor for connector/element/widget name combinations + */ + private static final class ConnectorPath { + private String name; + private ComponentConnector connector; + } + + public VaadinFinderLocatorStrategy(ApplicationConnection clientConnection) { + client = clientConnection; + } + + /** + * {@inheritDoc} + */ + @Override + public String getPathForElement(Element targetElement) { + if (targetElement == null) { + return ""; + } + + List<ConnectorPath> hierarchy = getConnectorHierarchyForElement(targetElement); + List<String> path = new ArrayList<String>(); + + // Assemble longname path components back-to-forth with useful + // predicates - first try ID, then caption. + for (int i = 0; i < hierarchy.size(); ++i) { + ConnectorPath cp = hierarchy.get(i); + String pathFragment = cp.name; + String identifier = getPropertyValue(cp.connector, "id"); + + if (identifier != null) { + pathFragment += "[id=\"" + identifier + "\"]"; + } else { + identifier = getPropertyValue(cp.connector, "caption"); + if (identifier != null) { + pathFragment += "[caption=\"" + identifier + "\"]"; + } + } + path.add(pathFragment); + } + + if (path.size() == 0) { + // If we didn't find a single element, return null.. + return null; + } + + return getBestSelector(generateQueries(path), targetElement); + } + + /** + * Search different queries for the best one. Use the fact that the lowest + * possible index is with the last selector. Last selector is the full + * search path containing the complete Component hierarchy. + * + * @param selectors + * List of selectors + * @param target + * Target element + * @return Best selector string formatted with a post filter + */ + private String getBestSelector(List<String> selectors, Element target) { + // The last selector gives us smallest list index for target element. + String bestSelector = selectors.get(selectors.size() - 1); + int min = getElementsByPath(bestSelector).indexOf(target); + if (selectors.size() > 1 + && min == getElementsByPath(selectors.get(0)).indexOf(target)) { + // The first selector has same index as last. It's much shorter. + bestSelector = selectors.get(0); + } else if (selectors.size() > 2) { + // See if we get minimum from second last. If not then we already + // have the best one.. Second last one contains almost full + // component hierarchy. + if (getElementsByPath(selectors.get(selectors.size() - 2)).indexOf( + target) == min) { + for (int i = 1; i < selectors.size() - 2; ++i) { + // Loop through the remaining selectors and look for one + // with the same index + if (getElementsByPath(selectors.get(i)).indexOf(target) == min) { + bestSelector = selectors.get(i); + break; + } + } + + } + } + return "(" + bestSelector + ")[" + min + "]"; + + } + + /** + * Function to generate all possible search paths for given component list. + * Function strips out all the com.vaadin.ui. prefixes from elements as this + * functionality makes generating a query later on easier. + * + * @param components + * List of components + * @return List of Vaadin selectors + */ + private List<String> generateQueries(List<String> components) { + // Prepare to loop through all the elements. + List<String> paths = new ArrayList<String>(); + int compIdx = 0; + String basePath = components.get(compIdx).replace("com.vaadin.ui.", ""); + // Add a basic search for the first element (eg. //Button) + paths.add((components.size() == 1 ? "/" : "//") + basePath); + while (++compIdx < components.size()) { + // Loop through the remaining components + for (int i = components.size() - 1; i >= compIdx; --i) { + boolean recursive = false; + if (i > compIdx) { + recursive = true; + } + paths.add((i == components.size() - 1 ? "/" : "//") + + components.get(i).replace("com.vaadin.ui.", "") + + (recursive ? "//" : "/") + basePath); + } + // Add the element at index compIdx to the basePath so it is + // included in all the following searches. + basePath = components.get(compIdx).replace("com.vaadin.ui.", "") + + "/" + basePath; + } + + return paths; + } + + /** + * Helper method to get the string-form value of a named property of a + * component connector + * + * @since 7.2 + * @param c + * any ComponentConnector instance + * @param propertyName + * property name to test for + * @return a string, if the property is found, or null, if the property does + * not exist on the object (or some other error is encountered). + */ + private String getPropertyValue(ComponentConnector c, String propertyName) { + Property prop = AbstractConnector.getStateType(c).getProperty( + propertyName); + try { + return prop.getValue(c.getState()).toString(); + } catch (Exception e) { + return null; + } + } + + /** + * Generate a list representing the top-to-bottom connector hierarchy for + * any given element. ConnectorPath element provides long- and short names, + * as well as connector and widget root element references. + * + * @since 7.2 + * @param elem + * any Element that is part of a widget hierarchy + * @return a list of ConnectorPath objects, in descending order towards the + * common root container. + */ + private List<ConnectorPath> getConnectorHierarchyForElement(Element elem) { + Element e = elem; + ComponentConnector c = Util.findPaintable(client, e); + List<ConnectorPath> connectorHierarchy = new ArrayList<ConnectorPath>(); + + while (c != null) { + + for (String id : getIDsForConnector(c)) { + ConnectorPath cp = new ConnectorPath(); + cp.name = getFullClassName(id); + cp.connector = c; + + // We want to make an exception for the UI object, since it's + // our default search context (and can't be found inside itself) + if (!cp.name.equals("com.vaadin.ui.UI")) { + connectorHierarchy.add(cp); + } + } + + e = (Element) e.getParentElement(); + if (e != null) { + c = Util.findPaintable(client, e); + e = c != null ? c.getWidget().getElement() : null; + } + + } + + return connectorHierarchy; + } + + private boolean isNotificationExpression(String path) { + String[] starts = { "//", "/" }; + + String[] frags = { "com.vaadin.ui.Notification.class", + "com.vaadin.ui.Notification", "VNotification.class", + "VNotification", "Notification.class", "Notification" }; + + String[] ends = { "/", "[" }; + + for (String s : starts) { + for (String f : frags) { + if (path.equals(s + f)) { + return true; + } + + for (String e : ends) { + if (path.startsWith(s + f + e)) { + return true; + } + } + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public List<Element> getElementsByPath(String path) { + List<SelectorPredicate> postFilters = SelectorPredicate + .extractPostFilterPredicates(path); + if (postFilters.size() > 0) { + path = path.substring(1, path.lastIndexOf(')')); + } + + List<Element> elements = new ArrayList<Element>(); + if (isNotificationExpression(path)) { + + for (VNotification n : findNotificationsByPath(path)) { + elements.add(n.getElement()); + } + + } else { + + elements.addAll(eliminateDuplicates(getElementsByPathStartingAtConnector( + path, client.getUIConnector()))); + } + + for (SelectorPredicate p : postFilters) { + // Post filtering supports only indexes and follows instruction + // blindly. Index that is outside of our list results into an empty + // list and multiple indexes are likely to ruin a search completely + if (p.getIndex() >= 0) { + if (p.getIndex() >= elements.size()) { + elements.clear(); + } else { + Element e = elements.get(p.getIndex()); + elements.clear(); + elements.add(e); + } + } + } + + return elements; + } + + /** + * {@inheritDoc} + */ + @Override + public Element getElementByPath(String path) { + List<Element> elements = getElementsByPath(path); + if (elements.isEmpty()) { + return null; + } + return elements.get(0); + } + + /** + * {@inheritDoc} + */ + @Override + public Element getElementByPathStartingAt(String path, Element root) { + List<Element> elements = getElementsByPathStartingAt(path, root); + if (elements.isEmpty()) { + return null; + } + return elements.get(0); + + } + + /** + * {@inheritDoc} + */ + @Override + public List<Element> getElementsByPathStartingAt(String path, Element root) { + List<SelectorPredicate> postFilters = SelectorPredicate + .extractPostFilterPredicates(path); + if (postFilters.size() > 0) { + path = path.substring(1, path.lastIndexOf(')')); + } + + List<Element> elements = getElementsByPathStartingAtConnector(path, + Util.findPaintable(client, root)); + + for (SelectorPredicate p : postFilters) { + // Post filtering supports only indexes and follows instruction + // blindly. Index that is outside of our list results into an empty + // list and multiple indexes are likely to ruin a search completely + if (p.getIndex() >= 0) { + if (p.getIndex() >= elements.size()) { + elements.clear(); + } else { + Element e = elements.get(p.getIndex()); + elements.clear(); + elements.add(e); + } + } + } + + return elements; + } + + /** + * Special case for finding notifications as they have no connectors and are + * directly attached to {@link RootPanel}. + * + * @param path + * The path of the notification, should be + * {@code "//VNotification"} optionally followed by an index in + * brackets. + * @return the notification element or null if not found. + */ + private List<VNotification> findNotificationsByPath(String path) { + + List<VNotification> notifications = new ArrayList<VNotification>(); + for (Widget w : RootPanel.get()) { + if (w instanceof VNotification) { + notifications.add((VNotification) w); + } + } + + List<SelectorPredicate> predicates = SelectorPredicate + .extractPredicates(path); + for (SelectorPredicate p : predicates) { + + if (p.getIndex() > -1) { + VNotification n = notifications.get(p.getIndex()); + notifications.clear(); + if (n != null) { + notifications.add(n); + } + } + + } + + return eliminateDuplicates(notifications); + } + + /** + * Finds a list of elements by the specified path, starting traversal of the + * connector hierarchy from the specified root. + * + * @param path + * the locator path + * @param root + * the root connector + * @return the list of elements identified by path or empty list if not + * found. + */ + private List<Element> getElementsByPathStartingAtConnector(String path, + ComponentConnector root) { + String[] pathComponents = path.split(SUBPART_SEPARATOR); + List<ComponentConnector> connectors; + if (pathComponents[0].length() > 0) { + connectors = findConnectorsByPath(pathComponents[0], + Arrays.asList(root)); + } else { + connectors = Arrays.asList(root); + } + + List<Element> output = new ArrayList<Element>(); + if (null != connectors && !connectors.isEmpty()) { + if (pathComponents.length > 1) { + // We have subparts + for (ComponentConnector connector : connectors) { + if (connector.getWidget() instanceof SubPartAware) { + output.add(((SubPartAware) connector.getWidget()) + .getSubPartElement(pathComponents[1])); + } + } + } else { + for (ComponentConnector connector : connectors) { + output.add(connector.getWidget().getElement()); + } + } + } + return eliminateDuplicates(output); + } + + /** + * Recursively finds connectors for the elements identified by the provided + * path by traversing the connector hierarchy starting from {@code parents} + * connectors. + * + * @param path + * The path identifying elements. + * @param parents + * The list of connectors to start traversing from. + * @return The list of connectors identified by {@code path} or empty list + * if no such connectors could be found. + */ + private List<ComponentConnector> findConnectorsByPath(String path, + List<ComponentConnector> parents) { + boolean findRecursively = path.startsWith("//"); + // Strip away the one or two slashes from the beginning of the path + path = path.substring(findRecursively ? 2 : 1); + + String[] fragments = splitFirstFragmentFromTheRest(path); + + List<ComponentConnector> connectors = new ArrayList<ComponentConnector>(); + for (ComponentConnector parent : parents) { + connectors.addAll(filterMatches( + collectPotentialMatches(parent, fragments[0], + findRecursively), SelectorPredicate + .extractPredicates(fragments[0]))); + } + + if (!connectors.isEmpty() && fragments.length > 1) { + return (findConnectorsByPath(fragments[1], connectors)); + } + return eliminateDuplicates(connectors); + } + + /** + * Go through a list of potentially matching components, modifying that list + * until all elements that remain in that list match the complete list of + * predicates. + * + * @param potentialMatches + * a list of component connectors. Will be changed. + * @param predicates + * an immutable list of predicates + * @return filtered list of component connectors. + */ + private List<ComponentConnector> filterMatches( + List<ComponentConnector> potentialMatches, + List<SelectorPredicate> predicates) { + + for (SelectorPredicate p : predicates) { + + if (p.getIndex() > -1) { + try { + ComponentConnector v = potentialMatches.get(p.getIndex()); + potentialMatches.clear(); + potentialMatches.add(v); + } catch (IndexOutOfBoundsException e) { + potentialMatches.clear(); + } + + continue; + } + + for (int i = 0, l = potentialMatches.size(); i < l; ++i) { + + String propData = getPropertyValue(potentialMatches.get(i), + p.getName()); + + if ((p.isWildcard() && propData == null) + || (!p.isWildcard() && !p.getValue().equals(propData))) { + potentialMatches.remove(i); + --l; + --i; + } + } + + } + + return eliminateDuplicates(potentialMatches); + } + + /** + * Collects all connectors that match the widget class name of the path + * fragment. If the {@code collectRecursively} parameter is true, a + * depth-first search of the connector hierarchy is performed. + * + * Searching depth-first ensure that we can return the matches in correct + * order for selecting based on index predicates. + * + * @param parent + * The {@link ComponentConnector} to start the search from. + * @param pathFragment + * The path fragment identifying which type of widget to search + * for. + * @param collectRecursively + * If true, all matches from all levels below {@code parent} will + * be collected. If false only direct children will be collected. + * @return A list of {@link ComponentConnector}s matching the widget type + * specified in the {@code pathFragment}. + */ + private List<ComponentConnector> collectPotentialMatches( + ComponentConnector parent, String pathFragment, + boolean collectRecursively) { + ArrayList<ComponentConnector> potentialMatches = new ArrayList<ComponentConnector>(); + if (parent instanceof HasComponentsConnector) { + List<ComponentConnector> children = ((HasComponentsConnector) parent) + .getChildComponents(); + for (ComponentConnector child : children) { + String widgetName = getWidgetName(pathFragment); + if (connectorMatchesPathFragment(child, widgetName)) { + potentialMatches.add(child); + } + if (collectRecursively) { + potentialMatches.addAll(collectPotentialMatches(child, + pathFragment, collectRecursively)); + } + } + } + return eliminateDuplicates(potentialMatches); + } + + private List<String> getIDsForConnector(ComponentConnector connector) { + Class<?> connectorClass = connector.getClass(); + List<String> ids = new ArrayList<String>(); + + TypeDataStore.get().findIdentifiersFor(connectorClass).addAllTo(ids); + + return ids; + } + + /** + * Determines whether a connector matches a path fragment. This is done by + * comparing the path fragment to the name of the widget type of the + * connector. + * + * @param connector + * The connector to compare. + * @param widgetName + * The name of the widget class. + * @return true if the widget type of the connector equals the widget type + * identified by the path fragment. + */ + private boolean connectorMatchesPathFragment(ComponentConnector connector, + String widgetName) { + + List<String> ids = getIDsForConnector(connector); + + Integer[] widgetTags = client.getConfiguration() + .getTagsForServerSideClassName(getFullClassName(widgetName)); + if (widgetTags.length == 0) { + widgetTags = client.getConfiguration() + .getTagsForServerSideClassName( + getFullClassName("com.vaadin.ui." + widgetName)); + } + + for (int i = 0, l = ids.size(); i < l; ++i) { + + // Fuzz the connector name, so that the client can provide (for + // example: /Button, /Button.class, /com.vaadin.ui.Button, + // /com.vaadin.ui.Button.class, etc) + + String name = ids.get(i); + final String simpleName = getSimpleClassName(name); + final String fullName = getFullClassName(name); + + if (widgetTags.length > 0) { + Integer[] foundTags = client.getConfiguration() + .getTagsForServerSideClassName(fullName); + for (int tag : foundTags) { + if (tagsMatch(widgetTags, tag)) { + return true; + } + } + } + + // Fallback if something failed before. + if (widgetName.equals(fullName + ".class") + || widgetName.equals(fullName) + || widgetName.equals(simpleName + ".class") + || widgetName.equals(simpleName) || widgetName.equals(name)) { + return true; + } + } + + // If the server-side class name didn't match, fall back to testing for + // the explicit widget name + String widget = Util.getSimpleName(connector.getWidget()); + return widgetName.equals(widget) + || widgetName.equals(widget + ".class"); + + } + + /** + * Extracts the name of the widget class from a path fragment + * + * @param pathFragment + * the path fragment + * @return the name of the widget class. + */ + private String getWidgetName(String pathFragment) { + String widgetName = pathFragment; + int ixBracket = pathFragment.indexOf('['); + if (ixBracket >= 0) { + widgetName = pathFragment.substring(0, ixBracket); + } + return widgetName; + } + + /** + * Splits off the first path fragment from a path and returns an array of + * two elements, where the first element is the first path fragment and the + * second element is the rest of the path (all remaining path fragments + * untouched). + * + * @param path + * The path to split. + * @return An array of two elements: The first path fragment and the rest of + * the path. + */ + private String[] splitFirstFragmentFromTheRest(String path) { + int ixOfSlash = LocatorUtil.indexOfIgnoringQuoted(path, '/'); + if (ixOfSlash > 0) { + return new String[] { path.substring(0, ixOfSlash), + path.substring(ixOfSlash) }; + } + return new String[] { path }; + } + + private String getSimpleClassName(String s) { + String[] parts = s.split("\\."); + if (s.endsWith(".class")) { + return parts[parts.length - 2]; + } + return parts.length > 0 ? parts[parts.length - 1] : s; + } + + private String getFullClassName(String s) { + if (s.endsWith(".class")) { + return s.substring(0, s.lastIndexOf(".class")); + } + return s; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.client.componentlocator.LocatorStrategy#validatePath(java. + * lang.String) + */ + @Override + public boolean validatePath(String path) { + // This syntax is so difficult to regexp properly, that we'll just try + // to find something with it regardless of the correctness of the + // syntax... + return true; + } + + /** + * Go through a list, removing all duplicate elements from it. This method + * is used to avoid accumulation of duplicate entries in result lists + * resulting from low-context recursion. + * + * Preserves first entry in list, removes others. Preserves list order. + * + * @return list passed as parameter, after modification + */ + private final <T> List<T> eliminateDuplicates(List<T> list) { + + int l = list.size(); + for (int j = 0; j < l; ++j) { + T ref = list.get(j); + + for (int i = j + 1; i < l; ++i) { + if (list.get(i) == ref) { + list.remove(i); + --i; + --l; + } + } + } + + return list; + } + + private boolean tagsMatch(Integer[] targets, Integer tag) { + for (int i = 0; i < targets.length; ++i) { + if (targets[i].equals(tag)) { + return true; + } + } + + try { + return tagsMatch(targets, + client.getConfiguration().getParentTag(tag)); + } catch (Exception e) { + return false; + } + } +} diff --git a/client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java b/client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java new file mode 100644 index 0000000000..7561bc2c03 --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java @@ -0,0 +1,267 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gwt.core.client.JsArray; +import com.google.gwt.dom.client.Style.TextDecoration; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +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.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ComputedStyle; +import com.vaadin.client.ConnectorMap; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.SimpleTree; +import com.vaadin.client.Util; +import com.vaadin.client.ValueMap; + +/** + * Analyze layouts view panel of the debug window. + * + * @since 7.1.4 + */ +public class AnalyzeLayoutsPanel extends FlowPanel { + + private List<SelectConnectorListener> listeners = new ArrayList<SelectConnectorListener>(); + + public void update() { + clear(); + add(new Label("Analyzing layouts...")); + List<ApplicationConnection> runningApplications = ApplicationConfiguration + .getRunningApplications(); + for (ApplicationConnection applicationConnection : runningApplications) { + applicationConnection.analyzeLayouts(); + } + } + + public void meta(ApplicationConnection ac, ValueMap meta) { + clear(); + JsArray<ValueMap> valueMapArray = meta + .getJSValueMapArray("invalidLayouts"); + int size = valueMapArray.length(); + + if (size > 0) { + SimpleTree root = new SimpleTree("Layouts analyzed, " + size + + " top level problems"); + for (int i = 0; i < size; i++) { + printLayoutError(ac, valueMapArray.get(i), root); + } + root.open(false); + add(root); + } else { + add(new Label("Layouts analyzed, no top level problems")); + } + + Set<ComponentConnector> zeroHeightComponents = new HashSet<ComponentConnector>(); + Set<ComponentConnector> zeroWidthComponents = new HashSet<ComponentConnector>(); + findZeroSizeComponents(zeroHeightComponents, zeroWidthComponents, + ac.getUIConnector()); + if (zeroHeightComponents.size() > 0 || zeroWidthComponents.size() > 0) { + add(new HTML("<h4> Client side notifications</h4>" + + " <em>The following relative sized components were " + + "rendered to a zero size container on the client side." + + " Note that these are not necessarily invalid " + + "states, but reported here as they might be.</em>")); + if (zeroHeightComponents.size() > 0) { + add(new HTML("<p><strong>Vertically zero size:</strong></p>")); + printClientSideDetectedIssues(zeroHeightComponents, ac); + } + if (zeroWidthComponents.size() > 0) { + add(new HTML("<p><strong>Horizontally zero size:</strong></p>")); + printClientSideDetectedIssues(zeroWidthComponents, ac); + } + } + + } + + private void printClientSideDetectedIssues( + Set<ComponentConnector> zeroSized, ApplicationConnection ac) { + + // keep track of already highlighted parents + HashSet<String> parents = new HashSet<String>(); + + for (final ComponentConnector connector : zeroSized) { + final ServerConnector parent = connector.getParent(); + final String parentId = parent.getConnectorId(); + + final Label errorDetails = new Label(Util.getSimpleName(connector) + + "[" + connector.getConnectorId() + "]" + " inside " + + Util.getSimpleName(parent)); + + if (parent instanceof ComponentConnector) { + final ComponentConnector parentConnector = (ComponentConnector) parent; + if (!parents.contains(parentId)) { + parents.add(parentId); + Highlight.show(parentConnector, "yellow"); + } + + errorDetails.addMouseOverHandler(new MouseOverHandler() { + @Override + public void onMouseOver(MouseOverEvent event) { + Highlight.hideAll(); + Highlight.show(parentConnector, "yellow"); + Highlight.show(connector); + errorDetails.getElement().getStyle() + .setTextDecoration(TextDecoration.UNDERLINE); + } + }); + errorDetails.addMouseOutHandler(new MouseOutHandler() { + @Override + public void onMouseOut(MouseOutEvent event) { + Highlight.hideAll(); + errorDetails.getElement().getStyle() + .setTextDecoration(TextDecoration.NONE); + } + }); + errorDetails.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + fireSelectEvent(connector); + } + }); + + } + + Highlight.show(connector); + add(errorDetails); + + } + } + + private void printLayoutError(ApplicationConnection ac, ValueMap valueMap, + SimpleTree root) { + final String pid = valueMap.getString("id"); + + // find connector + final ComponentConnector connector = (ComponentConnector) ConnectorMap + .get(ac).getConnector(pid); + + if (connector == null) { + root.add(new SimpleTree("[" + pid + "] NOT FOUND")); + return; + } + + Highlight.show(connector); + + final SimpleTree errorNode = new SimpleTree( + Util.getSimpleName(connector) + " id: " + pid); + errorNode.addDomHandler(new MouseOverHandler() { + @Override + public void onMouseOver(MouseOverEvent event) { + Highlight.showOnly(connector); + ((Widget) event.getSource()).getElement().getStyle() + .setTextDecoration(TextDecoration.UNDERLINE); + } + }, MouseOverEvent.getType()); + errorNode.addDomHandler(new MouseOutHandler() { + @Override + public void onMouseOut(MouseOutEvent event) { + Highlight.hideAll(); + ((Widget) event.getSource()).getElement().getStyle() + .setTextDecoration(TextDecoration.NONE); + } + }, MouseOutEvent.getType()); + + errorNode.addDomHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (event.getNativeEvent().getEventTarget().cast() == errorNode + .getElement().getChild(1).cast()) { + fireSelectEvent(connector); + } + } + }, ClickEvent.getType()); + + VerticalPanel errorDetails = new VerticalPanel(); + + if (valueMap.containsKey("heightMsg")) { + errorDetails.add(new Label("Height problem: " + + valueMap.getString("heightMsg"))); + } + if (valueMap.containsKey("widthMsg")) { + errorDetails.add(new Label("Width problem: " + + valueMap.getString("widthMsg"))); + } + if (errorDetails.getWidgetCount() > 0) { + errorNode.add(errorDetails); + } + if (valueMap.containsKey("subErrors")) { + HTML l = new HTML( + "<em>Expand this node to show problems that may be dependent on this problem.</em>"); + errorDetails.add(l); + JsArray<ValueMap> suberrors = valueMap + .getJSValueMapArray("subErrors"); + for (int i = 0; i < suberrors.length(); i++) { + ValueMap value = suberrors.get(i); + printLayoutError(ac, value, errorNode); + } + + } + root.add(errorNode); + } + + private void findZeroSizeComponents( + Set<ComponentConnector> zeroHeightComponents, + Set<ComponentConnector> zeroWidthComponents, + ComponentConnector connector) { + Widget widget = connector.getWidget(); + ComputedStyle computedStyle = new ComputedStyle(widget.getElement()); + if (computedStyle.getIntProperty("height") == 0) { + zeroHeightComponents.add(connector); + } + if (computedStyle.getIntProperty("width") == 0) { + zeroWidthComponents.add(connector); + } + List<ServerConnector> children = connector.getChildren(); + for (ServerConnector serverConnector : children) { + if (serverConnector instanceof ComponentConnector) { + findZeroSizeComponents(zeroHeightComponents, + zeroWidthComponents, + (ComponentConnector) serverConnector); + } + } + } + + public void addListener(SelectConnectorListener listener) { + listeners.add(listener); + } + + public void removeListener(SelectConnectorListener listener) { + listeners.remove(listener); + } + + private void fireSelectEvent(ServerConnector connector) { + for (SelectConnectorListener listener : listeners) { + listener.select(connector, null); + } + } + +} diff --git a/client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java b/client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java new file mode 100644 index 0000000000..fc7b55497e --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java @@ -0,0 +1,107 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.JsArrayObject; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.Util; +import com.vaadin.client.VConsole; +import com.vaadin.client.metadata.NoDataException; +import com.vaadin.client.metadata.Property; +import com.vaadin.client.ui.AbstractConnector; +import com.vaadin.shared.AbstractComponentState; +import com.vaadin.shared.communication.SharedState; + +/** + * Connector information view panel of the debug window. + * + * @since 7.1.4 + */ +public class ConnectorInfoPanel extends FlowPanel { + + /** + * Update the panel to show information about a connector. + * + * @param connector + */ + public void update(ServerConnector connector) { + SharedState state = connector.getState(); + + Set<String> ignoreProperties = new HashSet<String>(); + ignoreProperties.add("id"); + + String html = getRowHTML("Id", connector.getConnectorId()); + html += getRowHTML("Connector", Util.getSimpleName(connector)); + + if (connector instanceof ComponentConnector) { + ComponentConnector component = (ComponentConnector) connector; + + ignoreProperties.addAll(Arrays.asList("caption", "description", + "width", "height")); + + AbstractComponentState componentState = component.getState(); + + html += getRowHTML("Widget", + Util.getSimpleName(component.getWidget())); + html += getRowHTML("Caption", componentState.caption); + html += getRowHTML("Description", componentState.description); + html += getRowHTML("Width", componentState.width + " (actual: " + + component.getWidget().getOffsetWidth() + "px)"); + html += getRowHTML("Height", componentState.height + " (actual: " + + component.getWidget().getOffsetHeight() + "px)"); + } + + try { + JsArrayObject<Property> properties = AbstractConnector + .getStateType(connector).getPropertiesAsArray(); + for (int i = 0; i < properties.size(); i++) { + Property property = properties.get(i); + String name = property.getName(); + if (!ignoreProperties.contains(name)) { + html += getRowHTML(property.getDisplayName(), + property.getValue(state)); + } + } + } catch (NoDataException e) { + html += "<div>Could not read state, error has been logged to the console</div>"; + VConsole.error(e); + } + + clear(); + add(new HTML(html)); + } + + private String getRowHTML(String caption, Object value) { + return "<div class=\"" + VDebugWindow.STYLENAME + + "-row\"><span class=\"caption\">" + caption + + "</span><span class=\"value\">" + + Util.escapeHTML(String.valueOf(value)) + "</span></div>"; + } + + /** + * Clear the contents of the panel. + */ + public void clearContents() { + clear(); + } +} diff --git a/client/src/com/vaadin/client/debug/internal/HierarchyPanel.java b/client/src/com/vaadin/client/debug/internal/HierarchyPanel.java new file mode 100644 index 0000000000..755f076b7a --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/HierarchyPanel.java @@ -0,0 +1,178 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.DoubleClickEvent; +import com.google.gwt.event.dom.client.DoubleClickHandler; +import com.google.gwt.event.dom.client.HasDoubleClickHandlers; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.FastStringSet; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.SimpleTree; +import com.vaadin.client.Util; + +/** + * Hierarchy view panel of the debug window. This class can be used in various + * debug window sections to show the current connector hierarchy. + * + * @since 7.1.4 + */ +public class HierarchyPanel extends FlowPanel { + + // TODO separate click listeners for simple selection and doubleclick + private List<SelectConnectorListener> listeners = new ArrayList<SelectConnectorListener>(); + + public void update() { + // Try to keep track of currently open nodes and reopen them + FastStringSet openNodes = FastStringSet.create(); + Iterator<Widget> it = iterator(); + while (it.hasNext()) { + collectOpenNodes(it.next(), openNodes); + } + + clear(); + + SimplePanel trees = new SimplePanel(); + + for (ApplicationConnection application : ApplicationConfiguration + .getRunningApplications()) { + ServerConnector uiConnector = application.getUIConnector(); + Widget connectorTree = buildConnectorTree(uiConnector, openNodes); + + trees.add(connectorTree); + } + + add(trees); + } + + /** + * Adds the captions of all open (non-leaf) nodes in the hierarchy tree + * recursively. + * + * @param widget + * the widget in which to search for open nodes (if SimpleTree) + * @param openNodes + * the set in which open nodes should be added + */ + private void collectOpenNodes(Widget widget, FastStringSet openNodes) { + if (widget instanceof SimpleTree) { + SimpleTree tree = (SimpleTree) widget; + if (tree.isOpen()) { + openNodes.add(tree.getCaption()); + } else { + // no need to look inside closed nodes + return; + } + } + if (widget instanceof HasWidgets) { + Iterator<Widget> it = ((HasWidgets) widget).iterator(); + while (it.hasNext()) { + collectOpenNodes(it.next(), openNodes); + } + } + } + + private Widget buildConnectorTree(final ServerConnector connector, + FastStringSet openNodes) { + String connectorString = Util.getConnectorString(connector); + + List<ServerConnector> children = connector.getChildren(); + + Widget widget; + if (children == null || children.isEmpty()) { + // Leaf node, just add a label + Label label = new Label(connectorString); + label.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + Highlight.showOnly(connector); + showServerDebugInfo(connector); + } + }); + widget = label; + } else { + SimpleTree tree = new SimpleTree(connectorString) { + @Override + protected void select(ClickEvent event) { + super.select(event); + Highlight.showOnly(connector); + showServerDebugInfo(connector); + } + }; + for (ServerConnector child : children) { + tree.add(buildConnectorTree(child, openNodes)); + } + if (openNodes.contains(connectorString)) { + tree.open(false); + } + widget = tree; + } + + if (widget instanceof HasDoubleClickHandlers) { + HasDoubleClickHandlers has = (HasDoubleClickHandlers) widget; + has.addDoubleClickHandler(new DoubleClickHandler() { + @Override + public void onDoubleClick(DoubleClickEvent event) { + fireSelectEvent(connector); + } + }); + } + + return widget; + } + + public void addListener(SelectConnectorListener listener) { + listeners.add(listener); + } + + public void removeListener(SelectConnectorListener listener) { + listeners.remove(listener); + } + + private void fireSelectEvent(ServerConnector connector) { + for (SelectConnectorListener listener : listeners) { + listener.select(connector, null); + } + } + + /** + * Outputs debug information on the server - usually in the console of an + * IDE, with a clickable reference to the relevant code location. + * + * @since 7.1 + * @param connector + * show debug info for this connector + */ + static void showServerDebugInfo(ServerConnector connector) { + if (connector != null) { + connector.getConnection().getUIConnector() + .showServerDebugInfo(connector); + } + } + +} diff --git a/client/src/com/vaadin/client/debug/internal/HierarchySection.java b/client/src/com/vaadin/client/debug/internal/HierarchySection.java index 90c9086d7d..616bf70c38 100644 --- a/client/src/com/vaadin/client/debug/internal/HierarchySection.java +++ b/client/src/com/vaadin/client/debug/internal/HierarchySection.java @@ -15,23 +15,9 @@ */ package com.vaadin.client.debug.internal; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import com.google.gwt.core.client.JsArray; -import com.google.gwt.dom.client.Style.TextDecoration; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.DoubleClickEvent; -import com.google.gwt.event.dom.client.DoubleClickHandler; -import com.google.gwt.event.dom.client.HasDoubleClickHandlers; import com.google.gwt.event.dom.client.KeyCodes; -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.shared.HandlerRegistration; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; @@ -40,28 +26,15 @@ import com.google.gwt.user.client.Event.NativePreviewHandler; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FlowPanel; 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; -import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConfiguration; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.ComponentConnector; -import com.vaadin.client.ComputedStyle; -import com.vaadin.client.ConnectorMap; -import com.vaadin.client.JsArrayObject; import com.vaadin.client.ServerConnector; -import com.vaadin.client.SimpleTree; import com.vaadin.client.Util; -import com.vaadin.client.VConsole; import com.vaadin.client.ValueMap; -import com.vaadin.client.metadata.NoDataException; -import com.vaadin.client.metadata.Property; -import com.vaadin.client.ui.AbstractConnector; -import com.vaadin.client.ui.UnknownComponentConnector; -import com.vaadin.shared.AbstractComponentState; -import com.vaadin.shared.communication.SharedState; /** * Provides functionality for examining the UI component hierarchy. @@ -73,7 +46,15 @@ public class HierarchySection implements Section { private final DebugButton tabButton = new DebugButton(Icon.HIERARCHY, "Examine component hierarchy"); - private final FlowPanel content = new FlowPanel(); + private final SimplePanel content = new SimplePanel(); + + // TODO highlighting logic is split between these, should be refactored + private final FlowPanel helpPanel = new FlowPanel(); + private final ConnectorInfoPanel infoPanel = new ConnectorInfoPanel(); + private final HierarchyPanel hierarchyPanel = new HierarchyPanel(); + private final OptimizedWidgetsetPanel widgetsetPanel = new OptimizedWidgetsetPanel(); + private final AnalyzeLayoutsPanel analyzeLayoutsPanel = new AnalyzeLayoutsPanel(); + private final FlowPanel controls = new FlowPanel(); private final Button find = new DebugButton(Icon.HIGHLIGHT, @@ -125,79 +106,40 @@ public class HierarchySection implements Section { } }); + hierarchyPanel.addListener(new SelectConnectorListener() { + @Override + public void select(ServerConnector connector, Element element) { + printState(connector, true); + } + }); + + analyzeLayoutsPanel.addListener(new SelectConnectorListener() { + @Override + public void select(ServerConnector connector, Element element) { + printState(connector, true); + } + }); + content.setStylePrimaryName(VDebugWindow.STYLENAME + "-hierarchy"); + initializeHelpPanel(); + content.setWidget(helpPanel); + } + + private void initializeHelpPanel() { HTML info = new HTML(showHierarchy.getHTML() + " " + showHierarchy.getTitle() + "<br/>" + find.getHTML() + " " + find.getTitle() + "<br/>" + analyze.getHTML() + " " + analyze.getTitle() + "<br/>" + generateWS.getHTML() + " " + generateWS.getTitle() + "<br/>"); info.setStyleName(VDebugWindow.STYLENAME + "-info"); - content.add(info); + helpPanel.add(info); } private void showHierarchy() { Highlight.hideAll(); - content.clear(); - - // TODO Clearing and rebuilding the contents is not optimal for UX as - // any previous expansions are lost. - SimplePanel trees = new SimplePanel(); - - for (ApplicationConnection application : ApplicationConfiguration - .getRunningApplications()) { - ServerConnector uiConnector = application.getUIConnector(); - Widget connectorTree = buildConnectorTree(uiConnector); - - trees.add(connectorTree); - } - - content.add(trees); - } - - private Widget buildConnectorTree(final ServerConnector connector) { - String connectorString = Util.getConnectorString(connector); - - List<ServerConnector> children = connector.getChildren(); - - Widget widget; - if (children == null || children.isEmpty()) { - // Leaf node, just add a label - Label label = new Label(connectorString); - label.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - Highlight.showOnly(connector); - Highlight.showServerDebugInfo(connector); - } - }); - widget = label; - } else { - SimpleTree tree = new SimpleTree(connectorString) { - @Override - protected void select(ClickEvent event) { - super.select(event); - Highlight.showOnly(connector); - Highlight.showServerDebugInfo(connector); - } - }; - for (ServerConnector child : children) { - tree.add(buildConnectorTree(child)); - } - widget = tree; - } - - if (widget instanceof HasDoubleClickHandlers) { - HasDoubleClickHandlers has = (HasDoubleClickHandlers) widget; - has.addDoubleClickHandler(new DoubleClickHandler() { - @Override - public void onDoubleClick(DoubleClickEvent event) { - printState(connector, true); - } - }); - } - - return widget; + hierarchyPanel.update(); + content.setWidget(hierarchyPanel); } @Override @@ -226,302 +168,19 @@ public class HierarchySection implements Section { } private void generateWidgetset() { - - content.clear(); - HTML h = new HTML("Getting used connectors"); - content.add(h); - - String s = ""; - for (ApplicationConnection ac : ApplicationConfiguration - .getRunningApplications()) { - ApplicationConfiguration conf = ac.getConfiguration(); - s += "<h1>Used connectors for " + conf.getServiceUrl() + "</h1>"; - - for (String connectorName : getUsedConnectorNames(conf)) { - s += connectorName + "<br/>"; - } - - s += "<h2>To make an optimized widgetset based on these connectors, do:</h2>"; - s += "<h3>1. Add to your widgetset.gwt.xml file:</h2>"; - s += "<textarea rows=\"3\" style=\"width:90%\">"; - s += "<generate-with class=\"OptimizedConnectorBundleLoaderFactory\">\n"; - s += " <when-type-assignable class=\"com.vaadin.client.metadata.ConnectorBundleLoader\" />\n"; - s += "</generate-with>"; - s += "</textarea>"; - - s += "<h3>2. Add the following java file to your project:</h2>"; - s += "<textarea rows=\"5\" style=\"width:90%\">"; - s += generateOptimizedWidgetSet(getUsedConnectorNames(conf)); - s += "</textarea>"; - s += "<h3>3. Recompile widgetset</h2>"; - - } - - h.setHTML(s); - } - - private Set<String> getUsedConnectorNames( - ApplicationConfiguration configuration) { - int tag = 0; - Set<String> usedConnectors = new HashSet<String>(); - while (true) { - String serverSideClass = configuration - .getServerSideClassNameForTag(tag); - if (serverSideClass == null) { - break; - } - Class<? extends ServerConnector> connectorClass = configuration - .getConnectorClassByEncodedTag(tag); - if (connectorClass == null) { - break; - } - - if (connectorClass != UnknownComponentConnector.class) { - usedConnectors.add(connectorClass.getName()); - } - tag++; - if (tag > 10000) { - // Sanity check - VConsole.error("Search for used connector classes was forcefully terminated"); - break; - } - } - return usedConnectors; - } - - public String generateOptimizedWidgetSet(Set<String> usedConnectors) { - String s = "import java.util.HashSet;\n"; - s += "import java.util.Set;\n"; - - s += "import com.google.gwt.core.ext.typeinfo.JClassType;\n"; - s += "import com.vaadin.client.ui.ui.UIConnector;\n"; - s += "import com.vaadin.server.widgetsetutils.ConnectorBundleLoaderFactory;\n"; - s += "import com.vaadin.shared.ui.Connect.LoadStyle;\n\n"; - - s += "public class OptimizedConnectorBundleLoaderFactory extends\n"; - s += " ConnectorBundleLoaderFactory {\n"; - s += " private Set<String> eagerConnectors = new HashSet<String>();\n"; - s += " {\n"; - for (String c : usedConnectors) { - s += " eagerConnectors.add(" + c - + ".class.getName());\n"; - } - s += " }\n"; - s += "\n"; - s += " @Override\n"; - s += " protected LoadStyle getLoadStyle(JClassType connectorType) {\n"; - s += " if (eagerConnectors.contains(connectorType.getQualifiedBinaryName())) {\n"; - s += " return LoadStyle.EAGER;\n"; - s += " } else {\n"; - s += " // Loads all other connectors immediately after the initial view has\n"; - s += " // been rendered\n"; - s += " return LoadStyle.DEFERRED;\n"; - s += " }\n"; - s += " }\n"; - s += "}\n"; - - return s; + widgetsetPanel.update(); + content.setWidget(widgetsetPanel); } private void analyzeLayouts() { - content.clear(); - content.add(new Label("Analyzing layouts...")); - List<ApplicationConnection> runningApplications = ApplicationConfiguration - .getRunningApplications(); - for (ApplicationConnection applicationConnection : runningApplications) { - applicationConnection.analyzeLayouts(); - } + analyzeLayoutsPanel.update(); + content.setWidget(analyzeLayoutsPanel); } @Override public void meta(ApplicationConnection ac, ValueMap meta) { - content.clear(); - JsArray<ValueMap> valueMapArray = meta - .getJSValueMapArray("invalidLayouts"); - int size = valueMapArray.length(); - - if (size > 0) { - SimpleTree root = new SimpleTree("Layouts analyzed, " + size - + " top level problems"); - for (int i = 0; i < size; i++) { - printLayoutError(ac, valueMapArray.get(i), root); - } - root.open(false); - content.add(root); - } else { - content.add(new Label("Layouts analyzed, no top level problems")); - } - - Set<ComponentConnector> zeroHeightComponents = new HashSet<ComponentConnector>(); - Set<ComponentConnector> zeroWidthComponents = new HashSet<ComponentConnector>(); - findZeroSizeComponents(zeroHeightComponents, zeroWidthComponents, - ac.getUIConnector()); - if (zeroHeightComponents.size() > 0 || zeroWidthComponents.size() > 0) { - content.add(new HTML("<h4> Client side notifications</h4>" - + " <em>The following relative sized components were " - + "rendered to a zero size container on the client side." - + " Note that these are not necessarily invalid " - + "states, but reported here as they might be.</em>")); - if (zeroHeightComponents.size() > 0) { - content.add(new HTML( - "<p><strong>Vertically zero size:</strong></p>")); - printClientSideDetectedIssues(zeroHeightComponents, ac); - } - if (zeroWidthComponents.size() > 0) { - content.add(new HTML( - "<p><strong>Horizontally zero size:</strong></p>")); - printClientSideDetectedIssues(zeroWidthComponents, ac); - } - } - - } - - private void printClientSideDetectedIssues( - Set<ComponentConnector> zeroSized, ApplicationConnection ac) { - - // keep track of already highlighted parents - HashSet<String> parents = new HashSet<String>(); - - for (final ComponentConnector connector : zeroSized) { - final ServerConnector parent = connector.getParent(); - final String parentId = parent.getConnectorId(); - - final Label errorDetails = new Label(Util.getSimpleName(connector) - + "[" + connector.getConnectorId() + "]" + " inside " - + Util.getSimpleName(parent)); - - if (parent instanceof ComponentConnector) { - final ComponentConnector parentConnector = (ComponentConnector) parent; - if (!parents.contains(parentId)) { - parents.add(parentId); - Highlight.show(parentConnector, "yellow"); - } - - errorDetails.addMouseOverHandler(new MouseOverHandler() { - @Override - public void onMouseOver(MouseOverEvent event) { - Highlight.hideAll(); - Highlight.show(parentConnector, "yellow"); - Highlight.show(connector); - errorDetails.getElement().getStyle() - .setTextDecoration(TextDecoration.UNDERLINE); - } - }); - errorDetails.addMouseOutHandler(new MouseOutHandler() { - @Override - public void onMouseOut(MouseOutEvent event) { - Highlight.hideAll(); - errorDetails.getElement().getStyle() - .setTextDecoration(TextDecoration.NONE); - } - }); - errorDetails.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - printState(connector, true); - } - }); - - } - - Highlight.show(connector); - content.add(errorDetails); - - } - } - - private void printLayoutError(ApplicationConnection ac, ValueMap valueMap, - SimpleTree root) { - final String pid = valueMap.getString("id"); - - // find connector - final ComponentConnector connector = (ComponentConnector) ConnectorMap - .get(ac).getConnector(pid); - - if (connector == null) { - root.add(new SimpleTree("[" + pid + "] NOT FOUND")); - return; - } - - Highlight.show(connector); - - final SimpleTree errorNode = new SimpleTree( - Util.getSimpleName(connector) + " id: " + pid); - errorNode.addDomHandler(new MouseOverHandler() { - @Override - public void onMouseOver(MouseOverEvent event) { - Highlight.showOnly(connector); - ((Widget) event.getSource()).getElement().getStyle() - .setTextDecoration(TextDecoration.UNDERLINE); - } - }, MouseOverEvent.getType()); - errorNode.addDomHandler(new MouseOutHandler() { - @Override - public void onMouseOut(MouseOutEvent event) { - Highlight.hideAll(); - ((Widget) event.getSource()).getElement().getStyle() - .setTextDecoration(TextDecoration.NONE); - } - }, MouseOutEvent.getType()); - - errorNode.addDomHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - if (event.getNativeEvent().getEventTarget().cast() == errorNode - .getElement().getChild(1).cast()) { - printState(connector, true); - } - } - }, ClickEvent.getType()); - - VerticalPanel errorDetails = new VerticalPanel(); - - if (valueMap.containsKey("heightMsg")) { - errorDetails.add(new Label("Height problem: " - + valueMap.getString("heightMsg"))); - } - if (valueMap.containsKey("widthMsg")) { - errorDetails.add(new Label("Width problem: " - + valueMap.getString("widthMsg"))); - } - if (errorDetails.getWidgetCount() > 0) { - errorNode.add(errorDetails); - } - if (valueMap.containsKey("subErrors")) { - HTML l = new HTML( - "<em>Expand this node to show problems that may be dependent on this problem.</em>"); - errorDetails.add(l); - JsArray<ValueMap> suberrors = valueMap - .getJSValueMapArray("subErrors"); - for (int i = 0; i < suberrors.length(); i++) { - ValueMap value = suberrors.get(i); - printLayoutError(ac, value, errorNode); - } - - } - root.add(errorNode); - } - - private void findZeroSizeComponents( - Set<ComponentConnector> zeroHeightComponents, - Set<ComponentConnector> zeroWidthComponents, - ComponentConnector connector) { - Widget widget = connector.getWidget(); - ComputedStyle computedStyle = new ComputedStyle(widget.getElement()); - if (computedStyle.getIntProperty("height") == 0) { - zeroHeightComponents.add(connector); - } - if (computedStyle.getIntProperty("width") == 0) { - zeroWidthComponents.add(connector); - } - List<ServerConnector> children = connector.getChildren(); - for (ServerConnector serverConnector : children) { - if (serverConnector instanceof ComponentConnector) { - findZeroSizeComponents(zeroHeightComponents, - zeroWidthComponents, - (ComponentConnector) serverConnector); - } - } + // show the results of analyzeLayouts + analyzeLayoutsPanel.meta(ac, meta); } @Override @@ -561,60 +220,11 @@ public class HierarchySection implements Section { private void printState(ServerConnector connector, boolean serverDebug) { Highlight.showOnly(connector); if (serverDebug) { - Highlight.showServerDebugInfo(connector); + HierarchyPanel.showServerDebugInfo(connector); } - SharedState state = connector.getState(); - - Set<String> ignoreProperties = new HashSet<String>(); - ignoreProperties.add("id"); - - String html = getRowHTML("Id", connector.getConnectorId()); - html += getRowHTML("Connector", Util.getSimpleName(connector)); - - if (connector instanceof ComponentConnector) { - ComponentConnector component = (ComponentConnector) connector; - - ignoreProperties.addAll(Arrays.asList("caption", "description", - "width", "height")); - - AbstractComponentState componentState = component.getState(); - - html += getRowHTML("Widget", - Util.getSimpleName(component.getWidget())); - html += getRowHTML("Caption", componentState.caption); - html += getRowHTML("Description", componentState.description); - html += getRowHTML("Width", componentState.width + " (actual: " - + component.getWidget().getOffsetWidth() + "px)"); - html += getRowHTML("Height", componentState.height + " (actual: " - + component.getWidget().getOffsetHeight() + "px)"); - } - - try { - JsArrayObject<Property> properties = AbstractConnector - .getStateType(connector).getPropertiesAsArray(); - for (int i = 0; i < properties.size(); i++) { - Property property = properties.get(i); - String name = property.getName(); - if (!ignoreProperties.contains(name)) { - html += getRowHTML(property.getDisplayName(), - property.getValue(state)); - } - } - } catch (NoDataException e) { - html += "<div>Could not read state, error has been logged to the console</div>"; - VConsole.error(e); - } - - content.clear(); - content.add(new HTML(html)); - } - - private String getRowHTML(String caption, Object value) { - return "<div class=\"" + VDebugWindow.STYLENAME - + "-row\"><span class=\"caption\">" + caption - + "</span><span class=\"value\">" - + Util.escapeHTML(String.valueOf(value)) + "</span></div>"; + infoPanel.update(connector); + content.setWidget(infoPanel); } private final NativePreviewHandler highlightModeHandler = new NativePreviewHandler() { @@ -634,7 +244,7 @@ public class HierarchySection implements Section { .getNativeEvent().getClientX(), event.getNativeEvent() .getClientY()); if (VDebugWindow.get().getElement().isOrHasChild(eventTarget)) { - content.clear(); + infoPanel.clear(); return; } @@ -654,7 +264,7 @@ public class HierarchySection implements Section { return; } } - content.clear(); + infoPanel.clear(); } if (event.getTypeInt() == Event.ONCLICK) { Highlight.hideAll(); diff --git a/client/src/com/vaadin/client/debug/internal/Highlight.java b/client/src/com/vaadin/client/debug/internal/Highlight.java index 3c1af445a9..5ee3a25e2c 100644 --- a/client/src/com/vaadin/client/debug/internal/Highlight.java +++ b/client/src/com/vaadin/client/debug/internal/Highlight.java @@ -144,20 +144,55 @@ public class Highlight { */ static Element show(Widget widget, String color) { if (widget != null) { + show(widget.getElement(), color); + } + return null; + } + + /** + * Highlights the given {@link Element}. + * <p> + * Pass the returned {@link Element} to {@link #hide(Element)} to remove + * this particular highlight. + * </p> + * + * @param element + * Element to highlight + * @return Highlight element + */ + static Element show(Element element) { + return show(element, DEFAULT_COLOR); + } + + /** + * Highlight the given {@link Element} using the given color. + * <p> + * Pass the returned highlight {@link Element} to {@link #hide(Element)} to + * remove this particular highlight. + * </p> + * + * @param element + * Element to highlight + * @param color + * Color of highlight + * @return Highlight element + */ + static Element show(Element element, String color) { + if (element != null) { if (highlights == null) { highlights = new HashSet<Element>(); } Element highlight = DOM.createDiv(); Style style = highlight.getStyle(); - style.setTop(widget.getAbsoluteTop(), Unit.PX); - style.setLeft(widget.getAbsoluteLeft(), Unit.PX); - int width = widget.getOffsetWidth(); + style.setTop(element.getAbsoluteTop(), Unit.PX); + style.setLeft(element.getAbsoluteLeft(), Unit.PX); + int width = element.getOffsetWidth(); if (width < MIN_WIDTH) { width = MIN_WIDTH; } style.setWidth(width, Unit.PX); - int height = widget.getOffsetHeight(); + int height = element.getOffsetHeight(); if (height < MIN_HEIGHT) { height = MIN_HEIGHT; } @@ -207,19 +242,4 @@ public class Highlight { } } - /** - * Outputs debug information on the server - usually in the console of an - * IDE, with a clickable reference to the relevant code location. - * - * @since 7.1 - * @param connector - * show debug info for this connector - */ - static void showServerDebugInfo(ServerConnector connector) { - if (connector != null) { - connector.getConnection().getUIConnector() - .showServerDebugInfo(connector); - } - } - } diff --git a/client/src/com/vaadin/client/debug/internal/Icon.java b/client/src/com/vaadin/client/debug/internal/Icon.java index cc2ef3b348..9ef6d833e2 100644 --- a/client/src/com/vaadin/client/debug/internal/Icon.java +++ b/client/src/com/vaadin/client/debug/internal/Icon.java @@ -32,6 +32,8 @@ public enum Icon { LOG(""), // OPTIMIZE(""), // HIERARCHY(""), // + // TODO create more appropriate icon + SELECTOR("≣"), // MENU(""), // NETWORK(""), // ANALYZE(""), // diff --git a/client/src/com/vaadin/client/debug/internal/OptimizedWidgetsetPanel.java b/client/src/com/vaadin/client/debug/internal/OptimizedWidgetsetPanel.java new file mode 100644 index 0000000000..a8d8aad888 --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/OptimizedWidgetsetPanel.java @@ -0,0 +1,137 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import java.util.HashSet; +import java.util.Set; + +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.VConsole; +import com.vaadin.client.ui.UnknownComponentConnector; + +/** + * Optimized widgetset view panel of the debug window. + * + * @since 7.1.4 + */ +public class OptimizedWidgetsetPanel extends FlowPanel { + + /** + * Update the panel contents based on the connectors that have been used so + * far on this execution of the application. + */ + public void update() { + clear(); + HTML h = new HTML("Getting used connectors"); + add(h); + + String s = ""; + for (ApplicationConnection ac : ApplicationConfiguration + .getRunningApplications()) { + ApplicationConfiguration conf = ac.getConfiguration(); + s += "<h1>Used connectors for " + conf.getServiceUrl() + "</h1>"; + + for (String connectorName : getUsedConnectorNames(conf)) { + s += connectorName + "<br/>"; + } + + s += "<h2>To make an optimized widgetset based on these connectors, do:</h2>"; + s += "<h3>1. Add to your widgetset.gwt.xml file:</h2>"; + s += "<textarea rows=\"3\" style=\"width:90%\">"; + s += "<generate-with class=\"OptimizedConnectorBundleLoaderFactory\">\n"; + s += " <when-type-assignable class=\"com.vaadin.client.metadata.ConnectorBundleLoader\" />\n"; + s += "</generate-with>"; + s += "</textarea>"; + + s += "<h3>2. Add the following java file to your project:</h2>"; + s += "<textarea rows=\"5\" style=\"width:90%\">"; + s += generateOptimizedWidgetSet(getUsedConnectorNames(conf)); + s += "</textarea>"; + s += "<h3>3. Recompile widgetset</h2>"; + + } + + h.setHTML(s); + } + + private Set<String> getUsedConnectorNames( + ApplicationConfiguration configuration) { + int tag = 0; + Set<String> usedConnectors = new HashSet<String>(); + while (true) { + String serverSideClass = configuration + .getServerSideClassNameForTag(tag); + if (serverSideClass == null) { + break; + } + Class<? extends ServerConnector> connectorClass = configuration + .getConnectorClassByEncodedTag(tag); + if (connectorClass == null) { + break; + } + + if (connectorClass != UnknownComponentConnector.class) { + usedConnectors.add(connectorClass.getName()); + } + tag++; + if (tag > 10000) { + // Sanity check + VConsole.error("Search for used connector classes was forcefully terminated"); + break; + } + } + return usedConnectors; + } + + public String generateOptimizedWidgetSet(Set<String> usedConnectors) { + String s = "import java.util.HashSet;\n"; + s += "import java.util.Set;\n"; + + s += "import com.google.gwt.core.ext.typeinfo.JClassType;\n"; + s += "import com.vaadin.client.ui.ui.UIConnector;\n"; + s += "import com.vaadin.server.widgetsetutils.ConnectorBundleLoaderFactory;\n"; + s += "import com.vaadin.shared.ui.Connect.LoadStyle;\n\n"; + + s += "public class OptimizedConnectorBundleLoaderFactory extends\n"; + s += " ConnectorBundleLoaderFactory {\n"; + s += " private Set<String> eagerConnectors = new HashSet<String>();\n"; + s += " {\n"; + for (String c : usedConnectors) { + s += " eagerConnectors.add(" + c + + ".class.getName());\n"; + } + s += " }\n"; + s += "\n"; + s += " @Override\n"; + s += " protected LoadStyle getLoadStyle(JClassType connectorType) {\n"; + s += " if (eagerConnectors.contains(connectorType.getQualifiedBinaryName())) {\n"; + s += " return LoadStyle.EAGER;\n"; + s += " } else {\n"; + s += " // Loads all other connectors immediately after the initial view has\n"; + s += " // been rendered\n"; + s += " return LoadStyle.DEFERRED;\n"; + s += " }\n"; + s += " }\n"; + s += "}\n"; + + return s; + } + +} diff --git a/client/src/com/vaadin/client/debug/internal/SelectConnectorListener.java b/client/src/com/vaadin/client/debug/internal/SelectConnectorListener.java new file mode 100644 index 0000000000..409f9d14ce --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/SelectConnectorListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import com.google.gwt.user.client.Element; +import com.vaadin.client.ServerConnector; + +/** + * Listener for the selection of a connector in the debug window. + * + * @since 7.1.4 + */ +public interface SelectConnectorListener { + /** + * Listener method called when a connector has been selected. If a specific + * element of the connector was selected, it is also given. + * + * @param connector + * selected connector + * @param element + * selected element of the connector or null if unknown + */ + public void select(ServerConnector connector, Element element); +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/debug/internal/SelectorPath.java b/client/src/com/vaadin/client/debug/internal/SelectorPath.java new file mode 100644 index 0000000000..b8732e134e --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/SelectorPath.java @@ -0,0 +1,238 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.debug.internal; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gwt.user.client.Element; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.componentlocator.ComponentLocator; +import com.vaadin.client.componentlocator.SelectorPredicate; + +/** + * A single segment of a selector path pointing to an Element. + * <p> + * This class should be considered internal to the framework and may change at + * any time. + * <p> + * + * @since 7.1.x + */ +public class SelectorPath { + private final String path; + private final Element element; + private final ComponentLocator locator; + private static Map<String, Integer> counter = new HashMap<String, Integer>(); + private static Map<String, String> legacyNames = new HashMap<String, String>(); + + static { + legacyNames.put("FilterSelect", "ComboBox"); + legacyNames.put("ScrollTable", "Table"); + } + + protected SelectorPath(ServerConnector c, Element e) { + element = e; + locator = new ComponentLocator(c.getConnection()); + path = locator.getPathForElement(e); + } + + public String getPath() { + return path; + } + + public Element getElement() { + return element; + } + + public ComponentLocator getLocator() { + return locator; + } + + /** + * Generate ElementQuery code for Java. Fallback to By.vaadin(path) if + * dealing with LegacyLocator + * + * @return String containing Java code for finding the element described by + * path + */ + public String getElementQuery() { + if (locator.isValidForLegacyLocator(path)) { + return getLegacyLocatorQuery(); + } + + String[] fragments; + String tmpPath = path; + List<SelectorPredicate> postFilters = SelectorPredicate + .extractPostFilterPredicates(path); + if (postFilters.size() > 0) { + tmpPath = tmpPath.substring(1, tmpPath.lastIndexOf(')')); + } + + // Generate an ElementQuery + fragments = tmpPath.split("/"); + String elementQueryString; + int index = 0; + for (SelectorPredicate p : postFilters) { + if (p.getIndex() > 0) { + index = p.getIndex(); + } + } + if (index > 0) { + elementQueryString = ".get(" + index + ");"; + } else { + elementQueryString = ".first();"; + } + for (int i = 1; i < fragments.length; ++i) { + if (fragments[i].isEmpty()) { + // Recursive search has occasional empty fragments + continue; + } + + // Get Element.class -name + String queryFragment = ""; + String elementClass = getComponentName(fragments[i]) + + "Element.class"; + for (SelectorPredicate p : SelectorPredicate + .extractPredicates(fragments[i])) { + // Add in predicates like .caption and .id + queryFragment += "." + p.getName() + "(\"" + p.getValue() + + "\")"; + } + if (i == fragments.length - 1) { + // Last element in path. + queryFragment = "$(" + elementClass + ")" + queryFragment; + } else { + // If followed by an empty fragment search is recursive + boolean recursive = fragments[i + 1].isEmpty(); + if (recursive) { + queryFragment = ".in(" + elementClass + ")" + queryFragment; + } else { + queryFragment = ".childOf(" + elementClass + ")" + + queryFragment; + } + } + elementQueryString = queryFragment + elementQueryString; + } + + if (!tmpPath.startsWith("//")) { + elementQueryString = "$" + elementQueryString; + } + + // Return full Java variable assignment and eQuery + return generateJavaVariable(fragments[fragments.length - 1]) + + elementQueryString; + } + + /** + * @since + * @param frags + * @param i + * @return + */ + protected String getComponentName(String fragment) { + return fragment.split("\\[")[0]; + } + + /** + * Generates a legacy locator for SelectorPath. + * + * @return String containing Java code for element search and assignment + */ + private String getLegacyLocatorQuery() { + String[] frags = path.split("/"); + String name = getComponentName(frags[frags.length - 1]).substring(1); + + if (legacyNames.containsKey(name)) { + name = legacyNames.get(name); + } + + name = getNameWithCount(name); + + // Use direct path and elementX naming style. + return "WebElement " + name.substring(0, 1).toLowerCase() + + name.substring(1) + " = getDriver().findElement(By.vaadin(\"" + + path + "\"));"; + } + + /** + * Get variable name with counter for given component name. + * + * @param name + * Component name + * @return name followed by count + */ + protected String getNameWithCount(String name) { + if (!counter.containsKey(name)) { + counter.put(name, 0); + } + counter.put(name, counter.get(name) + 1); + name += counter.get(name); + return name; + } + + /** + * Generate Java variable assignment from given selector fragment + * + * @param pathFragment + * Selector fragment + * @return piece of java code + */ + private String generateJavaVariable(String pathFragment) { + // Get element type and predicates from fragment + List<SelectorPredicate> predicates = SelectorPredicate + .extractPredicates(pathFragment); + String elementType = pathFragment.split("\\[")[0]; + String name = getNameFromPredicates(predicates, elementType); + + if (name.equals(elementType)) { + name = getNameWithCount(name); + } + + // Replace unusable characters + name = name.replaceAll("\\W", ""); + + // Lowercase the first character of name + return elementType + "Element " + name.substring(0, 1).toLowerCase() + + name.substring(1) + " = "; + } + + /** + * Get variable name based on predicates. Fallback to elementType + * + * @param predicates + * Predicates related to element + * @param elementType + * Element type + * @return name for Variable + */ + private String getNameFromPredicates(List<SelectorPredicate> predicates, + String elementType) { + String name = elementType; + for (SelectorPredicate p : predicates) { + if ("caption".equals(p.getName())) { + // Caption + elementType is a suitable name + name = p.getValue() + elementType; + } else if ("id".equals(p.getName())) { + // Just id. This is unique, use it. + return p.getValue(); + } + } + return name; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/debug/internal/TestBenchSection.java b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java new file mode 100644 index 0000000000..5be75f2003 --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java @@ -0,0 +1,281 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.KeyCodes; +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.shared.HandlerRegistration; +import com.google.gwt.user.client.Element; +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.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.Util; +import com.vaadin.client.ValueMap; + +/** + * Provides functionality for picking selectors for Vaadin TestBench. + * + * @since 7.1.x + * @author Vaadin Ltd + */ +public class TestBenchSection implements Section { + + /** + * Selector widget showing a selector in a program-usable form. + */ + private static class SelectorWidget extends HTML implements + MouseOverHandler, MouseOutHandler { + private final SelectorPath path; + + public SelectorWidget(final SelectorPath path) { + this.path = path; + + String html = "<div class=\"" + VDebugWindow.STYLENAME + + "-selector\"><span class=\"tb-selector\">" + + Util.escapeHTML(path.getElementQuery()) + "</span></div>"; + setHTML(html); + + addMouseOverHandler(this); + addMouseOutHandler(this); + } + + @Override + public void onMouseOver(MouseOverEvent event) { + Highlight.hideAll(); + + Element element = path.getElement(); + if (null != element) { + Highlight.show(element); + } + } + + @Override + public void onMouseOut(MouseOutEvent event) { + Highlight.hideAll(); + } + } + + private final DebugButton tabButton = new DebugButton(Icon.WARNING, + "Pick Vaadin TestBench selectors"); + + private final FlowPanel content = new FlowPanel(); + + private final FlowPanel selectorPanel = new FlowPanel(); + // map from full path to SelectorWidget to enable reuse of old selectors + private Map<SelectorPath, SelectorWidget> selectorWidgets = new HashMap<SelectorPath, SelectorWidget>(); + + private final FlowPanel controls = new FlowPanel(); + + private final Button find = new DebugButton(Icon.HIGHLIGHT, + "Pick an element and generate a query for it"); + + private final Button clear = new DebugButton(Icon.CLEAR, + "Clear current elements"); + + private HandlerRegistration highlightModeRegistration = null; + + public TestBenchSection() { + + controls.add(find); + find.setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + find.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + toggleFind(); + } + }); + + controls.add(clear); + clear.setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + clear.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + clearResults(); + } + }); + + content.setStylePrimaryName(VDebugWindow.STYLENAME + "-testbench"); + content.add(selectorPanel); + } + + @Override + public DebugButton getTabButton() { + return tabButton; + } + + @Override + public Widget getControls() { + return controls; + } + + @Override + public Widget getContent() { + return content; + } + + @Override + public void show() { + + } + + @Override + public void hide() { + stopFind(); + } + + @Override + public void meta(ApplicationConnection ac, ValueMap meta) { + // NOP + } + + @Override + public void uidl(ApplicationConnection ac, ValueMap uidl) { + // NOP + } + + private boolean isFindMode() { + return (highlightModeRegistration != null); + } + + private void toggleFind() { + if (isFindMode()) { + stopFind(); + } else { + startFind(); + } + } + + private void startFind() { + Highlight.hideAll(); + if (!isFindMode()) { + highlightModeRegistration = Event + .addNativePreviewHandler(highlightModeHandler); + find.addStyleDependentName(VDebugWindow.STYLENAME_ACTIVE); + } + } + + private void stopFind() { + if (isFindMode()) { + highlightModeRegistration.removeHandler(); + highlightModeRegistration = null; + find.removeStyleDependentName(VDebugWindow.STYLENAME_ACTIVE); + } + Highlight.hideAll(); + } + + private void pickSelector(ServerConnector connector, Element element) { + + SelectorPath p = new SelectorPath(connector, Util + .findPaintable(connector.getConnection(), element).getWidget() + .getElement()); + SelectorWidget w = new SelectorWidget(p); + + content.add(w); + } + + private final NativePreviewHandler highlightModeHandler = new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + + if (event.getTypeInt() == Event.ONKEYDOWN + && event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) { + stopFind(); + Highlight.hideAll(); + return; + } + if (event.getTypeInt() == Event.ONMOUSEMOVE + || event.getTypeInt() == Event.ONCLICK) { + Element eventTarget = Util.getElementFromPoint(event + .getNativeEvent().getClientX(), event.getNativeEvent() + .getClientY()); + if (VDebugWindow.get().getElement().isOrHasChild(eventTarget)) { + if (isFindMode() && event.getTypeInt() == Event.ONCLICK) { + stopFind(); + event.cancel(); + } + return; + } + + // make sure that not finding the highlight element only + Highlight.hideAll(); + + eventTarget = Util.getElementFromPoint(event.getNativeEvent() + .getClientX(), event.getNativeEvent().getClientY()); + ComponentConnector connector = findConnector(eventTarget); + + if (event.getTypeInt() == Event.ONMOUSEMOVE) { + if (connector != null) { + Highlight.showOnly(connector); + event.cancel(); + event.consume(); + event.getNativeEvent().stopPropagation(); + return; + } + } else if (event.getTypeInt() == Event.ONCLICK) { + event.cancel(); + event.consume(); + event.getNativeEvent().stopPropagation(); + if (connector != null) { + Highlight.showOnly(connector); + pickSelector(connector, eventTarget); + return; + } + } + } + event.cancel(); + } + + }; + + private ComponentConnector findConnector(Element element) { + for (ApplicationConnection a : ApplicationConfiguration + .getRunningApplications()) { + ComponentConnector connector = Util.getConnectorForElement(a, a + .getUIConnector().getWidget(), element); + if (connector == null) { + connector = Util.getConnectorForElement(a, RootPanel.get(), + element); + } + if (connector != null) { + return connector; + } + } + return null; + } + + private void clearResults() { + content.clear(); + } + +} diff --git a/client/src/com/vaadin/client/metadata/ConnectorBundleLoader.java b/client/src/com/vaadin/client/metadata/ConnectorBundleLoader.java index f1a9fa1ee7..8148010b54 100644 --- a/client/src/com/vaadin/client/metadata/ConnectorBundleLoader.java +++ b/client/src/com/vaadin/client/metadata/ConnectorBundleLoader.java @@ -15,11 +15,10 @@ */ package com.vaadin.client.metadata; -import java.util.HashMap; import java.util.List; -import java.util.Map; import com.google.gwt.core.shared.GWT; +import com.vaadin.client.FastStringMap; import com.vaadin.client.metadata.AsyncBundleLoader.State; public abstract class ConnectorBundleLoader { @@ -28,8 +27,9 @@ public abstract class ConnectorBundleLoader { private static ConnectorBundleLoader impl; - private Map<String, AsyncBundleLoader> asyncBlockLoaders = new HashMap<String, AsyncBundleLoader>(); - private Map<String, String> identifierToBundle = new HashMap<String, String>(); + private FastStringMap<AsyncBundleLoader> asyncBlockLoaders = FastStringMap + .create(); + private FastStringMap<String> identifierToBundle = FastStringMap.create(); private final TypeDataStore datStore = new TypeDataStore(); diff --git a/client/src/com/vaadin/client/metadata/Property.java b/client/src/com/vaadin/client/metadata/Property.java index 2e0ea49c88..64fbb79ca1 100644 --- a/client/src/com/vaadin/client/metadata/Property.java +++ b/client/src/com/vaadin/client/metadata/Property.java @@ -30,11 +30,11 @@ public class Property { } public Object getValue(Object bean) throws NoDataException { - return TypeDataStore.getGetter(this).invoke(bean); + return TypeDataStore.getValue(this, bean); } public void setValue(Object bean, Object value) throws NoDataException { - TypeDataStore.getSetter(this).invoke(bean, value); + TypeDataStore.setValue(this, bean, value); } public String getDelegateToWidgetMethodName() { @@ -50,6 +50,10 @@ public class Property { return TypeDataStore.getType(this); } + public Type getBeanType() { + return bean; + } + /** * The unique signature used to identify this property. The structure of the * returned string may change without notice and should not be used for any diff --git a/client/src/com/vaadin/client/metadata/TypeDataStore.java b/client/src/com/vaadin/client/metadata/TypeDataStore.java index aa37d75dc8..a3939b7994 100644 --- a/client/src/com/vaadin/client/metadata/TypeDataStore.java +++ b/client/src/com/vaadin/client/metadata/TypeDataStore.java @@ -34,8 +34,6 @@ public class TypeDataStore { .create(); private final FastStringMap<ProxyHandler> proxyHandlers = FastStringMap .create(); - private final FastStringMap<JsArrayObject<Property>> properties = FastStringMap - .create(); private final FastStringMap<JsArrayString> delegateToWidgetProperties = FastStringMap .create(); @@ -46,12 +44,11 @@ public class TypeDataStore { private final FastStringMap<Invoker> invokers = FastStringMap.create(); private final FastStringMap<Type[]> paramTypes = FastStringMap.create(); - private final FastStringMap<Type> propertyTypes = FastStringMap.create(); - private final FastStringMap<Invoker> setters = FastStringMap.create(); - private final FastStringMap<Invoker> getters = FastStringMap.create(); private final FastStringMap<String> delegateToWidget = FastStringMap .create(); + private final JavaScriptObject jsTypeData = JavaScriptObject.createObject(); + public static TypeDataStore get() { return ConnectorBundleLoader.get().getTypeDataStore(); } @@ -69,6 +66,22 @@ public class TypeDataStore { return class1; } + // this is a very inefficient implementation for getting all the identifiers + // for a class + public FastStringSet findIdentifiersFor(Class<?> type) { + FastStringSet result = FastStringSet.create(); + + JsArrayString keys = identifiers.getKeys(); + for (int i = 0; i < keys.length(); i++) { + String key = keys.get(i); + if (identifiers.get(key) == type) { + result.add(key); + } + } + + return result; + } + public static Type getType(Class<?> clazz) { return new Type(clazz); } @@ -101,19 +114,10 @@ public class TypeDataStore { return invoker; } - public static Invoker getGetter(Property property) throws NoDataException { - Invoker getter = get().getters.get(property.getSignature()); - if (getter == null) { - throw new NoDataException("There is no getter for " - + property.getSignature()); - } - - return getter; - } - - public void setGetter(Class<?> clazz, String propertyName, Invoker invoker) { - getters.put(new Property(getType(clazz), propertyName).getSignature(), - invoker); + public static Object getValue(Property property, Object target) + throws NoDataException { + return getJsPropertyValue(get().jsTypeData, property.getBeanType() + .getBaseTypeName(), property.getName(), target); } public static String getDelegateToWidget(Property property) { @@ -227,51 +231,31 @@ public class TypeDataStore { public static JsArrayObject<Property> getPropertiesAsArray(Type type) throws NoDataException { - JsArrayObject<Property> properties = get().properties.get(type - .getSignature()); - if (properties == null) { - throw new NoDataException("No property list for " - + type.getSignature()); - } - return properties; - } + JsArrayString names = getJsPropertyNames(get().jsTypeData, + type.getBaseTypeName()); - public void setProperties(Class<?> clazz, String[] propertyNames) { + // Create Property instances for each property name JsArrayObject<Property> properties = JavaScriptObject.createArray() .cast(); - Type type = getType(clazz); - for (String name : propertyNames) { - properties.add(new Property(type, name)); + for (int i = 0; i < names.length(); i++) { + properties.add(new Property(type, names.get(i))); } - this.properties.put(type.getSignature(), properties); - } - public static Type getType(Property property) throws NoDataException { - Type type = get().propertyTypes.get(property.getSignature()); - if (type == null) { - throw new NoDataException("No return type for " - + property.getSignature()); - } - return type; + return properties; } - public void setPropertyType(Class<?> clazz, String propertName, Type type) { - propertyTypes.put( - new Property(getType(clazz), propertName).getSignature(), type); + public static Type getType(Property property) throws NoDataException { + return getJsPropertyType(get().jsTypeData, property.getBeanType() + .getBaseTypeName(), property.getName()); } - public static Invoker getSetter(Property property) throws NoDataException { - Invoker setter = get().setters.get(property.getSignature()); - if (setter == null) { - throw new NoDataException("No setter for " - + property.getSignature()); - } - return setter; + public void setPropertyType(Class<?> clazz, String propertyName, Type type) { + setJsPropertyType(jsTypeData, clazz.getName(), propertyName, type); } - public void setSetter(Class<?> clazz, String propertyName, Invoker setter) { - setters.put(new Property(getType(clazz), propertyName).getSignature(), - setter); + public static void setValue(Property property, Object target, Object value) { + setJsPropertyValue(get().jsTypeData, property.getBeanType() + .getBaseTypeName(), property.getName(), target, value); } public void setSerializerFactory(Class<?> clazz, Invoker factory) { @@ -288,6 +272,99 @@ public class TypeDataStore { } public static boolean hasProperties(Type type) { - return get().properties.containsKey(type.getSignature()); + return hasJsProperties(get().jsTypeData, type.getBaseTypeName()); } + + public void setSuperClass(Class<?> baseClass, Class<?> superClass) { + String superClassName = superClass == null ? null : superClass + .getName(); + setSuperClass(jsTypeData, baseClass.getName(), superClassName); + } + + public void setPropertyData(Class<?> type, String propertyName, + JavaScriptObject propertyData) { + setPropertyData(jsTypeData, type.getName(), propertyName, propertyData); + } + + private static native void setPropertyData(JavaScriptObject typeData, + String className, String propertyName, JavaScriptObject propertyData) + /*-{ + typeData[className][propertyName] = propertyData; + }-*/; + + /* + * This method sets up prototypes chain for <code>baseClassName</code>. + * Precondition is : <code>superClassName</code> had to be handled before + * its child <code>baseClassName</code>. + * + * It makes all properties defined in the <code>superClassName</code> + * available for <code>baseClassName</code> as well. + */ + private static native void setSuperClass(JavaScriptObject typeData, + String baseClassName, String superClassName) + /*-{ + var parentType = typeData[superClassName]; + if (parentType !== undefined ){ + var ctor = function () {}; + ctor.prototype = parentType; + typeData[baseClassName] = new ctor; + } + else { + typeData[baseClassName] = {}; + } + }-*/; + + private static native boolean hasGetter(JavaScriptObject typeData, + String beanName, String propertyName) + /*-{ + return typeData[beanName][propertyName].getter !== undefined; + }-*/; + + private static native boolean hasSetter(JavaScriptObject typeData, + String beanName, String propertyName) + /*-{ + return typeData[beanName][propertyName].setter !== undefined; + }-*/; + + private static native Object getJsPropertyValue(JavaScriptObject typeData, + String beanName, String propertyName, Object beanInstance) + /*-{ + return typeData[beanName][propertyName].getter(beanInstance); + }-*/; + + private static native void setJsPropertyValue(JavaScriptObject typeData, + String beanName, String propertyName, Object beanInstance, + Object value) + /*-{ + typeData[beanName][propertyName].setter(beanInstance, value); + }-*/; + + private static native Type getJsPropertyType(JavaScriptObject typeData, + String beanName, String propertyName) + /*-{ + return typeData[beanName][propertyName].type; + }-*/; + + private static native void setJsPropertyType(JavaScriptObject typeData, + String beanName, String propertyName, Type type) + /*-{ + typeData[beanName][propertyName].type = type; + }-*/; + + private static native JsArrayString getJsPropertyNames( + JavaScriptObject typeData, String beanName) + /*-{ + var names = []; + for(var name in typeData[beanName]) { + names.push(name); + } + return names; + }-*/; + + private static native boolean hasJsProperties(JavaScriptObject typeData, + String beanName) + /*-{ + return typeData[beanName] !== undefined ; + }-*/; + } diff --git a/client/src/com/vaadin/client/ui/SubPartAware.java b/client/src/com/vaadin/client/ui/SubPartAware.java index a7d72fab01..36959e7b1f 100644 --- a/client/src/com/vaadin/client/ui/SubPartAware.java +++ b/client/src/com/vaadin/client/ui/SubPartAware.java @@ -17,7 +17,7 @@ package com.vaadin.client.ui; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.ui.Widget; -import com.vaadin.client.ComponentLocator; +import com.vaadin.client.componentlocator.ComponentLocator; /** * Interface implemented by {@link Widget}s which can provide identifiers for at @@ -59,4 +59,4 @@ public interface SubPartAware { */ String getSubPartName(Element subElement); -}
\ No newline at end of file +} diff --git a/client/src/com/vaadin/client/ui/UnknownComponentConnector.java b/client/src/com/vaadin/client/ui/UnknownComponentConnector.java index ca461eb640..b9b0388d9a 100644 --- a/client/src/com/vaadin/client/ui/UnknownComponentConnector.java +++ b/client/src/com/vaadin/client/ui/UnknownComponentConnector.java @@ -16,6 +16,8 @@ package com.vaadin.client.ui; +import com.google.gwt.core.client.GWT; + public class UnknownComponentConnector extends AbstractComponentConnector { @Override @@ -31,7 +33,9 @@ public class UnknownComponentConnector extends AbstractComponentConnector { public void setServerSideClassName(String serverClassName) { getWidget() .setCaption( - "Widgetset does not contain implementation for " + "Widgetset '" + + GWT.getModuleName() + + "' does not contain implementation for " + serverClassName + ". Check its component connector's @Connect mapping, widgetsets " + "GWT module description file and re-compile your" diff --git a/client/src/com/vaadin/client/ui/VAccordion.java b/client/src/com/vaadin/client/ui/VAccordion.java index f87186fe37..ddfe9dbc2c 100644 --- a/client/src/com/vaadin/client/ui/VAccordion.java +++ b/client/src/com/vaadin/client/ui/VAccordion.java @@ -463,7 +463,6 @@ public class VAccordion extends VTabsheetBase { } @Override - @SuppressWarnings("unchecked") public Iterator<Widget> getWidgetIterator() { return widgets.iterator(); } diff --git a/client/src/com/vaadin/client/ui/VCalendarPanel.java b/client/src/com/vaadin/client/ui/VCalendarPanel.java index 96678fd133..b043cd0ab7 100644 --- a/client/src/com/vaadin/client/ui/VCalendarPanel.java +++ b/client/src/com/vaadin/client/ui/VCalendarPanel.java @@ -170,8 +170,6 @@ public class VCalendarPanel extends FocusableFlexTable implements private Resolution resolution = Resolution.YEAR; - private int focusedRow; - private Timer mouseTimer; private Date value; @@ -256,7 +254,6 @@ public class VCalendarPanel extends FocusableFlexTable implements if (curday.getDate().equals(date)) { curday.addStyleDependentName(CN_FOCUSED); focusedDay = curday; - focusedRow = i; return; } } @@ -741,7 +738,6 @@ public class VCalendarPanel extends FocusableFlexTable implements } if (curr.equals(focusedDate)) { focusedDay = day; - focusedRow = weekOfMonth; if (hasFocus) { day.addStyleDependentName(CN_FOCUSED); } @@ -1795,10 +1791,8 @@ public class VCalendarPanel extends FocusableFlexTable implements * Updates the valus to correspond to the values in value */ public void updateTimes() { - boolean selected = true; if (value == null) { value = new Date(); - selected = false; } if (getDateTimeService().isTwelveHourClock()) { int h = value.getHours(); @@ -1833,10 +1827,6 @@ public class VCalendarPanel extends FocusableFlexTable implements } - private int getMilliseconds() { - return DateTimeService.getMilliseconds(value); - } - private DateTimeService getDateTimeService() { if (dateTimeService == null) { dateTimeService = new DateTimeService(); @@ -2034,7 +2024,6 @@ public class VCalendarPanel extends FocusableFlexTable implements private static final String SUBPART_HOUR_SELECT = "h"; private static final String SUBPART_MINUTE_SELECT = "m"; private static final String SUBPART_SECS_SELECT = "s"; - private static final String SUBPART_MSECS_SELECT = "ms"; private static final String SUBPART_AMPM_SELECT = "ampm"; private static final String SUBPART_DAY = "day"; private static final String SUBPART_MONTH_YEAR_HEADER = "header"; diff --git a/client/src/com/vaadin/client/ui/VFilterSelect.java b/client/src/com/vaadin/client/ui/VFilterSelect.java index 7efb5b8867..9bace9141c 100644 --- a/client/src/com/vaadin/client/ui/VFilterSelect.java +++ b/client/src/com/vaadin/client/ui/VFilterSelect.java @@ -1988,6 +1988,8 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, return tb.getElement(); } else if ("button".equals(subPart)) { return popupOpener.getElement(); + } else if ("popup".equals(subPart) && suggestionPopup.isAttached()) { + return suggestionPopup.getElement(); } return null; } @@ -1998,6 +2000,8 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, return "textbox"; } else if (popupOpener.getElement().isOrHasChild(subElement)) { return "button"; + } else if (suggestionPopup.getElement().isOrHasChild(subElement)) { + return "popup"; } return null; } diff --git a/client/src/com/vaadin/client/ui/VLabel.java b/client/src/com/vaadin/client/ui/VLabel.java index 8acd653778..35f47d540a 100644 --- a/client/src/com/vaadin/client/ui/VLabel.java +++ b/client/src/com/vaadin/client/ui/VLabel.java @@ -18,7 +18,6 @@ package com.vaadin.client.ui; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.HTML; -import com.vaadin.client.ApplicationConnection; import com.vaadin.client.BrowserInfo; import com.vaadin.client.Util; import com.vaadin.client.VTooltip; @@ -28,8 +27,6 @@ public class VLabel extends HTML { public static final String CLASSNAME = "v-label"; private static final String CLASSNAME_UNDEFINED_WIDTH = "v-label-undef-w"; - private ApplicationConnection connection; - public VLabel() { super(); setStyleName(CLASSNAME); @@ -71,9 +68,4 @@ public class VLabel extends HTML { super.setText(text); } } - - /** For internal use only. May be removed or replaced in the future. */ - public void setConnection(ApplicationConnection client) { - connection = client; - } } diff --git a/client/src/com/vaadin/client/ui/VOverlay.java b/client/src/com/vaadin/client/ui/VOverlay.java index 9f84c16020..545af2ce83 100644 --- a/client/src/com/vaadin/client/ui/VOverlay.java +++ b/client/src/com/vaadin/client/ui/VOverlay.java @@ -241,10 +241,10 @@ public class VOverlay extends PopupPanel implements CloseHandler<PopupPanel> { private void removeShadowIfPresent() { if (isShadowAttached()) { - shadow.removeFromParent(); - // Remove event listener from the shadow unsinkShadowEvents(); + + shadow.removeFromParent(); } } diff --git a/client/src/com/vaadin/client/ui/VPanel.java b/client/src/com/vaadin/client/ui/VPanel.java index 6b02f079d1..ffeacade46 100644 --- a/client/src/com/vaadin/client/ui/VPanel.java +++ b/client/src/com/vaadin/client/ui/VPanel.java @@ -170,7 +170,6 @@ public class VPanel extends SimplePanel implements ShortcutActionHandlerOwner, public void onBrowserEvent(Event event) { super.onBrowserEvent(event); - final Element target = DOM.eventGetTarget(event); final int type = DOM.eventGetType(event); if (type == Event.ONKEYDOWN && shortcutHandler != null) { shortcutHandler.handleKeyboardEvent(event); diff --git a/client/src/com/vaadin/client/ui/VScrollTable.java b/client/src/com/vaadin/client/ui/VScrollTable.java index 8bd875690b..bbf06bfec1 100644 --- a/client/src/com/vaadin/client/ui/VScrollTable.java +++ b/client/src/com/vaadin/client/ui/VScrollTable.java @@ -61,6 +61,8 @@ import com.google.gwt.event.dom.client.ScrollHandler; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.regexp.shared.MatchResult; +import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; @@ -123,7 +125,7 @@ import com.vaadin.shared.ui.table.TableConstants; */ public class VScrollTable extends FlowPanel implements HasWidgets, ScrollHandler, VHasDropHandler, FocusHandler, BlurHandler, Focusable, - ActionOwner { + ActionOwner, SubPartAware { public static final String STYLENAME = "v-table"; @@ -991,6 +993,12 @@ public class VScrollTable extends FlowPanel implements HasWidgets, if (scrollBody != null) { scrollBody.removeFromParent(); } + + // Without this call the scroll position is messed up in IE even after + // the lazy scroller has set the scroll position to the first visible + // item + scrollBodyPanel.getScrollPosition(); + scrollBody = createScrollBody(); scrollBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"), @@ -1054,6 +1062,8 @@ public class VScrollTable extends FlowPanel implements HasWidgets, if (uidl.hasVariable("selected")) { final Set<String> selectedKeys = uidl .getStringArrayVariableAsSet("selected"); + removeUnselectedRowKeys(selectedKeys); + if (scrollBody != null) { Iterator<Widget> iterator = scrollBody.iterator(); while (iterator.hasNext()) { @@ -1096,6 +1106,16 @@ public class VScrollTable extends FlowPanel implements HasWidgets, return keyboardSelectionOverRowFetchInProgress; } + private void removeUnselectedRowKeys(final Set<String> selectedKeys) { + List<String> unselectedKeys = new ArrayList<String>(0); + for (String key : selectedRowKeys) { + if (!selectedKeys.contains(key)) { + unselectedKeys.add(key); + } + } + selectedRowKeys.removeAll(unselectedKeys); + } + /** For internal use only. May be removed or replaced in the future. */ public void updateSortingProperties(UIDL uidl) { oldSortColumn = sortColumn; @@ -1121,7 +1141,28 @@ public class VScrollTable extends FlowPanel implements HasWidgets, } } + private boolean lazyScrollerIsActive; + + private void disableLazyScroller() { + lazyScrollerIsActive = false; + scrollBodyPanel.getElement().getStyle().clearOverflowX(); + scrollBodyPanel.getElement().getStyle().clearOverflowY(); + } + + private void enableLazyScroller() { + Scheduler.get().scheduleDeferred(lazyScroller); + lazyScrollerIsActive = true; + // prevent scrolling to jump in IE11 + scrollBodyPanel.getElement().getStyle().setOverflowX(Overflow.HIDDEN); + scrollBodyPanel.getElement().getStyle().setOverflowY(Overflow.HIDDEN); + } + + private boolean isLazyScrollerActive() { + return lazyScrollerIsActive; + } + private ScheduledCommand lazyScroller = new ScheduledCommand() { + @Override public void execute() { if (firstvisible > 0) { @@ -1134,6 +1175,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets, .setScrollPosition(measureRowHeightOffset(firstvisible)); } } + disableLazyScroller(); } }; @@ -1152,7 +1194,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets, // Only scroll if the first visible changes from the server side. // Else we might unintentionally scroll even when the scroll // position has not changed. - Scheduler.get().scheduleDeferred(lazyScroller); + enableLazyScroller(); } } @@ -2172,7 +2214,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets, isNewBody = false; if (firstvisible > 0) { - Scheduler.get().scheduleDeferred(lazyScroller); + enableLazyScroller(); } if (enabled) { @@ -5056,6 +5098,20 @@ public class VScrollTable extends FlowPanel implements HasWidgets, } } + public int indexOf(Widget row) { + int relIx = -1; + for (int ix = 0; ix < renderedRows.size(); ix++) { + if (renderedRows.get(ix) == row) { + relIx = ix; + break; + } + } + if (relIx >= 0) { + return this.firstRendered + relIx; + } + return -1; + } + public class VScrollTableRow extends Panel implements ActionOwner { private static final int TOUCHSCROLL_TIMEOUT = 100; @@ -6037,7 +6093,6 @@ public class VScrollTable extends FlowPanel implements HasWidgets, private Element getEventTargetTdOrTr(Event event) { final Element eventTarget = event.getEventTarget().cast(); Widget widget = Util.findWidget(eventTarget, null); - final Element thisTrElement = getElement(); if (widget != this) { /* @@ -6884,6 +6939,12 @@ public class VScrollTable extends FlowPanel implements HasWidgets, @Override public void onScroll(ScrollEvent event) { + // Do not handle scroll events while there is scroll initiated from + // server side which is not yet executed (#11454) + if (isLazyScrollerActive()) { + return; + } + scrollLeft = scrollBodyPanel.getElement().getScrollLeft(); scrollTop = scrollBodyPanel.getScrollPosition(); /* @@ -6932,8 +6993,9 @@ public class VScrollTable extends FlowPanel implements HasWidgets, } firstRowInViewPort = calcFirstRowInViewPort(); - if (firstRowInViewPort > totalRows - pageLength) { - firstRowInViewPort = totalRows - pageLength; + int maxFirstRow = totalRows - pageLength; + if (firstRowInViewPort > maxFirstRow && maxFirstRow >= 0) { + firstRowInViewPort = maxFirstRow; } int postLimit = (int) (firstRowInViewPort + (pageLength - 1) + pageLength @@ -7764,4 +7826,94 @@ public class VScrollTable extends FlowPanel implements HasWidgets, return this; } + private static final String SUBPART_HEADER = "header"; + private static final String SUBPART_FOOTER = "footer"; + private static final String SUBPART_ROW = "row"; + private static final String SUBPART_COL = "col"; + /** Matches header[ix] - used for extracting the index of the targeted header cell */ + private static final RegExp SUBPART_HEADER_REGEXP = RegExp + .compile(SUBPART_HEADER + "\\[(\\d+)\\]"); + /** Matches footer[ix] - used for extracting the index of the targeted footer cell */ + private static final RegExp SUBPART_FOOTER_REGEXP = RegExp + .compile(SUBPART_FOOTER + "\\[(\\d+)\\]"); + /** Matches row[ix] - used for extracting the index of the targeted row */ + private static final RegExp SUBPART_ROW_REGEXP = RegExp.compile(SUBPART_ROW + + "\\[(\\d+)]"); + /** Matches col[ix] - used for extracting the index of the targeted column */ + private static final RegExp SUBPART_ROW_COL_REGEXP = RegExp + .compile(SUBPART_ROW + "\\[(\\d+)\\]/" + SUBPART_COL + "\\[(\\d+)\\]"); + + @Override + public Element getSubPartElement(String subPart) { + if (SUBPART_ROW_COL_REGEXP.test(subPart)) { + MatchResult result = SUBPART_ROW_COL_REGEXP.exec(subPart); + int rowIx = Integer.valueOf(result.getGroup(1)); + int colIx = Integer.valueOf(result.getGroup(2)); + VScrollTableRow row = scrollBody.getRowByRowIndex(rowIx); + if (row != null) { + Element rowElement = row.getElement(); + if (colIx < rowElement.getChildCount()) { + return rowElement.getChild(colIx).getFirstChild().cast(); + } + } + + } else if (SUBPART_ROW_REGEXP.test(subPart)) { + MatchResult result = SUBPART_ROW_REGEXP.exec(subPart); + int rowIx = Integer.valueOf(result.getGroup(1)); + VScrollTableRow row = scrollBody.getRowByRowIndex(rowIx); + if (row != null) { + return row.getElement(); + } + + } else if (SUBPART_HEADER_REGEXP.test(subPart)) { + MatchResult result = SUBPART_HEADER_REGEXP.exec(subPart); + int headerIx = Integer.valueOf(result.getGroup(1)); + HeaderCell headerCell = tHead.getHeaderCell(headerIx); + if (headerCell != null) { + return headerCell.getElement(); + } + + } else if (SUBPART_FOOTER_REGEXP.test(subPart)) { + MatchResult result = SUBPART_FOOTER_REGEXP.exec(subPart); + int footerIx = Integer.valueOf(result.getGroup(1)); + FooterCell footerCell = tFoot.getFooterCell(footerIx); + if (footerCell != null) { + return footerCell.getElement(); + } + } + // Nothing found. + return null; + } + + @Override + public String getSubPartName(Element subElement) { + Widget widget = Util.findWidget(subElement, null); + if (widget instanceof HeaderCell) { + return SUBPART_HEADER + "[" + tHead.visibleCells.indexOf(widget) + + "]"; + } else if (widget instanceof FooterCell) { + return SUBPART_FOOTER + "[" + tFoot.visibleCells.indexOf(widget) + + "]"; + } else if (widget instanceof VScrollTableRow) { + // a cell in a row + VScrollTableRow row = (VScrollTableRow) widget; + int rowIx = scrollBody.indexOf(row); + if (rowIx >= 0) { + int colIx = -1; + for (int ix = 0; ix < row.getElement().getChildCount(); ix++) { + if (row.getElement().getChild(ix).isOrHasChild(subElement)) { + colIx = ix; + break; + } + } + if (colIx >= 0) { + return SUBPART_ROW + "[" + rowIx + "]/" + SUBPART_COL + "[" + + colIx + "]"; + } + return SUBPART_ROW + "[" + rowIx + "]"; + } + } + // Nothing found. + return null; + } } diff --git a/client/src/com/vaadin/client/ui/VTabsheet.java b/client/src/com/vaadin/client/ui/VTabsheet.java index 9ad518b85b..82eb9e7694 100644 --- a/client/src/com/vaadin/client/ui/VTabsheet.java +++ b/client/src/com/vaadin/client/ui/VTabsheet.java @@ -135,7 +135,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable, SelectedValue.FALSE); div = DOM.createDiv(); - focusImpl.setTabIndex(td, -1); + setTabulatorIndex(-1); setStyleName(div, DIV_CLASSNAME); DOM.appendChild(td, div); @@ -213,7 +213,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable, } public void setTabulatorIndex(int tabIndex) { - focusImpl.setTabIndex(td, tabIndex); + getElement().setTabIndex(tabIndex); } public boolean isClosable() { @@ -313,11 +313,9 @@ public class VTabsheet extends VTabsheetBase implements Focusable, private boolean closable = false; private Element closeButton; private Tab tab; - private ApplicationConnection client; TabCaption(Tab tab, ApplicationConnection client) { super(client); - this.client = client; this.tab = tab; AriaHelper.ensureHasId(getElement()); @@ -488,6 +486,9 @@ public class VTabsheet extends VTabsheetBase implements Focusable, int index = getWidgetIndex(caption.getParent()); + navigateTab(getTabsheet().focusedTabIndex, index); + getTabsheet().focusedTabIndex = index; + getTabsheet().focusedTab = getTab(index); getTabsheet().focus(); getTabsheet().loadTabSheet(index); } @@ -702,15 +703,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable, if (activeTabIndex != tabIndex && canSelectTab(tabIndex)) { tb.selectTab(tabIndex); - // If this TabSheet already has focus, set the new selected tab - // as focused. - if (focusedTab != null) { - focusedTab = tb.getTab(tabIndex); - focusedTab.focus(); - } - activeTabIndex = tabIndex; - focusedTabIndex = tabIndex; addStyleDependentName("loading"); // Hide the current contents so a loading indicator can be shown @@ -722,6 +715,8 @@ public class VTabsheet extends VTabsheetBase implements Focusable, client.updateVariable(id, "selected", tabKeys.get(tabIndex) .toString(), true); waitingForResponse = true; + + tb.getTab(tabIndex).focus(); // move keyboard focus to active tab } } @@ -952,6 +947,10 @@ public class VTabsheet extends VTabsheetBase implements Focusable, if (tab == null) { tab = tb.addTab(); } + if (selected) { + tb.selectTab(index); + renderContent(tabUidl.getChildUIDL(0)); + } tab.updateFromUIDL(tabUidl); tab.setEnabledOnServer((!disabledTabKeys.contains(tabKeys.get(index)))); tab.setHiddenOnServer(hidden); @@ -968,11 +967,6 @@ public class VTabsheet extends VTabsheetBase implements Focusable, * and tabs won't be too narrow in certain browsers */ tab.recalculateCaptionWidth(); - - if (selected) { - renderContent(tabUidl.getChildUIDL(0)); - tb.selectTab(index); - } } /** @@ -1089,16 +1083,18 @@ public class VTabsheet extends VTabsheetBase implements Focusable, SCROLLER_CLASSNAME + (scrolled ? "Prev" : "Prev-disabled")); DOM.setElementProperty(scrollerNext, "className", SCROLLER_CLASSNAME + (clipped ? "Next" : "Next-disabled")); + + // the active tab should be focusable if and only if it is visible + boolean isActiveTabVisible = scrollerIndex <= activeTabIndex + && !isClipped(tb.selected); + tb.selected.setTabulatorIndex(isActiveTabVisible ? tabulatorIndex + : -1); + } else { DOM.setStyleAttribute(scroller, "display", "none"); } if (BrowserInfo.get().isSafari()) { - // fix tab height for safari, bugs sometimes if tabs contain icons - String property = tabs.getStyle().getProperty("height"); - if (property == null || property.equals("")) { - tabs.getStyle().setPropertyPx("height", tb.getOffsetHeight()); - } /* * another hack for webkits. tabscroller sometimes drops without * "shaking it" reproducable in @@ -1194,7 +1190,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable, public void onBlur(BlurEvent event) { getApplicationConnection().getVTooltip().hideTooltip(); - if (focusedTab != null && event.getSource() instanceof Tab) { + if (focusedTab != null && focusedTab == event.getSource()) { focusedTab.removeAssistiveDescription(); focusedTab = null; if (client.hasEventListeners(this, EventId.BLUR)) { @@ -1300,13 +1296,10 @@ public class VTabsheet extends VTabsheetBase implements Focusable, } if (isScrolledTabs()) { - // Scroll until the new active tab is visible - int newScrollerIndex = scrollerIndex; - while (tb.getTab(focusedTabIndex).getAbsoluteLeft() < getAbsoluteLeft() - && newScrollerIndex != -1) { - newScrollerIndex = tb.scrollLeft(newScrollerIndex); + // Scroll until the new focused tab is visible + while (!tb.getTab(focusedTabIndex).isVisible()) { + scrollerIndex = tb.scrollLeft(scrollerIndex); } - scrollerIndex = newScrollerIndex; updateTabScroller(); } } diff --git a/client/src/com/vaadin/client/ui/VTreeTable.java b/client/src/com/vaadin/client/ui/VTreeTable.java index 097b9c7ab2..54c9c2d30c 100644 --- a/client/src/com/vaadin/client/ui/VTreeTable.java +++ b/client/src/com/vaadin/client/ui/VTreeTable.java @@ -131,7 +131,7 @@ public class VTreeTable extends VScrollTable { private int indentWidth = -1; private int maxIndent = 0; - VTreeTableScrollBody() { + protected VTreeTableScrollBody() { super(); } diff --git a/client/src/com/vaadin/client/ui/VUpload.java b/client/src/com/vaadin/client/ui/VUpload.java index c08d75e9b7..8e55387d39 100644 --- a/client/src/com/vaadin/client/ui/VUpload.java +++ b/client/src/com/vaadin/client/ui/VUpload.java @@ -295,10 +295,13 @@ public class VUpload extends SimplePanel { /** For internal use only. May be removed or replaced in the future. */ public void submit() { - if (fu.getFilename().length() == 0 || submitted || !enabled) { - VConsole.log("Submit cancelled (disabled, no file or already submitted)"); + if (submitted || !enabled) { + VConsole.log("Submit cancelled (disabled or already submitted)"); return; } + if (fu.getFilename().length() == 0) { + VConsole.log("Submitting empty selection (no file)"); + } // flush possibly pending variable changes, so they will be handled // before upload client.sendPendingVariableChanges(); diff --git a/client/src/com/vaadin/client/ui/VWindow.java b/client/src/com/vaadin/client/ui/VWindow.java index 964adfe303..705787d6c8 100644 --- a/client/src/com/vaadin/client/ui/VWindow.java +++ b/client/src/com/vaadin/client/ui/VWindow.java @@ -27,6 +27,7 @@ import com.google.gwt.aria.client.RelevantValue; import com.google.gwt.aria.client.Roles; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Position; @@ -83,6 +84,8 @@ public class VWindow extends VWindowOverlay implements public static final String CLASSNAME = "v-window"; + private static final String MODAL_WINDOW_OPEN_CLASSNAME = "v-modal-window-open"; + private static final int STACKING_OFFSET_PIXELS = 15; public static final int Z_INDEX = 10000; @@ -725,10 +728,14 @@ public class VWindow extends VWindowOverlay implements getOverlayContainer().appendChild(getModalityCurtain()); } + Document.get().getBody().addClassName(MODAL_WINDOW_OPEN_CLASSNAME); } private void hideModalityCurtain() { + Document.get().getBody().removeClassName(MODAL_WINDOW_OPEN_CLASSNAME); + modalityCurtain.removeFromParent(); + if (BrowserInfo.get().isIE()) { // IE leaks memory in certain cases unless we release the reference // (#9197) diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java index 39de122694..344b5ce739 100644 --- a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java +++ b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java @@ -42,7 +42,6 @@ import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.HorizontalPanel; import com.vaadin.client.Util; -import com.vaadin.client.ui.VCalendar; import com.vaadin.shared.ui.calendar.DateConstants; /** @@ -105,7 +104,6 @@ public class DateCellDayEvent extends FocusableHTML implements eventContent.addClassName("v-calendar-event-content"); getElement().appendChild(eventContent); - VCalendar calendar = weekGrid.getCalendar(); if (weekGrid.getCalendar().isEventResizeAllowed()) { topResizeBar = DOM.createDiv(); bottomResizeBar = DOM.createDiv(); @@ -189,9 +187,11 @@ public class DateCellDayEvent extends FocusableHTML implements String escapedCaption = Util.escapeHTML(calendarEvent.getCaption()); String timeAsText = calendarEvent.getTimeAsText(); if (bigMode) { - innerHtml = "<span>" + timeAsText + "</span><br />" + escapedCaption; + innerHtml = "<span>" + timeAsText + "</span><br />" + + escapedCaption; } else { - innerHtml = "<span>" + timeAsText + "<span>:</span></span> " + escapedCaption; + innerHtml = "<span>" + timeAsText + "<span>:</span></span> " + + escapedCaption; } caption.setInnerHTML(innerHtml); eventContent.setInnerHTML(""); diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DayToolbar.java b/client/src/com/vaadin/client/ui/calendar/schedule/DayToolbar.java index 6233e8111e..58b5fafa7f 100644 --- a/client/src/com/vaadin/client/ui/calendar/schedule/DayToolbar.java +++ b/client/src/com/vaadin/client/ui/calendar/schedule/DayToolbar.java @@ -72,8 +72,6 @@ public class DayToolbar extends HorizontalPanel implements ClickHandler { setCellWidth(nextLabel, MARGINRIGHT + "px"); setCellHorizontalAlignment(nextLabel, ALIGN_RIGHT); int cellw = width / (count - 2); - int remain = width % (count - 2); - int cellw2 = cellw + 1; if (cellw > 0) { int[] cellWidths = VCalendar .distributeSize(width, count - 2, 0); diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/MonthGrid.java b/client/src/com/vaadin/client/ui/calendar/schedule/MonthGrid.java index df9bc42d2a..3b1c774793 100644 --- a/client/src/com/vaadin/client/ui/calendar/schedule/MonthGrid.java +++ b/client/src/com/vaadin/client/ui/calendar/schedule/MonthGrid.java @@ -35,7 +35,6 @@ public class MonthGrid extends FocusableGrid implements KeyDownHandler { private SimpleDayCell selectionEnd; private final VCalendar calendar; private boolean rangeSelectDisabled; - private boolean disabled; private boolean enabled = true; private final HandlerRegistration keyDownHandler; diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java b/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java index cf8006ef66..00fc1ef3ea 100644 --- a/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java +++ b/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java @@ -83,7 +83,6 @@ public class SimpleDayCell extends FocusableFlowPanel implements private Widget clickedWidget; private HandlerRegistration bottomSpacerMouseDownHandler; private boolean scrollable = false; - private boolean eventCanceled; private MonthGrid monthGrid; private HandlerRegistration keyDownHandler; diff --git a/client/src/com/vaadin/client/ui/combobox/ComboBoxConnector.java b/client/src/com/vaadin/client/ui/combobox/ComboBoxConnector.java index f91ff9e2b9..8dec26cf90 100644 --- a/client/src/com/vaadin/client/ui/combobox/ComboBoxConnector.java +++ b/client/src/com/vaadin/client/ui/combobox/ComboBoxConnector.java @@ -37,6 +37,10 @@ import com.vaadin.ui.ComboBox; public class ComboBoxConnector extends AbstractFieldConnector implements Paintable, SimpleManagedLayout { + // oldSuggestionTextMatchTheOldSelection is used to detect when it's safe to + // update textbox text by a changed item caption. + private boolean oldSuggestionTextMatchTheOldSelection; + /* * (non-Javadoc) * @@ -117,7 +121,10 @@ public class ComboBoxConnector extends AbstractFieldConnector implements boolean suggestionsChanged = !getWidget().initDone || !newSuggestions.equals(getWidget().currentSuggestions); + oldSuggestionTextMatchTheOldSelection = false; + if (suggestionsChanged) { + oldSuggestionTextMatchTheOldSelection = isWidgetsCurrentSelectionTextInTextBox(); getWidget().currentSuggestions.clear(); if (!getWidget().waitingForFilteringResponse) { @@ -212,29 +219,38 @@ public class ComboBoxConnector extends AbstractFieldConnector implements // some item selected for (FilterSelectSuggestion suggestion : getWidget().currentSuggestions) { String suggestionKey = suggestion.getOptionKey(); - if (suggestionKey.equals(selectedKey)) { - if (!getWidget().waitingForFilteringResponse - || getWidget().popupOpenerClicked) { - if (!suggestionKey.equals(getWidget().selectedOptionKey) - || suggestion.getReplacementString().equals( - getWidget().tb.getText())) { - // Update text field if we've got a new - // selection - // Also update if we've got the same text to - // retain old text selection behavior - getWidget().setPromptingOff( - suggestion.getReplacementString()); - getWidget().selectedOptionKey = suggestionKey; - } + if (!suggestionKey.equals(selectedKey)) { + continue; + } + if (!getWidget().waitingForFilteringResponse + || getWidget().popupOpenerClicked) { + if (!suggestionKey.equals(getWidget().selectedOptionKey) + || suggestion.getReplacementString().equals( + getWidget().tb.getText()) + || oldSuggestionTextMatchTheOldSelection) { + // Update text field if we've got a new + // selection + // Also update if we've got the same text to + // retain old text selection behavior + // OR if selected item caption is changed. + getWidget().setPromptingOff( + suggestion.getReplacementString()); + getWidget().selectedOptionKey = suggestionKey; } - getWidget().currentSuggestion = suggestion; - getWidget().setSelectedItemIcon(suggestion.getIconUri()); - // only a single item can be selected - break; } + getWidget().currentSuggestion = suggestion; + getWidget().setSelectedItemIcon(suggestion.getIconUri()); + // only a single item can be selected + break; } } + private boolean isWidgetsCurrentSelectionTextInTextBox() { + return getWidget().currentSuggestion != null + && getWidget().currentSuggestion.getReplacementString().equals( + getWidget().tb.getText()); + } + private void resetSelection() { if (!getWidget().waitingForFilteringResponse || getWidget().popupOpenerClicked) { diff --git a/client/src/com/vaadin/client/ui/dd/VIsOverId.java b/client/src/com/vaadin/client/ui/dd/VIsOverId.java index f8083f8b60..7e2f596a20 100644 --- a/client/src/com/vaadin/client/ui/dd/VIsOverId.java +++ b/client/src/com/vaadin/client/ui/dd/VIsOverId.java @@ -19,7 +19,6 @@ package com.vaadin.client.ui.dd; import com.vaadin.client.ComponentConnector; -import com.vaadin.client.ConnectorMap; import com.vaadin.client.UIDL; import com.vaadin.shared.ui.dd.AcceptCriterion; import com.vaadin.ui.AbstractSelect; @@ -36,8 +35,6 @@ final public class VIsOverId extends VAcceptCriterion { .getCurrentDropHandler(); ComponentConnector dropHandlerConnector = currentDropHandler .getConnector(); - ConnectorMap paintableMap = ConnectorMap.get(currentDropHandler - .getApplicationConnection()); String pid2 = dropHandlerConnector.getConnectorId(); if (pid2.equals(pid)) { diff --git a/client/src/com/vaadin/client/ui/dd/VItemIdIs.java b/client/src/com/vaadin/client/ui/dd/VItemIdIs.java index 7d60eda4f9..b022f434f4 100644 --- a/client/src/com/vaadin/client/ui/dd/VItemIdIs.java +++ b/client/src/com/vaadin/client/ui/dd/VItemIdIs.java @@ -32,8 +32,6 @@ final public class VItemIdIs extends VAcceptCriterion { String pid = configuration.getStringAttribute("s"); ComponentConnector dragSource = drag.getTransferable() .getDragSource(); - VDropHandler currentDropHandler = VDragAndDropManager.get() - .getCurrentDropHandler(); String pid2 = dragSource.getConnectorId(); if (pid2.equals(pid)) { Object searchedId = drag.getTransferable().getData("itemId"); diff --git a/client/src/com/vaadin/client/ui/label/LabelConnector.java b/client/src/com/vaadin/client/ui/label/LabelConnector.java index 9639987e8d..6a04c91562 100644 --- a/client/src/com/vaadin/client/ui/label/LabelConnector.java +++ b/client/src/com/vaadin/client/ui/label/LabelConnector.java @@ -36,12 +36,6 @@ public class LabelConnector extends AbstractComponentConnector { } @Override - protected void init() { - super.init(); - getWidget().setConnection(getConnection()); - } - - @Override public void onStateChanged(StateChangeEvent stateChangeEvent) { super.onStateChanged(stateChangeEvent); boolean sinkOnloads = false; diff --git a/client/src/com/vaadin/client/ui/orderedlayout/Slot.java b/client/src/com/vaadin/client/ui/orderedlayout/Slot.java index 49b3661431..37a97f3399 100644 --- a/client/src/com/vaadin/client/ui/orderedlayout/Slot.java +++ b/client/src/com/vaadin/client/ui/orderedlayout/Slot.java @@ -19,9 +19,11 @@ package com.vaadin.client.ui.orderedlayout; import java.util.List; import com.google.gwt.aria.client.Roles; +import com.google.gwt.dom.client.Document; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; @@ -456,6 +458,9 @@ public final class Slot extends SimplePanel { // Caption wrappers Widget widget = getWidget(); + final Element focusedElement = Util.getFocusedElement(); + // By default focus will not be lost + boolean focusLost = false; if (captionText != null || iconUrl != null || error != null || required) { if (caption == null) { caption = DOM.createDiv(); @@ -466,6 +471,10 @@ public final class Slot extends SimplePanel { orphan(widget); captionWrap.appendChild(widget.getElement()); adopt(widget); + + // Made changes to DOM. Focus can be lost if it was in the + // widget. + focusLost = widget.getElement().isOrHasChild(focusedElement); } } else if (caption != null) { orphan(widget); @@ -474,6 +483,9 @@ public final class Slot extends SimplePanel { captionWrap.removeFromParent(); caption = null; captionWrap = null; + + // Made changes to DOM. Focus can be lost if it was in the widget. + focusLost = widget.getElement().isOrHasChild(focusedElement); } // Caption text @@ -560,6 +572,45 @@ public final class Slot extends SimplePanel { setCaptionPosition(CaptionPosition.RIGHT); } } + + if (focusLost) { + // Find out what element is currently focused. + Element currentFocus = Util.getFocusedElement(); + if (currentFocus != null + && currentFocus.equals(Document.get().getBody())) { + // Focus has moved to BodyElement and should be moved back to + // original location. This happened because of adding or + // removing the captionWrap + focusedElement.focus(); + } else if (currentFocus != focusedElement) { + // Focus is either moved somewhere else on purpose or IE has + // lost it. Investigate further. + Timer focusTimer = new Timer() { + + @Override + public void run() { + if (Util.getFocusedElement() == null) { + // This should never become an infinite loop and + // even if it does it will be stopped once something + // is done with the browser. + schedule(25); + } else if (Util.getFocusedElement().equals( + Document.get().getBody())) { + // Focus found it's way to BodyElement. Now it can + // be restored + focusedElement.focus(); + } + } + }; + if (BrowserInfo.get().isIE8()) { + // IE8 can't fix the focus immediately. It will fail. + focusTimer.schedule(25); + } else { + // Newer IE versions can handle things immediately. + focusTimer.run(); + } + } + } } /** diff --git a/client/src/com/vaadin/client/ui/table/TableConnector.java b/client/src/com/vaadin/client/ui/table/TableConnector.java index eacd5bc77a..d2c700ab06 100644 --- a/client/src/com/vaadin/client/ui/table/TableConnector.java +++ b/client/src/com/vaadin/client/ui/table/TableConnector.java @@ -142,9 +142,6 @@ public class TableConnector extends AbstractHasComponentsConnector implements getWidget().updateSortingProperties(uidl); - boolean keyboardSelectionOverRowFetchInProgress = getWidget() - .selectSelectedRows(uidl); - getWidget().updateActionMap(uidl); getWidget().updateColumnProperties(uidl); @@ -216,6 +213,9 @@ public class TableConnector extends AbstractHasComponentsConnector implements } } + boolean keyboardSelectionOverRowFetchInProgress = getWidget() + .selectSelectedRows(uidl); + // If a row had an open context menu before the update, and after the // update there's a row with the same key as that row, restore the // context menu. See #8526. diff --git a/client/src/com/vaadin/client/ui/tree/TreeConnector.java b/client/src/com/vaadin/client/ui/tree/TreeConnector.java index 6f89137918..61207ffa53 100644 --- a/client/src/com/vaadin/client/ui/tree/TreeConnector.java +++ b/client/src/com/vaadin/client/ui/tree/TreeConnector.java @@ -18,6 +18,7 @@ package com.vaadin.client.ui.tree; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.Set; import com.google.gwt.aria.client.Roles; import com.google.gwt.dom.client.Element; @@ -141,9 +142,24 @@ public class TreeConnector extends AbstractComponentConnector implements getWidget().lastSelection = getWidget().getNodeByKey( getWidget().lastSelection.key); } + if (getWidget().focusedNode != null) { - getWidget().setFocusedNode( - getWidget().getNodeByKey(getWidget().focusedNode.key)); + + Set<String> selectedIds = getWidget().selectedIds; + + // If the focused node is not between the selected nodes, we need to + // refresh the focused node to prevent an undesired scroll. #12618. + if (!selectedIds.isEmpty() + && !selectedIds.contains(getWidget().focusedNode.key)) { + String keySelectedId = selectedIds.iterator().next(); + + TreeNode nodeToSelect = getWidget().getNodeByKey(keySelectedId); + + getWidget().setFocusedNode(nodeToSelect); + } else { + getWidget().setFocusedNode( + getWidget().getNodeByKey(getWidget().focusedNode.key)); + } } if (getWidget().lastSelection == null diff --git a/client/src/com/vaadin/client/ui/ui/UIConnector.java b/client/src/com/vaadin/client/ui/ui/UIConnector.java index d0f3c8603f..17a23baad5 100644 --- a/client/src/com/vaadin/client/ui/ui/UIConnector.java +++ b/client/src/com/vaadin/client/ui/ui/UIConnector.java @@ -49,7 +49,6 @@ import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent; import com.vaadin.client.BrowserInfo; import com.vaadin.client.ComponentConnector; import com.vaadin.client.ConnectorHierarchyChangeEvent; -import com.vaadin.client.ConnectorMap; import com.vaadin.client.Focusable; import com.vaadin.client.Paintable; import com.vaadin.client.ServerConnector; @@ -192,7 +191,6 @@ public class UIConnector extends AbstractSingleComponentContainerConnector @Override public void updateFromUIDL(final UIDL uidl, ApplicationConnection client) { - ConnectorMap paintableMap = ConnectorMap.get(getConnection()); getWidget().id = getConnectorId(); boolean firstPaint = getWidget().connection == null; getWidget().connection = client; diff --git a/client/src/com/vaadin/client/ui/upload/UploadConnector.java b/client/src/com/vaadin/client/ui/upload/UploadConnector.java index 937ff438ac..989a913adc 100644 --- a/client/src/com/vaadin/client/ui/upload/UploadConnector.java +++ b/client/src/com/vaadin/client/ui/upload/UploadConnector.java @@ -22,12 +22,22 @@ import com.vaadin.client.UIDL; import com.vaadin.client.ui.AbstractComponentConnector; import com.vaadin.client.ui.VUpload; import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.upload.UploadClientRpc; import com.vaadin.ui.Upload; @Connect(Upload.class) public class UploadConnector extends AbstractComponentConnector implements Paintable { + public UploadConnector() { + registerRpc(UploadClientRpc.class, new UploadClientRpc() { + @Override + public void submitUpload() { + getWidget().submit(); + } + }); + } + @Override public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { if (!isRealUpdate(uidl)) { @@ -37,10 +47,6 @@ public class UploadConnector extends AbstractComponentConnector implements getWidget().t.schedule(400); return; } - if (uidl.hasAttribute("forceSubmit")) { - getWidget().submit(); - return; - } getWidget().setImmediate(getState().immediate); getWidget().client = client; getWidget().paintableId = uidl.getId(); |