diff options
author | Johannes Dahlström <johannesd@vaadin.com> | 2012-08-17 13:32:54 +0300 |
---|---|---|
committer | Johannes Dahlström <johannesd@vaadin.com> | 2012-08-17 13:32:54 +0300 |
commit | 47a0326c2fb2d6d8621e7a6fbfa2f011a48e15a4 (patch) | |
tree | 416685c3912f716eb09497cc7cce071786fe2d12 /client | |
parent | 8e3aa0a9823556896f1af00599c3e79ca2ce2e01 (diff) | |
parent | 9bdbd7efbb7fa599910dc85182968334e4dced78 (diff) | |
download | vaadin-framework-47a0326c2fb2d6d8621e7a6fbfa2f011a48e15a4.tar.gz vaadin-framework-47a0326c2fb2d6d8621e7a6fbfa2f011a48e15a4.zip |
Merge branch 'master' into root-cleanup
Conflicts:
server/src/com/vaadin/terminal/DeploymentConfiguration.java
server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java
server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java
server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java
Diffstat (limited to 'client')
296 files changed, 61319 insertions, 0 deletions
diff --git a/client/src/com/vaadin/Vaadin.gwt.xml b/client/src/com/vaadin/Vaadin.gwt.xml new file mode 100644 index 0000000000..07d7c941e6 --- /dev/null +++ b/client/src/com/vaadin/Vaadin.gwt.xml @@ -0,0 +1,85 @@ +<module> + <!-- This GWT module inherits all Vaadin client side functionality modules. + This is the module you want to inherit in your client side project to be + able to use com.vaadin.* classes. --> + + <!-- Hint for WidgetSetBuilder not to automatically update the file --> + <!-- WS Compiler: manually edited --> + + <inherits name="com.google.gwt.user.User" /> + + <inherits name="com.google.gwt.http.HTTP" /> + + <inherits name="com.google.gwt.json.JSON" /> + + <inherits name="com.vaadin.terminal.gwt.VaadinBrowserSpecificOverrides" /> + + <source path="terminal/gwt/client" /> + <source path="shared" /> + + <!-- Use own Scheduler implementation to be able to track if commands are + running --> + <replace-with class="com.vaadin.terminal.gwt.client.VSchedulerImpl"> + <when-type-is class="com.google.gwt.core.client.impl.SchedulerImpl" /> + </replace-with> + + <!-- Generators for serializators for classes used in communication between + server and client --> + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.SerializerMapGenerator"> + <when-type-is + class="com.vaadin.terminal.gwt.client.communication.SerializerMap" /> + </generate-with> + + <replace-with class="com.vaadin.terminal.gwt.client.VDebugConsole"> + <when-type-is class="com.vaadin.terminal.gwt.client.Console" /> + </replace-with> + + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.EagerWidgetMapGenerator"> + <when-type-is class="com.vaadin.terminal.gwt.client.WidgetMap" /> + </generate-with> + + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.AcceptCriteriaFactoryGenerator"> + <when-type-is + class="com.vaadin.terminal.gwt.client.ui.dd.VAcceptCriterionFactory" /> + </generate-with> + + <!-- Generate client side proxies for client to server RPC interfaces --> + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.RpcProxyGenerator"> + <when-type-assignable + class="com.vaadin.shared.communication.ServerRpc" /> + </generate-with> + + <!-- Generate client side proxies for client to server RPC interfaces --> + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.RpcProxyCreatorGenerator"> + <when-type-assignable + class="com.vaadin.terminal.gwt.client.communication.RpcProxy.RpcProxyCreator" /> + </generate-with> + + <!-- Generate client side RPC manager for server to client RPC --> + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.GeneratedRpcMethodProviderGenerator"> + <when-type-assignable + class="com.vaadin.terminal.gwt.client.communication.GeneratedRpcMethodProvider" /> + </generate-with> + + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.ConnectorWidgetFactoryGenerator"> + <when-type-assignable + class="com.vaadin.terminal.gwt.client.ui.ConnectorWidgetFactory" /> + </generate-with> + + <generate-with + class="com.vaadin.terminal.gwt.widgetsetutils.ConnectorStateFactoryGenerator"> + <when-type-assignable + class="com.vaadin.terminal.gwt.client.ui.ConnectorStateFactory" /> + </generate-with> + + <!-- Use the new cross site linker to get a nocache.js without document.write --> + <add-linker name="xsiframe" /> + +</module> diff --git a/client/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml b/client/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml new file mode 100644 index 0000000000..278d92f38f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml @@ -0,0 +1,13 @@ +<module> + <!-- This GWT module defines the Vaadin DefaultWidgetSet. This is the module + you want to extend when creating an extended widget set, or when creating + a specialized widget set with a subset of the components. --> + + <!-- Hint for WidgetSetBuilder not to automatically update the file --> + <!-- WS Compiler: manually edited --> + + <inherits name="com.vaadin.Vaadin" /> + + <entry-point class="com.vaadin.terminal.gwt.client.ApplicationConfiguration" /> + +</module> diff --git a/client/src/com/vaadin/terminal/gwt/VaadinBrowserSpecificOverrides.gwt.xml b/client/src/com/vaadin/terminal/gwt/VaadinBrowserSpecificOverrides.gwt.xml new file mode 100644 index 0000000000..04d2c18060 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/VaadinBrowserSpecificOverrides.gwt.xml @@ -0,0 +1,47 @@ +<module> + <!-- This GWT module defines the browser specific overrides used by Vaadin --> + + <!-- Hint for WidgetSetBuilder not to automatically update the file --> + <!-- WS Compiler: manually edited --> + + <!-- Fall through to this rule for everything but IE --> + <replace-with + class="com.vaadin.terminal.gwt.client.ui.upload.UploadIFrameOnloadStrategy"> + <when-type-is + class="com.vaadin.terminal.gwt.client.ui.upload.UploadIFrameOnloadStrategy" /> + </replace-with> + + <replace-with + class="com.vaadin.terminal.gwt.client.ui.upload.UploadIFrameOnloadStrategyIE"> + <when-type-is + class="com.vaadin.terminal.gwt.client.ui.upload.UploadIFrameOnloadStrategy" /> + <any> + <when-property-is name="user.agent" value="ie8" /> + </any> + </replace-with> + + <!-- Fall through to this rule for everything but IE --> + <replace-with class="com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper"> + <when-type-is class="com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper" /> + </replace-with> + + <replace-with class="com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapperIE"> + <when-type-is class="com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper" /> + <any> + <when-property-is name="user.agent" value="ie8" /> + </any> + </replace-with> + + <!-- Fall through to this rule for everything but IE --> + <replace-with class="com.vaadin.terminal.gwt.client.LayoutManager"> + <when-type-is class="com.vaadin.terminal.gwt.client.LayoutManager" /> + </replace-with> + + <replace-with class="com.vaadin.terminal.gwt.client.LayoutManagerIE8"> + <when-type-is class="com.vaadin.terminal.gwt.client.LayoutManager" /> + <any> + <when-property-is name="user.agent" value="ie8" /> + </any> + </replace-with> + +</module> diff --git a/client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java b/client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java new file mode 100644 index 0000000000..eea60b04ea --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java @@ -0,0 +1,673 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.google.gwt.core.client.EntryPoint; +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.GWT.UncaughtExceptionHandler; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArrayString; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.terminal.gwt.client.ui.UnknownComponentConnector; + +public class ApplicationConfiguration implements EntryPoint { + + /** + * Helper class for reading configuration options from the bootstap + * javascript + * + * @since 7.0 + */ + private static class JsoConfiguration extends JavaScriptObject { + protected JsoConfiguration() { + // JSO Constructor + } + + /** + * Reads a configuration parameter as a string. Please note that the + * javascript value of the parameter should also be a string, or else an + * undefined exception may be thrown. + * + * @param name + * name of the configuration parameter + * @return value of the configuration parameter, or <code>null</code> if + * not defined + */ + private native String getConfigString(String name) + /*-{ + var value = this.getConfig(name); + if (value === null || value === undefined) { + return null; + } else { + return value +""; + } + }-*/; + + /** + * Reads a configuration parameter as a boolean object. Please note that + * the javascript value of the parameter should also be a boolean, or + * else an undefined exception may be thrown. + * + * @param name + * name of the configuration parameter + * @return boolean value of the configuration paramter, or + * <code>null</code> if no value is defined + */ + private native Boolean getConfigBoolean(String name) + /*-{ + var value = this.getConfig(name); + if (value === null || value === undefined) { + return null; + } else { + // $entry not needed as function is not exported + return @java.lang.Boolean::valueOf(Z)(value); + } + }-*/; + + /** + * Reads a configuration parameter as an integer object. Please note + * that the javascript value of the parameter should also be an integer, + * or else an undefined exception may be thrown. + * + * @param name + * name of the configuration parameter + * @return integer value of the configuration paramter, or + * <code>null</code> if no value is defined + */ + private native Integer getConfigInteger(String name) + /*-{ + var value = this.getConfig(name); + if (value === null || value === undefined) { + return null; + } else { + // $entry not needed as function is not exported + return @java.lang.Integer::valueOf(I)(value); + } + }-*/; + + /** + * Reads a configuration parameter as an {@link ErrorMessage} object. + * Please note that the javascript value of the parameter should also be + * an object with appropriate fields, or else an undefined exception may + * be thrown when calling this method or when calling methods on the + * returned object. + * + * @param name + * name of the configuration parameter + * @return error message with the given name, or <code>null</code> if no + * value is defined + */ + private native ErrorMessage getConfigError(String name) + /*-{ + return this.getConfig(name); + }-*/; + + /** + * Returns a native javascript object containing version information + * from the server. + * + * @return a javascript object with the version information + */ + private native JavaScriptObject getVersionInfoJSObject() + /*-{ + return this.getConfig("versionInfo"); + }-*/; + + /** + * Gets the version of the Vaadin framework used on the server. + * + * @return a string with the version + * + * @see com.vaadin.terminal.gwt.server.AbstractApplicationServlet#VERSION + */ + private native String getVaadinVersion() + /*-{ + return this.getConfig("versionInfo").vaadinVersion; + }-*/; + + /** + * Gets the version of the application running on the server. + * + * @return a string with the application version + * + * @see com.vaadin.Application#getVersion() + */ + private native String getApplicationVersion() + /*-{ + return this.getConfig("versionInfo").applicationVersion; + }-*/; + + private native String getUIDL() + /*-{ + return this.getConfig("uidl"); + }-*/; + } + + /** + * Wraps a native javascript object containing fields for an error message + * + * @since 7.0 + */ + public static final class ErrorMessage extends JavaScriptObject { + + protected ErrorMessage() { + // JSO constructor + } + + public final native String getCaption() + /*-{ + return this.caption; + }-*/; + + public final native String getMessage() + /*-{ + return this.message; + }-*/; + + public final native String getUrl() + /*-{ + return this.url; + }-*/; + } + + private static WidgetSet widgetSet = GWT.create(WidgetSet.class); + + private String id; + private String themeUri; + private String appUri; + private int rootId; + private boolean standalone; + private ErrorMessage communicationError; + private ErrorMessage authorizationError; + private boolean useDebugIdInDom = true; + + private HashMap<Integer, String> unknownComponents; + + private Class<? extends ServerConnector>[] classes = new Class[1024]; + + private boolean browserDetailsSent = false; + private boolean widgetsetVersionSent = false; + + static// TODO consider to make this hashmap per application + LinkedList<Command> callbacks = new LinkedList<Command>(); + + private static int dependenciesLoading; + + private static ArrayList<ApplicationConnection> runningApplications = new ArrayList<ApplicationConnection>(); + + private Map<Integer, Integer> componentInheritanceMap = new HashMap<Integer, Integer>(); + private Map<Integer, String> tagToServerSideClassName = new HashMap<Integer, String>(); + + public boolean usePortletURLs() { + return getPortletResourceUrl() != null; + } + + public String getPortletResourceUrl() { + return getJsoConfiguration(id).getConfigString( + ApplicationConstants.PORTLET_RESOUCE_URL_BASE); + } + + public String getRootPanelId() { + return id; + } + + /** + * Gets the application base URI. Using this other than as the download + * action URI can cause problems in Portlet 2.0 deployments. + * + * @return application base URI + */ + public String getApplicationUri() { + return appUri; + } + + public String getThemeName() { + String uri = getThemeUri(); + String themeName = uri.substring(uri.lastIndexOf('/')); + themeName = themeName.replaceAll("[^a-zA-Z0-9]", ""); + return themeName; + } + + public String getThemeUri() { + return themeUri; + } + + public void setAppId(String appId) { + id = appId; + } + + /** + * Gets the initial UIDL from the DOM, if it was provided during the init + * process. + * + * @return + */ + public String getUIDL() { + return getJsoConfiguration(id).getUIDL(); + } + + /** + * @return true if the application is served by std. Vaadin servlet and is + * considered to be the only or main content of the host page. + */ + public boolean isStandalone() { + return standalone; + } + + /** + * Gets the root if of this application instance. The root id should be + * included in every request originating from this instance in order to + * associate it with the right Root instance on the server. + * + * @return the root id + */ + public int getRootId() { + return rootId; + } + + public JavaScriptObject getVersionInfoJSObject() { + return getJsoConfiguration(id).getVersionInfoJSObject(); + } + + public ErrorMessage getCommunicationError() { + return communicationError; + } + + public ErrorMessage getAuthorizationError() { + return authorizationError; + } + + /** + * Reads the configuration values defined by the bootstrap javascript. + */ + private void loadFromDOM() { + JsoConfiguration jsoConfiguration = getJsoConfiguration(id); + appUri = jsoConfiguration.getConfigString("appUri"); + if (appUri != null && !appUri.endsWith("/")) { + appUri += '/'; + } + themeUri = jsoConfiguration.getConfigString("themeUri"); + rootId = jsoConfiguration.getConfigInteger("rootId").intValue(); + + // null -> true + useDebugIdInDom = jsoConfiguration.getConfigBoolean("useDebugIdInDom") != Boolean.FALSE; + + // null -> false + standalone = jsoConfiguration.getConfigBoolean("standalone") == Boolean.TRUE; + + communicationError = jsoConfiguration.getConfigError("comErrMsg"); + authorizationError = jsoConfiguration.getConfigError("authErrMsg"); + + // boostrap sets initPending to false if it has sent the browser details + if (jsoConfiguration.getConfigBoolean("initPending") == Boolean.FALSE) { + setBrowserDetailsSent(); + } + + } + + /** + * Starts the application with a given id by reading the configuration + * options stored by the bootstrap javascript. + * + * @param applicationId + * id of the application to load, this is also the id of the html + * element into which the application should be rendered. + */ + public static void startApplication(final String applicationId) { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + + @Override + public void execute() { + ApplicationConfiguration appConf = getConfigFromDOM(applicationId); + ApplicationConnection a = GWT + .create(ApplicationConnection.class); + a.init(widgetSet, appConf); + a.start(); + runningApplications.add(a); + } + }); + } + + public static List<ApplicationConnection> getRunningApplications() { + return runningApplications; + } + + /** + * Gets the configuration object for a specific application from the + * bootstrap javascript. + * + * @param appId + * the id of the application to get configuration data for + * @return a native javascript object containing the configuration data + */ + private native static JsoConfiguration getJsoConfiguration(String appId) + /*-{ + return $wnd.vaadin.getApp(appId); + }-*/; + + public static ApplicationConfiguration getConfigFromDOM(String appId) { + ApplicationConfiguration conf = new ApplicationConfiguration(); + conf.setAppId(appId); + conf.loadFromDOM(); + return conf; + } + + public String getServletVersion() { + return getJsoConfiguration(id).getVaadinVersion(); + } + + public String getApplicationVersion() { + return getJsoConfiguration(id).getApplicationVersion(); + } + + public boolean useDebugIdInDOM() { + return useDebugIdInDom; + } + + public Class<? extends ServerConnector> getConnectorClassByEncodedTag( + int tag) { + try { + return classes[tag]; + } catch (Exception e) { + // component was not present in mappings + return UnknownComponentConnector.class; + } + } + + public void addComponentInheritanceInfo(ValueMap valueMap) { + JsArrayString keyArray = valueMap.getKeyArray(); + for (int i = 0; i < keyArray.length(); i++) { + String key = keyArray.get(i); + int value = valueMap.getInt(key); + componentInheritanceMap.put(Integer.parseInt(key), value); + } + } + + public void addComponentMappings(ValueMap valueMap, WidgetSet widgetSet) { + JsArrayString keyArray = valueMap.getKeyArray(); + for (int i = 0; i < keyArray.length(); i++) { + String key = keyArray.get(i).intern(); + int value = valueMap.getInt(key); + tagToServerSideClassName.put(value, key); + } + + for (int i = 0; i < keyArray.length(); i++) { + String key = keyArray.get(i).intern(); + int value = valueMap.getInt(key); + classes[value] = widgetSet.getConnectorClassByTag(value, this); + if (classes[value] == UnknownComponentConnector.class) { + if (unknownComponents == null) { + unknownComponents = new HashMap<Integer, String>(); + } + unknownComponents.put(value, key); + } + } + } + + public Integer getParentTag(int tag) { + return componentInheritanceMap.get(tag); + } + + public String getServerSideClassNameForTag(Integer tag) { + return tagToServerSideClassName.get(tag); + } + + String getUnknownServerClassNameByTag(int tag) { + if (unknownComponents != null) { + return unknownComponents.get(tag); + } + return null; + } + + /** + * + * @param c + */ + static void runWhenDependenciesLoaded(Command c) { + if (dependenciesLoading == 0) { + c.execute(); + } else { + callbacks.add(c); + } + } + + static void startDependencyLoading() { + dependenciesLoading++; + } + + static void endDependencyLoading() { + dependenciesLoading--; + if (dependenciesLoading == 0 && !callbacks.isEmpty()) { + for (Command cmd : callbacks) { + cmd.execute(); + } + callbacks.clear(); + } else if (dependenciesLoading == 0 && deferredWidgetLoader != null) { + deferredWidgetLoader.trigger(); + } + + } + + /* + * This loop loads widget implementation that should be loaded deferred. + */ + static class DeferredWidgetLoader extends Timer { + private static final int FREE_LIMIT = 4; + private static final int FREE_CHECK_TIMEOUT = 100; + + int communicationFree = 0; + int nextWidgetIndex = 0; + private boolean pending; + + public DeferredWidgetLoader() { + schedule(5000); + } + + public void trigger() { + if (!pending) { + schedule(FREE_CHECK_TIMEOUT); + } + } + + @Override + public void schedule(int delayMillis) { + super.schedule(delayMillis); + pending = true; + } + + @Override + public void run() { + pending = false; + if (!isBusy()) { + Class<? extends ServerConnector> nextType = getNextType(); + if (nextType == null) { + // ensured that all widgets are loaded + deferredWidgetLoader = null; + } else { + communicationFree = 0; + widgetSet.loadImplementation(nextType); + } + } else { + schedule(FREE_CHECK_TIMEOUT); + } + } + + private Class<? extends ServerConnector> getNextType() { + Class<? extends ServerConnector>[] deferredLoadedConnectors = widgetSet + .getDeferredLoadedConnectors(); + if (deferredLoadedConnectors.length <= nextWidgetIndex) { + return null; + } else { + return deferredLoadedConnectors[nextWidgetIndex++]; + } + } + + private boolean isBusy() { + if (dependenciesLoading > 0) { + communicationFree = 0; + return true; + } + for (ApplicationConnection app : runningApplications) { + if (app.hasActiveRequest()) { + // if an UIDL request or widget loading is active, mark as + // busy + communicationFree = 0; + return true; + } + } + communicationFree++; + return communicationFree < FREE_LIMIT; + } + } + + private static DeferredWidgetLoader deferredWidgetLoader; + + @Override + public void onModuleLoad() { + + // Prepare VConsole for debugging + if (isDebugMode()) { + Console console = GWT.create(Console.class); + console.setQuietMode(isQuietDebugMode()); + console.init(); + VConsole.setImplementation(console); + } else { + VConsole.setImplementation((Console) GWT.create(NullConsole.class)); + } + /* + * Display some sort of error of exceptions in web mode to debug + * console. After this, exceptions are reported to VConsole and possible + * GWT hosted mode. + */ + GWT.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + + @Override + public void onUncaughtException(Throwable e) { + /* + * Note in case of null console (without ?debug) we eat + * exceptions. "a1 is not an object" style errors helps nobody, + * especially end user. It does not work tells just as much. + */ + VConsole.getImplementation().error(e); + } + }); + + if (SuperDevMode.enableBasedOnParameter()) { + // Do not start any application as super dev mode will refresh the + // page once done compiling + return; + } + registerCallback(GWT.getModuleName()); + deferredWidgetLoader = new DeferredWidgetLoader(); + } + + /** + * Registers that callback that the bootstrap javascript uses to start + * applications once the widgetset is loaded and all required information is + * available + * + * @param widgetsetName + * the name of this widgetset + */ + public native static void registerCallback(String widgetsetName) + /*-{ + var callbackHandler = $entry(@com.vaadin.terminal.gwt.client.ApplicationConfiguration::startApplication(Ljava/lang/String;)); + $wnd.vaadin.registerWidgetset(widgetsetName, callbackHandler); + }-*/; + + /** + * Checks if client side is in debug mode. Practically this is invoked by + * adding ?debug parameter to URI. + * + * @return true if client side is currently been debugged + */ + public static boolean isDebugMode() { + return isDebugAvailable() + && Window.Location.getParameter("debug") != null; + } + + private native static boolean isDebugAvailable() + /*-{ + if($wnd.vaadin.debug) { + return true; + } else { + return false; + } + }-*/; + + /** + * Checks whether debug logging should be quiet + * + * @return <code>true</code> if debug logging should be quiet + */ + public static boolean isQuietDebugMode() { + String debugParameter = Window.Location.getParameter("debug"); + return isDebugAvailable() && debugParameter != null + && debugParameter.startsWith("q"); + } + + /** + * Checks whether information from the web browser (e.g. uri fragment and + * screen size) has been sent to the server. + * + * @return <code>true</code> if browser information has already been sent + * + * @see ApplicationConnection#getNativeBrowserDetailsParameters(String) + */ + public boolean isBrowserDetailsSent() { + return browserDetailsSent; + } + + /** + * Registers that the browser details have been sent. + * {@link #isBrowserDetailsSent()} will return + * <code> after this method has been invoked. + */ + public void setBrowserDetailsSent() { + browserDetailsSent = true; + } + + /** + * Checks whether the widget set version has been sent to the server. It is + * sent in the first UIDL request. + * + * @return <code>true</code> if browser information has already been sent + * + * @see ApplicationConnection#getNativeBrowserDetailsParameters(String) + */ + public boolean isWidgetsetVersionSent() { + return widgetsetVersionSent; + } + + /** + * Registers that the widget set version has been sent to the server. + */ + public void setWidgetsetVersionSent() { + widgetsetVersionSent = true; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java b/client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java new file mode 100644 index 0000000000..a8852fe9fa --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java @@ -0,0 +1,2523 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gwt.core.client.Duration; +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArray; +import com.google.gwt.core.client.JsArrayString; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +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.http.client.URL; +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONString; +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; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.ComponentState; +import com.vaadin.shared.Version; +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.shared.communication.UidlValue; +import com.vaadin.terminal.gwt.client.ApplicationConfiguration.ErrorMessage; +import com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadEvent; +import com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadListener; +import com.vaadin.terminal.gwt.client.communication.HasJavaScriptConnectorHelper; +import com.vaadin.terminal.gwt.client.communication.JsonDecoder; +import com.vaadin.terminal.gwt.client.communication.JsonEncoder; +import com.vaadin.terminal.gwt.client.communication.RpcManager; +import com.vaadin.terminal.gwt.client.communication.SerializerMap; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.communication.Type; +import com.vaadin.terminal.gwt.client.extensions.AbstractExtensionConnector; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector; +import com.vaadin.terminal.gwt.client.ui.VContextMenu; +import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager; +import com.vaadin.terminal.gwt.client.ui.notification.VNotification; +import com.vaadin.terminal.gwt.client.ui.notification.VNotification.HideEvent; +import com.vaadin.terminal.gwt.client.ui.root.RootConnector; +import com.vaadin.terminal.gwt.client.ui.window.WindowConnector; + +/** + * This is the client side communication "engine", managing client-server + * communication with its server side counterpart + * com.vaadin.terminal.gwt.server.AbstractCommunicationManager. + * + * Client-side connectors receive updates from the corresponding server-side + * connector (typically component) as state updates or RPC calls. The connector + * has the possibility to communicate back with its server side counter part + * through RPC calls. + * + * TODO document better + * + * Entry point classes (widgetsets) define <code>onModuleLoad()</code>. + */ +public class ApplicationConnection { + + public static final String MODIFIED_CLASSNAME = "v-modified"; + + public static final String DISABLED_CLASSNAME = "v-disabled"; + + public static final String REQUIRED_CLASSNAME_EXT = "-required"; + + public static final String ERROR_CLASSNAME_EXT = "-error"; + + public static final char VAR_BURST_SEPARATOR = '\u001d'; + + public static final char VAR_ESCAPE_CHARACTER = '\u001b'; + + private static SerializerMap serializerMap; + + /** + * A string that, if found in a non-JSON response to a UIDL request, will + * cause the browser to refresh the page. If followed by a colon, optional + * whitespace, and a URI, causes the browser to synchronously load the URI. + * + * <p> + * This allows, for instance, a servlet filter to redirect the application + * to a custom login page when the session expires. For example: + * </p> + * + * <pre> + * if (sessionExpired) { + * response.setHeader("Content-Type", "text/html"); + * response.getWriter().write( + * myLoginPageHtml + "<!-- Vaadin-Refresh: " + * + request.getContextPath() + " -->"); + * } + * </pre> + */ + public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh"; + + // will hold the UIDL security key (for XSS protection) once received + private String uidlSecurityKey = "init"; + + private final HashMap<String, String> resourcesMap = new HashMap<String, String>(); + + private ArrayList<MethodInvocation> pendingInvocations = new ArrayList<MethodInvocation>(); + + private WidgetSet widgetSet; + + private VContextMenu contextMenu = null; + + private Timer loadTimer; + private Timer loadTimer2; + private Timer loadTimer3; + private Element loadElement; + + private final RootConnector rootConnector; + + protected boolean applicationRunning = false; + + private boolean hasActiveRequest = false; + + protected boolean cssLoaded = false; + + /** Parameters for this application connection loaded from the web-page */ + private ApplicationConfiguration configuration; + + /** List of pending variable change bursts that must be submitted in order */ + private final ArrayList<ArrayList<MethodInvocation>> pendingBursts = new ArrayList<ArrayList<MethodInvocation>>(); + + /** Timer for automatic refirect to SessionExpiredURL */ + private Timer redirectTimer; + + /** redirectTimer scheduling interval in seconds */ + private int sessionExpirationInterval; + + private ArrayList<Widget> componentCaptionSizeChanges = new ArrayList<Widget>(); + + private Date requestStartTime; + + private boolean validatingLayouts = false; + + private Set<ComponentConnector> zeroWidthComponents = null; + + private Set<ComponentConnector> zeroHeightComponents = null; + + private final LayoutManager layoutManager; + + private final RpcManager rpcManager; + + public static class MultiStepDuration extends Duration { + private int previousStep = elapsedMillis(); + + public void logDuration(String message) { + logDuration(message, 0); + } + + public void logDuration(String message, int minDuration) { + int currentTime = elapsedMillis(); + int stepDuration = currentTime - previousStep; + if (stepDuration >= minDuration) { + VConsole.log(message + ": " + stepDuration + " ms"); + } + previousStep = currentTime; + } + } + + public ApplicationConnection() { + rootConnector = GWT.create(RootConnector.class); + rpcManager = GWT.create(RpcManager.class); + layoutManager = GWT.create(LayoutManager.class); + layoutManager.setConnection(this); + serializerMap = GWT.create(SerializerMap.class); + } + + public void init(WidgetSet widgetSet, ApplicationConfiguration cnf) { + VConsole.log("Starting application " + cnf.getRootPanelId()); + + VConsole.log("Vaadin application servlet version: " + + cnf.getServletVersion()); + VConsole.log("Application version: " + cnf.getApplicationVersion()); + + if (!cnf.getServletVersion().equals(Version.getFullVersion())) { + VConsole.error("Warning: your widget set seems to be built with a different " + + "version than the one used on server. Unexpected " + + "behavior may occur."); + } + + this.widgetSet = widgetSet; + configuration = cnf; + + ComponentLocator componentLocator = new ComponentLocator(this); + + String appRootPanelName = cnf.getRootPanelId(); + // remove the end (window name) of autogenerated rootpanel id + appRootPanelName = appRootPanelName.replaceFirst("-\\d+$", ""); + + initializeTestbenchHooks(componentLocator, appRootPanelName); + + initializeClientHooks(); + + rootConnector.init(cnf.getRootPanelId(), this); + showLoadingIndicator(); + } + + /** + * Starts this application. Don't call this method directly - it's called by + * {@link ApplicationConfiguration#startNextApplication()}, which should be + * called once this application has started (first response received) or + * failed to start. This ensures that the applications are started in order, + * to avoid session-id problems. + * + */ + public void start() { + String jsonText = configuration.getUIDL(); + if (jsonText == null) { + // inital UIDL not in DOM, request later + repaintAll(); + } else { + // Update counter so TestBench knows something is still going on + hasActiveRequest = true; + + // initial UIDL provided in DOM, continue as if returned by request + handleJSONText(jsonText, -1); + } + } + + private native void initializeTestbenchHooks( + ComponentLocator componentLocator, String TTAppId) + /*-{ + var ap = this; + var client = {}; + client.isActive = $entry(function() { + return ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::hasActiveRequest()() + || ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::isExecutingDeferredCommands()(); + }); + var vi = ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::getVersionInfo()(); + if (vi) { + client.getVersionInfo = function() { + return vi; + } + } + + client.getProfilingData = $entry(function() { + var pd = [ + ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::lastProcessingTime, + ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::totalProcessingTime + ]; + pd = pd.concat(ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::serverTimingInfo); + return pd; + }); + + client.getElementByPath = $entry(function(id) { + return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getElementByPath(Ljava/lang/String;)(id); + }); + client.getPathForElement = $entry(function(element) { + return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element); + }); + + $wnd.vaadin.clients[TTAppId] = client; + }-*/; + + /** + * Helper for tt initialization + */ + private JavaScriptObject getVersionInfo() { + return configuration.getVersionInfoJSObject(); + } + + /** + * Publishes a JavaScript API for mash-up applications. + * <ul> + * <li><code>vaadin.forceSync()</code> sends pending variable changes, in + * effect synchronizing the server and client state. This is done for all + * applications on host page.</li> + * <li><code>vaadin.postRequestHooks</code> is a map of functions which gets + * called after each XHR made by vaadin application. Note, that it is + * attaching js functions responsibility to create the variable like this: + * + * <code><pre> + * if(!vaadin.postRequestHooks) {vaadin.postRequestHooks = new Object();} + * postRequestHooks.myHook = function(appId) { + * if(appId == "MyAppOfInterest") { + * // do the staff you need on xhr activity + * } + * } + * </pre></code> First parameter passed to these functions is the identifier + * of Vaadin application that made the request. + * </ul> + * + * TODO make this multi-app aware + */ + private native void initializeClientHooks() + /*-{ + var app = this; + var oldSync; + if ($wnd.vaadin.forceSync) { + oldSync = $wnd.vaadin.forceSync; + } + $wnd.vaadin.forceSync = $entry(function() { + if (oldSync) { + oldSync(); + } + app.@com.vaadin.terminal.gwt.client.ApplicationConnection::sendPendingVariableChanges()(); + }); + var oldForceLayout; + if ($wnd.vaadin.forceLayout) { + oldForceLayout = $wnd.vaadin.forceLayout; + } + $wnd.vaadin.forceLayout = $entry(function() { + if (oldForceLayout) { + oldForceLayout(); + } + app.@com.vaadin.terminal.gwt.client.ApplicationConnection::forceLayout()(); + }); + }-*/; + + /** + * Runs possibly registered client side post request hooks. This is expected + * to be run after each uidl request made by Vaadin application. + * + * @param appId + */ + private static native void runPostRequestHooks(String appId) + /*-{ + if ($wnd.vaadin.postRequestHooks) { + for ( var hook in $wnd.vaadin.postRequestHooks) { + if (typeof ($wnd.vaadin.postRequestHooks[hook]) == "function") { + try { + $wnd.vaadin.postRequestHooks[hook](appId); + } catch (e) { + } + } + } + } + }-*/; + + /** + * If on Liferay and logged in, ask the client side session management + * JavaScript to extend the session duration. + * + * Otherwise, Liferay client side JavaScript will explicitly expire the + * session even though the server side considers the session to be active. + * See ticket #8305 for more information. + */ + protected native void extendLiferaySession() + /*-{ + if ($wnd.Liferay && $wnd.Liferay.Session) { + $wnd.Liferay.Session.extend(); + // if the extend banner is visible, hide it + if ($wnd.Liferay.Session.banner) { + $wnd.Liferay.Session.banner.remove(); + } + } + }-*/; + + /** + * Get the active Console for writing debug messages. May return an actual + * logging console, or the NullConsole if debugging is not turned on. + * + * @deprecated Developers should use {@link VConsole} since 6.4.5 + * + * @return the active Console + */ + @Deprecated + public static Console getConsole() { + return VConsole.getImplementation(); + } + + /** + * Checks if client side is in debug mode. Practically this is invoked by + * adding ?debug parameter to URI. + * + * @deprecated use ApplicationConfiguration isDebugMode instead. + * + * @return true if client side is currently been debugged + */ + @Deprecated + public static boolean isDebugMode() { + return ApplicationConfiguration.isDebugMode(); + } + + /** + * Gets the application base URI. Using this other than as the download + * action URI can cause problems in Portlet 2.0 deployments. + * + * @return application base URI + */ + public String getAppUri() { + return configuration.getApplicationUri(); + }; + + /** + * Indicates whether or not there are currently active UIDL requests. Used + * internally to sequence requests properly, seldom needed in Widgets. + * + * @return true if there are active requests + */ + public boolean hasActiveRequest() { + return hasActiveRequest; + } + + private String getRepaintAllParameters() { + // collect some client side data that will be sent to server on + // initial uidl request + String nativeBootstrapParameters = getNativeBrowserDetailsParameters(getConfiguration() + .getRootPanelId()); + // TODO figure out how client and view size could be used better on + // server. screen size can be accessed via Browser object, but other + // values currently only via transaction listener. + String parameters = "repaintAll=1&" + nativeBootstrapParameters; + return parameters; + } + + /** + * Gets the browser detail parameters that are sent by the bootstrap + * javascript for two-request initialization. + * + * @param parentElementId + * @return + */ + private static native String getNativeBrowserDetailsParameters( + String parentElementId) + /*-{ + return $wnd.vaadin.getBrowserDetailsParameters(parentElementId); + }-*/; + + protected void repaintAll() { + String repainAllParameters = getRepaintAllParameters(); + makeUidlRequest("", repainAllParameters, false); + } + + /** + * Requests an analyze of layouts, to find inconsistencies. Exclusively used + * for debugging during development. + */ + public void analyzeLayouts() { + String params = getRepaintAllParameters() + "&analyzeLayouts=1"; + makeUidlRequest("", params, false); + } + + /** + * Sends a request to the server to print details to console that will help + * developer to locate component in the source code. + * + * @param componentConnector + */ + void highlightComponent(ComponentConnector componentConnector) { + String params = getRepaintAllParameters() + "&highlightComponent=" + + componentConnector.getConnectorId(); + makeUidlRequest("", params, false); + } + + /** + * Makes an UIDL request to the server. + * + * @param requestData + * Data that is passed to the server. + * @param extraParams + * Parameters that are added as GET parameters to the url. + * Contains key=value pairs joined by & characters or is empty if + * no parameters should be added. Should not start with any + * special character. + * @param forceSync + * true if the request should be synchronous, false otherwise + */ + protected void makeUidlRequest(final String requestData, + final String extraParams, final boolean forceSync) { + startRequest(); + // Security: double cookie submission pattern + final String payload = uidlSecurityKey + VAR_BURST_SEPARATOR + + requestData; + VConsole.log("Making UIDL Request with params: " + payload); + String uri = translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX + + ApplicationConstants.UIDL_REQUEST_PATH); + + if (extraParams != null && extraParams.length() > 0) { + uri = addGetParameters(uri, extraParams); + } + uri = addGetParameters(uri, ApplicationConstants.ROOT_ID_PARAMETER + + "=" + configuration.getRootId()); + + doUidlRequest(uri, payload, forceSync); + + } + + /** + * Sends an asynchronous or synchronous UIDL request to the server using the + * given URI. + * + * @param uri + * The URI to use for the request. May includes GET parameters + * @param payload + * The contents of the request to send + * @param synchronous + * true if the request should be synchronous, false otherwise + */ + protected void doUidlRequest(final String uri, final String payload, + final boolean synchronous) { + if (!synchronous) { + RequestCallback requestCallback = new RequestCallback() { + @Override + public void onError(Request request, Throwable exception) { + showCommunicationError(exception.getMessage(), -1); + endRequest(); + } + + @Override + public void onResponseReceived(Request request, + Response response) { + VConsole.log("Server visit took " + + String.valueOf((new Date()).getTime() + - requestStartTime.getTime()) + "ms"); + + int statusCode = response.getStatusCode(); + + switch (statusCode) { + case 0: + showCommunicationError( + "Invalid status code 0 (server down?)", + statusCode); + endRequest(); + return; + + case 401: + /* + * Authorization has failed. Could be that the session + * has timed out and the container is redirecting to a + * login page. + */ + showAuthenticationError(""); + endRequest(); + return; + + case 503: + /* + * We'll assume msec instead of the usual seconds. If + * there's no Retry-After header, handle the error like + * a 500, as per RFC 2616 section 10.5.4. + */ + String delay = response.getHeader("Retry-After"); + if (delay != null) { + VConsole.log("503, retrying in " + delay + "msec"); + (new Timer() { + @Override + public void run() { + doUidlRequest(uri, payload, synchronous); + } + }).schedule(Integer.parseInt(delay)); + return; + } + } + + if ((statusCode / 100) == 4) { + // Handle all 4xx errors the same way as (they are + // all permanent errors) + showCommunicationError( + "UIDL could not be read from server. Check servlets mappings. Error code: " + + statusCode, statusCode); + endRequest(); + return; + } else if ((statusCode / 100) == 5) { + // Something's wrong on the server, there's nothing the + // client can do except maybe try again. + showCommunicationError("Server error. Error code: " + + statusCode, statusCode); + endRequest(); + return; + } + + String contentType = response.getHeader("Content-Type"); + if (contentType == null + || !contentType.startsWith("application/json")) { + /* + * A servlet filter or equivalent may have intercepted + * the request and served non-UIDL content (for + * instance, a login page if the session has expired.) + * If the response contains a magic substring, do a + * synchronous refresh. See #8241. + */ + MatchResult refreshToken = RegExp.compile( + UIDL_REFRESH_TOKEN + "(:\\s*(.*?))?(\\s|$)") + .exec(response.getText()); + if (refreshToken != null) { + redirect(refreshToken.getGroup(2)); + return; + } + } + + // for(;;);[realjson] + final String jsonText = response.getText().substring(9, + response.getText().length() - 1); + handleJSONText(jsonText, statusCode); + } + + }; + try { + doAsyncUIDLRequest(uri, payload, requestCallback); + } catch (RequestException e) { + VConsole.error(e); + endRequest(); + } + } else { + // Synchronized call, discarded response (leaving the page) + SynchronousXHR syncXHR = (SynchronousXHR) SynchronousXHR.create(); + syncXHR.synchronousPost(uri + "&" + + ApplicationConstants.PARAM_UNLOADBURST + "=1", payload); + /* + * Although we are in theory leaving the page, the page may still + * stay open. End request properly here too. See #3289 + */ + endRequest(); + } + + } + + /** + * Handles received UIDL JSON text, parsing it, and passing it on to the + * appropriate handlers, while logging timiing information. + * + * @param jsonText + * @param statusCode + */ + private void handleJSONText(String jsonText, int statusCode) { + final Date start = new Date(); + final ValueMap json; + try { + json = parseJSONResponse(jsonText); + } catch (final Exception e) { + endRequest(); + showCommunicationError(e.getMessage() + " - Original JSON-text:" + + jsonText, statusCode); + return; + } + + VConsole.log("JSON parsing took " + + (new Date().getTime() - start.getTime()) + "ms"); + if (applicationRunning) { + handleReceivedJSONMessage(start, jsonText, json); + } else { + applicationRunning = true; + handleWhenCSSLoaded(jsonText, json); + } + } + + /** + * Sends an asynchronous UIDL request to the server using the given URI. + * + * @param uri + * The URI to use for the request. May includes GET parameters + * @param payload + * The contents of the request to send + * @param requestCallback + * The handler for the response + * @throws RequestException + * if the request could not be sent + */ + protected void doAsyncUIDLRequest(String uri, String payload, + RequestCallback requestCallback) throws RequestException { + RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri); + // TODO enable timeout + // rb.setTimeoutMillis(timeoutMillis); + rb.setHeader("Content-Type", "text/plain;charset=utf-8"); + rb.setRequestData(payload); + rb.setCallback(requestCallback); + + rb.send(); + } + + int cssWaits = 0; + + /** + * Holds the time spent rendering the last request + */ + protected int lastProcessingTime; + + /** + * Holds the total time spent rendering requests during the lifetime of the + * session. + */ + protected int totalProcessingTime; + + /** + * Holds the timing information from the server-side. How much time was + * spent servicing the last request and how much time has been spent + * servicing the session so far. These values are always one request behind, + * since they cannot be measured before the request is finished. + */ + private ValueMap serverTimingInfo; + + static final int MAX_CSS_WAITS = 100; + + protected void handleWhenCSSLoaded(final String jsonText, + final ValueMap json) { + if (!isCSSLoaded() && cssWaits < MAX_CSS_WAITS) { + (new Timer() { + @Override + public void run() { + handleWhenCSSLoaded(jsonText, json); + } + }).schedule(50); + VConsole.log("Assuming CSS loading is not complete, " + + "postponing render phase. " + + "(.v-loading-indicator height == 0)"); + cssWaits++; + } else { + cssLoaded = true; + handleReceivedJSONMessage(new Date(), jsonText, json); + if (cssWaits >= MAX_CSS_WAITS) { + VConsole.error("CSS files may have not loaded properly."); + } + } + } + + /** + * Checks whether or not the CSS is loaded. By default checks the size of + * the loading indicator element. + * + * @return + */ + protected boolean isCSSLoaded() { + return cssLoaded + || DOM.getElementPropertyInt(loadElement, "offsetHeight") != 0; + } + + /** + * Shows the communication error notification. + * + * @param details + * Optional details for debugging. + * @param statusCode + * The status code returned for the request + * + */ + protected void showCommunicationError(String details, int statusCode) { + VConsole.error("Communication error: " + details); + ErrorMessage communicationError = configuration.getCommunicationError(); + showError(details, communicationError.getCaption(), + communicationError.getMessage(), communicationError.getUrl()); + } + + /** + * Shows the authentication error notification. + * + * @param details + * Optional details for debugging. + */ + protected void showAuthenticationError(String details) { + VConsole.error("Authentication error: " + details); + ErrorMessage authorizationError = configuration.getAuthorizationError(); + showError(details, authorizationError.getCaption(), + authorizationError.getMessage(), authorizationError.getUrl()); + } + + /** + * Shows the error notification. + * + * @param details + * Optional details for debugging. + */ + private void showError(String details, String caption, String message, + String url) { + + StringBuilder html = new StringBuilder(); + if (caption != null) { + html.append("<h1>"); + html.append(caption); + html.append("</h1>"); + } + if (message != null) { + html.append("<p>"); + html.append(message); + html.append("</p>"); + } + + if (html.length() > 0) { + + // Add error description + html.append("<br/><p><I style=\"font-size:0.7em\">"); + html.append(details); + html.append("</I></p>"); + + VNotification n = VNotification.createNotification(1000 * 60 * 45); + n.addEventListener(new NotificationRedirect(url)); + n.show(html.toString(), VNotification.CENTERED_TOP, + VNotification.STYLE_SYSTEM); + } else { + redirect(url); + } + } + + protected void startRequest() { + if (hasActiveRequest) { + VConsole.error("Trying to start a new request while another is active"); + } + hasActiveRequest = true; + requestStartTime = new Date(); + // show initial throbber + if (loadTimer == null) { + loadTimer = new Timer() { + @Override + public void run() { + /* + * IE7 does not properly cancel the event with + * loadTimer.cancel() so we have to check that we really + * should make it visible + */ + if (loadTimer != null) { + showLoadingIndicator(); + } + + } + }; + // First one kicks in at 300ms + } + loadTimer.schedule(300); + } + + protected void endRequest() { + if (!hasActiveRequest) { + VConsole.error("No active request"); + } + // After checkForPendingVariableBursts() there may be a new active + // request, so we must set hasActiveRequest to false before, not after, + // the call. Active requests used to be tracked with an integer counter, + // so setting it after used to work but not with the #8505 changes. + hasActiveRequest = false; + if (applicationRunning) { + checkForPendingVariableBursts(); + runPostRequestHooks(configuration.getRootPanelId()); + } + // deferring to avoid flickering + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + if (!hasActiveRequest()) { + hideLoadingIndicator(); + + // If on Liferay and session expiration management is in + // use, extend session duration on each request. + // Doing it here rather than before the request to improve + // responsiveness. + // Postponed until the end of the next request if other + // requests still pending. + extendLiferaySession(); + } + } + }); + } + + /** + * This method is called after applying uidl change set to application. + * + * It will clean current and queued variable change sets. And send next + * change set if it exists. + */ + private void checkForPendingVariableBursts() { + cleanVariableBurst(pendingInvocations); + if (pendingBursts.size() > 0) { + for (Iterator<ArrayList<MethodInvocation>> iterator = pendingBursts + .iterator(); iterator.hasNext();) { + cleanVariableBurst(iterator.next()); + } + ArrayList<MethodInvocation> nextBurst = pendingBursts.get(0); + pendingBursts.remove(0); + buildAndSendVariableBurst(nextBurst, false); + } + } + + /** + * Cleans given queue of variable changes of such changes that came from + * components that do not exist anymore. + * + * @param variableBurst + */ + private void cleanVariableBurst(ArrayList<MethodInvocation> variableBurst) { + for (int i = 1; i < variableBurst.size(); i++) { + String id = variableBurst.get(i).getConnectorId(); + if (!getConnectorMap().hasConnector(id) + && !getConnectorMap().isDragAndDropPaintable(id)) { + // variable owner does not exist anymore + variableBurst.remove(i); + VConsole.log("Removed variable from removed component: " + id); + } + } + } + + private void showLoadingIndicator() { + // show initial throbber + if (loadElement == null) { + loadElement = DOM.createDiv(); + DOM.setStyleAttribute(loadElement, "position", "absolute"); + DOM.appendChild(rootConnector.getWidget().getElement(), loadElement); + VConsole.log("inserting load indicator"); + } + DOM.setElementProperty(loadElement, "className", "v-loading-indicator"); + DOM.setStyleAttribute(loadElement, "display", "block"); + // Initialize other timers + loadTimer2 = new Timer() { + @Override + public void run() { + DOM.setElementProperty(loadElement, "className", + "v-loading-indicator-delay"); + } + }; + // Second one kicks in at 1500ms from request start + loadTimer2.schedule(1200); + + loadTimer3 = new Timer() { + @Override + public void run() { + DOM.setElementProperty(loadElement, "className", + "v-loading-indicator-wait"); + } + }; + // Third one kicks in at 5000ms from request start + loadTimer3.schedule(4700); + } + + private void hideLoadingIndicator() { + if (loadTimer != null) { + loadTimer.cancel(); + loadTimer = null; + } + if (loadTimer2 != null) { + loadTimer2.cancel(); + loadTimer3.cancel(); + loadTimer2 = null; + loadTimer3 = null; + } + if (loadElement != null) { + DOM.setStyleAttribute(loadElement, "display", "none"); + } + } + + /** + * Checks if deferred commands are (potentially) still being executed as a + * result of an update from the server. Returns true if a deferred command + * might still be executing, false otherwise. This will not work correctly + * if a deferred command is added in another deferred command. + * <p> + * Used by the native "client.isActive" function. + * </p> + * + * @return true if deferred commands are (potentially) being executed, false + * otherwise + */ + private boolean isExecutingDeferredCommands() { + Scheduler s = Scheduler.get(); + if (s instanceof VSchedulerImpl) { + return ((VSchedulerImpl) s).hasWorkQueued(); + } else { + return false; + } + } + + /** + * Determines whether or not the loading indicator is showing. + * + * @return true if the loading indicator is visible + */ + public boolean isLoadingIndicatorVisible() { + if (loadElement == null) { + return false; + } + if (loadElement.getStyle().getProperty("display").equals("none")) { + return false; + } + + return true; + } + + private static native ValueMap parseJSONResponse(String jsonText) + /*-{ + try { + return JSON.parse(jsonText); + } catch (ignored) { + return eval('(' + jsonText + ')'); + } + }-*/; + + private void handleReceivedJSONMessage(Date start, String jsonText, + ValueMap json) { + handleUIDLMessage(start, jsonText, json); + } + + protected void handleUIDLMessage(final Date start, final String jsonText, + final ValueMap json) { + VConsole.log("Handling message from server"); + // Handle redirect + if (json.containsKey("redirect")) { + String url = json.getValueMap("redirect").getString("url"); + VConsole.log("redirecting to " + url); + redirect(url); + return; + } + + final MultiStepDuration handleUIDLDuration = new MultiStepDuration(); + + // Get security key + if (json.containsKey(ApplicationConstants.UIDL_SECURITY_TOKEN_ID)) { + uidlSecurityKey = json + .getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID); + } + VConsole.log(" * Handling resources from server"); + + if (json.containsKey("resources")) { + ValueMap resources = json.getValueMap("resources"); + JsArrayString keyArray = resources.getKeyArray(); + int l = keyArray.length(); + for (int i = 0; i < l; i++) { + String key = keyArray.get(i); + resourcesMap.put(key, resources.getAsString(key)); + } + } + handleUIDLDuration.logDuration( + " * Handling resources from server completed", 10); + + VConsole.log(" * Handling type inheritance map from server"); + + if (json.containsKey("typeInheritanceMap")) { + configuration.addComponentInheritanceInfo(json + .getValueMap("typeInheritanceMap")); + } + handleUIDLDuration.logDuration( + " * Handling type inheritance map from server completed", 10); + + VConsole.log("Handling type mappings from server"); + + if (json.containsKey("typeMappings")) { + configuration.addComponentMappings( + json.getValueMap("typeMappings"), widgetSet); + } + + VConsole.log("Handling resource dependencies"); + if (json.containsKey("scriptDependencies")) { + loadScriptDependencies(json.getJSStringArray("scriptDependencies")); + } + if (json.containsKey("styleDependencies")) { + loadStyleDependencies(json.getJSStringArray("styleDependencies")); + } + + handleUIDLDuration.logDuration( + " * Handling type mappings from server completed", 10); + /* + * Hook for e.g. TestBench to get details about server peformance + */ + if (json.containsKey("timings")) { + serverTimingInfo = json.getValueMap("timings"); + } + + Command c = new Command() { + @Override + public void execute() { + handleUIDLDuration.logDuration(" * Loading widgets completed", + 10); + + MultiStepDuration updateDuration = new MultiStepDuration(); + + if (json.containsKey("locales")) { + VConsole.log(" * Handling locales"); + // Store locale data + JsArray<ValueMap> valueMapArray = json + .getJSValueMapArray("locales"); + LocaleService.addLocales(valueMapArray); + } + + updateDuration.logDuration(" * Handling locales completed", 10); + + boolean repaintAll = false; + ValueMap meta = null; + if (json.containsKey("meta")) { + VConsole.log(" * Handling meta information"); + meta = json.getValueMap("meta"); + if (meta.containsKey("repaintAll")) { + repaintAll = true; + rootConnector.getWidget().clear(); + getConnectorMap().clear(); + if (meta.containsKey("invalidLayouts")) { + validatingLayouts = true; + zeroWidthComponents = new HashSet<ComponentConnector>(); + zeroHeightComponents = new HashSet<ComponentConnector>(); + } + } + if (meta.containsKey("timedRedirect")) { + final ValueMap timedRedirect = meta + .getValueMap("timedRedirect"); + redirectTimer = new Timer() { + @Override + public void run() { + redirect(timedRedirect.getString("url")); + } + }; + sessionExpirationInterval = timedRedirect + .getInt("interval"); + } + } + + updateDuration.logDuration( + " * Handling meta information completed", 10); + + if (redirectTimer != null) { + redirectTimer.schedule(1000 * sessionExpirationInterval); + } + + componentCaptionSizeChanges.clear(); + + int startProcessing = updateDuration.elapsedMillis(); + + // Ensure that all connectors that we are about to update exist + createConnectorsIfNeeded(json); + + updateDuration.logDuration(" * Creating connectors completed", + 10); + + // Update states, do not fire events + Collection<StateChangeEvent> pendingStateChangeEvents = updateConnectorState(json); + + updateDuration.logDuration( + " * Update of connector states completed", 10); + + // Update hierarchy, do not fire events + Collection<ConnectorHierarchyChangeEvent> pendingHierarchyChangeEvents = updateConnectorHierarchy(json); + + updateDuration.logDuration( + " * Update of connector hierarchy completed", 10); + + // Fire hierarchy change events + sendHierarchyChangeEvents(pendingHierarchyChangeEvents); + + updateDuration.logDuration( + " * Hierarchy state change event processing completed", + 10); + + // Fire state change events. + sendStateChangeEvents(pendingStateChangeEvents); + + updateDuration.logDuration( + " * State change event processing completed", 10); + + // Update of legacy (UIDL) style connectors + updateVaadin6StyleConnectors(json); + + updateDuration + .logDuration( + " * Vaadin 6 style connector updates (updateFromUidl) completed", + 10); + + // Handle any RPC invocations done on the server side + handleRpcInvocations(json); + + updateDuration.logDuration( + " * Processing of RPC invocations completed", 10); + + if (json.containsKey("dd")) { + // response contains data for drag and drop service + VDragAndDropManager.get().handleServerResponse( + json.getValueMap("dd")); + } + + updateDuration + .logDuration( + " * Processing of drag and drop server response completed", + 10); + + unregisterRemovedConnectors(); + + updateDuration.logDuration( + " * Unregistering of removed components completed", 10); + + VConsole.log("handleUIDLMessage: " + + (updateDuration.elapsedMillis() - startProcessing) + + " ms"); + + LayoutManager layoutManager = getLayoutManager(); + layoutManager.setEverythingNeedsMeasure(); + layoutManager.layoutNow(); + + updateDuration + .logDuration(" * Layout processing completed", 10); + + if (ApplicationConfiguration.isDebugMode()) { + VConsole.log(" * Dumping state changes to the console"); + VConsole.dirUIDL(json, ApplicationConnection.this); + + updateDuration + .logDuration( + " * Dumping state changes to the console completed", + 10); + } + + if (meta != null) { + if (meta.containsKey("appError")) { + ValueMap error = meta.getValueMap("appError"); + String html = ""; + if (error.containsKey("caption") + && error.getString("caption") != null) { + html += "<h1>" + error.getAsString("caption") + + "</h1>"; + } + if (error.containsKey("message") + && error.getString("message") != null) { + html += "<p>" + error.getAsString("message") + + "</p>"; + } + String url = null; + if (error.containsKey("url")) { + url = error.getString("url"); + } + + if (html.length() != 0) { + /* 45 min */ + VNotification n = VNotification + .createNotification(1000 * 60 * 45); + n.addEventListener(new NotificationRedirect(url)); + n.show(html, VNotification.CENTERED_TOP, + VNotification.STYLE_SYSTEM); + } else { + redirect(url); + } + applicationRunning = false; + } + if (validatingLayouts) { + VConsole.printLayoutProblems(meta, + ApplicationConnection.this, + zeroHeightComponents, zeroWidthComponents); + zeroHeightComponents = null; + zeroWidthComponents = null; + validatingLayouts = false; + + } + } + + updateDuration.logDuration(" * Error handling completed", 10); + + // TODO build profiling for widget impl loading time + + lastProcessingTime = (int) ((new Date().getTime()) - start + .getTime()); + totalProcessingTime += lastProcessingTime; + + VConsole.log(" Processing time was " + + String.valueOf(lastProcessingTime) + "ms for " + + jsonText.length() + " characters of JSON"); + VConsole.log("Referenced paintables: " + connectorMap.size()); + + endRequest(); + + } + + /** + * Sends the state change events created while updating the state + * information. + * + * This must be called after hierarchy change listeners have been + * called. At least caption updates for the parent are strange if + * fired from state change listeners and thus calls the parent + * BEFORE the parent is aware of the child (through a + * ConnectorHierarchyChangedEvent) + * + * @param pendingStateChangeEvents + * The events to send + */ + private void sendStateChangeEvents( + Collection<StateChangeEvent> pendingStateChangeEvents) { + VConsole.log(" * Sending state change events"); + + for (StateChangeEvent sce : pendingStateChangeEvents) { + try { + sce.getConnector().fireEvent(sce); + } catch (final Throwable e) { + VConsole.error(e); + } + } + + } + + private void unregisterRemovedConnectors() { + int unregistered = 0; + List<ServerConnector> currentConnectors = new ArrayList<ServerConnector>( + connectorMap.getConnectors()); + for (ServerConnector c : currentConnectors) { + if (c.getParent() != null) { + if (!c.getParent().getChildren().contains(c)) { + VConsole.error("ERROR: Connector is connected to a parent but the parent does not contain the connector"); + } + } else if ((c instanceof RootConnector && c == getRootConnector())) { + // RootConnector for this connection, leave as-is + } else if (c instanceof WindowConnector + && getRootConnector().hasSubWindow( + (WindowConnector) c)) { + // Sub window attached to this RootConnector, leave + // as-is + } else { + // The connector has been detached from the + // hierarchy, unregister it and any possible + // children. The RootConnector should never be + // unregistered even though it has no parent. + connectorMap.unregisterConnector(c); + unregistered++; + } + + } + + VConsole.log("* Unregistered " + unregistered + " connectors"); + } + + private void createConnectorsIfNeeded(ValueMap json) { + VConsole.log(" * Creating connectors (if needed)"); + + if (!json.containsKey("types")) { + return; + } + + ValueMap types = json.getValueMap("types"); + JsArrayString keyArray = types.getKeyArray(); + for (int i = 0; i < keyArray.length(); i++) { + try { + String connectorId = keyArray.get(i); + int connectorType = Integer.parseInt(types + .getString((connectorId))); + ServerConnector connector = connectorMap + .getConnector(connectorId); + if (connector != null) { + continue; + } + + Class<? extends ServerConnector> connectorClass = configuration + .getConnectorClassByEncodedTag(connectorType); + + // Connector does not exist so we must create it + if (connectorClass != RootConnector.class) { + // create, initialize and register the paintable + getConnector(connectorId, connectorType); + } else { + // First RootConnector update. Before this the + // RootConnector has been created but not + // initialized as the connector id has not been + // known + connectorMap.registerConnector(connectorId, + rootConnector); + rootConnector.doInit(connectorId, + ApplicationConnection.this); + } + } catch (final Throwable e) { + VConsole.error(e); + } + } + } + + private void updateVaadin6StyleConnectors(ValueMap json) { + JsArray<ValueMap> changes = json.getJSValueMapArray("changes"); + int length = changes.length(); + + VConsole.log(" * Passing UIDL to Vaadin 6 style connectors"); + // update paintables + for (int i = 0; i < length; i++) { + try { + final UIDL change = changes.get(i).cast(); + final UIDL uidl = change.getChildUIDL(0); + String connectorId = uidl.getId(); + + final ComponentConnector legacyConnector = (ComponentConnector) connectorMap + .getConnector(connectorId); + if (legacyConnector instanceof Paintable) { + ((Paintable) legacyConnector).updateFromUIDL(uidl, + ApplicationConnection.this); + } else if (legacyConnector == null) { + VConsole.error("Received update for " + + uidl.getTag() + + ", but there is no such paintable (" + + connectorId + ") rendered."); + } else { + VConsole.error("Server sent Vaadin 6 style updates for " + + Util.getConnectorString(legacyConnector) + + " but this is not a Vaadin 6 Paintable"); + } + + } catch (final Throwable e) { + VConsole.error(e); + } + } + } + + private void sendHierarchyChangeEvents( + Collection<ConnectorHierarchyChangeEvent> pendingHierarchyChangeEvents) { + if (pendingHierarchyChangeEvents.isEmpty()) { + return; + } + + VConsole.log(" * Sending hierarchy change events"); + for (ConnectorHierarchyChangeEvent event : pendingHierarchyChangeEvents) { + try { + event.getConnector().fireEvent(event); + } catch (final Throwable e) { + VConsole.error(e); + } + } + + } + + private Collection<StateChangeEvent> updateConnectorState( + ValueMap json) { + ArrayList<StateChangeEvent> events = new ArrayList<StateChangeEvent>(); + VConsole.log(" * Updating connector states"); + if (!json.containsKey("state")) { + return events; + } + // set states for all paintables mentioned in "state" + ValueMap states = json.getValueMap("state"); + JsArrayString keyArray = states.getKeyArray(); + for (int i = 0; i < keyArray.length(); i++) { + try { + String connectorId = keyArray.get(i); + ServerConnector connector = connectorMap + .getConnector(connectorId); + if (null != connector) { + + JSONObject stateJson = new JSONObject( + states.getJavaScriptObject(connectorId)); + + if (connector instanceof HasJavaScriptConnectorHelper) { + ((HasJavaScriptConnectorHelper) connector) + .getJavascriptConnectorHelper() + .setNativeState( + stateJson.getJavaScriptObject()); + } + + SharedState state = connector.getState(); + JsonDecoder.decodeValue(new Type(state.getClass() + .getName(), null), stateJson, state, + ApplicationConnection.this); + + StateChangeEvent event = GWT + .create(StateChangeEvent.class); + event.setConnector(connector); + events.add(event); + } + } catch (final Throwable e) { + VConsole.error(e); + } + } + + return events; + } + + /** + * Updates the connector hierarchy and returns a list of events that + * should be fired after update of the hierarchy and the state is + * done. + * + * @param json + * The JSON containing the hierarchy information + * @return A collection of events that should be fired when update + * of hierarchy and state is complete + */ + private Collection<ConnectorHierarchyChangeEvent> updateConnectorHierarchy( + ValueMap json) { + List<ConnectorHierarchyChangeEvent> events = new LinkedList<ConnectorHierarchyChangeEvent>(); + + VConsole.log(" * Updating connector hierarchy"); + if (!json.containsKey("hierarchy")) { + return events; + } + + ValueMap hierarchies = json.getValueMap("hierarchy"); + JsArrayString hierarchyKeys = hierarchies.getKeyArray(); + for (int i = 0; i < hierarchyKeys.length(); i++) { + try { + String connectorId = hierarchyKeys.get(i); + ServerConnector parentConnector = connectorMap + .getConnector(connectorId); + JsArrayString childConnectorIds = hierarchies + .getJSStringArray(connectorId); + int childConnectorSize = childConnectorIds.length(); + + List<ServerConnector> newChildren = new ArrayList<ServerConnector>(); + List<ComponentConnector> newComponents = new ArrayList<ComponentConnector>(); + for (int connectorIndex = 0; connectorIndex < childConnectorSize; connectorIndex++) { + String childConnectorId = childConnectorIds + .get(connectorIndex); + ServerConnector childConnector = connectorMap + .getConnector(childConnectorId); + if (childConnector == null) { + VConsole.error("Hierarchy claims that " + + childConnectorId + " is a child for " + + connectorId + " (" + + parentConnector.getClass().getName() + + ") but no connector with id " + + childConnectorId + + " has been registered"); + continue; + } + newChildren.add(childConnector); + if (childConnector instanceof ComponentConnector) { + newComponents + .add((ComponentConnector) childConnector); + } else if (!(childConnector instanceof AbstractExtensionConnector)) { + throw new IllegalStateException( + Util.getConnectorString(childConnector) + + " is not a ComponentConnector nor an AbstractExtensionConnector"); + } + if (childConnector.getParent() != parentConnector) { + // Avoid extra calls to setParent + childConnector.setParent(parentConnector); + } + } + + // TODO This check should be done on the server side in + // the future so the hierarchy update is only sent when + // something actually has changed + List<ServerConnector> oldChildren = parentConnector + .getChildren(); + boolean actuallyChanged = !Util.collectionsEquals( + oldChildren, newChildren); + + if (!actuallyChanged) { + continue; + } + + if (parentConnector instanceof ComponentContainerConnector) { + ComponentContainerConnector ccc = (ComponentContainerConnector) parentConnector; + List<ComponentConnector> oldComponents = ccc + .getChildComponents(); + if (!Util.collectionsEquals(oldComponents, + newComponents)) { + // Fire change event if the hierarchy has + // changed + ConnectorHierarchyChangeEvent event = GWT + .create(ConnectorHierarchyChangeEvent.class); + event.setOldChildren(oldComponents); + event.setConnector(parentConnector); + ccc.setChildComponents(newComponents); + events.add(event); + } + } else if (!newComponents.isEmpty()) { + VConsole.error("Hierachy claims " + + Util.getConnectorString(parentConnector) + + " has component children even though it isn't a ComponentContainerConnector"); + } + + parentConnector.setChildren(newChildren); + + // Remove parent for children that are no longer + // attached to this (avoid updating children if they + // have already been assigned to a new parent) + for (ServerConnector oldChild : oldChildren) { + if (oldChild.getParent() != parentConnector) { + continue; + } + + // TODO This could probably be optimized + if (!newChildren.contains(oldChild)) { + oldChild.setParent(null); + } + } + } catch (final Throwable e) { + VConsole.error(e); + } + } + return events; + + } + + private void handleRpcInvocations(ValueMap json) { + if (json.containsKey("rpc")) { + VConsole.log(" * Performing server to client RPC calls"); + + JSONArray rpcCalls = new JSONArray( + json.getJavaScriptObject("rpc")); + + int rpcLength = rpcCalls.size(); + for (int i = 0; i < rpcLength; i++) { + try { + JSONArray rpcCall = (JSONArray) rpcCalls.get(i); + rpcManager.parseAndApplyInvocation(rpcCall, + ApplicationConnection.this); + } catch (final Throwable e) { + VConsole.error(e); + } + } + } + + } + + }; + ApplicationConfiguration.runWhenDependenciesLoaded(c); + } + + private void loadStyleDependencies(JsArrayString dependencies) { + // Assuming no reason to interpret in a defined order + ResourceLoadListener resourceLoadListener = new ResourceLoadListener() { + @Override + public void onLoad(ResourceLoadEvent event) { + ApplicationConfiguration.endDependencyLoading(); + } + + @Override + public void onError(ResourceLoadEvent event) { + VConsole.error(event.getResourceUrl() + + " could not be loaded, or the load detection failed because the stylesheet is empty."); + // The show must go on + onLoad(event); + } + }; + ResourceLoader loader = ResourceLoader.get(); + for (int i = 0; i < dependencies.length(); i++) { + String url = translateVaadinUri(dependencies.get(i)); + ApplicationConfiguration.startDependencyLoading(); + loader.loadStylesheet(url, resourceLoadListener); + } + } + + private void loadScriptDependencies(final JsArrayString dependencies) { + if (dependencies.length() == 0) { + return; + } + + // Listener that loads the next when one is completed + ResourceLoadListener resourceLoadListener = new ResourceLoadListener() { + @Override + public void onLoad(ResourceLoadEvent event) { + if (dependencies.length() != 0) { + String url = translateVaadinUri(dependencies.shift()); + ApplicationConfiguration.startDependencyLoading(); + // Load next in chain (hopefully already preloaded) + event.getResourceLoader().loadScript(url, this); + } + // Call start for next before calling end for current + ApplicationConfiguration.endDependencyLoading(); + } + + @Override + public void onError(ResourceLoadEvent event) { + VConsole.error(event.getResourceUrl() + " could not be loaded."); + // The show must go on + onLoad(event); + } + }; + + ResourceLoader loader = ResourceLoader.get(); + + // Start chain by loading first + String url = translateVaadinUri(dependencies.shift()); + ApplicationConfiguration.startDependencyLoading(); + loader.loadScript(url, resourceLoadListener); + + // Preload all remaining + for (int i = 0; i < dependencies.length(); i++) { + String preloadUrl = translateVaadinUri(dependencies.get(i)); + loader.preloadResource(preloadUrl, null); + } + } + + // Redirect browser, null reloads current page + private static native void redirect(String url) + /*-{ + if (url) { + $wnd.location = url; + } else { + $wnd.location.reload(false); + } + }-*/; + + private void addVariableToQueue(String connectorId, String variableName, + Object value, boolean immediate) { + // note that type is now deduced from value + // TODO could eliminate invocations of same shared variable setter + addMethodInvocationToQueue(new MethodInvocation(connectorId, + ApplicationConstants.UPDATE_VARIABLE_INTERFACE, + ApplicationConstants.UPDATE_VARIABLE_METHOD, new Object[] { + variableName, new UidlValue(value) }), immediate); + } + + /** + * Adds an explicit RPC method invocation to the send queue. + * + * @since 7.0 + * + * @param invocation + * RPC method invocation + * @param immediate + * true to trigger sending within a short time window (possibly + * combining subsequent calls to a single request), false to let + * the framework delay sending of RPC calls and variable changes + * until the next immediate change + */ + public void addMethodInvocationToQueue(MethodInvocation invocation, + boolean immediate) { + pendingInvocations.add(invocation); + if (immediate) { + sendPendingVariableChanges(); + } + } + + /** + * This method sends currently queued variable changes to server. It is + * called when immediate variable update must happen. + * + * To ensure correct order for variable changes (due servers multithreading + * or network), we always wait for active request to be handler before + * sending a new one. If there is an active request, we will put varible + * "burst" to queue that will be purged after current request is handled. + * + */ + public void sendPendingVariableChanges() { + if (!deferedSendPending) { + deferedSendPending = true; + Scheduler.get().scheduleDeferred(sendPendingCommand); + } + } + + private final ScheduledCommand sendPendingCommand = new ScheduledCommand() { + @Override + public void execute() { + deferedSendPending = false; + doSendPendingVariableChanges(); + } + }; + private boolean deferedSendPending = false; + + @SuppressWarnings("unchecked") + private void doSendPendingVariableChanges() { + if (applicationRunning) { + if (hasActiveRequest()) { + // skip empty queues if there are pending bursts to be sent + if (pendingInvocations.size() > 0 || pendingBursts.size() == 0) { + pendingBursts.add(pendingInvocations); + pendingInvocations = new ArrayList<MethodInvocation>(); + } + } else { + buildAndSendVariableBurst(pendingInvocations, false); + } + } + } + + /** + * Build the variable burst and send it to server. + * + * When sync is forced, we also force sending of all pending variable-bursts + * at the same time. This is ok as we can assume that DOM will never be + * updated after this. + * + * @param pendingInvocations + * List of RPC method invocations to send + * @param forceSync + * Should we use synchronous request? + */ + private void buildAndSendVariableBurst( + ArrayList<MethodInvocation> pendingInvocations, boolean forceSync) { + final StringBuffer req = new StringBuffer(); + + while (!pendingInvocations.isEmpty()) { + if (ApplicationConfiguration.isDebugMode()) { + Util.logVariableBurst(this, pendingInvocations); + } + + JSONArray reqJson = new JSONArray(); + + for (MethodInvocation invocation : pendingInvocations) { + JSONArray invocationJson = new JSONArray(); + invocationJson.set(0, + new JSONString(invocation.getConnectorId())); + invocationJson.set(1, + new JSONString(invocation.getInterfaceName())); + invocationJson.set(2, + new JSONString(invocation.getMethodName())); + JSONArray paramJson = new JSONArray(); + boolean restrictToInternalTypes = isLegacyVariableChange(invocation); + for (int i = 0; i < invocation.getParameters().length; ++i) { + // TODO non-static encoder? type registration? + paramJson.set(i, JsonEncoder.encode( + invocation.getParameters()[i], + restrictToInternalTypes, this)); + } + invocationJson.set(3, paramJson); + reqJson.set(reqJson.size(), invocationJson); + } + + // escape burst separators (if any) + req.append(escapeBurstContents(reqJson.toString())); + + pendingInvocations.clear(); + // Append all the bursts to this synchronous request + if (forceSync && !pendingBursts.isEmpty()) { + pendingInvocations = pendingBursts.get(0); + pendingBursts.remove(0); + req.append(VAR_BURST_SEPARATOR); + } + } + + // Include the browser detail parameters if they aren't already sent + String extraParams; + if (!getConfiguration().isBrowserDetailsSent()) { + extraParams = getNativeBrowserDetailsParameters(getConfiguration() + .getRootPanelId()); + getConfiguration().setBrowserDetailsSent(); + } else { + extraParams = ""; + } + if (!getConfiguration().isWidgetsetVersionSent()) { + if (!extraParams.isEmpty()) { + extraParams += "&"; + } + String widgetsetVersion = Version.getFullVersion(); + extraParams += "wsver=" + widgetsetVersion; + + getConfiguration().setWidgetsetVersionSent(); + } + makeUidlRequest(req.toString(), extraParams, forceSync); + } + + private boolean isLegacyVariableChange(MethodInvocation invocation) { + return ApplicationConstants.UPDATE_VARIABLE_METHOD.equals(invocation + .getInterfaceName()) + && ApplicationConstants.UPDATE_VARIABLE_METHOD + .equals(invocation.getMethodName()); + } + + /** + * Sends a new value for the given paintables given variable to the server. + * <p> + * The update is actually queued to be sent at a suitable time. If immediate + * is true, the update is sent as soon as possible. If immediate is false, + * the update will be sent along with the next immediate update. + * </p> + * + * @param paintableId + * the id of the paintable that owns the variable + * @param variableName + * the name of the variable + * @param newValue + * the new value to be sent + * @param immediate + * true if the update is to be sent as soon as possible + */ + public void updateVariable(String paintableId, String variableName, + ServerConnector newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, newValue, immediate); + } + + /** + * Sends a new value for the given paintables given variable to the server. + * <p> + * The update is actually queued to be sent at a suitable time. If immediate + * is true, the update is sent as soon as possible. If immediate is false, + * the update will be sent along with the next immediate update. + * </p> + * + * @param paintableId + * the id of the paintable that owns the variable + * @param variableName + * the name of the variable + * @param newValue + * the new value to be sent + * @param immediate + * true if the update is to be sent as soon as possible + */ + + public void updateVariable(String paintableId, String variableName, + String newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, newValue, immediate); + } + + /** + * Sends a new value for the given paintables given variable to the server. + * <p> + * The update is actually queued to be sent at a suitable time. If immediate + * is true, the update is sent as soon as possible. If immediate is false, + * the update will be sent along with the next immediate update. + * </p> + * + * @param paintableId + * the id of the paintable that owns the variable + * @param variableName + * the name of the variable + * @param newValue + * the new value to be sent + * @param immediate + * true if the update is to be sent as soon as possible + */ + + public void updateVariable(String paintableId, String variableName, + int newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, newValue, immediate); + } + + /** + * Sends a new value for the given paintables given variable to the server. + * <p> + * The update is actually queued to be sent at a suitable time. If immediate + * is true, the update is sent as soon as possible. If immediate is false, + * the update will be sent along with the next immediate update. + * </p> + * + * @param paintableId + * the id of the paintable that owns the variable + * @param variableName + * the name of the variable + * @param newValue + * the new value to be sent + * @param immediate + * true if the update is to be sent as soon as possible + */ + + public void updateVariable(String paintableId, String variableName, + long newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, newValue, immediate); + } + + /** + * Sends a new value for the given paintables given variable to the server. + * <p> + * The update is actually queued to be sent at a suitable time. If immediate + * is true, the update is sent as soon as possible. If immediate is false, + * the update will be sent along with the next immediate update. + * </p> + * + * @param paintableId + * the id of the paintable that owns the variable + * @param variableName + * the name of the variable + * @param newValue + * the new value to be sent + * @param immediate + * true if the update is to be sent as soon as possible + */ + + public void updateVariable(String paintableId, String variableName, + float newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, newValue, immediate); + } + + /** + * Sends a new value for the given paintables given variable to the server. + * <p> + * The update is actually queued to be sent at a suitable time. If immediate + * is true, the update is sent as soon as possible. If immediate is false, + * the update will be sent along with the next immediate update. + * </p> + * + * @param paintableId + * the id of the paintable that owns the variable + * @param variableName + * the name of the variable + * @param newValue + * the new value to be sent + * @param immediate + * true if the update is to be sent as soon as possible + */ + + public void updateVariable(String paintableId, String variableName, + double newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, newValue, immediate); + } + + /** + * Sends a new value for the given paintables given variable to the server. + * <p> + * The update is actually queued to be sent at a suitable time. If immediate + * is true, the update is sent as soon as possible. If immediate is false, + * the update will be sent along with the next immediate update. + * </p> + * + * @param paintableId + * the id of the paintable that owns the variable + * @param variableName + * the name of the variable + * @param newValue + * the new value to be sent + * @param immediate + * true if the update is to be sent as soon as possible + */ + + public void updateVariable(String paintableId, String variableName, + boolean newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, newValue, immediate); + } + + /** + * Sends a new value for the given paintables given variable to the server. + * <p> + * The update is actually queued to be sent at a suitable time. If immediate + * is true, the update is sent as soon as possible. If immediate is false, + * the update will be sent along with the next immediate update. + * </p> + * + * @param paintableId + * the id of the paintable that owns the variable + * @param variableName + * the name of the variable + * @param map + * the new values to be sent + * @param immediate + * true if the update is to be sent as soon as possible + */ + public void updateVariable(String paintableId, String variableName, + Map<String, Object> map, boolean immediate) { + addVariableToQueue(paintableId, variableName, map, immediate); + } + + /** + * Sends a new value for the given paintables given variable to the server. + * + * The update is actually queued to be sent at a suitable time. If immediate + * is true, the update is sent as soon as possible. If immediate is false, + * the update will be sent along with the next immediate update. + * + * A null array is sent as an empty array. + * + * @param paintableId + * the id of the paintable that owns the variable + * @param variableName + * the name of the variable + * @param values + * the new value to be sent + * @param immediate + * true if the update is to be sent as soon as possible + */ + public void updateVariable(String paintableId, String variableName, + String[] values, boolean immediate) { + addVariableToQueue(paintableId, variableName, values, immediate); + } + + /** + * Sends a new value for the given paintables given variable to the server. + * + * The update is actually queued to be sent at a suitable time. If immediate + * is true, the update is sent as soon as possible. If immediate is false, + * the update will be sent along with the next immediate update. </p> + * + * A null array is sent as an empty array. + * + * + * @param paintableId + * the id of the paintable that owns the variable + * @param variableName + * the name of the variable + * @param values + * the new value to be sent + * @param immediate + * true if the update is to be sent as soon as possible + */ + public void updateVariable(String paintableId, String variableName, + Object[] values, boolean immediate) { + addVariableToQueue(paintableId, variableName, values, immediate); + } + + /** + * Encode burst separator characters in a String for transport over the + * network. This protects from separator injection attacks. + * + * @param value + * to encode + * @return encoded value + */ + protected String escapeBurstContents(String value) { + final StringBuilder result = new StringBuilder(); + for (int i = 0; i < value.length(); ++i) { + char character = value.charAt(i); + switch (character) { + case VAR_ESCAPE_CHARACTER: + // fall-through - escape character is duplicated + case VAR_BURST_SEPARATOR: + result.append(VAR_ESCAPE_CHARACTER); + // encode as letters for easier reading + result.append(((char) (character + 0x30))); + break; + default: + // the char is not a special one - add it to the result as is + result.append(character); + break; + } + } + return result.toString(); + } + + private boolean runningLayout = false; + + /** + * Causes a re-calculation/re-layout of all paintables in a container. + * + * @param container + */ + public void runDescendentsLayout(HasWidgets container) { + if (runningLayout) { + return; + } + runningLayout = true; + internalRunDescendentsLayout(container); + runningLayout = false; + } + + /** + * This will cause re-layouting of all components. Mainly used for + * development. Published to JavaScript. + */ + public void forceLayout() { + Duration duration = new Duration(); + + layoutManager.forceLayout(); + + VConsole.log("forceLayout in " + duration.elapsedMillis() + " ms"); + } + + private void internalRunDescendentsLayout(HasWidgets container) { + // getConsole().log( + // "runDescendentsLayout(" + Util.getSimpleName(container) + ")"); + final Iterator<Widget> childWidgets = container.iterator(); + while (childWidgets.hasNext()) { + final Widget child = childWidgets.next(); + + if (getConnectorMap().isConnector(child)) { + + if (handleComponentRelativeSize(child)) { + /* + * Only need to propagate event if "child" has a relative + * size + */ + + if (child instanceof ContainerResizedListener) { + ((ContainerResizedListener) child).iLayout(); + } + + if (child instanceof HasWidgets) { + final HasWidgets childContainer = (HasWidgets) child; + internalRunDescendentsLayout(childContainer); + } + } + } else if (child instanceof HasWidgets) { + // propagate over non Paintable HasWidgets + internalRunDescendentsLayout((HasWidgets) child); + } + + } + } + + /** + * Converts relative sizes into pixel sizes. + * + * @param child + * @return true if the child has a relative size + */ + private boolean handleComponentRelativeSize(ComponentConnector paintable) { + return false; + } + + /** + * Converts relative sizes into pixel sizes. + * + * @param child + * @return true if the child has a relative size + */ + public boolean handleComponentRelativeSize(Widget widget) { + return handleComponentRelativeSize(connectorMap.getConnector(widget)); + + } + + @Deprecated + public ComponentConnector getPaintable(UIDL uidl) { + // Non-component connectors shouldn't be painted from legacy connectors + return (ComponentConnector) getConnector(uidl.getId(), + Integer.parseInt(uidl.getTag())); + } + + /** + * Get either an existing ComponentConnector or create a new + * ComponentConnector with the given type and id. + * + * If a ComponentConnector with the given id already exists, returns it. + * Otherwise creates and registers a new ComponentConnector of the given + * type. + * + * @param connectorId + * Id of the paintable + * @param connectorType + * Type of the connector, as passed from the server side + * + * @return Either an existing ComponentConnector or a new ComponentConnector + * of the given type + */ + public ServerConnector getConnector(String connectorId, int connectorType) { + if (!connectorMap.hasConnector(connectorId)) { + return createAndRegisterConnector(connectorId, connectorType); + } + return connectorMap.getConnector(connectorId); + } + + /** + * Creates a new ServerConnector with the given type and id. + * + * Creates and registers a new ServerConnector of the given type. Should + * never be called with the connector id of an existing connector. + * + * @param connectorId + * Id of the new connector + * @param connectorType + * Type of the connector, as passed from the server side + * + * @return A new ServerConnector of the given type + */ + private ServerConnector createAndRegisterConnector(String connectorId, + int connectorType) { + // Create and register a new connector with the given type + ServerConnector p = widgetSet.createConnector(connectorType, + configuration); + connectorMap.registerConnector(connectorId, p); + p.doInit(connectorId, this); + + return p; + } + + /** + * Gets a recource that has been pre-loaded via UIDL, such as custom + * layouts. + * + * @param name + * identifier of the resource to get + * @return the resource + */ + public String getResource(String name) { + return resourcesMap.get(name); + } + + /** + * Singleton method to get instance of app's context menu. + * + * @return VContextMenu object + */ + public VContextMenu getContextMenu() { + if (contextMenu == null) { + contextMenu = new VContextMenu(); + DOM.setElementProperty(contextMenu.getElement(), "id", + "PID_VAADIN_CM"); + } + return contextMenu; + } + + /** + * Translates custom protocols in UIDL URI's to be recognizable by browser. + * All uri's from UIDL should be routed via this method before giving them + * to browser due URI's in UIDL may contain custom protocols like theme://. + * + * @param uidlUri + * Vaadin URI from uidl + * @return translated URI ready for browser + */ + public String translateVaadinUri(String uidlUri) { + if (uidlUri == null) { + return null; + } + if (uidlUri.startsWith("theme://")) { + final String themeUri = configuration.getThemeUri(); + if (themeUri == null) { + VConsole.error("Theme not set: ThemeResource will not be found. (" + + uidlUri + ")"); + } + uidlUri = themeUri + uidlUri.substring(7); + } + + if (uidlUri.startsWith(ApplicationConstants.CONNECTOR_PROTOCOL_PREFIX)) { + // getAppUri *should* always end with / + // substring *should* always start with / (connector:///foo.bar + // without connector://) + uidlUri = ApplicationConstants.APP_PROTOCOL_PREFIX + + ApplicationConstants.CONNECTOR_RESOURCE_PREFIX + + uidlUri + .substring(ApplicationConstants.CONNECTOR_PROTOCOL_PREFIX + .length()); + // Let translation of app:// urls take care of the rest + } + if (uidlUri.startsWith(ApplicationConstants.APP_PROTOCOL_PREFIX)) { + String relativeUrl = uidlUri + .substring(ApplicationConstants.APP_PROTOCOL_PREFIX + .length()); + if (getConfiguration().usePortletURLs()) { + // Should put path in v-resourcePath parameter and append query + // params to base portlet url + String[] parts = relativeUrl.split("\\?", 2); + String path = parts[0]; + + String url = getConfiguration().getPortletResourceUrl(); + + // If there's a "?" followed by something, append it as a query + // string to the base URL + if (parts.length > 1) { + String appUrlParams = parts[1]; + url = addGetParameters(url, appUrlParams); + } + if (!path.startsWith("/")) { + path = '/' + path; + } + String pathParam = ApplicationConstants.V_RESOURCE_PATH + "=" + + URL.encodeQueryString(path); + url = addGetParameters(url, pathParam); + uidlUri = url; + } else { + uidlUri = getAppUri() + relativeUrl; + } + } + return uidlUri; + } + + /** + * Gets the URI for the current theme. Can be used to reference theme + * resources. + * + * @return URI to the current theme + */ + public String getThemeUri() { + return configuration.getThemeUri(); + } + + /** + * Listens for Notification hide event, and redirects. Used for system + * messages, such as session expired. + * + */ + private class NotificationRedirect implements VNotification.EventListener { + String url; + + NotificationRedirect(String url) { + this.url = url; + } + + @Override + public void notificationHidden(HideEvent event) { + redirect(url); + } + + } + + /* Extended title handling */ + + private final VTooltip tooltip = new VTooltip(this); + + private ConnectorMap connectorMap = GWT.create(ConnectorMap.class); + + protected String getUidlSecurityKey() { + return uidlSecurityKey; + } + + /** + * Use to notify that the given component's caption has changed; layouts may + * have to be recalculated. + * + * @param component + * the Paintable whose caption has changed + */ + public void captionSizeUpdated(Widget widget) { + componentCaptionSizeChanges.add(widget); + } + + /** + * Gets the main view + * + * @return the main view + */ + public RootConnector getRootConnector() { + return rootConnector; + } + + /** + * Gets the {@link ApplicationConfiguration} for the current application. + * + * @see ApplicationConfiguration + * @return the configuration for this application + */ + public ApplicationConfiguration getConfiguration() { + return configuration; + } + + /** + * Checks if there is a registered server side listener for the event. The + * list of events which has server side listeners is updated automatically + * before the component is updated so the value is correct if called from + * updatedFromUIDL. + * + * @param paintable + * The connector to register event listeners for + * @param eventIdentifier + * The identifier for the event + * @return true if at least one listener has been registered on server side + * for the event identified by eventIdentifier. + * @deprecated Use {@link ComponentState#hasEventListener(String)} instead + */ + @Deprecated + public boolean hasEventListeners(ComponentConnector paintable, + String eventIdentifier) { + return paintable.hasEventListener(eventIdentifier); + } + + /** + * Adds the get parameters to the uri and returns the new uri that contains + * the parameters. + * + * @param uri + * The uri to which the parameters should be added. + * @param extraParams + * One or more parameters in the format "a=b" or "c=d&e=f". An + * empty string is allowed but will not modify the url. + * @return The modified URI with the get parameters in extraParams added. + */ + public static String addGetParameters(String uri, String extraParams) { + if (extraParams == null || extraParams.length() == 0) { + return uri; + } + // RFC 3986: The query component is indicated by the first question + // mark ("?") character and terminated by a number sign ("#") character + // or by the end of the URI. + String fragment = null; + int hashPosition = uri.indexOf('#'); + if (hashPosition != -1) { + // Fragment including "#" + fragment = uri.substring(hashPosition); + // The full uri before the fragment + uri = uri.substring(0, hashPosition); + } + + if (uri.contains("?")) { + uri += "&"; + } else { + uri += "?"; + } + uri += extraParams; + + if (fragment != null) { + uri += fragment; + } + + return uri; + } + + ConnectorMap getConnectorMap() { + return connectorMap; + } + + @Deprecated + public void unregisterPaintable(ServerConnector p) { + System.out.println("unregisterPaintable (unnecessarily) called for " + + Util.getConnectorString(p)); + // connectorMap.unregisterConnector(p); + } + + /** + * Get VTooltip instance related to application connection + * + * @return VTooltip instance + */ + public VTooltip getVTooltip() { + return tooltip; + } + + /** + * Method provided for backwards compatibility. Duties previously done by + * this method is now handled by the state change event handler in + * AbstractComponentConnector. The only function this method has is to + * return true if the UIDL is a "cached" update. + * + * @param component + * @param uidl + * @param manageCaption + * @return + */ + @Deprecated + public boolean updateComponent(Widget component, UIDL uidl, + boolean manageCaption) { + ComponentConnector connector = getConnectorMap() + .getConnector(component); + if (!AbstractComponentConnector.isRealUpdate(uidl)) { + return true; + } + + if (!manageCaption) { + VConsole.error(Util.getConnectorString(connector) + + " called updateComponent with manageCaption=false. The parameter was ignored - override delegateCaption() to return false instead. It is however not recommended to use caption this way at all."); + } + return false; + } + + @Deprecated + public boolean hasEventListeners(Widget widget, String eventIdentifier) { + return hasEventListeners(getConnectorMap().getConnector(widget), + eventIdentifier); + } + + LayoutManager getLayoutManager() { + return layoutManager; + } + + public SerializerMap getSerializerMap() { + return serializerMap; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/BrowserInfo.java b/client/src/com/vaadin/terminal/gwt/client/BrowserInfo.java new file mode 100644 index 0000000000..de2d9a9cd8 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/BrowserInfo.java @@ -0,0 +1,390 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.user.client.ui.RootPanel; +import com.vaadin.shared.VBrowserDetails; + +/** + * Class used to query information about web browser. + * + * Browser details are detected only once and those are stored in this singleton + * class. + * + */ +public class BrowserInfo { + + private static final String BROWSER_OPERA = "op"; + private static final String BROWSER_IE = "ie"; + private static final String BROWSER_FIREFOX = "ff"; + private static final String BROWSER_SAFARI = "sa"; + + public static final String ENGINE_GECKO = "gecko"; + public static final String ENGINE_WEBKIT = "webkit"; + public static final String ENGINE_PRESTO = "presto"; + public static final String ENGINE_TRIDENT = "trident"; + + private static final String OS_WINDOWS = "win"; + private static final String OS_LINUX = "lin"; + private static final String OS_MACOSX = "mac"; + private static final String OS_ANDROID = "android"; + private static final String OS_IOS = "ios"; + + // Common CSS class for all touch devices + private static final String UI_TOUCH = "touch"; + + private static BrowserInfo instance; + + private static String cssClass = null; + + static { + // Add browser dependent v-* classnames to body to help css hacks + String browserClassnames = get().getCSSClass(); + RootPanel.get().addStyleName(browserClassnames); + } + + /** + * Singleton method to get BrowserInfo object. + * + * @return instance of BrowserInfo object + */ + public static BrowserInfo get() { + if (instance == null) { + instance = new BrowserInfo(); + } + return instance; + } + + private VBrowserDetails browserDetails; + private boolean touchDevice; + + private BrowserInfo() { + browserDetails = new VBrowserDetails(getBrowserString()); + if (browserDetails.isIE()) { + // Use document mode instead user agent to accurately detect how we + // are rendering + int documentMode = getIEDocumentMode(); + if (documentMode != -1) { + browserDetails.setIEMode(documentMode); + } + } + + if (browserDetails.isChrome()) { + touchDevice = detectChromeTouchDevice(); + } else { + touchDevice = detectTouchDevice(); + } + } + + private native boolean detectTouchDevice() + /*-{ + try { document.createEvent("TouchEvent");return true;} catch(e){return false;}; + }-*/; + + private native boolean detectChromeTouchDevice() + /*-{ + return ("ontouchstart" in window); + }-*/; + + private native int getIEDocumentMode() + /*-{ + var mode = $wnd.document.documentMode; + if (!mode) + return -1; + return mode; + }-*/; + + /** + * Returns a string representing the browser in use, for use in CSS + * classnames. The classnames will be space separated abbreviations, + * optionally with a version appended. + * + * Abbreviations: Firefox: ff Internet Explorer: ie Safari: sa Opera: op + * + * Browsers that CSS-wise behave like each other will get the same + * abbreviation (this usually depends on the rendering engine). + * + * This is quite simple at the moment, more heuristics will be added when + * needed. + * + * Examples: Internet Explorer 6: ".v-ie .v-ie6 .v-ie60", Firefox 3.0.4: + * ".v-ff .v-ff3 .v-ff30", Opera 9.60: ".v-op .v-op9 .v-op960", Opera 10.10: + * ".v-op .v-op10 .v-op1010" + * + * @return + */ + public String getCSSClass() { + String prefix = "v-"; + + if (cssClass == null) { + String browserIdentifier = ""; + String majorVersionClass = ""; + String minorVersionClass = ""; + String browserEngineClass = ""; + + if (browserDetails.isFirefox()) { + browserIdentifier = BROWSER_FIREFOX; + majorVersionClass = browserIdentifier + + browserDetails.getBrowserMajorVersion(); + minorVersionClass = majorVersionClass + + browserDetails.getBrowserMinorVersion(); + browserEngineClass = ENGINE_GECKO; + } else if (browserDetails.isChrome()) { + // TODO update when Chrome is more stable + browserIdentifier = BROWSER_SAFARI; + majorVersionClass = "ch"; + browserEngineClass = ENGINE_WEBKIT; + } else if (browserDetails.isSafari()) { + browserIdentifier = BROWSER_SAFARI; + majorVersionClass = browserIdentifier + + browserDetails.getBrowserMajorVersion(); + minorVersionClass = majorVersionClass + + browserDetails.getBrowserMinorVersion(); + browserEngineClass = ENGINE_WEBKIT; + } else if (browserDetails.isIE()) { + browserIdentifier = BROWSER_IE; + majorVersionClass = browserIdentifier + + browserDetails.getBrowserMajorVersion(); + minorVersionClass = majorVersionClass + + browserDetails.getBrowserMinorVersion(); + browserEngineClass = ENGINE_TRIDENT; + } else if (browserDetails.isOpera()) { + browserIdentifier = BROWSER_OPERA; + majorVersionClass = browserIdentifier + + browserDetails.getBrowserMajorVersion(); + minorVersionClass = majorVersionClass + + browserDetails.getBrowserMinorVersion(); + browserEngineClass = ENGINE_PRESTO; + } + + cssClass = prefix + browserIdentifier; + if (!"".equals(majorVersionClass)) { + cssClass = cssClass + " " + prefix + majorVersionClass; + } + if (!"".equals(minorVersionClass)) { + cssClass = cssClass + " " + prefix + minorVersionClass; + } + if (!"".equals(browserEngineClass)) { + cssClass = cssClass + " " + prefix + browserEngineClass; + } + String osClass = getOperatingSystemClass(); + if (osClass != null) { + cssClass = cssClass + " " + prefix + osClass; + } + if (isTouchDevice()) { + cssClass = cssClass + " " + prefix + UI_TOUCH; + } + } + + return cssClass; + } + + private String getOperatingSystemClass() { + if (browserDetails.isAndroid()) { + return OS_ANDROID; + } else if (browserDetails.isIOS()) { + return OS_IOS; + } else if (browserDetails.isWindows()) { + return OS_WINDOWS; + } else if (browserDetails.isLinux()) { + return OS_LINUX; + } else if (browserDetails.isMacOSX()) { + return OS_MACOSX; + } + // Unknown OS + return null; + } + + public boolean isIE() { + return browserDetails.isIE(); + } + + public boolean isFirefox() { + return browserDetails.isFirefox(); + } + + public boolean isSafari() { + return browserDetails.isSafari(); + } + + public boolean isIE8() { + return isIE() && browserDetails.getBrowserMajorVersion() == 8; + } + + public boolean isIE9() { + return isIE() && browserDetails.getBrowserMajorVersion() == 9; + } + + public boolean isChrome() { + return browserDetails.isChrome(); + } + + public boolean isGecko() { + return browserDetails.isGecko(); + } + + public boolean isWebkit() { + return browserDetails.isWebKit(); + } + + /** + * Returns the Gecko version if the browser is Gecko based. The Gecko + * version for Firefox 2 is 1.8 and 1.9 for Firefox 3. + * + * @return The Gecko version or -1 if the browser is not Gecko based + */ + public float getGeckoVersion() { + if (!browserDetails.isGecko()) { + return -1; + } + + return browserDetails.getBrowserEngineVersion(); + } + + /** + * Returns the WebKit version if the browser is WebKit based. The WebKit + * version returned is the major version e.g., 523. + * + * @return The WebKit version or -1 if the browser is not WebKit based + */ + public float getWebkitVersion() { + if (!browserDetails.isWebKit()) { + return -1; + } + + return browserDetails.getBrowserEngineVersion(); + } + + public float getIEVersion() { + if (!browserDetails.isIE()) { + return -1; + } + + return browserDetails.getBrowserMajorVersion(); + } + + public float getOperaVersion() { + if (!browserDetails.isOpera()) { + return -1; + } + + return browserDetails.getBrowserMajorVersion(); + } + + public boolean isOpera() { + return browserDetails.isOpera(); + } + + public boolean isOpera10() { + return browserDetails.isOpera() + && browserDetails.getBrowserMajorVersion() == 10; + } + + public boolean isOpera11() { + return browserDetails.isOpera() + && browserDetails.getBrowserMajorVersion() == 11; + } + + public native static String getBrowserString() + /*-{ + return $wnd.navigator.userAgent; + }-*/; + + public native int getScreenWidth() + /*-{ + return $wnd.screen.width; + }-*/; + + public native int getScreenHeight() + /*-{ + return $wnd.screen.height; + }-*/; + + /** + * @return true if the browser runs on a touch based device. + */ + public boolean isTouchDevice() { + return touchDevice; + } + + /** + * Indicates whether the browser might require juggling to properly update + * sizes inside elements with overflow: auto. + * + * @return <code>true</code> if the browser requires the workaround, + * otherwise <code>false</code> + */ + public boolean requiresOverflowAutoFix() { + return (getWebkitVersion() > 0 || getOperaVersion() >= 11) + && Util.getNativeScrollbarSize() > 0; + } + + /** + * Checks if the browser is run on iOS + * + * @return true if the browser is run on iOS, false otherwise + */ + public boolean isIOS() { + return browserDetails.isIOS(); + } + + /** + * Checks if the browser is run on Android + * + * @return true if the browser is run on Android, false otherwise + */ + public boolean isAndroid() { + return browserDetails.isAndroid(); + } + + /** + * Checks if the browser is capable of handling scrolling natively or if a + * touch scroll helper is needed for scrolling. + * + * @return true if browser needs a touch scroll helper, false if the browser + * can handle scrolling natively + */ + public boolean requiresTouchScrollDelegate() { + if (!isTouchDevice()) { + return false; + } + if (isAndroid() && isWebkit() && getWebkitVersion() >= 534) { + return false; + } + // Cannot enable native touch scrolling on iOS 5 until #8792 is resolved + // if (isIOS() && isWebkit() && getWebkitVersion() >= 534) { + // return false; + // } + return true; + } + + /** + * Tests if this is an Android devices with a broken scrollTop + * implementation + * + * @return true if scrollTop cannot be trusted on this device, false + * otherwise + */ + public boolean isAndroidWithBrokenScrollTop() { + return isAndroid() + && (getOperatingSystemMajorVersion() == 3 || getOperatingSystemMajorVersion() == 4); + } + + private int getOperatingSystemMajorVersion() { + return browserDetails.getOperatingSystemMajorVersion(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/CSSRule.java b/client/src/com/vaadin/terminal/gwt/client/CSSRule.java new file mode 100644 index 0000000000..1571e9bdf0 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/CSSRule.java @@ -0,0 +1,132 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.core.client.JavaScriptObject; + +/** + * Utility class for fetching CSS properties from DOM StyleSheets JS object. + */ +public class CSSRule { + + private final String selector; + private JavaScriptObject rules = null; + + /** + * + * @param selector + * the CSS selector to search for in the stylesheets + * @param deep + * should the search follow any @import statements? + */ + public CSSRule(final String selector, final boolean deep) { + this.selector = selector; + fetchRule(selector, deep); + } + + // TODO how to find the right LINK-element? We should probably give the + // stylesheet a name. + private native void fetchRule(final String selector, final boolean deep) + /*-{ + var sheets = $doc.styleSheets; + for(var i = 0; i < sheets.length; i++) { + var sheet = sheets[i]; + if(sheet.href && sheet.href.indexOf("VAADIN/themes")>-1) { + // $entry not needed as function is not exported + this.@com.vaadin.terminal.gwt.client.CSSRule::rules = @com.vaadin.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;Z)(sheet, selector, deep); + return; + } + } + this.@com.vaadin.terminal.gwt.client.CSSRule::rules = []; + }-*/; + + /* + * Loops through all current style rules and collects all matching to + * 'rules' array. The array is reverse ordered (last one found is first). + */ + private static native JavaScriptObject searchForRule( + final JavaScriptObject sheet, final String selector, + final boolean deep) + /*-{ + if(!$doc.styleSheets) + return null; + + selector = selector.toLowerCase(); + + var allMatches = []; + + // IE handles imported sheet differently + if(deep && sheet.imports && sheet.imports.length > 0) { + for(var i=0; i < sheet.imports.length; i++) { + // $entry not needed as function is not exported + var imports = @com.vaadin.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;Z)(sheet.imports[i], selector, deep); + allMatches.concat(imports); + } + } + + var theRules = new Array(); + if (sheet.cssRules) + theRules = sheet.cssRules + else if (sheet.rules) + theRules = sheet.rules + + var j = theRules.length; + for(var i=0; i<j; i++) { + var r = theRules[i]; + if(r.type == 1 || sheet.imports) { + var selectors = r.selectorText.toLowerCase().split(","); + var n = selectors.length; + for(var m=0; m<n; m++) { + if(selectors[m].replace(/^\s+|\s+$/g, "") == selector) { + allMatches.unshift(r); + break; // No need to loop other selectors for this rule + } + } + } else if(deep && r.type == 3) { + // Search @import stylesheet + // $entry not needed as function is not exported + var imports = @com.vaadin.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;Z)(r.styleSheet, selector, deep); + allMatches = allMatches.concat(imports); + } + } + + return allMatches; + }-*/; + + /** + * Returns a specific property value from this CSS rule. + * + * @param propertyName + * camelCase CSS property name + * @return the value of the property as a String + */ + public native String getPropertyValue(final String propertyName) + /*-{ + var j = this.@com.vaadin.terminal.gwt.client.CSSRule::rules.length; + for(var i=0; i<j; i++) { + // $entry not needed as function is not exported + var value = this.@com.vaadin.terminal.gwt.client.CSSRule::rules[i].style[propertyName]; + if(value) + return value; + } + return null; + }-*/; + + public String getSelector() { + return selector; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java b/client/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java new file mode 100644 index 0000000000..d8c7e67638 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.core.client.GWT; + +@Deprecated +public class ClientExceptionHandler { + + public static void displayError(Throwable e) { + displayError(e.getClass().getName() + ": " + e.getMessage()); + + GWT.log(e.getMessage(), e); + } + + @Deprecated + public static void displayError(String msg) { + VConsole.error(msg); + GWT.log(msg); + } + + @Deprecated + public static void displayError(String msg, Throwable e) { + displayError(msg); + displayError(e); + + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ComponentConnector.java b/client/src/com/vaadin/terminal/gwt/client/ComponentConnector.java new file mode 100644 index 0000000000..066e609d2c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ComponentConnector.java @@ -0,0 +1,132 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ComponentState; + +/** + * An interface used by client-side widgets or paintable parts to receive + * updates from the corresponding server-side components in the form of + * {@link UIDL}. + * + * Updates can be sent back to the server using the + * {@link ApplicationConnection#updateVariable()} methods. + */ +public interface ComponentConnector extends ServerConnector { + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.VPaintable#getState() + */ + @Override + public ComponentState getState(); + + /** + * Returns the widget for this {@link ComponentConnector} + */ + public Widget getWidget(); + + public LayoutManager getLayoutManager(); + + /** + * Returns <code>true</code> if the width of this paintable is currently + * undefined. If the width is undefined, the actual width of the paintable + * is defined by its contents. + * + * @return <code>true</code> if the width is undefined, else + * <code>false</code> + */ + public boolean isUndefinedWidth(); + + /** + * Returns <code>true</code> if the height of this paintable is currently + * undefined. If the height is undefined, the actual height of the paintable + * is defined by its contents. + * + * @return <code>true</code> if the height is undefined, else + * <code>false</code> + */ + public boolean isUndefinedHeight(); + + /** + * Returns <code>true</code> if the width of this paintable is currently + * relative. If the width is relative, the actual width of the paintable is + * a percentage of the size allocated to it by its parent. + * + * @return <code>true</code> if the width is undefined, else + * <code>false</code> + */ + public boolean isRelativeWidth(); + + /** + * Returns <code>true</code> if the height of this paintable is currently + * relative. If the height is relative, the actual height of the paintable + * is a percentage of the size allocated to it by its parent. + * + * @return <code>true</code> if the width is undefined, else + * <code>false</code> + */ + public boolean isRelativeHeight(); + + /** + * Checks if the connector is read only. + * + * @deprecated This belongs in AbstractFieldConnector, see #8514 + * @return true + */ + @Deprecated + public boolean isReadOnly(); + + public boolean hasEventListener(String eventIdentifier); + + /** + * Return true if parent handles caption, false if the paintable handles the + * caption itself. + * + * <p> + * This should always return true and all components should let the parent + * handle the caption and use other attributes for internal texts in the + * component + * </p> + * + * @return true if caption handling is delegated to the parent, false if + * parent should not be allowed to render caption + */ + public boolean delegateCaptionHandling(); + + /** + * Sets the enabled state of the widget associated to this connector. + * + * @param widgetEnabled + * true if the widget should be enabled, false otherwise + */ + public void setWidgetEnabled(boolean widgetEnabled); + + /** + * Gets the tooltip info for the given element. + * + * @param element + * The element to lookup a tooltip for + * @return The tooltip for the element or null if no tooltip is defined for + * this element. + */ + public TooltipInfo getTooltipInfo(Element element); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ComponentContainerConnector.java b/client/src/com/vaadin/terminal/gwt/client/ComponentContainerConnector.java new file mode 100644 index 0000000000..b74e77d01f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ComponentContainerConnector.java @@ -0,0 +1,86 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.List; + +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.HasWidgets; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler; + +/** + * An interface used by client-side connectors whose widget is a component + * container (implements {@link HasWidgets}). + */ +public interface ComponentContainerConnector extends ServerConnector { + + /** + * Update child components caption, description and error message. + * + * <p> + * Each component is responsible for maintaining its caption, description + * and error message. In most cases components doesn't want to do that and + * those elements reside outside of the component. Because of this layouts + * must provide service for it's childen to show those elements for them. + * </p> + * + * @param connector + * Child component for which service is requested. + */ + void updateCaption(ComponentConnector connector); + + /** + * Returns the children for this connector. + * <p> + * The children for this connector are defined as all + * {@link ComponentConnector}s whose parent is this + * {@link ComponentContainerConnector}. + * </p> + * + * @return A collection of children for this connector. An empty collection + * if there are no children. Never returns null. + */ + public List<ComponentConnector> getChildComponents(); + + /** + * Sets the children for this connector. This method should only be called + * by the framework to ensure that the connector hierarchy on the client + * side and the server side are in sync. + * <p> + * Note that calling this method does not call + * {@link ConnectorHierarchyChangeHandler#onConnectorHierarchyChange(ConnectorHierarchyChangeEvent)} + * . The event method is called only when the hierarchy has been updated for + * all connectors. + * + * @param children + * The new child connectors + */ + public void setChildComponents(List<ComponentConnector> children); + + /** + * Adds a handler that is called whenever the child hierarchy of this + * connector has been updated by the server. + * + * @param handler + * The handler that should be added. + * @return A handler registration reference that can be used to unregister + * the handler + */ + public HandlerRegistration addConnectorHierarchyChangeHandler( + ConnectorHierarchyChangeHandler handler); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ComponentDetail.java b/client/src/com/vaadin/terminal/gwt/client/ComponentDetail.java new file mode 100644 index 0000000000..cd7ad0178f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ComponentDetail.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.HashMap; + +class ComponentDetail { + + private TooltipInfo tooltipInfo = new TooltipInfo(); + + public ComponentDetail() { + + } + + /** + * Returns a TooltipInfo assosiated with Component. If element is given, + * returns an additional TooltipInfo. + * + * @param key + * @return the tooltipInfo + */ + public TooltipInfo getTooltipInfo(Object key) { + if (key == null) { + return tooltipInfo; + } else { + if (additionalTooltips != null) { + return additionalTooltips.get(key); + } else { + return null; + } + } + } + + /** + * @param tooltipInfo + * the tooltipInfo to set + */ + public void setTooltipInfo(TooltipInfo tooltipInfo) { + this.tooltipInfo = tooltipInfo; + } + + private HashMap<Object, TooltipInfo> additionalTooltips; + + public void putAdditionalTooltip(Object key, TooltipInfo tooltip) { + if (tooltip == null && additionalTooltips != null) { + additionalTooltips.remove(key); + } else { + if (additionalTooltips == null) { + additionalTooltips = new HashMap<Object, TooltipInfo>(); + } + additionalTooltips.put(key, tooltip); + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java b/client/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java new file mode 100644 index 0000000000..3566d4e86f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java @@ -0,0 +1,88 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.ArrayList; +import java.util.Collection; + +import com.google.gwt.core.client.JavaScriptObject; + +final class ComponentDetailMap extends JavaScriptObject { + + protected ComponentDetailMap() { + } + + static ComponentDetailMap create() { + return (ComponentDetailMap) JavaScriptObject.createObject(); + } + + boolean isEmpty() { + return size() == 0; + } + + final native boolean containsKey(String key) + /*-{ + return this.hasOwnProperty(key); + }-*/; + + final native ComponentDetail get(String key) + /*-{ + return this[key]; + }-*/; + + final native void put(String id, ComponentDetail value) + /*-{ + this[id] = value; + }-*/; + + final native void remove(String id) + /*-{ + delete this[id]; + }-*/; + + final native int size() + /*-{ + var count = 0; + for(var key in this) { + count++; + } + return count; + }-*/; + + final native void clear() + /*-{ + for(var key in this) { + if(this.hasOwnProperty(key)) { + delete this[key]; + } + } + }-*/; + + private final native void fillWithValues(Collection<ComponentDetail> list) + /*-{ + for(var key in this) { + // $entry not needed as function is not exported + list.@java.util.Collection::add(Ljava/lang/Object;)(this[key]); + } + }-*/; + + final Collection<ComponentDetail> values() { + ArrayList<ComponentDetail> list = new ArrayList<ComponentDetail>(); + fillWithValues(list); + return list; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ComponentLocator.java b/client/src/com/vaadin/terminal/gwt/client/ComponentLocator.java new file mode 100644 index 0000000000..959f03e46d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ComponentLocator.java @@ -0,0 +1,622 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +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.shared.ComponentState; +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.terminal.gwt.client.ui.SubPartAware; +import com.vaadin.terminal.gwt.client.ui.gridlayout.VGridLayout; +import com.vaadin.terminal.gwt.client.ui.orderedlayout.VMeasuringOrderedLayout; +import com.vaadin.terminal.gwt.client.ui.root.VRoot; +import com.vaadin.terminal.gwt.client.ui.tabsheet.VTabsheetPanel; +import com.vaadin.terminal.gwt.client.ui.window.VWindow; +import com.vaadin.terminal.gwt.client.ui.window.WindowConnector; + +/** + * ComponentLocator provides methods for generating a String locator for a given + * DOM element and for locating a DOM element using a String locator. + */ +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; + + /** + * Construct a ComponentLocator for the given ApplicationConnection. + * + * @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; + + 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; + } + + 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 + */ + return path + getDOMPathForElement(targetElement, w.getElement()); + } + + /** + * 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); + try { + int childIndex = Integer.parseInt(childIndexString); + element = DOM.getChild(element, childIndex); + } catch (Exception e) { + return null; + } + } + } + + 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(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) { + Element parent = DOM.getParent(e); + if (parent == null) { + return null; + } + + int childIndex = -1; + + int childCount = DOM.getChildCount(parent); + for (int i = 0; i < childCount; i++) { + if (e == DOM.getChild(parent, i)) { + childIndex = i; + break; + } + } + if (childIndex == -1) { + return null; + } + + path = PARENTCHILD_SEPARATOR + "domChild[" + childIndex + "]" + + path; + + if (parent == baseElement) { + break; + } + + e = parent; + } + + 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; + } + + if (w instanceof VRoot) { + return ""; + } else if (w instanceof VWindow) { + Connector windowConnector = ConnectorMap.get(client) + .getConnector(w); + List<WindowConnector> subWindowList = client.getRootConnector() + .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.getRootConnector().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 debugId + // TODO Optimize this + connector = findConnectorById(client.getRootConnector(), + 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]; + int widgetPosition = Integer.parseInt(indexString.substring(0, + indexString.length() - 1)); + + // AbsolutePanel in GridLayout has been removed -> skip it + if (w instanceof VGridLayout + && "AbsolutePanel".equals(widgetClassName)) { + continue; + } + + 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; + } + + /* + * 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 VMeasuringOrderedLayout || 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.getRootConnector() + .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)) { + 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 ComponentState + && id.equals(((ComponentState) state).getDebugId())) { + 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/terminal/gwt/client/ComputedStyle.java b/client/src/com/vaadin/terminal/gwt/client/ComputedStyle.java new file mode 100644 index 0000000000..7662ba634b --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ComputedStyle.java @@ -0,0 +1,198 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.dom.client.Element; + +public class ComputedStyle { + + protected final JavaScriptObject computedStyle; + private final Element elem; + + /** + * Gets this element's computed style object which can be used to gather + * information about the current state of the rendered node. + * <p> + * Note that this method is expensive. Wherever possible, reuse the returned + * object. + * + * @param elem + * the element + * @return the computed style + */ + public ComputedStyle(Element elem) { + computedStyle = getComputedStyle(elem); + this.elem = elem; + } + + private static native JavaScriptObject getComputedStyle(Element elem) + /*-{ + if(elem.nodeType != 1) { + return {}; + } + + if($wnd.document.defaultView && $wnd.document.defaultView.getComputedStyle) { + return $wnd.document.defaultView.getComputedStyle(elem, null); + } + + if(elem.currentStyle) { + return elem.currentStyle; + } + }-*/; + + /** + * + * @param name + * name of the CSS property in camelCase + * @return the value of the property, normalized for across browsers (each + * browser returns pixel values whenever possible). + */ + public final native String getProperty(String name) + /*-{ + var cs = this.@com.vaadin.terminal.gwt.client.ComputedStyle::computedStyle; + var elem = this.@com.vaadin.terminal.gwt.client.ComputedStyle::elem; + + // Border values need to be checked separately. The width might have a + // meaningful value even if the border style is "none". In that case the + // value should be 0. + if(name.indexOf("border") > -1 && name.indexOf("Width") > -1) { + var borderStyleProp = name.substring(0,name.length-5) + "Style"; + if(cs.getPropertyValue) + var borderStyle = cs.getPropertyValue(borderStyleProp); + else // IE + var borderStyle = cs[borderStyleProp]; + if(borderStyle == "none") + return "0px"; + } + + if(cs.getPropertyValue) { + + // Convert name to dashed format + name = name.replace(/([A-Z])/g, "-$1").toLowerCase(); + var ret = cs.getPropertyValue(name); + + } else { + + var ret = cs[name]; + var style = elem.style; + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { + // Remember the original values + var left = style.left, rsLeft = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = cs.left; + style.left = ret || 0; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + elem.runtimeStyle.left = rsLeft; + } + + } + + // Normalize margin values. This is not totally valid, but in most cases + // it is what the user wants to know. + if(name.indexOf("margin") > -1 && ret == "auto") { + return "0px"; + } + + // Some browsers return undefined width and height values as "auto", so + // we need to retrieve those ourselves. + if (name == "width" && ret == "auto") { + ret = elem.clientWidth + "px"; + } else if (name == "height" && ret == "auto") { + ret = elem.clientHeight + "px"; + } + + return ret; + + }-*/; + + public final int getIntProperty(String name) { + Integer parsed = parseInt(getProperty(name)); + if (parsed != null) { + return parsed.intValue(); + } + return 0; + } + + /** + * Get current margin values from the DOM. The array order is the default + * CSS order: top, right, bottom, left. + */ + public final int[] getMargin() { + int[] margin = { 0, 0, 0, 0 }; + margin[0] = getIntProperty("marginTop"); + margin[1] = getIntProperty("marginRight"); + margin[2] = getIntProperty("marginBottom"); + margin[3] = getIntProperty("marginLeft"); + return margin; + } + + /** + * Get current padding values from the DOM. The array order is the default + * CSS order: top, right, bottom, left. + */ + public final int[] getPadding() { + int[] padding = { 0, 0, 0, 0 }; + padding[0] = getIntProperty("paddingTop"); + padding[1] = getIntProperty("paddingRight"); + padding[2] = getIntProperty("paddingBottom"); + padding[3] = getIntProperty("paddingLeft"); + return padding; + } + + /** + * Get current border values from the DOM. The array order is the default + * CSS order: top, right, bottom, left. + */ + public final int[] getBorder() { + int[] border = { 0, 0, 0, 0 }; + border[0] = getIntProperty("borderTopWidth"); + border[1] = getIntProperty("borderRightWidth"); + border[2] = getIntProperty("borderBottomWidth"); + border[3] = getIntProperty("borderLeftWidth"); + return border; + } + + /** + * Takes a String value e.g. "12px" and parses that to int 12. + * + * @param String + * a value starting with a number + * @return int the value from the string before any non-numeric characters. + * If the value cannot be parsed to a number, returns + * <code>null</code>. + */ + public static native Integer parseInt(final String value) + /*-{ + var number = parseInt(value, 10); + if (isNaN(number)) + return null; + else + // $entry not needed as function is not exported + return @java.lang.Integer::valueOf(I)(number); + }-*/; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ConnectorHierarchyChangeEvent.java b/client/src/com/vaadin/terminal/gwt/client/ConnectorHierarchyChangeEvent.java new file mode 100644 index 0000000000..6abaa89891 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ConnectorHierarchyChangeEvent.java @@ -0,0 +1,108 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.io.Serializable; +import java.util.List; + +import com.google.gwt.event.shared.EventHandler; +import com.google.gwt.event.shared.GwtEvent; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler; +import com.vaadin.terminal.gwt.client.communication.AbstractServerConnectorEvent; + +/** + * Event for containing data related to a change in the {@link ServerConnector} + * hierarchy. A {@link ConnectorHierarchyChangedEvent} is fired when an update + * from the server has been fully processed and all hierarchy updates have been + * completed. + * + * @author Vaadin Ltd + * @since 7.0.0 + * + */ +public class ConnectorHierarchyChangeEvent extends + AbstractServerConnectorEvent<ConnectorHierarchyChangeHandler> { + /** + * Type of this event, used by the event bus. + */ + public static final Type<ConnectorHierarchyChangeHandler> TYPE = new Type<ConnectorHierarchyChangeHandler>(); + + List<ComponentConnector> oldChildren; + private ComponentContainerConnector parent; + + public ConnectorHierarchyChangeEvent() { + } + + /** + * Returns a collection of the old children for the connector. This was the + * state before the update was received from the server. + * + * @return A collection of old child connectors. Never returns null. + */ + public List<ComponentConnector> getOldChildren() { + return oldChildren; + } + + /** + * Sets the collection of the old children for the connector. + * + * @param oldChildren + * The old child connectors. Must not be null. + */ + public void setOldChildren(List<ComponentConnector> oldChildren) { + this.oldChildren = oldChildren; + } + + /** + * Returns the {@link ComponentContainerConnector} for which this event + * occurred. + * + * @return The {@link ComponentContainerConnector} whose child collection + * has changed. Never returns null. + */ + public ComponentContainerConnector getParent() { + return parent; + } + + /** + * Sets the {@link ComponentContainerConnector} for which this event + * occurred. + * + * @param The + * {@link ComponentContainerConnector} whose child collection has + * changed. + */ + public void setParent(ComponentContainerConnector parent) { + this.parent = parent; + } + + public interface ConnectorHierarchyChangeHandler extends Serializable, + EventHandler { + public void onConnectorHierarchyChange( + ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent); + } + + @Override + public void dispatch(ConnectorHierarchyChangeHandler handler) { + handler.onConnectorHierarchyChange(this); + } + + @Override + public GwtEvent.Type<ConnectorHierarchyChangeHandler> getAssociatedType() { + return TYPE; + } + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ConnectorMap.java b/client/src/com/vaadin/terminal/gwt/client/ConnectorMap.java new file mode 100644 index 0000000000..8202ef7d17 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ConnectorMap.java @@ -0,0 +1,231 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.ui.Widget; + +public class ConnectorMap { + + private Map<String, ServerConnector> idToConnector = new HashMap<String, ServerConnector>(); + + public static ConnectorMap get(ApplicationConnection applicationConnection) { + return applicationConnection.getConnectorMap(); + } + + @Deprecated + private final ComponentDetailMap idToComponentDetail = ComponentDetailMap + .create(); + + /** + * Returns a {@link ServerConnector} by its id + * + * @param id + * The connector id + * @return A connector or null if a connector with the given id has not been + * registered + */ + public ServerConnector getConnector(String connectorId) { + return idToConnector.get(connectorId); + } + + /** + * Returns a {@link ComponentConnector} element by its root element + * + * @param element + * Root element of the {@link ComponentConnector} + * @return A connector or null if a connector with the given id has not been + * registered + */ + public ComponentConnector getConnector(Element element) { + return (ComponentConnector) getConnector(getConnectorId(element)); + } + + /** + * FIXME: What does this even do and why? + * + * @param pid + * @return + */ + public boolean isDragAndDropPaintable(String pid) { + return (pid.startsWith("DD")); + } + + /** + * Checks if a connector with the given id has been registered. + * + * @param connectorId + * The id to check for + * @return true if a connector has been registered with the given id, false + * otherwise + */ + public boolean hasConnector(String connectorId) { + return idToConnector.containsKey(connectorId); + } + + /** + * Removes all registered connectors + */ + public void clear() { + idToConnector.clear(); + idToComponentDetail.clear(); + } + + /** + * Retrieves the connector whose widget matches the parameter. + * + * @param widget + * The widget + * @return A connector with {@literal widget} as its root widget or null if + * no connector was found + */ + public ComponentConnector getConnector(Widget widget) { + return getConnector(widget.getElement()); + } + + public void registerConnector(String id, ServerConnector connector) { + ComponentDetail componentDetail = GWT.create(ComponentDetail.class); + idToComponentDetail.put(id, componentDetail); + idToConnector.put(id, connector); + if (connector instanceof ComponentConnector) { + ComponentConnector pw = (ComponentConnector) connector; + setConnectorId(pw.getWidget().getElement(), id); + } + } + + private native void setConnectorId(Element el, String id) + /*-{ + el.tkPid = id; + }-*/; + + /** + * Gets the connector id using a DOM element - the element should be the + * root element for a connector, otherwise no id will be found. Use + * {@link #getConnectorId(ServerConnector)} instead whenever possible. + * + * @see #getConnectorId(ServerConnector) + * @param el + * element of the connector whose id is desired + * @return the id of the element's connector, if it's a connector + */ + native String getConnectorId(Element el) + /*-{ + return el.tkPid; + }-*/; + + /** + * Gets the main element for the connector with the given id. The reverse of + * {@link #getConnectorId(Element)}. + * + * @param connectorId + * the id of the widget whose element is desired + * @return the element for the connector corresponding to the id + */ + public Element getElement(String connectorId) { + ServerConnector p = getConnector(connectorId); + if (p instanceof ComponentConnector) { + return ((ComponentConnector) p).getWidget().getElement(); + } + + return null; + } + + /** + * Unregisters the given connector; always use after removing a connector. + * This method does not remove the connector from the DOM, but marks the + * connector so that ApplicationConnection may clean up its references to + * it. Removing the widget from DOM is component containers responsibility. + * + * @param connector + * the connector to remove + */ + public void unregisterConnector(ServerConnector connector) { + if (connector == null) { + VConsole.error("Trying to unregister null connector"); + return; + } + + String connectorId = connector.getConnectorId(); + + idToComponentDetail.remove(connectorId); + idToConnector.remove(connectorId); + connector.onUnregister(); + + for (ServerConnector child : connector.getChildren()) { + if (child.getParent() == connector) { + /* + * Only unregister children that are actually connected to this + * parent. For instance when moving connectors from one layout + * to another and removing the first layout it will still + * contain references to its old children, which are now + * attached to another connector. + */ + unregisterConnector(child); + } + } + } + + /** + * Gets all registered {@link ComponentConnector} instances + * + * @return An array of all registered {@link ComponentConnector} instances + */ + public ComponentConnector[] getComponentConnectors() { + ArrayList<ComponentConnector> result = new ArrayList<ComponentConnector>(); + + for (ServerConnector connector : getConnectors()) { + if (connector instanceof ComponentConnector) { + result.add((ComponentConnector) connector); + } + } + + return result.toArray(new ComponentConnector[result.size()]); + } + + @Deprecated + private ComponentDetail getComponentDetail( + ComponentConnector componentConnector) { + return idToComponentDetail.get(componentConnector.getConnectorId()); + } + + public int size() { + return idToConnector.size(); + } + + public Collection<? extends ServerConnector> getConnectors() { + return Collections.unmodifiableCollection(idToConnector.values()); + } + + /** + * Tests if the widget is the root widget of a {@link ComponentConnector}. + * + * @param widget + * The widget to test + * @return true if the widget is the root widget of a + * {@link ComponentConnector}, false otherwise + */ + public boolean isConnector(Widget w) { + return getConnectorId(w.getElement()) != null; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/Console.java b/client/src/com/vaadin/terminal/gwt/client/Console.java new file mode 100644 index 0000000000..4f292e4e28 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/Console.java @@ -0,0 +1,44 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.Set; + +public interface Console { + + public abstract void log(String msg); + + public abstract void log(Throwable e); + + public abstract void error(Throwable e); + + public abstract void error(String msg); + + public abstract void printObject(Object msg); + + public abstract void dirUIDL(ValueMap u, ApplicationConnection client); + + public abstract void printLayoutProblems(ValueMap meta, + ApplicationConnection applicationConnection, + Set<ComponentConnector> zeroHeightComponents, + Set<ComponentConnector> zeroWidthComponents); + + public abstract void setQuietMode(boolean quietDebugMode); + + public abstract void init(); + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java b/client/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java new file mode 100644 index 0000000000..01ece2ed80 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java @@ -0,0 +1,33 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +/** + * ContainerResizedListener interface is useful for Widgets that support + * relative sizes and who need some additional sizing logic. + */ +public interface ContainerResizedListener { + /** + * This function is run when container box has been resized. Object + * implementing ContainerResizedListener is responsible to call the same + * function on its ancestors that implement NeedsLayout in case their + * container has resized. runAnchestorsLayout(HasWidgets parent) function + * from Util class may be a good helper for this. + * + */ + public void iLayout(); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/DateTimeService.java b/client/src/com/vaadin/terminal/gwt/client/DateTimeService.java new file mode 100644 index 0000000000..8cce0846ac --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/DateTimeService.java @@ -0,0 +1,451 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.Date; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.LocaleInfo; +import com.vaadin.terminal.gwt.client.ui.datefield.VDateField; + +/** + * This class provides date/time parsing services to all components on the + * client side. + * + * @author Vaadin Ltd. + * + */ +@SuppressWarnings("deprecation") +public class DateTimeService { + + private String currentLocale; + + private static int[] maxDaysInMonth = { 31, 28, 31, 30, 31, 30, 31, 31, 30, + 31, 30, 31 }; + + /** + * Creates a new date time service with the application default locale. + */ + public DateTimeService() { + currentLocale = LocaleService.getDefaultLocale(); + } + + /** + * Creates a new date time service with a given locale. + * + * @param locale + * e.g. fi, en etc. + * @throws LocaleNotLoadedException + */ + public DateTimeService(String locale) throws LocaleNotLoadedException { + setLocale(locale); + } + + public void setLocale(String locale) throws LocaleNotLoadedException { + if (LocaleService.getAvailableLocales().contains(locale)) { + currentLocale = locale; + } else { + throw new LocaleNotLoadedException(locale); + } + } + + public String getLocale() { + return currentLocale; + } + + public String getMonth(int month) { + try { + return LocaleService.getMonthNames(currentLocale)[month]; + } catch (final LocaleNotLoadedException e) { + VConsole.error(e); + return null; + } + } + + public String getShortMonth(int month) { + try { + return LocaleService.getShortMonthNames(currentLocale)[month]; + } catch (final LocaleNotLoadedException e) { + VConsole.error(e); + return null; + } + } + + public String getDay(int day) { + try { + return LocaleService.getDayNames(currentLocale)[day]; + } catch (final LocaleNotLoadedException e) { + VConsole.error(e); + return null; + } + } + + public String getShortDay(int day) { + try { + return LocaleService.getShortDayNames(currentLocale)[day]; + } catch (final LocaleNotLoadedException e) { + VConsole.error(e); + return null; + } + } + + public int getFirstDayOfWeek() { + try { + return LocaleService.getFirstDayOfWeek(currentLocale); + } catch (final LocaleNotLoadedException e) { + VConsole.error(e); + return 0; + } + } + + public boolean isTwelveHourClock() { + try { + return LocaleService.isTwelveHourClock(currentLocale); + } catch (final LocaleNotLoadedException e) { + VConsole.error(e); + return false; + } + } + + public String getClockDelimeter() { + try { + return LocaleService.getClockDelimiter(currentLocale); + } catch (final LocaleNotLoadedException e) { + VConsole.error(e); + return ":"; + } + } + + private static final String[] DEFAULT_AMPM_STRINGS = { "AM", "PM" }; + + public String[] getAmPmStrings() { + try { + return LocaleService.getAmPmStrings(currentLocale); + } catch (final LocaleNotLoadedException e) { + // TODO can this practically even happen? Should die instead? + VConsole.error("Locale not loaded, using fallback : AM/PM"); + VConsole.error(e); + return DEFAULT_AMPM_STRINGS; + } + } + + public int getStartWeekDay(Date date) { + final Date dateForFirstOfThisMonth = new Date(date.getYear(), + date.getMonth(), 1); + int firstDay; + try { + firstDay = LocaleService.getFirstDayOfWeek(currentLocale); + } catch (final LocaleNotLoadedException e) { + VConsole.error("Locale not loaded, using fallback 0"); + VConsole.error(e); + firstDay = 0; + } + int start = dateForFirstOfThisMonth.getDay() - firstDay; + if (start < 0) { + start = 6; + } + return start; + } + + public static void setMilliseconds(Date date, int ms) { + date.setTime(date.getTime() / 1000 * 1000 + ms); + } + + public static int getMilliseconds(Date date) { + if (date == null) { + return 0; + } + + return (int) (date.getTime() - date.getTime() / 1000 * 1000); + } + + public static int getNumberOfDaysInMonth(Date date) { + final int month = date.getMonth(); + if (month == 1 && true == isLeapYear(date)) { + return 29; + } + return maxDaysInMonth[month]; + } + + public static boolean isLeapYear(Date date) { + // Instantiate the date for 1st March of that year + final Date firstMarch = new Date(date.getYear(), 2, 1); + + // Go back 1 day + final long firstMarchTime = firstMarch.getTime(); + final long lastDayTimeFeb = firstMarchTime - (24 * 60 * 60 * 1000); // NUM_MILLISECS_A_DAY + + // Instantiate new Date with this time + final Date febLastDay = new Date(lastDayTimeFeb); + + // Check for date in this new instance + return (29 == febLastDay.getDate()) ? true : false; + } + + public static boolean isSameDay(Date d1, Date d2) { + return (getDayInt(d1) == getDayInt(d2)); + } + + public static boolean isInRange(Date date, Date rangeStart, Date rangeEnd, + int resolution) { + Date s; + Date e; + if (rangeStart.after(rangeEnd)) { + s = rangeEnd; + e = rangeStart; + } else { + e = rangeEnd; + s = rangeStart; + } + long start = s.getYear() * 10000000000l; + long end = e.getYear() * 10000000000l; + long target = date.getYear() * 10000000000l; + + if (resolution == VDateField.RESOLUTION_YEAR) { + return (start <= target && end >= target); + } + start += s.getMonth() * 100000000l; + end += e.getMonth() * 100000000l; + target += date.getMonth() * 100000000l; + if (resolution == VDateField.RESOLUTION_MONTH) { + return (start <= target && end >= target); + } + start += s.getDate() * 1000000l; + end += e.getDate() * 1000000l; + target += date.getDate() * 1000000l; + if (resolution == VDateField.RESOLUTION_DAY) { + return (start <= target && end >= target); + } + start += s.getHours() * 10000l; + end += e.getHours() * 10000l; + target += date.getHours() * 10000l; + if (resolution == VDateField.RESOLUTION_HOUR) { + return (start <= target && end >= target); + } + start += s.getMinutes() * 100l; + end += e.getMinutes() * 100l; + target += date.getMinutes() * 100l; + if (resolution == VDateField.RESOLUTION_MIN) { + return (start <= target && end >= target); + } + start += s.getSeconds(); + end += e.getSeconds(); + target += date.getSeconds(); + return (start <= target && end >= target); + + } + + private static int getDayInt(Date date) { + final int y = date.getYear(); + final int m = date.getMonth(); + final int d = date.getDate(); + + return ((y + 1900) * 10000 + m * 100 + d) * 1000000000; + } + + /** + * Returns the ISO-8601 week number of the given date. + * + * @param date + * The date for which the week number should be resolved + * @return The ISO-8601 week number for {@literal date} + */ + public static int getISOWeekNumber(Date date) { + final long MILLISECONDS_PER_DAY = 24 * 3600 * 1000; + int dayOfWeek = date.getDay(); // 0 == sunday + + // ISO 8601 use weeks that start on monday so we use + // mon=1,tue=2,...sun=7; + if (dayOfWeek == 0) { + dayOfWeek = 7; + } + // Find nearest thursday (defines the week in ISO 8601). The week number + // for the nearest thursday is the same as for the target date. + int nearestThursdayDiff = 4 - dayOfWeek; // 4 is thursday + Date nearestThursday = new Date(date.getTime() + nearestThursdayDiff + * MILLISECONDS_PER_DAY); + + Date firstOfJanuary = new Date(nearestThursday.getYear(), 0, 1); + long timeDiff = nearestThursday.getTime() - firstOfJanuary.getTime(); + int daysSinceFirstOfJanuary = (int) (timeDiff / MILLISECONDS_PER_DAY); + + int weekNumber = (daysSinceFirstOfJanuary) / 7 + 1; + + return weekNumber; + } + + /** + * Check if format contains the month name. If it does we manually convert + * it to the month name since DateTimeFormat.format always uses the current + * locale and will replace the month name wrong if current locale is + * different from the locale set for the DateField. + * + * MMMM is converted into long month name, MMM is converted into short month + * name. '' are added around the name to avoid that DateTimeFormat parses + * the month name as a pattern. + * + * @param date + * The date to convert + * @param formatStr + * The format string that might contain MMM or MMMM + * @param dateTimeService + * Reference to the Vaadin DateTimeService + * @return + */ + public String formatDate(Date date, String formatStr) { + /* + * Format month names separately when locale for the DateTimeService is + * not the same as the browser locale + */ + formatStr = formatMonthNames(date, formatStr); + + // Format uses the browser locale + DateTimeFormat format = DateTimeFormat.getFormat(formatStr); + + String result = format.format(date); + + return result; + } + + private String formatMonthNames(Date date, String formatStr) { + if (formatStr.contains("MMMM")) { + String monthName = getMonth(date.getMonth()); + + if (monthName != null) { + /* + * Replace 4 or more M:s with the quoted month name. Also + * concatenate generated string with any other string prepending + * or following the MMMM pattern, i.e. 'MMMM'ta ' becomes + * 'MONTHta ' and not 'MONTH''ta ', 'ab'MMMM becomes 'abMONTH', + * 'x'MMMM'y' becomes 'xMONTHy'. + */ + formatStr = formatStr.replaceAll("'([M]{4,})'", monthName); + formatStr = formatStr.replaceAll("([M]{4,})'", "'" + monthName); + formatStr = formatStr.replaceAll("'([M]{4,})", monthName + "'"); + formatStr = formatStr.replaceAll("[M]{4,}", "'" + monthName + + "'"); + } + } + + if (formatStr.contains("MMM")) { + + String monthName = getShortMonth(date.getMonth()); + + if (monthName != null) { + /* + * Replace 3 or more M:s with the quoted month name. Also + * concatenate generated string with any other string prepending + * or following the MMM pattern, i.e. 'MMM'ta ' becomes 'MONTHta + * ' and not 'MONTH''ta ', 'ab'MMM becomes 'abMONTH', 'x'MMM'y' + * becomes 'xMONTHy'. + */ + formatStr = formatStr.replaceAll("'([M]{3,})'", monthName); + formatStr = formatStr.replaceAll("([M]{3,})'", "'" + monthName); + formatStr = formatStr.replaceAll("'([M]{3,})", monthName + "'"); + formatStr = formatStr.replaceAll("[M]{3,}", "'" + monthName + + "'"); + } + } + + return formatStr; + } + + /** + * Replaces month names in the entered date with the name in the current + * browser locale. + * + * @param enteredDate + * Date string e.g. "5 May 2010" + * @param formatString + * Format string e.g. "d M yyyy" + * @return The date string where the month names have been replaced by the + * browser locale version + */ + private String parseMonthName(String enteredDate, String formatString) { + LocaleInfo browserLocale = LocaleInfo.getCurrentLocale(); + if (browserLocale.getLocaleName().equals(getLocale())) { + // No conversion needs to be done when locales match + return enteredDate; + } + String[] browserMonthNames = browserLocale.getDateTimeConstants() + .months(); + String[] browserShortMonthNames = browserLocale.getDateTimeConstants() + .shortMonths(); + + if (formatString.contains("MMMM")) { + // Full month name + for (int i = 0; i < 12; i++) { + enteredDate = enteredDate.replaceAll(getMonth(i), + browserMonthNames[i]); + } + } + if (formatString.contains("MMM")) { + // Short month name + for (int i = 0; i < 12; i++) { + enteredDate = enteredDate.replaceAll(getShortMonth(i), + browserShortMonthNames[i]); + } + } + + return enteredDate; + } + + /** + * Parses the given date string using the given format string and the locale + * set in this DateTimeService instance. + * + * @param dateString + * Date string e.g. "1 February 2010" + * @param formatString + * Format string e.g. "d MMMM yyyy" + * @param lenient + * true to use lenient parsing, false to use strict parsing + * @return A Date object representing the dateString. Never returns null. + * @throws IllegalArgumentException + * if the parsing fails + * + */ + public Date parseDate(String dateString, String formatString, + boolean lenient) throws IllegalArgumentException { + /* DateTimeFormat uses the browser's locale */ + DateTimeFormat format = DateTimeFormat.getFormat(formatString); + + /* + * Parse month names separately when locale for the DateTimeService is + * not the same as the browser locale + */ + dateString = parseMonthName(dateString, formatString); + + Date date; + + if (lenient) { + date = format.parse(dateString); + } else { + date = format.parseStrict(dateString); + } + + // Some version of Firefox sets the timestamp to 0 if parsing fails. + if (date != null && date.getTime() == 0) { + throw new IllegalArgumentException("Parsing of '" + dateString + + "' failed"); + } + + return date; + + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/DirectionalManagedLayout.java b/client/src/com/vaadin/terminal/gwt/client/DirectionalManagedLayout.java new file mode 100644 index 0000000000..323fc5bdf2 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/DirectionalManagedLayout.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.vaadin.terminal.gwt.client.ui.ManagedLayout; + +public interface DirectionalManagedLayout extends ManagedLayout { + public void layoutVertically(); + + public void layoutHorizontally(); +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/EventHelper.java b/client/src/com/vaadin/terminal/gwt/client/EventHelper.java new file mode 100644 index 0000000000..8d09094c1f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/EventHelper.java @@ -0,0 +1,109 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import static com.vaadin.shared.EventId.BLUR; +import static com.vaadin.shared.EventId.FOCUS; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.DomEvent.Type; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.shared.EventHandler; +import com.google.gwt.event.shared.HandlerRegistration; + +/** + * Helper class for attaching/detaching handlers for Vaadin client side + * components, based on identifiers in UIDL. Helpers expect Paintables to be + * both listeners and sources for events. This helper cannot be used for more + * complex widgets. + * <p> + * Possible current registration is given as parameter. The returned + * registration (possibly the same as given, should be store for next update. + * <p> + * Pseudocode what helpers do: + * + * <pre> + * + * if paintable has event listener in UIDL + * if registration is null + * register paintable as as handler for event + * return the registration + * else + * if registration is not null + * remove the handler from paintable + * return null + * + * + * </pre> + * + */ +public class EventHelper { + + /** + * Adds or removes a focus handler depending on if the connector has focus + * listeners on the server side or not. + * + * @param connector + * The connector to update. Must implement focusHandler. + * @param handlerRegistration + * The old registration reference or null no handler has been + * registered previously + * @return a new registration handler that can be used to unregister the + * handler later + */ + public static <T extends ComponentConnector & FocusHandler> HandlerRegistration updateFocusHandler( + T connector, HandlerRegistration handlerRegistration) { + return updateHandler(connector, FOCUS, handlerRegistration, + FocusEvent.getType()); + } + + /** + * Adds or removes a blur handler depending on if the connector has blur + * listeners on the server side or not. + * + * @param connector + * The connector to update. Must implement BlurHandler. + * @param handlerRegistration + * The old registration reference or null no handler has been + * registered previously + * @return a new registration handler that can be used to unregister the + * handler later + */ + public static <T extends ComponentConnector & BlurHandler> HandlerRegistration updateBlurHandler( + T connector, HandlerRegistration handlerRegistration) { + return updateHandler(connector, BLUR, handlerRegistration, + BlurEvent.getType()); + } + + private static <H extends EventHandler> HandlerRegistration updateHandler( + ComponentConnector connector, String eventIdentifier, + HandlerRegistration handlerRegistration, Type<H> type) { + if (connector.hasEventListener(eventIdentifier)) { + if (handlerRegistration == null) { + handlerRegistration = connector.getWidget().addDomHandler( + (H) connector, type); + } + } else if (handlerRegistration != null) { + handlerRegistration.removeHandler(); + handlerRegistration = null; + } + return handlerRegistration; + + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/FastStringSet.java b/client/src/com/vaadin/terminal/gwt/client/FastStringSet.java new file mode 100644 index 0000000000..d88f56ed61 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/FastStringSet.java @@ -0,0 +1,72 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArrayString; + +public final class FastStringSet extends JavaScriptObject { + protected FastStringSet() { + // JSO constructor + } + + public native boolean contains(String string) + /*-{ + return this.hasOwnProperty(string); + }-*/; + + public native void add(String string) + /*-{ + this[string] = true; + }-*/; + + public native void addAll(JsArrayString array) + /*-{ + for(var i = 0; i < array.length; i++) { + this[array[i]] = true; + } + }-*/; + + public native JsArrayString dump() + /*-{ + var array = []; + for(var string in this) { + if (this.hasOwnProperty(string)) { + array.push(string); + } + } + return array; + }-*/; + + public native void remove(String string) + /*-{ + delete this[string]; + }-*/; + + public native boolean isEmpty() + /*-{ + for(var string in this) { + if (this.hasOwnProperty(string)) { + return false; + } + } + return true; + }-*/; + + public static FastStringSet create() { + return JavaScriptObject.createObject().cast(); + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/Focusable.java b/client/src/com/vaadin/terminal/gwt/client/Focusable.java new file mode 100644 index 0000000000..fe468a0548 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/Focusable.java @@ -0,0 +1,31 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +/** + * GWT's HasFocus is way too overkill for just receiving focus in simple + * components. Vaadin uses this interface in addition to GWT's HasFocus to pass + * focus requests from server to actual ui widgets in browsers. + * + * So in to make your server side focusable component receive focus on client + * side it must either implement this or HasFocus interface. + */ +public interface Focusable { + /** + * Sets focus to this widget. + */ + public void focus(); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/JavaScriptConnectorHelper.java b/client/src/com/vaadin/terminal/gwt/client/JavaScriptConnectorHelper.java new file mode 100644 index 0000000000..98c014b5ec --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/JavaScriptConnectorHelper.java @@ -0,0 +1,384 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArray; +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.user.client.Element; +import com.vaadin.shared.JavaScriptConnectorState; +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent.StateChangeHandler; + +public class JavaScriptConnectorHelper { + + private final ServerConnector connector; + private final JavaScriptObject nativeState = JavaScriptObject + .createObject(); + private final JavaScriptObject rpcMap = JavaScriptObject.createObject(); + + private final Map<String, JavaScriptObject> rpcObjects = new HashMap<String, JavaScriptObject>(); + private final Map<String, Set<String>> rpcMethods = new HashMap<String, Set<String>>(); + + private JavaScriptObject connectorWrapper; + private int tag; + + private boolean inited = false; + + public JavaScriptConnectorHelper(ServerConnector connector) { + this.connector = connector; + + // Wildcard rpc object + rpcObjects.put("", JavaScriptObject.createObject()); + } + + public void init() { + connector.addStateChangeHandler(new StateChangeHandler() { + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + JavaScriptObject wrapper = getConnectorWrapper(); + JavaScriptConnectorState state = getConnectorState(); + + for (String callback : state.getCallbackNames()) { + ensureCallback(JavaScriptConnectorHelper.this, wrapper, + callback); + } + + for (Entry<String, Set<String>> entry : state + .getRpcInterfaces().entrySet()) { + String rpcName = entry.getKey(); + String jsName = getJsInterfaceName(rpcName); + if (!rpcObjects.containsKey(jsName)) { + Set<String> methods = entry.getValue(); + rpcObjects.put(jsName, + createRpcObject(rpcName, methods)); + + // Init all methods for wildcard rpc + for (String method : methods) { + JavaScriptObject wildcardRpcObject = rpcObjects + .get(""); + Set<String> interfaces = rpcMethods.get(method); + if (interfaces == null) { + interfaces = new HashSet<String>(); + rpcMethods.put(method, interfaces); + attachRpcMethod(wildcardRpcObject, null, method); + } + interfaces.add(rpcName); + } + } + } + + // Init after setting up callbacks & rpc + if (!inited) { + initJavaScript(); + inited = true; + } + + fireNativeStateChange(wrapper); + } + }); + } + + private static String getJsInterfaceName(String rpcName) { + return rpcName.replace('$', '.'); + } + + protected JavaScriptObject createRpcObject(String iface, Set<String> methods) { + JavaScriptObject object = JavaScriptObject.createObject(); + + for (String method : methods) { + attachRpcMethod(object, iface, method); + } + + return object; + } + + private boolean initJavaScript() { + ApplicationConfiguration conf = connector.getConnection() + .getConfiguration(); + ArrayList<String> attemptedNames = new ArrayList<String>(); + Integer tag = Integer.valueOf(this.tag); + while (tag != null) { + String serverSideClassName = conf.getServerSideClassNameForTag(tag); + String initFunctionName = serverSideClassName + .replaceAll("\\.", "_"); + if (tryInitJs(initFunctionName, getConnectorWrapper())) { + VConsole.log("JavaScript connector initialized using " + + initFunctionName); + return true; + } else { + VConsole.log("No JavaScript function " + initFunctionName + + " found"); + attemptedNames.add(initFunctionName); + tag = conf.getParentTag(tag.intValue()); + } + } + VConsole.log("No JavaScript init for connector not found"); + showInitProblem(attemptedNames); + return false; + } + + protected void showInitProblem(ArrayList<String> attemptedNames) { + // Default does nothing + } + + private static native boolean tryInitJs(String initFunctionName, + JavaScriptObject connectorWrapper) + /*-{ + if (typeof $wnd[initFunctionName] == 'function') { + $wnd[initFunctionName].apply(connectorWrapper); + return true; + } else { + return false; + } + }-*/; + + private JavaScriptObject getConnectorWrapper() { + if (connectorWrapper == null) { + connectorWrapper = createConnectorWrapper(this, + connector.getConnection(), nativeState, rpcMap, + connector.getConnectorId(), rpcObjects); + } + + return connectorWrapper; + } + + private static native void fireNativeStateChange( + JavaScriptObject connectorWrapper) + /*-{ + if (typeof connectorWrapper.onStateChange == 'function') { + connectorWrapper.onStateChange(); + } + }-*/; + + private static native JavaScriptObject createConnectorWrapper( + JavaScriptConnectorHelper h, ApplicationConnection c, + JavaScriptObject nativeState, JavaScriptObject registeredRpc, + String connectorId, Map<String, JavaScriptObject> rpcObjects) + /*-{ + return { + 'getConnectorId': function() { + return connectorId; + }, + 'getParentId': $entry(function(connectorId) { + return h.@com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper::getParentId(Ljava/lang/String;)(connectorId); + }), + 'getState': function() { + return nativeState; + }, + 'getRpcProxy': $entry(function(iface) { + if (!iface) { + iface = ''; + } + return rpcObjects.@java.util.Map::get(Ljava/lang/Object;)(iface); + }), + 'getElement': $entry(function(connectorId) { + return h.@com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper::getWidgetElement(Ljava/lang/String;)(connectorId); + }), + 'registerRpc': function(iface, rpcHandler) { + //registerRpc(handler) -> registerRpc('', handler); + if (!rpcHandler) { + rpcHandler = iface; + iface = ''; + } + if (!registeredRpc[iface]) { + registeredRpc[iface] = []; + } + registeredRpc[iface].push(rpcHandler); + }, + 'translateVaadinUri': $entry(function(uri) { + return c.@com.vaadin.terminal.gwt.client.ApplicationConnection::translateVaadinUri(Ljava/lang/String;)(uri); + }), + }; + }-*/; + + private native void attachRpcMethod(JavaScriptObject rpc, String iface, + String method) + /*-{ + var self = this; + rpc[method] = $entry(function() { + self.@com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper::fireRpc(Ljava/lang/String;Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(iface, method, arguments); + }); + }-*/; + + private String getParentId(String connectorId) { + ServerConnector target = getConnector(connectorId); + if (target == null) { + return null; + } + ServerConnector parent = target.getParent(); + if (parent == null) { + return null; + } else { + return parent.getConnectorId(); + } + } + + private Element getWidgetElement(String connectorId) { + ServerConnector target = getConnector(connectorId); + if (target instanceof ComponentConnector) { + return ((ComponentConnector) target).getWidget().getElement(); + } else { + return null; + } + } + + private ServerConnector getConnector(String connectorId) { + if (connectorId == null || connectorId.length() == 0) { + return connector; + } + + return ConnectorMap.get(connector.getConnection()).getConnector( + connectorId); + } + + private void fireRpc(String iface, String method, + JsArray<JavaScriptObject> arguments) { + if (iface == null) { + iface = findWildcardInterface(method); + } + + JSONArray argumentsArray = new JSONArray(arguments); + Object[] parameters = new Object[arguments.length()]; + for (int i = 0; i < parameters.length; i++) { + parameters[i] = argumentsArray.get(i); + } + connector.getConnection().addMethodInvocationToQueue( + new MethodInvocation(connector.getConnectorId(), iface, method, + parameters), true); + } + + private String findWildcardInterface(String method) { + Set<String> interfaces = rpcMethods.get(method); + if (interfaces.size() == 1) { + return interfaces.iterator().next(); + } else { + // TODO Resolve conflicts using argument count and types + String interfaceList = ""; + for (String iface : interfaces) { + if (interfaceList.length() != 0) { + interfaceList += ", "; + } + interfaceList += getJsInterfaceName(iface); + } + + throw new IllegalStateException( + "Can not call method " + + method + + " for wildcard rpc proxy because the function is defined for multiple rpc interfaces: " + + interfaceList + + ". Retrieve a rpc proxy for a specific interface using getRpcProxy(interfaceName) to use the function."); + } + } + + private void fireCallback(String name, JsArray<JavaScriptObject> arguments) { + MethodInvocation invocation = new MethodInvocation( + connector.getConnectorId(), + "com.vaadin.ui.JavaScript$JavaScriptCallbackRpc", "call", + new Object[] { name, new JSONArray(arguments) }); + connector.getConnection().addMethodInvocationToQueue(invocation, true); + } + + public void setNativeState(JavaScriptObject state) { + updateNativeState(nativeState, state); + } + + private static native void updateNativeState(JavaScriptObject state, + JavaScriptObject input) + /*-{ + // Copy all fields to existing state object + for(var key in state) { + if (state.hasOwnProperty(key)) { + delete state[key]; + } + } + + for(var key in input) { + if (input.hasOwnProperty(key)) { + state[key] = input[key]; + } + } + }-*/; + + public Object[] decodeRpcParameters(JSONArray parametersJson) { + return new Object[] { parametersJson.getJavaScriptObject() }; + } + + public void setTag(int tag) { + this.tag = tag; + } + + public void invokeJsRpc(MethodInvocation invocation, + JSONArray parametersJson) { + String iface = invocation.getInterfaceName(); + String method = invocation.getMethodName(); + if ("com.vaadin.ui.JavaScript$JavaScriptCallbackRpc".equals(iface) + && "call".equals(method)) { + String callbackName = parametersJson.get(0).isString() + .stringValue(); + JavaScriptObject arguments = parametersJson.get(1).isArray() + .getJavaScriptObject(); + invokeCallback(getConnectorWrapper(), callbackName, arguments); + } else { + JavaScriptObject arguments = parametersJson.getJavaScriptObject(); + invokeJsRpc(rpcMap, iface, method, arguments); + // Also invoke wildcard interface + invokeJsRpc(rpcMap, "", method, arguments); + } + } + + private static native void invokeCallback(JavaScriptObject connector, + String name, JavaScriptObject arguments) + /*-{ + connector[name].apply(connector, arguments); + }-*/; + + private static native void invokeJsRpc(JavaScriptObject rpcMap, + String interfaceName, String methodName, JavaScriptObject parameters) + /*-{ + var targets = rpcMap[interfaceName]; + if (!targets) { + return; + } + for(var i = 0; i < targets.length; i++) { + var target = targets[i]; + target[methodName].apply(target, parameters); + } + }-*/; + + private static native void ensureCallback(JavaScriptConnectorHelper h, + JavaScriptObject connector, String name) + /*-{ + connector[name] = $entry(function() { + var args = Array.prototype.slice.call(arguments, 0); + h.@com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper::fireCallback(Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(name, args); + }); + }-*/; + + private JavaScriptConnectorState getConnectorState() { + return (JavaScriptConnectorState) connector.getState(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/JavaScriptExtension.java b/client/src/com/vaadin/terminal/gwt/client/JavaScriptExtension.java new file mode 100644 index 0000000000..e94e0a739b --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/JavaScriptExtension.java @@ -0,0 +1,46 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.vaadin.shared.JavaScriptExtensionState; +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.AbstractJavaScriptExtension; +import com.vaadin.terminal.gwt.client.communication.HasJavaScriptConnectorHelper; +import com.vaadin.terminal.gwt.client.extensions.AbstractExtensionConnector; + +@Connect(AbstractJavaScriptExtension.class) +public final class JavaScriptExtension extends AbstractExtensionConnector + implements HasJavaScriptConnectorHelper { + private final JavaScriptConnectorHelper helper = new JavaScriptConnectorHelper( + this); + + @Override + protected void init() { + super.init(); + helper.init(); + } + + @Override + public JavaScriptConnectorHelper getJavascriptConnectorHelper() { + return helper; + } + + @Override + public JavaScriptExtensionState getState() { + return (JavaScriptExtensionState) super.getState(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/LayoutManager.java b/client/src/com/vaadin/terminal/gwt/client/LayoutManager.java new file mode 100644 index 0000000000..c95860a029 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/LayoutManager.java @@ -0,0 +1,1227 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.google.gwt.core.client.Duration; +import com.google.gwt.core.client.JsArrayString; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.user.client.Timer; +import com.vaadin.terminal.gwt.client.MeasuredSize.MeasureResult; +import com.vaadin.terminal.gwt.client.ui.ManagedLayout; +import com.vaadin.terminal.gwt.client.ui.PostLayoutListener; +import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout; +import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeEvent; +import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeListener; +import com.vaadin.terminal.gwt.client.ui.layout.LayoutDependencyTree; +import com.vaadin.terminal.gwt.client.ui.notification.VNotification; + +public class LayoutManager { + private static final String LOOP_ABORT_MESSAGE = "Aborting layout after 100 passes. This would probably be an infinite loop."; + + private static final boolean debugLogging = false; + + private ApplicationConnection connection; + private final Set<Element> measuredNonConnectorElements = new HashSet<Element>(); + private final MeasuredSize nullSize = new MeasuredSize(); + + private LayoutDependencyTree currentDependencyTree; + + private final Collection<ManagedLayout> needsHorizontalLayout = new HashSet<ManagedLayout>(); + private final Collection<ManagedLayout> needsVerticalLayout = new HashSet<ManagedLayout>(); + + private final Collection<ComponentConnector> needsMeasure = new HashSet<ComponentConnector>(); + + private Collection<ComponentConnector> pendingOverflowFixes = new HashSet<ComponentConnector>(); + + private final Map<Element, Collection<ElementResizeListener>> elementResizeListeners = new HashMap<Element, Collection<ElementResizeListener>>(); + private final Set<Element> listenersToFire = new HashSet<Element>(); + + private boolean layoutPending = false; + private Timer layoutTimer = new Timer() { + @Override + public void run() { + cancel(); + layoutNow(); + } + }; + private boolean everythingNeedsMeasure = false; + + public void setConnection(ApplicationConnection connection) { + if (this.connection != null) { + throw new RuntimeException( + "LayoutManager connection can never be changed"); + } + this.connection = connection; + } + + /** + * Gets the layout manager associated with the given + * {@link ApplicationConnection}. + * + * @param connection + * the application connection to get a layout manager for + * @return the layout manager associated with the provided application + * connection + */ + public static LayoutManager get(ApplicationConnection connection) { + return connection.getLayoutManager(); + } + + /** + * Registers that a ManagedLayout is depending on the size of an Element. + * This causes this layout manager to measure the element in the beginning + * of every layout phase and call the appropriate layout method of the + * managed layout if the size of the element has changed. + * + * @param owner + * the ManagedLayout that depends on an element + * @param element + * the Element that should be measured + */ + public void registerDependency(ManagedLayout owner, Element element) { + MeasuredSize measuredSize = ensureMeasured(element); + setNeedsLayout(owner); + measuredSize.addDependent(owner.getConnectorId()); + } + + private MeasuredSize ensureMeasured(Element element) { + MeasuredSize measuredSize = getMeasuredSize(element, null); + if (measuredSize == null) { + measuredSize = new MeasuredSize(); + + if (ConnectorMap.get(connection).getConnector(element) == null) { + measuredNonConnectorElements.add(element); + } + setMeasuredSize(element, measuredSize); + } + return measuredSize; + } + + private boolean needsMeasure(Element e) { + if (connection.getConnectorMap().getConnectorId(e) != null) { + return true; + } else if (elementResizeListeners.containsKey(e)) { + return true; + } else if (getMeasuredSize(e, nullSize).hasDependents()) { + return true; + } else { + return false; + } + } + + /** + * Assigns a measured size to an element. Method defined as protected to + * allow separate implementation for IE8. + * + * @param element + * the dom element to attach the measured size to + * @param measuredSize + * the measured size to attach to the element. If + * <code>null</code>, any previous measured size is removed. + */ + protected native void setMeasuredSize(Element element, + MeasuredSize measuredSize) + /*-{ + if (measuredSize) { + element.vMeasuredSize = measuredSize; + } else { + delete element.vMeasuredSize; + } + }-*/; + + /** + * Gets the measured size for an element. Method defined as protected to + * allow separate implementation for IE8. + * + * @param element + * The element to get measured size for + * @param defaultSize + * The size to return if no measured size could be found + * @return The measured size for the element or {@literal defaultSize} + */ + protected native MeasuredSize getMeasuredSize(Element element, + MeasuredSize defaultSize) + /*-{ + return element.vMeasuredSize || defaultSize; + }-*/; + + private final MeasuredSize getMeasuredSize(ComponentConnector connector) { + Element element = connector.getWidget().getElement(); + MeasuredSize measuredSize = getMeasuredSize(element, null); + if (measuredSize == null) { + measuredSize = new MeasuredSize(); + setMeasuredSize(element, measuredSize); + } + return measuredSize; + } + + /** + * Registers that a ManagedLayout is no longer depending on the size of an + * Element. + * + * @see #registerDependency(ManagedLayout, Element) + * + * @param owner + * the ManagedLayout no longer depends on an element + * @param element + * the Element that that no longer needs to be measured + */ + public void unregisterDependency(ManagedLayout owner, Element element) { + MeasuredSize measuredSize = getMeasuredSize(element, null); + if (measuredSize == null) { + return; + } + measuredSize.removeDependent(owner.getConnectorId()); + stopMeasuringIfUnecessary(element); + } + + public boolean isLayoutRunning() { + return currentDependencyTree != null; + } + + private void countLayout(Map<ManagedLayout, Integer> layoutCounts, + ManagedLayout layout) { + Integer count = layoutCounts.get(layout); + if (count == null) { + count = Integer.valueOf(0); + } else { + count = Integer.valueOf(count.intValue() + 1); + } + layoutCounts.put(layout, count); + if (count.intValue() > 2) { + VConsole.error(Util.getConnectorString(layout) + + " has been layouted " + count.intValue() + " times"); + } + } + + private void layoutLater() { + if (!layoutPending) { + layoutPending = true; + layoutTimer.schedule(100); + } + } + + public void layoutNow() { + if (isLayoutRunning()) { + throw new IllegalStateException( + "Can't start a new layout phase before the previous layout phase ends."); + } + layoutPending = false; + try { + currentDependencyTree = new LayoutDependencyTree(); + doLayout(); + } finally { + currentDependencyTree = null; + } + } + + private void doLayout() { + VConsole.log("Starting layout phase"); + + Map<ManagedLayout, Integer> layoutCounts = new HashMap<ManagedLayout, Integer>(); + + int passes = 0; + Duration totalDuration = new Duration(); + + for (ManagedLayout layout : needsHorizontalLayout) { + currentDependencyTree.setNeedsHorizontalLayout(layout, true); + } + for (ManagedLayout layout : needsVerticalLayout) { + currentDependencyTree.setNeedsVerticalLayout(layout, true); + } + needsHorizontalLayout.clear(); + needsVerticalLayout.clear(); + + for (ComponentConnector connector : needsMeasure) { + currentDependencyTree.setNeedsMeasure(connector, true); + } + needsMeasure.clear(); + + measureNonConnectors(); + + VConsole.log("Layout init in " + totalDuration.elapsedMillis() + " ms"); + + while (true) { + Duration passDuration = new Duration(); + passes++; + + int measuredConnectorCount = measureConnectors( + currentDependencyTree, everythingNeedsMeasure); + everythingNeedsMeasure = false; + if (measuredConnectorCount == 0) { + VConsole.log("No more changes in pass " + passes); + break; + } + + int measureTime = passDuration.elapsedMillis(); + VConsole.log(" Measured " + measuredConnectorCount + + " elements in " + measureTime + " ms"); + + if (!listenersToFire.isEmpty()) { + for (Element element : listenersToFire) { + Collection<ElementResizeListener> listeners = elementResizeListeners + .get(element); + ElementResizeListener[] array = listeners + .toArray(new ElementResizeListener[listeners.size()]); + ElementResizeEvent event = new ElementResizeEvent(this, + element); + for (ElementResizeListener listener : array) { + try { + listener.onElementResize(event); + } catch (RuntimeException e) { + VConsole.error(e); + } + } + } + int measureListenerTime = passDuration.elapsedMillis(); + VConsole.log(" Fired resize listeners for " + + listenersToFire.size() + " elements in " + + (measureListenerTime - measureTime) + " ms"); + measureTime = measuredConnectorCount; + listenersToFire.clear(); + } + + FastStringSet updatedSet = FastStringSet.create(); + + while (currentDependencyTree.hasHorizontalConnectorToLayout() + || currentDependencyTree.hasVerticaConnectorToLayout()) { + for (ManagedLayout layout : currentDependencyTree + .getHorizontalLayoutTargets()) { + if (layout instanceof DirectionalManagedLayout) { + currentDependencyTree + .markAsHorizontallyLayouted(layout); + DirectionalManagedLayout cl = (DirectionalManagedLayout) layout; + try { + cl.layoutHorizontally(); + } catch (RuntimeException e) { + VConsole.log(e); + } + countLayout(layoutCounts, cl); + } else { + currentDependencyTree + .markAsHorizontallyLayouted(layout); + currentDependencyTree.markAsVerticallyLayouted(layout); + SimpleManagedLayout rr = (SimpleManagedLayout) layout; + try { + rr.layout(); + } catch (RuntimeException e) { + VConsole.log(e); + } + countLayout(layoutCounts, rr); + } + if (debugLogging) { + updatedSet.add(layout.getConnectorId()); + } + } + + for (ManagedLayout layout : currentDependencyTree + .getVerticalLayoutTargets()) { + if (layout instanceof DirectionalManagedLayout) { + currentDependencyTree.markAsVerticallyLayouted(layout); + DirectionalManagedLayout cl = (DirectionalManagedLayout) layout; + try { + cl.layoutVertically(); + } catch (RuntimeException e) { + VConsole.log(e); + } + countLayout(layoutCounts, cl); + } else { + currentDependencyTree + .markAsHorizontallyLayouted(layout); + currentDependencyTree.markAsVerticallyLayouted(layout); + SimpleManagedLayout rr = (SimpleManagedLayout) layout; + try { + rr.layout(); + } catch (RuntimeException e) { + VConsole.log(e); + } + countLayout(layoutCounts, rr); + } + if (debugLogging) { + updatedSet.add(layout.getConnectorId()); + } + } + } + + if (debugLogging) { + JsArrayString changedCids = updatedSet.dump(); + + StringBuilder b = new StringBuilder(" "); + b.append(changedCids.length()); + b.append(" requestLayout invocations in "); + b.append(passDuration.elapsedMillis() - measureTime); + b.append(" ms"); + if (changedCids.length() < 30) { + for (int i = 0; i < changedCids.length(); i++) { + if (i != 0) { + b.append(", "); + } else { + b.append(": "); + } + String connectorString = changedCids.get(i); + if (changedCids.length() < 10) { + ServerConnector connector = ConnectorMap.get( + connection).getConnector(connectorString); + connectorString = Util + .getConnectorString(connector); + } + b.append(connectorString); + } + } + VConsole.log(b.toString()); + } + + VConsole.log("Pass " + passes + " completed in " + + passDuration.elapsedMillis() + " ms"); + + if (passes > 100) { + VConsole.log(LOOP_ABORT_MESSAGE); + VNotification.createNotification(VNotification.DELAY_FOREVER) + .show(LOOP_ABORT_MESSAGE, VNotification.CENTERED, + "error"); + break; + } + } + + int postLayoutStart = totalDuration.elapsedMillis(); + for (ComponentConnector connector : connection.getConnectorMap() + .getComponentConnectors()) { + if (connector instanceof PostLayoutListener) { + ((PostLayoutListener) connector).postLayout(); + } + } + int postLayoutDone = (totalDuration.elapsedMillis() - postLayoutStart); + VConsole.log("Invoke post layout listeners in " + postLayoutDone + + " ms"); + + cleanMeasuredSizes(); + int cleaningDone = (totalDuration.elapsedMillis() - postLayoutDone); + VConsole.log("Cleaned old measured sizes in " + cleaningDone + "ms"); + + VConsole.log("Total layout phase time: " + + totalDuration.elapsedMillis() + "ms"); + } + + private void logConnectorStatus(int connectorId) { + currentDependencyTree + .logDependencyStatus((ComponentConnector) ConnectorMap.get( + connection).getConnector(Integer.toString(connectorId))); + } + + private int measureConnectors(LayoutDependencyTree layoutDependencyTree, + boolean measureAll) { + if (!pendingOverflowFixes.isEmpty()) { + Duration duration = new Duration(); + + HashMap<Element, String> originalOverflows = new HashMap<Element, String>(); + + HashSet<ComponentConnector> delayedOverflowFixes = new HashSet<ComponentConnector>(); + + // First set overflow to hidden (and save previous value so it can + // be restored later) + for (ComponentConnector componentConnector : pendingOverflowFixes) { + // Delay the overflow fix if the involved connectors might still + // change + boolean connectorChangesExpected = !currentDependencyTree + .noMoreChangesExpected(componentConnector); + boolean parentChangesExcpected = componentConnector.getParent() instanceof ComponentConnector + && !currentDependencyTree + .noMoreChangesExpected((ComponentConnector) componentConnector + .getParent()); + if (connectorChangesExpected || parentChangesExcpected) { + delayedOverflowFixes.add(componentConnector); + continue; + } + + if (debugLogging) { + VConsole.log("Doing overflow fix for " + + Util.getConnectorString(componentConnector) + + " in " + + Util.getConnectorString(componentConnector + .getParent())); + } + + Element parentElement = componentConnector.getWidget() + .getElement().getParentElement(); + Style style = parentElement.getStyle(); + String originalOverflow = style.getOverflow(); + + if (originalOverflow != null + && !originalOverflows.containsKey(parentElement)) { + // Store original value for restore, but only the first time + // the value is changed + originalOverflows.put(parentElement, originalOverflow); + } + + style.setOverflow(Overflow.HIDDEN); + } + + pendingOverflowFixes.removeAll(delayedOverflowFixes); + + // Then ensure all scrolling elements are reflowed by measuring + for (ComponentConnector componentConnector : pendingOverflowFixes) { + componentConnector.getWidget().getElement().getParentElement() + .getOffsetHeight(); + } + + // Finally restore old overflow value and update bookkeeping + for (ComponentConnector componentConnector : pendingOverflowFixes) { + Element parentElement = componentConnector.getWidget() + .getElement().getParentElement(); + parentElement.getStyle().setProperty("overflow", + originalOverflows.get(parentElement)); + + layoutDependencyTree.setNeedsMeasure(componentConnector, true); + } + if (!pendingOverflowFixes.isEmpty()) { + VConsole.log("Did overflow fix for " + + pendingOverflowFixes.size() + " elements in " + + duration.elapsedMillis() + " ms"); + } + pendingOverflowFixes = delayedOverflowFixes; + } + + int measureCount = 0; + if (measureAll) { + ComponentConnector[] connectors = ConnectorMap.get(connection) + .getComponentConnectors(); + for (ComponentConnector connector : connectors) { + measureConnector(connector); + } + for (ComponentConnector connector : connectors) { + layoutDependencyTree.setNeedsMeasure(connector, false); + } + measureCount += connectors.length; + } + + while (layoutDependencyTree.hasConnectorsToMeasure()) { + Collection<ComponentConnector> measureTargets = layoutDependencyTree + .getMeasureTargets(); + for (ComponentConnector connector : measureTargets) { + measureConnector(connector); + measureCount++; + } + for (ComponentConnector connector : measureTargets) { + layoutDependencyTree.setNeedsMeasure(connector, false); + } + } + return measureCount; + } + + private void measureConnector(ComponentConnector connector) { + Element element = connector.getWidget().getElement(); + MeasuredSize measuredSize = getMeasuredSize(connector); + MeasureResult measureResult = measuredAndUpdate(element, measuredSize); + + if (measureResult.isChanged()) { + onConnectorChange(connector, measureResult.isWidthChanged(), + measureResult.isHeightChanged()); + } + } + + private void onConnectorChange(ComponentConnector connector, + boolean widthChanged, boolean heightChanged) { + setNeedsOverflowFix(connector); + if (heightChanged) { + currentDependencyTree.markHeightAsChanged(connector); + } + if (widthChanged) { + currentDependencyTree.markWidthAsChanged(connector); + } + } + + private void setNeedsOverflowFix(ComponentConnector connector) { + // IE9 doesn't need the original fix, but for some reason it needs this + if (BrowserInfo.get().requiresOverflowAutoFix() + || BrowserInfo.get().isIE9()) { + ComponentConnector scrollingBoundary = currentDependencyTree + .getScrollingBoundary(connector); + if (scrollingBoundary != null) { + pendingOverflowFixes.add(scrollingBoundary); + } + } + } + + private void measureNonConnectors() { + for (Element element : measuredNonConnectorElements) { + measuredAndUpdate(element, getMeasuredSize(element, null)); + } + VConsole.log("Measured " + measuredNonConnectorElements.size() + + " non connector elements"); + } + + private MeasureResult measuredAndUpdate(Element element, + MeasuredSize measuredSize) { + MeasureResult measureResult = measuredSize.measure(element); + if (measureResult.isChanged()) { + notifyListenersAndDepdendents(element, + measureResult.isWidthChanged(), + measureResult.isHeightChanged()); + } + return measureResult; + } + + private void notifyListenersAndDepdendents(Element element, + boolean widthChanged, boolean heightChanged) { + assert widthChanged || heightChanged; + + MeasuredSize measuredSize = getMeasuredSize(element, nullSize); + JsArrayString dependents = measuredSize.getDependents(); + for (int i = 0; i < dependents.length(); i++) { + String pid = dependents.get(i); + ManagedLayout dependent = (ManagedLayout) connection + .getConnectorMap().getConnector(pid); + if (dependent != null) { + if (heightChanged) { + currentDependencyTree.setNeedsVerticalLayout(dependent, + true); + } + if (widthChanged) { + currentDependencyTree.setNeedsHorizontalLayout(dependent, + true); + } + } + } + if (elementResizeListeners.containsKey(element)) { + listenersToFire.add(element); + } + } + + private static boolean isManagedLayout(ComponentConnector connector) { + return connector instanceof ManagedLayout; + } + + public void forceLayout() { + ConnectorMap connectorMap = connection.getConnectorMap(); + ComponentConnector[] componentConnectors = connectorMap + .getComponentConnectors(); + for (ComponentConnector connector : componentConnectors) { + if (connector instanceof ManagedLayout) { + setNeedsLayout((ManagedLayout) connector); + } + } + setEverythingNeedsMeasure(); + layoutNow(); + } + + /** + * Marks that a ManagedLayout should be layouted in the next layout phase + * even if none of the elements managed by the layout have been resized. + * + * @param layout + * the managed layout that should be layouted + */ + public final void setNeedsLayout(ManagedLayout layout) { + setNeedsHorizontalLayout(layout); + setNeedsVerticalLayout(layout); + } + + /** + * Marks that a ManagedLayout should be layouted horizontally in the next + * layout phase even if none of the elements managed by the layout have been + * resized horizontally. + * + * For SimpleManagedLayout which is always layouted in both directions, this + * has the same effect as {@link #setNeedsLayout(ManagedLayout)}. + * + * @param layout + * the managed layout that should be layouted + */ + public final void setNeedsHorizontalLayout(ManagedLayout layout) { + needsHorizontalLayout.add(layout); + } + + /** + * Marks that a ManagedLayout should be layouted vertically in the next + * layout phase even if none of the elements managed by the layout have been + * resized vertically. + * + * For SimpleManagedLayout which is always layouted in both directions, this + * has the same effect as {@link #setNeedsLayout(ManagedLayout)}. + * + * @param layout + * the managed layout that should be layouted + */ + public final void setNeedsVerticalLayout(ManagedLayout layout) { + needsVerticalLayout.add(layout); + } + + /** + * Gets the outer height (including margins, paddings and borders) of the + * given element, provided that it has been measured. These elements are + * guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * -1 is returned if the element has not been measured. If 0 is returned, it + * might indicate that the element is not attached to the DOM. + * + * @param element + * the element to get the measured size for + * @return the measured outer height (including margins, paddings and + * borders) of the element in pixels. + */ + public final int getOuterHeight(Element element) { + return getMeasuredSize(element, nullSize).getOuterHeight(); + } + + /** + * Gets the outer width (including margins, paddings and borders) of the + * given element, provided that it has been measured. These elements are + * guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * -1 is returned if the element has not been measured. If 0 is returned, it + * might indicate that the element is not attached to the DOM. + * + * @param element + * the element to get the measured size for + * @return the measured outer width (including margins, paddings and + * borders) of the element in pixels. + */ + public final int getOuterWidth(Element element) { + return getMeasuredSize(element, nullSize).getOuterWidth(); + } + + /** + * Gets the inner height (excluding margins, paddings and borders) of the + * given element, provided that it has been measured. These elements are + * guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * -1 is returned if the element has not been measured. If 0 is returned, it + * might indicate that the element is not attached to the DOM. + * + * @param element + * the element to get the measured size for + * @return the measured inner height (excluding margins, paddings and + * borders) of the element in pixels. + */ + public final int getInnerHeight(Element element) { + return getMeasuredSize(element, nullSize).getInnerHeight(); + } + + /** + * Gets the inner width (excluding margins, paddings and borders) of the + * given element, provided that it has been measured. These elements are + * guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * -1 is returned if the element has not been measured. If 0 is returned, it + * might indicate that the element is not attached to the DOM. + * + * @param element + * the element to get the measured size for + * @return the measured inner width (excluding margins, paddings and + * borders) of the element in pixels. + */ + public final int getInnerWidth(Element element) { + return getMeasuredSize(element, nullSize).getInnerWidth(); + } + + /** + * Gets the border height (top border + bottom border) of the given element, + * provided that it has been measured. These elements are guaranteed to be + * measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured border height (top border + bottom border) of the + * element in pixels. + */ + public final int getBorderHeight(Element element) { + return getMeasuredSize(element, nullSize).getBorderHeight(); + } + + /** + * Gets the padding height (top padding + bottom padding) of the given + * element, provided that it has been measured. These elements are + * guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured padding height (top padding + bottom padding) of the + * element in pixels. + */ + public int getPaddingHeight(Element element) { + return getMeasuredSize(element, nullSize).getPaddingHeight(); + } + + /** + * Gets the border width (left border + right border) of the given element, + * provided that it has been measured. These elements are guaranteed to be + * measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured border width (left border + right border) of the + * element in pixels. + */ + public int getBorderWidth(Element element) { + return getMeasuredSize(element, nullSize).getBorderWidth(); + } + + /** + * Gets the padding width (left padding + right padding) of the given + * element, provided that it has been measured. These elements are + * guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured padding width (left padding + right padding) of the + * element in pixels. + */ + public int getPaddingWidth(Element element) { + return getMeasuredSize(element, nullSize).getPaddingWidth(); + } + + /** + * Gets the top padding of the given element, provided that it has been + * measured. These elements are guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured top padding of the element in pixels. + */ + public int getPaddingTop(Element element) { + return getMeasuredSize(element, nullSize).getPaddingTop(); + } + + /** + * Gets the left padding of the given element, provided that it has been + * measured. These elements are guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured left padding of the element in pixels. + */ + public int getPaddingLeft(Element element) { + return getMeasuredSize(element, nullSize).getPaddingLeft(); + } + + /** + * Gets the bottom padding of the given element, provided that it has been + * measured. These elements are guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured bottom padding of the element in pixels. + */ + public int getPaddingBottom(Element element) { + return getMeasuredSize(element, nullSize).getPaddingBottom(); + } + + /** + * Gets the right padding of the given element, provided that it has been + * measured. These elements are guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured right padding of the element in pixels. + */ + public int getPaddingRight(Element element) { + return getMeasuredSize(element, nullSize).getPaddingRight(); + } + + /** + * Gets the top margin of the given element, provided that it has been + * measured. These elements are guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured top margin of the element in pixels. + */ + public int getMarginTop(Element element) { + return getMeasuredSize(element, nullSize).getMarginTop(); + } + + /** + * Gets the right margin of the given element, provided that it has been + * measured. These elements are guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured right margin of the element in pixels. + */ + public int getMarginRight(Element element) { + return getMeasuredSize(element, nullSize).getMarginRight(); + } + + /** + * Gets the bottom margin of the given element, provided that it has been + * measured. These elements are guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured bottom margin of the element in pixels. + */ + public int getMarginBottom(Element element) { + return getMeasuredSize(element, nullSize).getMarginBottom(); + } + + /** + * Gets the left margin of the given element, provided that it has been + * measured. These elements are guaranteed to be measured: + * <ul> + * <li>ManagedLayotus and their child Connectors + * <li>Elements for which there is at least one ElementResizeListener + * <li>Elements for which at least one ManagedLayout has registered a + * dependency + * </ul> + * + * A negative number is returned if the element has not been measured. If 0 + * is returned, it might indicate that the element is not attached to the + * DOM. + * + * @param element + * the element to get the measured size for + * @return the measured left margin of the element in pixels. + */ + public int getMarginLeft(Element element) { + return getMeasuredSize(element, nullSize).getMarginLeft(); + } + + /** + * Registers the outer height (including margins, borders and paddings) of a + * component. This can be used as an optimization by ManagedLayouts; by + * informing the LayoutManager about what size a component will have, the + * layout propagation can continue directly without first measuring the + * potentially resized elements. + * + * @param component + * the component for which the size is reported + * @param outerHeight + * the new outer height (including margins, borders and paddings) + * of the component in pixels + */ + public void reportOuterHeight(ComponentConnector component, int outerHeight) { + MeasuredSize measuredSize = getMeasuredSize(component); + if (isLayoutRunning()) { + boolean heightChanged = measuredSize.setOuterHeight(outerHeight); + + if (heightChanged) { + onConnectorChange(component, false, true); + notifyListenersAndDepdendents(component.getWidget() + .getElement(), false, true); + } + currentDependencyTree.setNeedsVerticalMeasure(component, false); + } else if (measuredSize.getOuterHeight() != outerHeight) { + setNeedsMeasure(component); + } + } + + /** + * Registers the height reserved for a relatively sized component. This can + * be used as an optimization by ManagedLayouts; by informing the + * LayoutManager about what size a component will have, the layout + * propagation can continue directly without first measuring the potentially + * resized elements. + * + * @param component + * the relatively sized component for which the size is reported + * @param assignedHeight + * the inner height of the relatively sized component's parent + * element in pixels + */ + public void reportHeightAssignedToRelative(ComponentConnector component, + int assignedHeight) { + assert component.isRelativeHeight(); + + float percentSize = parsePercent(component.getState().getHeight()); + int effectiveHeight = Math.round(assignedHeight * (percentSize / 100)); + + reportOuterHeight(component, effectiveHeight); + } + + /** + * Registers the width reserved for a relatively sized component. This can + * be used as an optimization by ManagedLayouts; by informing the + * LayoutManager about what size a component will have, the layout + * propagation can continue directly without first measuring the potentially + * resized elements. + * + * @param component + * the relatively sized component for which the size is reported + * @param assignedWidth + * the inner width of the relatively sized component's parent + * element in pixels + */ + public void reportWidthAssignedToRelative(ComponentConnector component, + int assignedWidth) { + assert component.isRelativeWidth(); + + float percentSize = parsePercent(component.getState().getWidth()); + int effectiveWidth = Math.round(assignedWidth * (percentSize / 100)); + + reportOuterWidth(component, effectiveWidth); + } + + private static float parsePercent(String size) { + return Float.parseFloat(size.substring(0, size.length() - 1)); + } + + /** + * Registers the outer width (including margins, borders and paddings) of a + * component. This can be used as an optimization by ManagedLayouts; by + * informing the LayoutManager about what size a component will have, the + * layout propagation can continue directly without first measuring the + * potentially resized elements. + * + * @param component + * the component for which the size is reported + * @param outerWidth + * the new outer width (including margins, borders and paddings) + * of the component in pixels + */ + public void reportOuterWidth(ComponentConnector component, int outerWidth) { + MeasuredSize measuredSize = getMeasuredSize(component); + if (isLayoutRunning()) { + boolean widthChanged = measuredSize.setOuterWidth(outerWidth); + + if (widthChanged) { + onConnectorChange(component, true, false); + notifyListenersAndDepdendents(component.getWidget() + .getElement(), true, false); + } + currentDependencyTree.setNeedsHorizontalMeasure(component, false); + } else if (measuredSize.getOuterWidth() != outerWidth) { + setNeedsMeasure(component); + } + } + + /** + * Adds a listener that will be notified whenever the size of a specific + * element changes. Adding a listener to an element also ensures that all + * sizes for that element will be available starting from the next layout + * phase. + * + * @param element + * the element that should be checked for size changes + * @param listener + * an ElementResizeListener that will be informed whenever the + * size of the target element has changed + */ + public void addElementResizeListener(Element element, + ElementResizeListener listener) { + Collection<ElementResizeListener> listeners = elementResizeListeners + .get(element); + if (listeners == null) { + listeners = new HashSet<ElementResizeListener>(); + elementResizeListeners.put(element, listeners); + ensureMeasured(element); + } + listeners.add(listener); + } + + /** + * Removes an element resize listener from the provided element. This might + * cause this LayoutManager to stop tracking the size of the element if no + * other sources are interested in the size. + * + * @param element + * the element to which the element resize listener was + * previously added + * @param listener + * the ElementResizeListener that should no longer get informed + * about size changes to the target element. + */ + public void removeElementResizeListener(Element element, + ElementResizeListener listener) { + Collection<ElementResizeListener> listeners = elementResizeListeners + .get(element); + if (listeners != null) { + listeners.remove(listener); + if (listeners.isEmpty()) { + elementResizeListeners.remove(element); + stopMeasuringIfUnecessary(element); + } + } + } + + private void stopMeasuringIfUnecessary(Element element) { + if (!needsMeasure(element)) { + measuredNonConnectorElements.remove(element); + setMeasuredSize(element, null); + } + } + + /** + * Informs this LayoutManager that the size of a component might have + * changed. If there is no upcoming layout phase, a new layout phase is + * scheduled. This method should be used whenever a size might have changed + * from outside of Vaadin's normal update phase, e.g. when an icon has been + * loaded or when the user resizes some part of the UI using the mouse. + * + * @param component + * the component whose size might have changed. + */ + public void setNeedsMeasure(ComponentConnector component) { + if (isLayoutRunning()) { + currentDependencyTree.setNeedsMeasure(component, true); + } else { + needsMeasure.add(component); + layoutLater(); + } + } + + public void setEverythingNeedsMeasure() { + everythingNeedsMeasure = true; + } + + /** + * Clean measured sizes which are no longer needed. Only for IE8. + */ + protected void cleanMeasuredSizes() { + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/LayoutManagerIE8.java b/client/src/com/vaadin/terminal/gwt/client/LayoutManagerIE8.java new file mode 100644 index 0000000000..3e47865cdc --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/LayoutManagerIE8.java @@ -0,0 +1,62 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.ui.RootPanel; + +public class LayoutManagerIE8 extends LayoutManager { + + private Map<Element, MeasuredSize> measuredSizes = new HashMap<Element, MeasuredSize>(); + + @Override + protected void setMeasuredSize(Element element, MeasuredSize measuredSize) { + if (measuredSize != null) { + measuredSizes.put(element, measuredSize); + } else { + measuredSizes.remove(element); + } + } + + @Override + protected MeasuredSize getMeasuredSize(Element element, + MeasuredSize defaultSize) { + MeasuredSize measured = measuredSizes.get(element); + if (measured != null) { + return measured; + } else { + return defaultSize; + } + } + + @Override + protected void cleanMeasuredSizes() { + Document document = RootPanel.get().getElement().getOwnerDocument(); + + Iterator<Element> i = measuredSizes.keySet().iterator(); + while (i.hasNext()) { + Element e = i.next(); + if (e.getOwnerDocument() != document) { + i.remove(); + } + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java b/client/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java new file mode 100644 index 0000000000..a2d6bdc860 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +@SuppressWarnings("serial") +public class LocaleNotLoadedException extends Exception { + + public LocaleNotLoadedException(String locale) { + super(locale); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/LocaleService.java b/client/src/com/vaadin/terminal/gwt/client/LocaleService.java new file mode 100644 index 0000000000..15a3230c58 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/LocaleService.java @@ -0,0 +1,160 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import com.google.gwt.core.client.JsArray; + +/** + * Date / time etc. localisation service for all widgets. Caches all loaded + * locales as JSONObjects. + * + * @author Vaadin Ltd. + * + */ +public class LocaleService { + + private static Map<String, ValueMap> cache = new HashMap<String, ValueMap>(); + private static String defaultLocale; + + public static void addLocale(ValueMap valueMap) { + + final String key = valueMap.getString("name"); + if (cache.containsKey(key)) { + cache.remove(key); + } + cache.put(key, valueMap); + if (cache.size() == 1) { + setDefaultLocale(key); + } + } + + public static void setDefaultLocale(String locale) { + defaultLocale = locale; + } + + public static String getDefaultLocale() { + return defaultLocale; + } + + public static Set<String> getAvailableLocales() { + return cache.keySet(); + } + + public static String[] getMonthNames(String locale) + throws LocaleNotLoadedException { + if (cache.containsKey(locale)) { + final ValueMap l = cache.get(locale); + return l.getStringArray("mn"); + } else { + throw new LocaleNotLoadedException(locale); + } + } + + public static String[] getShortMonthNames(String locale) + throws LocaleNotLoadedException { + if (cache.containsKey(locale)) { + final ValueMap l = cache.get(locale); + return l.getStringArray("smn"); + } else { + throw new LocaleNotLoadedException(locale); + } + } + + public static String[] getDayNames(String locale) + throws LocaleNotLoadedException { + if (cache.containsKey(locale)) { + final ValueMap l = cache.get(locale); + return l.getStringArray("dn"); + } else { + throw new LocaleNotLoadedException(locale); + } + } + + public static String[] getShortDayNames(String locale) + throws LocaleNotLoadedException { + if (cache.containsKey(locale)) { + final ValueMap l = cache.get(locale); + return l.getStringArray("sdn"); + } else { + throw new LocaleNotLoadedException(locale); + } + } + + public static int getFirstDayOfWeek(String locale) + throws LocaleNotLoadedException { + if (cache.containsKey(locale)) { + final ValueMap l = cache.get(locale); + return l.getInt("fdow"); + } else { + throw new LocaleNotLoadedException(locale); + } + } + + public static String getDateFormat(String locale) + throws LocaleNotLoadedException { + if (cache.containsKey(locale)) { + final ValueMap l = cache.get(locale); + return l.getString("df"); + } else { + throw new LocaleNotLoadedException(locale); + } + } + + public static boolean isTwelveHourClock(String locale) + throws LocaleNotLoadedException { + if (cache.containsKey(locale)) { + final ValueMap l = cache.get(locale); + return l.getBoolean("thc"); + } else { + throw new LocaleNotLoadedException(locale); + } + } + + public static String getClockDelimiter(String locale) + throws LocaleNotLoadedException { + if (cache.containsKey(locale)) { + final ValueMap l = cache.get(locale); + return l.getString("hmd"); + } else { + throw new LocaleNotLoadedException(locale); + } + } + + public static String[] getAmPmStrings(String locale) + throws LocaleNotLoadedException { + if (cache.containsKey(locale)) { + final ValueMap l = cache.get(locale); + return l.getStringArray("ampm"); + } else { + throw new LocaleNotLoadedException(locale); + } + + } + + public static void addLocales(JsArray<ValueMap> valueMapArray) { + for (int i = 0; i < valueMapArray.length(); i++) { + addLocale(valueMapArray.get(i)); + + } + + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/MeasuredSize.java b/client/src/com/vaadin/terminal/gwt/client/MeasuredSize.java new file mode 100644 index 0000000000..ff8b591461 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/MeasuredSize.java @@ -0,0 +1,240 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.core.client.JsArrayString; +import com.google.gwt.dom.client.Element; + +public class MeasuredSize { + public static class MeasureResult { + private final boolean widthChanged; + private final boolean heightChanged; + + private MeasureResult(boolean widthChanged, boolean heightChanged) { + this.widthChanged = widthChanged; + this.heightChanged = heightChanged; + } + + public boolean isHeightChanged() { + return heightChanged; + } + + public boolean isWidthChanged() { + return widthChanged; + } + + public boolean isChanged() { + return heightChanged || widthChanged; + } + } + + private int width = -1; + private int height = -1; + + private int[] paddings = new int[4]; + private int[] borders = new int[4]; + private int[] margins = new int[4]; + + private FastStringSet dependents = FastStringSet.create(); + + public int getOuterHeight() { + return height; + } + + public int getOuterWidth() { + return width; + } + + public void addDependent(String pid) { + dependents.add(pid); + } + + public void removeDependent(String pid) { + dependents.remove(pid); + } + + public boolean hasDependents() { + return !dependents.isEmpty(); + } + + public JsArrayString getDependents() { + return dependents.dump(); + } + + private static int sumWidths(int[] sizes) { + return sizes[1] + sizes[3]; + } + + private static int sumHeights(int[] sizes) { + return sizes[0] + sizes[2]; + } + + public int getInnerHeight() { + return height - sumHeights(margins) - sumHeights(borders) + - sumHeights(paddings); + } + + public int getInnerWidth() { + return width - sumWidths(margins) - sumWidths(borders) + - sumWidths(paddings); + } + + public boolean setOuterHeight(int height) { + if (this.height != height) { + this.height = height; + return true; + } else { + return false; + } + } + + public boolean setOuterWidth(int width) { + if (this.width != width) { + this.width = width; + return true; + } else { + return false; + } + } + + public int getBorderHeight() { + return sumHeights(borders); + } + + public int getBorderWidth() { + return sumWidths(borders); + } + + public int getPaddingHeight() { + return sumHeights(paddings); + } + + public int getPaddingWidth() { + return sumWidths(paddings); + } + + public int getMarginHeight() { + return sumHeights(margins); + } + + public int getMarginWidth() { + return sumWidths(margins); + } + + public int getMarginTop() { + return margins[0]; + } + + public int getMarginRight() { + return margins[1]; + } + + public int getMarginBottom() { + return margins[2]; + } + + public int getMarginLeft() { + return margins[3]; + } + + public int getBorderTop() { + return margins[0]; + } + + public int getBorderRight() { + return margins[1]; + } + + public int getBorderBottom() { + return margins[2]; + } + + public int getBorderLeft() { + return margins[3]; + } + + public int getPaddingTop() { + return paddings[0]; + } + + public int getPaddingRight() { + return paddings[1]; + } + + public int getPaddingBottom() { + return paddings[2]; + } + + public int getPaddingLeft() { + return paddings[3]; + } + + public MeasureResult measure(Element element) { + boolean heightChanged = false; + boolean widthChanged = false; + + ComputedStyle computedStyle = new ComputedStyle(element); + int[] paddings = computedStyle.getPadding(); + if (!heightChanged && hasHeightChanged(this.paddings, paddings)) { + heightChanged = true; + } + if (!widthChanged && hasWidthChanged(this.paddings, paddings)) { + widthChanged = true; + } + this.paddings = paddings; + + int[] margins = computedStyle.getMargin(); + if (!heightChanged && hasHeightChanged(this.margins, margins)) { + heightChanged = true; + } + if (!widthChanged && hasWidthChanged(this.margins, margins)) { + widthChanged = true; + } + this.margins = margins; + + int[] borders = computedStyle.getBorder(); + if (!heightChanged && hasHeightChanged(this.borders, borders)) { + heightChanged = true; + } + if (!widthChanged && hasWidthChanged(this.borders, borders)) { + widthChanged = true; + } + this.borders = borders; + + int requiredHeight = Util.getRequiredHeight(element); + int marginHeight = sumHeights(margins); + if (setOuterHeight(requiredHeight + marginHeight)) { + heightChanged = true; + } + + int requiredWidth = Util.getRequiredWidth(element); + int marginWidth = sumWidths(margins); + if (setOuterWidth(requiredWidth + marginWidth)) { + widthChanged = true; + } + + return new MeasureResult(widthChanged, heightChanged); + } + + private static boolean hasWidthChanged(int[] sizes1, int[] sizes2) { + return sizes1[1] != sizes2[1] || sizes1[3] != sizes2[3]; + } + + private static boolean hasHeightChanged(int[] sizes1, int[] sizes2) { + return sizes1[0] != sizes2[0] || sizes1[2] != sizes2[2]; + } + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/MouseEventDetailsBuilder.java b/client/src/com/vaadin/terminal/gwt/client/MouseEventDetailsBuilder.java new file mode 100644 index 0000000000..51af1a73a9 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/MouseEventDetailsBuilder.java @@ -0,0 +1,86 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.user.client.Event; +import com.vaadin.shared.MouseEventDetails; + +/** + * Helper class for constructing a MouseEventDetails object from a + * {@link NativeEvent}. + * + * @author Vaadin Ltd + * @since 7.0.0 + * + */ +public class MouseEventDetailsBuilder { + + /** + * Construct a {@link MouseEventDetails} object from the given event + * + * @param evt + * The event to use as a source for the details + * @return a MouseEventDetails containing information from the event + */ + public static MouseEventDetails buildMouseEventDetails(NativeEvent evt) { + return buildMouseEventDetails(evt, null); + } + + /** + * Construct a {@link MouseEventDetails} object from the given event + * + * @param evt + * The event to use as a source for the details + * @param relativeToElement + * The element whose position + * {@link MouseEventDetails#getRelativeX()} and + * {@link MouseEventDetails#getRelativeY()} are relative to. + * @return a MouseEventDetails containing information from the event + */ + public static MouseEventDetails buildMouseEventDetails(NativeEvent evt, + Element relativeToElement) { + MouseEventDetails mouseEventDetails = new MouseEventDetails(); + mouseEventDetails.setType(Event.getTypeInt(evt.getType())); + mouseEventDetails.setClientX(Util.getTouchOrMouseClientX(evt)); + mouseEventDetails.setClientY(Util.getTouchOrMouseClientY(evt)); + mouseEventDetails.setButton(evt.getButton()); + mouseEventDetails.setAltKey(evt.getAltKey()); + mouseEventDetails.setCtrlKey(evt.getCtrlKey()); + mouseEventDetails.setMetaKey(evt.getMetaKey()); + mouseEventDetails.setShiftKey(evt.getShiftKey()); + if (relativeToElement != null) { + mouseEventDetails.setRelativeX(getRelativeX( + mouseEventDetails.getClientX(), relativeToElement)); + mouseEventDetails.setRelativeY(getRelativeY( + mouseEventDetails.getClientY(), relativeToElement)); + } + return mouseEventDetails; + + } + + private static int getRelativeX(int clientX, Element target) { + return clientX - target.getAbsoluteLeft() + target.getScrollLeft() + + target.getOwnerDocument().getScrollLeft(); + } + + private static int getRelativeY(int clientY, Element target) { + return clientY - target.getAbsoluteTop() + target.getScrollTop() + + target.getOwnerDocument().getScrollTop(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/NullConsole.java b/client/src/com/vaadin/terminal/gwt/client/NullConsole.java new file mode 100644 index 0000000000..b17eaddd9f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/NullConsole.java @@ -0,0 +1,75 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.Set; + +import com.google.gwt.core.client.GWT; + +/** + * Client side console implementation for non-debug mode that discards all + * messages. + * + */ +public class NullConsole implements Console { + + @Override + public void dirUIDL(ValueMap u, ApplicationConnection conn) { + } + + @Override + public void error(String msg) { + GWT.log(msg); + } + + @Override + public void log(String msg) { + GWT.log(msg); + } + + @Override + public void printObject(Object msg) { + GWT.log(msg.toString()); + } + + @Override + public void printLayoutProblems(ValueMap meta, + ApplicationConnection applicationConnection, + Set<ComponentConnector> zeroHeightComponents, + Set<ComponentConnector> zeroWidthComponents) { + } + + @Override + public void log(Throwable e) { + GWT.log(e.getMessage(), e); + } + + @Override + public void error(Throwable e) { + // Borrow exception handling from VDebugConsole + VDebugConsole.handleError(e, this); + } + + @Override + public void setQuietMode(boolean quietDebugMode) { + } + + @Override + public void init() { + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/Paintable.java b/client/src/com/vaadin/terminal/gwt/client/Paintable.java new file mode 100644 index 0000000000..739f9d6594 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/Paintable.java @@ -0,0 +1,31 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +/** + * An interface used by client-side widgets or paintable parts to receive + * updates from the corresponding server-side components in the form of + * {@link UIDL}. + * + * Updates can be sent back to the server using the + * {@link ApplicationConnection#updateVariable()} methods. + */ +@Deprecated +public interface Paintable { + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/RenderInformation.java b/client/src/com/vaadin/terminal/gwt/client/RenderInformation.java new file mode 100644 index 0000000000..0fef0a9d65 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/RenderInformation.java @@ -0,0 +1,148 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.user.client.Element; + +/** + * Contains size information about a rendered container and its content area. + * + * @author Artur Signell + * + */ +public class RenderInformation { + + private RenderSpace contentArea = new RenderSpace(); + private Size renderedSize = new Size(-1, -1); + + public void setContentAreaWidth(int w) { + contentArea.setWidth(w); + } + + public void setContentAreaHeight(int h) { + contentArea.setHeight(h); + } + + public RenderSpace getContentAreaSize() { + return contentArea; + + } + + public Size getRenderedSize() { + return renderedSize; + } + + /** + * Update the size of the widget. + * + * @param widget + * + * @return true if the size has changed since last update + */ + public boolean updateSize(Element element) { + Size newSize = new Size(element.getOffsetWidth(), + element.getOffsetHeight()); + if (newSize.equals(renderedSize)) { + return false; + } else { + renderedSize = newSize; + return true; + } + } + + @Override + public String toString() { + return "RenderInformation [contentArea=" + contentArea + + ",renderedSize=" + renderedSize + "]"; + + } + + public static class FloatSize { + + private float width, height; + + public FloatSize(float width, float height) { + this.width = width; + this.height = height; + } + + public float getWidth() { + return width; + } + + public void setWidth(float width) { + this.width = width; + } + + public float getHeight() { + return height; + } + + public void setHeight(float height) { + this.height = height; + } + + } + + public static class Size { + + private int width, height; + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Size)) { + return false; + } + Size other = (Size) obj; + return other.width == width && other.height == height; + } + + @Override + public int hashCode() { + return (width << 8) | height; + } + + public Size() { + } + + public Size(int width, int height) { + this.height = height; + this.width = width; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + @Override + public String toString() { + return "Size [width=" + width + ",height=" + height + "]"; + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/RenderSpace.java b/client/src/com/vaadin/terminal/gwt/client/RenderSpace.java new file mode 100644 index 0000000000..e92a72de86 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/RenderSpace.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.vaadin.terminal.gwt.client.RenderInformation.Size; + +/** + * Contains information about render area. + */ +public class RenderSpace extends Size { + + private int scrollBarSize = 0; + + public RenderSpace(int width, int height) { + super(width, height); + } + + public RenderSpace() { + } + + public RenderSpace(int width, int height, boolean useNativeScrollbarSize) { + super(width, height); + if (useNativeScrollbarSize) { + scrollBarSize = Util.getNativeScrollbarSize(); + } + } + + /** + * Returns pixels available vertically for contained widget, including + * possible scrollbars. + */ + @Override + public int getHeight() { + return super.getHeight(); + } + + /** + * Returns pixels available horizontally for contained widget, including + * possible scrollbars. + */ + @Override + public int getWidth() { + return super.getWidth(); + } + + /** + * In case containing block has oveflow: auto, this method must return + * number of pixels used by scrollbar. Returning zero means either that no + * scrollbar will be visible. + */ + public int getScrollbarSize() { + return scrollBarSize; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ResourceLoader.java b/client/src/com/vaadin/terminal/gwt/client/ResourceLoader.java new file mode 100644 index 0000000000..57083641ba --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ResourceLoader.java @@ -0,0 +1,551 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.google.gwt.core.client.Duration; +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.RepeatingCommand; +import com.google.gwt.dom.client.AnchorElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.LinkElement; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.ObjectElement; +import com.google.gwt.dom.client.ScriptElement; +import com.google.gwt.user.client.Timer; + +/** + * ResourceLoader lets you dynamically include external scripts and styles on + * the page and lets you know when the resource has been loaded. + * + * You can also preload resources, allowing them to get cached by the browser + * without being evaluated. This enables downloading multiple resources at once + * while still controlling in which order e.g. scripts are executed. + * + * @author Vaadin Ltd + * @since 7.0.0 + */ +public class ResourceLoader { + /** + * Event fired when a resource has been loaded. + */ + public static class ResourceLoadEvent { + private ResourceLoader loader; + private String resourceUrl; + private final boolean preload; + + /** + * Creates a new event. + * + * @param loader + * the resource loader that has loaded the resource + * @param resourceUrl + * the url of the loaded resource + * @param preload + * true if the resource has only been preloaded, false if + * it's fully loaded + */ + public ResourceLoadEvent(ResourceLoader loader, String resourceUrl, + boolean preload) { + this.loader = loader; + this.resourceUrl = resourceUrl; + this.preload = preload; + } + + /** + * Gets the resource loader that has fired this event + * + * @return the resource loader + */ + public ResourceLoader getResourceLoader() { + return loader; + } + + /** + * Gets the absolute url of the loaded resource. + * + * @return the absolute url of the loaded resource + */ + public String getResourceUrl() { + return resourceUrl; + } + + /** + * Returns true if the resource has been preloaded, false if it's fully + * loaded + * + * @see ResourceLoader#preloadResource(String, ResourceLoadListener) + * + * @return true if the resource has been preloaded, false if it's fully + * loaded + */ + public boolean isPreload() { + return preload; + } + } + + /** + * Event listener that gets notified when a resource has been loaded + */ + public interface ResourceLoadListener { + /** + * Notifies this ResourceLoadListener that a resource has been loaded. + * Some browsers do not support any way of detecting load errors. In + * these cases, onLoad will be called regardless of the status. + * + * @see ResourceLoadEvent + * + * @param event + * a resource load event with information about the loaded + * resource + */ + public void onLoad(ResourceLoadEvent event); + + /** + * Notifies this ResourceLoadListener that a resource could not be + * loaded, e.g. because the file could not be found or because the + * server did not respond. Some browsers do not support any way of + * detecting load errors. In these cases, onLoad will be called + * regardless of the status. + * + * @see ResourceLoadEvent + * + * @param event + * a resource load event with information about the resource + * that could not be loaded. + */ + public void onError(ResourceLoadEvent event); + } + + private static final ResourceLoader INSTANCE = GWT + .create(ResourceLoader.class); + + private ApplicationConnection connection; + + private final Set<String> loadedResources = new HashSet<String>(); + private final Set<String> preloadedResources = new HashSet<String>(); + + private final Map<String, Collection<ResourceLoadListener>> loadListeners = new HashMap<String, Collection<ResourceLoadListener>>(); + private final Map<String, Collection<ResourceLoadListener>> preloadListeners = new HashMap<String, Collection<ResourceLoadListener>>(); + + private final Element head; + + /** + * Creates a new resource loader. You should generally not create you own + * resource loader, but instead use {@link ResourceLoader#get()} to get an + * instance. + */ + protected ResourceLoader() { + Document document = Document.get(); + head = document.getElementsByTagName("head").getItem(0); + + // detect already loaded scripts and stylesheets + NodeList<Element> scripts = document.getElementsByTagName("script"); + for (int i = 0; i < scripts.getLength(); i++) { + ScriptElement element = ScriptElement.as(scripts.getItem(i)); + String src = element.getSrc(); + if (src != null && src.length() != 0) { + loadedResources.add(src); + } + } + + NodeList<Element> links = document.getElementsByTagName("link"); + for (int i = 0; i < links.getLength(); i++) { + LinkElement linkElement = LinkElement.as(links.getItem(i)); + String rel = linkElement.getRel(); + String href = linkElement.getHref(); + if ("stylesheet".equalsIgnoreCase(rel) && href != null + && href.length() != 0) { + loadedResources.add(href); + } + } + } + + /** + * Returns the default ResourceLoader + * + * @return the default ResourceLoader + */ + public static ResourceLoader get() { + return INSTANCE; + } + + /** + * Load a script and notify a listener when the script is loaded. Calling + * this method when the script is currently loading or already loaded + * doesn't cause the script to be loaded again, but the listener will still + * be notified when appropriate. + * + * + * @param scriptUrl + * the url of the script to load + * @param resourceLoadListener + * the listener that will get notified when the script is loaded + */ + public void loadScript(final String scriptUrl, + final ResourceLoadListener resourceLoadListener) { + final String url = getAbsoluteUrl(scriptUrl); + ResourceLoadEvent event = new ResourceLoadEvent(this, url, false); + if (loadedResources.contains(url)) { + if (resourceLoadListener != null) { + resourceLoadListener.onLoad(event); + } + return; + } + + if (preloadListeners.containsKey(url)) { + // Preload going on, continue when preloaded + preloadResource(url, new ResourceLoadListener() { + @Override + public void onLoad(ResourceLoadEvent event) { + loadScript(url, resourceLoadListener); + } + + @Override + public void onError(ResourceLoadEvent event) { + // Preload failed -> signal error to own listener + if (resourceLoadListener != null) { + resourceLoadListener.onError(event); + } + } + }); + return; + } + + if (addListener(url, resourceLoadListener, loadListeners)) { + ScriptElement scriptTag = Document.get().createScriptElement(); + scriptTag.setSrc(url); + scriptTag.setType("text/javascript"); + addOnloadHandler(scriptTag, new ResourceLoadListener() { + @Override + public void onLoad(ResourceLoadEvent event) { + fireLoad(event); + } + + @Override + public void onError(ResourceLoadEvent event) { + fireError(event); + } + }, event); + head.appendChild(scriptTag); + } + } + + private static String getAbsoluteUrl(String url) { + AnchorElement a = Document.get().createAnchorElement(); + a.setHref(url); + return a.getHref(); + } + + /** + * Download a resource and notify a listener when the resource is loaded + * without attempting to interpret the resource. When a resource has been + * preloaded, it will be present in the browser's cache (provided the HTTP + * headers allow caching), making a subsequent load operation complete + * without having to wait for the resource to be downloaded again. + * + * Calling this method when the resource is currently loading, currently + * preloading, already preloaded or already loaded doesn't cause the + * resource to be preloaded again, but the listener will still be notified + * when appropriate. + * + * @param url + * the url of the resource to preload + * @param resourceLoadListener + * the listener that will get notified when the resource is + * preloaded + */ + public void preloadResource(String url, + ResourceLoadListener resourceLoadListener) { + url = getAbsoluteUrl(url); + ResourceLoadEvent event = new ResourceLoadEvent(this, url, true); + if (loadedResources.contains(url) || preloadedResources.contains(url)) { + // Already loaded or preloaded -> just fire listener + if (resourceLoadListener != null) { + resourceLoadListener.onLoad(event); + } + return; + } + + if (addListener(url, resourceLoadListener, preloadListeners) + && !loadListeners.containsKey(url)) { + // Inject loader element if this is the first time this is preloaded + // AND the resources isn't already being loaded in the normal way + + Element element = getPreloadElement(url); + addOnloadHandler(element, new ResourceLoadListener() { + @Override + public void onLoad(ResourceLoadEvent event) { + fireLoad(event); + } + + @Override + public void onError(ResourceLoadEvent event) { + fireError(event); + } + }, event); + + // TODO Remove object when loaded (without causing spinner in FF) + Document.get().getBody().appendChild(element); + } + } + + private static Element getPreloadElement(String url) { + if (BrowserInfo.get().isIE()) { + ScriptElement element = Document.get().createScriptElement(); + element.setSrc(url); + element.setType("text/cache"); + return element; + } else { + ObjectElement element = Document.get().createObjectElement(); + element.setData(url); + element.setType("text/plain"); + element.setHeight("0px"); + element.setWidth("0px"); + return element; + } + } + + private native void addOnloadHandler(Element element, + ResourceLoadListener listener, ResourceLoadEvent event) + /*-{ + element.onload = $entry(function() { + element.onload = null; + element.onerror = null; + element.onreadystatechange = null; + listener.@com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadListener::onLoad(Lcom/vaadin/terminal/gwt/client/ResourceLoader$ResourceLoadEvent;)(event); + }); + element.onerror = $entry(function() { + element.onload = null; + element.onerror = null; + element.onreadystatechange = null; + listener.@com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadListener::onError(Lcom/vaadin/terminal/gwt/client/ResourceLoader$ResourceLoadEvent;)(event); + }); + element.onreadystatechange = function() { + if ("loaded" === element.readyState || "complete" === element.readyState ) { + element.onload(arguments[0]); + } + }; + }-*/; + + /** + * Load a stylesheet and notify a listener when the stylesheet is loaded. + * Calling this method when the stylesheet is currently loading or already + * loaded doesn't cause the stylesheet to be loaded again, but the listener + * will still be notified when appropriate. + * + * @param stylesheetUrl + * the url of the stylesheet to load + * @param resourceLoadListener + * the listener that will get notified when the stylesheet is + * loaded + */ + public void loadStylesheet(final String stylesheetUrl, + final ResourceLoadListener resourceLoadListener) { + final String url = getAbsoluteUrl(stylesheetUrl); + final ResourceLoadEvent event = new ResourceLoadEvent(this, url, false); + if (loadedResources.contains(url)) { + if (resourceLoadListener != null) { + resourceLoadListener.onLoad(event); + } + return; + } + + if (preloadListeners.containsKey(url)) { + // Preload going on, continue when preloaded + preloadResource(url, new ResourceLoadListener() { + @Override + public void onLoad(ResourceLoadEvent event) { + loadStylesheet(url, resourceLoadListener); + } + + @Override + public void onError(ResourceLoadEvent event) { + // Preload failed -> signal error to own listener + if (resourceLoadListener != null) { + resourceLoadListener.onError(event); + } + } + }); + return; + } + + if (addListener(url, resourceLoadListener, loadListeners)) { + LinkElement linkElement = Document.get().createLinkElement(); + linkElement.setRel("stylesheet"); + linkElement.setType("text/css"); + linkElement.setHref(url); + + if (BrowserInfo.get().isSafari()) { + // Safari doesn't fire any events for link elements + // See http://www.phpied.com/when-is-a-stylesheet-really-loaded/ + Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() { + private final Duration duration = new Duration(); + + @Override + public boolean execute() { + int styleSheetLength = getStyleSheetLength(url); + if (getStyleSheetLength(url) > 0) { + fireLoad(event); + return false; // Stop repeating + } else if (styleSheetLength == 0) { + // "Loaded" empty sheet -> most likely 404 error + fireError(event); + return true; + } else if (duration.elapsedMillis() > 60 * 1000) { + fireError(event); + return false; + } else { + return true; // Continue repeating + } + } + }, 10); + } else { + addOnloadHandler(linkElement, new ResourceLoadListener() { + @Override + public void onLoad(ResourceLoadEvent event) { + // Chrome && IE fires load for errors, must check + // stylesheet data + if (BrowserInfo.get().isChrome() + || BrowserInfo.get().isIE()) { + int styleSheetLength = getStyleSheetLength(url); + // Error if there's an empty stylesheet + if (styleSheetLength == 0) { + fireError(event); + return; + } + } + fireLoad(event); + } + + @Override + public void onError(ResourceLoadEvent event) { + fireError(event); + } + }, event); + if (BrowserInfo.get().isOpera()) { + // Opera onerror never fired, assume error if no onload in x + // seconds + new Timer() { + @Override + public void run() { + if (!loadedResources.contains(url)) { + fireError(event); + } + } + }.schedule(5 * 1000); + } + } + + head.appendChild(linkElement); + } + } + + private static native int getStyleSheetLength(String url) + /*-{ + for(var i = 0; i < $doc.styleSheets.length; i++) { + if ($doc.styleSheets[i].href === url) { + var sheet = $doc.styleSheets[i]; + try { + var rules = sheet.cssRules + if (rules === undefined) { + rules = sheet.rules; + } + + if (rules === null) { + // Style sheet loaded, but can't access length because of XSS -> assume there's something there + return 1; + } + + // Return length so we can distinguish 0 (probably 404 error) from normal case. + return rules.length; + } catch (err) { + return 1; + } + } + } + // No matching stylesheet found -> not yet loaded + return -1; + }-*/; + + private static boolean addListener(String url, + ResourceLoadListener listener, + Map<String, Collection<ResourceLoadListener>> listenerMap) { + Collection<ResourceLoadListener> listeners = listenerMap.get(url); + if (listeners == null) { + listeners = new HashSet<ResourceLoader.ResourceLoadListener>(); + listeners.add(listener); + listenerMap.put(url, listeners); + return true; + } else { + listeners.add(listener); + return false; + } + } + + private void fireError(ResourceLoadEvent event) { + String resource = event.getResourceUrl(); + + Collection<ResourceLoadListener> listeners; + if (event.isPreload()) { + // Also fire error for load listeners + fireError(new ResourceLoadEvent(this, resource, false)); + listeners = preloadListeners.remove(resource); + } else { + listeners = loadListeners.remove(resource); + } + if (listeners != null && !listeners.isEmpty()) { + for (ResourceLoadListener listener : listeners) { + if (listener != null) { + listener.onError(event); + } + } + } + } + + private void fireLoad(ResourceLoadEvent event) { + String resource = event.getResourceUrl(); + Collection<ResourceLoadListener> listeners; + if (event.isPreload()) { + preloadedResources.add(resource); + listeners = preloadListeners.remove(resource); + } else { + if (preloadListeners.containsKey(resource)) { + // Also fire preload events for potential listeners + fireLoad(new ResourceLoadEvent(this, resource, true)); + } + preloadedResources.remove(resource); + loadedResources.add(resource); + listeners = loadListeners.remove(resource); + } + if (listeners != null && !listeners.isEmpty()) { + for (ResourceLoadListener listener : listeners) { + if (listener != null) { + listener.onLoad(event); + } + } + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ServerConnector.java b/client/src/com/vaadin/terminal/gwt/client/ServerConnector.java new file mode 100644 index 0000000000..ff37f04f04 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ServerConnector.java @@ -0,0 +1,130 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.Collection; +import java.util.List; + +import com.google.gwt.event.shared.GwtEvent; +import com.google.web.bindery.event.shared.HandlerRegistration; +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.ClientRpc; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent.StateChangeHandler; + +/** + * Interface implemented by all client side classes that can be communicate with + * the server. Classes implementing this interface are initialized by the + * framework when needed and have the ability to communicate with the server. + * + * @author Vaadin Ltd + * @since 7.0.0 + */ +public interface ServerConnector extends Connector { + + /** + * Gets ApplicationConnection instance that created this connector. + * + * @return The ApplicationConnection as set by + * {@link #doInit(String, ApplicationConnection)} + */ + public ApplicationConnection getConnection(); + + /** + * Tests whether the connector is enabled or not. This method checks that + * the connector is enabled in context, i.e. if the parent connector is + * disabled, this method must return false. + * + * @return true if the connector is enabled, false otherwise + */ + public boolean isEnabled(); + + /** + * + * Called once by the framework to initialize the connector. + * <p> + * Note that the shared state is not yet available at this point nor any + * hierarchy information. + */ + public void doInit(String connectorId, ApplicationConnection connection); + + /** + * For internal use by the framework: returns the registered RPC + * implementations for an RPC interface identifier. + * + * TODO interface identifier type or format may change + * + * @param rpcInterfaceId + * RPC interface identifier: fully qualified interface type name + * @return RPC interface implementations registered for an RPC interface, + * not null + */ + public <T extends ClientRpc> Collection<T> getRpcImplementations( + String rpcInterfaceId); + + /** + * Adds a handler that is called whenever some part of the state has been + * updated by the server. + * + * @param handler + * The handler that should be added. + * @return A handler registration reference that can be used to unregister + * the handler + */ + public HandlerRegistration addStateChangeHandler(StateChangeHandler handler); + + /** + * Sends the given event to all registered handlers. + * + * @param event + * The event to send. + */ + public void fireEvent(GwtEvent<?> event); + + /** + * Event called when connector has been unregistered. + */ + public void onUnregister(); + + /** + * Returns the parent of this connector. Can be null for only the root + * connector. + * + * @return The parent of this connector, as set by + * {@link #setParent(ServerConnector)}. + */ + @Override + public ServerConnector getParent(); + + /** + * Sets the parent for this connector. This method should only be called by + * the framework to ensure that the connector hierarchy on the client side + * and the server side are in sync. + * <p> + * Note that calling this method does not fire a + * {@link ConnectorHierarchyChangeEvent}. The event is fired only when the + * whole hierarchy has been updated. + * + * @param parent + * The new parent of the connector + */ + public void setParent(ServerConnector parent); + + public void updateEnabledState(boolean enabledState); + + public void setChildren(List<ServerConnector> children); + + public List<ServerConnector> getChildren(); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/SimpleTree.java b/client/src/com/vaadin/terminal/gwt/client/SimpleTree.java new file mode 100644 index 0000000000..d5d81d7e44 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/SimpleTree.java @@ -0,0 +1,129 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.SpanElement; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.BorderStyle; +import com.google.gwt.dom.client.Style.Cursor; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; + +public class SimpleTree extends ComplexPanel { + private Element children = Document.get().createDivElement().cast(); + private SpanElement handle = Document.get().createSpanElement(); + private SpanElement text = Document.get().createSpanElement(); + + public SimpleTree() { + setElement(Document.get().createDivElement()); + Style style = getElement().getStyle(); + style.setProperty("whiteSpace", "nowrap"); + style.setPadding(3, Unit.PX); + style.setPaddingLeft(12, Unit.PX); + + style = handle.getStyle(); + style.setDisplay(Display.NONE); + style.setProperty("textAlign", "center"); + style.setWidth(10, Unit.PX); + style.setCursor(Cursor.POINTER); + style.setBorderStyle(BorderStyle.SOLID); + style.setBorderColor("#666"); + style.setBorderWidth(1, Unit.PX); + style.setMarginRight(3, Unit.PX); + style.setProperty("borderRadius", "4px"); + handle.setInnerHTML("+"); + getElement().appendChild(handle); + getElement().appendChild(text); + style = children.getStyle(); + style.setPaddingLeft(9, Unit.PX); + style.setDisplay(Display.NONE); + + getElement().appendChild(children); + addDomHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (event.getNativeEvent().getEventTarget().cast() == handle) { + if (children.getStyle().getDisplay().intern() == Display.NONE + .getCssName()) { + open(event.getNativeEvent().getShiftKey()); + } else { + close(); + } + + } else if (event.getNativeEvent().getEventTarget().cast() == text) { + select(event); + } + } + }, ClickEvent.getType()); + } + + protected void select(ClickEvent event) { + + } + + public void close() { + children.getStyle().setDisplay(Display.NONE); + handle.setInnerHTML("+"); + } + + public void open(boolean recursive) { + handle.setInnerHTML("-"); + children.getStyle().setDisplay(Display.BLOCK); + if (recursive) { + for (Widget w : getChildren()) { + if (w instanceof SimpleTree) { + SimpleTree str = (SimpleTree) w; + str.open(true); + } + } + } + } + + public SimpleTree(String caption) { + this(); + setText(caption); + } + + public void setText(String text) { + this.text.setInnerText(text); + } + + public void addItem(String text) { + Label label = new Label(text); + add(label, children); + } + + @Override + public void add(Widget child) { + add(child, children); + } + + @Override + protected void add(Widget child, Element container) { + super.add(child, container); + handle.getStyle().setDisplay(Display.INLINE_BLOCK); + getElement().getStyle().setPaddingLeft(3, Unit.PX); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/StyleConstants.java b/client/src/com/vaadin/terminal/gwt/client/StyleConstants.java new file mode 100644 index 0000000000..b4955ccd14 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/StyleConstants.java @@ -0,0 +1,29 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +public class StyleConstants { + + public static final String MARGIN_TOP = "margin-top"; + public static final String MARGIN_RIGHT = "margin-right"; + public static final String MARGIN_BOTTOM = "margin-bottom"; + public static final String MARGIN_LEFT = "margin-left"; + + public static final String VERTICAL_SPACING = "vspacing"; + public static final String HORIZONTAL_SPACING = "hspacing"; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/SuperDevMode.java b/client/src/com/vaadin/terminal/gwt/client/SuperDevMode.java new file mode 100644 index 0000000000..200ceb730a --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/SuperDevMode.java @@ -0,0 +1,264 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.http.client.UrlBuilder; +import com.google.gwt.jsonp.client.JsonpRequestBuilder; +import com.google.gwt.storage.client.Storage; +import com.google.gwt.user.client.Window.Location; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.vaadin.terminal.gwt.client.ui.notification.VNotification; +import com.vaadin.terminal.gwt.client.ui.notification.VNotification.EventListener; +import com.vaadin.terminal.gwt.client.ui.notification.VNotification.HideEvent; + +/** + * Class that enables SuperDevMode using a ?superdevmode parameter in the url. + * + * @author Vaadin Ltd + * @since 7.0 + * + */ +public class SuperDevMode { + + private static final int COMPILE_TIMEOUT_IN_SECONDS = 60; + protected static final String SKIP_RECOMPILE = "VaadinSuperDevMode_skip_recompile"; + + public static class RecompileResult extends JavaScriptObject { + protected RecompileResult() { + + } + + public final native boolean ok() + /*-{ + return this.status == "ok"; + }-*/; + } + + private static void recompileWidgetsetAndStartInDevMode( + final String serverUrl) { + VConsole.log("Recompiling widgetset using<br/>" + serverUrl + + "<br/>and then reloading in super dev mode"); + VNotification n = new VNotification(); + n.show("<b>Recompiling widgetset, this should not take too long</b>", + VNotification.CENTERED, VNotification.STYLE_SYSTEM); + + JsonpRequestBuilder b = new JsonpRequestBuilder(); + b.setCallbackParam("_callback"); + b.setTimeout(COMPILE_TIMEOUT_IN_SECONDS * 1000); + b.requestObject(serverUrl + "recompile/" + GWT.getModuleName() + "?" + + getRecompileParameters(GWT.getModuleName()), + new AsyncCallback<RecompileResult>() { + + @Override + public void onSuccess(RecompileResult result) { + VConsole.log("JSONP compile call successful"); + + if (!result.ok()) { + VConsole.log("* result: " + result); + failed(); + return; + } + + setSession( + getSuperDevModeHookKey(), + getSuperDevWidgetSetUrl(GWT.getModuleName(), + serverUrl)); + setSession(SKIP_RECOMPILE, "1"); + + VConsole.log("* result: OK. Reloading"); + Location.reload(); + } + + @Override + public void onFailure(Throwable caught) { + VConsole.error("JSONP compile call failed"); + // Don't log exception as they are shown as + // notifications + VConsole.error(Util.getSimpleName(caught) + ": " + + caught.getMessage()); + failed(); + + } + + private void failed() { + VNotification n = new VNotification(); + n.addEventListener(new EventListener() { + + @Override + public void notificationHidden(HideEvent event) { + recompileWidgetsetAndStartInDevMode(serverUrl); + } + }); + n.show("Recompilation failed.<br/>" + + "Make sure CodeServer is running, " + + "check its output and click to retry", + VNotification.CENTERED, + VNotification.STYLE_SYSTEM); + } + }); + + } + + protected static String getSuperDevWidgetSetUrl(String widgetsetName, + String serverUrl) { + return serverUrl + GWT.getModuleName() + "/" + GWT.getModuleName() + + ".nocache.js"; + } + + private native static String getRecompileParameters(String moduleName) + /*-{ + var prop_map = $wnd.__gwt_activeModules[moduleName].bindings(); + + // convert map to URL parameter string + var props = []; + for (var key in prop_map) { + props.push(encodeURIComponent(key) + '=' + encodeURIComponent(prop_map[key])) + } + + return props.join('&') + '&'; + }-*/; + + private static void setSession(String key, String value) { + Storage.getSessionStorageIfSupported().setItem(key, value); + } + + private static String getSession(String key) { + return Storage.getSessionStorageIfSupported().getItem(key); + } + + private static void removeSession(String key) { + Storage.getSessionStorageIfSupported().removeItem(key); + } + + protected static void disableDevModeAndReload() { + removeSession(getSuperDevModeHookKey()); + redirect(false); + } + + protected static void redirect(boolean devModeOn) { + UrlBuilder createUrlBuilder = Location.createUrlBuilder(); + if (!devModeOn) { + createUrlBuilder.removeParameter("superdevmode"); + } else { + createUrlBuilder.setParameter("superdevmode", ""); + } + + Location.assign(createUrlBuilder.buildString()); + + } + + private static String getSuperDevModeHookKey() { + String widgetsetName = GWT.getModuleName(); + final String superDevModeKey = "__gwtDevModeHook:" + widgetsetName; + return superDevModeKey; + } + + private static boolean hasSession(String key) { + return getSession(key) != null; + } + + /** + * The URL of the code server. The default URL (http://localhost:9876/) will + * be used if this is empty or null. + * + * @param serverUrl + * The url of the code server or null to use the default + * @return true if recompile started, false if we are running in + * SuperDevMode + */ + protected static boolean recompileIfNeeded(String serverUrl) { + if (serverUrl == null || "".equals(serverUrl)) { + serverUrl = "http://localhost:9876/"; + } else { + serverUrl = "http://" + serverUrl + "/"; + } + + if (hasSession(SKIP_RECOMPILE)) { + VConsole.log("Running in SuperDevMode"); + // When we get here, we are running in super dev mode + + // Remove the flag so next reload will recompile + removeSession(SKIP_RECOMPILE); + + // Remove the gwt flag so we will not end up in dev mode if we + // remove the url parameter manually + removeSession(getSuperDevModeHookKey()); + + return false; + } + + recompileWidgetsetAndStartInDevMode(serverUrl); + return true; + } + + protected static boolean isSuperDevModeEnabledInModule() { + String moduleName = GWT.getModuleName(); + return isSuperDevModeEnabledInModule(moduleName); + } + + protected native static boolean isSuperDevModeEnabledInModule( + String moduleName) + /*-{ + if (!$wnd.__gwt_activeModules) + return false; + var mod = $wnd.__gwt_activeModules[moduleName]; + if (!mod) + return false; + + if (mod.superdevmode) { + // Running in super dev mode already, it is supported + return true; + } + + return !!mod.canRedirect; + }-*/; + + /** + * Enables SuperDevMode if the url contains the "superdevmode" parameter. + * <p> + * The caller should not continue initialization of the application if this + * method returns true. The application will be restarted once compilation + * is done and then this method will return false. + * </p> + * + * @return true if a recompile operation has started and the page will be + * reloaded once it is done, false if no recompilation will be done. + */ + public static boolean enableBasedOnParameter() { + String superDevModeParameter = Location.getParameter("superdevmode"); + if (superDevModeParameter != null) { + // Need to check the recompile flag also because if we are running + // in super dev mode, as a result of the recompile, the enabled + // check will fail... + if (!isSuperDevModeEnabledInModule()) { + showError("SuperDevMode is not enabled for this module/widgetset.<br/>" + + "Ensure that your module definition (.gwt.xml) contains <br/>" + + "<add-linker name="xsiframe"/><br/>" + + "<set-configuration-property name="devModeRedirectEnabled" value="true" /><br/>"); + return false; + } + return SuperDevMode.recompileIfNeeded(superDevModeParameter); + } + return false; + } + + private static void showError(String message) { + VNotification n = new VNotification(); + n.show(message, VNotification.CENTERED_TOP, VNotification.STYLE_SYSTEM); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/SynchronousXHR.java b/client/src/com/vaadin/terminal/gwt/client/SynchronousXHR.java new file mode 100644 index 0000000000..a0a399842a --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/SynchronousXHR.java @@ -0,0 +1,36 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.xhr.client.XMLHttpRequest; + +public class SynchronousXHR extends XMLHttpRequest { + + protected SynchronousXHR() { + } + + public native final void synchronousPost(String uri, String requestData) + /*-{ + try { + this.open("POST", uri, false); + this.setRequestHeader("Content-Type", "text/plain;charset=utf-8"); + this.send(requestData); + } catch (e) { + // No errors are managed as this is synchronous forceful send that can just fail + } + }-*/; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/TooltipInfo.java b/client/src/com/vaadin/terminal/gwt/client/TooltipInfo.java new file mode 100644 index 0000000000..e0a398b90d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/TooltipInfo.java @@ -0,0 +1,66 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +public class TooltipInfo { + + private String title; + + private String errorMessageHtml; + + public TooltipInfo() { + } + + public TooltipInfo(String tooltip) { + setTitle(tooltip); + } + + public TooltipInfo(String tooltip, String errorMessage) { + setTitle(tooltip); + setErrorMessage(errorMessage); + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getErrorMessage() { + return errorMessageHtml; + } + + public void setErrorMessage(String errorMessage) { + errorMessageHtml = errorMessage; + } + + /** + * Checks is a message has been defined for the tooltip. + * + * @return true if title or error message is present, false if both are + * empty + */ + public boolean hasMessage() { + return (title != null && !title.isEmpty()) + || (errorMessageHtml != null && !errorMessageHtml.isEmpty()); + } + + public boolean equals(TooltipInfo other) { + return (other != null && other.title == title && other.errorMessageHtml == errorMessageHtml); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/UIDL.java b/client/src/com/vaadin/terminal/gwt/client/UIDL.java new file mode 100644 index 0000000000..9032a04e24 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/UIDL.java @@ -0,0 +1,563 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArrayString; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Component; + +/** + * When a component is updated, it's client side widget's + * {@link ComponentConnector#updateFromUIDL(UIDL, ApplicationConnection) + * updateFromUIDL()} will be called with the updated ("changes") UIDL received + * from the server. + * <p> + * UIDL is hierarchical, and there are a few methods to retrieve the children, + * {@link #getChildCount()}, {@link #getChildIterator()} + * {@link #getChildString(int)}, {@link #getChildUIDL(int)}. + * </p> + * <p> + * It can be helpful to keep in mind that UIDL was originally modeled in XML, so + * it's structure is very XML -like. For instance, the first to children in the + * underlying UIDL representation will contain the "tag" name and attributes, + * but will be skipped by the methods mentioned above. + * </p> + */ +public final class UIDL extends JavaScriptObject { + + protected UIDL() { + } + + /** + * Shorthand for getting the attribute named "id", which for Paintables is + * the essential paintableId which binds the server side component to the + * client side widget. + * + * @return the value of the id attribute, if available + */ + public String getId() { + return getStringAttribute("id"); + } + + /** + * Gets the name of this UIDL section, as created with + * {@link PaintTarget#startTag(String) PaintTarget.startTag()} in the + * server-side {@link Component#paint(PaintTarget) Component.paint()} or + * (usually) {@link AbstractComponent#paintContent(PaintTarget) + * AbstractComponent.paintContent()}. Note that if the UIDL corresponds to a + * Paintable, a component identifier will be returned instead - this is used + * internally and is not needed within + * {@link ComponentConnector#updateFromUIDL(UIDL, ApplicationConnection) + * updateFromUIDL()}. + * + * @return the name for this section + */ + public native String getTag() + /*-{ + return this[0]; + }-*/; + + private native ValueMap attr() + /*-{ + return this[1]; + }-*/; + + private native ValueMap var() + /*-{ + return this[1]["v"]; + }-*/; + + private native boolean hasVariables() + /*-{ + return Boolean(this[1]["v"]); + }-*/; + + /** + * Gets the named attribute as a String. + * + * @param name + * the name of the attribute to get + * @return the attribute value + */ + public String getStringAttribute(String name) { + return attr().getString(name); + } + + /** + * Gets the names of the attributes available. + * + * @return the names of available attributes + */ + public Set<String> getAttributeNames() { + Set<String> keySet = attr().getKeySet(); + keySet.remove("v"); + return keySet; + } + + /** + * Gets the names of variables available. + * + * @return the names of available variables + */ + public Set<String> getVariableNames() { + if (!hasVariables()) { + return new HashSet<String>(); + } else { + Set<String> keySet = var().getKeySet(); + return keySet; + } + } + + /** + * Gets the named attribute as an int. + * + * @param name + * the name of the attribute to get + * @return the attribute value + */ + public int getIntAttribute(String name) { + return attr().getInt(name); + } + + /** + * Gets the named attribute as a long. + * + * @param name + * the name of the attribute to get + * @return the attribute value + */ + public long getLongAttribute(String name) { + return (long) attr().getRawNumber(name); + } + + /** + * Gets the named attribute as a float. + * + * @param name + * the name of the attribute to get + * @return the attribute value + */ + public float getFloatAttribute(String name) { + return (float) attr().getRawNumber(name); + } + + /** + * Gets the named attribute as a double. + * + * @param name + * the name of the attribute to get + * @return the attribute value + */ + public double getDoubleAttribute(String name) { + return attr().getRawNumber(name); + } + + /** + * Gets the named attribute as a boolean. + * + * @param name + * the name of the attribute to get + * @return the attribute value + */ + public boolean getBooleanAttribute(String name) { + return attr().getBoolean(name); + } + + /** + * Gets the named attribute as a Map of named values (key/value pairs). + * + * @param name + * the name of the attribute to get + * @return the attribute Map + */ + public ValueMap getMapAttribute(String name) { + return attr().getValueMap(name); + } + + /** + * Gets the named attribute as an array of Strings. + * + * @param name + * the name of the attribute to get + * @return the attribute value + */ + public String[] getStringArrayAttribute(String name) { + return attr().getStringArray(name); + } + + /** + * Gets the named attribute as an int array. + * + * @param name + * the name of the attribute to get + * @return the attribute value + */ + public int[] getIntArrayAttribute(final String name) { + return attr().getIntArray(name); + } + + /** + * Get attributes value as string whatever the type is + * + * @param name + * @return string presentation of attribute + */ + native String getAttribute(String name) + /*-{ + return '' + this[1][name]; + }-*/; + + native String getVariable(String name) + /*-{ + return '' + this[1]['v'][name]; + }-*/; + + /** + * Indicates whether or not the named attribute is available. + * + * @param name + * the name of the attribute to check + * @return true if the attribute is available, false otherwise + */ + public boolean hasAttribute(final String name) { + return attr().containsKey(name); + } + + /** + * Gets the UIDL for the child at the given index. + * + * @param i + * the index of the child to get + * @return the UIDL of the child if it exists + */ + public native UIDL getChildUIDL(int i) + /*-{ + return this[i + 2]; + }-*/; + + /** + * Gets the child at the given index as a String. + * + * @param i + * the index of the child to get + * @return the String representation of the child if it exists + */ + public native String getChildString(int i) + /*-{ + return this[i + 2]; + }-*/; + + private native XML getChildXML(int index) + /*-{ + return this[index + 2]; + }-*/; + + /** + * Gets an iterator that can be used to iterate trough the children of this + * UIDL. + * <p> + * The Object returned by <code>next()</code> will be appropriately typed - + * if it's UIDL, {@link #getTag()} can be used to check which section is in + * question. + * </p> + * <p> + * The basic use case is to iterate over the children of an UIDL update, and + * update the appropriate part of the widget for each child encountered, e.g + * if <code>getTag()</code> returns "color", one would update the widgets + * color to reflect the value of the "color" section. + * </p> + * + * @return an iterator for iterating over UIDL children + */ + public Iterator<Object> getChildIterator() { + + return new Iterator<Object>() { + + int index = -1; + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public Object next() { + + if (hasNext()) { + int typeOfChild = typeOfChild(++index); + switch (typeOfChild) { + case CHILD_TYPE_UIDL: + UIDL childUIDL = getChildUIDL(index); + return childUIDL; + case CHILD_TYPE_STRING: + return getChildString(index); + case CHILD_TYPE_XML: + return getChildXML(index); + default: + throw new IllegalStateException( + "Illegal child in tag " + getTag() + + " at index " + index); + } + } + return null; + } + + @Override + public boolean hasNext() { + int count = getChildCount(); + return count > index + 1; + } + + }; + } + + private static final int CHILD_TYPE_STRING = 0; + private static final int CHILD_TYPE_UIDL = 1; + private static final int CHILD_TYPE_XML = 2; + + private native int typeOfChild(int index) + /*-{ + var t = typeof this[index + 2]; + if(t == "object") { + if(typeof(t.length) == "number") { + return 1; + } else { + return 2; + } + } else if (t == "string") { + return 0; + } + return -1; + }-*/; + + /** + * @deprecated + */ + @Deprecated + public String getChildrenAsXML() { + return toString(); + } + + /** + * Checks if the named variable is available. + * + * @param name + * the name of the variable desired + * @return true if the variable exists, false otherwise + */ + public boolean hasVariable(String name) { + return hasVariables() && var().containsKey(name); + } + + /** + * Gets the value of the named variable. + * + * @param name + * the name of the variable + * @return the value of the variable + */ + public String getStringVariable(String name) { + return var().getString(name); + } + + /** + * Gets the value of the named variable. + * + * @param name + * the name of the variable + * @return the value of the variable + */ + public int getIntVariable(String name) { + return var().getInt(name); + } + + /** + * Gets the value of the named variable. + * + * @param name + * the name of the variable + * @return the value of the variable + */ + public long getLongVariable(String name) { + return (long) var().getRawNumber(name); + } + + /** + * Gets the value of the named variable. + * + * @param name + * the name of the variable + * @return the value of the variable + */ + public float getFloatVariable(String name) { + return (float) var().getRawNumber(name); + } + + /** + * Gets the value of the named variable. + * + * @param name + * the name of the variable + * @return the value of the variable + */ + public double getDoubleVariable(String name) { + return var().getRawNumber(name); + } + + /** + * Gets the value of the named variable. + * + * @param name + * the name of the variable + * @return the value of the variable + */ + public boolean getBooleanVariable(String name) { + return var().getBoolean(name); + } + + /** + * Gets the value of the named variable. + * + * @param name + * the name of the variable + * @return the value of the variable + */ + public String[] getStringArrayVariable(String name) { + return var().getStringArray(name); + } + + /** + * Gets the value of the named String[] variable as a Set of Strings. + * + * @param name + * the name of the variable + * @return the value of the variable + */ + public Set<String> getStringArrayVariableAsSet(final String name) { + final HashSet<String> s = new HashSet<String>(); + JsArrayString a = var().getJSStringArray(name); + for (int i = 0; i < a.length(); i++) { + s.add(a.get(i)); + } + return s; + } + + /** + * Gets the value of the named variable. + * + * @param name + * the name of the variable + * @return the value of the variable + */ + public int[] getIntArrayVariable(String name) { + return var().getIntArray(name); + } + + /** + * @deprecated should not be used anymore + */ + @Deprecated + public final static class XML extends JavaScriptObject { + protected XML() { + } + + public native String getXMLAsString() + /*-{ + var buf = new Array(); + var self = this; + for(j in self) { + buf.push("<"); + buf.push(j); + buf.push(">"); + buf.push(self[j]); + buf.push("</"); + buf.push(j); + buf.push(">"); + } + return buf.join(""); + }-*/; + } + + /** + * Returns the number of children. + * + * @return the number of children + */ + public native int getChildCount() + /*-{ + return this.length - 2; + }-*/; + + native boolean isMapAttribute(String name) + /*-{ + return typeof this[1][name] == "object"; + }-*/; + + /** + * Gets the Paintable with the id found in the named attributes's value. + * + * @param name + * the name of the attribute + * @return the Paintable referenced by the attribute, if it exists + */ + public ServerConnector getPaintableAttribute(String name, + ApplicationConnection connection) { + return ConnectorMap.get(connection).getConnector( + getStringAttribute(name)); + } + + /** + * Gets the Paintable with the id found in the named variable's value. + * + * @param name + * the name of the variable + * @return the Paintable referenced by the variable, if it exists + */ + public ServerConnector getPaintableVariable(String name, + ApplicationConnection connection) { + return ConnectorMap.get(connection).getConnector( + getStringVariable(name)); + } + + /** + * Returns the child UIDL by its name. If several child nodes exist with the + * given name, the first child UIDL will be returned. + * + * @param tagName + * @return the child UIDL or null if child wit given name was not found + */ + public UIDL getChildByTagName(String tagName) { + Iterator<Object> childIterator = getChildIterator(); + while (childIterator.hasNext()) { + Object next = childIterator.next(); + if (next instanceof UIDL) { + UIDL childUIDL = (UIDL) next; + if (childUIDL.getTag().equals(tagName)) { + return childUIDL; + } + } + } + return null; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/Util.java b/client/src/com/vaadin/terminal/gwt/client/Util.java new file mode 100644 index 0000000000..571258dbe3 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/Util.java @@ -0,0 +1,1194 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +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.Node; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Touch; +import com.google.gwt.user.client.Command; +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.EventListener; +import com.google.gwt.user.client.Window; +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.shared.ApplicationConstants; +import com.vaadin.shared.ComponentState; +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize; +import com.vaadin.terminal.gwt.client.ui.VOverlay; + +public class Util { + + /** + * Helper method for debugging purposes. + * + * Stops execution on firefox browsers on a breakpoint. + * + */ + public static native void browserDebugger() + /*-{ + if($wnd.console) + debugger; + }-*/; + + /** + * + * Returns the topmost element of from given coordinates. + * + * TODO fix crossplat issues clientX vs pageX. See quircksmode. Not critical + * for vaadin as we scroll div istead of page. + * + * @param x + * @param y + * @return the element at given coordinates + */ + public static native Element getElementFromPoint(int clientX, int clientY) + /*-{ + var el = $wnd.document.elementFromPoint(clientX, clientY); + if(el != null && el.nodeType == 3) { + el = el.parentNode; + } + return el; + }-*/; + + /** + * This helper method can be called if components size have been changed + * outside rendering phase. It notifies components parent about the size + * change so it can react. + * + * When using this method, developer should consider if size changes could + * be notified lazily. If lazy flag is true, method will save widget and + * wait for a moment until it notifies parents in chunks. This may vastly + * optimize layout in various situation. Example: if component have a lot of + * images their onload events may fire "layout phase" many times in a short + * period. + * + * @param widget + * @param lazy + * run componentSizeUpdated lazyly + * + * @deprecated since 7.0, use + * {@link LayoutManager#setNeedsMeasure(ComponentConnector)} + * instead + */ + @Deprecated + public static void notifyParentOfSizeChange(Widget widget, boolean lazy) { + ComponentConnector connector = findConnectorFor(widget); + if (connector != null) { + connector.getLayoutManager().setNeedsMeasure(connector); + if (!lazy) { + connector.getLayoutManager().layoutNow(); + } + } + } + + private static ComponentConnector findConnectorFor(Widget widget) { + List<ApplicationConnection> runningApplications = ApplicationConfiguration + .getRunningApplications(); + for (ApplicationConnection applicationConnection : runningApplications) { + ConnectorMap connectorMap = applicationConnection.getConnectorMap(); + ComponentConnector connector = connectorMap.getConnector(widget); + if (connector == null) { + continue; + } + if (connector.getConnection() == applicationConnection) { + return connector; + } + } + + return null; + } + + public static float parseRelativeSize(String size) { + if (size == null || !size.endsWith("%")) { + return -1; + } + + try { + return Float.parseFloat(size.substring(0, size.length() - 1)); + } catch (Exception e) { + VConsole.log("Unable to parse relative size"); + return -1; + } + } + + private static final Element escapeHtmlHelper = DOM.createDiv(); + + /** + * Converts html entities to text. + * + * @param html + * @return escaped string presentation of given html + */ + public static String escapeHTML(String html) { + DOM.setInnerText(escapeHtmlHelper, html); + String escapedText = DOM.getInnerHTML(escapeHtmlHelper); + if (BrowserInfo.get().isIE8()) { + // #7478 IE8 "incorrectly" returns "<br>" for newlines set using + // setInnerText. The same for " " which is converted to " " + escapedText = escapedText.replaceAll("<(BR|br)>", "\n"); + escapedText = escapedText.replaceAll(" ", " "); + } + return escapedText; + } + + /** + * Escapes the string so it is safe to write inside an HTML attribute. + * + * @param attribute + * The string to escape + * @return An escaped version of <literal>attribute</literal>. + */ + public static String escapeAttribute(String attribute) { + attribute = attribute.replace("\"", """); + attribute = attribute.replace("'", "'"); + attribute = attribute.replace(">", ">"); + attribute = attribute.replace("<", "<"); + attribute = attribute.replace("&", "&"); + return attribute; + } + + /** + * Clones given element as in JavaScript. + * + * Deprecate this if there appears similar method into GWT someday. + * + * @param element + * @param deep + * clone child tree also + * @return + */ + public static native Element cloneNode(Element element, boolean deep) + /*-{ + return element.cloneNode(deep); + }-*/; + + public static int measureHorizontalPaddingAndBorder(Element element, + int paddingGuess) { + String originalWidth = DOM.getStyleAttribute(element, "width"); + + int originalOffsetWidth = element.getOffsetWidth(); + int widthGuess = (originalOffsetWidth - paddingGuess); + if (widthGuess < 1) { + widthGuess = 1; + } + DOM.setStyleAttribute(element, "width", widthGuess + "px"); + int padding = element.getOffsetWidth() - widthGuess; + + DOM.setStyleAttribute(element, "width", originalWidth); + + return padding; + } + + public static int measureVerticalPaddingAndBorder(Element element, + int paddingGuess) { + String originalHeight = DOM.getStyleAttribute(element, "height"); + int originalOffsetHeight = element.getOffsetHeight(); + int widthGuess = (originalOffsetHeight - paddingGuess); + if (widthGuess < 1) { + widthGuess = 1; + } + DOM.setStyleAttribute(element, "height", widthGuess + "px"); + int padding = element.getOffsetHeight() - widthGuess; + + DOM.setStyleAttribute(element, "height", originalHeight); + return padding; + } + + public static int measureHorizontalBorder(Element element) { + int borders; + + if (BrowserInfo.get().isIE()) { + String width = element.getStyle().getProperty("width"); + String height = element.getStyle().getProperty("height"); + + int offsetWidth = element.getOffsetWidth(); + int offsetHeight = element.getOffsetHeight(); + if (offsetHeight < 1) { + offsetHeight = 1; + } + if (offsetWidth < 1) { + offsetWidth = 10; + } + element.getStyle().setPropertyPx("height", offsetHeight); + element.getStyle().setPropertyPx("width", offsetWidth); + + borders = element.getOffsetWidth() - element.getClientWidth(); + + element.getStyle().setProperty("width", width); + element.getStyle().setProperty("height", height); + } else { + borders = element.getOffsetWidth() + - element.getPropertyInt("clientWidth"); + } + assert borders >= 0; + + return borders; + } + + public static int measureVerticalBorder(Element element) { + int borders; + if (BrowserInfo.get().isIE()) { + String width = element.getStyle().getProperty("width"); + String height = element.getStyle().getProperty("height"); + + int offsetWidth = element.getOffsetWidth(); + int offsetHeight = element.getOffsetHeight(); + if (offsetHeight < 1) { + offsetHeight = 1; + } + if (offsetWidth < 1) { + offsetWidth = 10; + } + element.getStyle().setPropertyPx("width", offsetWidth); + + element.getStyle().setPropertyPx("height", offsetHeight); + + borders = element.getOffsetHeight() + - element.getPropertyInt("clientHeight"); + + element.getStyle().setProperty("height", height); + element.getStyle().setProperty("width", width); + } else { + borders = element.getOffsetHeight() + - element.getPropertyInt("clientHeight"); + } + assert borders >= 0; + + return borders; + } + + public static int measureMarginLeft(Element element) { + return element.getAbsoluteLeft() + - element.getParentElement().getAbsoluteLeft(); + } + + public static int setHeightExcludingPaddingAndBorder(Widget widget, + String height, int paddingBorderGuess) { + if (height.equals("")) { + setHeight(widget, ""); + return paddingBorderGuess; + } else if (height.endsWith("px")) { + int pixelHeight = Integer.parseInt(height.substring(0, + height.length() - 2)); + return setHeightExcludingPaddingAndBorder(widget.getElement(), + pixelHeight, paddingBorderGuess, false); + } else { + // Set the height in unknown units + setHeight(widget, height); + // Use the offsetWidth + return setHeightExcludingPaddingAndBorder(widget.getElement(), + widget.getOffsetHeight(), paddingBorderGuess, true); + } + } + + private static void setWidth(Widget widget, String width) { + DOM.setStyleAttribute(widget.getElement(), "width", width); + } + + private static void setHeight(Widget widget, String height) { + DOM.setStyleAttribute(widget.getElement(), "height", height); + } + + public static int setWidthExcludingPaddingAndBorder(Widget widget, + String width, int paddingBorderGuess) { + if (width.equals("")) { + setWidth(widget, ""); + return paddingBorderGuess; + } else if (width.endsWith("px")) { + int pixelWidth = Integer.parseInt(width.substring(0, + width.length() - 2)); + return setWidthExcludingPaddingAndBorder(widget.getElement(), + pixelWidth, paddingBorderGuess, false); + } else { + setWidth(widget, width); + return setWidthExcludingPaddingAndBorder(widget.getElement(), + widget.getOffsetWidth(), paddingBorderGuess, true); + } + } + + public static int setWidthExcludingPaddingAndBorder(Element element, + int requestedWidth, int horizontalPaddingBorderGuess, + boolean requestedWidthIncludesPaddingBorder) { + + int widthGuess = requestedWidth - horizontalPaddingBorderGuess; + if (widthGuess < 0) { + widthGuess = 0; + } + + DOM.setStyleAttribute(element, "width", widthGuess + "px"); + int captionOffsetWidth = DOM.getElementPropertyInt(element, + "offsetWidth"); + + int actualPadding = captionOffsetWidth - widthGuess; + + if (requestedWidthIncludesPaddingBorder) { + actualPadding += actualPadding; + } + + if (actualPadding != horizontalPaddingBorderGuess) { + int w = requestedWidth - actualPadding; + if (w < 0) { + // Cannot set negative width even if we would want to + w = 0; + } + DOM.setStyleAttribute(element, "width", w + "px"); + + } + + return actualPadding; + + } + + public static int setHeightExcludingPaddingAndBorder(Element element, + int requestedHeight, int verticalPaddingBorderGuess, + boolean requestedHeightIncludesPaddingBorder) { + + int heightGuess = requestedHeight - verticalPaddingBorderGuess; + if (heightGuess < 0) { + heightGuess = 0; + } + + DOM.setStyleAttribute(element, "height", heightGuess + "px"); + int captionOffsetHeight = DOM.getElementPropertyInt(element, + "offsetHeight"); + + int actualPadding = captionOffsetHeight - heightGuess; + + if (requestedHeightIncludesPaddingBorder) { + actualPadding += actualPadding; + } + + if (actualPadding != verticalPaddingBorderGuess) { + int h = requestedHeight - actualPadding; + if (h < 0) { + // Cannot set negative height even if we would want to + h = 0; + } + DOM.setStyleAttribute(element, "height", h + "px"); + + } + + return actualPadding; + + } + + public static String getSimpleName(Object widget) { + if (widget == null) { + return "(null)"; + } + + String name = widget.getClass().getName(); + return name.substring(name.lastIndexOf('.') + 1); + } + + public static void setFloat(Element element, String value) { + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(element, "styleFloat", value); + } else { + DOM.setStyleAttribute(element, "cssFloat", value); + } + } + + private static int detectedScrollbarSize = -1; + + public static int getNativeScrollbarSize() { + if (detectedScrollbarSize < 0) { + Element scroller = DOM.createDiv(); + scroller.getStyle().setProperty("width", "50px"); + scroller.getStyle().setProperty("height", "50px"); + scroller.getStyle().setProperty("overflow", "scroll"); + scroller.getStyle().setProperty("position", "absolute"); + scroller.getStyle().setProperty("marginLeft", "-5000px"); + RootPanel.getBodyElement().appendChild(scroller); + detectedScrollbarSize = scroller.getOffsetWidth() + - scroller.getPropertyInt("clientWidth"); + + RootPanel.getBodyElement().removeChild(scroller); + } + return detectedScrollbarSize; + } + + /** + * Run workaround for webkits overflow auto issue. + * + * See: our bug #2138 and https://bugs.webkit.org/show_bug.cgi?id=21462 + * + * @param elem + * with overflow auto + */ + public static void runWebkitOverflowAutoFix(final Element elem) { + // Add max version if fix lands sometime to Webkit + // Starting from Opera 11.00, also a problem in Opera + if (BrowserInfo.get().requiresOverflowAutoFix()) { + final String originalOverflow = elem.getStyle().getProperty( + "overflow"); + if ("hidden".equals(originalOverflow)) { + return; + } + + // check the scrolltop value before hiding the element + final int scrolltop = elem.getScrollTop(); + final int scrollleft = elem.getScrollLeft(); + elem.getStyle().setProperty("overflow", "hidden"); + + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + // Dough, Safari scroll auto means actually just a moped + elem.getStyle().setProperty("overflow", originalOverflow); + + if (scrolltop > 0 || elem.getScrollTop() > 0) { + int scrollvalue = scrolltop; + if (scrollvalue == 0) { + // mysterious are the ways of webkits scrollbar + // handling. In some cases webkit reports bad (0) + // scrolltop before hiding the element temporary, + // sometimes after. + scrollvalue = elem.getScrollTop(); + } + // fix another bug where scrollbar remains in wrong + // position + elem.setScrollTop(scrollvalue - 1); + elem.setScrollTop(scrollvalue); + } + + // fix for #6940 : Table horizontal scroll sometimes not + // updated when collapsing/expanding columns + // Also appeared in Safari 5.1 with webkit 534 (#7667) + if ((BrowserInfo.get().isChrome() || (BrowserInfo.get() + .isSafari() && BrowserInfo.get().getWebkitVersion() >= 534)) + && (scrollleft > 0 || elem.getScrollLeft() > 0)) { + int scrollvalue = scrollleft; + + if (scrollvalue == 0) { + // mysterious are the ways of webkits scrollbar + // handling. In some cases webkit may report a bad + // (0) scrollleft before hiding the element + // temporary, sometimes after. + scrollvalue = elem.getScrollLeft(); + } + // fix another bug where scrollbar remains in wrong + // position + elem.setScrollLeft(scrollvalue - 1); + elem.setScrollLeft(scrollvalue); + } + } + }); + } + + } + + /** + * Parses shared state and fetches the relative size of the component. If a + * dimension is not specified as relative it will return -1. If the shared + * state does not contain width or height specifications this will return + * null. + * + * @param state + * @return + */ + public static FloatSize parseRelativeSize(ComponentState state) { + if (state.isUndefinedHeight() && state.isUndefinedWidth()) { + return null; + } + + float relativeWidth = Util.parseRelativeSize(state.getWidth()); + float relativeHeight = Util.parseRelativeSize(state.getHeight()); + + FloatSize relativeSize = new FloatSize(relativeWidth, relativeHeight); + return relativeSize; + + } + + @Deprecated + public static boolean isCached(UIDL uidl) { + return uidl.getBooleanAttribute("cached"); + } + + public static void alert(String string) { + if (true) { + Window.alert(string); + } + } + + public static boolean equals(Object a, Object b) { + if (a == null) { + return b == null; + } + + return a.equals(b); + } + + public static void updateRelativeChildrenAndSendSizeUpdateEvent( + ApplicationConnection client, HasWidgets container, Widget widget) { + notifyParentOfSizeChange(widget, false); + } + + public static native int getRequiredWidth( + com.google.gwt.dom.client.Element element) + /*-{ + if (element.getBoundingClientRect) { + var rect = element.getBoundingClientRect(); + return Math.ceil(rect.right - rect.left); + } else { + return element.offsetWidth; + } + }-*/; + + public static native int getRequiredHeight( + com.google.gwt.dom.client.Element element) + /*-{ + var height; + if (element.getBoundingClientRect != null) { + var rect = element.getBoundingClientRect(); + height = Math.ceil(rect.bottom - rect.top); + } else { + height = element.offsetHeight; + } + return height; + }-*/; + + public static int getRequiredWidth(Widget widget) { + return getRequiredWidth(widget.getElement()); + } + + public static int getRequiredHeight(Widget widget) { + return getRequiredHeight(widget.getElement()); + } + + /** + * Detects what is currently the overflow style attribute in given element. + * + * @param pe + * the element to detect + * @return true if auto or scroll + */ + public static boolean mayHaveScrollBars(com.google.gwt.dom.client.Element pe) { + String overflow = getComputedStyle(pe, "overflow"); + if (overflow != null) { + if (overflow.equals("auto") || overflow.equals("scroll")) { + return true; + } else { + return false; + } + } else { + return false; + } + } + + /** + * A simple helper method to detect "computed style" (aka style sheets + + * element styles). Values returned differ a lot depending on browsers. + * Always be very careful when using this. + * + * @param el + * the element from which the style property is detected + * @param p + * the property to detect + * @return String value of style property + */ + private static native String getComputedStyle( + com.google.gwt.dom.client.Element el, String p) + /*-{ + try { + + if (el.currentStyle) { + // IE + return el.currentStyle[p]; + } else if (window.getComputedStyle) { + // Sa, FF, Opera + var view = el.ownerDocument.defaultView; + return view.getComputedStyle(el,null).getPropertyValue(p); + } else { + // fall back for non IE, Sa, FF, Opera + return ""; + } + } catch (e) { + return ""; + } + + }-*/; + + /** + * Locates the nested child component of <literal>parent</literal> which + * contains the element <literal>element</literal>. The child component is + * also returned if "element" is part of its caption. If + * <literal>element</literal> is not part of any child component, null is + * returned. + * + * This method returns the deepest nested VPaintableWidget. + * + * @param client + * A reference to ApplicationConnection + * @param parent + * The widget that contains <literal>element</literal>. + * @param element + * An element that is a sub element of the parent + * @return The VPaintableWidget which the element is a part of. Null if the + * element does not belong to a child. + */ + public static ComponentConnector getConnectorForElement( + ApplicationConnection client, Widget parent, Element element) { + + Element browseElement = element; + Element rootElement = parent.getElement(); + + while (browseElement != null && browseElement != rootElement) { + + ComponentConnector connector = ConnectorMap.get(client) + .getConnector(browseElement); + + if (connector == null) { + String ownerPid = VCaption.getCaptionOwnerPid(browseElement); + if (ownerPid != null) { + connector = (ComponentConnector) ConnectorMap.get(client) + .getConnector(ownerPid); + } + } + + if (connector != null) { + // check that inside the rootElement + while (browseElement != null && browseElement != rootElement) { + browseElement = (Element) browseElement.getParentElement(); + } + if (browseElement != rootElement) { + return null; + } else { + return connector; + } + } + + browseElement = (Element) browseElement.getParentElement(); + } + + // No connector found, element is possibly inside a VOverlay + // If the overlay has an owner, try to find the owner's connector + VOverlay overlay = findWidget(element, VOverlay.class); + if (overlay != null && overlay.getOwner() != null) { + return getConnectorForElement(client, RootPanel.get(), overlay + .getOwner().getElement()); + } else { + return null; + } + } + + /** + * Will (attempt) to focus the given DOM Element. + * + * @param el + * the element to focus + */ + public static native void focus(Element el) + /*-{ + try { + el.focus(); + } catch (e) { + + } + }-*/; + + /** + * Helper method to find the nearest parent paintable instance by traversing + * the DOM upwards from given element. + * + * @param element + * the element to start from + */ + public static ComponentConnector findPaintable( + ApplicationConnection client, Element element) { + Widget widget = Util.findWidget(element, null); + ConnectorMap vPaintableMap = ConnectorMap.get(client); + while (widget != null && !vPaintableMap.isConnector(widget)) { + widget = widget.getParent(); + } + return vPaintableMap.getConnector(widget); + + } + + /** + * Helper method to find first instance of given Widget type found by + * traversing DOM upwards from given element. + * + * @param element + * the element where to start seeking of Widget + * @param class1 + * the Widget type to seek for + */ + public static <T> T findWidget(Element element, + Class<? extends Widget> class1) { + if (element != null) { + /* First seek for the first EventListener (~Widget) from dom */ + EventListener eventListener = null; + while (eventListener == null && element != null) { + eventListener = Event.getEventListener(element); + if (eventListener == null) { + element = (Element) element.getParentElement(); + } + } + if (eventListener != null) { + /* + * Then find the first widget of type class1 from widget + * hierarchy + */ + Widget w = (Widget) eventListener; + while (w != null) { + if (class1 == null || w.getClass() == class1) { + return (T) w; + } + w = w.getParent(); + } + } + } + return null; + } + + /** + * Force webkit to redraw an element + * + * @param element + * The element that should be redrawn + */ + public static void forceWebkitRedraw(Element element) { + Style style = element.getStyle(); + String s = style.getProperty("webkitTransform"); + if (s == null || s.length() == 0) { + style.setProperty("webkitTransform", "scale(1)"); + } else { + style.setProperty("webkitTransform", ""); + } + } + + /** + * Detaches and re-attaches the element from its parent. The element is + * reattached at the same position in the DOM as it was before. + * + * Does nothing if the element is not attached to the DOM. + * + * @param element + * The element to detach and re-attach + */ + public static void detachAttach(Element element) { + if (element == null) { + return; + } + + Node nextSibling = element.getNextSibling(); + Node parent = element.getParentNode(); + if (parent == null) { + return; + } + + parent.removeChild(element); + if (nextSibling == null) { + parent.appendChild(element); + } else { + parent.insertBefore(element, nextSibling); + } + + } + + public static void sinkOnloadForImages(Element element) { + NodeList<com.google.gwt.dom.client.Element> imgElements = element + .getElementsByTagName("img"); + for (int i = 0; i < imgElements.getLength(); i++) { + DOM.sinkEvents((Element) imgElements.getItem(i), Event.ONLOAD); + } + + } + + /** + * Returns the index of the childElement within its parent. + * + * @param subElement + * @return + */ + public static int getChildElementIndex(Element childElement) { + int idx = 0; + Node n = childElement; + while ((n = n.getPreviousSibling()) != null) { + idx++; + } + + return idx; + } + + private static void printConnectorInvocations( + ArrayList<MethodInvocation> invocations, String id, + ApplicationConnection c) { + ServerConnector connector = ConnectorMap.get(c).getConnector(id); + if (connector != null) { + VConsole.log("\t" + id + " (" + connector.getClass() + ") :"); + } else { + VConsole.log("\t" + id + + ": Warning: no corresponding connector for id " + id); + } + for (MethodInvocation invocation : invocations) { + Object[] parameters = invocation.getParameters(); + String formattedParams = null; + if (ApplicationConstants.UPDATE_VARIABLE_METHOD.equals(invocation + .getMethodName()) && parameters.length == 2) { + // name, value + Object value = parameters[1]; + // TODO paintables inside lists/maps get rendered as + // components in the debug console + String formattedValue = value instanceof ServerConnector ? ((ServerConnector) value) + .getConnectorId() : String.valueOf(value); + formattedParams = parameters[0] + " : " + formattedValue; + } + if (null == formattedParams) { + formattedParams = (null != parameters) ? Arrays + .toString(parameters) : null; + } + VConsole.log("\t\t" + invocation.getInterfaceName() + "." + + invocation.getMethodName() + "(" + formattedParams + ")"); + } + } + + static void logVariableBurst(ApplicationConnection c, + ArrayList<MethodInvocation> loggedBurst) { + try { + VConsole.log("Variable burst to be sent to server:"); + String curId = null; + ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>(); + for (int i = 0; i < loggedBurst.size(); i++) { + String id = loggedBurst.get(i).getConnectorId(); + + if (curId == null) { + curId = id; + } else if (!curId.equals(id)) { + printConnectorInvocations(invocations, curId, c); + invocations.clear(); + curId = id; + } + invocations.add(loggedBurst.get(i)); + } + if (!invocations.isEmpty()) { + printConnectorInvocations(invocations, curId, c); + } + } catch (Exception e) { + VConsole.error(e); + } + } + + /** + * Temporarily sets the {@code styleProperty} to {@code tempValue} and then + * resets it to its current value. Used mainly to work around rendering + * issues in IE (and possibly in other browsers) + * + * @param element + * The target element + * @param styleProperty + * The name of the property to set + * @param tempValue + * The temporary value + */ + public static void setStyleTemporarily(Element element, + final String styleProperty, String tempValue) { + final Style style = element.getStyle(); + final String currentValue = style.getProperty(styleProperty); + + style.setProperty(styleProperty, tempValue); + element.getOffsetWidth(); + style.setProperty(styleProperty, currentValue); + + } + + /** + * A helper method to return the client position from an event. Returns + * position from either first changed touch (if touch event) or from the + * event itself. + * + * @param event + * @return + */ + public static int getTouchOrMouseClientX(Event event) { + if (isTouchEvent(event)) { + return event.getChangedTouches().get(0).getClientX(); + } else { + return event.getClientX(); + } + } + + /** + * Find the element corresponding to the coordinates in the passed mouse + * event. Please note that this is not always the same as the target of the + * event e.g. if event capture is used. + * + * @param event + * the mouse event to get coordinates from + * @return the element at the coordinates of the event + */ + public static Element getElementUnderMouse(NativeEvent event) { + int pageX = getTouchOrMouseClientX(event); + int pageY = getTouchOrMouseClientY(event); + + return getElementFromPoint(pageX, pageY); + } + + /** + * A helper method to return the client position from an event. Returns + * position from either first changed touch (if touch event) or from the + * event itself. + * + * @param event + * @return + */ + public static int getTouchOrMouseClientY(Event event) { + if (isTouchEvent(event)) { + return event.getChangedTouches().get(0).getClientY(); + } else { + return event.getClientY(); + } + } + + /** + * + * @see #getTouchOrMouseClientY(Event) + * @param currentGwtEvent + * @return + */ + public static int getTouchOrMouseClientY(NativeEvent currentGwtEvent) { + return getTouchOrMouseClientY(Event.as(currentGwtEvent)); + } + + /** + * @see #getTouchOrMouseClientX(Event) + * + * @param event + * @return + */ + public static int getTouchOrMouseClientX(NativeEvent event) { + return getTouchOrMouseClientX(Event.as(event)); + } + + public static boolean isTouchEvent(Event event) { + return event.getType().contains("touch"); + } + + public static boolean isTouchEvent(NativeEvent event) { + return isTouchEvent(Event.as(event)); + } + + public static void simulateClickFromTouchEvent(Event touchevent, + Widget widget) { + Touch touch = touchevent.getChangedTouches().get(0); + final NativeEvent createMouseUpEvent = Document.get() + .createMouseUpEvent(0, touch.getScreenX(), touch.getScreenY(), + touch.getClientX(), touch.getClientY(), false, false, + false, false, NativeEvent.BUTTON_LEFT); + final NativeEvent createMouseDownEvent = Document.get() + .createMouseDownEvent(0, touch.getScreenX(), + touch.getScreenY(), touch.getClientX(), + touch.getClientY(), false, false, false, false, + NativeEvent.BUTTON_LEFT); + final NativeEvent createMouseClickEvent = Document.get() + .createClickEvent(0, touch.getScreenX(), touch.getScreenY(), + touch.getClientX(), touch.getClientY(), false, false, + false, false); + + /* + * Get target with element from point as we want the actual element, not + * the one that sunk the event. + */ + final Element target = getElementFromPoint(touch.getClientX(), + touch.getClientY()); + + /* + * Fixes infocusable form fields in Safari of iOS 5.x and some Android + * browsers. + */ + Widget targetWidget = findWidget(target, null); + if (targetWidget instanceof com.google.gwt.user.client.ui.Focusable) { + final com.google.gwt.user.client.ui.Focusable toBeFocusedWidget = (com.google.gwt.user.client.ui.Focusable) targetWidget; + toBeFocusedWidget.setFocus(true); + } else if (targetWidget instanceof Focusable) { + ((Focusable) targetWidget).focus(); + } + + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + try { + target.dispatchEvent(createMouseDownEvent); + target.dispatchEvent(createMouseUpEvent); + target.dispatchEvent(createMouseClickEvent); + } catch (Exception e) { + } + + } + }); + + } + + /** + * Gets the currently focused element for Internet Explorer. + * + * @return The currently focused element + */ + public native static Element getIEFocusedElement() + /*-{ + if ($wnd.document.activeElement) { + return $wnd.document.activeElement; + } + + return null; + }-*/ + ; + + /** + * Kind of stronger version of isAttached(). In addition to std isAttached, + * this method checks that this widget nor any of its parents is hidden. Can + * be e.g used to check whether component should react to some events or + * not. + * + * @param widget + * @return true if attached and displayed + */ + public static boolean isAttachedAndDisplayed(Widget widget) { + if (widget.isAttached()) { + /* + * Failfast using offset size, then by iterating the widget tree + */ + boolean notZeroSized = widget.getOffsetHeight() > 0 + || widget.getOffsetWidth() > 0; + return notZeroSized || checkVisibilityRecursively(widget); + } else { + return false; + } + } + + private static boolean checkVisibilityRecursively(Widget widget) { + if (widget.isVisible()) { + Widget parent = widget.getParent(); + if (parent == null) { + return true; // root panel + } else { + return checkVisibilityRecursively(parent); + } + } else { + return false; + } + } + + /** + * Scrolls an element into view vertically only. Modified version of + * Element.scrollIntoView. + * + * @param elem + * The element to scroll into view + */ + public static native void scrollIntoViewVertically(Element elem) + /*-{ + var top = elem.offsetTop; + var height = elem.offsetHeight; + + if (elem.parentNode != elem.offsetParent) { + top -= elem.parentNode.offsetTop; + } + + var cur = elem.parentNode; + while (cur && (cur.nodeType == 1)) { + if (top < cur.scrollTop) { + cur.scrollTop = top; + } + if (top + height > cur.scrollTop + cur.clientHeight) { + cur.scrollTop = (top + height) - cur.clientHeight; + } + + var offsetTop = cur.offsetTop; + if (cur.parentNode != cur.offsetParent) { + offsetTop -= cur.parentNode.offsetTop; + } + + top += offsetTop - cur.scrollTop; + cur = cur.parentNode; + } + }-*/; + + /** + * Checks if the given event is either a touch event or caused by the left + * mouse button + * + * @param event + * @return true if the event is a touch event or caused by the left mouse + * button, false otherwise + */ + public static boolean isTouchEventOrLeftMouseButton(Event event) { + boolean touchEvent = Util.isTouchEvent(event); + return touchEvent || event.getButton() == Event.BUTTON_LEFT; + } + + /** + * Performs a shallow comparison of the collections. + * + * @param collection1 + * The first collection + * @param collection2 + * The second collection + * @return true if the collections contain the same elements in the same + * order, false otherwise + */ + public static boolean collectionsEquals(Collection collection1, + Collection collection2) { + if (collection1 == null) { + return collection2 == null; + } + if (collection2 == null) { + return false; + } + Iterator<Object> collection1Iterator = collection1.iterator(); + Iterator<Object> collection2Iterator = collection2.iterator(); + + while (collection1Iterator.hasNext()) { + if (!collection2Iterator.hasNext()) { + return false; + } + Object collection1Object = collection1Iterator.next(); + Object collection2Object = collection2Iterator.next(); + if (collection1Object != collection2Object) { + return false; + } + } + if (collection2Iterator.hasNext()) { + return false; + } + + return true; + } + + public static String getConnectorString(ServerConnector p) { + if (p == null) { + return "null"; + } + return getSimpleName(p) + " (" + p.getConnectorId() + ")"; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/VCaption.java b/client/src/com/vaadin/terminal/gwt/client/VCaption.java new file mode 100644 index 0000000000..d6da84eb8c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/VCaption.java @@ -0,0 +1,607 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +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.ui.HTML; +import com.vaadin.shared.AbstractFieldState; +import com.vaadin.shared.ComponentState; +import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector; +import com.vaadin.terminal.gwt.client.ui.Icon; + +public class VCaption extends HTML { + + public static final String CLASSNAME = "v-caption"; + + private final ComponentConnector owner; + + private Element errorIndicatorElement; + + private Element requiredFieldIndicator; + + private Icon icon; + + private Element captionText; + + private final ApplicationConnection client; + + private boolean placedAfterComponent = false; + + private int maxWidth = -1; + + private enum InsertPosition { + ICON, CAPTION, REQUIRED, ERROR + } + + private TooltipInfo tooltipInfo = null; + + /** + * Creates a caption that is not linked to a {@link ComponentConnector}. + * + * When using this constructor, {@link #getOwner()} returns null. + * + * @param client + * ApplicationConnection + * @deprecated all captions should be associated with a paintable widget and + * be updated from shared state, not UIDL + */ + @Deprecated + public VCaption(ApplicationConnection client) { + super(); + this.client = client; + owner = null; + + setStyleName(CLASSNAME); + sinkEvents(VTooltip.TOOLTIP_EVENTS); + + } + + /** + * Creates a caption for a {@link ComponentConnector}. + * + * @param component + * owner of caption, not null + * @param client + * ApplicationConnection + */ + public VCaption(ComponentConnector component, ApplicationConnection client) { + super(); + this.client = client; + owner = component; + + if (client != null && owner != null) { + setOwnerPid(getElement(), owner.getConnectorId()); + } + + setStyleName(CLASSNAME); + } + + /** + * Updates the caption from UIDL. + * + * This method may only be called when the caption has an owner - otherwise, + * use {@link #updateCaptionWithoutOwner(UIDL, String, boolean, boolean)}. + * + * @return true if the position where the caption should be placed has + * changed + */ + public boolean updateCaption() { + boolean wasPlacedAfterComponent = placedAfterComponent; + + // Caption is placed after component unless there is some part which + // moves it above. + placedAfterComponent = true; + + String style = CLASSNAME; + if (owner.getState().hasStyles()) { + for (String customStyle : owner.getState().getStyles()) { + style += " " + CLASSNAME + "-" + customStyle; + } + } + if (!owner.isEnabled()) { + style += " " + ApplicationConnection.DISABLED_CLASSNAME; + } + setStyleName(style); + + boolean hasIcon = owner.getState().getIcon() != null; + boolean showRequired = false; + boolean showError = owner.getState().getErrorMessage() != null; + if (owner.getState() instanceof AbstractFieldState) { + AbstractFieldState abstractFieldState = (AbstractFieldState) owner + .getState(); + showError = showError && !abstractFieldState.isHideErrors(); + } + if (owner instanceof AbstractFieldConnector) { + showRequired = ((AbstractFieldConnector) owner).isRequired(); + } + + if (hasIcon) { + if (icon == null) { + icon = new Icon(client); + icon.setWidth("0"); + icon.setHeight("0"); + + DOM.insertChild(getElement(), icon.getElement(), + getInsertPosition(InsertPosition.ICON)); + } + // Icon forces the caption to be above the component + placedAfterComponent = false; + + icon.setUri(owner.getState().getIcon().getURL()); + + } else if (icon != null) { + // Remove existing + DOM.removeChild(getElement(), icon.getElement()); + icon = null; + } + + if (owner.getState().getCaption() != null) { + // A caption text should be shown if the attribute is set + // If the caption is null the ATTRIBUTE_CAPTION should not be set to + // avoid ending up here. + + if (captionText == null) { + captionText = DOM.createDiv(); + captionText.setClassName("v-captiontext"); + + DOM.insertChild(getElement(), captionText, + getInsertPosition(InsertPosition.CAPTION)); + } + + // Update caption text + String c = owner.getState().getCaption(); + // A text forces the caption to be above the component. + placedAfterComponent = false; + if (c == null || c.trim().equals("")) { + // Not sure if c even can be null. Should not. + + // This is required to ensure that the caption uses space in all + // browsers when it is set to the empty string. If there is an + // icon, error indicator or required indicator they will ensure + // that space is reserved. + if (!hasIcon && !showRequired && !showError) { + captionText.setInnerHTML(" "); + } + } else { + DOM.setInnerText(captionText, c); + } + + } else if (captionText != null) { + // Remove existing + DOM.removeChild(getElement(), captionText); + captionText = null; + } + + if (owner.getState().hasDescription() && captionText != null) { + addStyleDependentName("hasdescription"); + } else { + removeStyleDependentName("hasdescription"); + } + + if (showRequired) { + if (requiredFieldIndicator == null) { + requiredFieldIndicator = DOM.createDiv(); + requiredFieldIndicator + .setClassName("v-required-field-indicator"); + DOM.setInnerText(requiredFieldIndicator, "*"); + + DOM.insertChild(getElement(), requiredFieldIndicator, + getInsertPosition(InsertPosition.REQUIRED)); + } + } else if (requiredFieldIndicator != null) { + // Remove existing + DOM.removeChild(getElement(), requiredFieldIndicator); + requiredFieldIndicator = null; + } + + if (showError) { + if (errorIndicatorElement == null) { + errorIndicatorElement = DOM.createDiv(); + DOM.setInnerHTML(errorIndicatorElement, " "); + DOM.setElementProperty(errorIndicatorElement, "className", + "v-errorindicator"); + + DOM.insertChild(getElement(), errorIndicatorElement, + getInsertPosition(InsertPosition.ERROR)); + } + } else if (errorIndicatorElement != null) { + // Remove existing + getElement().removeChild(errorIndicatorElement); + errorIndicatorElement = null; + } + + return (wasPlacedAfterComponent != placedAfterComponent); + } + + private int getInsertPosition(InsertPosition element) { + int pos = 0; + if (InsertPosition.ICON.equals(element)) { + return pos; + } + if (icon != null) { + pos++; + } + + if (InsertPosition.CAPTION.equals(element)) { + return pos; + } + + if (captionText != null) { + pos++; + } + + if (InsertPosition.REQUIRED.equals(element)) { + return pos; + } + if (requiredFieldIndicator != null) { + pos++; + } + + // if (InsertPosition.ERROR.equals(element)) { + // } + return pos; + + } + + @Deprecated + public boolean updateCaptionWithoutOwner(String caption, boolean disabled, + boolean hasDescription, boolean hasError, String iconURL) { + boolean wasPlacedAfterComponent = placedAfterComponent; + + // Caption is placed after component unless there is some part which + // moves it above. + placedAfterComponent = true; + + String style = VCaption.CLASSNAME; + if (disabled) { + style += " " + ApplicationConnection.DISABLED_CLASSNAME; + } + setStyleName(style); + if (hasDescription) { + if (captionText != null) { + addStyleDependentName("hasdescription"); + } else { + removeStyleDependentName("hasdescription"); + } + } + boolean hasIcon = iconURL != null; + + if (hasIcon) { + if (icon == null) { + icon = new Icon(client); + icon.setWidth("0"); + icon.setHeight("0"); + + DOM.insertChild(getElement(), icon.getElement(), + getInsertPosition(InsertPosition.ICON)); + } + // Icon forces the caption to be above the component + placedAfterComponent = false; + + icon.setUri(iconURL); + + } else if (icon != null) { + // Remove existing + DOM.removeChild(getElement(), icon.getElement()); + icon = null; + } + + if (caption != null) { + // A caption text should be shown if the attribute is set + // If the caption is null the ATTRIBUTE_CAPTION should not be set to + // avoid ending up here. + + if (captionText == null) { + captionText = DOM.createDiv(); + captionText.setClassName("v-captiontext"); + + DOM.insertChild(getElement(), captionText, + getInsertPosition(InsertPosition.CAPTION)); + } + + // Update caption text + // A text forces the caption to be above the component. + placedAfterComponent = false; + if (caption.trim().equals("")) { + // This is required to ensure that the caption uses space in all + // browsers when it is set to the empty string. If there is an + // icon, error indicator or required indicator they will ensure + // that space is reserved. + if (!hasIcon && !hasError) { + captionText.setInnerHTML(" "); + } + } else { + DOM.setInnerText(captionText, caption); + } + + } else if (captionText != null) { + // Remove existing + DOM.removeChild(getElement(), captionText); + captionText = null; + } + + if (hasError) { + if (errorIndicatorElement == null) { + errorIndicatorElement = DOM.createDiv(); + DOM.setInnerHTML(errorIndicatorElement, " "); + DOM.setElementProperty(errorIndicatorElement, "className", + "v-errorindicator"); + + DOM.insertChild(getElement(), errorIndicatorElement, + getInsertPosition(InsertPosition.ERROR)); + } + } else if (errorIndicatorElement != null) { + // Remove existing + getElement().removeChild(errorIndicatorElement); + errorIndicatorElement = null; + } + + return (wasPlacedAfterComponent != placedAfterComponent); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + final Element target = DOM.eventGetTarget(event); + + if (DOM.eventGetType(event) == Event.ONLOAD + && icon.getElement() == target) { + icon.setWidth(""); + icon.setHeight(""); + + // if max width defined, recalculate + if (maxWidth != -1) { + setMaxWidth(maxWidth); + } else { + String width = getElement().getStyle().getProperty("width"); + if (width != null && !width.equals("")) { + setWidth(getRequiredWidth() + "px"); + } + } + + /* + * The size of the icon might affect the size of the component so we + * must report the size change to the parent TODO consider moving + * the responsibility of reacting to ONLOAD from VCaption to layouts + */ + if (owner != null) { + Util.notifyParentOfSizeChange(owner.getWidget(), true); + } else { + VConsole.log("Warning: Icon load event was not propagated because VCaption owner is unknown."); + } + } + } + + public static boolean isNeeded(ComponentState state) { + if (state.getCaption() != null) { + return true; + } + if (state.getIcon() != null) { + return true; + } + if (state.getErrorMessage() != null) { + return true; + } + + return false; + } + + /** + * Returns Paintable for which this Caption belongs to. + * + * @return owner Widget + */ + public ComponentConnector getOwner() { + return owner; + } + + public boolean shouldBePlacedAfterComponent() { + return placedAfterComponent; + } + + public int getRenderedWidth() { + int width = 0; + + if (icon != null) { + width += Util.getRequiredWidth(icon.getElement()); + } + + if (captionText != null) { + width += Util.getRequiredWidth(captionText); + } + if (requiredFieldIndicator != null) { + width += Util.getRequiredWidth(requiredFieldIndicator); + } + if (errorIndicatorElement != null) { + width += Util.getRequiredWidth(errorIndicatorElement); + } + + return width; + + } + + public int getRequiredWidth() { + int width = 0; + + if (icon != null) { + width += Util.getRequiredWidth(icon.getElement()); + } + if (captionText != null) { + int textWidth = captionText.getScrollWidth(); + if (BrowserInfo.get().isFirefox()) { + /* + * In Firefox3 the caption might require more space than the + * scrollWidth returns as scrollWidth is rounded down. + */ + int requiredWidth = Util.getRequiredWidth(captionText); + if (requiredWidth > textWidth) { + textWidth = requiredWidth; + } + + } + width += textWidth; + } + if (requiredFieldIndicator != null) { + width += Util.getRequiredWidth(requiredFieldIndicator); + } + if (errorIndicatorElement != null) { + width += Util.getRequiredWidth(errorIndicatorElement); + } + + return width; + + } + + public int getHeight() { + int height = 0; + int h; + + if (icon != null) { + h = Util.getRequiredHeight(icon.getElement()); + if (h > height) { + height = h; + } + } + + if (captionText != null) { + h = Util.getRequiredHeight(captionText); + if (h > height) { + height = h; + } + } + if (requiredFieldIndicator != null) { + h = Util.getRequiredHeight(requiredFieldIndicator); + if (h > height) { + height = h; + } + } + if (errorIndicatorElement != null) { + h = Util.getRequiredHeight(errorIndicatorElement); + if (h > height) { + height = h; + } + } + + return height; + } + + public void setAlignment(String alignment) { + DOM.setStyleAttribute(getElement(), "textAlign", alignment); + } + + public void setMaxWidth(int maxWidth) { + this.maxWidth = maxWidth; + DOM.setStyleAttribute(getElement(), "width", maxWidth + "px"); + + if (icon != null) { + DOM.setStyleAttribute(icon.getElement(), "width", ""); + } + + if (captionText != null) { + DOM.setStyleAttribute(captionText, "width", ""); + } + + int requiredWidth = getRequiredWidth(); + /* + * ApplicationConnection.getConsole().log( "Caption maxWidth: " + + * maxWidth + ", requiredWidth: " + requiredWidth); + */ + if (requiredWidth > maxWidth) { + // Needs to truncate and clip + int availableWidth = maxWidth; + + // DOM.setStyleAttribute(getElement(), "width", maxWidth + "px"); + if (requiredFieldIndicator != null) { + availableWidth -= Util.getRequiredWidth(requiredFieldIndicator); + } + + if (errorIndicatorElement != null) { + availableWidth -= Util.getRequiredWidth(errorIndicatorElement); + } + + if (availableWidth < 0) { + availableWidth = 0; + } + + if (icon != null) { + int iconRequiredWidth = Util + .getRequiredWidth(icon.getElement()); + if (availableWidth > iconRequiredWidth) { + availableWidth -= iconRequiredWidth; + } else { + DOM.setStyleAttribute(icon.getElement(), "width", + availableWidth + "px"); + availableWidth = 0; + } + } + if (captionText != null) { + int captionWidth = Util.getRequiredWidth(captionText); + if (availableWidth > captionWidth) { + availableWidth -= captionWidth; + + } else { + DOM.setStyleAttribute(captionText, "width", availableWidth + + "px"); + availableWidth = 0; + } + + } + + } + } + + /** + * Sets the tooltip that should be shown for the caption + * + * @param tooltipInfo + * The tooltip that should be shown or null if no tooltip should + * be shown + */ + public void setTooltipInfo(TooltipInfo tooltipInfo) { + this.tooltipInfo = tooltipInfo; + } + + /** + * Returns the tooltip that should be shown for the caption + * + * @return The tooltip to show or null if no tooltip should be shown + */ + public TooltipInfo getTooltipInfo() { + return tooltipInfo; + } + + protected Element getTextElement() { + return captionText; + } + + public static String getCaptionOwnerPid(Element e) { + return getOwnerPid(e); + } + + private native static void setOwnerPid(Element el, String pid) + /*-{ + el.vOwnerPid = pid; + }-*/; + + public native static String getOwnerPid(Element el) + /*-{ + return el.vOwnerPid; + }-*/; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/VCaptionWrapper.java b/client/src/com/vaadin/terminal/gwt/client/VCaptionWrapper.java new file mode 100644 index 0000000000..f3e1802689 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/VCaptionWrapper.java @@ -0,0 +1,51 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.user.client.ui.FlowPanel; + +public class VCaptionWrapper extends FlowPanel { + + public static final String CLASSNAME = "v-captionwrapper"; + VCaption caption; + ComponentConnector wrappedConnector; + + /** + * Creates a new caption wrapper panel. + * + * @param toBeWrapped + * paintable that the caption is associated with, not null + * @param client + * ApplicationConnection + */ + public VCaptionWrapper(ComponentConnector toBeWrapped, + ApplicationConnection client) { + caption = new VCaption(toBeWrapped, client); + add(caption); + wrappedConnector = toBeWrapped; + add(wrappedConnector.getWidget()); + setStyleName(CLASSNAME); + } + + public void updateCaption() { + caption.updateCaption(); + } + + public ComponentConnector getWrappedConnector() { + return wrappedConnector; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/VConsole.java b/client/src/com/vaadin/terminal/gwt/client/VConsole.java new file mode 100644 index 0000000000..73427c5beb --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/VConsole.java @@ -0,0 +1,117 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.Set; + +import com.google.gwt.core.client.GWT; + +/** + * A helper class to do some client side logging. + * <p> + * This class replaces previously used loggin style: + * ApplicationConnection.getConsole().log("foo"). + * <p> + * The default widgetset provides three modes for debugging: + * <ul> + * <li>NullConsole (Default, displays no errors at all) + * <li>VDebugConsole ( Enabled by appending ?debug to url. Displays a floating + * console in the browser and also prints to browsers internal console (builtin + * or Firebug) and GWT's development mode console if available.) + * <li>VDebugConsole in quiet mode (Enabled by appending ?debug=quiet. Same as + * previous but without the console floating over application). + * </ul> + * <p> + * Implementations can be customized with GWT deferred binding by overriding + * NullConsole.class or VDebugConsole.class. This way developer can for example + * build mechanism to send client side logging data to a server. + * <p> + * Note that logging in client side is not fully optimized away even in + * production mode. Use logging moderately in production code to keep the size + * of client side engine small. An exception is {@link GWT#log(String)} style + * logging, which is available only in GWT development mode, but optimized away + * when compiled to web mode. + * + * + * TODO improve javadocs of individual methods + * + */ +public class VConsole { + private static Console impl; + + /** + * Used by ApplicationConfiguration to initialize VConsole. + * + * @param console + */ + static void setImplementation(Console console) { + impl = console; + } + + /** + * Used by ApplicationConnection to support deprecated getConsole() api. + */ + static Console getImplementation() { + return impl; + } + + public static void log(String msg) { + if (impl != null) { + impl.log(msg); + } + } + + public static void log(Throwable e) { + if (impl != null) { + impl.log(e); + } + } + + public static void error(Throwable e) { + if (impl != null) { + impl.error(e); + } + } + + public static void error(String msg) { + if (impl != null) { + impl.error(msg); + } + } + + public static void printObject(Object msg) { + if (impl != null) { + impl.printObject(msg); + } + } + + public static void dirUIDL(ValueMap u, ApplicationConnection client) { + if (impl != null) { + impl.dirUIDL(u, client); + } + } + + public static void printLayoutProblems(ValueMap meta, + ApplicationConnection applicationConnection, + Set<ComponentConnector> zeroHeightComponents, + Set<ComponentConnector> zeroWidthComponents) { + if (impl != null) { + impl.printLayoutProblems(meta, applicationConnection, + zeroHeightComponents, zeroWidthComponents); + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/VDebugConsole.java b/client/src/com/vaadin/terminal/gwt/client/VDebugConsole.java new file mode 100644 index 0000000000..1e2a3062f1 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/VDebugConsole.java @@ -0,0 +1,1015 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.JsArray; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.FontWeight; +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +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.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.event.shared.UmbrellaException; +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.http.client.UrlBuilder; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.storage.client.Storage; +import com.google.gwt.user.client.Cookies; +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.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; +import com.google.gwt.user.client.EventPreview; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.Window.Location; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.Version; +import com.vaadin.terminal.gwt.client.ui.VLazyExecutor; +import com.vaadin.terminal.gwt.client.ui.VOverlay; +import com.vaadin.terminal.gwt.client.ui.notification.VNotification; +import com.vaadin.terminal.gwt.client.ui.root.RootConnector; +import com.vaadin.terminal.gwt.client.ui.window.WindowConnector; + +/** + * A helper console for client side development. The debug console can also be + * used to resolve layout issues, inspect the communication between browser and + * the server, start GWT dev mode and restart application. + * + * <p> + * This implementation is used vaadin is in debug mode (see manual) and + * developer appends "?debug" query parameter to url. Debug information can also + * be shown on browsers internal console only, by appending "?debug=quiet" query + * parameter. + * <p> + * This implementation can be overridden with GWT deferred binding. + * + */ +public class VDebugConsole extends VOverlay implements Console { + + private final class HighlightModeHandler implements NativePreviewHandler { + private final Label label; + + private HighlightModeHandler(Label label) { + this.label = label; + } + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + if (event.getTypeInt() == Event.ONKEYDOWN + && event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) { + highlightModeRegistration.removeHandler(); + VUIDLBrowser.deHiglight(); + return; + } + if (event.getTypeInt() == Event.ONMOUSEMOVE) { + VUIDLBrowser.deHiglight(); + Element eventTarget = Util.getElementFromPoint(event + .getNativeEvent().getClientX(), event.getNativeEvent() + .getClientY()); + if (getElement().isOrHasChild(eventTarget)) { + return; + } + + for (ApplicationConnection a : ApplicationConfiguration + .getRunningApplications()) { + ComponentConnector connector = Util.getConnectorForElement( + a, a.getRootConnector().getWidget(), eventTarget); + if (connector == null) { + connector = Util.getConnectorForElement(a, + RootPanel.get(), eventTarget); + } + if (connector != null) { + String pid = connector.getConnectorId(); + VUIDLBrowser.highlight(connector); + label.setText("Currently focused :" + + connector.getClass() + " ID:" + pid); + event.cancel(); + event.consume(); + event.getNativeEvent().stopPropagation(); + return; + } + } + } + if (event.getTypeInt() == Event.ONCLICK) { + VUIDLBrowser.deHiglight(); + event.cancel(); + event.consume(); + event.getNativeEvent().stopPropagation(); + highlightModeRegistration.removeHandler(); + Element eventTarget = Util.getElementFromPoint(event + .getNativeEvent().getClientX(), event.getNativeEvent() + .getClientY()); + for (ApplicationConnection a : ApplicationConfiguration + .getRunningApplications()) { + ComponentConnector paintable = Util.getConnectorForElement( + a, a.getRootConnector().getWidget(), eventTarget); + if (paintable == null) { + paintable = Util.getConnectorForElement(a, + RootPanel.get(), eventTarget); + } + + if (paintable != null) { + a.highlightComponent(paintable); + return; + } + } + } + event.cancel(); + } + } + + private static final String POS_COOKIE_NAME = "VDebugConsolePos"; + + private HandlerRegistration highlightModeRegistration; + + Element caption = DOM.createDiv(); + + private Panel panel; + + private Button clear = new Button("C"); + private Button restart = new Button("R"); + private Button forceLayout = new Button("FL"); + private Button analyzeLayout = new Button("AL"); + private Button savePosition = new Button("S"); + private Button highlight = new Button("H"); + private Button connectorStats = new Button("CS"); + private CheckBox devMode = new CheckBox("Dev"); + private CheckBox superDevMode = new CheckBox("SDev"); + private CheckBox autoScroll = new CheckBox("Autoscroll "); + private HorizontalPanel actions; + private boolean collapsed = false; + + private boolean resizing; + private int startX; + private int startY; + private int initialW; + private int initialH; + + private boolean moving = false; + + private int origTop; + + private int origLeft; + + private static final String help = "Drag title=move, shift-drag=resize, doubleclick title=min/max." + + "Use debug=quiet to log only to browser console."; + + private static final int DEFAULT_WIDTH = 650; + private static final int DEFAULT_HEIGHT = 400; + + public VDebugConsole() { + super(false, false); + getElement().getStyle().setOverflow(Overflow.HIDDEN); + clear.setTitle("Clear console"); + restart.setTitle("Restart app"); + forceLayout.setTitle("Force layout"); + analyzeLayout.setTitle("Analyze layouts"); + savePosition.setTitle("Save pos"); + } + + private EventPreview dragpreview = new EventPreview() { + + @Override + public boolean onEventPreview(Event event) { + onBrowserEvent(event); + return false; + } + }; + + private boolean quietMode; + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + switch (DOM.eventGetType(event)) { + case Event.ONMOUSEDOWN: + if (DOM.eventGetShiftKey(event)) { + resizing = true; + DOM.setCapture(getElement()); + startX = DOM.eventGetScreenX(event); + startY = DOM.eventGetScreenY(event); + initialW = VDebugConsole.this.getOffsetWidth(); + initialH = VDebugConsole.this.getOffsetHeight(); + DOM.eventCancelBubble(event, true); + DOM.eventPreventDefault(event); + DOM.addEventPreview(dragpreview); + } else if (DOM.eventGetTarget(event) == caption) { + moving = true; + startX = DOM.eventGetScreenX(event); + startY = DOM.eventGetScreenY(event); + origTop = getAbsoluteTop(); + origLeft = getAbsoluteLeft(); + DOM.eventCancelBubble(event, true); + DOM.eventPreventDefault(event); + DOM.addEventPreview(dragpreview); + } + + break; + case Event.ONMOUSEMOVE: + if (resizing) { + int deltaX = startX - DOM.eventGetScreenX(event); + int detalY = startY - DOM.eventGetScreenY(event); + int w = initialW - deltaX; + if (w < 30) { + w = 30; + } + int h = initialH - detalY; + if (h < 40) { + h = 40; + } + VDebugConsole.this.setPixelSize(w, h); + DOM.eventCancelBubble(event, true); + DOM.eventPreventDefault(event); + } else if (moving) { + int deltaX = startX - DOM.eventGetScreenX(event); + int detalY = startY - DOM.eventGetScreenY(event); + int left = origLeft - deltaX; + if (left < 0) { + left = 0; + } + int top = origTop - detalY; + if (top < 0) { + top = 0; + } + VDebugConsole.this.setPopupPosition(left, top); + DOM.eventCancelBubble(event, true); + DOM.eventPreventDefault(event); + } + break; + case Event.ONLOSECAPTURE: + case Event.ONMOUSEUP: + if (resizing) { + DOM.releaseCapture(getElement()); + resizing = false; + } else if (moving) { + DOM.releaseCapture(getElement()); + moving = false; + } + DOM.removeEventPreview(dragpreview); + break; + case Event.ONDBLCLICK: + if (DOM.eventGetTarget(event) == caption) { + if (collapsed) { + panel.setVisible(true); + setToDefaultSizeAndPos(); + } else { + panel.setVisible(false); + setPixelSize(120, 20); + setPopupPosition(Window.getClientWidth() - 125, + Window.getClientHeight() - 25); + } + collapsed = !collapsed; + } + break; + default: + break; + } + + } + + private void setToDefaultSizeAndPos() { + String cookie = Cookies.getCookie(POS_COOKIE_NAME); + int width, height, top, left; + boolean autoScrollValue = false; + if (cookie != null) { + String[] split = cookie.split(","); + left = Integer.parseInt(split[0]); + top = Integer.parseInt(split[1]); + width = Integer.parseInt(split[2]); + height = Integer.parseInt(split[3]); + autoScrollValue = Boolean.valueOf(split[4]); + } else { + int windowHeight = Window.getClientHeight(); + int windowWidth = Window.getClientWidth(); + width = DEFAULT_WIDTH; + height = DEFAULT_HEIGHT; + + if (height > windowHeight / 2) { + height = windowHeight / 2; + } + if (width > windowWidth / 2) { + width = windowWidth / 2; + } + + top = windowHeight - (height + 10); + left = windowWidth - (width + 10); + } + setPixelSize(width, height); + setPopupPosition(left, top); + autoScroll.setValue(autoScrollValue); + } + + @Override + public void setPixelSize(int width, int height) { + if (height < 20) { + height = 20; + } + if (width < 2) { + width = 2; + } + panel.setHeight((height - 20) + "px"); + panel.setWidth((width - 2) + "px"); + getElement().getStyle().setWidth(width, Unit.PX); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Console#log(java.lang.String) + */ + @Override + public void log(String msg) { + if (msg == null) { + msg = "null"; + } + msg = addTimestamp(msg); + // remoteLog(msg); + + logToDebugWindow(msg, false); + GWT.log(msg); + consoleLog(msg); + System.out.println(msg); + } + + private List<String> msgQueue = new LinkedList<String>(); + + private ScheduledCommand doSend = new ScheduledCommand() { + @Override + public void execute() { + if (!msgQueue.isEmpty()) { + RequestBuilder requestBuilder = new RequestBuilder( + RequestBuilder.POST, getRemoteLogUrl()); + try { + String requestData = ""; + for (String str : msgQueue) { + requestData += str; + requestData += "\n"; + } + requestBuilder.sendRequest(requestData, + new RequestCallback() { + + @Override + public void onResponseReceived(Request request, + Response response) { + // TODO Auto-generated method stub + + } + + @Override + public void onError(Request request, + Throwable exception) { + // TODO Auto-generated method stub + + } + }); + } catch (RequestException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + msgQueue.clear(); + } + } + + }; + private VLazyExecutor sendToRemoteLog = new VLazyExecutor(350, doSend); + + protected String getRemoteLogUrl() { + return "http://sun-vehje.local:8080/remotelog/"; + } + + protected void remoteLog(String msg) { + msgQueue.add(msg); + sendToRemoteLog.trigger(); + } + + /** + * Logs the given message to the debug window. + * + * @param msg + * The message to log. Must not be null. + */ + private void logToDebugWindow(String msg, boolean error) { + Widget row; + if (error) { + row = createErrorHtml(msg); + } else { + row = new HTML(msg); + } + panel.add(row); + if (autoScroll.getValue()) { + row.getElement().scrollIntoView(); + } + } + + private HTML createErrorHtml(String msg) { + HTML html = new HTML(msg); + html.getElement().getStyle().setColor("#f00"); + html.getElement().getStyle().setFontWeight(FontWeight.BOLD); + return html; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Console#error(java.lang.String) + */ + @Override + public void error(String msg) { + if (msg == null) { + msg = "null"; + } + msg = addTimestamp(msg); + logToDebugWindow(msg, true); + + GWT.log(msg); + consoleErr(msg); + System.out.println(msg); + + } + + DateTimeFormat timestampFormat = DateTimeFormat.getFormat("HH:mm:ss:SSS"); + + @SuppressWarnings("deprecation") + private String addTimestamp(String msg) { + Date date = new Date(); + String timestamp = timestampFormat.format(date); + return timestamp + " " + msg; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Console#printObject(java.lang. + * Object) + */ + @Override + public void printObject(Object msg) { + String str; + if (msg == null) { + str = "null"; + } else { + str = msg.toString(); + } + panel.add((new Label(str))); + consoleLog(str); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Console#dirUIDL(com.vaadin + * .terminal.gwt.client.UIDL) + */ + @Override + public void dirUIDL(ValueMap u, ApplicationConnection client) { + if (panel.isAttached()) { + VUIDLBrowser vuidlBrowser = new VUIDLBrowser(u, client); + vuidlBrowser.setText("Response:"); + panel.add(vuidlBrowser); + } + consoleDir(u); + // consoleLog(u.getChildrenAsXML()); + } + + private static native void consoleDir(ValueMap u) + /*-{ + if($wnd.console && $wnd.console.log) { + if($wnd.console.dir) { + $wnd.console.dir(u); + } else { + $wnd.console.log(u); + } + } + + }-*/; + + private static native void consoleLog(String msg) + /*-{ + if($wnd.console && $wnd.console.log) { + $wnd.console.log(msg); + } + }-*/; + + private static native void consoleErr(String msg) + /*-{ + if($wnd.console) { + if ($wnd.console.error) + $wnd.console.error(msg); + else if ($wnd.console.log) + $wnd.console.log(msg); + } + }-*/; + + @Override + public void printLayoutProblems(ValueMap meta, ApplicationConnection ac, + Set<ComponentConnector> zeroHeightComponents, + Set<ComponentConnector> zeroWidthComponents) { + JsArray<ValueMap> valueMapArray = meta + .getJSValueMapArray("invalidLayouts"); + int size = valueMapArray.length(); + panel.add(new HTML("<div>************************</di>" + + "<h4>Layouts analyzed on server, total top level problems: " + + size + " </h4>")); + if (size > 0) { + SimpleTree root = new SimpleTree("Root problems"); + + for (int i = 0; i < size; i++) { + printLayoutError(valueMapArray.get(i), root, ac); + } + panel.add(root); + + } + if (zeroHeightComponents.size() > 0 || zeroWidthComponents.size() > 0) { + panel.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) { + panel.add(new HTML( + "<p><strong>Vertically zero size:</strong><p>")); + printClientSideDetectedIssues(zeroHeightComponents, ac); + } + if (zeroWidthComponents.size() > 0) { + panel.add(new HTML( + "<p><strong>Horizontally zero size:</strong><p>")); + printClientSideDetectedIssues(zeroWidthComponents, ac); + } + } + log("************************"); + } + + private void printClientSideDetectedIssues( + Set<ComponentConnector> zeroHeightComponents, + ApplicationConnection ac) { + for (final ComponentConnector paintable : zeroHeightComponents) { + final ServerConnector parent = paintable.getParent(); + + VerticalPanel errorDetails = new VerticalPanel(); + errorDetails.add(new Label("" + Util.getSimpleName(paintable) + + " inside " + Util.getSimpleName(parent))); + if (parent instanceof ComponentConnector) { + ComponentConnector parentComponent = (ComponentConnector) parent; + final Widget layout = parentComponent.getWidget(); + + final CheckBox emphasisInUi = new CheckBox( + "Emphasize components parent in UI (the actual component is not visible)"); + emphasisInUi.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + Element element2 = layout.getElement(); + Widget.setStyleName(element2, "invalidlayout", + emphasisInUi.getValue().booleanValue()); + } + }); + + errorDetails.add(emphasisInUi); + } + panel.add(errorDetails); + } + } + + private void printLayoutError(ValueMap valueMap, SimpleTree root, + final ApplicationConnection ac) { + final String pid = valueMap.getString("id"); + final ComponentConnector paintable = (ComponentConnector) ConnectorMap + .get(ac).getConnector(pid); + + SimpleTree errorNode = new SimpleTree(); + VerticalPanel errorDetails = new VerticalPanel(); + errorDetails.add(new Label(Util.getSimpleName(paintable) + " id: " + + pid)); + 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"))); + } + final CheckBox emphasisInUi = new CheckBox("Emphasize component in UI"); + emphasisInUi.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (paintable != null) { + Element element2 = paintable.getWidget().getElement(); + Widget.setStyleName(element2, "invalidlayout", + emphasisInUi.getValue()); + } + } + }); + errorDetails.add(emphasisInUi); + 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(value, errorNode, ac); + } + + } + root.add(errorNode); + } + + @Override + public void log(Throwable e) { + if (e instanceof UmbrellaException) { + UmbrellaException ue = (UmbrellaException) e; + for (Throwable t : ue.getCauses()) { + log(t); + } + return; + } + log(Util.getSimpleName(e) + ": " + e.getMessage()); + GWT.log(e.getMessage(), e); + } + + @Override + public void error(Throwable e) { + handleError(e, this); + } + + static void handleError(Throwable e, Console target) { + if (e instanceof UmbrellaException) { + UmbrellaException ue = (UmbrellaException) e; + for (Throwable t : ue.getCauses()) { + target.error(t); + } + return; + } + String exceptionText = Util.getSimpleName(e); + String message = e.getMessage(); + if (message != null && message.length() != 0) { + exceptionText += ": " + e.getMessage(); + } + target.error(exceptionText); + GWT.log(e.getMessage(), e); + if (!GWT.isProdMode()) { + e.printStackTrace(); + } + try { + VNotification.createNotification(VNotification.DELAY_FOREVER).show( + "<h1>Uncaught client side exception</h1><br />" + + exceptionText, VNotification.CENTERED, "error"); + } catch (Exception e2) { + // Just swallow this exception + } + } + + @Override + public void init() { + panel = new FlowPanel(); + if (!quietMode) { + DOM.appendChild(getContainerElement(), caption); + setWidget(panel); + caption.setClassName("v-debug-console-caption"); + setStyleName("v-debug-console"); + getElement().getStyle().setZIndex(20000); + getElement().getStyle().setOverflow(Overflow.HIDDEN); + + sinkEvents(Event.ONDBLCLICK); + + sinkEvents(Event.MOUSEEVENTS); + + panel.setStyleName("v-debug-console-content"); + + caption.setInnerHTML("Debug window"); + caption.getStyle().setHeight(25, Unit.PX); + caption.setTitle(help); + + show(); + setToDefaultSizeAndPos(); + + actions = new HorizontalPanel(); + Style style = actions.getElement().getStyle(); + style.setPosition(Position.ABSOLUTE); + style.setBackgroundColor("#666"); + style.setLeft(135, Unit.PX); + style.setHeight(25, Unit.PX); + style.setTop(0, Unit.PX); + + actions.add(clear); + actions.add(restart); + actions.add(forceLayout); + actions.add(analyzeLayout); + actions.add(highlight); + actions.add(connectorStats); + connectorStats.setTitle("Show connector statistics for client"); + highlight + .setTitle("Select a component and print details about it to the server log and client side console."); + actions.add(savePosition); + savePosition + .setTitle("Saves the position and size of debug console to a cookie"); + actions.add(autoScroll); + addDevMode(); + addSuperDevMode(); + + autoScroll + .setTitle("Automatically scroll so that new messages are visible"); + + panel.add(actions); + + panel.add(new HTML("<i>" + help + "</i>")); + + clear.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + int width = panel.getOffsetWidth(); + int height = panel.getOffsetHeight(); + panel = new FlowPanel(); + panel.setPixelSize(width, height); + panel.setStyleName("v-debug-console-content"); + panel.add(actions); + setWidget(panel); + } + }); + + restart.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + + String queryString = Window.Location.getQueryString(); + if (queryString != null + && queryString.contains("restartApplications")) { + Window.Location.reload(); + } else { + String url = Location.getHref(); + String separator = "?"; + if (url.contains("?")) { + separator = "&"; + } + if (!url.contains("restartApplication")) { + url += separator; + url += "restartApplication"; + } + if (!"".equals(Location.getHash())) { + String hash = Location.getHash(); + url = url.replace(hash, "") + hash; + } + Window.Location.replace(url); + } + + } + }); + + forceLayout.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + for (ApplicationConnection applicationConnection : ApplicationConfiguration + .getRunningApplications()) { + applicationConnection.forceLayout(); + } + } + }); + + analyzeLayout.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + List<ApplicationConnection> runningApplications = ApplicationConfiguration + .getRunningApplications(); + for (ApplicationConnection applicationConnection : runningApplications) { + applicationConnection.analyzeLayouts(); + } + } + }); + analyzeLayout + .setTitle("Analyzes currently rendered view and " + + "reports possible common problems in usage of relative sizes." + + "Will cause server visit/rendering of whole screen and loss of" + + " all non committed variables form client side."); + + savePosition.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + String pos = getAbsoluteLeft() + "," + getAbsoluteTop() + + "," + getOffsetWidth() + "," + getOffsetHeight() + + "," + autoScroll.getValue(); + Cookies.setCookie(POS_COOKIE_NAME, pos); + } + }); + + highlight.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + final Label label = new Label("--"); + log("<i>Use mouse to select a component or click ESC to exit highlight mode.</i>"); + panel.add(label); + highlightModeRegistration = Event + .addNativePreviewHandler(new HighlightModeHandler( + label)); + + } + }); + + } + connectorStats.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + for (ApplicationConnection a : ApplicationConfiguration + .getRunningApplications()) { + dumpConnectorInfo(a); + } + } + }); + log("Starting Vaadin client side engine. Widgetset: " + + GWT.getModuleName()); + + log("Widget set is built on version: " + Version.getFullVersion()); + + logToDebugWindow("<div class=\"v-theme-version v-theme-version-" + + Version.getFullVersion().replaceAll("\\.", "_") + + "\">Warning: widgetset version " + Version.getFullVersion() + + " does not seem to match theme version </div>", true); + + } + + private void addSuperDevMode() { + final Storage sessionStorage = Storage.getSessionStorageIfSupported(); + if (sessionStorage == null) { + return; + } + actions.add(superDevMode); + if (Location.getParameter("superdevmode") != null) { + superDevMode.setValue(true); + } + superDevMode.addValueChangeHandler(new ValueChangeHandler<Boolean>() { + + @Override + public void onValueChange(ValueChangeEvent<Boolean> event) { + SuperDevMode.redirect(event.getValue()); + } + + }); + + } + + private void addDevMode() { + actions.add(devMode); + if (Location.getParameter("gwt.codesvr") != null) { + devMode.setValue(true); + } + devMode.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (devMode.getValue()) { + addHMParameter(); + } else { + removeHMParameter(); + } + } + + private void addHMParameter() { + UrlBuilder createUrlBuilder = Location.createUrlBuilder(); + createUrlBuilder.setParameter("gwt.codesvr", "localhost:9997"); + Location.assign(createUrlBuilder.buildString()); + } + + private void removeHMParameter() { + UrlBuilder createUrlBuilder = Location.createUrlBuilder(); + createUrlBuilder.removeParameter("gwt.codesvr"); + Location.assign(createUrlBuilder.buildString()); + + } + }); + } + + protected void dumpConnectorInfo(ApplicationConnection a) { + RootConnector root = a.getRootConnector(); + log("================"); + log("Connector hierarchy for Root: " + root.getState().getCaption() + + " (" + root.getConnectorId() + ")"); + Set<ServerConnector> connectorsInHierarchy = new HashSet<ServerConnector>(); + SimpleTree rootHierachy = dumpConnectorHierarchy(root, "", + connectorsInHierarchy); + if (panel.isAttached()) { + rootHierachy.open(true); + panel.add(rootHierachy); + } + + ConnectorMap connectorMap = a.getConnectorMap(); + Collection<? extends ServerConnector> registeredConnectors = connectorMap + .getConnectors(); + log("Sub windows:"); + Set<ServerConnector> subWindowHierarchyConnectors = new HashSet<ServerConnector>(); + for (WindowConnector wc : root.getSubWindows()) { + SimpleTree windowHierachy = dumpConnectorHierarchy(wc, "", + subWindowHierarchyConnectors); + if (panel.isAttached()) { + windowHierachy.open(true); + panel.add(windowHierachy); + } + } + log("Registered connectors not in hierarchy (should be empty):"); + for (ServerConnector registeredConnector : registeredConnectors) { + + if (connectorsInHierarchy.contains(registeredConnector)) { + continue; + } + + if (subWindowHierarchyConnectors.contains(registeredConnector)) { + continue; + } + error(getConnectorString(registeredConnector)); + + } + log("Unregistered connectors in hierarchy (should be empty):"); + for (ServerConnector hierarchyConnector : connectorsInHierarchy) { + if (!connectorMap.hasConnector(hierarchyConnector.getConnectorId())) { + error(getConnectorString(hierarchyConnector)); + } + + } + + log("================"); + + } + + private SimpleTree dumpConnectorHierarchy(final ServerConnector connector, + String indent, Set<ServerConnector> connectors) { + SimpleTree simpleTree = new SimpleTree(getConnectorString(connector)) { + @Override + protected void select(ClickEvent event) { + super.select(event); + if (connector instanceof ComponentConnector) { + VUIDLBrowser.highlight((ComponentConnector) connector); + } + } + }; + simpleTree.addDomHandler(new MouseOutHandler() { + @Override + public void onMouseOut(MouseOutEvent event) { + VUIDLBrowser.deHiglight(); + } + }, MouseOutEvent.getType()); + connectors.add(connector); + + String msg = indent + "* " + getConnectorString(connector); + GWT.log(msg); + consoleLog(msg); + System.out.println(msg); + + for (ServerConnector c : connector.getChildren()) { + simpleTree.add(dumpConnectorHierarchy(c, indent + " ", connectors)); + } + return simpleTree; + } + + private static String getConnectorString(ServerConnector connector) { + return Util.getConnectorString(connector); + } + + @Override + public void setQuietMode(boolean quietDebugMode) { + quietMode = quietDebugMode; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/VErrorMessage.java b/client/src/com/vaadin/terminal/gwt/client/VErrorMessage.java new file mode 100644 index 0000000000..af94241361 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/VErrorMessage.java @@ -0,0 +1,73 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.terminal.gwt.client.ui.VOverlay; + +public class VErrorMessage extends FlowPanel { + public static final String CLASSNAME = "v-errormessage"; + + public VErrorMessage() { + super(); + setStyleName(CLASSNAME); + } + + public void updateMessage(String htmlErrorMessage) { + clear(); + if (htmlErrorMessage == null || htmlErrorMessage.length() == 0) { + add(new HTML(" ")); + } else { + // pre-formatted on the server as div per child + add(new HTML(htmlErrorMessage)); + } + } + + /** + * Shows this error message next to given element. + * + * @param indicatorElement + */ + public void showAt(Element indicatorElement) { + VOverlay errorContainer = (VOverlay) getParent(); + if (errorContainer == null) { + errorContainer = new VOverlay(); + errorContainer.setWidget(this); + } + errorContainer.setPopupPosition( + DOM.getAbsoluteLeft(indicatorElement) + + 2 + * DOM.getElementPropertyInt(indicatorElement, + "offsetHeight"), + DOM.getAbsoluteTop(indicatorElement) + + 2 + * DOM.getElementPropertyInt(indicatorElement, + "offsetHeight")); + errorContainer.show(); + + } + + public void hide() { + final VOverlay errorContainer = (VOverlay) getParent(); + if (errorContainer != null) { + errorContainer.hide(); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/VSchedulerImpl.java b/client/src/com/vaadin/terminal/gwt/client/VSchedulerImpl.java new file mode 100644 index 0000000000..56dec16289 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/VSchedulerImpl.java @@ -0,0 +1,45 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.core.client.impl.SchedulerImpl; + +public class VSchedulerImpl extends SchedulerImpl { + + /** + * Keeps track of if there are deferred commands that are being executed. 0 + * == no deferred commands currently in progress, > 0 otherwise. + */ + private int deferredCommandTrackers = 0; + + @Override + public void scheduleDeferred(ScheduledCommand cmd) { + deferredCommandTrackers++; + super.scheduleDeferred(cmd); + super.scheduleDeferred(new ScheduledCommand() { + + @Override + public void execute() { + deferredCommandTrackers--; + } + }); + } + + public boolean hasWorkQueued() { + boolean hasWorkQueued = (deferredCommandTrackers != 0); + return hasWorkQueued; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/VTooltip.java b/client/src/com/vaadin/terminal/gwt/client/VTooltip.java new file mode 100644 index 0000000000..1b9321fb16 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/VTooltip.java @@ -0,0 +1,364 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.MouseMoveEvent; +import com.google.gwt.event.dom.client.MouseMoveHandler; +import com.google.gwt.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.Window; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ui.VOverlay; + +/** + * TODO open for extension + */ +public class VTooltip extends VOverlay { + private static final String CLASSNAME = "v-tooltip"; + private static final int MARGIN = 4; + public static final int TOOLTIP_EVENTS = Event.ONKEYDOWN + | Event.ONMOUSEOVER | Event.ONMOUSEOUT | Event.ONMOUSEMOVE + | Event.ONCLICK; + protected static final int MAX_WIDTH = 500; + private static final int QUICK_OPEN_TIMEOUT = 1000; + private static final int CLOSE_TIMEOUT = 300; + private static final int OPEN_DELAY = 750; + private static final int QUICK_OPEN_DELAY = 100; + VErrorMessage em = new VErrorMessage(); + Element description = DOM.createDiv(); + + private boolean closing = false; + private boolean opening = false; + private ApplicationConnection ac; + // Open next tooltip faster. Disabled after 2 sec of showTooltip-silence. + private boolean justClosed = false; + + public VTooltip(ApplicationConnection client) { + super(false, false, true); + ac = client; + setStyleName(CLASSNAME); + FlowPanel layout = new FlowPanel(); + setWidget(layout); + layout.add(em); + DOM.setElementProperty(description, "className", CLASSNAME + "-text"); + DOM.appendChild(layout.getElement(), description); + setSinkShadowEvents(true); + } + + /** + * Show a popup containing the information in the "info" tooltip + * + * @param info + */ + private void show(TooltipInfo info) { + boolean hasContent = false; + if (info.getErrorMessage() != null) { + em.setVisible(true); + em.updateMessage(info.getErrorMessage()); + hasContent = true; + } else { + em.setVisible(false); + } + if (info.getTitle() != null && !"".equals(info.getTitle())) { + DOM.setInnerHTML(description, info.getTitle()); + DOM.setStyleAttribute(description, "display", ""); + hasContent = true; + } else { + DOM.setInnerHTML(description, ""); + DOM.setStyleAttribute(description, "display", "none"); + } + if (hasContent) { + // Issue #8454: With IE7 the tooltips size is calculated based on + // the last tooltip's position, causing problems if the last one was + // in the right or bottom edge. For this reason the tooltip is moved + // first to 0,0 position so that the calculation goes correctly. + setPopupPosition(0, 0); + setPopupPositionAndShow(new PositionCallback() { + @Override + public void setPosition(int offsetWidth, int offsetHeight) { + + if (offsetWidth > MAX_WIDTH) { + setWidth(MAX_WIDTH + "px"); + + // Check new height and width with reflowed content + offsetWidth = getOffsetWidth(); + offsetHeight = getOffsetHeight(); + } + + int x = tooltipEventMouseX + 10 + Window.getScrollLeft(); + int y = tooltipEventMouseY + 10 + Window.getScrollTop(); + + if (x + offsetWidth + MARGIN - Window.getScrollLeft() > Window + .getClientWidth()) { + x = Window.getClientWidth() - offsetWidth - MARGIN; + } + + if (y + offsetHeight + MARGIN - Window.getScrollTop() > Window + .getClientHeight()) { + y = tooltipEventMouseY - 5 - offsetHeight; + if (y - Window.getScrollTop() < 0) { + // tooltip does not fit on top of the mouse either, + // put it at the top of the screen + y = Window.getScrollTop(); + } + } + + setPopupPosition(x, y); + sinkEvents(Event.ONMOUSEOVER | Event.ONMOUSEOUT); + } + }); + } else { + hide(); + } + } + + private void showTooltip() { + + // Close current tooltip + if (isShowing()) { + closeNow(); + } + + // Schedule timer for showing the tooltip according to if it was + // recently closed or not. + int timeout = justClosed ? QUICK_OPEN_DELAY : OPEN_DELAY; + showTimer.schedule(timeout); + opening = true; + } + + private void closeNow() { + hide(); + setWidth(""); + closing = false; + } + + private Timer showTimer = new Timer() { + @Override + public void run() { + TooltipInfo info = tooltipEventHandler.getTooltipInfo(); + if (null != info) { + show(info); + } + opening = false; + } + }; + + private Timer closeTimer = new Timer() { + @Override + public void run() { + closeNow(); + justClosedTimer.schedule(2000); + justClosed = true; + } + }; + + private Timer justClosedTimer = new Timer() { + @Override + public void run() { + justClosed = false; + } + }; + + public void hideTooltip() { + if (opening) { + showTimer.cancel(); + opening = false; + } + if (!isAttached()) { + return; + } + if (closing) { + // already about to close + return; + } + closeTimer.schedule(CLOSE_TIMEOUT); + closing = true; + justClosed = true; + justClosedTimer.schedule(QUICK_OPEN_TIMEOUT); + + } + + private int tooltipEventMouseX; + private int tooltipEventMouseY; + + public void updatePosition(Event event) { + tooltipEventMouseX = DOM.eventGetClientX(event); + tooltipEventMouseY = DOM.eventGetClientY(event); + } + + @Override + public void onBrowserEvent(Event event) { + final int type = DOM.eventGetType(event); + // cancel closing event if tooltip is mouseovered; the user might want + // to scroll of cut&paste + + if (type == Event.ONMOUSEOVER) { + // Cancel closing so tooltip stays open and user can copy paste the + // tooltip + closeTimer.cancel(); + closing = false; + } + } + + /** + * Replace current open tooltip with new content + */ + public void replaceCurrentTooltip() { + if (closing) { + closeTimer.cancel(); + closeNow(); + } + + TooltipInfo info = tooltipEventHandler.getTooltipInfo(); + if (null != info) { + show(info); + } + opening = false; + } + + private class TooltipEventHandler implements MouseMoveHandler, + ClickHandler, KeyDownHandler { + + /** + * Current element hovered + */ + private com.google.gwt.dom.client.Element currentElement = null; + + /** + * Current tooltip active + */ + private TooltipInfo currentTooltipInfo = null; + + /** + * Get current active tooltip information + * + * @return Current active tooltip information or null + */ + public TooltipInfo getTooltipInfo() { + return currentTooltipInfo; + } + + /** + * Locate connector and it's tooltip for given element + * + * @param element + * Element used in search + * @return true if connector and tooltip found + */ + private boolean resolveConnector(Element element) { + + ComponentConnector connector = Util.getConnectorForElement(ac, + RootPanel.get(), element); + + // Try to find first connector with proper tooltip info + TooltipInfo info = null; + while (connector != null) { + + info = connector.getTooltipInfo(element); + + if (info != null && info.hasMessage()) { + break; + } + + if (!(connector.getParent() instanceof ComponentConnector)) { + connector = null; + info = null; + break; + } + connector = (ComponentConnector) connector.getParent(); + } + + if (connector != null && info != null) { + currentTooltipInfo = info; + return true; + } + + return false; + } + + /** + * Handle hide event + * + * @param event + * Event causing hide + */ + private void handleHideEvent() { + hideTooltip(); + currentTooltipInfo = null; + } + + @Override + public void onMouseMove(MouseMoveEvent mme) { + Event event = Event.as(mme.getNativeEvent()); + com.google.gwt.dom.client.Element element = Element.as(event + .getEventTarget()); + + // We can ignore move event if it's handled by move or over already + if (currentElement == element) { + return; + } + currentElement = element; + + boolean connectorAndTooltipFound = resolveConnector((com.google.gwt.user.client.Element) element); + if (!connectorAndTooltipFound) { + if (isShowing()) { + handleHideEvent(); + } else { + currentTooltipInfo = null; + } + } else { + updatePosition(event); + if (isShowing()) { + replaceCurrentTooltip(); + } else { + showTooltip(); + } + } + } + + @Override + public void onClick(ClickEvent event) { + handleHideEvent(); + } + + @Override + public void onKeyDown(KeyDownEvent event) { + handleHideEvent(); + } + } + + private final TooltipEventHandler tooltipEventHandler = new TooltipEventHandler(); + + /** + * Connects DOM handlers to widget that are needed for tooltip presentation. + * + * @param widget + * Widget which DOM handlers are connected + */ + public void connectHandlersToWidget(Widget widget) { + widget.addDomHandler(tooltipEventHandler, MouseMoveEvent.getType()); + widget.addDomHandler(tooltipEventHandler, ClickEvent.getType()); + widget.addDomHandler(tooltipEventHandler, KeyDownEvent.getType()); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/VUIDLBrowser.java b/client/src/com/vaadin/terminal/gwt/client/VUIDLBrowser.java new file mode 100644 index 0000000000..3cdc03ee86 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/VUIDLBrowser.java @@ -0,0 +1,362 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.core.client.JsArray; +import com.google.gwt.core.client.JsArrayString; +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.Element; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.MouseOutEvent; +import com.google.gwt.event.dom.client.MouseOutHandler; +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONValue; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.Connector; +import com.vaadin.terminal.gwt.client.ui.UnknownComponentConnector; +import com.vaadin.terminal.gwt.client.ui.window.VWindow; + +/** + * TODO Rename to something more Vaadin7-ish? + */ +public class VUIDLBrowser extends SimpleTree { + private static final String HELP = "Shift click handle to open recursively. " + + " Click components to highlight them on client side." + + " Shift click components to highlight them also on the server side."; + private ApplicationConnection client; + private String highlightedPid; + + public VUIDLBrowser(final UIDL uidl, ApplicationConnection client) { + this.client = client; + final UIDLItem root = new UIDLItem(uidl); + add(root); + } + + public VUIDLBrowser(ValueMap u, ApplicationConnection client) { + this.client = client; + ValueMap valueMap = u.getValueMap("meta"); + if (valueMap.containsKey("hl")) { + highlightedPid = valueMap.getString("hl"); + } + Set<String> keySet = u.getKeySet(); + for (String key : keySet) { + if (key.equals("state")) { + ValueMap stateJson = u.getValueMap(key); + SimpleTree stateChanges = new SimpleTree("shared state"); + + for (String connectorId : stateJson.getKeySet()) { + stateChanges.add(new SharedStateItem(connectorId, stateJson + .getValueMap(connectorId))); + } + add(stateChanges); + + } else if (key.equals("changes")) { + JsArray<UIDL> jsValueMapArray = u.getJSValueMapArray(key) + .cast(); + for (int i = 0; i < jsValueMapArray.length(); i++) { + UIDL uidl = jsValueMapArray.get(i); + UIDLItem change = new UIDLItem(uidl); + change.setTitle("change " + i); + add(change); + } + } else if (key.equals("meta")) { + + } else { + // TODO consider pretty printing other request data such as + // hierarchy changes + // addItem(key + " : " + u.getAsString(key)); + } + } + open(highlightedPid != null); + setTitle(HELP); + } + + /** + * A debug view of a server-originated component state change. + */ + abstract class StateChangeItem extends SimpleTree { + + protected StateChangeItem() { + setTitle(HELP); + + addDomHandler(new MouseOutHandler() { + @Override + public void onMouseOut(MouseOutEvent event) { + deHiglight(); + } + }, MouseOutEvent.getType()); + } + + @Override + protected void select(ClickEvent event) { + ComponentConnector connector = getConnector(); + highlight(connector); + if (event != null && event.getNativeEvent().getShiftKey()) { + connector.getConnection().highlightComponent(connector); + } + super.select(event); + } + + /** + * Returns the Connector associated with this state change. + */ + protected ComponentConnector getConnector() { + Connector connector = client.getConnectorMap().getConnector( + getConnectorId()); + + if (connector instanceof ComponentConnector) { + return (ComponentConnector) connector; + } else { + return null; + } + } + + protected abstract String getConnectorId(); + } + + /** + * A debug view of a Vaadin 7 style shared state change. + */ + class SharedStateItem extends StateChangeItem { + + private String connectorId; + + SharedStateItem(String connectorId, ValueMap stateChanges) { + this.connectorId = connectorId; + ComponentConnector connector = getConnector(); + if (connector != null) { + setText(Util.getConnectorString(connector)); + } else { + setText("Unknown connector " + connectorId); + } + dir(new JSONObject(stateChanges), this); + } + + @Override + protected String getConnectorId() { + return connectorId; + } + + private void dir(String key, JSONValue value, SimpleTree tree) { + if (value.isObject() != null) { + SimpleTree subtree = new SimpleTree(key + "=object"); + tree.add(subtree); + dir(value.isObject(), subtree); + } else if (value.isArray() != null) { + SimpleTree subtree = new SimpleTree(key + "=array"); + dir(value.isArray(), subtree); + tree.add(subtree); + } else { + tree.addItem(key + "=" + value); + } + } + + private void dir(JSONObject state, SimpleTree tree) { + for (String key : state.keySet()) { + dir(key, state.get(key), tree); + } + } + + private void dir(JSONArray array, SimpleTree tree) { + for (int i = 0; i < array.size(); ++i) { + dir("" + i, array.get(i), tree); + } + } + } + + /** + * A debug view of a Vaadin 6 style hierarchical component state change. + */ + class UIDLItem extends StateChangeItem { + + private UIDL uidl; + + UIDLItem(UIDL uidl) { + this.uidl = uidl; + try { + String name = uidl.getTag(); + try { + name = getNodeName(uidl, client.getConfiguration(), + Integer.parseInt(name)); + } catch (Exception e) { + // NOP + } + setText(name); + addItem("LOADING"); + } catch (Exception e) { + setText(uidl.toString()); + } + } + + @Override + protected String getConnectorId() { + return uidl.getId(); + } + + private String getNodeName(UIDL uidl, ApplicationConfiguration conf, + int tag) { + Class<? extends ServerConnector> widgetClassByDecodedTag = conf + .getConnectorClassByEncodedTag(tag); + if (widgetClassByDecodedTag == UnknownComponentConnector.class) { + return conf.getUnknownServerClassNameByTag(tag) + + "(NO CLIENT IMPLEMENTATION FOUND)"; + } else { + return widgetClassByDecodedTag.getName(); + } + } + + @Override + public void open(boolean recursive) { + if (getWidgetCount() == 1 + && getWidget(0).getElement().getInnerText() + .equals("LOADING")) { + dir(); + } + super.open(recursive); + } + + public void dir() { + remove(0); + + String nodeName = uidl.getTag(); + try { + nodeName = getNodeName(uidl, client.getConfiguration(), + Integer.parseInt(nodeName)); + } catch (Exception e) { + // NOP + } + + Set<String> attributeNames = uidl.getAttributeNames(); + for (String name : attributeNames) { + if (uidl.isMapAttribute(name)) { + try { + ValueMap map = uidl.getMapAttribute(name); + JsArrayString keyArray = map.getKeyArray(); + nodeName += " " + name + "=" + "{"; + for (int i = 0; i < keyArray.length(); i++) { + nodeName += keyArray.get(i) + ":" + + map.getAsString(keyArray.get(i)) + ","; + } + nodeName += "}"; + } catch (Exception e) { + + } + } else { + final String value = uidl.getAttribute(name); + nodeName += " " + name + "=" + value; + } + } + setText(nodeName); + + try { + SimpleTree tmp = null; + Set<String> variableNames = uidl.getVariableNames(); + for (String name : variableNames) { + String value = ""; + try { + value = uidl.getVariable(name); + } catch (final Exception e) { + try { + String[] stringArrayAttribute = uidl + .getStringArrayAttribute(name); + value = stringArrayAttribute.toString(); + } catch (final Exception e2) { + try { + final int intVal = uidl.getIntVariable(name); + value = String.valueOf(intVal); + } catch (final Exception e3) { + value = "unknown"; + } + } + } + if (tmp == null) { + tmp = new SimpleTree("variables"); + } + tmp.addItem(name + "=" + value); + } + if (tmp != null) { + add(tmp); + } + } catch (final Exception e) { + // Ignored, no variables + } + + final Iterator<Object> i = uidl.getChildIterator(); + while (i.hasNext()) { + final Object child = i.next(); + try { + add(new UIDLItem((UIDL) child)); + } catch (final Exception e) { + addItem(child.toString()); + } + } + if (highlightedPid != null && highlightedPid.equals(uidl.getId())) { + getElement().getStyle().setBackgroundColor("#fdd"); + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + getElement().scrollIntoView(); + } + }); + } + } + } + + static Element highlight = Document.get().createDivElement(); + + static { + Style style = highlight.getStyle(); + style.setPosition(Position.ABSOLUTE); + style.setZIndex(VWindow.Z_INDEX + 1000); + style.setBackgroundColor("red"); + style.setOpacity(0.2); + if (BrowserInfo.get().isIE()) { + style.setProperty("filter", "alpha(opacity=20)"); + } + } + + static void highlight(ComponentConnector paintable) { + if (paintable != null) { + Widget w = paintable.getWidget(); + Style style = highlight.getStyle(); + style.setTop(w.getAbsoluteTop(), Unit.PX); + style.setLeft(w.getAbsoluteLeft(), Unit.PX); + style.setWidth(w.getOffsetWidth(), Unit.PX); + style.setHeight(w.getOffsetHeight(), Unit.PX); + RootPanel.getBodyElement().appendChild(highlight); + } + } + + static void deHiglight() { + if (highlight.getParentElement() != null) { + highlight.getParentElement().removeChild(highlight); + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ValueMap.java b/client/src/com/vaadin/terminal/gwt/client/ValueMap.java new file mode 100644 index 0000000000..bade480e7c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ValueMap.java @@ -0,0 +1,121 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.HashSet; +import java.util.Set; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArray; +import com.google.gwt.core.client.JsArrayString; + +public final class ValueMap extends JavaScriptObject { + protected ValueMap() { + } + + public native double getRawNumber(final String name) + /*-{ + return this[name]; + }-*/; + + public native int getInt(final String name) + /*-{ + return this[name]; + }-*/; + + public native boolean getBoolean(final String name) + /*-{ + return Boolean(this[name]); + }-*/; + + public native String getString(String name) + /*-{ + return this[name]; + }-*/; + + public native JsArrayString getKeyArray() + /*-{ + var a = new Array(); + var attr = this; + for(var j in attr) { + // workaround for the infamous chrome hosted mode hack (__gwt_ObjectId) + if(attr.hasOwnProperty(j)) + a.push(j); + } + return a; + }-*/; + + public Set<String> getKeySet() { + final HashSet<String> attrs = new HashSet<String>(); + JsArrayString attributeNamesArray = getKeyArray(); + for (int i = 0; i < attributeNamesArray.length(); i++) { + attrs.add(attributeNamesArray.get(i)); + } + return attrs; + } + + native JsArrayString getJSStringArray(String name) + /*-{ + return this[name]; + }-*/; + + native JsArray<ValueMap> getJSValueMapArray(String name) + /*-{ + return this[name]; + }-*/; + + public String[] getStringArray(final String name) { + JsArrayString stringArrayAttribute = getJSStringArray(name); + final String[] s = new String[stringArrayAttribute.length()]; + for (int i = 0; i < stringArrayAttribute.length(); i++) { + s[i] = stringArrayAttribute.get(i); + } + return s; + } + + public int[] getIntArray(final String name) { + JsArrayString stringArrayAttribute = getJSStringArray(name); + final int[] s = new int[stringArrayAttribute.length()]; + for (int i = 0; i < stringArrayAttribute.length(); i++) { + s[i] = Integer.parseInt(stringArrayAttribute.get(i)); + } + return s; + } + + public native boolean containsKey(final String name) + /*-{ + return name in this; + }-*/; + + public native ValueMap getValueMap(String name) + /*-{ + return this[name]; + }-*/; + + native String getAsString(String name) + /*-{ + return '' + this[name]; + }-*/; + + native JavaScriptObject getJavaScriptObject(String name) + /*-{ + return this[name]; + }-*/; + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/WidgetInstantiator.java b/client/src/com/vaadin/terminal/gwt/client/WidgetInstantiator.java new file mode 100644 index 0000000000..347a8dd631 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/WidgetInstantiator.java @@ -0,0 +1,23 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +/** + * A helper class used by WidgetMap implementation. Used by the generated code. + */ +interface WidgetInstantiator { + public ServerConnector get(); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/WidgetLoader.java b/client/src/com/vaadin/terminal/gwt/client/WidgetLoader.java new file mode 100644 index 0000000000..e98aa031c4 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/WidgetLoader.java @@ -0,0 +1,35 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.core.client.RunAsyncCallback; + +/** A helper class used by WidgetMap implementation. Used by the generated code. */ +abstract class WidgetLoader implements RunAsyncCallback { + + @Override + public void onFailure(Throwable reason) { + ApplicationConfiguration.endDependencyLoading(); + } + + @Override + public void onSuccess() { + addInstantiator(); + ApplicationConfiguration.endDependencyLoading(); + } + + abstract void addInstantiator(); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/WidgetMap.java b/client/src/com/vaadin/terminal/gwt/client/WidgetMap.java new file mode 100644 index 0000000000..4c929714c3 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/WidgetMap.java @@ -0,0 +1,74 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import java.util.HashMap; + +/** + * Abstract class mapping between {@link ComponentConnector} instances and their + * instances. + * + * A concrete implementation of this class is generated by WidgetMapGenerator or + * one of its subclasses during widgetset compilation. + */ +abstract class WidgetMap { + + protected static HashMap<Class<? extends ServerConnector>, WidgetInstantiator> instmap = new HashMap<Class<? extends ServerConnector>, WidgetInstantiator>(); + + /** + * Create a new instance of a connector based on its type. + * + * @param classType + * {@link ComponentConnector} class to instantiate + * @return new instance of the connector + */ + public ServerConnector instantiate( + Class<? extends ServerConnector> classType) { + return instmap.get(classType).get(); + } + + /** + * Return the connector class to use for a fully qualified server side + * component class name. + * + * @param fullyqualifiedName + * fully qualified name of the server side component class + * @return component connector class to use + */ + public abstract Class<? extends ServerConnector> getConnectorClassForServerSideClassName( + String fullyqualifiedName); + + /** + * Return the connector classes to load after the initial widgetset load and + * start. + * + * @return component connector class to load after the initial widgetset + * loading + */ + public abstract Class<? extends ServerConnector>[] getDeferredLoadedConnectors(); + + /** + * Make sure the code for a (deferred or lazy) component connector type has + * been loaded, triggering the load and waiting for its completion if + * necessary. + * + * @param classType + * component connector class + */ + public abstract void ensureInstantiator( + Class<? extends ServerConnector> classType); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/WidgetSet.java b/client/src/com/vaadin/terminal/gwt/client/WidgetSet.java new file mode 100644 index 0000000000..fbcfbb68d9 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/WidgetSet.java @@ -0,0 +1,127 @@ +/* + * Copyright 2011 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.terminal.gwt.client; + +import com.google.gwt.core.client.GWT; +import com.vaadin.terminal.gwt.client.communication.HasJavaScriptConnectorHelper; +import com.vaadin.terminal.gwt.client.ui.UnknownComponentConnector; + +public class WidgetSet { + + /** + * WidgetSet (and its extensions) delegate instantiation of widgets and + * client-server matching to WidgetMap. The actual implementations are + * generated with gwts generators/deferred binding. + */ + private WidgetMap widgetMap = GWT.create(WidgetMap.class); + + /** + * Create an uninitialized connector that best matches given UIDL. The + * connector must implement {@link ServerConnector}. + * + * @param tag + * connector type tag for the connector to create + * @param conf + * the application configuration to use when creating the + * connector + * + * @return New uninitialized and unregistered connector that can paint given + * UIDL. + */ + public ServerConnector createConnector(int tag, + ApplicationConfiguration conf) { + /* + * Yes, this (including the generated code in WidgetMap) may look very + * odd code, but due the nature of GWT, we cannot do this any cleaner. + * Luckily this is mostly written by WidgetSetGenerator, here are just + * some hacks. Extra instantiation code is needed if client side + * connector has no "native" counterpart on client side. + */ + + Class<? extends ServerConnector> classType = resolveInheritedConnectorType( + conf, tag); + + if (classType == null || classType == UnknownComponentConnector.class) { + String serverSideName = conf.getUnknownServerClassNameByTag(tag); + UnknownComponentConnector c = GWT + .create(UnknownComponentConnector.class); + c.setServerSideClassName(serverSideName); + return c; + } else { + /* + * let the auto generated code instantiate this type + */ + ServerConnector connector = widgetMap.instantiate(classType); + if (connector instanceof HasJavaScriptConnectorHelper) { + ((HasJavaScriptConnectorHelper) connector) + .getJavascriptConnectorHelper().setTag(tag); + } + return connector; + } + } + + private Class<? extends ServerConnector> resolveInheritedConnectorType( + ApplicationConfiguration conf, int tag) { + Class<? extends ServerConnector> classType = null; + Integer t = tag; + do { + classType = resolveConnectorType(t, conf); + t = conf.getParentTag(t); + } while (classType == null && t != null); + return classType; + } + + protected Class<? extends ServerConnector> resolveConnectorType(int tag, + ApplicationConfiguration conf) { + Class<? extends ServerConnector> connectorClass = conf + .getConnectorClassByEncodedTag(tag); + + return connectorClass; + } + + /** + * Due its nature, GWT does not support dynamic classloading. To bypass this + * limitation, widgetset must have function that returns Class by its fully + * qualified name. + * + * @param tag + * @param applicationConfiguration + * @return + */ + public Class<? extends ServerConnector> getConnectorClassByTag(int tag, + ApplicationConfiguration conf) { + Class<? extends ServerConnector> connectorClass = null; + Integer t = tag; + do { + String serverSideClassName = conf.getServerSideClassNameForTag(t); + connectorClass = widgetMap + .getConnectorClassForServerSideClassName(serverSideClassName); + t = conf.getParentTag(t); + } while (connectorClass == UnknownComponentConnector.class && t != null); + + return connectorClass; + } + + public Class<? extends ServerConnector>[] getDeferredLoadedConnectors() { + return widgetMap.getDeferredLoadedConnectors(); + } + + public void loadImplementation(Class<? extends ServerConnector> nextType) { + widgetMap.ensureInstantiator(nextType); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/AbstractServerConnectorEvent.java b/client/src/com/vaadin/terminal/gwt/client/communication/AbstractServerConnectorEvent.java new file mode 100644 index 0000000000..7b4114ec07 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/AbstractServerConnectorEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import com.google.gwt.event.shared.EventHandler; +import com.google.gwt.event.shared.GwtEvent; +import com.vaadin.terminal.gwt.client.ServerConnector; + +public abstract class AbstractServerConnectorEvent<H extends EventHandler> + extends GwtEvent<H> { + private ServerConnector connector; + + protected AbstractServerConnectorEvent() { + } + + public ServerConnector getConnector() { + return connector; + } + + public void setConnector(ServerConnector connector) { + this.connector = connector; + } + + /** + * Sends this event to the given handler. + * + * @param handler + * The handler to dispatch. + */ + @Override + public abstract void dispatch(H handler); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/DiffJSONSerializer.java b/client/src/com/vaadin/terminal/gwt/client/communication/DiffJSONSerializer.java new file mode 100644 index 0000000000..a3b96a6cb2 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/DiffJSONSerializer.java @@ -0,0 +1,31 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import com.google.gwt.json.client.JSONValue; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +public interface DiffJSONSerializer<T> extends JSONSerializer<T> { + /** + * Update the target object in place based on the passed JSON data. + * + * @param target + * @param jsonValue + * @param connection + */ + public void update(T target, Type type, JSONValue jsonValue, + ApplicationConnection connection); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/GeneratedRpcMethodProvider.java b/client/src/com/vaadin/terminal/gwt/client/communication/GeneratedRpcMethodProvider.java new file mode 100644 index 0000000000..e865dbc1b1 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/GeneratedRpcMethodProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import java.util.Collection; + +/** + * Provides runtime data about client side RPC calls received from the server to + * the client-side code. + * + * A GWT generator is used to create an implementation of this class at + * run-time. + * + * @since 7.0 + */ +public interface GeneratedRpcMethodProvider { + + public Collection<RpcMethod> getGeneratedRpcMethods(); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/HasJavaScriptConnectorHelper.java b/client/src/com/vaadin/terminal/gwt/client/communication/HasJavaScriptConnectorHelper.java new file mode 100644 index 0000000000..1c8a96a814 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/HasJavaScriptConnectorHelper.java @@ -0,0 +1,23 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper; + +public interface HasJavaScriptConnectorHelper { + public JavaScriptConnectorHelper getJavascriptConnectorHelper(); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/InitializableServerRpc.java b/client/src/com/vaadin/terminal/gwt/client/communication/InitializableServerRpc.java new file mode 100644 index 0000000000..65887bf62e --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/InitializableServerRpc.java @@ -0,0 +1,39 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.terminal.gwt.client.ServerConnector; + +/** + * Initialization support for client to server RPC interfaces. + * + * This is in a separate interface used by the GWT generator class. The init + * method is not in {@link ServerRpc} because then also server side proxies + * would have to implement the initialization method. + * + * @since 7.0 + */ +public interface InitializableServerRpc extends ServerRpc { + /** + * Associates the RPC proxy with a connector. Called by generated code. + * Should never be called manually. + * + * @param connector + * The connector the ServerRPC instance is assigned to. + */ + public void initRpc(ServerConnector connector); +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/JSONSerializer.java b/client/src/com/vaadin/terminal/gwt/client/communication/JSONSerializer.java new file mode 100644 index 0000000000..a8fe2c7ccc --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/JSONSerializer.java @@ -0,0 +1,64 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONValue; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ConnectorMap; + +/** + * 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. + * + * The {@link #serialize(Object, ConnectorMap)} and + * {@link #deserialize(JSONObject, ConnectorMap)} methods must be symmetric so + * they can be chained and produce the original result (or an equal result). + * + * Each {@link JSONSerializer} implementation can handle an object of a single + * type - see {@link SerializerMap}. + * + * @since 7.0 + */ +public interface JSONSerializer<T> { + + /** + * Creates and deserializes an object received from the server. Must be + * compatible with {@link #serialize(Object, ConnectorMap)} and also with + * the server side JsonCodec.encode(Object, + * com.vaadin.terminal.gwt.server.PaintableIdMapper) . + * + * @param jsonValue + * JSON map from property name to property value + * @return A deserialized object + */ + T deserialize(Type type, JSONValue jsonValue, + ApplicationConnection connection); + + /** + * Serialize the given object into JSON. Must be compatible with + * {@link #deserialize(JSONObject, ConnectorMap)} and also with the server + * side JsonCodec.decode(com.vaadin.external.json.JSONArray, + * com.vaadin.terminal.gwt.server.PaintableIdMapper) + * + * @param value + * The object to serialize + * @return A JSON serialized version of the object + */ + JSONValue serialize(T value, ApplicationConnection connection); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java b/client/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java new file mode 100644 index 0000000000..7d2046982c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java @@ -0,0 +1,225 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONString; +import com.google.gwt.json.client.JSONValue; +import com.vaadin.shared.Connector; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ConnectorMap; + +/** + * Client side decoder for decodeing shared state and other values from JSON + * received from the server. + * + * Currently, basic data types as well as Map, String[] and Object[] are + * supported, where maps and Object[] can contain other supported data types. + * + * TODO extensible type support + * + * @since 7.0 + */ +public class JsonDecoder { + + /** + * Decode a JSON array with two elements (type and value) into a client-side + * type, recursively if necessary. + * + * @param jsonValue + * JSON value with encoded data + * @param connection + * reference to the current ApplicationConnection + * @return decoded value (does not contain JSON types) + */ + public static Object decodeValue(Type type, JSONValue jsonValue, + Object target, ApplicationConnection connection) { + + // Null is null, regardless of type + if (jsonValue.isNull() != null) { + return null; + } + + String baseTypeName = type.getBaseTypeName(); + if (Map.class.getName().equals(baseTypeName) + || HashMap.class.getName().equals(baseTypeName)) { + return decodeMap(type, jsonValue, connection); + } else if (List.class.getName().equals(baseTypeName) + || ArrayList.class.getName().equals(baseTypeName)) { + return decodeList(type, (JSONArray) jsonValue, connection); + } else if (Set.class.getName().equals(baseTypeName)) { + return decodeSet(type, (JSONArray) jsonValue, connection); + } else if (String.class.getName().equals(baseTypeName)) { + return ((JSONString) jsonValue).stringValue(); + } else if (Integer.class.getName().equals(baseTypeName)) { + return Integer.valueOf(String.valueOf(jsonValue)); + } else if (Long.class.getName().equals(baseTypeName)) { + // TODO handle properly + return Long.valueOf(String.valueOf(jsonValue)); + } else if (Float.class.getName().equals(baseTypeName)) { + // TODO handle properly + return Float.valueOf(String.valueOf(jsonValue)); + } else if (Double.class.getName().equals(baseTypeName)) { + // TODO handle properly + return Double.valueOf(String.valueOf(jsonValue)); + } else if (Boolean.class.getName().equals(baseTypeName)) { + // TODO handle properly + return Boolean.valueOf(String.valueOf(jsonValue)); + } else if (Byte.class.getName().equals(baseTypeName)) { + // TODO handle properly + return Byte.valueOf(String.valueOf(jsonValue)); + } else if (Character.class.getName().equals(baseTypeName)) { + // TODO handle properly + return Character.valueOf(((JSONString) jsonValue).stringValue() + .charAt(0)); + } else if (Connector.class.getName().equals(baseTypeName)) { + return ConnectorMap.get(connection).getConnector( + ((JSONString) jsonValue).stringValue()); + } else { + return decodeObject(type, jsonValue, target, connection); + } + } + + private static Object decodeObject(Type type, JSONValue jsonValue, + Object target, ApplicationConnection connection) { + JSONSerializer<Object> serializer = connection.getSerializerMap() + .getSerializer(type.getBaseTypeName()); + // TODO handle case with no serializer found + // Currently getSerializer throws exception if not found + + if (target != null && serializer instanceof DiffJSONSerializer<?>) { + DiffJSONSerializer<Object> diffSerializer = (DiffJSONSerializer<Object>) serializer; + diffSerializer.update(target, type, jsonValue, connection); + return target; + } else { + Object object = serializer.deserialize(type, jsonValue, connection); + return object; + } + } + + private static Map<Object, Object> decodeMap(Type type, JSONValue jsonMap, + ApplicationConnection connection) { + // Client -> server encodes empty map as an empty array because of + // #8906. Do the same for server -> client to maintain symmetry. + if (jsonMap instanceof JSONArray) { + JSONArray array = (JSONArray) jsonMap; + if (array.size() == 0) { + return new HashMap<Object, Object>(); + } + } + + Type keyType = type.getParameterTypes()[0]; + Type valueType = type.getParameterTypes()[1]; + + if (keyType.getBaseTypeName().equals(String.class.getName())) { + return decodeStringMap(valueType, jsonMap, connection); + } else if (keyType.getBaseTypeName().equals(Connector.class.getName())) { + return decodeConnectorMap(valueType, jsonMap, connection); + } else { + return decodeObjectMap(keyType, valueType, jsonMap, connection); + } + } + + private static Map<Object, Object> decodeObjectMap(Type keyType, + Type valueType, JSONValue jsonValue, + ApplicationConnection connection) { + Map<Object, Object> map = new HashMap<Object, Object>(); + + JSONArray mapArray = (JSONArray) jsonValue; + JSONArray keys = (JSONArray) mapArray.get(0); + JSONArray values = (JSONArray) mapArray.get(1); + + assert (keys.size() == values.size()); + + for (int i = 0; i < keys.size(); i++) { + Object decodedKey = decodeValue(keyType, keys.get(i), null, + connection); + Object decodedValue = decodeValue(valueType, values.get(i), null, + connection); + + map.put(decodedKey, decodedValue); + } + + return map; + } + + private static Map<Object, Object> decodeConnectorMap(Type valueType, + JSONValue jsonValue, ApplicationConnection connection) { + Map<Object, Object> map = new HashMap<Object, Object>(); + + JSONObject jsonMap = (JSONObject) jsonValue; + ConnectorMap connectorMap = ConnectorMap.get(connection); + + for (String connectorId : jsonMap.keySet()) { + Object value = decodeValue(valueType, jsonMap.get(connectorId), + null, connection); + map.put(connectorMap.getConnector(connectorId), value); + } + + return map; + } + + private static Map<Object, Object> decodeStringMap(Type valueType, + JSONValue jsonValue, ApplicationConnection connection) { + Map<Object, Object> map = new HashMap<Object, Object>(); + + JSONObject jsonMap = (JSONObject) jsonValue; + + for (String key : jsonMap.keySet()) { + Object value = decodeValue(valueType, jsonMap.get(key), null, + connection); + map.put(key, value); + } + + return map; + } + + private static List<Object> decodeList(Type type, JSONArray jsonArray, + ApplicationConnection connection) { + List<Object> tokens = new ArrayList<Object>(); + decodeIntoCollection(type.getParameterTypes()[0], jsonArray, + connection, tokens); + return tokens; + } + + private static Set<Object> decodeSet(Type type, JSONArray jsonArray, + ApplicationConnection connection) { + Set<Object> tokens = new HashSet<Object>(); + decodeIntoCollection(type.getParameterTypes()[0], jsonArray, + connection, tokens); + return tokens; + } + + private static void decodeIntoCollection(Type childType, + JSONArray jsonArray, ApplicationConnection connection, + Collection<Object> tokens) { + for (int i = 0; i < jsonArray.size(); ++i) { + // each entry always has two elements: type and value + JSONValue entryValue = jsonArray.get(i); + tokens.add(decodeValue(childType, entryValue, null, connection)); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java b/client/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java new file mode 100644 index 0000000000..3730cad4c3 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java @@ -0,0 +1,285 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONBoolean; +import com.google.gwt.json.client.JSONNull; +import com.google.gwt.json.client.JSONNumber; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONString; +import com.google.gwt.json.client.JSONValue; +import com.vaadin.shared.Connector; +import com.vaadin.shared.JsonConstants; +import com.vaadin.shared.communication.UidlValue; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +/** + * Encoder for converting RPC parameters and other values to JSON for transfer + * between the client and the server. + * + * Currently, basic data types as well as Map, String[] and Object[] are + * supported, where maps and Object[] can contain other supported data types. + * + * TODO extensible type support + * + * @since 7.0 + */ +public class JsonEncoder { + + /** + * Encode a value to a JSON representation for transport from the client to + * the server. + * + * @param value + * value to convert + * @param connection + * @return JSON representation of the value + */ + public static JSONValue encode(Object value, + boolean restrictToInternalTypes, ApplicationConnection connection) { + if (null == value) { + return JSONNull.getInstance(); + } else if (value instanceof JSONValue) { + return (JSONValue) value; + } else if (value instanceof String[]) { + String[] array = (String[]) value; + JSONArray jsonArray = new JSONArray(); + for (int i = 0; i < array.length; ++i) { + jsonArray.set(i, new JSONString(array[i])); + } + return jsonArray; + } else if (value instanceof String) { + return new JSONString((String) value); + } else if (value instanceof Boolean) { + return JSONBoolean.getInstance((Boolean) value); + } else if (value instanceof Byte) { + return new JSONNumber((Byte) value); + } else if (value instanceof Character) { + return new JSONString(String.valueOf(value)); + } else if (value instanceof Object[]) { + return encodeObjectArray((Object[]) value, restrictToInternalTypes, + connection); + } else if (value instanceof Enum) { + return encodeEnum((Enum<?>) value, connection); + } else if (value instanceof Map) { + return encodeMap((Map) value, restrictToInternalTypes, connection); + } else if (value instanceof Connector) { + Connector connector = (Connector) value; + return new JSONString(connector.getConnectorId()); + } else if (value instanceof Collection) { + return encodeCollection((Collection) value, + restrictToInternalTypes, connection); + } else if (value instanceof UidlValue) { + return encodeVariableChange((UidlValue) value, connection); + } else { + String transportType = getTransportType(value); + if (transportType != null) { + return new JSONString(String.valueOf(value)); + } else { + // Try to find a generated serializer object, class name is the + // type + transportType = value.getClass().getName(); + JSONSerializer serializer = connection.getSerializerMap() + .getSerializer(transportType); + + // TODO handle case with no serializer found + return serializer.serialize(value, connection); + } + } + } + + private static JSONValue encodeVariableChange(UidlValue uidlValue, + ApplicationConnection connection) { + Object value = uidlValue.getValue(); + + JSONArray jsonArray = new JSONArray(); + jsonArray.set(0, new JSONString(getTransportType(value))); + jsonArray.set(1, encode(value, true, connection)); + + return jsonArray; + } + + private static JSONValue encodeMap(Map<Object, Object> map, + boolean restrictToInternalTypes, ApplicationConnection connection) { + /* + * As we have no info about declared types, we instead select encoding + * scheme based on actual type of first key. We can't do this if there's + * no first key, so instead we send some special value that the + * server-side decoding must check for. (see #8906) + */ + if (map.isEmpty()) { + return new JSONArray(); + } + + Object firstKey = map.keySet().iterator().next(); + if (firstKey instanceof String) { + return encodeStringMap(map, restrictToInternalTypes, connection); + } else if (restrictToInternalTypes) { + throw new IllegalStateException( + "Only string keys supported for legacy maps"); + } else if (firstKey instanceof Connector) { + return encodeConenctorMap(map, connection); + } else { + return encodeObjectMap(map, connection); + } + } + + private static JSONValue encodeObjectMap(Map<Object, Object> map, + ApplicationConnection connection) { + JSONArray keys = new JSONArray(); + JSONArray values = new JSONArray(); + for (Entry<?, ?> entry : map.entrySet()) { + // restrictToInternalTypes always false if we end up here + keys.set(keys.size(), encode(entry.getKey(), false, connection)); + values.set(values.size(), + encode(entry.getValue(), false, connection)); + } + + JSONArray keysAndValues = new JSONArray(); + keysAndValues.set(0, keys); + keysAndValues.set(1, values); + + return keysAndValues; + } + + private static JSONValue encodeConenctorMap(Map<Object, Object> map, + ApplicationConnection connection) { + JSONObject jsonMap = new JSONObject(); + + for (Entry<?, ?> entry : map.entrySet()) { + Connector connector = (Connector) entry.getKey(); + + // restrictToInternalTypes always false if we end up here + JSONValue encodedValue = encode(entry.getValue(), false, connection); + + jsonMap.put(connector.getConnectorId(), encodedValue); + } + + return jsonMap; + } + + private static JSONValue encodeStringMap(Map<Object, Object> map, + boolean restrictToInternalTypes, ApplicationConnection connection) { + JSONObject jsonMap = new JSONObject(); + + for (Entry<?, ?> entry : map.entrySet()) { + String key = (String) entry.getKey(); + Object value = entry.getValue(); + + if (restrictToInternalTypes) { + value = new UidlValue(value); + } + + JSONValue encodedValue = encode(value, restrictToInternalTypes, + connection); + + jsonMap.put(key, encodedValue); + } + + return jsonMap; + } + + private static JSONValue encodeEnum(Enum<?> e, + ApplicationConnection connection) { + return new JSONString(e.toString()); + } + + private static JSONValue encodeObjectArray(Object[] array, + boolean restrictToInternalTypes, ApplicationConnection connection) { + JSONArray jsonArray = new JSONArray(); + for (int i = 0; i < array.length; ++i) { + // TODO handle object graph loops? + Object value = array[i]; + if (restrictToInternalTypes) { + value = new UidlValue(value); + } + jsonArray + .set(i, encode(value, restrictToInternalTypes, connection)); + } + return jsonArray; + } + + private static JSONValue encodeCollection(Collection collection, + boolean restrictToInternalTypes, ApplicationConnection connection) { + JSONArray jsonArray = new JSONArray(); + int idx = 0; + for (Object o : collection) { + JSONValue encodedObject = encode(o, restrictToInternalTypes, + connection); + jsonArray.set(idx++, encodedObject); + } + if (collection instanceof Set) { + return jsonArray; + } else if (collection instanceof List) { + return jsonArray; + } else { + throw new RuntimeException("Unsupport collection type: " + + collection.getClass().getName()); + } + + } + + /** + * Returns the transport type for the given value. Only returns a transport + * type for internally handled values. + * + * @param value + * The value that should be transported + * @return One of the JsonEncode.VTYPE_ constants or null if the value + * cannot be transported using an internally handled type. + */ + private static String getTransportType(Object value) { + if (value == null) { + return JsonConstants.VTYPE_NULL; + } else if (value instanceof String) { + return JsonConstants.VTYPE_STRING; + } else if (value instanceof Connector) { + return JsonConstants.VTYPE_CONNECTOR; + } else if (value instanceof Boolean) { + return JsonConstants.VTYPE_BOOLEAN; + } else if (value instanceof Integer) { + return JsonConstants.VTYPE_INTEGER; + } else if (value instanceof Float) { + return JsonConstants.VTYPE_FLOAT; + } else if (value instanceof Double) { + return JsonConstants.VTYPE_DOUBLE; + } else if (value instanceof Long) { + return JsonConstants.VTYPE_LONG; + } else if (value instanceof List) { + return JsonConstants.VTYPE_LIST; + } else if (value instanceof Set) { + return JsonConstants.VTYPE_SET; + } else if (value instanceof String[]) { + return JsonConstants.VTYPE_STRINGARRAY; + } else if (value instanceof Object[]) { + return JsonConstants.VTYPE_ARRAY; + } else if (value instanceof Map) { + return JsonConstants.VTYPE_MAP; + } else if (value instanceof Enum<?>) { + // Enum value is processed as a string + return JsonConstants.VTYPE_STRING; + } + return null; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/RpcManager.java b/client/src/com/vaadin/terminal/gwt/client/communication/RpcManager.java new file mode 100644 index 0000000000..04d0e3f56f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/RpcManager.java @@ -0,0 +1,136 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONString; +import com.vaadin.shared.communication.ClientRpc; +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.ServerConnector; +import com.vaadin.terminal.gwt.client.VConsole; + +/** + * Client side RPC manager that can invoke methods based on RPC calls received + * from the server. + * + * A GWT generator is used to create an implementation of this class at + * run-time. + * + * @since 7.0 + */ +public class RpcManager { + + private final Map<String, RpcMethod> methodMap = new HashMap<String, RpcMethod>(); + + public RpcManager() { + GeneratedRpcMethodProvider provider = GWT + .create(GeneratedRpcMethodProvider.class); + Collection<RpcMethod> methods = provider.getGeneratedRpcMethods(); + for (RpcMethod rpcMethod : methods) { + methodMap.put( + rpcMethod.getInterfaceName() + "." + + rpcMethod.getMethodName(), rpcMethod); + } + } + + /** + * Perform server to client RPC invocation. + * + * @param invocation + * method to invoke + */ + public void applyInvocation(MethodInvocation invocation, + ServerConnector connector) { + String signature = getSignature(invocation); + + RpcMethod rpcMethod = getRpcMethod(signature); + Collection<ClientRpc> implementations = connector + .getRpcImplementations(invocation.getInterfaceName()); + for (ClientRpc clientRpc : implementations) { + rpcMethod.applyInvocation(clientRpc, invocation.getParameters()); + } + } + + private RpcMethod getRpcMethod(String signature) { + RpcMethod rpcMethod = methodMap.get(signature); + if (rpcMethod == null) { + throw new IllegalStateException("There is no information about " + + signature + + ". Did you remember to compile the right widgetset?"); + } + return rpcMethod; + } + + private static String getSignature(MethodInvocation invocation) { + return invocation.getInterfaceName() + "." + invocation.getMethodName(); + } + + public Type[] getParameterTypes(MethodInvocation invocation) { + return getRpcMethod(getSignature(invocation)).getParameterTypes(); + } + + public void parseAndApplyInvocation(JSONArray rpcCall, + ApplicationConnection connection) { + ConnectorMap connectorMap = ConnectorMap.get(connection); + + String connectorId = ((JSONString) rpcCall.get(0)).stringValue(); + String interfaceName = ((JSONString) rpcCall.get(1)).stringValue(); + String methodName = ((JSONString) rpcCall.get(2)).stringValue(); + JSONArray parametersJson = (JSONArray) rpcCall.get(3); + + ServerConnector connector = connectorMap.getConnector(connectorId); + + MethodInvocation invocation = new MethodInvocation(connectorId, + interfaceName, methodName); + if (connector instanceof HasJavaScriptConnectorHelper) { + ((HasJavaScriptConnectorHelper) connector) + .getJavascriptConnectorHelper().invokeJsRpc(invocation, + parametersJson); + } else { + if (connector == null) { + throw new IllegalStateException("Target connector (" + + connector + ") not found for RCC to " + + getSignature(invocation)); + } + + parseMethodParameters(invocation, parametersJson, connection); + VConsole.log("Server to client RPC call: " + invocation); + applyInvocation(invocation, connector); + } + } + + private void parseMethodParameters(MethodInvocation methodInvocation, + JSONArray parametersJson, ApplicationConnection connection) { + Type[] parameterTypes = getParameterTypes(methodInvocation); + + Object[] parameters = new Object[parametersJson.size()]; + for (int j = 0; j < parametersJson.size(); ++j) { + parameters[j] = JsonDecoder.decodeValue(parameterTypes[j], + parametersJson.get(j), null, connection); + } + + methodInvocation.setParameters(parameters); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/RpcMethod.java b/client/src/com/vaadin/terminal/gwt/client/communication/RpcMethod.java new file mode 100644 index 0000000000..a47fa5eab2 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/RpcMethod.java @@ -0,0 +1,46 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import com.vaadin.shared.communication.ClientRpc; + +public abstract class RpcMethod { + private String interfaceName; + private String methodName; + private Type[] parameterTypes; + + public RpcMethod(String interfaceName, String methodName, + Type... parameterTypes) { + this.interfaceName = interfaceName; + this.methodName = methodName; + this.parameterTypes = parameterTypes; + } + + public String getInterfaceName() { + return interfaceName; + } + + public String getMethodName() { + return methodName; + } + + public Type[] getParameterTypes() { + return parameterTypes; + } + + public abstract void applyInvocation(ClientRpc target, Object... parameters); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/RpcProxy.java b/client/src/com/vaadin/terminal/gwt/client/communication/RpcProxy.java new file mode 100644 index 0000000000..226594adc6 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/RpcProxy.java @@ -0,0 +1,50 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import com.google.gwt.core.client.GWT; +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.terminal.gwt.client.ServerConnector; + +/** + * Class for creating proxy instances for Client to Server RPC. + * + * @since 7.0 + */ +public class RpcProxy { + + private static RpcProxyCreator impl = GWT.create(RpcProxyCreator.class); + + /** + * Create a proxy class for the given Rpc interface and assign it to the + * given connector. + * + * @param rpcInterface + * The rpc interface to construct a proxy for + * @param connector + * The connector this proxy is connected to + * @return A proxy class used for calling Rpc methods. + */ + public static <T extends ServerRpc> T create(Class<T> rpcInterface, + ServerConnector connector) { + return impl.create(rpcInterface, connector); + } + + public interface RpcProxyCreator { + <T extends ServerRpc> T create(Class<T> rpcInterface, + ServerConnector connector); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/SerializerMap.java b/client/src/com/vaadin/terminal/gwt/client/communication/SerializerMap.java new file mode 100644 index 0000000000..77df4c7b08 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/SerializerMap.java @@ -0,0 +1,44 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +/** + * Provide a mapping from a type (communicated between the server and the + * client) and a {@link JSONSerializer} instance. + * + * An implementation of this class is created at GWT compilation time by + * SerializerMapGenerator, so this interface can be instantiated with + * GWT.create(). + * + * @since 7.0 + */ +public interface SerializerMap { + + /** + * Returns a serializer instance for a given type. + * + * @param type + * type communicated on between the server and the client + * (currently fully qualified class name) + * @return serializer instance, not null + * @throws RuntimeException + * if no serializer is found + */ + // TODO better error handling in javadoc and in generator + public JSONSerializer getSerializer(String type); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/StateChangeEvent.java b/client/src/com/vaadin/terminal/gwt/client/communication/StateChangeEvent.java new file mode 100644 index 0000000000..e1847bdab7 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/StateChangeEvent.java @@ -0,0 +1,46 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import java.io.Serializable; + +import com.google.gwt.event.shared.EventHandler; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent.StateChangeHandler; + +public class StateChangeEvent extends + AbstractServerConnectorEvent<StateChangeHandler> { + /** + * Type of this event, used by the event bus. + */ + public static final Type<StateChangeHandler> TYPE = new Type<StateChangeHandler>(); + + @Override + public Type<StateChangeHandler> getAssociatedType() { + return TYPE; + } + + public StateChangeEvent() { + } + + @Override + public void dispatch(StateChangeHandler listener) { + listener.onStateChanged(this); + } + + public interface StateChangeHandler extends Serializable, EventHandler { + public void onStateChanged(StateChangeEvent stateChangeEvent); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/Type.java b/client/src/com/vaadin/terminal/gwt/client/communication/Type.java new file mode 100644 index 0000000000..ff93234a1d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/Type.java @@ -0,0 +1,52 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +public class Type { + private final String baseTypeName; + private final Type[] parameterTypes; + + public Type(String baseTypeName, Type[] parameterTypes) { + this.baseTypeName = baseTypeName; + this.parameterTypes = parameterTypes; + } + + public String getBaseTypeName() { + return baseTypeName; + } + + public Type[] getParameterTypes() { + return parameterTypes; + } + + @Override + public String toString() { + String string = baseTypeName; + if (parameterTypes != null) { + string += '<'; + for (int i = 0; i < parameterTypes.length; i++) { + if (i != 0) { + string += ','; + } + string += parameterTypes[i].toString(); + } + string += '>'; + } + + return string; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/URLReference_Serializer.java b/client/src/com/vaadin/terminal/gwt/client/communication/URLReference_Serializer.java new file mode 100644 index 0000000000..f77553d3c0 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/communication/URLReference_Serializer.java @@ -0,0 +1,53 @@ +/* + * Copyright 2011 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.terminal.gwt.client.communication; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONValue; +import com.vaadin.shared.communication.URLReference; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +public class URLReference_Serializer implements JSONSerializer<URLReference> { + + // setURL() -> uRL as first char becomes lower case... + private static final String URL_FIELD = "uRL"; + + @Override + public URLReference deserialize(Type type, JSONValue jsonValue, + ApplicationConnection connection) { + URLReference reference = GWT.create(URLReference.class); + JSONObject json = (JSONObject) jsonValue; + if (json.containsKey(URL_FIELD)) { + JSONValue jsonURL = json.get(URL_FIELD); + String URL = (String) JsonDecoder.decodeValue( + new Type(String.class.getName(), null), jsonURL, null, + connection); + reference.setURL(connection.translateVaadinUri(URL)); + } + return reference; + } + + @Override + public JSONValue serialize(URLReference value, + ApplicationConnection connection) { + JSONObject json = new JSONObject(); + json.put(URL_FIELD, + JsonEncoder.encode(value.getURL(), true, connection)); + return json; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/extensions/AbstractExtensionConnector.java b/client/src/com/vaadin/terminal/gwt/client/extensions/AbstractExtensionConnector.java new file mode 100644 index 0000000000..dabbfe22f0 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/extensions/AbstractExtensionConnector.java @@ -0,0 +1,48 @@ +/* + * Copyright 2011 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.terminal.gwt.client.extensions; + +import com.vaadin.terminal.gwt.client.ServerConnector; +import com.vaadin.terminal.gwt.client.ui.AbstractConnector; + +public abstract class AbstractExtensionConnector extends AbstractConnector { + boolean hasBeenAttached = false; + + @Override + public void setParent(ServerConnector parent) { + ServerConnector oldParent = getParent(); + if (oldParent == parent) { + // Nothing to do + return; + } + if (hasBeenAttached && parent != null) { + throw new IllegalStateException( + "An extension can not be moved from one parent to another."); + } + + super.setParent(parent); + + if (parent != null) { + extend(parent); + hasBeenAttached = true; + } + } + + protected void extend(ServerConnector target) { + // Default does nothing + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/extensions/javascriptmanager/JavaScriptManagerConnector.java b/client/src/com/vaadin/terminal/gwt/client/extensions/javascriptmanager/JavaScriptManagerConnector.java new file mode 100644 index 0000000000..5cc5911bb1 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/extensions/javascriptmanager/JavaScriptManagerConnector.java @@ -0,0 +1,134 @@ +/* + * Copyright 2011 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.terminal.gwt.client.extensions.javascriptmanager; + +import java.util.HashSet; +import java.util.Set; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArray; +import com.google.gwt.json.client.JSONArray; +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.shared.extension.javascriptmanager.ExecuteJavaScriptRpc; +import com.vaadin.shared.extension.javascriptmanager.JavaScriptManagerState; +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.extensions.AbstractExtensionConnector; +import com.vaadin.ui.JavaScript; + +@Connect(JavaScript.class) +public class JavaScriptManagerConnector extends AbstractExtensionConnector { + private Set<String> currentNames = new HashSet<String>(); + + @Override + protected void init() { + registerRpc(ExecuteJavaScriptRpc.class, new ExecuteJavaScriptRpc() { + @Override + public void executeJavaScript(String Script) { + eval(Script); + } + }); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + Set<String> newNames = getState().getNames(); + + // Current names now only contains orphan callbacks + currentNames.removeAll(newNames); + + for (String name : currentNames) { + removeCallback(name); + } + + currentNames = new HashSet<String>(newNames); + for (String name : newNames) { + addCallback(name); + } + } + + // TODO Ensure we don't overwrite anything (important) in $wnd + private native void addCallback(String name) + /*-{ + var m = this; + var target = $wnd; + var parts = name.split('.'); + + for(var i = 0; i < parts.length - 1; i++) { + var part = parts[i]; + if (target[part] === undefined) { + target[part] = {}; + } + target = target[part]; + } + + target[parts[parts.length - 1]] = $entry(function() { + //Must make a copy because arguments is an array-like object (not instanceof Array), causing suboptimal JSON encoding + var args = Array.prototype.slice.call(arguments, 0); + m.@com.vaadin.terminal.gwt.client.extensions.javascriptmanager.JavaScriptManagerConnector::sendRpc(Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(name, args); + }); + }-*/; + + // TODO only remove what we actually added + // TODO We might leave empty objects behind, but there's no good way of + // knowing whether they are unused + private native void removeCallback(String name) + /*-{ + var target = $wnd; + var parts = name.split('.'); + + for(var i = 0; i < parts.length - 1; i++) { + var part = parts[i]; + if (target[part] === undefined) { + $wnd.console.log(part,'not defined in',target); + // No longer attached -> nothing more to do + return; + } + target = target[part]; + } + + $wnd.console.log('removing',parts[parts.length - 1],'from',target); + delete target[parts[parts.length - 1]]; + }-*/; + + private static native void eval(String script) + /*-{ + if(script) { + (new $wnd.Function(script)).apply($wnd); + } + }-*/; + + public void sendRpc(String name, JsArray<JavaScriptObject> arguments) { + Object[] parameters = new Object[] { name, new JSONArray(arguments) }; + + /* + * Must invoke manually as the RPC interface can't be used in GWT + * because of the JSONArray parameter + */ + getConnection().addMethodInvocationToQueue( + new MethodInvocation(getConnectorId(), + "com.vaadin.ui.JavaScript$JavaScriptCallbackRpc", + "call", parameters), true); + } + + @Override + public JavaScriptManagerState getState() { + return (JavaScriptManagerState) super.getState(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractClickEventHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractClickEventHandler.java new file mode 100644 index 0000000000..e7060f1c6d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractClickEventHandler.java @@ -0,0 +1,246 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.event.dom.client.ContextMenuHandler; +import com.google.gwt.event.dom.client.DomEvent; +import com.google.gwt.event.dom.client.DoubleClickEvent; +import com.google.gwt.event.dom.client.DoubleClickHandler; +import com.google.gwt.event.dom.client.MouseDownEvent; +import com.google.gwt.event.dom.client.MouseDownHandler; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; +import com.google.gwt.event.shared.EventHandler; +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.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.Util; + +public abstract class AbstractClickEventHandler implements MouseDownHandler, + MouseUpHandler, DoubleClickHandler, ContextMenuHandler { + + private HandlerRegistration mouseDownHandlerRegistration; + private HandlerRegistration mouseUpHandlerRegistration; + private HandlerRegistration doubleClickHandlerRegistration; + private HandlerRegistration contextMenuHandlerRegistration; + + protected ComponentConnector connector; + private String clickEventIdentifier; + + /** + * The element where the last mouse down event was registered. + */ + private JavaScriptObject lastMouseDownTarget; + + /** + * Set to true by {@link #mouseUpPreviewHandler} if it gets a mouseup at the + * same element as {@link #lastMouseDownTarget}. + */ + private boolean mouseUpPreviewMatched = false; + + private HandlerRegistration mouseUpEventPreviewRegistration; + + /** + * Previews events after a mousedown to detect where the following mouseup + * hits. + */ + private final NativePreviewHandler mouseUpPreviewHandler = new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + if (event.getTypeInt() == Event.ONMOUSEUP) { + mouseUpEventPreviewRegistration.removeHandler(); + + // Event's reported target not always correct if event + // capture is in use + Element elementUnderMouse = Util.getElementUnderMouse(event + .getNativeEvent()); + if (lastMouseDownTarget != null + && elementUnderMouse.cast() == lastMouseDownTarget) { + mouseUpPreviewMatched = true; + } else { + System.out.println("Ignoring mouseup from " + + elementUnderMouse + " when mousedown was on " + + lastMouseDownTarget); + } + } + } + }; + + public AbstractClickEventHandler(ComponentConnector connector, + String clickEventIdentifier) { + this.connector = connector; + this.clickEventIdentifier = clickEventIdentifier; + } + + public void handleEventHandlerRegistration() { + // Handle registering/unregistering of click handler depending on if + // server side listeners have been added or removed. + if (hasEventListener()) { + if (mouseDownHandlerRegistration == null) { + mouseDownHandlerRegistration = registerHandler(this, + MouseDownEvent.getType()); + mouseUpHandlerRegistration = registerHandler(this, + MouseUpEvent.getType()); + doubleClickHandlerRegistration = registerHandler(this, + DoubleClickEvent.getType()); + contextMenuHandlerRegistration = registerHandler(this, + ContextMenuEvent.getType()); + } + } else { + if (mouseDownHandlerRegistration != null) { + // Remove existing handlers + mouseDownHandlerRegistration.removeHandler(); + mouseUpHandlerRegistration.removeHandler(); + doubleClickHandlerRegistration.removeHandler(); + contextMenuHandlerRegistration.removeHandler(); + + mouseDownHandlerRegistration = null; + mouseUpHandlerRegistration = null; + doubleClickHandlerRegistration = null; + contextMenuHandlerRegistration = null; + } + } + + } + + /** + * Registers the given handler to the widget so that the necessary events + * are passed to this {@link ClickEventHandler}. + * <p> + * By default registers the handler with the connector root widget. + * </p> + * + * @param <H> + * @param handler + * The handler to register + * @param type + * The type of the handler. + * @return A reference for the registration of the handler. + */ + protected <H extends EventHandler> HandlerRegistration registerHandler( + final H handler, DomEvent.Type<H> type) { + return connector.getWidget().addDomHandler(handler, type); + } + + /** + * Checks if there is a server side event listener registered for clicks + * + * @return true if there is a server side event listener registered, false + * otherwise + */ + public boolean hasEventListener() { + return connector.hasEventListener(clickEventIdentifier); + } + + /** + * Event handler for context menu. Prevents the browser context menu from + * popping up if there is a listener for right clicks. + */ + + @Override + public void onContextMenu(ContextMenuEvent event) { + if (hasEventListener() && shouldFireEvent(event)) { + // Prevent showing the browser's context menu when there is a right + // click listener. + event.preventDefault(); + } + } + + @Override + public void onMouseDown(MouseDownEvent event) { + /* + * When getting a mousedown event, we must detect where the + * corresponding mouseup event if it's on a different part of the page. + */ + lastMouseDownTarget = event.getNativeEvent().getEventTarget(); + mouseUpPreviewMatched = false; + mouseUpEventPreviewRegistration = Event + .addNativePreviewHandler(mouseUpPreviewHandler); + } + + @Override + public void onMouseUp(MouseUpEvent event) { + /* + * Only fire a click if the mouseup hits the same element as the + * corresponding mousedown. This is first checked in the event preview + * but we can't fire the even there as the event might get canceled + * before it gets here. + */ + if (hasEventListener() + && mouseUpPreviewMatched + && lastMouseDownTarget != null + && Util.getElementUnderMouse(event.getNativeEvent()) == lastMouseDownTarget + && shouldFireEvent(event)) { + // "Click" with left, right or middle button + fireClick(event.getNativeEvent()); + } + mouseUpPreviewMatched = false; + lastMouseDownTarget = null; + } + + /** + * Sends the click event based on the given native event. + * + * @param event + * The native event that caused this click event + */ + protected abstract void fireClick(NativeEvent event); + + /** + * Called before firing a click event. Allows sub classes to decide if this + * in an event that should cause an event or not. + * + * @param event + * The user event + * @return true if the event should be fired, false otherwise + */ + protected boolean shouldFireEvent(DomEvent<?> event) { + return true; + } + + /** + * Event handler for double clicks. Used to fire double click events. Note + * that browsers typically fail to prevent the second click event so a + * double click will result in two click events and one double click event. + */ + + @Override + public void onDoubleClick(DoubleClickEvent event) { + if (hasEventListener() && shouldFireEvent(event)) { + fireClick(event.getNativeEvent()); + } + } + + /** + * Click event calculates and returns coordinates relative to the element + * returned by this method. Default implementation uses the root element of + * the widget. Override to provide a different relative element. + * + * @return The Element used for calculating relative coordinates for a click + * or null if no relative coordinates can be calculated. + */ + protected Element getRelativeToElement() { + return connector.getWidget().getElement(); + } + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentConnector.java new file mode 100644 index 0000000000..48842e29a0 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentConnector.java @@ -0,0 +1,419 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.ui.Focusable; +import com.google.gwt.user.client.ui.HasEnabled; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ComponentState; +import com.vaadin.shared.Connector; +import com.vaadin.shared.ui.TabIndexState; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.ServerConnector; +import com.vaadin.terminal.gwt.client.TooltipInfo; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.datefield.PopupDateFieldConnector; +import com.vaadin.terminal.gwt.client.ui.root.RootConnector; + +public abstract class AbstractComponentConnector extends AbstractConnector + implements ComponentConnector { + + private Widget widget; + + private String lastKnownWidth = ""; + private String lastKnownHeight = ""; + + /** + * The style names from getState().getStyles() which are currently applied + * to the widget. + */ + protected List<String> styleNames = new ArrayList<String>(); + + /** + * Default constructor + */ + public AbstractComponentConnector() { + } + + @Override + protected void init() { + super.init(); + + getConnection().getVTooltip().connectHandlersToWidget(getWidget()); + + // Set v-connector style names for the widget + getWidget().setStyleName("v-connector", true); + } + + /** + * Creates and returns the widget for this VPaintableWidget. This method + * should only be called once when initializing the paintable. + * + * @return + */ + protected Widget createWidget() { + return ConnectorWidgetFactory.createWidget(getClass()); + } + + /** + * Returns the widget associated with this paintable. The widget returned by + * this method must not changed during the life time of the paintable. + * + * @return The widget associated with this paintable + */ + @Override + public Widget getWidget() { + if (widget == null) { + widget = createWidget(); + } + + return widget; + } + + @Deprecated + public static boolean isRealUpdate(UIDL uidl) { + return !uidl.hasAttribute("cached"); + } + + @Override + public ComponentState getState() { + return (ComponentState) super.getState(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + ConnectorMap paintableMap = ConnectorMap.get(getConnection()); + + if (getState().getDebugId() != null) { + getWidget().getElement().setId(getState().getDebugId()); + } else { + getWidget().getElement().removeAttribute("id"); + + } + + /* + * Disabled state may affect (override) tabindex so the order must be + * first setting tabindex, then enabled state (through super + * implementation). + */ + if (getState() instanceof TabIndexState + && getWidget() instanceof Focusable) { + ((Focusable) getWidget()).setTabIndex(((TabIndexState) getState()) + .getTabIndex()); + } + + super.onStateChanged(stateChangeEvent); + + // Style names + updateWidgetStyleNames(); + + // Set captions + if (delegateCaptionHandling()) { + ServerConnector parent = getParent(); + if (parent instanceof ComponentContainerConnector) { + ((ComponentContainerConnector) parent).updateCaption(this); + } else if (parent == null && !(this instanceof RootConnector)) { + VConsole.error("Parent of connector " + + Util.getConnectorString(this) + + " is null. This is typically an indication of a broken component hierarchy"); + } + } + + /* + * updateComponentSize need to be after caption update so caption can be + * taken into account + */ + + updateComponentSize(); + } + + @Override + public void setWidgetEnabled(boolean widgetEnabled) { + // add or remove v-disabled style name from the widget + setWidgetStyleName(ApplicationConnection.DISABLED_CLASSNAME, + !widgetEnabled); + + if (getWidget() instanceof HasEnabled) { + // set widget specific enabled state + ((HasEnabled) getWidget()).setEnabled(widgetEnabled); + + // make sure the caption has or has not v-disabled style + if (delegateCaptionHandling()) { + ServerConnector parent = getParent(); + if (parent instanceof ComponentContainerConnector) { + ((ComponentContainerConnector) parent).updateCaption(this); + } else if (parent == null && !(this instanceof RootConnector)) { + VConsole.error("Parent of connector " + + Util.getConnectorString(this) + + " is null. This is typically an indication of a broken component hierarchy"); + } + } + } + } + + private void updateComponentSize() { + String newWidth = getState().getWidth(); + String newHeight = getState().getHeight(); + + // Parent should be updated if either dimension changed between relative + // and non-relative + if (newWidth.endsWith("%") != lastKnownWidth.endsWith("%")) { + Connector parent = getParent(); + if (parent instanceof ManagedLayout) { + getLayoutManager().setNeedsHorizontalLayout( + (ManagedLayout) parent); + } + } + + if (newHeight.endsWith("%") != lastKnownHeight.endsWith("%")) { + Connector parent = getParent(); + if (parent instanceof ManagedLayout) { + getLayoutManager().setNeedsVerticalLayout( + (ManagedLayout) parent); + } + } + + lastKnownWidth = newWidth; + lastKnownHeight = newHeight; + + // Set defined sizes + Widget widget = getWidget(); + + widget.setStyleName("v-has-width", !isUndefinedWidth()); + widget.setStyleName("v-has-height", !isUndefinedHeight()); + + widget.setHeight(newHeight); + widget.setWidth(newWidth); + } + + @Override + public boolean isRelativeHeight() { + return getState().getHeight().endsWith("%"); + } + + @Override + public boolean isRelativeWidth() { + return getState().getWidth().endsWith("%"); + } + + @Override + public boolean isUndefinedHeight() { + return getState().getHeight().length() == 0; + } + + @Override + public boolean isUndefinedWidth() { + return getState().getWidth().length() == 0; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ComponentConnector#delegateCaptionHandling + * () + */ + @Override + public boolean delegateCaptionHandling() { + return true; + } + + /** + * Updates the user defined, read-only and error style names for the widget + * based the shared state. User defined style names are prefixed with the + * primary style name of the widget returned by {@link #getWidget()} + * <p> + * This method can be overridden to provide additional style names for the + * component, for example see + * {@link AbstractFieldConnector#updateWidgetStyleNames()} + * </p> + */ + protected void updateWidgetStyleNames() { + ComponentState state = getState(); + + String primaryStyleName = getWidget().getStylePrimaryName(); + + // should be in AbstractFieldConnector ? + // add / remove read-only style name + setWidgetStyleName("v-readonly", isReadOnly()); + + // add / remove error style name + setWidgetStyleNameWithPrefix(primaryStyleName, + ApplicationConnection.ERROR_CLASSNAME_EXT, + null != state.getErrorMessage()); + + // add additional user defined style names as class names, prefixed with + // component default class name. remove nonexistent style names. + if (state.hasStyles()) { + // add new style names + List<String> newStyles = new ArrayList<String>(); + newStyles.addAll(state.getStyles()); + newStyles.removeAll(styleNames); + for (String newStyle : newStyles) { + setWidgetStyleName(newStyle, true); + setWidgetStyleNameWithPrefix(primaryStyleName + "-", newStyle, + true); + } + // remove nonexistent style names + styleNames.removeAll(state.getStyles()); + for (String oldStyle : styleNames) { + setWidgetStyleName(oldStyle, false); + setWidgetStyleNameWithPrefix(primaryStyleName + "-", oldStyle, + false); + } + styleNames.clear(); + styleNames.addAll(state.getStyles()); + } else { + // remove all old style names + for (String oldStyle : styleNames) { + setWidgetStyleName(oldStyle, false); + setWidgetStyleNameWithPrefix(primaryStyleName + "-", oldStyle, + false); + } + styleNames.clear(); + } + + } + + /** + * This is used to add / remove state related style names from the widget. + * <p> + * Override this method for example if the style name given here should be + * updated in another widget in addition to the one returned by the + * {@link #getWidget()}. + * </p> + * + * @param styleName + * the style name to be added or removed + * @param add + * <code>true</code> to add the given style, <code>false</code> + * to remove it + */ + protected void setWidgetStyleName(String styleName, boolean add) { + getWidget().setStyleName(styleName, add); + } + + /** + * This is used to add / remove state related prefixed style names from the + * widget. + * <p> + * Override this method if the prefixed style name given here should be + * updated in another widget in addition to the one returned by the + * <code>Connector</code>'s {@link #getWidget()}, or if the prefix should be + * different. For example see + * {@link PopupDateFieldConnector#setWidgetStyleNameWithPrefix(String, String, boolean)} + * </p> + * + * @param styleName + * the style name to be added or removed + * @param add + * <code>true</code> to add the given style, <code>false</code> + * to remove it + * @deprecated This will be removed once styles are no longer added with + * prefixes. + */ + @Deprecated + protected void setWidgetStyleNameWithPrefix(String prefix, + String styleName, boolean add) { + if (!styleName.startsWith("-")) { + if (!prefix.endsWith("-")) { + prefix += "-"; + } + } else { + if (prefix.endsWith("-")) { + styleName.replaceFirst("-", ""); + } + } + getWidget().setStyleName(prefix + styleName, add); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ComponentConnector#isReadOnly() + */ + @Override + @Deprecated + public boolean isReadOnly() { + return getState().isReadOnly(); + } + + @Override + public LayoutManager getLayoutManager() { + return LayoutManager.get(getConnection()); + } + + /** + * Checks if there is a registered server side listener for the given event + * identifier. + * + * @param eventIdentifier + * The identifier to check for + * @return true if an event listener has been registered with the given + * event identifier on the server side, false otherwise + */ + @Override + public boolean hasEventListener(String eventIdentifier) { + Set<String> reg = getState().getRegisteredEventListeners(); + return (reg != null && reg.contains(eventIdentifier)); + } + + @Override + public void updateEnabledState(boolean enabledState) { + super.updateEnabledState(enabledState); + + setWidgetEnabled(isEnabled()); + } + + @Override + public void onUnregister() { + super.onUnregister(); + + // Show an error if widget is still attached to DOM. It should never be + // at this point. + if (getWidget() != null && getWidget().isAttached()) { + getWidget().removeFromParent(); + VConsole.error("Widget is still attached to the DOM after the connector (" + + Util.getConnectorString(this) + + ") has been unregistered. Widget was removed."); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ComponentConnector#getTooltipInfo(com. + * google.gwt.dom.client.Element) + */ + @Override + public TooltipInfo getTooltipInfo(Element element) { + return new TooltipInfo(getState().getDescription(), getState() + .getErrorMessage()); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentContainerConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentContainerConnector.java new file mode 100644 index 0000000000..16eab60a75 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentContainerConnector.java @@ -0,0 +1,103 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import java.util.Collections; +import java.util.List; + +import com.google.gwt.event.shared.HandlerRegistration; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; + +public abstract class AbstractComponentContainerConnector extends + AbstractComponentConnector implements ComponentContainerConnector, + ConnectorHierarchyChangeHandler { + + List<ComponentConnector> childComponents; + + private final boolean debugLogging = false; + + /** + * Default constructor + */ + public AbstractComponentContainerConnector() { + addConnectorHierarchyChangeHandler(this); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ComponentContainerConnector#getChildren() + */ + @Override + public List<ComponentConnector> getChildComponents() { + if (childComponents == null) { + return Collections.emptyList(); + } + + return childComponents; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ComponentContainerConnector#setChildren + * (java.util.Collection) + */ + @Override + public void setChildComponents(List<ComponentConnector> childComponents) { + this.childComponents = childComponents; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ComponentContainerConnector# + * connectorHierarchyChanged + * (com.vaadin.terminal.gwt.client.ConnectorHierarchyChangedEvent) + */ + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + if (debugLogging) { + VConsole.log("Hierarchy changed for " + + Util.getConnectorString(this)); + String oldChildren = "* Old children: "; + for (ComponentConnector child : event.getOldChildren()) { + oldChildren += Util.getConnectorString(child) + " "; + } + VConsole.log(oldChildren); + + String newChildren = "* New children: "; + for (ComponentConnector child : getChildComponents()) { + newChildren += Util.getConnectorString(child) + " "; + } + VConsole.log(newChildren); + } + } + + @Override + public HandlerRegistration addConnectorHierarchyChangeHandler( + ConnectorHierarchyChangeHandler handler) { + return ensureHandlerManager().addHandler( + ConnectorHierarchyChangeEvent.TYPE, handler); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractConnector.java new file mode 100644 index 0000000000..514f63fdd8 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractConnector.java @@ -0,0 +1,289 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gwt.event.shared.GwtEvent; +import com.google.gwt.event.shared.HandlerManager; +import com.google.web.bindery.event.shared.HandlerRegistration; +import com.vaadin.shared.communication.ClientRpc; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ServerConnector; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent.StateChangeHandler; + +/** + * An abstract implementation of Connector. + * + * @author Vaadin Ltd + * @since 7.0.0 + * + */ +public abstract class AbstractConnector implements ServerConnector, + StateChangeHandler { + + private ApplicationConnection connection; + private String id; + + private HandlerManager handlerManager; + private Map<String, Collection<ClientRpc>> rpcImplementations; + private final boolean debugLogging = false; + + private SharedState state; + private ServerConnector parent; + + /** + * Temporary storage for last enabled state to be able to see if it has + * changed. Can be removed once we are able to listen specifically for + * enabled changes in the state. Widget.isEnabled() cannot be used as all + * Widgets do not implement HasEnabled + */ + private boolean lastEnabledState = true; + private List<ServerConnector> children; + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.VPaintable#getConnection() + */ + @Override + public final ApplicationConnection getConnection() { + return connection; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Connector#getId() + */ + @Override + public String getConnectorId() { + return id; + } + + /** + * Called once by the framework to initialize the connector. + * <p> + * Note that the shared state is not yet available when this method is + * called. + * <p> + * Connector classes should override {@link #init()} instead of this method. + */ + @Override + public final void doInit(String connectorId, + ApplicationConnection connection) { + this.connection = connection; + id = connectorId; + + addStateChangeHandler(this); + init(); + } + + /** + * Called when the connector has been initialized. Override this method to + * perform initialization of the connector. + */ + // FIXME: It might make sense to make this abstract to force users to + // use init instead of constructor, where connection and id has not yet been + // set. + protected void init() { + + } + + /** + * Registers an implementation for a server to client RPC interface. + * + * Multiple registrations can be made for a single interface, in which case + * all of them receive corresponding RPC calls. + * + * @param rpcInterface + * RPC interface + * @param implementation + * implementation that should receive RPC calls + * @param <T> + * The type of the RPC interface that is being registered + */ + protected <T extends ClientRpc> void registerRpc(Class<T> rpcInterface, + T implementation) { + String rpcInterfaceId = rpcInterface.getName().replaceAll("\\$", "."); + if (null == rpcImplementations) { + rpcImplementations = new HashMap<String, Collection<ClientRpc>>(); + } + if (null == rpcImplementations.get(rpcInterfaceId)) { + rpcImplementations.put(rpcInterfaceId, new ArrayList<ClientRpc>()); + } + rpcImplementations.get(rpcInterfaceId).add(implementation); + } + + /** + * Unregisters an implementation for a server to client RPC interface. + * + * @param rpcInterface + * RPC interface + * @param implementation + * implementation to unregister + */ + protected <T extends ClientRpc> void unregisterRpc(Class<T> rpcInterface, + T implementation) { + String rpcInterfaceId = rpcInterface.getName().replaceAll("\\$", "."); + if (null != rpcImplementations + && null != rpcImplementations.get(rpcInterfaceId)) { + rpcImplementations.get(rpcInterfaceId).remove(implementation); + } + } + + @Override + public <T extends ClientRpc> Collection<T> getRpcImplementations( + String rpcInterfaceId) { + if (null == rpcImplementations) { + return Collections.emptyList(); + } + return (Collection<T>) rpcImplementations.get(rpcInterfaceId); + } + + @Override + public void fireEvent(GwtEvent<?> event) { + if (handlerManager != null) { + handlerManager.fireEvent(event); + } + } + + protected HandlerManager ensureHandlerManager() { + if (handlerManager == null) { + handlerManager = new HandlerManager(this); + } + + return handlerManager; + } + + @Override + public HandlerRegistration addStateChangeHandler(StateChangeHandler handler) { + return ensureHandlerManager() + .addHandler(StateChangeEvent.TYPE, handler); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + if (debugLogging) { + VConsole.log("State change event for " + + Util.getConnectorString(stateChangeEvent.getConnector()) + + " received by " + Util.getConnectorString(this)); + } + + updateEnabledState(isEnabled()); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ServerConnector#onUnregister() + */ + @Override + public void onUnregister() { + if (debugLogging) { + VConsole.log("Unregistered connector " + + Util.getConnectorString(this)); + } + + } + + /** + * Returns the shared state object for this connector. + * + * Override this method to define the shared state type for your connector. + * + * @return the current shared state (never null) + */ + @Override + public SharedState getState() { + if (state == null) { + state = createState(); + } + + return state; + } + + /** + * Creates a state object with default values for this connector. The + * created state object must be compatible with the return type of + * {@link #getState()}. The default implementation creates a state object + * using GWT.create() using the defined return type of {@link #getState()}. + * + * @return A new state object + */ + protected SharedState createState() { + return ConnectorStateFactory.createState(getClass()); + } + + @Override + public ServerConnector getParent() { + return parent; + } + + @Override + public void setParent(ServerConnector parent) { + this.parent = parent; + } + + @Override + public List<ServerConnector> getChildren() { + if (children == null) { + return Collections.emptyList(); + } + return children; + } + + @Override + public void setChildren(List<ServerConnector> children) { + this.children = children; + } + + @Override + public boolean isEnabled() { + if (!getState().isEnabled()) { + return false; + } + + if (getParent() == null) { + return true; + } else { + return getParent().isEnabled(); + } + } + + @Override + public void updateEnabledState(boolean enabledState) { + if (lastEnabledState == enabledState) { + return; + } + lastEnabledState = enabledState; + + for (ServerConnector c : getChildren()) { + // Update children as they might be affected by the enabled state of + // their parent + c.updateEnabledState(c.isEnabled()); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractFieldConnector.java new file mode 100644 index 0000000000..c007eb8529 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractFieldConnector.java @@ -0,0 +1,61 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.vaadin.shared.AbstractFieldState; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +public abstract class AbstractFieldConnector extends AbstractComponentConnector { + + @Override + public AbstractFieldState getState() { + return (AbstractFieldState) super.getState(); + } + + @Override + public boolean isReadOnly() { + return super.isReadOnly() || getState().isPropertyReadOnly(); + } + + public boolean isModified() { + return getState().isModified(); + } + + /** + * Checks whether the required indicator should be shown for the field. + * + * Required indicators are hidden if the field or its data source is + * read-only. + * + * @return true if required indicator should be shown + */ + public boolean isRequired() { + return getState().isRequired() && !isReadOnly(); + } + + @Override + protected void updateWidgetStyleNames() { + super.updateWidgetStyleNames(); + + // add / remove modified style name to Fields + setWidgetStyleName(ApplicationConnection.MODIFIED_CLASSNAME, + isModified()); + + // add / remove error style name to Fields + setWidgetStyleNameWithPrefix(getWidget().getStylePrimaryName(), + ApplicationConnection.REQUIRED_CLASSNAME_EXT, isRequired()); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractLayoutConnector.java new file mode 100644 index 0000000000..b8a16d697d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractLayoutConnector.java @@ -0,0 +1,27 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.vaadin.shared.ui.AbstractLayoutState; + +public abstract class AbstractLayoutConnector extends + AbstractComponentContainerConnector { + + @Override + public AbstractLayoutState getState() { + return (AbstractLayoutState) super.getState(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/Action.java b/client/src/com/vaadin/terminal/gwt/client/ui/Action.java new file mode 100644 index 0000000000..b97599c872 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/Action.java @@ -0,0 +1,70 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.user.client.Command; +import com.vaadin.terminal.gwt.client.Util; + +/** + * + */ +public abstract class Action implements Command { + + protected ActionOwner owner; + + protected String iconUrl = null; + + protected String caption = ""; + + public Action(ActionOwner owner) { + this.owner = owner; + } + + /** + * Executed when action fired + */ + @Override + public abstract void execute(); + + public String getHTML() { + final StringBuffer sb = new StringBuffer(); + sb.append("<div>"); + if (getIconUrl() != null) { + sb.append("<img src=\"" + Util.escapeAttribute(getIconUrl()) + + "\" alt=\"icon\" />"); + } + sb.append(getCaption()); + sb.append("</div>"); + return sb.toString(); + } + + public String getCaption() { + return caption; + } + + public void setCaption(String caption) { + this.caption = caption; + } + + public String getIconUrl() { + return iconUrl; + } + + public void setIconUrl(String url) { + iconUrl = url; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java b/client/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java new file mode 100644 index 0000000000..bb714d1081 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java @@ -0,0 +1,31 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +public interface ActionOwner { + + /** + * @return Array of IActions + */ + public Action[] getActions(); + + public ApplicationConnection getClient(); + + public String getPaintableId(); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java b/client/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java new file mode 100644 index 0000000000..72c7bea806 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java @@ -0,0 +1,140 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import java.util.Date; + +import com.vaadin.terminal.gwt.client.DateTimeService; + +public class CalendarEntry { + private final String styleName; + private Date start; + private Date end; + private String title; + private String description; + private boolean notime; + + @SuppressWarnings("deprecation") + public CalendarEntry(String styleName, Date start, Date end, String title, + String description, boolean notime) { + this.styleName = styleName; + if (notime) { + Date d = new Date(start.getTime()); + d.setSeconds(0); + d.setMinutes(0); + this.start = d; + if (end != null) { + d = new Date(end.getTime()); + d.setSeconds(0); + d.setMinutes(0); + this.end = d; + } else { + end = start; + } + } else { + this.start = start; + this.end = end; + } + this.title = title; + this.description = description; + this.notime = notime; + } + + public CalendarEntry(String styleName, Date start, Date end, String title, + String description) { + this(styleName, start, end, title, description, false); + } + + public String getStyleName() { + return styleName; + } + + public Date getStart() { + return start; + } + + public void setStart(Date start) { + this.start = start; + } + + public Date getEnd() { + return end; + } + + public void setEnd(Date end) { + this.end = end; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isNotime() { + return notime; + } + + public void setNotime(boolean notime) { + this.notime = notime; + } + + @SuppressWarnings("deprecation") + public String getStringForDate(Date d) { + // TODO format from DateTimeService + String s = ""; + if (!notime) { + if (!DateTimeService.isSameDay(d, start)) { + s += (start.getYear() + 1900) + "." + (start.getMonth() + 1) + + "." + start.getDate() + " "; + } + int i = start.getHours(); + s += (i < 10 ? "0" : "") + i; + s += ":"; + i = start.getMinutes(); + s += (i < 10 ? "0" : "") + i; + if (!start.equals(end)) { + s += " - "; + if (!DateTimeService.isSameDay(start, end)) { + s += (end.getYear() + 1900) + "." + (end.getMonth() + 1) + + "." + end.getDate() + " "; + } + i = end.getHours(); + s += (i < 10 ? "0" : "") + i; + s += ":"; + i = end.getMinutes(); + s += (i < 10 ? "0" : "") + i; + } + s += " "; + } + if (title != null) { + s += title; + } + return s; + } + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ClickEventHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/ClickEventHandler.java new file mode 100644 index 0000000000..5614837615 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/ClickEventHandler.java @@ -0,0 +1,62 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.dom.client.NativeEvent; +import com.vaadin.shared.EventId; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder; + +public abstract class ClickEventHandler extends AbstractClickEventHandler { + + public ClickEventHandler(ComponentConnector connector) { + this(connector, EventId.CLICK_EVENT_IDENTIFIER); + } + + public ClickEventHandler(ComponentConnector connector, + String clickEventIdentifier) { + super(connector, clickEventIdentifier); + } + + /** + * Sends the click event based on the given native event. Delegates actual + * sending to {@link #fireClick(MouseEventDetails)}. + * + * @param event + * The native event that caused this click event + */ + @Override + protected void fireClick(NativeEvent event) { + MouseEventDetails mouseDetails = MouseEventDetailsBuilder + .buildMouseEventDetails(event, getRelativeToElement()); + fireClick(event, mouseDetails); + } + + /** + * Sends the click event to the server. Must be implemented by sub classes, + * typically by calling an RPC method. + * + * @param event + * The event that caused this click to be fired + * + * @param mouseDetails + * The mouse details for the event + */ + protected abstract void fireClick(NativeEvent event, + MouseEventDetails mouseDetails); + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorClassBasedFactory.java b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorClassBasedFactory.java new file mode 100644 index 0000000000..698d8e6e61 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorClassBasedFactory.java @@ -0,0 +1,52 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.shared.Connector; + +public abstract class ConnectorClassBasedFactory<T> { + public interface Creator<T> { + public T create(); + } + + private Map<Class<? extends Connector>, Creator<? extends T>> creators = new HashMap<Class<? extends Connector>, Creator<? extends T>>(); + + protected void addCreator(Class<? extends Connector> cls, + Creator<? extends T> creator) { + creators.put(cls, creator); + } + + /** + * Creates a widget using GWT.create for the given connector, based on its + * {@link AbstractComponentConnector#getWidget()} return type. + * + * @param connector + * @return + */ + public T create(Class<? extends Connector> connector) { + Creator<? extends T> foo = creators.get(connector); + if (foo == null) { + throw new RuntimeException(getClass().getName() + + " could not find a creator for connector of type " + + connector.getName()); + } + return foo.create(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorStateFactory.java b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorStateFactory.java new file mode 100644 index 0000000000..b04daa6910 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorStateFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.core.client.GWT; +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.SharedState; + +public abstract class ConnectorStateFactory extends + ConnectorClassBasedFactory<SharedState> { + private static ConnectorStateFactory impl = null; + + /** + * Creates a SharedState using GWT.create for the given connector, based on + * its {@link AbstractComponentConnector#getSharedState ()} return type. + * + * @param connector + * @return + */ + public static SharedState createState(Class<? extends Connector> connector) { + return getImpl().create(connector); + } + + private static ConnectorStateFactory getImpl() { + if (impl == null) { + impl = GWT.create(ConnectorStateFactory.class); + } + return impl; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorWidgetFactory.java b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorWidgetFactory.java new file mode 100644 index 0000000000..073e36cabb --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorWidgetFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ui.textfield.TextFieldConnector; +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; + +public abstract class ConnectorWidgetFactory extends + ConnectorClassBasedFactory<Widget> { + private static ConnectorWidgetFactory impl = null; + + // TODO Move to generator + { + addCreator(TextFieldConnector.class, new Creator<Widget>() { + @Override + public Widget create() { + return GWT.create(VTextField.class); + } + }); + } + + /** + * Creates a widget using GWT.create for the given connector, based on its + * {@link AbstractComponentConnector#getWidget()} return type. + * + * @param connector + * @return + */ + public static Widget createWidget( + Class<? extends AbstractComponentConnector> connector) { + return getImpl().create(connector); + } + + private static ConnectorWidgetFactory getImpl() { + if (impl == null) { + impl = GWT.create(ConnectorWidgetFactory.class); + } + return impl; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/Field.java b/client/src/com/vaadin/terminal/gwt/client/ui/Field.java new file mode 100644 index 0000000000..b81e365608 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/Field.java @@ -0,0 +1,28 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +/** + * This interface indicates that the component is a Field (serverside), and + * wants (for instance) to automatically get the v-modified classname. + * + */ +public interface Field { + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/FocusElementPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/FocusElementPanel.java new file mode 100644 index 0000000000..6a86ab5679 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/FocusElementPanel.java @@ -0,0 +1,92 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +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.ui.Widget; +import com.google.gwt.user.client.ui.impl.FocusImpl; + +/** + * A panel that contains an always visible 0x0 size element that holds the focus + */ +public class FocusElementPanel extends SimpleFocusablePanel { + + private DivElement focusElement; + + public FocusElementPanel() { + focusElement = Document.get().createDivElement(); + } + + @Override + public void setWidget(Widget w) { + super.setWidget(w); + if (focusElement.getParentElement() == null) { + Style style = focusElement.getStyle(); + style.setPosition(Position.FIXED); + style.setTop(0, Unit.PX); + style.setLeft(0, Unit.PX); + getElement().appendChild(focusElement); + /* Sink from focusElement too as focus and blur don't bubble */ + DOM.sinkEvents( + (com.google.gwt.user.client.Element) focusElement.cast(), + Event.FOCUSEVENTS); + // revert to original, not focusable + getElement().setPropertyObject("tabIndex", null); + } else { + moveFocusElementAfterWidget(); + } + } + + /** + * Helper to keep focus element always in domChild[1]. Aids testing. + */ + private void moveFocusElementAfterWidget() { + getElement().insertAfter(focusElement, getWidget().getElement()); + } + + @Override + public void setFocus(boolean focus) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus( + (Element) focusElement.cast()); + } else { + FocusImpl.getFocusImplForPanel() + .blur((Element) focusElement.cast()); + } + } + + @Override + public void setTabIndex(int tabIndex) { + getElement().setTabIndex(-1); + if (focusElement != null) { + focusElement.setTabIndex(tabIndex); + } + } + + /** + * @return the focus element + */ + public Element getFocusElement() { + return focusElement.cast(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlexTable.java b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlexTable.java new file mode 100644 index 0000000000..8ad7002a79 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlexTable.java @@ -0,0 +1,122 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.dom.client.HasKeyDownHandlers; +import com.google.gwt.event.dom.client.HasKeyPressHandlers; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.FlexTable; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.terminal.gwt.client.Focusable; + +/** + * Adds keyboard focus to {@link FlexPanel}. + */ +public class FocusableFlexTable extends FlexTable implements HasFocusHandlers, + HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable { + + /** + * Default constructor. + */ + public FocusableFlexTable() { + // make focusable, as we don't need access key magic we don't need to + // use FocusImpl.createFocusable + getElement().setTabIndex(0); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com. + * google.gwt.event.dom.client.FocusHandler) + */ + @Override + public HandlerRegistration addFocusHandler(FocusHandler handler) { + return addDomHandler(handler, FocusEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google + * .gwt.event.dom.client.BlurHandler) + */ + @Override + public HandlerRegistration addBlurHandler(BlurHandler handler) { + return addDomHandler(handler, BlurEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler( + * com.google.gwt.event.dom.client.KeyDownHandler) + */ + @Override + public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { + return addDomHandler(handler, KeyDownEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler + * (com.google.gwt.event.dom.client.KeyPressHandler) + */ + @Override + public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { + return addDomHandler(handler, KeyPressEvent.getType()); + } + + /** + * Sets the keyboard focus to the panel + * + * @param focus + * Should the panel have keyboard focus. If true the keyboard + * focus will be moved to the + */ + public void setFocus(boolean focus) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus(getElement()); + } else { + FocusImpl.getFocusImplForPanel().blur(getElement()); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Focusable#focus() + */ + @Override + public void focus() { + setFocus(true); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlowPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlowPanel.java new file mode 100644 index 0000000000..c162a750ce --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlowPanel.java @@ -0,0 +1,117 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.dom.client.HasKeyDownHandlers; +import com.google.gwt.event.dom.client.HasKeyPressHandlers; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.terminal.gwt.client.Focusable; + +public class FocusableFlowPanel extends FlowPanel implements HasFocusHandlers, + HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable { + + /** + * Constructor + */ + public FocusableFlowPanel() { + // make focusable, as we don't need access key magic we don't need to + // use FocusImpl.createFocusable + getElement().setTabIndex(0); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com. + * google.gwt.event.dom.client.FocusHandler) + */ + @Override + public HandlerRegistration addFocusHandler(FocusHandler handler) { + return addDomHandler(handler, FocusEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google + * .gwt.event.dom.client.BlurHandler) + */ + @Override + public HandlerRegistration addBlurHandler(BlurHandler handler) { + return addDomHandler(handler, BlurEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler( + * com.google.gwt.event.dom.client.KeyDownHandler) + */ + @Override + public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { + return addDomHandler(handler, KeyDownEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler + * (com.google.gwt.event.dom.client.KeyPressHandler) + */ + @Override + public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { + return addDomHandler(handler, KeyPressEvent.getType()); + } + + /** + * Sets/Removes the keyboard focus to the panel. + * + * @param focus + * If set to true then the focus is moved to the panel, if set to + * false the focus is removed + */ + public void setFocus(boolean focus) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus(getElement()); + } else { + FocusImpl.getFocusImplForPanel().blur(getElement()); + } + } + + /** + * Focus the panel + */ + @Override + public void focus() { + setFocus(true); + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java new file mode 100644 index 0000000000..d20b3e9e65 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java @@ -0,0 +1,196 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import java.util.ArrayList; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.HasScrollHandlers; +import com.google.gwt.event.dom.client.ScrollEvent; +import com.google.gwt.event.dom.client.ScrollHandler; +import com.google.gwt.event.shared.HandlerRegistration; +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.ui.ScrollPanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.terminal.gwt.client.BrowserInfo; + +/** + * A scrollhandlers similar to {@link ScrollPanel}. + * + */ +public class FocusableScrollPanel extends SimpleFocusablePanel implements + HasScrollHandlers, ScrollHandler { + + public FocusableScrollPanel() { + // Prevent IE standard mode bug when a AbsolutePanel is contained. + TouchScrollDelegate.enableTouchScrolling(this, getElement()); + Style style = getElement().getStyle(); + style.setProperty("zoom", "1"); + style.setPosition(Position.RELATIVE); + } + + private DivElement focusElement; + + public FocusableScrollPanel(boolean useFakeFocusElement) { + this(); + if (useFakeFocusElement) { + focusElement = Document.get().createDivElement(); + } + } + + private boolean useFakeFocusElement() { + return focusElement != null; + } + + @Override + public void setWidget(Widget w) { + super.setWidget(w); + if (useFakeFocusElement()) { + if (focusElement.getParentElement() == null) { + Style style = focusElement.getStyle(); + style.setPosition(Position.FIXED); + style.setTop(0, Unit.PX); + style.setLeft(0, Unit.PX); + getElement().appendChild(focusElement); + /* Sink from focusElemet too as focusa and blur don't bubble */ + DOM.sinkEvents( + (com.google.gwt.user.client.Element) focusElement + .cast(), Event.FOCUSEVENTS); + // revert to original, not focusable + getElement().setPropertyObject("tabIndex", null); + + } else { + moveFocusElementAfterWidget(); + } + } + } + + /** + * Helper to keep focus element always in domChild[1]. Aids testing. + */ + private void moveFocusElementAfterWidget() { + getElement().insertAfter(focusElement, getWidget().getElement()); + } + + @Override + public void setFocus(boolean focus) { + if (useFakeFocusElement()) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus( + (Element) focusElement.cast()); + } else { + FocusImpl.getFocusImplForPanel().blur( + (Element) focusElement.cast()); + } + } else { + super.setFocus(focus); + } + } + + @Override + public void setTabIndex(int tabIndex) { + if (useFakeFocusElement()) { + getElement().setTabIndex(-1); + if (focusElement != null) { + focusElement.setTabIndex(tabIndex); + } + } else { + super.setTabIndex(tabIndex); + } + } + + @Override + public HandlerRegistration addScrollHandler(ScrollHandler handler) { + return addDomHandler(handler, ScrollEvent.getType()); + } + + /** + * Gets the horizontal scroll position. + * + * @return the horizontal scroll position, in pixels + */ + public int getHorizontalScrollPosition() { + return getElement().getScrollLeft(); + } + + /** + * Gets the vertical scroll position. + * + * @return the vertical scroll position, in pixels + */ + public int getScrollPosition() { + if (getElement().getPropertyJSO("_vScrollTop") != null) { + return getElement().getPropertyInt("_vScrollTop"); + } else { + return getElement().getScrollTop(); + } + } + + /** + * Sets the horizontal scroll position. + * + * @param position + * the new horizontal scroll position, in pixels + */ + public void setHorizontalScrollPosition(int position) { + getElement().setScrollLeft(position); + } + + /** + * Sets the vertical scroll position. + * + * @param position + * the new vertical scroll position, in pixels + */ + public void setScrollPosition(int position) { + if (BrowserInfo.get().isAndroidWithBrokenScrollTop() + && BrowserInfo.get().requiresTouchScrollDelegate()) { + ArrayList<com.google.gwt.dom.client.Element> elements = TouchScrollDelegate + .getElements(getElement()); + for (com.google.gwt.dom.client.Element el : elements) { + final Style style = el.getStyle(); + style.setProperty("webkitTransform", "translate3d(0px," + + -position + "px,0px)"); + } + getElement().setPropertyInt("_vScrollTop", position); + } else { + getElement().setScrollTop(position); + } + } + + @Override + public void onScroll(ScrollEvent event) { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + focusElement.getStyle().setTop(getScrollPosition(), Unit.PX); + focusElement.getStyle().setLeft(getHorizontalScrollPosition(), + Unit.PX); + } + }); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/Icon.java b/client/src/com/vaadin/terminal/gwt/client/ui/Icon.java new file mode 100644 index 0000000000..21ee5bea93 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/Icon.java @@ -0,0 +1,56 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.UIObject; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +public class Icon extends UIObject { + public static final String CLASSNAME = "v-icon"; + private final ApplicationConnection client; + private String myUri; + + public Icon(ApplicationConnection client) { + setElement(DOM.createImg()); + DOM.setElementProperty(getElement(), "alt", ""); + setStyleName(CLASSNAME); + this.client = client; + } + + public Icon(ApplicationConnection client, String uidlUri) { + this(client); + setUri(uidlUri); + } + + public void setUri(String uidlUri) { + if (!uidlUri.equals(myUri)) { + /* + * Start sinking onload events, widgets responsibility to react. We + * must do this BEFORE we set src as IE fires the event immediately + * if the image is found in cache (#2592). + */ + sinkEvents(Event.ONLOAD); + + String uri = client.translateVaadinUri(uidlUri); + DOM.setElementProperty(getElement(), "src", uri); + myUri = uidlUri; + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptComponentConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptComponentConnector.java new file mode 100644 index 0000000000..4ab7c45161 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptComponentConnector.java @@ -0,0 +1,57 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.JavaScriptComponentState; +import com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper; +import com.vaadin.terminal.gwt.client.communication.HasJavaScriptConnectorHelper; +import com.vaadin.ui.AbstractJavaScriptComponent; + +@Connect(AbstractJavaScriptComponent.class) +public final class JavaScriptComponentConnector extends + AbstractComponentConnector implements HasJavaScriptConnectorHelper { + + private final JavaScriptConnectorHelper helper = new JavaScriptConnectorHelper( + this) { + @Override + protected void showInitProblem( + java.util.ArrayList<String> attemptedNames) { + getWidget().showNoInitFound(attemptedNames); + } + }; + + @Override + public JavaScriptWidget getWidget() { + return (JavaScriptWidget) super.getWidget(); + } + + @Override + protected void init() { + super.init(); + helper.init(); + } + + @Override + public JavaScriptConnectorHelper getJavascriptConnectorHelper() { + return helper; + } + + @Override + public JavaScriptComponentState getState() { + return (JavaScriptComponentState) super.getState(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptWidget.java b/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptWidget.java new file mode 100644 index 0000000000..8e59115671 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptWidget.java @@ -0,0 +1,37 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import java.util.ArrayList; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.ui.Widget; + +public class JavaScriptWidget extends Widget { + public JavaScriptWidget() { + setElement(Document.get().createDivElement()); + } + + public void showNoInitFound(ArrayList<String> attemptedNames) { + String message = "Could not initialize JavaScriptConnector because no JavaScript init function was found. Make sure one of these functions are defined: <ul>"; + for (String name : attemptedNames) { + message += "<li>" + name + "</li>"; + } + message += "</ul>"; + + getElement().setInnerHTML(message); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/LayoutClickEventHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/LayoutClickEventHandler.java new file mode 100644 index 0000000000..63c3c84ce4 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/LayoutClickEventHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.user.client.Element; +import com.vaadin.shared.EventId; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.LayoutClickRpc; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder; + +public abstract class LayoutClickEventHandler extends AbstractClickEventHandler { + + public LayoutClickEventHandler(ComponentConnector connector) { + this(connector, EventId.LAYOUT_CLICK_EVENT_IDENTIFIER); + } + + public LayoutClickEventHandler(ComponentConnector connector, + String clickEventIdentifier) { + super(connector, clickEventIdentifier); + } + + protected abstract ComponentConnector getChildComponent(Element element); + + protected ComponentConnector getChildComponent(NativeEvent event) { + return getChildComponent((Element) event.getEventTarget().cast()); + } + + @Override + protected void fireClick(NativeEvent event) { + MouseEventDetails mouseDetails = MouseEventDetailsBuilder + .buildMouseEventDetails(event, getRelativeToElement()); + getLayoutClickRPC().layoutClick(mouseDetails, getChildComponent(event)); + } + + protected abstract LayoutClickRpc getLayoutClickRPC(); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ManagedLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/ManagedLayout.java new file mode 100644 index 0000000000..bdb01113d6 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/ManagedLayout.java @@ -0,0 +1,22 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.vaadin.terminal.gwt.client.ComponentConnector; + +public interface ManagedLayout extends ComponentConnector { + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/MediaBaseConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/MediaBaseConnector.java new file mode 100644 index 0000000000..2f52971aeb --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/MediaBaseConnector.java @@ -0,0 +1,84 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.vaadin.shared.communication.URLReference; +import com.vaadin.shared.ui.AbstractMediaState; +import com.vaadin.shared.ui.MediaControl; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; + +public abstract class MediaBaseConnector extends AbstractComponentConnector { + + @Override + protected void init() { + super.init(); + + registerRpc(MediaControl.class, new MediaControl() { + @Override + public void play() { + getWidget().play(); + } + + @Override + public void pause() { + getWidget().pause(); + } + }); + } + + @Override + public AbstractMediaState getState() { + return (AbstractMediaState) super.getState(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + getWidget().setControls(getState().isShowControls()); + getWidget().setAutoplay(getState().isAutoplay()); + getWidget().setMuted(getState().isMuted()); + for (int i = 0; i < getState().getSources().size(); i++) { + URLReference source = getState().getSources().get(i); + String sourceType = getState().getSourceTypes().get(i); + getWidget().addSource(source.getURL(), sourceType); + } + setAltText(getState().getAltText()); + } + + @Override + public VMediaBase getWidget() { + return (VMediaBase) super.getWidget(); + } + + private void setAltText(String altText) { + + if (altText == null || "".equals(altText)) { + altText = getDefaultAltHtml(); + } else if (!getState().isHtmlContentAllowed()) { + altText = Util.escapeHTML(altText); + } + getWidget().setAltText(altText); + } + + /** + * @return the default HTML to show users with browsers that do not support + * HTML5 media markup. + */ + protected abstract String getDefaultAltHtml(); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/PostLayoutListener.java b/client/src/com/vaadin/terminal/gwt/client/ui/PostLayoutListener.java new file mode 100644 index 0000000000..a56c464ad2 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/PostLayoutListener.java @@ -0,0 +1,20 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +public interface PostLayoutListener { + public void postLayout(); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java new file mode 100644 index 0000000000..65ea7579fd --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java @@ -0,0 +1,310 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import java.util.ArrayList; +import java.util.Iterator; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.user.client.Command; +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.ui.HasWidgets; +import com.google.gwt.user.client.ui.KeyboardListener; +import com.google.gwt.user.client.ui.KeyboardListenerCollection; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.richtextarea.VRichTextArea; + +/** + * A helper class to implement keyboard shorcut handling. Keeps a list of owners + * actions and fires actions to server. User class needs to delegate keyboard + * events to handleKeyboardEvents function. + * + * @author Vaadin Ltd + */ +public class ShortcutActionHandler { + + /** + * An interface implemented by those users of this helper class that want to + * support special components like {@link VRichTextArea} that don't properly + * propagate key down events. Those components can build support for + * shortcut actions by traversing the closest + * {@link ShortcutActionHandlerOwner} from the component hierarchy an + * passing keydown events to {@link ShortcutActionHandler}. + */ + public interface ShortcutActionHandlerOwner extends HasWidgets { + + /** + * Returns the ShortCutActionHandler currently used or null if there is + * currently no shortcutactionhandler + */ + ShortcutActionHandler getShortcutActionHandler(); + } + + /** + * A focusable {@link ComponentConnector} implementing this interface will + * be notified before shortcut actions are handled if it will be the target + * of the action (most commonly means it is the focused component during the + * keyboard combination is triggered by the user). + */ + public interface BeforeShortcutActionListener extends ComponentConnector { + /** + * This method is called by ShortcutActionHandler before firing the + * shortcut if the Paintable is currently focused (aka the target of the + * shortcut action). Eg. a field can update its possibly changed value + * to the server before shortcut action is fired. + * + * @param e + * the event that triggered the shortcut action + */ + public void onBeforeShortcutAction(Event e); + } + + private final ArrayList<ShortcutAction> actions = new ArrayList<ShortcutAction>(); + private ApplicationConnection client; + private String paintableId; + + /** + * + * @param pid + * Paintable id + * @param c + * reference to application connections + */ + public ShortcutActionHandler(String pid, ApplicationConnection c) { + paintableId = pid; + client = c; + } + + /** + * Updates list of actions this handler listens to. + * + * @param c + * UIDL snippet containing actions + */ + public void updateActionMap(UIDL c) { + actions.clear(); + final Iterator<?> it = c.getChildIterator(); + while (it.hasNext()) { + final UIDL action = (UIDL) it.next(); + + int[] modifiers = null; + if (action.hasAttribute("mk")) { + modifiers = action.getIntArrayAttribute("mk"); + } + + final ShortcutKeyCombination kc = new ShortcutKeyCombination( + action.getIntAttribute("kc"), modifiers); + final String key = action.getStringAttribute("key"); + final String caption = action.getStringAttribute("caption"); + actions.add(new ShortcutAction(key, kc, caption)); + } + } + + public void handleKeyboardEvent(final Event event, ComponentConnector target) { + final int modifiers = KeyboardListenerCollection + .getKeyboardModifiers(event); + final char keyCode = (char) DOM.eventGetKeyCode(event); + final ShortcutKeyCombination kc = new ShortcutKeyCombination(keyCode, + modifiers); + final Iterator<ShortcutAction> it = actions.iterator(); + while (it.hasNext()) { + final ShortcutAction a = it.next(); + if (a.getShortcutCombination().equals(kc)) { + fireAction(event, a, target); + break; + } + } + + } + + public void handleKeyboardEvent(final Event event) { + handleKeyboardEvent(event, null); + } + + private void fireAction(final Event event, final ShortcutAction a, + ComponentConnector target) { + final Element et = DOM.eventGetTarget(event); + if (target == null) { + target = Util.findPaintable(client, et); + } + final ComponentConnector finalTarget = target; + + event.preventDefault(); + + /* + * The target component might have unpublished changes, try to + * synchronize them before firing shortcut action. + */ + if (finalTarget instanceof BeforeShortcutActionListener) { + ((BeforeShortcutActionListener) finalTarget) + .onBeforeShortcutAction(event); + } else { + shakeTarget(et); + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + shakeTarget(et); + } + }); + } + + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + if (finalTarget != null) { + client.updateVariable(paintableId, "actiontarget", + finalTarget, false); + } + client.updateVariable(paintableId, "action", a.getKey(), true); + } + }); + } + + /** + * We try to fire value change in the component the key combination was + * typed. Eg. textfield may contain newly typed text that is expected to be + * sent to server. This is done by removing focus and then returning it + * immediately back to target element. + * <p> + * This is practically a hack and should be replaced with an interface + * {@link BeforeShortcutActionListener} via widgets could be notified when + * they should fire value change. Big task for TextFields, DateFields and + * various selects. + * + * <p> + * TODO separate opera impl with generator + */ + private static void shakeTarget(final Element e) { + blur(e); + if (BrowserInfo.get().isOpera()) { + // will mess up with focus and blur event if the focus is not + // deferred. Will cause a small flickering, so not doing it for all + // browsers. + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + focus(e); + } + }); + } else { + focus(e); + } + } + + private static native void blur(Element e) + /*-{ + if(e.blur) { + e.blur(); + } + }-*/; + + private static native void focus(Element e) + /*-{ + if(e.blur) { + e.focus(); + } + }-*/; + +} + +class ShortcutKeyCombination { + + public static final int SHIFT = 16; + public static final int CTRL = 17; + public static final int ALT = 18; + public static final int META = 91; + + char keyCode = 0; + private int modifiersMask; + + public ShortcutKeyCombination() { + } + + ShortcutKeyCombination(char kc, int modifierMask) { + keyCode = kc; + modifiersMask = modifierMask; + } + + ShortcutKeyCombination(int kc, int[] modifiers) { + keyCode = (char) kc; + + modifiersMask = 0; + if (modifiers != null) { + for (int i = 0; i < modifiers.length; i++) { + switch (modifiers[i]) { + case ALT: + modifiersMask = modifiersMask + | KeyboardListener.MODIFIER_ALT; + break; + case CTRL: + modifiersMask = modifiersMask + | KeyboardListener.MODIFIER_CTRL; + break; + case SHIFT: + modifiersMask = modifiersMask + | KeyboardListener.MODIFIER_SHIFT; + break; + case META: + modifiersMask = modifiersMask + | KeyboardListener.MODIFIER_META; + break; + default: + break; + } + } + } + } + + public boolean equals(ShortcutKeyCombination other) { + if (keyCode == other.keyCode && modifiersMask == other.modifiersMask) { + return true; + } + return false; + } +} + +class ShortcutAction { + + private final ShortcutKeyCombination sc; + private final String caption; + private final String key; + + public ShortcutAction(String key, ShortcutKeyCombination sc, String caption) { + this.sc = sc; + this.key = key; + this.caption = caption; + } + + public ShortcutKeyCombination getShortcutCombination() { + return sc; + } + + public String getCaption() { + return caption; + } + + public String getKey() { + return key; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/SimpleFocusablePanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/SimpleFocusablePanel.java new file mode 100644 index 0000000000..d76d6c14f3 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/SimpleFocusablePanel.java @@ -0,0 +1,91 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.dom.client.HasKeyDownHandlers; +import com.google.gwt.event.dom.client.HasKeyPressHandlers; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.event.dom.client.KeyUpHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.terminal.gwt.client.Focusable; + +/** + * Compared to FocusPanel in GWT this panel does not support eg. accesskeys, but + * is simpler by its dom hierarchy nor supports focusing via java api. + */ +public class SimpleFocusablePanel extends SimplePanel implements + HasFocusHandlers, HasBlurHandlers, HasKeyDownHandlers, + HasKeyPressHandlers, Focusable { + + public SimpleFocusablePanel() { + // make focusable, as we don't need access key magic we don't need to + // use FocusImpl.createFocusable + setTabIndex(0); + } + + @Override + public HandlerRegistration addFocusHandler(FocusHandler handler) { + return addDomHandler(handler, FocusEvent.getType()); + } + + @Override + public HandlerRegistration addBlurHandler(BlurHandler handler) { + return addDomHandler(handler, BlurEvent.getType()); + } + + @Override + public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { + return addDomHandler(handler, KeyDownEvent.getType()); + } + + @Override + public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { + return addDomHandler(handler, KeyPressEvent.getType()); + } + + public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) { + return addDomHandler(handler, KeyUpEvent.getType()); + } + + public void setFocus(boolean focus) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus(getElement()); + } else { + FocusImpl.getFocusImplForPanel().blur(getElement()); + } + } + + @Override + public void focus() { + setFocus(true); + } + + public void setTabIndex(int tabIndex) { + getElement().setTabIndex(tabIndex); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/SimpleManagedLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/SimpleManagedLayout.java new file mode 100644 index 0000000000..1b404b5fb0 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/SimpleManagedLayout.java @@ -0,0 +1,20 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +public interface SimpleManagedLayout extends ManagedLayout { + public void layout(); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java b/client/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java new file mode 100644 index 0000000000..145d03287f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java @@ -0,0 +1,62 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ComponentLocator; + +/** + * Interface implemented by {@link Widget}s which can provide identifiers for at + * least one element inside the component. Used by {@link ComponentLocator}. + * + */ +public interface SubPartAware { + + /** + * Locates an element inside a component using the identifier provided in + * {@code subPart}. The {@code subPart} identifier is component specific and + * may be any string of characters, numbers, space characters and brackets. + * + * @param subPart + * The identifier for the element inside the component + * @return The element identified by subPart or null if the element could + * not be found. + */ + Element getSubPartElement(String subPart); + + /** + * Provides an identifier that identifies the element within the component. + * The {@code subElement} is a part of the component and must never be null. + * <p> + * <b>Note!</b> + * {@code getSubPartElement(getSubPartName(element)) == element} is <i>not + * always</i> true. A component can choose to provide a more generic + * identifier for any given element if the results of all interactions with + * {@code subElement} are the same as interactions with the element + * identified by the return value. For example a button can return an + * identifier for the root element even though a DIV inside the button was + * passed as {@code subElement} because interactions with the DIV and the + * root button element produce the same result. + * + * @param subElement + * The element the identifier string should uniquely identify + * @return An identifier that uniquely identifies {@code subElement} or null + * if no identifier could be provided. + */ + String getSubPartName(Element subElement); + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/TouchScrollDelegate.java b/client/src/com/vaadin/terminal/gwt/client/ui/TouchScrollDelegate.java new file mode 100644 index 0000000000..4838576c41 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/TouchScrollDelegate.java @@ -0,0 +1,669 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; + +import com.google.gwt.animation.client.Animation; +import com.google.gwt.core.client.Duration; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Touch; +import com.google.gwt.event.dom.client.ScrollHandler; +import com.google.gwt.event.dom.client.TouchStartEvent; +import com.google.gwt.event.dom.client.TouchStartHandler; +import com.google.gwt.event.shared.HandlerRegistration; +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.Widget; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.VConsole; + +/** + * Provides one finger touch scrolling for elements with once scrollable + * elements inside. One widget can have several of these scrollable elements. + * Scrollable elements are provided in the constructor. Users must pass + * touchStart events to this delegate, from there on the delegate takes over + * with an event preview. Other touch events needs to be sunken though. + * <p> + * This is bit similar as Scroller class in GWT expenses example, but ideas + * drawn from iscroll.js project: + * <ul> + * <li>uses GWT event mechanism. + * <li>uses modern CSS trick during scrolling for smoother experience: + * translate3d and transitions + * </ul> + * <p> + * Scroll event should only happen when the "touch scrolling actually ends". + * Later we might also tune this so that a scroll event happens if user stalls + * her finger long enought. + * + * TODO static getter for active touch scroll delegate. Components might need to + * prevent scrolling in some cases. Consider Table with drag and drop, or drag + * and drop in scrollable area. Optimal implementation might be to start the + * drag and drop only if user keeps finger down for a moment, otherwise do the + * scroll. In this case, the draggable component would need to cancel scrolling + * in a timer after touchstart event and take over from there. + * + * TODO support scrolling horizontally + * + * TODO cancel if user add second finger to the screen (user expects a gesture). + * + * TODO "scrollbars", see e.g. iscroll.js + * + * TODO write an email to sjobs ät apple dot com and beg for this feature to be + * built into webkit. Seriously, we should try to lobbying this to webkit folks. + * This sure ain't our business to implement this with javascript. + * + * TODO collect all general touch related constant to better place. + * + * @author Matti Tahvonen, Vaadin Ltd + */ +public class TouchScrollDelegate implements NativePreviewHandler { + + private static final double FRICTION = 0.002; + private static final double DECELERATION = 0.002; + private static final int MAX_DURATION = 1500; + private int origY; + private HashSet<Element> scrollableElements; + private Element scrolledElement; + private int origScrollTop; + private HandlerRegistration handlerRegistration; + private double lastAnimatedTranslateY; + private int lastClientY; + private int deltaScrollPos; + private boolean transitionOn = false; + private int finalScrollTop; + private ArrayList<Element> layers; + private boolean moved; + private ScrollHandler scrollHandler; + + private static TouchScrollDelegate activeScrollDelegate; + + private static final boolean androidWithBrokenScrollTop = BrowserInfo.get() + .isAndroidWithBrokenScrollTop(); + + /** + * A helper class for making a widget scrollable. Uses native scrolling if + * supported by the browser, otherwise registers a touch start handler + * delegating to a TouchScrollDelegate instance. + */ + public static class TouchScrollHandler implements TouchStartHandler { + + private static final String SCROLLABLE_CLASSNAME = "v-scrollable"; + + private final TouchScrollDelegate delegate; + private final boolean requiresDelegate = BrowserInfo.get() + .requiresTouchScrollDelegate(); + + /** + * Constructs a scroll handler for the given widget. + * + * @param widget + * The widget that contains scrollable elements + * @param scrollables + * The elements of the widget that should be scrollable. + */ + public TouchScrollHandler(Widget widget, Element... scrollables) { + if (requiresDelegate) { + delegate = new TouchScrollDelegate(); + widget.addDomHandler(this, TouchStartEvent.getType()); + } else { + delegate = null; + } + setElements(scrollables); + } + + @Override + public void onTouchStart(TouchStartEvent event) { + assert delegate != null; + delegate.onTouchStart(event); + } + + public void debug(Element e) { + VConsole.log("Classes: " + e.getClassName() + " overflow: " + + e.getStyle().getProperty("overflow") + " w-o-s: " + + e.getStyle().getProperty("WebkitOverflowScrolling")); + } + + /** + * Registers the given element as scrollable. + */ + public void addElement(Element scrollable) { + scrollable.addClassName(SCROLLABLE_CLASSNAME); + if (requiresDelegate) { + delegate.scrollableElements.add(scrollable); + } + } + + /** + * Unregisters the given element as scrollable. Should be called when a + * previously-registered element is removed from the DOM to prevent + * memory leaks. + */ + public void removeElement(Element scrollable) { + scrollable.removeClassName(SCROLLABLE_CLASSNAME); + if (requiresDelegate) { + delegate.scrollableElements.remove(scrollable); + } + } + + /** + * Registers the given elements as scrollable, removing previously + * registered scrollables from this handler. + * + * @param scrollables + * The elements that should be scrollable + */ + public void setElements(Element... scrollables) { + if (requiresDelegate) { + for (Element e : delegate.scrollableElements) { + e.removeClassName(SCROLLABLE_CLASSNAME); + } + delegate.scrollableElements.clear(); + } + for (Element e : scrollables) { + addElement(e); + } + } + } + + /** + * Makes the given elements scrollable, either natively or by using a + * TouchScrollDelegate, depending on platform capabilities. + * + * @param widget + * The widget that contains scrollable elements + * @param scrollables + * The elements inside the widget that should be scrollable + * @return A scroll handler for the given widget. + */ + public static TouchScrollHandler enableTouchScrolling(Widget widget, + Element... scrollables) { + return new TouchScrollHandler(widget, scrollables); + } + + public TouchScrollDelegate(Element... elements) { + setElements(elements); + } + + public void setScrollHandler(ScrollHandler scrollHandler) { + this.scrollHandler = scrollHandler; + } + + public static TouchScrollDelegate getActiveScrollDelegate() { + return activeScrollDelegate; + } + + /** + * Has user moved the touch. + * + * @return + */ + public boolean isMoved() { + return moved; + } + + /** + * Forces the scroll delegate to cancels scrolling process. Can be called by + * users if they e.g. decide to handle touch event by themselves after all + * (e.g. a pause after touch start before moving touch -> interpreted as + * long touch/click or drag start). + */ + public void stopScrolling() { + handlerRegistration.removeHandler(); + handlerRegistration = null; + if (moved) { + moveTransformationToScrolloffset(); + } else { + activeScrollDelegate = null; + } + } + + public void onTouchStart(TouchStartEvent event) { + if (activeScrollDelegate == null && event.getTouches().length() == 1) { + NativeEvent nativeEvent = event.getNativeEvent(); + doTouchStart(nativeEvent); + } else { + /* + * Touch scroll is currenly on (possibly bouncing). Ignore. + */ + } + } + + private void doTouchStart(NativeEvent nativeEvent) { + if (transitionOn) { + momentum.cancel(); + } + Touch touch = nativeEvent.getTouches().get(0); + if (detectScrolledElement(touch)) { + VConsole.log("TouchDelegate takes over"); + nativeEvent.stopPropagation(); + handlerRegistration = Event.addNativePreviewHandler(this); + activeScrollDelegate = this; + origY = touch.getClientY(); + yPositions[0] = origY; + eventTimeStamps[0] = getTimeStamp(); + nextEvent = 1; + + origScrollTop = getScrollTop(); + VConsole.log("ST" + origScrollTop); + + moved = false; + // event.preventDefault(); + // event.stopPropagation(); + } + } + + private int getScrollTop() { + if (androidWithBrokenScrollTop) { + if (scrolledElement.getPropertyJSO("_vScrollTop") != null) { + return scrolledElement.getPropertyInt("_vScrollTop"); + } + return 0; + } + return scrolledElement.getScrollTop(); + } + + private void onTransitionEnd() { + if (finalScrollTop < 0) { + animateToScrollPosition(0, finalScrollTop); + finalScrollTop = 0; + } else if (finalScrollTop > getMaxFinalY()) { + animateToScrollPosition(getMaxFinalY(), finalScrollTop); + finalScrollTop = getMaxFinalY(); + } else { + moveTransformationToScrolloffset(); + } + } + + private void animateToScrollPosition(int to, int from) { + int dist = Math.abs(to - from); + int time = getAnimationTimeForDistance(dist); + if (time <= 0) { + time = 1; // get animation and transition end event + } + VConsole.log("Animate " + time + " " + from + " " + to); + int translateTo = -to + origScrollTop; + int fromY = -from + origScrollTop; + if (androidWithBrokenScrollTop) { + fromY -= origScrollTop; + translateTo -= origScrollTop; + } + translateTo(time, fromY, translateTo); + } + + private int getAnimationTimeForDistance(int dist) { + return 350; // 350ms seems to work quite fine for all distances + // if (dist < 0) { + // dist = -dist; + // } + // return MAX_DURATION * dist / (scrolledElement.getClientHeight() * 3); + } + + /** + * Called at the end of scrolling. Moves possible translate values to + * scrolltop, causing onscroll event. + */ + private void moveTransformationToScrolloffset() { + if (androidWithBrokenScrollTop) { + scrolledElement.setPropertyInt("_vScrollTop", finalScrollTop); + if (scrollHandler != null) { + scrollHandler.onScroll(null); + } + } else { + for (Element el : layers) { + Style style = el.getStyle(); + style.setProperty("webkitTransform", "translate3d(0,0,0)"); + } + scrolledElement.setScrollTop(finalScrollTop); + } + activeScrollDelegate = null; + handlerRegistration.removeHandler(); + handlerRegistration = null; + } + + /** + * Detects if a touch happens on a predefined element and the element has + * something to scroll. + * + * @param touch + * @return + */ + private boolean detectScrolledElement(Touch touch) { + Element target = touch.getTarget().cast(); + for (Element el : scrollableElements) { + if (el.isOrHasChild(target) + && el.getScrollHeight() > el.getClientHeight()) { + scrolledElement = el; + layers = getElements(scrolledElement); + return true; + + } + } + return false; + } + + public static ArrayList<Element> getElements(Element scrolledElement2) { + NodeList<Node> childNodes = scrolledElement2.getChildNodes(); + ArrayList<Element> l = new ArrayList<Element>(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node item = childNodes.getItem(i); + if (item.getNodeType() == Node.ELEMENT_NODE) { + l.add((Element) item); + } + } + return l; + } + + private void onTouchMove(NativeEvent event) { + if (!moved) { + double l = (getTimeStamp() - eventTimeStamps[0]); + VConsole.log(l + " ms from start to move"); + } + boolean handleMove = readPositionAndSpeed(event); + if (handleMove) { + int deltaScrollTop = origY - lastClientY; + int finalPos = origScrollTop + deltaScrollTop; + if (finalPos > getMaxFinalY()) { + // spring effect at the end + int overscroll = (deltaScrollTop + origScrollTop) + - getMaxFinalY(); + overscroll = overscroll / 2; + if (overscroll > getMaxOverScroll()) { + overscroll = getMaxOverScroll(); + } + deltaScrollTop = getMaxFinalY() + overscroll - origScrollTop; + } else if (finalPos < 0) { + // spring effect at the beginning + int overscroll = finalPos / 2; + if (-overscroll > getMaxOverScroll()) { + overscroll = -getMaxOverScroll(); + } + deltaScrollTop = overscroll - origScrollTop; + } + quickSetScrollPosition(0, deltaScrollTop); + moved = true; + event.preventDefault(); + event.stopPropagation(); + } + } + + private void quickSetScrollPosition(int deltaX, int deltaY) { + deltaScrollPos = deltaY; + if (androidWithBrokenScrollTop) { + deltaY += origScrollTop; + translateTo(-deltaY); + } else { + translateTo(-deltaScrollPos); + } + } + + private static final int EVENTS_FOR_SPEED_CALC = 3; + public static final int SIGNIFICANT_MOVE_THRESHOLD = 3; + private int[] yPositions = new int[EVENTS_FOR_SPEED_CALC]; + private double[] eventTimeStamps = new double[EVENTS_FOR_SPEED_CALC]; + private int nextEvent = 0; + private Animation momentum; + + /** + * + * @param event + * @return + */ + private boolean readPositionAndSpeed(NativeEvent event) { + Touch touch = event.getChangedTouches().get(0); + lastClientY = touch.getClientY(); + int eventIndx = nextEvent++; + eventIndx = eventIndx % EVENTS_FOR_SPEED_CALC; + eventTimeStamps[eventIndx] = getTimeStamp(); + yPositions[eventIndx] = lastClientY; + return isMovedSignificantly(); + } + + private boolean isMovedSignificantly() { + return moved ? moved + : Math.abs(origY - lastClientY) >= SIGNIFICANT_MOVE_THRESHOLD; + } + + private void onTouchEnd(NativeEvent event) { + if (!moved) { + activeScrollDelegate = null; + handlerRegistration.removeHandler(); + handlerRegistration = null; + return; + } + + int currentY = origScrollTop + deltaScrollPos; + + int maxFinalY = getMaxFinalY(); + + int pixelsToMove; + int finalY; + int duration = -1; + if (currentY > maxFinalY) { + // we are over the max final pos, animate to end + pixelsToMove = maxFinalY - currentY; + finalY = maxFinalY; + } else if (currentY < 0) { + // we are below the max final pos, animate to beginning + pixelsToMove = -currentY; + finalY = 0; + } else { + double pixelsPerMs = calculateSpeed(); + // we are currently within scrollable area, calculate pixels that + // we'll move due to momentum + VConsole.log("pxPerMs" + pixelsPerMs); + pixelsToMove = (int) (0.5 * pixelsPerMs * pixelsPerMs / FRICTION); + if (pixelsPerMs < 0) { + pixelsToMove = -pixelsToMove; + } + // VConsole.log("pixels to move" + pixelsToMove); + + finalY = currentY + pixelsToMove; + + if (finalY > maxFinalY + getMaxOverScroll()) { + // VConsole.log("To max overscroll"); + finalY = getMaxFinalY() + getMaxOverScroll(); + int fixedPixelsToMove = finalY - currentY; + pixelsToMove = fixedPixelsToMove; + } else if (finalY < 0 - getMaxOverScroll()) { + // VConsole.log("to min overscroll"); + finalY = -getMaxOverScroll(); + int fixedPixelsToMove = finalY - currentY; + pixelsToMove = fixedPixelsToMove; + } else { + duration = (int) (Math.abs(pixelsPerMs / DECELERATION)); + } + } + if (duration == -1) { + // did not keep in side borders or was outside borders, calculate + // a good enough duration based on pixelsToBeMoved. + duration = getAnimationTimeForDistance(pixelsToMove); + } + if (duration > MAX_DURATION) { + VConsole.log("Max animation time. " + duration); + duration = MAX_DURATION; + } + finalScrollTop = finalY; + + if (Math.abs(pixelsToMove) < 3 || duration < 20) { + VConsole.log("Small 'momentum' " + pixelsToMove + " | " + duration + + " Skipping animation,"); + moveTransformationToScrolloffset(); + return; + } + + int translateTo = -finalY + origScrollTop; + int fromY = -currentY + origScrollTop; + if (androidWithBrokenScrollTop) { + fromY -= origScrollTop; + translateTo -= origScrollTop; + } + translateTo(duration, fromY, translateTo); + } + + private double calculateSpeed() { + if (nextEvent < EVENTS_FOR_SPEED_CALC) { + VConsole.log("Not enough data for speed calculation"); + // not enough data for decent speed calculation, no momentum :-( + return 0; + } + int idx = nextEvent % EVENTS_FOR_SPEED_CALC; + final int firstPos = yPositions[idx]; + final double firstTs = eventTimeStamps[idx]; + idx += EVENTS_FOR_SPEED_CALC; + idx--; + idx = idx % EVENTS_FOR_SPEED_CALC; + final int lastPos = yPositions[idx]; + final double lastTs = eventTimeStamps[idx]; + // speed as in change of scrolltop == -speedOfTouchPos + return (firstPos - lastPos) / (lastTs - firstTs); + + } + + /** + * Note positive scrolltop moves layer up, positive translate moves layer + * down. + */ + private void translateTo(double translateY) { + for (Element el : layers) { + Style style = el.getStyle(); + style.setProperty("webkitTransform", "translate3d(0px," + + translateY + "px,0px)"); + } + } + + /** + * Note positive scrolltop moves layer up, positive translate moves layer + * down. + * + * @param duration + */ + private void translateTo(int duration, final int fromY, final int finalY) { + if (duration > 0) { + transitionOn = true; + + momentum = new Animation() { + + @Override + protected void onUpdate(double progress) { + lastAnimatedTranslateY = (fromY + (finalY - fromY) + * progress); + translateTo(lastAnimatedTranslateY); + } + + @Override + protected double interpolate(double progress) { + return 1 + Math.pow(progress - 1, 3); + } + + @Override + protected void onComplete() { + super.onComplete(); + transitionOn = false; + onTransitionEnd(); + } + + @Override + protected void onCancel() { + int delta = (int) (finalY - lastAnimatedTranslateY); + finalScrollTop -= delta; + moveTransformationToScrolloffset(); + transitionOn = false; + } + }; + momentum.run(duration); + } + } + + private int getMaxOverScroll() { + return androidWithBrokenScrollTop ? 0 : scrolledElement + .getClientHeight() / 3; + } + + private int getMaxFinalY() { + return scrolledElement.getScrollHeight() + - scrolledElement.getClientHeight(); + } + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + int typeInt = event.getTypeInt(); + if (transitionOn) { + /* + * TODO allow starting new events. See issue in onTouchStart + */ + event.cancel(); + + if (typeInt == Event.ONTOUCHSTART) { + doTouchStart(event.getNativeEvent()); + } + return; + } + switch (typeInt) { + case Event.ONTOUCHMOVE: + if (!event.isCanceled()) { + onTouchMove(event.getNativeEvent()); + if (moved) { + event.cancel(); + } + } + break; + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + if (!event.isCanceled()) { + if (moved) { + event.cancel(); + } + onTouchEnd(event.getNativeEvent()); + } + break; + case Event.ONMOUSEMOVE: + if (moved) { + // no debug message, mobile safari generates these for some + // compatibility purposes. + event.cancel(); + } + break; + default: + VConsole.log("Non touch event:" + event.getNativeEvent().getType()); + event.cancel(); + break; + } + } + + public void setElements(Element[] elements) { + scrollableElements = new HashSet<Element>(Arrays.asList(elements)); + } + + /** + * long calcucation are not very efficient in GWT, so this helper method + * returns timestamp in double. + * + * @return + */ + public static double getTimeStamp() { + return Duration.currentTimeMillis(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java b/client/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java new file mode 100644 index 0000000000..9cf63a609c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +/** + * This class is used for "row actions" in VTree and ITable + */ +public class TreeAction extends Action { + + String targetKey = ""; + String actionKey = ""; + + public TreeAction(ActionOwner owner) { + super(owner); + } + + public TreeAction(ActionOwner owner, String target, String action) { + this(owner); + targetKey = target; + actionKey = action; + } + + /** + * Sends message to server that this action has been fired. Messages are + * "standard" Vaadin messages whose value is comma separated pair of + * targetKey (row, treeNod ...) and actions id. + * + * Variablename is always "action". + * + * Actions are always sent immediatedly to server. + */ + @Override + public void execute() { + owner.getClient().updateVariable(owner.getPaintableId(), "action", + targetKey + "," + actionKey, true); + owner.getClient().getContextMenu().hide(); + } + + public String getActionKey() { + return actionKey; + } + + public void setActionKey(String actionKey) { + this.actionKey = actionKey; + } + + public String getTargetKey() { + return targetKey; + } + + public void setTargetKey(String targetKey) { + this.targetKey = targetKey; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java b/client/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java new file mode 100644 index 0000000000..9b03c03794 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java @@ -0,0 +1,43 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.user.client.ui.AbstractImagePrototype; + +public interface TreeImages extends com.google.gwt.user.client.ui.TreeImages { + + /** + * An image indicating an open branch. + * + * @return a prototype of this image + * @gwt.resource com/vaadin/terminal/gwt/public/default/tree/img/expanded + * .png + */ + @Override + AbstractImagePrototype treeOpen(); + + /** + * An image indicating a closed branch. + * + * @return a prototype of this image + * @gwt.resource com/vaadin/terminal/gwt/public/default/tree/img/collapsed + * .png + */ + @Override + AbstractImagePrototype treeClosed(); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/UnknownComponentConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/UnknownComponentConnector.java new file mode 100644 index 0000000000..aaf37d0345 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/UnknownComponentConnector.java @@ -0,0 +1,43 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +public class UnknownComponentConnector extends AbstractComponentConnector { + + @Override + public boolean delegateCaptionHandling() { + return false; + } + + @Override + public VUnknownComponent getWidget() { + return (VUnknownComponent) super.getWidget(); + } + + public void setServerSideClassName(String serverClassName) { + getWidget() + .setCaption( + "Widgetset does not contain implementation for " + + serverClassName + + ". Check its component connector's @Connect mapping, widgetsets " + + "GWT module description file and re-compile your" + + " widgetset. In case you have downloaded a vaadin" + + " add-on package, you might want to refer to " + + "<a href='http://vaadin.com/using-addons'>add-on " + + "instructions</a>."); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/VContextMenu.java b/client/src/com/vaadin/terminal/gwt/client/ui/VContextMenu.java new file mode 100644 index 0000000000..cf4f772bc9 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/VContextMenu.java @@ -0,0 +1,292 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.TableRowElement; +import com.google.gwt.dom.client.TableSectionElement; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.dom.client.HasKeyDownHandlers; +import com.google.gwt.event.dom.client.HasKeyPressHandlers; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.dom.client.LoadEvent; +import com.google.gwt.event.dom.client.LoadHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.MenuBar; +import com.google.gwt.user.client.ui.MenuItem; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.Util; + +public class VContextMenu extends VOverlay implements SubPartAware { + + private ActionOwner actionOwner; + + private final CMenuBar menu = new CMenuBar(); + + private int left; + + private int top; + + private VLazyExecutor delayedImageLoadExecutioner = new VLazyExecutor(100, + new ScheduledCommand() { + @Override + public void execute() { + imagesLoaded(); + } + }); + + /** + * This method should be used only by Client object as only one per client + * should exists. Request an instance via client.getContextMenu(); + * + * @param cli + * to be set as an owner of menu + */ + public VContextMenu() { + super(true, false, true); + setWidget(menu); + setStyleName("v-contextmenu"); + } + + protected void imagesLoaded() { + if (isVisible()) { + show(); + } + } + + /** + * Sets the element from which to build menu + * + * @param ao + */ + public void setActionOwner(ActionOwner ao) { + actionOwner = ao; + } + + /** + * Shows context menu at given location IF it contain at least one item. + * + * @param left + * @param top + */ + public void showAt(int left, int top) { + final Action[] actions = actionOwner.getActions(); + if (actions == null || actions.length == 0) { + // Only show if there really are actions + return; + } + this.left = left; + this.top = top; + menu.clearItems(); + for (int i = 0; i < actions.length; i++) { + final Action a = actions[i]; + menu.addItem(new MenuItem(a.getHTML(), true, a)); + } + + // Attach onload listeners to all images + Util.sinkOnloadForImages(menu.getElement()); + + setPopupPositionAndShow(new PositionCallback() { + @Override + public void setPosition(int offsetWidth, int offsetHeight) { + // mac FF gets bad width due GWT popups overflow hacks, + // re-determine width + offsetWidth = menu.getOffsetWidth(); + int left = VContextMenu.this.left; + int top = VContextMenu.this.top; + if (offsetWidth + left > Window.getClientWidth()) { + left = left - offsetWidth; + if (left < 0) { + left = 0; + } + } + if (offsetHeight + top > Window.getClientHeight()) { + top = top - offsetHeight; + if (top < 0) { + top = 0; + } + } + setPopupPosition(left, top); + + /* + * Move keyboard focus to menu, deferring the focus setting so + * the focus is certainly moved to the menu in all browser after + * the positioning has been done. + */ + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + // Focus the menu. + menu.setFocus(true); + + // Unselect previously selected items + menu.selectItem(null); + } + }); + + } + }); + } + + public void showAt(ActionOwner ao, int left, int top) { + setActionOwner(ao); + showAt(left, top); + } + + /** + * Extend standard Gwt MenuBar to set proper settings and to override + * onPopupClosed method so that PopupPanel gets closed. + */ + class CMenuBar extends MenuBar implements HasFocusHandlers, + HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, + Focusable, LoadHandler { + public CMenuBar() { + super(true); + addDomHandler(this, LoadEvent.getType()); + } + + @Override + public void onPopupClosed(PopupPanel sender, boolean autoClosed) { + super.onPopupClosed(sender, autoClosed); + + // make focusable, as we don't need access key magic we don't need + // to + // use FocusImpl.createFocusable + getElement().setTabIndex(0); + + hide(); + } + + /* + * public void onBrowserEvent(Event event) { // Remove current selection + * when mouse leaves if (DOM.eventGetType(event) == Event.ONMOUSEOUT) { + * Element to = DOM.eventGetToElement(event); if + * (!DOM.isOrHasChild(getElement(), to)) { DOM.setElementProperty( + * super.getSelectedItem().getElement(), "className", + * super.getSelectedItem().getStylePrimaryName()); } } + * + * super.onBrowserEvent(event); } + */ + + private MenuItem getItem(int index) { + return super.getItems().get(index); + } + + @Override + public HandlerRegistration addFocusHandler(FocusHandler handler) { + return addDomHandler(handler, FocusEvent.getType()); + } + + @Override + public HandlerRegistration addBlurHandler(BlurHandler handler) { + return addDomHandler(handler, BlurEvent.getType()); + } + + @Override + public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { + return addDomHandler(handler, KeyDownEvent.getType()); + } + + @Override + public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { + return addDomHandler(handler, KeyPressEvent.getType()); + } + + public void setFocus(boolean focus) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus(getElement()); + } else { + FocusImpl.getFocusImplForPanel().blur(getElement()); + } + } + + @Override + public void focus() { + setFocus(true); + } + + @Override + public void onLoad(LoadEvent event) { + // Handle icon onload events to ensure shadow is resized correctly + delayedImageLoadExecutioner.trigger(); + } + + } + + @Override + public Element getSubPartElement(String subPart) { + int index = Integer.parseInt(subPart.substring(6)); + // ApplicationConnection.getConsole().log( + // "Searching element for selection index " + index); + MenuItem item = menu.getItem(index); + // ApplicationConnection.getConsole().log("Item: " + item); + // Item refers to the td, which is the parent of the clickable element + return item.getElement().getFirstChildElement().cast(); + } + + @Override + public String getSubPartName(Element subElement) { + if (getElement().isOrHasChild(subElement)) { + com.google.gwt.dom.client.Element e = subElement; + { + while (e != null && !e.getTagName().toLowerCase().equals("tr")) { + e = e.getParentElement(); + // ApplicationConnection.getConsole().log("Found row"); + } + } + com.google.gwt.dom.client.TableSectionElement parentElement = (TableSectionElement) e + .getParentElement(); + NodeList<TableRowElement> rows = parentElement.getRows(); + for (int i = 0; i < rows.getLength(); i++) { + if (rows.getItem(i) == e) { + // ApplicationConnection.getConsole().log( + // "Found index for row" + 1); + return "option" + i; + } + } + return null; + } else { + return null; + } + } + + /** + * Hides context menu if it is currently shown by given action owner. + * + * @param actionOwner + */ + public void ensureHidden(ActionOwner actionOwner) { + if (this.actionOwner == actionOwner) { + hide(); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/VLazyExecutor.java b/client/src/com/vaadin/terminal/gwt/client/ui/VLazyExecutor.java new file mode 100644 index 0000000000..85ee27a36a --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/VLazyExecutor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.user.client.Timer; + +/** + * Executes the given command {@code delayMs} milliseconds after a call to + * {@link #trigger()}. Calling {@link #trigger()} again before the command has + * been executed causes the execution to be rescheduled to {@code delayMs} after + * the second call. + * + */ +public class VLazyExecutor { + + private Timer timer; + private int delayMs; + private ScheduledCommand cmd; + + /** + * @param delayMs + * Delay in milliseconds to wait before executing the command + * @param cmd + * The command to execute + */ + public VLazyExecutor(int delayMs, ScheduledCommand cmd) { + this.delayMs = delayMs; + this.cmd = cmd; + } + + /** + * Triggers execution of the command. Each call reschedules any existing + * execution to {@link #delayMs} milliseconds from that point in time. + */ + public void trigger() { + if (timer == null) { + timer = new Timer() { + @Override + public void run() { + timer = null; + cmd.execute(); + } + }; + } + // Schedule automatically cancels any old schedule + timer.schedule(delayMs); + + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/VMediaBase.java b/client/src/com/vaadin/terminal/gwt/client/ui/VMediaBase.java new file mode 100644 index 0000000000..9d83dde3d6 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/VMediaBase.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.MediaElement; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.Widget; + +public abstract class VMediaBase extends Widget { + + private MediaElement media; + + /** + * Sets the MediaElement that is to receive all commands and properties. + * + * @param element + */ + public void setMediaElement(MediaElement element) { + setElement(element); + media = element; + } + + public void play() { + media.play(); + } + + public void pause() { + media.pause(); + } + + public void setAltText(String alt) { + media.appendChild(Document.get().createTextNode(alt)); + } + + public void setControls(boolean shouldShowControls) { + media.setControls(shouldShowControls); + } + + public void setAutoplay(boolean shouldAutoplay) { + media.setAutoplay(shouldAutoplay); + } + + public void setMuted(boolean mediaMuted) { + media.setMuted(mediaMuted); + } + + public void addSource(String sourceUrl, String sourceType) { + Element src = Document.get().createElement("source").cast(); + src.setAttribute("src", sourceUrl); + src.setAttribute("type", sourceType); + media.appendChild(src); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/VOverlay.java b/client/src/com/vaadin/terminal/gwt/client/ui/VOverlay.java new file mode 100644 index 0000000000..aef21ac737 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/VOverlay.java @@ -0,0 +1,567 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.animation.client.Animation; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.IFrameElement; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.BorderStyle; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.BrowserInfo; + +/** + * In Vaadin UI this Overlay should always be used for all elements that + * temporary float over other components like context menus etc. This is to deal + * stacking order correctly with VWindow objects. + */ +public class VOverlay extends PopupPanel implements CloseHandler<PopupPanel> { + + public static class PositionAndSize { + private int left, top, width, height; + + public int getLeft() { + return left; + } + + public void setLeft(int left) { + this.left = left; + } + + public int getTop() { + return top; + } + + public void setTop(int top) { + this.top = top; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public void setAnimationFromCenterProgress(double progress) { + left += (int) (width * (1.0 - progress) / 2.0); + top += (int) (height * (1.0 - progress) / 2.0); + width = (int) (width * progress); + height = (int) (height * progress); + } + } + + /* + * The z-index value from where all overlays live. This can be overridden in + * any extending class. + */ + public static int Z_INDEX = 20000; + + private static int leftFix = -1; + + private static int topFix = -1; + + /* + * Shadow element style. If an extending class wishes to use a different + * style of shadow, it can use setShadowStyle(String) to give the shadow + * element a new style name. + */ + public static final String CLASSNAME_SHADOW = "v-shadow"; + + /* + * The shadow element for this overlay. + */ + private Element shadow; + + /* + * Creator of VOverlow (widget that made the instance, not the layout + * parent) + */ + private Widget owner; + + /** + * The shim iframe behind the overlay, allowing PDFs and applets to be + * covered by overlays. + */ + private IFrameElement shimElement; + + /** + * The HTML snippet that is used to render the actual shadow. In consists of + * nine different DIV-elements with the following class names: + * + * <pre> + * .v-shadow[-stylename] + * ---------------------------------------------- + * | .top-left | .top | .top-right | + * |---------------|-----------|----------------| + * | | | | + * | .left | .center | .right | + * | | | | + * |---------------|-----------|----------------| + * | .bottom-left | .bottom | .bottom-right | + * ---------------------------------------------- + * </pre> + * + * See default theme 'shadow.css' for implementation example. + */ + private static final String SHADOW_HTML = "<div class=\"top-left\"></div><div class=\"top\"></div><div class=\"top-right\"></div><div class=\"left\"></div><div class=\"center\"></div><div class=\"right\"></div><div class=\"bottom-left\"></div><div class=\"bottom\"></div><div class=\"bottom-right\"></div>"; + + /** + * Matches {@link PopupPanel}.ANIMATION_DURATION + */ + private static final int POPUP_PANEL_ANIMATION_DURATION = 200; + + private boolean sinkShadowEvents = false; + + public VOverlay() { + super(); + adjustZIndex(); + } + + public VOverlay(boolean autoHide) { + super(autoHide); + adjustZIndex(); + } + + public VOverlay(boolean autoHide, boolean modal) { + super(autoHide, modal); + adjustZIndex(); + } + + public VOverlay(boolean autoHide, boolean modal, boolean showShadow) { + super(autoHide, modal); + setShadowEnabled(showShadow); + adjustZIndex(); + } + + /** + * Method to controle whether DOM elements for shadow are added. With this + * method subclasses can control displaying of shadow also after the + * constructor. + * + * @param enabled + * true if shadow should be displayed + */ + protected void setShadowEnabled(boolean enabled) { + if (enabled != isShadowEnabled()) { + if (enabled) { + shadow = DOM.createDiv(); + shadow.setClassName(CLASSNAME_SHADOW); + shadow.setInnerHTML(SHADOW_HTML); + DOM.setStyleAttribute(shadow, "position", "absolute"); + addCloseHandler(this); + } else { + removeShadowIfPresent(); + shadow = null; + } + } + } + + protected boolean isShadowEnabled() { + return shadow != null; + } + + private void removeShim() { + if (shimElement != null) { + shimElement.removeFromParent(); + } + } + + private void removeShadowIfPresent() { + if (isShadowAttached()) { + shadow.removeFromParent(); + + // Remove event listener from the shadow + unsinkShadowEvents(); + } + } + + private boolean isShadowAttached() { + return isShadowEnabled() && shadow.getParentElement() != null; + } + + private boolean isShimAttached() { + return shimElement != null && shimElement.hasParentElement(); + } + + private void adjustZIndex() { + setZIndex(Z_INDEX); + } + + /** + * Set the z-index (visual stack position) for this overlay. + * + * @param zIndex + * The new z-index + */ + protected void setZIndex(int zIndex) { + DOM.setStyleAttribute(getElement(), "zIndex", "" + zIndex); + if (isShadowEnabled()) { + DOM.setStyleAttribute(shadow, "zIndex", "" + zIndex); + } + } + + @Override + public void setPopupPosition(int left, int top) { + // TODO, this should in fact be part of + // Document.get().getBodyOffsetLeft/Top(). Would require overriding DOM + // for all permutations. Now adding fix as margin instead of fixing + // left/top because parent class saves the position. + Style style = getElement().getStyle(); + style.setMarginLeft(-adjustByRelativeLeftBodyMargin(), Unit.PX); + style.setMarginTop(-adjustByRelativeTopBodyMargin(), Unit.PX); + super.setPopupPosition(left, top); + sizeOrPositionUpdated(isAnimationEnabled() ? 0 : 1); + } + + private IFrameElement getShimElement() { + if (shimElement == null) { + shimElement = Document.get().createIFrameElement(); + + // Insert shim iframe before the main overlay element. It does not + // matter if it is in front or behind the shadow as we cannot put a + // shim behind the shadow due to its transparency. + shimElement.getStyle().setPosition(Position.ABSOLUTE); + shimElement.getStyle().setBorderStyle(BorderStyle.NONE); + shimElement.setTabIndex(-1); + shimElement.setFrameBorder(0); + shimElement.setMarginHeight(0); + } + return shimElement; + } + + private int getActualTop() { + int y = getAbsoluteTop(); + + /* This is needed for IE7 at least */ + // Account for the difference between absolute position and the + // body's positioning context. + y -= Document.get().getBodyOffsetTop(); + y -= adjustByRelativeTopBodyMargin(); + + return y; + } + + private int getActualLeft() { + int x = getAbsoluteLeft(); + + /* This is needed for IE7 at least */ + // Account for the difference between absolute position and the + // body's positioning context. + x -= Document.get().getBodyOffsetLeft(); + x -= adjustByRelativeLeftBodyMargin(); + + return x; + } + + private static int adjustByRelativeTopBodyMargin() { + if (topFix == -1) { + topFix = detectRelativeBodyFixes("top"); + } + return topFix; + } + + private native static int detectRelativeBodyFixes(String axis) + /*-{ + try { + var b = $wnd.document.body; + var cstyle = b.currentStyle ? b.currentStyle : getComputedStyle(b); + if(cstyle && cstyle.position == 'relative') { + return b.getBoundingClientRect()[axis]; + } + } catch(e){} + return 0; + }-*/; + + private static int adjustByRelativeLeftBodyMargin() { + if (leftFix == -1) { + leftFix = detectRelativeBodyFixes("left"); + + } + return leftFix; + } + + @Override + public void show() { + super.show(); + if (isAnimationEnabled()) { + new ResizeAnimation().run(POPUP_PANEL_ANIMATION_DURATION); + } else { + sizeOrPositionUpdated(1.0); + } + } + + @Override + protected void onDetach() { + super.onDetach(); + + // Always ensure shadow is removed when the overlay is removed. + removeShadowIfPresent(); + removeShim(); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (isShadowEnabled()) { + shadow.getStyle().setProperty("visibility", + visible ? "visible" : "hidden"); + } + } + + @Override + public void setWidth(String width) { + super.setWidth(width); + sizeOrPositionUpdated(1.0); + } + + @Override + public void setHeight(String height) { + super.setHeight(height); + sizeOrPositionUpdated(1.0); + } + + /** + * Sets the shadow style for this overlay. Will override any previous style + * for the shadow. The default style name is defined by CLASSNAME_SHADOW. + * The given style will be prefixed with CLASSNAME_SHADOW. + * + * @param style + * The new style name for the shadow element. Will be prefixed by + * CLASSNAME_SHADOW, e.g. style=='foobar' -> actual style + * name=='v-shadow-foobar'. + */ + protected void setShadowStyle(String style) { + if (isShadowEnabled()) { + shadow.setClassName(CLASSNAME_SHADOW + "-" + style); + } + } + + /** + * Extending classes should always call this method after they change the + * size of overlay without using normal 'setWidth(String)' and + * 'setHeight(String)' methods (if not calling super.setWidth/Height). + * + */ + public void sizeOrPositionUpdated() { + sizeOrPositionUpdated(1.0); + } + + /** + * Recalculates proper position and dimensions for the shadow and shim + * elements. Can be used to animate the related elements, using the + * 'progress' parameter (used to animate the shadow in sync with GWT + * PopupPanel's default animation 'PopupPanel.AnimationType.CENTER'). + * + * @param progress + * A value between 0.0 and 1.0, indicating the progress of the + * animation (0=start, 1=end). + */ + private void sizeOrPositionUpdated(final double progress) { + // Don't do anything if overlay element is not attached + if (!isAttached()) { + return; + } + // Calculate proper z-index + String zIndex = null; + try { + // Odd behaviour with Windows Hosted Mode forces us to use + // this redundant try/catch block (See dev.vaadin.com #2011) + zIndex = DOM.getStyleAttribute(getElement(), "zIndex"); + } catch (Exception ignore) { + // Ignored, will cause no harm + zIndex = "1000"; + } + if (zIndex == null) { + zIndex = "" + Z_INDEX; + } + // Calculate position and size + if (BrowserInfo.get().isIE()) { + // Shake IE + getOffsetHeight(); + getOffsetWidth(); + } + + PositionAndSize positionAndSize = new PositionAndSize(); + positionAndSize.left = getActualLeft(); + positionAndSize.top = getActualTop(); + positionAndSize.width = getOffsetWidth(); + positionAndSize.height = getOffsetHeight(); + + if (positionAndSize.width < 0) { + positionAndSize.width = 0; + } + if (positionAndSize.height < 0) { + positionAndSize.height = 0; + } + + // Animate the size + positionAndSize.setAnimationFromCenterProgress(progress); + + // Opera needs some shaking to get parts of the shadow showing + // properly + // (ticket #2704) + if (BrowserInfo.get().isOpera() && isShadowEnabled()) { + // Clear the height of all middle elements + DOM.getChild(shadow, 3).getStyle().setProperty("height", "auto"); + DOM.getChild(shadow, 4).getStyle().setProperty("height", "auto"); + DOM.getChild(shadow, 5).getStyle().setProperty("height", "auto"); + } + + // Update correct values + if (isShadowEnabled()) { + updateSizeAndPosition(shadow, positionAndSize); + DOM.setStyleAttribute(shadow, "zIndex", zIndex); + DOM.setStyleAttribute(shadow, "display", progress < 0.9 ? "none" + : ""); + } + updateSizeAndPosition((Element) Element.as(getShimElement()), + positionAndSize); + + // Opera fix, part 2 (ticket #2704) + if (BrowserInfo.get().isOpera() && isShadowEnabled()) { + // We'll fix the height of all the middle elements + DOM.getChild(shadow, 3) + .getStyle() + .setPropertyPx("height", + DOM.getChild(shadow, 3).getOffsetHeight()); + DOM.getChild(shadow, 4) + .getStyle() + .setPropertyPx("height", + DOM.getChild(shadow, 4).getOffsetHeight()); + DOM.getChild(shadow, 5) + .getStyle() + .setPropertyPx("height", + DOM.getChild(shadow, 5).getOffsetHeight()); + } + + // Attach to dom if not there already + if (isShadowEnabled() && !isShadowAttached()) { + RootPanel.get().getElement().insertBefore(shadow, getElement()); + sinkShadowEvents(); + } + if (!isShimAttached()) { + RootPanel.get().getElement() + .insertBefore(shimElement, getElement()); + } + + } + + private void updateSizeAndPosition(Element e, + PositionAndSize positionAndSize) { + e.getStyle().setLeft(positionAndSize.left, Unit.PX); + e.getStyle().setTop(positionAndSize.top, Unit.PX); + e.getStyle().setWidth(positionAndSize.width, Unit.PX); + e.getStyle().setHeight(positionAndSize.height, Unit.PX); + } + + protected class ResizeAnimation extends Animation { + @Override + protected void onUpdate(double progress) { + sizeOrPositionUpdated(progress); + } + } + + @Override + public void onClose(CloseEvent<PopupPanel> event) { + removeShadowIfPresent(); + } + + @Override + public void sinkEvents(int eventBitsToAdd) { + super.sinkEvents(eventBitsToAdd); + // Also sink events on the shadow if present + sinkShadowEvents(); + } + + private void sinkShadowEvents() { + if (isSinkShadowEvents() && isShadowAttached()) { + // Sink the same events as the actual overlay has sunk + DOM.sinkEvents(shadow, DOM.getEventsSunk(getElement())); + // Send events to VOverlay.onBrowserEvent + DOM.setEventListener(shadow, this); + } + } + + private void unsinkShadowEvents() { + if (isShadowAttached()) { + DOM.setEventListener(shadow, null); + DOM.sinkEvents(shadow, 0); + } + } + + /** + * Enables or disables sinking the events of the shadow to the same + * onBrowserEvent as events to the actual overlay goes. + * + * Please note, that if you enable this, you can't assume that e.g. + * event.getEventTarget returns an element inside the DOM structure of the + * overlay + * + * @param sinkShadowEvents + */ + protected void setSinkShadowEvents(boolean sinkShadowEvents) { + this.sinkShadowEvents = sinkShadowEvents; + if (sinkShadowEvents) { + sinkShadowEvents(); + } else { + unsinkShadowEvents(); + } + } + + protected boolean isSinkShadowEvents() { + return sinkShadowEvents; + } + + /** + * Get owner (Widget that made this VOverlay, not the layout parent) of + * VOverlay + * + * @return Owner (creator) or null if not defined + */ + public Widget getOwner() { + return owner; + } + + /** + * Set owner (Widget that made this VOverlay, not the layout parent) of + * VOverlay + * + * @param owner + * Owner (creator) of VOverlay + */ + public void setOwner(Widget owner) { + this.owner = owner; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/VUnknownComponent.java b/client/src/com/vaadin/terminal/gwt/client/ui/VUnknownComponent.java new file mode 100644 index 0000000000..f4c925a313 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/VUnknownComponent.java @@ -0,0 +1,40 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.vaadin.terminal.gwt.client.SimpleTree; + +public class VUnknownComponent extends Composite { + + com.google.gwt.user.client.ui.Label caption = new com.google.gwt.user.client.ui.Label();; + SimpleTree uidlTree; + protected VerticalPanel panel; + + public VUnknownComponent() { + panel = new VerticalPanel(); + panel.add(caption); + initWidget(panel); + setStyleName("vaadin-unknown"); + caption.setStyleName("vaadin-unknown-caption"); + } + + public void setCaption(String c) { + caption.getElement().setInnerHTML(c); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/Vaadin6Connector.java b/client/src/com/vaadin/terminal/gwt/client/ui/Vaadin6Connector.java new file mode 100644 index 0000000000..8d743bb10b --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/Vaadin6Connector.java @@ -0,0 +1,30 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui; + +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; + +public abstract class Vaadin6Connector extends AbstractComponentConnector + implements Paintable { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + ((Paintable) getWidget()).updateFromUIDL(uidl, client); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/AbsoluteLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/AbsoluteLayoutConnector.java new file mode 100644 index 0000000000..85de558f98 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/AbsoluteLayoutConnector.java @@ -0,0 +1,230 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.absolutelayout; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.LayoutClickRpc; +import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutServerRpc; +import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutState; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.DirectionalManagedLayout; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; +import com.vaadin.terminal.gwt.client.ui.absolutelayout.VAbsoluteLayout.AbsoluteWrapper; +import com.vaadin.ui.AbsoluteLayout; + +@Connect(AbsoluteLayout.class) +public class AbsoluteLayoutConnector extends + AbstractComponentContainerConnector implements DirectionalManagedLayout { + + private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler( + this) { + + @Override + protected ComponentConnector getChildComponent(Element element) { + return getConnectorForElement(element); + } + + @Override + protected LayoutClickRpc getLayoutClickRPC() { + return rpc; + }; + + }; + + private AbsoluteLayoutServerRpc rpc; + + private Map<String, AbsoluteWrapper> connectorIdToComponentWrapper = new HashMap<String, AbsoluteWrapper>(); + + @Override + protected void init() { + super.init(); + rpc = RpcProxy.create(AbsoluteLayoutServerRpc.class, this); + } + + /** + * Returns the deepest nested child component which contains "element". The + * child component is also returned if "element" is part of its caption. + * + * @param element + * An element that is a nested sub element of the root element in + * this layout + * @return The Paintable which the element is a part of. Null if the element + * belongs to the layout and not to a child. + */ + protected ComponentConnector getConnectorForElement(Element element) { + return Util.getConnectorForElement(getConnection(), getWidget(), + element); + } + + @Override + public void updateCaption(ComponentConnector component) { + VAbsoluteLayout absoluteLayoutWidget = getWidget(); + AbsoluteWrapper componentWrapper = getWrapper(component); + + boolean captionIsNeeded = VCaption.isNeeded(component.getState()); + + VCaption caption = componentWrapper.getCaption(); + + if (captionIsNeeded) { + if (caption == null) { + caption = new VCaption(component, getConnection()); + absoluteLayoutWidget.add(caption); + componentWrapper.setCaption(caption); + } + caption.updateCaption(); + componentWrapper.updateCaptionPosition(); + } else { + if (caption != null) { + caption.removeFromParent(); + } + } + + } + + @Override + public VAbsoluteLayout getWidget() { + return (VAbsoluteLayout) super.getWidget(); + } + + @Override + public AbsoluteLayoutState getState() { + return (AbsoluteLayoutState) super.getState(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + clickEventHandler.handleEventHandlerRegistration(); + + // TODO Margin handling + + for (ComponentConnector child : getChildComponents()) { + getWrapper(child).setPosition( + getState().getConnectorPosition(child)); + } + }; + + private AbsoluteWrapper getWrapper(ComponentConnector child) { + String childId = child.getConnectorId(); + AbsoluteWrapper wrapper = connectorIdToComponentWrapper.get(childId); + if (wrapper != null) { + return wrapper; + } + + wrapper = new AbsoluteWrapper(child.getWidget()); + connectorIdToComponentWrapper.put(childId, wrapper); + getWidget().add(wrapper); + return wrapper; + + } + + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + super.onConnectorHierarchyChange(event); + + for (ComponentConnector child : getChildComponents()) { + getWrapper(child); + } + + for (ComponentConnector oldChild : event.getOldChildren()) { + if (oldChild.getParent() != this) { + String connectorId = oldChild.getConnectorId(); + AbsoluteWrapper absoluteWrapper = connectorIdToComponentWrapper + .remove(connectorId); + absoluteWrapper.destroy(); + } + } + } + + @Override + public void layoutVertically() { + VAbsoluteLayout layout = getWidget(); + for (ComponentConnector paintable : getChildComponents()) { + Widget widget = paintable.getWidget(); + AbsoluteWrapper wrapper = (AbsoluteWrapper) widget.getParent(); + Style wrapperStyle = wrapper.getElement().getStyle(); + + if (paintable.isRelativeHeight()) { + int h; + if (wrapper.top != null && wrapper.bottom != null) { + h = wrapper.getOffsetHeight(); + } else if (wrapper.bottom != null) { + // top not defined, available space 0... bottom of + // wrapper + h = wrapper.getElement().getOffsetTop() + + wrapper.getOffsetHeight(); + } else { + // top defined or both undefined, available space == + // canvas - top + h = layout.canvas.getOffsetHeight() + - wrapper.getElement().getOffsetTop(); + } + wrapperStyle.setHeight(h, Unit.PX); + getLayoutManager().reportHeightAssignedToRelative(paintable, h); + } else { + wrapperStyle.clearHeight(); + } + + wrapper.updateCaptionPosition(); + } + } + + @Override + public void layoutHorizontally() { + VAbsoluteLayout layout = getWidget(); + for (ComponentConnector paintable : getChildComponents()) { + AbsoluteWrapper wrapper = getWrapper(paintable); + Style wrapperStyle = wrapper.getElement().getStyle(); + + if (paintable.isRelativeWidth()) { + int w; + if (wrapper.left != null && wrapper.right != null) { + w = wrapper.getOffsetWidth(); + } else if (wrapper.right != null) { + // left == null + // available width == right edge == offsetleft + width + w = wrapper.getOffsetWidth() + + wrapper.getElement().getOffsetLeft(); + } else { + // left != null && right == null || left == null && + // right == null + // available width == canvas width - offset left + w = layout.canvas.getOffsetWidth() + - wrapper.getElement().getOffsetLeft(); + } + wrapperStyle.setWidth(w, Unit.PX); + getLayoutManager().reportWidthAssignedToRelative(paintable, w); + } else { + wrapperStyle.clearWidth(); + } + + wrapper.updateCaptionPosition(); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/VAbsoluteLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/VAbsoluteLayout.java new file mode 100644 index 0000000000..8c572417df --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/VAbsoluteLayout.java @@ -0,0 +1,146 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.absolutelayout; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.VCaption; + +public class VAbsoluteLayout extends ComplexPanel { + + /** Tag name for widget creation */ + public static final String TAGNAME = "absolutelayout"; + + /** Class name, prefix in styling */ + public static final String CLASSNAME = "v-absolutelayout"; + + private DivElement marginElement; + + protected final Element canvas = DOM.createDiv(); + + private Object previousStyleName; + + protected ApplicationConnection client; + + public VAbsoluteLayout() { + setElement(Document.get().createDivElement()); + setStyleName(CLASSNAME); + marginElement = Document.get().createDivElement(); + canvas.getStyle().setProperty("position", "relative"); + canvas.getStyle().setProperty("overflow", "hidden"); + marginElement.appendChild(canvas); + getElement().appendChild(marginElement); + + canvas.setClassName(CLASSNAME + "-canvas"); + canvas.setClassName(CLASSNAME + "-margin"); + } + + @Override + public void add(Widget child) { + super.add(child, canvas); + } + + public static class AbsoluteWrapper extends SimplePanel { + private String css; + String left; + String top; + String right; + String bottom; + private String zIndex; + + private VCaption caption; + + public AbsoluteWrapper(Widget child) { + setWidget(child); + setStyleName(CLASSNAME + "-wrapper"); + } + + public VCaption getCaption() { + return caption; + } + + public void setCaption(VCaption caption) { + this.caption = caption; + } + + public void destroy() { + if (caption != null) { + caption.removeFromParent(); + } + removeFromParent(); + } + + public void setPosition(String stringAttribute) { + if (css == null || !css.equals(stringAttribute)) { + css = stringAttribute; + top = right = bottom = left = zIndex = null; + if (!css.equals("")) { + String[] properties = css.split(";"); + for (int i = 0; i < properties.length; i++) { + String[] keyValue = properties[i].split(":"); + if (keyValue[0].equals("left")) { + left = keyValue[1]; + } else if (keyValue[0].equals("top")) { + top = keyValue[1]; + } else if (keyValue[0].equals("right")) { + right = keyValue[1]; + } else if (keyValue[0].equals("bottom")) { + bottom = keyValue[1]; + } else if (keyValue[0].equals("z-index")) { + zIndex = keyValue[1]; + } + } + } + // ensure ne values + Style style = getElement().getStyle(); + /* + * IE8 dies when nulling zIndex, even in IE7 mode. All other css + * properties (and even in older IE's) accept null values just + * fine. Assign empty string instead of null. + */ + if (zIndex != null) { + style.setProperty("zIndex", zIndex); + } else { + style.setProperty("zIndex", ""); + } + style.setProperty("top", top); + style.setProperty("left", left); + style.setProperty("right", right); + style.setProperty("bottom", bottom); + + } + updateCaptionPosition(); + } + + void updateCaptionPosition() { + if (caption != null) { + Style style = caption.getElement().getStyle(); + style.setProperty("position", "absolute"); + style.setPropertyPx("left", getElement().getOffsetLeft()); + style.setPropertyPx("top", getElement().getOffsetTop() + - caption.getHeight()); + } + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/accordion/AccordionConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/accordion/AccordionConnector.java new file mode 100644 index 0000000000..b107f41285 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/accordion/AccordionConnector.java @@ -0,0 +1,90 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.accordion; + +import java.util.Iterator; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout; +import com.vaadin.terminal.gwt.client.ui.accordion.VAccordion.StackItem; +import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren; +import com.vaadin.terminal.gwt.client.ui.tabsheet.TabsheetBaseConnector; +import com.vaadin.ui.Accordion; + +@Connect(Accordion.class) +public class AccordionConnector extends TabsheetBaseConnector implements + SimpleManagedLayout, MayScrollChildren { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + getWidget().selectedUIDLItemIndex = -1; + super.updateFromUIDL(uidl, client); + /* + * Render content after all tabs have been created and we know how large + * the content area is + */ + if (getWidget().selectedUIDLItemIndex >= 0) { + StackItem selectedItem = getWidget().getStackItem( + getWidget().selectedUIDLItemIndex); + UIDL selectedTabUIDL = getWidget().lazyUpdateMap + .remove(selectedItem); + getWidget().open(getWidget().selectedUIDLItemIndex); + + selectedItem.setContent(selectedTabUIDL); + } else if (isRealUpdate(uidl) && getWidget().openTab != null) { + getWidget().close(getWidget().openTab); + } + + getWidget().iLayout(); + // finally render possible hidden tabs + if (getWidget().lazyUpdateMap.size() > 0) { + for (Iterator iterator = getWidget().lazyUpdateMap.keySet() + .iterator(); iterator.hasNext();) { + StackItem item = (StackItem) iterator.next(); + item.setContent(getWidget().lazyUpdateMap.get(item)); + } + getWidget().lazyUpdateMap.clear(); + } + + } + + @Override + public VAccordion getWidget() { + return (VAccordion) super.getWidget(); + } + + @Override + public void updateCaption(ComponentConnector component) { + /* Accordion does not render its children's captions */ + } + + @Override + public void layout() { + VAccordion accordion = getWidget(); + + accordion.updateOpenTabSize(); + + if (isUndefinedHeight()) { + accordion.openTab.setHeightFromWidget(); + } + accordion.iLayout(); + + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/accordion/VAccordion.java b/client/src/com/vaadin/terminal/gwt/client/ui/accordion/VAccordion.java new file mode 100644 index 0000000000..d911dc66f3 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/accordion/VAccordion.java @@ -0,0 +1,527 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.accordion; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +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.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.tabsheet.TabsheetBaseConstants; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate.TouchScrollHandler; +import com.vaadin.terminal.gwt.client.ui.tabsheet.VTabsheetBase; + +public class VAccordion extends VTabsheetBase { + + public static final String CLASSNAME = "v-accordion"; + + private Set<Widget> widgets = new HashSet<Widget>(); + + HashMap<StackItem, UIDL> lazyUpdateMap = new HashMap<StackItem, UIDL>(); + + StackItem openTab = null; + + int selectedUIDLItemIndex = -1; + + private final TouchScrollHandler touchScrollHandler; + + public VAccordion() { + super(CLASSNAME); + touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this); + } + + @Override + protected void renderTab(UIDL tabUidl, int index, boolean selected, + boolean hidden) { + StackItem item; + int itemIndex; + if (getWidgetCount() <= index) { + // Create stackItem and render caption + item = new StackItem(tabUidl); + if (getWidgetCount() == 0) { + item.addStyleDependentName("first"); + } + itemIndex = getWidgetCount(); + add(item, getElement()); + } else { + item = getStackItem(index); + item = moveStackItemIfNeeded(item, index, tabUidl); + itemIndex = index; + } + item.updateCaption(tabUidl); + + item.setVisible(!hidden); + + if (selected) { + selectedUIDLItemIndex = itemIndex; + } + + if (tabUidl.getChildCount() > 0) { + lazyUpdateMap.put(item, tabUidl.getChildUIDL(0)); + } + } + + /** + * This method tries to find out if a tab has been rendered with a different + * index previously. If this is the case it re-orders the children so the + * same StackItem is used for rendering this time. E.g. if the first tab has + * been removed all tabs which contain cached content must be moved 1 step + * up to preserve the cached content. + * + * @param item + * @param newIndex + * @param tabUidl + * @return + */ + private StackItem moveStackItemIfNeeded(StackItem item, int newIndex, + UIDL tabUidl) { + UIDL tabContentUIDL = null; + ComponentConnector tabContent = null; + if (tabUidl.getChildCount() > 0) { + tabContentUIDL = tabUidl.getChildUIDL(0); + tabContent = client.getPaintable(tabContentUIDL); + } + + Widget itemWidget = item.getComponent(); + if (tabContent != null) { + if (tabContent != itemWidget) { + /* + * This is not the same widget as before, find out if it has + * been moved + */ + int oldIndex = -1; + StackItem oldItem = null; + for (int i = 0; i < getWidgetCount(); i++) { + Widget w = getWidget(i); + oldItem = (StackItem) w; + if (tabContent == oldItem.getComponent()) { + oldIndex = i; + break; + } + } + + if (oldIndex != -1 && oldIndex > newIndex) { + /* + * The tab has previously been rendered in another position + * so we must move the cached content to correct position. + * We move only items with oldIndex > newIndex to prevent + * moving items already rendered in this update. If for + * instance tabs 1,2,3 are removed and added as 3,2,1 we + * cannot re-use "1" when we get to the third tab. + */ + insert(oldItem, getElement(), newIndex, true); + return oldItem; + } + } + } else { + // Tab which has never been loaded. Must assure we use an empty + // StackItem + Widget oldWidget = item.getComponent(); + if (oldWidget != null) { + oldWidget.removeFromParent(); + } + } + return item; + } + + void open(int itemIndex) { + StackItem item = (StackItem) getWidget(itemIndex); + boolean alreadyOpen = false; + if (openTab != null) { + if (openTab.isOpen()) { + if (openTab == item) { + alreadyOpen = true; + } else { + openTab.close(); + } + } + } + if (!alreadyOpen) { + item.open(); + activeTabIndex = itemIndex; + openTab = item; + } + + // Update the size for the open tab + updateOpenTabSize(); + } + + void close(StackItem item) { + if (!item.isOpen()) { + return; + } + + item.close(); + activeTabIndex = -1; + openTab = null; + + } + + @Override + protected void selectTab(final int index, final UIDL contentUidl) { + StackItem item = getStackItem(index); + if (index != activeTabIndex) { + open(index); + iLayout(); + // TODO Check if this is needed + client.runDescendentsLayout(this); + + } + item.setContent(contentUidl); + } + + public void onSelectTab(StackItem item) { + final int index = getWidgetIndex(item); + if (index != activeTabIndex && !disabled && !readonly + && !disabledTabKeys.contains(tabKeys.get(index))) { + addStyleDependentName("loading"); + client.updateVariable(id, "selected", "" + tabKeys.get(index), true); + } + } + + /** + * Sets the size of the open tab + */ + void updateOpenTabSize() { + if (openTab == null) { + return; + } + + // WIDTH + if (!isDynamicWidth()) { + openTab.setWidth("100%"); + } else { + openTab.setWidth(null); + } + + // HEIGHT + if (!isDynamicHeight()) { + int usedPixels = 0; + for (Widget w : getChildren()) { + StackItem item = (StackItem) w; + if (item == openTab) { + usedPixels += item.getCaptionHeight(); + } else { + // This includes the captionNode borders + usedPixels += item.getHeight(); + } + } + + int offsetHeight = getOffsetHeight(); + + int spaceForOpenItem = offsetHeight - usedPixels; + + if (spaceForOpenItem < 0) { + spaceForOpenItem = 0; + } + + openTab.setHeight(spaceForOpenItem); + } else { + openTab.setHeightFromWidget(); + + } + + } + + public void iLayout() { + if (openTab == null) { + return; + } + + if (isDynamicWidth()) { + int maxWidth = 40; + for (Widget w : getChildren()) { + StackItem si = (StackItem) w; + int captionWidth = si.getCaptionWidth(); + if (captionWidth > maxWidth) { + maxWidth = captionWidth; + } + } + int widgetWidth = openTab.getWidgetWidth(); + if (widgetWidth > maxWidth) { + maxWidth = widgetWidth; + } + super.setWidth(maxWidth + "px"); + openTab.setWidth(maxWidth); + } + } + + /** + * A StackItem has always two children, Child 0 is a VCaption, Child 1 is + * the actual child widget. + */ + protected class StackItem extends ComplexPanel implements ClickHandler { + + public void setHeight(int height) { + if (height == -1) { + super.setHeight(""); + DOM.setStyleAttribute(content, "height", "0px"); + } else { + super.setHeight((height + getCaptionHeight()) + "px"); + DOM.setStyleAttribute(content, "height", height + "px"); + DOM.setStyleAttribute(content, "top", getCaptionHeight() + "px"); + + } + } + + public Widget getComponent() { + if (getWidgetCount() < 2) { + return null; + } + return getWidget(1); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + } + + public void setHeightFromWidget() { + Widget widget = getChildWidget(); + if (widget == null) { + return; + } + + int paintableHeight = widget.getElement().getOffsetHeight(); + setHeight(paintableHeight); + + } + + /** + * Returns caption width including padding + * + * @return + */ + public int getCaptionWidth() { + if (caption == null) { + return 0; + } + + int captionWidth = caption.getRequiredWidth(); + int padding = Util.measureHorizontalPaddingAndBorder( + caption.getElement(), 18); + return captionWidth + padding; + } + + public void setWidth(int width) { + if (width == -1) { + super.setWidth(""); + } else { + super.setWidth(width + "px"); + } + } + + public int getHeight() { + return getOffsetHeight(); + } + + public int getCaptionHeight() { + return captionNode.getOffsetHeight(); + } + + private VCaption caption; + private boolean open = false; + private Element content = DOM.createDiv(); + private Element captionNode = DOM.createDiv(); + + public StackItem(UIDL tabUidl) { + setElement(DOM.createDiv()); + caption = new VCaption(client); + caption.addClickHandler(this); + super.add(caption, captionNode); + DOM.appendChild(captionNode, caption.getElement()); + DOM.appendChild(getElement(), captionNode); + DOM.appendChild(getElement(), content); + + getElement().addClassName(CLASSNAME + "-item"); + captionNode.addClassName(CLASSNAME + "-item-caption"); + content.addClassName(CLASSNAME + "-item-content"); + + touchScrollHandler.addElement(getContainerElement()); + + close(); + } + + @Override + public void onBrowserEvent(Event event) { + onSelectTab(this); + } + + public Element getContainerElement() { + return content; + } + + public Widget getChildWidget() { + if (getWidgetCount() > 1) { + return getWidget(1); + } else { + return null; + } + } + + public void replaceWidget(Widget newWidget) { + if (getWidgetCount() > 1) { + Widget oldWidget = getWidget(1); + ComponentConnector oldPaintable = ConnectorMap.get(client) + .getConnector(oldWidget); + ConnectorMap.get(client).unregisterConnector(oldPaintable); + widgets.remove(oldWidget); + remove(1); + } + add(newWidget, content); + widgets.add(newWidget); + } + + public void open() { + open = true; + DOM.setStyleAttribute(content, "top", getCaptionHeight() + "px"); + DOM.setStyleAttribute(content, "left", "0px"); + DOM.setStyleAttribute(content, "visibility", ""); + addStyleDependentName("open"); + } + + public void hide() { + DOM.setStyleAttribute(content, "visibility", "hidden"); + } + + public void close() { + DOM.setStyleAttribute(content, "visibility", "hidden"); + DOM.setStyleAttribute(content, "top", "-100000px"); + DOM.setStyleAttribute(content, "left", "-100000px"); + removeStyleDependentName("open"); + setHeight(-1); + setWidth(""); + open = false; + } + + public boolean isOpen() { + return open; + } + + public void setContent(UIDL contentUidl) { + final ComponentConnector newPntbl = client + .getPaintable(contentUidl); + Widget newWidget = newPntbl.getWidget(); + if (getChildWidget() == null) { + add(newWidget, content); + widgets.add(newWidget); + } else if (getChildWidget() != newWidget) { + replaceWidget(newWidget); + } + if (contentUidl.getBooleanAttribute("cached")) { + /* + * The size of a cached, relative sized component must be + * updated to report correct size. + */ + client.handleComponentRelativeSize(newPntbl.getWidget()); + } + if (isOpen() && isDynamicHeight()) { + setHeightFromWidget(); + } + } + + @Override + public void onClick(ClickEvent event) { + onSelectTab(this); + } + + public void updateCaption(UIDL uidl) { + // TODO need to call this because the caption does not have an owner + caption.updateCaptionWithoutOwner( + uidl.getStringAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_CAPTION), + uidl.hasAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_DISABLED), + uidl.hasAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_DESCRIPTION), + uidl.hasAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_ERROR_MESSAGE), + uidl.getStringAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_ICON)); + } + + public int getWidgetWidth() { + return DOM.getFirstChild(content).getOffsetWidth(); + } + + public boolean contains(ComponentConnector p) { + return (getChildWidget() == p.getWidget()); + } + + public boolean isCaptionVisible() { + return caption.isVisible(); + } + + } + + @Override + protected void clearPaintables() { + clear(); + } + + boolean isDynamicWidth() { + ComponentConnector paintable = ConnectorMap.get(client).getConnector( + this); + return paintable.isUndefinedWidth(); + } + + boolean isDynamicHeight() { + ComponentConnector paintable = ConnectorMap.get(client).getConnector( + this); + return paintable.isUndefinedHeight(); + } + + @Override + @SuppressWarnings("unchecked") + protected Iterator<Widget> getWidgetIterator() { + return widgets.iterator(); + } + + @Override + protected int getTabCount() { + return getWidgetCount(); + } + + @Override + protected void removeTab(int index) { + StackItem item = getStackItem(index); + remove(item); + touchScrollHandler.removeElement(item.getContainerElement()); + } + + @Override + protected ComponentConnector getTab(int index) { + if (index < getWidgetCount()) { + Widget w = getStackItem(index); + return ConnectorMap.get(client).getConnector(w); + } + + return null; + } + + StackItem getStackItem(int index) { + return (StackItem) getWidget(index); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/audio/AudioConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/audio/AudioConnector.java new file mode 100644 index 0000000000..df08e44f16 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/audio/AudioConnector.java @@ -0,0 +1,57 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.audio; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.MediaBaseConnector; +import com.vaadin.ui.Audio; + +@Connect(Audio.class) +public class AudioConnector extends MediaBaseConnector { + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + Style style = getWidget().getElement().getStyle(); + + // Make sure that the controls are not clipped if visible. + if (getState().isShowControls() + && (style.getHeight() == null || "".equals(style.getHeight()))) { + if (BrowserInfo.get().isChrome()) { + style.setHeight(32, Unit.PX); + } else { + style.setHeight(25, Unit.PX); + } + } + } + + @Override + protected Widget createWidget() { + return GWT.create(VAudio.class); + } + + @Override + protected String getDefaultAltHtml() { + return "Your browser does not support the <code>audio</code> element."; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/audio/VAudio.java b/client/src/com/vaadin/terminal/gwt/client/ui/audio/VAudio.java new file mode 100644 index 0000000000..121ad3cc94 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/audio/VAudio.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.audio; + +import com.google.gwt.dom.client.AudioElement; +import com.google.gwt.dom.client.Document; +import com.vaadin.terminal.gwt.client.ui.VMediaBase; + +public class VAudio extends VMediaBase { + private static String CLASSNAME = "v-audio"; + + private AudioElement audio; + + public VAudio() { + audio = Document.get().createAudioElement(); + setMediaElement(audio); + setStyleName(CLASSNAME); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/button/ButtonConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/button/ButtonConnector.java new file mode 100644 index 0000000000..59e187014c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/button/ButtonConnector.java @@ -0,0 +1,148 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.button; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.button.ButtonServerRpc; +import com.vaadin.shared.ui.button.ButtonState; +import com.vaadin.terminal.gwt.client.EventHelper; +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector; +import com.vaadin.terminal.gwt.client.ui.Icon; +import com.vaadin.ui.Button; + +@Connect(value = Button.class, loadStyle = LoadStyle.EAGER) +public class ButtonConnector extends AbstractComponentConnector implements + BlurHandler, FocusHandler, ClickHandler { + + private ButtonServerRpc rpc = RpcProxy.create(ButtonServerRpc.class, this); + private FocusAndBlurServerRpc focusBlurProxy = RpcProxy.create( + FocusAndBlurServerRpc.class, this); + + private HandlerRegistration focusHandlerRegistration = null; + private HandlerRegistration blurHandlerRegistration = null; + + @Override + public boolean delegateCaptionHandling() { + return false; + } + + @Override + public void init() { + super.init(); + getWidget().addClickHandler(this); + getWidget().client = getConnection(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + focusHandlerRegistration = EventHelper.updateFocusHandler(this, + focusHandlerRegistration); + blurHandlerRegistration = EventHelper.updateBlurHandler(this, + blurHandlerRegistration); + // Set text + if (getState().isHtmlContentAllowed()) { + getWidget().setHtml(getState().getCaption()); + } else { + getWidget().setText(getState().getCaption()); + } + + // handle error + if (null != getState().getErrorMessage()) { + if (getWidget().errorIndicatorElement == null) { + getWidget().errorIndicatorElement = DOM.createSpan(); + getWidget().errorIndicatorElement + .setClassName("v-errorindicator"); + } + getWidget().wrapper.insertBefore(getWidget().errorIndicatorElement, + getWidget().captionElement); + + } else if (getWidget().errorIndicatorElement != null) { + getWidget().wrapper.removeChild(getWidget().errorIndicatorElement); + getWidget().errorIndicatorElement = null; + } + + if (getState().getIcon() != null) { + if (getWidget().icon == null) { + getWidget().icon = new Icon(getConnection()); + getWidget().wrapper.insertBefore(getWidget().icon.getElement(), + getWidget().captionElement); + } + getWidget().icon.setUri(getState().getIcon().getURL()); + } else { + if (getWidget().icon != null) { + getWidget().wrapper.removeChild(getWidget().icon.getElement()); + getWidget().icon = null; + } + } + + getWidget().clickShortcut = getState().getClickShortcutKeyCode(); + } + + @Override + public VButton getWidget() { + return (VButton) super.getWidget(); + } + + @Override + public ButtonState getState() { + return (ButtonState) super.getState(); + } + + @Override + public void onFocus(FocusEvent event) { + // EventHelper.updateFocusHandler ensures that this is called only when + // there is a listener on server side + focusBlurProxy.focus(); + } + + @Override + public void onBlur(BlurEvent event) { + // EventHelper.updateFocusHandler ensures that this is called only when + // there is a listener on server side + focusBlurProxy.blur(); + } + + @Override + public void onClick(ClickEvent event) { + if (getState().isDisableOnClick()) { + getWidget().setEnabled(false); + rpc.disableOnClick(); + } + + // Add mouse details + MouseEventDetails details = MouseEventDetailsBuilder + .buildMouseEventDetails(event.getNativeEvent(), getWidget() + .getElement()); + rpc.click(details); + + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/button/VButton.java b/client/src/com/vaadin/terminal/gwt/client/ui/button/VButton.java new file mode 100644 index 0000000000..baacac08ed --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/button/VButton.java @@ -0,0 +1,431 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.button; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.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.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.Accessibility; +import com.google.gwt.user.client.ui.FocusWidget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.Icon; + +public class VButton extends FocusWidget implements ClickHandler { + + public static final String CLASSNAME = "v-button"; + private static final String CLASSNAME_PRESSED = "v-pressed"; + + // mouse movement is checked before synthesizing click event on mouseout + protected static int MOVE_THRESHOLD = 3; + protected int mousedownX = 0; + protected int mousedownY = 0; + + protected ApplicationConnection client; + + protected final Element wrapper = DOM.createSpan(); + + protected Element errorIndicatorElement; + + protected final Element captionElement = DOM.createSpan(); + + protected Icon icon; + + /** + * Helper flag to handle special-case where the button is moved from under + * mouse while clicking it. In this case mouse leaves the button without + * moving. + */ + protected boolean clickPending; + + private boolean enabled = true; + + private int tabIndex = 0; + + /* + * BELOW PRIVATE MEMBERS COPY-PASTED FROM GWT CustomButton + */ + + /** + * If <code>true</code>, this widget is capturing with the mouse held down. + */ + private boolean isCapturing; + + /** + * If <code>true</code>, this widget has focus with the space bar down. This + * means that we will get events when the button is released, but we should + * trigger the button only if the button is still focused at that point. + */ + private boolean isFocusing; + + /** + * Used to decide whether to allow clicks to propagate up to the superclass + * or container elements. + */ + private boolean disallowNextClick = false; + private boolean isHovering; + + protected int clickShortcut = 0; + + private HandlerRegistration focusHandlerRegistration; + private HandlerRegistration blurHandlerRegistration; + + /** + * If caption should be rendered in HTML + */ + protected boolean htmlCaption = false; + + public VButton() { + super(DOM.createDiv()); + setTabIndex(0); + sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.FOCUSEVENTS + | Event.KEYEVENTS); + + setStyleName(CLASSNAME); + + // Add a11y role "button" + Accessibility.setRole(getElement(), Accessibility.ROLE_BUTTON); + + wrapper.setClassName(getStylePrimaryName() + "-wrap"); + getElement().appendChild(wrapper); + captionElement.setClassName(getStylePrimaryName() + "-caption"); + wrapper.appendChild(captionElement); + + addClickHandler(this); + } + + public void setText(String text) { + captionElement.setInnerText(text); + } + + public void setHtml(String html) { + captionElement.setInnerHTML(html); + } + + @SuppressWarnings("deprecation") + @Override + /* + * Copy-pasted from GWT CustomButton, some minor modifications done: + * + * -for IE/Opera added CLASSNAME_PRESSED + * + * -event.preventDefault() commented from ONMOUSEDOWN (Firefox won't apply + * :active styles if it is present) + * + * -Tooltip event handling added + * + * -onload event handler added (for icon handling) + */ + public void onBrowserEvent(Event event) { + if (DOM.eventGetType(event) == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + } + // Should not act on button if disabled. + if (!isEnabled()) { + // This can happen when events are bubbled up from non-disabled + // children + return; + } + + int type = DOM.eventGetType(event); + switch (type) { + case Event.ONCLICK: + // If clicks are currently disallowed, keep it from bubbling or + // being passed to the superclass. + if (disallowNextClick) { + event.stopPropagation(); + disallowNextClick = false; + return; + } + break; + case Event.ONMOUSEDOWN: + if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) { + // This was moved from mouseover, which iOS sometimes skips. + // We're certainly hovering at this point, and we don't actually + // need that information before this point. + setHovering(true); + } + if (event.getButton() == Event.BUTTON_LEFT) { + // save mouse position to detect movement before synthesizing + // event later + mousedownX = event.getClientX(); + mousedownY = event.getClientY(); + + disallowNextClick = true; + clickPending = true; + setFocus(true); + DOM.setCapture(getElement()); + isCapturing = true; + // Prevent dragging (on some browsers); + // DOM.eventPreventDefault(event); + if (BrowserInfo.get().isIE() || BrowserInfo.get().isOpera()) { + addStyleName(CLASSNAME_PRESSED); + } + } + break; + case Event.ONMOUSEUP: + if (isCapturing) { + isCapturing = false; + DOM.releaseCapture(getElement()); + if (isHovering() && event.getButton() == Event.BUTTON_LEFT) { + // Click ok + disallowNextClick = false; + } + if (BrowserInfo.get().isIE() || BrowserInfo.get().isOpera()) { + removeStyleName(CLASSNAME_PRESSED); + } + // Explicitly prevent IE 8 from propagating mouseup events + // upward (fixes #6753) + if (BrowserInfo.get().isIE8()) { + event.stopPropagation(); + } + } + break; + case Event.ONMOUSEMOVE: + clickPending = false; + if (isCapturing) { + // Prevent dragging (on other browsers); + DOM.eventPreventDefault(event); + } + break; + case Event.ONMOUSEOUT: + Element to = event.getRelatedTarget(); + if (getElement().isOrHasChild(DOM.eventGetTarget(event)) + && (to == null || !getElement().isOrHasChild(to))) { + if (clickPending + && Math.abs(mousedownX - event.getClientX()) < MOVE_THRESHOLD + && Math.abs(mousedownY - event.getClientY()) < MOVE_THRESHOLD) { + onClick(); + break; + } + clickPending = false; + if (isCapturing) { + } + setHovering(false); + if (BrowserInfo.get().isIE() || BrowserInfo.get().isOpera()) { + removeStyleName(CLASSNAME_PRESSED); + } + } + break; + case Event.ONBLUR: + if (isFocusing) { + isFocusing = false; + } + break; + case Event.ONLOSECAPTURE: + if (isCapturing) { + isCapturing = false; + } + break; + } + + super.onBrowserEvent(event); + + // Synthesize clicks based on keyboard events AFTER the normal key + // handling. + if ((event.getTypeInt() & Event.KEYEVENTS) != 0) { + switch (type) { + case Event.ONKEYDOWN: + // Stop propagation when the user starts pressing a button that + // we are handling to prevent actions from getting triggered + if (event.getKeyCode() == 32 /* space */) { + isFocusing = true; + event.preventDefault(); + event.stopPropagation(); + } else if (event.getKeyCode() == KeyCodes.KEY_ENTER) { + event.stopPropagation(); + } + break; + case Event.ONKEYUP: + if (isFocusing && event.getKeyCode() == 32 /* space */) { + isFocusing = false; + onClick(); + event.stopPropagation(); + event.preventDefault(); + } + break; + case Event.ONKEYPRESS: + if (event.getKeyCode() == KeyCodes.KEY_ENTER) { + onClick(); + event.stopPropagation(); + event.preventDefault(); + } + break; + } + } + } + + final void setHovering(boolean hovering) { + if (hovering != isHovering()) { + isHovering = hovering; + } + } + + final boolean isHovering() { + return isHovering; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event + * .dom.client.ClickEvent) + */ + @Override + public void onClick(ClickEvent event) { + if (BrowserInfo.get().isSafari()) { + VButton.this.setFocus(true); + } + + clickPending = false; + } + + /* + * ALL BELOW COPY-PASTED FROM GWT CustomButton + */ + + /** + * Called internally when the user finishes clicking on this button. The + * default behavior is to fire the click event to listeners. Subclasses that + * override {@link #onClickStart()} should override this method to restore + * the normal widget display. + * <p> + * To add custom code for a click event, override + * {@link #onClick(ClickEvent)} instead of this. + */ + protected void onClick() { + // Allow the click we're about to synthesize to pass through to the + // superclass and containing elements. Element.dispatchEvent() is + // synchronous, so we simply set and clear the flag within this method. + + disallowNextClick = false; + + // Mouse coordinates are not always available (e.g., when the click is + // caused by a keyboard event). + NativeEvent evt = Document.get().createClickEvent(1, 0, 0, 0, 0, false, + false, false, false); + getElement().dispatchEvent(evt); + } + + /** + * Sets whether this button is enabled. + * + * @param enabled + * <code>true</code> to enable the button, <code>false</code> to + * disable it + */ + + @Override + public final void setEnabled(boolean enabled) { + if (isEnabled() != enabled) { + this.enabled = enabled; + if (!enabled) { + cleanupCaptureState(); + Accessibility.removeState(getElement(), + Accessibility.STATE_PRESSED); + super.setTabIndex(-1); + } else { + Accessibility.setState(getElement(), + Accessibility.STATE_PRESSED, "false"); + super.setTabIndex(tabIndex); + } + } + } + + @Override + public final boolean isEnabled() { + return enabled; + } + + @Override + public final void setTabIndex(int index) { + super.setTabIndex(index); + tabIndex = index; + } + + /** + * Resets internal state if this button can no longer service events. This + * can occur when the widget becomes detached or disabled. + */ + private void cleanupCaptureState() { + if (isCapturing || isFocusing) { + DOM.releaseCapture(getElement()); + isCapturing = false; + isFocusing = false; + } + } + + private static native int getHorizontalBorderAndPaddingWidth(Element elem) + /*-{ + // THIS METHOD IS ONLY USED FOR INTERNET EXPLORER, IT DOESN'T WORK WITH OTHERS + + var convertToPixel = function(elem, value) { + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // Remember the original values + var left = elem.style.left, rsLeft = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + elem.style.left = value || 0; + var ret = elem.style.pixelLeft; + + // Revert the changed values + elem.style.left = left; + elem.runtimeStyle.left = rsLeft; + + return ret; + } + + var ret = 0; + + var sides = ["Right","Left"]; + for(var i=0; i<2; i++) { + var side = sides[i]; + var value; + // Border ------------------------------------------------------- + if(elem.currentStyle["border"+side+"Style"] != "none") { + value = elem.currentStyle["border"+side+"Width"]; + if ( !/^\d+(px)?$/i.test( value ) && /^\d/.test( value ) ) { + ret += convertToPixel(elem, value); + } else if(value.length > 2) { + ret += parseInt(value.substr(0, value.length-2)); + } + } + + // Padding ------------------------------------------------------- + value = elem.currentStyle["padding"+side]; + if ( !/^\d+(px)?$/i.test( value ) && /^\d/.test( value ) ) { + ret += convertToPixel(elem, value); + } else if(value.length > 2) { + ret += parseInt(value.substr(0, value.length-2)); + } + } + + return ret; + }-*/; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/CheckBoxConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/CheckBoxConnector.java new file mode 100644 index 0000000000..7968a04b5e --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/CheckBoxConnector.java @@ -0,0 +1,158 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.checkbox; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.checkbox.CheckBoxServerRpc; +import com.vaadin.shared.ui.checkbox.CheckBoxState; +import com.vaadin.terminal.gwt.client.EventHelper; +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder; +import com.vaadin.terminal.gwt.client.VTooltip; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector; +import com.vaadin.terminal.gwt.client.ui.Icon; +import com.vaadin.ui.CheckBox; + +@Connect(CheckBox.class) +public class CheckBoxConnector extends AbstractFieldConnector implements + FocusHandler, BlurHandler, ClickHandler { + + private HandlerRegistration focusHandlerRegistration; + private HandlerRegistration blurHandlerRegistration; + + private CheckBoxServerRpc rpc = RpcProxy.create(CheckBoxServerRpc.class, + this); + private FocusAndBlurServerRpc focusBlurRpc = RpcProxy.create( + FocusAndBlurServerRpc.class, this); + + @Override + public boolean delegateCaptionHandling() { + return false; + } + + @Override + protected void init() { + super.init(); + getWidget().addClickHandler(this); + getWidget().client = getConnection(); + getWidget().id = getConnectorId(); + + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + focusHandlerRegistration = EventHelper.updateFocusHandler(this, + focusHandlerRegistration); + blurHandlerRegistration = EventHelper.updateBlurHandler(this, + blurHandlerRegistration); + + if (null != getState().getErrorMessage()) { + if (getWidget().errorIndicatorElement == null) { + getWidget().errorIndicatorElement = DOM.createSpan(); + getWidget().errorIndicatorElement.setInnerHTML(" "); + DOM.setElementProperty(getWidget().errorIndicatorElement, + "className", "v-errorindicator"); + DOM.appendChild(getWidget().getElement(), + getWidget().errorIndicatorElement); + DOM.sinkEvents(getWidget().errorIndicatorElement, + VTooltip.TOOLTIP_EVENTS | Event.ONCLICK); + } else { + DOM.setStyleAttribute(getWidget().errorIndicatorElement, + "display", ""); + } + } else if (getWidget().errorIndicatorElement != null) { + DOM.setStyleAttribute(getWidget().errorIndicatorElement, "display", + "none"); + } + + if (isReadOnly()) { + getWidget().setEnabled(false); + } + + if (getState().getIcon() != null) { + if (getWidget().icon == null) { + getWidget().icon = new Icon(getConnection()); + DOM.insertChild(getWidget().getElement(), + getWidget().icon.getElement(), 1); + getWidget().icon.sinkEvents(VTooltip.TOOLTIP_EVENTS); + getWidget().icon.sinkEvents(Event.ONCLICK); + } + getWidget().icon.setUri(getState().getIcon().getURL()); + } else if (getWidget().icon != null) { + // detach icon + DOM.removeChild(getWidget().getElement(), + getWidget().icon.getElement()); + getWidget().icon = null; + } + + // Set text + getWidget().setText(getState().getCaption()); + getWidget().setValue(getState().isChecked()); + getWidget().immediate = getState().isImmediate(); + } + + @Override + public CheckBoxState getState() { + return (CheckBoxState) super.getState(); + } + + @Override + public VCheckBox getWidget() { + return (VCheckBox) super.getWidget(); + } + + @Override + public void onFocus(FocusEvent event) { + // EventHelper.updateFocusHandler ensures that this is called only when + // there is a listener on server side + focusBlurRpc.focus(); + } + + @Override + public void onBlur(BlurEvent event) { + // EventHelper.updateFocusHandler ensures that this is called only when + // there is a listener on server side + focusBlurRpc.blur(); + } + + @Override + public void onClick(ClickEvent event) { + if (!isEnabled()) { + return; + } + + // Add mouse details + MouseEventDetails details = MouseEventDetailsBuilder + .buildMouseEventDetails(event.getNativeEvent(), getWidget() + .getElement()); + rpc.setChecked(getWidget().getValue(), details); + + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/VCheckBox.java b/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/VCheckBox.java new file mode 100644 index 0000000000..c796b440f3 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/VCheckBox.java @@ -0,0 +1,69 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.checkbox; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VTooltip; +import com.vaadin.terminal.gwt.client.ui.Field; +import com.vaadin.terminal.gwt.client.ui.Icon; + +public class VCheckBox extends com.google.gwt.user.client.ui.CheckBox implements + Field { + + public static final String CLASSNAME = "v-checkbox"; + + String id; + + boolean immediate; + + ApplicationConnection client; + + Element errorIndicatorElement; + + Icon icon; + + public VCheckBox() { + setStyleName(CLASSNAME); + + Element el = DOM.getFirstChild(getElement()); + while (el != null) { + DOM.sinkEvents(el, + (DOM.getEventsSunk(el) | VTooltip.TOOLTIP_EVENTS)); + el = DOM.getNextSibling(el); + } + } + + @Override + public void onBrowserEvent(Event event) { + if (icon != null && (event.getTypeInt() == Event.ONCLICK) + && (DOM.eventGetTarget(event) == icon.getElement())) { + // Click on icon should do nothing if widget is disabled + if (isEnabled()) { + setValue(!getValue()); + } + } + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/combobox/ComboBoxConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/combobox/ComboBoxConnector.java new file mode 100644 index 0000000000..7be8a1e139 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/combobox/ComboBoxConnector.java @@ -0,0 +1,254 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.combobox; + +import java.util.Iterator; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.combobox.ComboBoxConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector; +import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout; +import com.vaadin.terminal.gwt.client.ui.combobox.VFilterSelect.FilterSelectSuggestion; +import com.vaadin.terminal.gwt.client.ui.menubar.MenuItem; +import com.vaadin.ui.Select; + +@Connect(Select.class) +public class ComboBoxConnector extends AbstractFieldConnector implements + Paintable, SimpleManagedLayout { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin.terminal + * .gwt.client.UIDL, com.vaadin.terminal.gwt.client.ApplicationConnection) + */ + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + // Save details + getWidget().client = client; + getWidget().paintableId = uidl.getId(); + + getWidget().readonly = isReadOnly(); + getWidget().enabled = isEnabled(); + + getWidget().tb.setEnabled(getWidget().enabled); + getWidget().updateReadOnly(); + + if (!isRealUpdate(uidl)) { + return; + } + + // Inverse logic here to make the default case (text input enabled) + // work without additional UIDL messages + boolean noTextInput = uidl + .hasAttribute(ComboBoxConstants.ATTR_NO_TEXT_INPUT) + && uidl.getBooleanAttribute(ComboBoxConstants.ATTR_NO_TEXT_INPUT); + getWidget().setTextInputEnabled(!noTextInput); + + // not a FocusWidget -> needs own tabindex handling + if (uidl.hasAttribute("tabindex")) { + getWidget().tb.setTabIndex(uidl.getIntAttribute("tabindex")); + } + + if (uidl.hasAttribute("filteringmode")) { + getWidget().filteringmode = uidl.getIntAttribute("filteringmode"); + } + + getWidget().immediate = getState().isImmediate(); + + getWidget().nullSelectionAllowed = uidl.hasAttribute("nullselect"); + + getWidget().nullSelectItem = uidl.hasAttribute("nullselectitem") + && uidl.getBooleanAttribute("nullselectitem"); + + getWidget().currentPage = uidl.getIntVariable("page"); + + if (uidl.hasAttribute("pagelength")) { + getWidget().pageLength = uidl.getIntAttribute("pagelength"); + } + + if (uidl.hasAttribute(ComboBoxConstants.ATTR_INPUTPROMPT)) { + // input prompt changed from server + getWidget().inputPrompt = uidl + .getStringAttribute(ComboBoxConstants.ATTR_INPUTPROMPT); + } else { + getWidget().inputPrompt = ""; + } + + getWidget().suggestionPopup.updateStyleNames(uidl, getState()); + + getWidget().allowNewItem = uidl.hasAttribute("allownewitem"); + getWidget().lastNewItemString = null; + + getWidget().currentSuggestions.clear(); + if (!getWidget().waitingForFilteringResponse) { + /* + * Clear the current suggestions as the server response always + * includes the new ones. Exception is when filtering, then we need + * to retain the value if the user does not select any of the + * options matching the filter. + */ + getWidget().currentSuggestion = null; + /* + * Also ensure no old items in menu. Unless cleared the old values + * may cause odd effects on blur events. Suggestions in menu might + * not necessary exist in select at all anymore. + */ + getWidget().suggestionPopup.menu.clearItems(); + + } + + final UIDL options = uidl.getChildUIDL(0); + if (uidl.hasAttribute("totalMatches")) { + getWidget().totalMatches = uidl.getIntAttribute("totalMatches"); + } else { + getWidget().totalMatches = 0; + } + + // used only to calculate minimum popup width + String captions = Util.escapeHTML(getWidget().inputPrompt); + + for (final Iterator<?> i = options.getChildIterator(); i.hasNext();) { + final UIDL optionUidl = (UIDL) i.next(); + final FilterSelectSuggestion suggestion = getWidget().new FilterSelectSuggestion( + optionUidl); + getWidget().currentSuggestions.add(suggestion); + if (optionUidl.hasAttribute("selected")) { + if (!getWidget().waitingForFilteringResponse + || getWidget().popupOpenerClicked) { + String newSelectedOptionKey = Integer.toString(suggestion + .getOptionKey()); + if (!newSelectedOptionKey + .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 = newSelectedOptionKey; + } + } + getWidget().currentSuggestion = suggestion; + getWidget().setSelectedItemIcon(suggestion.getIconUri()); + } + + // Collect captions so we can calculate minimum width for textarea + if (captions.length() > 0) { + captions += "|"; + } + captions += Util.escapeHTML(suggestion.getReplacementString()); + } + + if ((!getWidget().waitingForFilteringResponse || getWidget().popupOpenerClicked) + && uidl.hasVariable("selected") + && uidl.getStringArrayVariable("selected").length == 0) { + // select nulled + if (!getWidget().waitingForFilteringResponse + || !getWidget().popupOpenerClicked) { + if (!getWidget().focused) { + /* + * client.updateComponent overwrites all styles so we must + * ALWAYS set the prompting style at this point, even though + * we think it has been set already... + */ + getWidget().prompting = false; + getWidget().setPromptingOn(); + } else { + // we have focus in field, prompting can't be set on, + // instead just clear the input + getWidget().tb.setValue(""); + } + } + getWidget().setSelectedItemIcon(null); + getWidget().selectedOptionKey = null; + } + + if (getWidget().waitingForFilteringResponse + && getWidget().lastFilter.toLowerCase().equals( + uidl.getStringVariable("filter"))) { + getWidget().suggestionPopup.showSuggestions( + getWidget().currentSuggestions, getWidget().currentPage, + getWidget().totalMatches); + getWidget().waitingForFilteringResponse = false; + if (!getWidget().popupOpenerClicked + && getWidget().selectPopupItemWhenResponseIsReceived != VFilterSelect.Select.NONE) { + // we're paging w/ arrows + if (getWidget().selectPopupItemWhenResponseIsReceived == VFilterSelect.Select.LAST) { + getWidget().suggestionPopup.menu.selectLastItem(); + } else { + getWidget().suggestionPopup.menu.selectFirstItem(); + } + + // This is used for paging so we update the keyboard selection + // variable as well. + MenuItem activeMenuItem = getWidget().suggestionPopup.menu + .getSelectedItem(); + getWidget().suggestionPopup.menu + .setKeyboardSelectedItem(activeMenuItem); + + // Update text field to contain the correct text + getWidget().setTextboxText(activeMenuItem.getText()); + getWidget().tb.setSelectionRange( + getWidget().lastFilter.length(), + activeMenuItem.getText().length() + - getWidget().lastFilter.length()); + + getWidget().selectPopupItemWhenResponseIsReceived = VFilterSelect.Select.NONE; // reset + } + if (getWidget().updateSelectionWhenReponseIsReceived) { + getWidget().suggestionPopup.menu + .doPostFilterSelectedItemAction(); + } + } + + // Calculate minumum textarea width + getWidget().suggestionPopupMinWidth = getWidget().minWidth(captions); + + getWidget().popupOpenerClicked = false; + + if (!getWidget().initDone) { + getWidget().updateRootWidth(); + } + + // Focus dependent style names are lost during the update, so we add + // them here back again + if (getWidget().focused) { + getWidget().addStyleDependentName("focus"); + } + + getWidget().initDone = true; + } + + @Override + public VFilterSelect getWidget() { + return (VFilterSelect) super.getWidget(); + } + + @Override + public void layout() { + VFilterSelect widget = getWidget(); + if (widget.initDone) { + widget.updateRootWidth(); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/combobox/VFilterSelect.java b/client/src/com/vaadin/terminal/gwt/client/ui/combobox/VFilterSelect.java new file mode 100644 index 0000000000..5f5826526c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/combobox/VFilterSelect.java @@ -0,0 +1,1717 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.combobox; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.event.dom.client.KeyUpHandler; +import com.google.gwt.event.dom.client.LoadEvent; +import com.google.gwt.event.dom.client.LoadHandler; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +import com.google.gwt.user.client.Command; +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.Window; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.PopupPanel.PositionCallback; +import com.google.gwt.user.client.ui.SuggestOracle.Suggestion; +import com.google.gwt.user.client.ui.TextBox; +import com.vaadin.shared.ComponentState; +import com.vaadin.shared.EventId; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.Field; +import com.vaadin.terminal.gwt.client.ui.SubPartAware; +import com.vaadin.terminal.gwt.client.ui.VLazyExecutor; +import com.vaadin.terminal.gwt.client.ui.VOverlay; +import com.vaadin.terminal.gwt.client.ui.menubar.MenuBar; +import com.vaadin.terminal.gwt.client.ui.menubar.MenuItem; + +/** + * Client side implementation of the Select component. + * + * TODO needs major refactoring (to be extensible etc) + */ +@SuppressWarnings("deprecation") +public class VFilterSelect extends Composite implements Field, KeyDownHandler, + KeyUpHandler, ClickHandler, FocusHandler, BlurHandler, Focusable, + SubPartAware { + + /** + * Represents a suggestion in the suggestion popup box + */ + public class FilterSelectSuggestion implements Suggestion, Command { + + private final String key; + private final String caption; + private String iconUri; + + /** + * Constructor + * + * @param uidl + * The UIDL recieved from the server + */ + public FilterSelectSuggestion(UIDL uidl) { + key = uidl.getStringAttribute("key"); + caption = uidl.getStringAttribute("caption"); + if (uidl.hasAttribute("icon")) { + iconUri = client.translateVaadinUri(uidl + .getStringAttribute("icon")); + } + } + + /** + * Gets the visible row in the popup as a HTML string. The string + * contains an image tag with the rows icon (if an icon has been + * specified) and the caption of the item + */ + + @Override + public String getDisplayString() { + final StringBuffer sb = new StringBuffer(); + if (iconUri != null) { + sb.append("<img src=\""); + sb.append(Util.escapeAttribute(iconUri)); + sb.append("\" alt=\"\" class=\"v-icon\" />"); + } + String content; + if ("".equals(caption)) { + // Ensure that empty options use the same height as other + // options and are not collapsed (#7506) + content = " "; + } else { + content = Util.escapeHTML(caption); + } + sb.append("<span>" + content + "</span>"); + return sb.toString(); + } + + /** + * Get a string that represents this item. This is used in the text box. + */ + + @Override + public String getReplacementString() { + return caption; + } + + /** + * Get the option key which represents the item on the server side. + * + * @return The key of the item + */ + public int getOptionKey() { + return Integer.parseInt(key); + } + + /** + * Get the URI of the icon. Used when constructing the displayed option. + * + * @return + */ + public String getIconUri() { + return iconUri; + } + + /** + * Executes a selection of this item. + */ + + @Override + public void execute() { + onSuggestionSelected(this); + } + } + + /** + * Represents the popup box with the selection options. Wraps a suggestion + * menu. + */ + public class SuggestionPopup extends VOverlay implements PositionCallback, + CloseHandler<PopupPanel> { + + private static final String Z_INDEX = "30000"; + + protected final SuggestionMenu menu; + + private final Element up = DOM.createDiv(); + private final Element down = DOM.createDiv(); + private final Element status = DOM.createDiv(); + + private boolean isPagingEnabled = true; + + private long lastAutoClosed; + + private int popupOuterPadding = -1; + + private int topPosition; + + /** + * Default constructor + */ + SuggestionPopup() { + super(true, false, true); + menu = new SuggestionMenu(); + setWidget(menu); + setStyleName(CLASSNAME + "-suggestpopup"); + DOM.setStyleAttribute(getElement(), "zIndex", Z_INDEX); + + final Element root = getContainerElement(); + + DOM.setInnerHTML(up, "<span>Prev</span>"); + DOM.sinkEvents(up, Event.ONCLICK); + DOM.setInnerHTML(down, "<span>Next</span>"); + DOM.sinkEvents(down, Event.ONCLICK); + DOM.insertChild(root, up, 0); + DOM.appendChild(root, down); + DOM.appendChild(root, status); + DOM.setElementProperty(status, "className", CLASSNAME + "-status"); + DOM.sinkEvents(root, Event.ONMOUSEDOWN | Event.ONMOUSEWHEEL); + addCloseHandler(this); + } + + /** + * Shows the popup where the user can see the filtered options + * + * @param currentSuggestions + * The filtered suggestions + * @param currentPage + * The current page number + * @param totalSuggestions + * The total amount of suggestions + */ + public void showSuggestions( + Collection<FilterSelectSuggestion> currentSuggestions, + int currentPage, int totalSuggestions) { + + // Add TT anchor point + DOM.setElementProperty(getElement(), "id", + "VAADIN_COMBOBOX_OPTIONLIST"); + + menu.setSuggestions(currentSuggestions); + final int x = VFilterSelect.this.getAbsoluteLeft(); + topPosition = tb.getAbsoluteTop(); + topPosition += tb.getOffsetHeight(); + setPopupPosition(x, topPosition); + + int nullOffset = (nullSelectionAllowed && "".equals(lastFilter) ? 1 + : 0); + boolean firstPage = (currentPage == 0); + final int first = currentPage * pageLength + 1 + - (firstPage ? 0 : nullOffset); + final int last = first + currentSuggestions.size() - 1 + - (firstPage && "".equals(lastFilter) ? nullOffset : 0); + final int matches = totalSuggestions - nullOffset; + if (last > 0) { + // nullsel not counted, as requested by user + DOM.setInnerText(status, (matches == 0 ? 0 : first) + "-" + + last + "/" + matches); + } else { + DOM.setInnerText(status, ""); + } + // We don't need to show arrows or statusbar if there is only one + // page + if (totalSuggestions <= pageLength || pageLength == 0) { + setPagingEnabled(false); + } else { + setPagingEnabled(true); + } + setPrevButtonActive(first > 1); + setNextButtonActive(last < matches); + + // clear previously fixed width + menu.setWidth(""); + DOM.setStyleAttribute(DOM.getFirstChild(menu.getElement()), + "width", ""); + + setPopupPositionAndShow(this); + + } + + /** + * Should the next page button be visible to the user? + * + * @param active + */ + private void setNextButtonActive(boolean active) { + if (active) { + DOM.sinkEvents(down, Event.ONCLICK); + DOM.setElementProperty(down, "className", CLASSNAME + + "-nextpage"); + } else { + DOM.sinkEvents(down, 0); + DOM.setElementProperty(down, "className", CLASSNAME + + "-nextpage-off"); + } + } + + /** + * Should the previous page button be visible to the user + * + * @param active + */ + private void setPrevButtonActive(boolean active) { + if (active) { + DOM.sinkEvents(up, Event.ONCLICK); + DOM.setElementProperty(up, "className", CLASSNAME + "-prevpage"); + } else { + DOM.sinkEvents(up, 0); + DOM.setElementProperty(up, "className", CLASSNAME + + "-prevpage-off"); + } + + } + + /** + * Selects the next item in the filtered selections + */ + public void selectNextItem() { + final MenuItem cur = menu.getSelectedItem(); + final int index = 1 + menu.getItems().indexOf(cur); + if (menu.getItems().size() > index) { + final MenuItem newSelectedItem = menu.getItems().get(index); + menu.selectItem(newSelectedItem); + tb.setText(newSelectedItem.getText()); + tb.setSelectionRange(lastFilter.length(), newSelectedItem + .getText().length() - lastFilter.length()); + + } else if (hasNextPage()) { + selectPopupItemWhenResponseIsReceived = Select.FIRST; + filterOptions(currentPage + 1, lastFilter); + } + } + + /** + * Selects the previous item in the filtered selections + */ + public void selectPrevItem() { + final MenuItem cur = menu.getSelectedItem(); + final int index = -1 + menu.getItems().indexOf(cur); + if (index > -1) { + final MenuItem newSelectedItem = menu.getItems().get(index); + menu.selectItem(newSelectedItem); + tb.setText(newSelectedItem.getText()); + tb.setSelectionRange(lastFilter.length(), newSelectedItem + .getText().length() - lastFilter.length()); + } else if (index == -1) { + if (currentPage > 0) { + selectPopupItemWhenResponseIsReceived = Select.LAST; + filterOptions(currentPage - 1, lastFilter); + } + } else { + final MenuItem newSelectedItem = menu.getItems().get( + menu.getItems().size() - 1); + menu.selectItem(newSelectedItem); + tb.setText(newSelectedItem.getText()); + tb.setSelectionRange(lastFilter.length(), newSelectedItem + .getText().length() - lastFilter.length()); + } + } + + /* + * Using a timer to scroll up or down the pages so when we receive lots + * of consecutive mouse wheel events the pages does not flicker. + */ + private LazyPageScroller lazyPageScroller = new LazyPageScroller(); + + private class LazyPageScroller extends Timer { + private int pagesToScroll = 0; + + @Override + public void run() { + if (pagesToScroll != 0) { + if (!waitingForFilteringResponse) { + /* + * Avoid scrolling while we are waiting for a response + * because otherwise the waiting flag will be reset in + * the first response and the second response will be + * ignored, causing an empty popup... + * + * As long as the scrolling delay is suitable + * double/triple clicks will work by scrolling two or + * three pages at a time and this should not be a + * problem. + */ + filterOptions(currentPage + pagesToScroll, lastFilter); + } + pagesToScroll = 0; + } + } + + public void scrollUp() { + if (currentPage + pagesToScroll > 0) { + pagesToScroll--; + cancel(); + schedule(200); + } + } + + public void scrollDown() { + if (totalMatches > (currentPage + pagesToScroll + 1) + * pageLength) { + pagesToScroll++; + cancel(); + schedule(200); + } + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt + * .user.client.Event) + */ + + @Override + public void onBrowserEvent(Event event) { + if (event.getTypeInt() == Event.ONCLICK) { + final Element target = DOM.eventGetTarget(event); + if (target == up || target == DOM.getChild(up, 0)) { + lazyPageScroller.scrollUp(); + } else if (target == down || target == DOM.getChild(down, 0)) { + lazyPageScroller.scrollDown(); + } + } else if (event.getTypeInt() == Event.ONMOUSEWHEEL) { + int velocity = event.getMouseWheelVelocityY(); + if (velocity > 0) { + lazyPageScroller.scrollDown(); + } else { + lazyPageScroller.scrollUp(); + } + } + + /* + * Prevent the keyboard focus from leaving the textfield by + * preventing the default behaviour of the browser. Fixes #4285. + */ + handleMouseDownEvent(event); + } + + /** + * Should paging be enabled. If paging is enabled then only a certain + * amount of items are visible at a time and a scrollbar or buttons are + * visible to change page. If paging is turned of then all options are + * rendered into the popup menu. + * + * @param paging + * Should the paging be turned on? + */ + public void setPagingEnabled(boolean paging) { + if (isPagingEnabled == paging) { + return; + } + if (paging) { + DOM.setStyleAttribute(down, "display", ""); + DOM.setStyleAttribute(up, "display", ""); + DOM.setStyleAttribute(status, "display", ""); + } else { + DOM.setStyleAttribute(down, "display", "none"); + DOM.setStyleAttribute(up, "display", "none"); + DOM.setStyleAttribute(status, "display", "none"); + } + isPagingEnabled = paging; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.PopupPanel$PositionCallback#setPosition + * (int, int) + */ + + @Override + public void setPosition(int offsetWidth, int offsetHeight) { + + int top = -1; + int left = -1; + + // reset menu size and retrieve its "natural" size + menu.setHeight(""); + if (currentPage > 0) { + // fix height to avoid height change when getting to last page + menu.fixHeightTo(pageLength); + } + offsetHeight = getOffsetHeight(); + + final int desiredWidth = getMainWidth(); + int naturalMenuWidth = DOM.getElementPropertyInt( + DOM.getFirstChild(menu.getElement()), "offsetWidth"); + + if (popupOuterPadding == -1) { + popupOuterPadding = Util.measureHorizontalPaddingAndBorder( + getElement(), 2); + } + + if (naturalMenuWidth < desiredWidth) { + menu.setWidth((desiredWidth - popupOuterPadding) + "px"); + DOM.setStyleAttribute(DOM.getFirstChild(menu.getElement()), + "width", "100%"); + naturalMenuWidth = desiredWidth; + } + + if (BrowserInfo.get().isIE()) { + /* + * IE requires us to specify the width for the container + * element. Otherwise it will be 100% wide + */ + int rootWidth = naturalMenuWidth - popupOuterPadding; + DOM.setStyleAttribute(getContainerElement(), "width", rootWidth + + "px"); + } + + if (offsetHeight + getPopupTop() > Window.getClientHeight() + + Window.getScrollTop()) { + // popup on top of input instead + top = getPopupTop() - offsetHeight + - VFilterSelect.this.getOffsetHeight(); + if (top < 0) { + top = 0; + } + } else { + top = getPopupTop(); + /* + * Take popup top margin into account. getPopupTop() returns the + * top value including the margin but the value we give must not + * include the margin. + */ + int topMargin = (top - topPosition); + top -= topMargin; + } + + // fetch real width (mac FF bugs here due GWT popups overflow:auto ) + offsetWidth = DOM.getElementPropertyInt( + DOM.getFirstChild(menu.getElement()), "offsetWidth"); + if (offsetWidth + getPopupLeft() > Window.getClientWidth() + + Window.getScrollLeft()) { + left = VFilterSelect.this.getAbsoluteLeft() + + VFilterSelect.this.getOffsetWidth() + + Window.getScrollLeft() - offsetWidth; + if (left < 0) { + left = 0; + } + } else { + left = getPopupLeft(); + } + setPopupPosition(left, top); + } + + /** + * Was the popup just closed? + * + * @return true if popup was just closed + */ + public boolean isJustClosed() { + final long now = (new Date()).getTime(); + return (lastAutoClosed > 0 && (now - lastAutoClosed) < 200); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google + * .gwt.event.logical.shared.CloseEvent) + */ + + @Override + public void onClose(CloseEvent<PopupPanel> event) { + if (event.isAutoClosed()) { + lastAutoClosed = (new Date()).getTime(); + } + } + + /** + * Updates style names in suggestion popup to help theme building. + * + * @param uidl + * UIDL for the whole combo box + * @param componentState + * shared state of the combo box + */ + public void updateStyleNames(UIDL uidl, ComponentState componentState) { + setStyleName(CLASSNAME + "-suggestpopup"); + if (componentState.hasStyles()) { + for (String style : componentState.getStyles()) { + if (!"".equals(style)) { + addStyleDependentName(style); + } + } + } + } + + } + + /** + * The menu where the suggestions are rendered + */ + public class SuggestionMenu extends MenuBar implements SubPartAware, + LoadHandler { + + /** + * Tracks the item that is currently selected using the keyboard. This + * is need only because mouseover changes the selection and we do not + * want to use that selection when pressing enter to select the item. + */ + private MenuItem keyboardSelectedItem; + + private VLazyExecutor delayedImageLoadExecutioner = new VLazyExecutor( + 100, new ScheduledCommand() { + + @Override + public void execute() { + if (suggestionPopup.isVisible() + && suggestionPopup.isAttached()) { + setWidth(""); + DOM.setStyleAttribute( + DOM.getFirstChild(getElement()), "width", + ""); + suggestionPopup + .setPopupPositionAndShow(suggestionPopup); + } + + } + }); + + /** + * Default constructor + */ + SuggestionMenu() { + super(true); + setStyleName(CLASSNAME + "-suggestmenu"); + addDomHandler(this, LoadEvent.getType()); + } + + /** + * Fixes menus height to use same space as full page would use. Needed + * to avoid height changes when quickly "scrolling" to last page + */ + public void fixHeightTo(int pagelenth) { + if (currentSuggestions.size() > 0) { + final int pixels = pagelenth * (getOffsetHeight() - 2) + / currentSuggestions.size(); + setHeight((pixels + 2) + "px"); + } + } + + /** + * Sets the suggestions rendered in the menu + * + * @param suggestions + * The suggestions to be rendered in the menu + */ + public void setSuggestions( + Collection<FilterSelectSuggestion> suggestions) { + // Reset keyboard selection when contents is updated to avoid + // reusing old, invalid data + setKeyboardSelectedItem(null); + + clearItems(); + final Iterator<FilterSelectSuggestion> it = suggestions.iterator(); + while (it.hasNext()) { + final FilterSelectSuggestion s = it.next(); + final MenuItem mi = new MenuItem(s.getDisplayString(), true, s); + + Util.sinkOnloadForImages(mi.getElement()); + + this.addItem(mi); + if (s == currentSuggestion) { + selectItem(mi); + } + } + } + + /** + * Send the current selection to the server. Triggered when a selection + * is made or on a blur event. + */ + public void doSelectedItemAction() { + // do not send a value change event if null was and stays selected + final String enteredItemValue = tb.getText(); + if (nullSelectionAllowed && "".equals(enteredItemValue) + && selectedOptionKey != null + && !"".equals(selectedOptionKey)) { + if (nullSelectItem) { + reset(); + return; + } + // null is not visible on pages != 0, and not visible when + // filtering: handle separately + client.updateVariable(paintableId, "filter", "", false); + client.updateVariable(paintableId, "page", 0, false); + client.updateVariable(paintableId, "selected", new String[] {}, + immediate); + suggestionPopup.hide(); + return; + } + + updateSelectionWhenReponseIsReceived = waitingForFilteringResponse; + if (!waitingForFilteringResponse) { + doPostFilterSelectedItemAction(); + } + } + + /** + * Triggered after a selection has been made + */ + public void doPostFilterSelectedItemAction() { + final MenuItem item = getSelectedItem(); + final String enteredItemValue = tb.getText(); + + updateSelectionWhenReponseIsReceived = false; + + // check for exact match in menu + int p = getItems().size(); + if (p > 0) { + for (int i = 0; i < p; i++) { + final MenuItem potentialExactMatch = getItems().get(i); + if (potentialExactMatch.getText().equals(enteredItemValue)) { + selectItem(potentialExactMatch); + // do not send a value change event if null was and + // stays selected + if (!"".equals(enteredItemValue) + || (selectedOptionKey != null && !"" + .equals(selectedOptionKey))) { + doItemAction(potentialExactMatch, true); + } + suggestionPopup.hide(); + return; + } + } + } + if (allowNewItem) { + + if (!prompting && !enteredItemValue.equals(lastNewItemString)) { + /* + * Store last sent new item string to avoid double sends + */ + lastNewItemString = enteredItemValue; + client.updateVariable(paintableId, "newitem", + enteredItemValue, immediate); + } + } else if (item != null + && !"".equals(lastFilter) + && (filteringmode == FILTERINGMODE_CONTAINS ? item + .getText().toLowerCase() + .contains(lastFilter.toLowerCase()) : item + .getText().toLowerCase() + .startsWith(lastFilter.toLowerCase()))) { + doItemAction(item, true); + } else { + // currentSuggestion has key="" for nullselection + if (currentSuggestion != null + && !currentSuggestion.key.equals("")) { + // An item (not null) selected + String text = currentSuggestion.getReplacementString(); + tb.setText(text); + selectedOptionKey = currentSuggestion.key; + } else { + // Null selected + tb.setText(""); + selectedOptionKey = null; + } + } + suggestionPopup.hide(); + } + + private static final String SUBPART_PREFIX = "item"; + + @Override + public Element getSubPartElement(String subPart) { + int index = Integer.parseInt(subPart.substring(SUBPART_PREFIX + .length())); + + MenuItem item = getItems().get(index); + + return item.getElement(); + } + + @Override + public String getSubPartName(Element subElement) { + if (!getElement().isOrHasChild(subElement)) { + return null; + } + + Element menuItemRoot = subElement; + while (menuItemRoot != null + && !menuItemRoot.getTagName().equalsIgnoreCase("td")) { + menuItemRoot = menuItemRoot.getParentElement().cast(); + } + // "menuItemRoot" is now the root of the menu item + + final int itemCount = getItems().size(); + for (int i = 0; i < itemCount; i++) { + if (getItems().get(i).getElement() == menuItemRoot) { + String name = SUBPART_PREFIX + i; + return name; + } + } + return null; + } + + @Override + public void onLoad(LoadEvent event) { + // Handle icon onload events to ensure shadow is resized + // correctly + delayedImageLoadExecutioner.trigger(); + + } + + public void selectFirstItem() { + MenuItem firstItem = getItems().get(0); + selectItem(firstItem); + } + + private MenuItem getKeyboardSelectedItem() { + return keyboardSelectedItem; + } + + protected void setKeyboardSelectedItem(MenuItem firstItem) { + keyboardSelectedItem = firstItem; + } + + public void selectLastItem() { + List<MenuItem> items = getItems(); + MenuItem lastItem = items.get(items.size() - 1); + selectItem(lastItem); + } + } + + public static final int FILTERINGMODE_OFF = 0; + public static final int FILTERINGMODE_STARTSWITH = 1; + public static final int FILTERINGMODE_CONTAINS = 2; + + private static final String CLASSNAME = "v-filterselect"; + private static final String STYLE_NO_INPUT = "no-input"; + + protected int pageLength = 10; + + private boolean enableDebug = false; + + private final FlowPanel panel = new FlowPanel(); + + /** + * The text box where the filter is written + */ + protected final TextBox tb = new TextBox() { + + // Overridden to avoid selecting text when text input is disabled + @Override + public void setSelectionRange(int pos, int length) { + if (textInputEnabled) { + super.setSelectionRange(pos, length); + } else { + super.setSelectionRange(getValue().length(), 0); + } + }; + }; + + protected final SuggestionPopup suggestionPopup = new SuggestionPopup(); + + /** + * Used when measuring the width of the popup + */ + private final HTML popupOpener = new HTML("") { + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt + * .user.client.Event) + */ + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + + /* + * Prevent the keyboard focus from leaving the textfield by + * preventing the default behaviour of the browser. Fixes #4285. + */ + handleMouseDownEvent(event); + } + }; + + private final Image selectedItemIcon = new Image(); + + protected ApplicationConnection client; + + protected String paintableId; + + protected int currentPage; + + /** + * A collection of available suggestions (options) as received from the + * server. + */ + protected final List<FilterSelectSuggestion> currentSuggestions = new ArrayList<FilterSelectSuggestion>(); + + protected boolean immediate; + + protected String selectedOptionKey; + + protected boolean waitingForFilteringResponse = false; + protected boolean updateSelectionWhenReponseIsReceived = false; + private boolean tabPressedWhenPopupOpen = false; + protected boolean initDone = false; + + protected String lastFilter = ""; + + protected enum Select { + NONE, FIRST, LAST + }; + + protected Select selectPopupItemWhenResponseIsReceived = Select.NONE; + + /** + * The current suggestion selected from the dropdown. This is one of the + * values in currentSuggestions except when filtering, in this case + * currentSuggestion might not be in currentSuggestions. + */ + protected FilterSelectSuggestion currentSuggestion; + + protected int totalMatches; + protected boolean allowNewItem; + protected boolean nullSelectionAllowed; + protected boolean nullSelectItem; + protected boolean enabled; + protected boolean readonly; + + protected int filteringmode = FILTERINGMODE_OFF; + + // shown in unfocused empty field, disappears on focus (e.g "Search here") + private static final String CLASSNAME_PROMPT = "prompt"; + protected String inputPrompt = ""; + protected boolean prompting = false; + + // Set true when popupopened has been clicked. Cleared on each UIDL-update. + // This handles the special case where are not filtering yet and the + // selected value has changed on the server-side. See #2119 + protected boolean popupOpenerClicked; + protected int suggestionPopupMinWidth = 0; + private int popupWidth = -1; + /* + * Stores the last new item string to avoid double submissions. Cleared on + * uidl updates + */ + protected String lastNewItemString; + protected boolean focused = false; + + /** + * If set to false, the component should not allow entering text to the + * field even for filtering. + */ + private boolean textInputEnabled = true; + + /** + * Default constructor + */ + public VFilterSelect() { + selectedItemIcon.setStyleName("v-icon"); + selectedItemIcon.addLoadHandler(new LoadHandler() { + + @Override + public void onLoad(LoadEvent event) { + if (BrowserInfo.get().isIE8()) { + // IE8 needs some help to discover it should reposition the + // text field + forceReflow(); + } + updateRootWidth(); + updateSelectedIconPosition(); + } + }); + + popupOpener.sinkEvents(Event.ONMOUSEDOWN); + panel.add(tb); + panel.add(popupOpener); + initWidget(panel); + setStyleName(CLASSNAME); + tb.addKeyDownHandler(this); + tb.addKeyUpHandler(this); + tb.setStyleName(CLASSNAME + "-input"); + tb.addFocusHandler(this); + tb.addBlurHandler(this); + tb.addClickHandler(this); + popupOpener.setStyleName(CLASSNAME + "-button"); + popupOpener.addClickHandler(this); + } + + /** + * Does the Select have more pages? + * + * @return true if a next page exists, else false if the current page is the + * last page + */ + public boolean hasNextPage() { + if (totalMatches > (currentPage + 1) * pageLength) { + return true; + } else { + return false; + } + } + + /** + * Filters the options at a certain page. Uses the text box input as a + * filter + * + * @param page + * The page which items are to be filtered + */ + public void filterOptions(int page) { + filterOptions(page, tb.getText()); + } + + /** + * Filters the options at certain page using the given filter + * + * @param page + * The page to filter + * @param filter + * The filter to apply to the components + */ + public void filterOptions(int page, String filter) { + filterOptions(page, filter, true); + } + + /** + * Filters the options at certain page using the given filter + * + * @param page + * The page to filter + * @param filter + * The filter to apply to the options + * @param immediate + * Whether to send the options request immediately + */ + private void filterOptions(int page, String filter, boolean immediate) { + if (filter.equals(lastFilter) && currentPage == page) { + if (!suggestionPopup.isAttached()) { + suggestionPopup.showSuggestions(currentSuggestions, + currentPage, totalMatches); + } + return; + } + if (!filter.equals(lastFilter)) { + // we are on subsequent page and text has changed -> reset page + if ("".equals(filter)) { + // let server decide + page = -1; + } else { + page = 0; + } + } + + waitingForFilteringResponse = true; + client.updateVariable(paintableId, "filter", filter, false); + client.updateVariable(paintableId, "page", page, immediate); + lastFilter = filter; + currentPage = page; + } + + protected void updateReadOnly() { + tb.setReadOnly(readonly || !textInputEnabled); + } + + protected void setTextInputEnabled(boolean textInputEnabled) { + // Always update styles as they might have been overwritten + if (textInputEnabled) { + removeStyleDependentName(STYLE_NO_INPUT); + } else { + addStyleDependentName(STYLE_NO_INPUT); + } + + if (this.textInputEnabled == textInputEnabled) { + return; + } + + this.textInputEnabled = textInputEnabled; + updateReadOnly(); + } + + /** + * Sets the text in the text box. + * + * @param text + * the text to set in the text box + */ + protected void setTextboxText(final String text) { + tb.setText(text); + } + + /** + * Turns prompting on. When prompting is turned on a command prompt is shown + * in the text box if nothing has been entered. + */ + protected void setPromptingOn() { + if (!prompting) { + prompting = true; + addStyleDependentName(CLASSNAME_PROMPT); + } + setTextboxText(inputPrompt); + } + + /** + * Turns prompting off. When prompting is turned on a command prompt is + * shown in the text box if nothing has been entered. + * + * @param text + * The text the text box should contain. + */ + protected void setPromptingOff(String text) { + setTextboxText(text); + if (prompting) { + prompting = false; + removeStyleDependentName(CLASSNAME_PROMPT); + } + } + + /** + * Triggered when a suggestion is selected + * + * @param suggestion + * The suggestion that just got selected. + */ + public void onSuggestionSelected(FilterSelectSuggestion suggestion) { + updateSelectionWhenReponseIsReceived = false; + + currentSuggestion = suggestion; + String newKey; + if (suggestion.key.equals("")) { + // "nullselection" + newKey = ""; + } else { + // normal selection + newKey = String.valueOf(suggestion.getOptionKey()); + } + + String text = suggestion.getReplacementString(); + if ("".equals(newKey) && !focused) { + setPromptingOn(); + } else { + setPromptingOff(text); + } + setSelectedItemIcon(suggestion.getIconUri()); + if (!(newKey.equals(selectedOptionKey) || ("".equals(newKey) && selectedOptionKey == null))) { + selectedOptionKey = newKey; + client.updateVariable(paintableId, "selected", + new String[] { selectedOptionKey }, immediate); + // currentPage = -1; // forget the page + } + suggestionPopup.hide(); + } + + /** + * Sets the icon URI of the selected item. The icon is shown on the left + * side of the item caption text. Set the URI to null to remove the icon. + * + * @param iconUri + * The URI of the icon + */ + protected void setSelectedItemIcon(String iconUri) { + if (iconUri == null || iconUri.length() == 0) { + if (selectedItemIcon.isAttached()) { + panel.remove(selectedItemIcon); + if (BrowserInfo.get().isIE8()) { + // IE8 needs some help to discover it should reposition the + // text field + forceReflow(); + } + updateRootWidth(); + } + } else { + panel.insert(selectedItemIcon, 0); + selectedItemIcon.setUrl(iconUri); + updateRootWidth(); + updateSelectedIconPosition(); + } + } + + private void forceReflow() { + Util.setStyleTemporarily(tb.getElement(), "zoom", "1"); + } + + /** + * Positions the icon vertically in the middle. Should be called after the + * icon has loaded + */ + private void updateSelectedIconPosition() { + // Position icon vertically to middle + int availableHeight = 0; + availableHeight = getOffsetHeight(); + + int iconHeight = Util.getRequiredHeight(selectedItemIcon); + int marginTop = (availableHeight - iconHeight) / 2; + DOM.setStyleAttribute(selectedItemIcon.getElement(), "marginTop", + marginTop + "px"); + } + + private static Set<Integer> navigationKeyCodes = new HashSet<Integer>(); + static { + navigationKeyCodes.add(KeyCodes.KEY_DOWN); + navigationKeyCodes.add(KeyCodes.KEY_UP); + navigationKeyCodes.add(KeyCodes.KEY_PAGEDOWN); + navigationKeyCodes.add(KeyCodes.KEY_PAGEUP); + navigationKeyCodes.add(KeyCodes.KEY_ENTER); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt + * .event.dom.client.KeyDownEvent) + */ + + @Override + public void onKeyDown(KeyDownEvent event) { + if (enabled && !readonly) { + int keyCode = event.getNativeKeyCode(); + + debug("key down: " + keyCode); + if (waitingForFilteringResponse + && navigationKeyCodes.contains(keyCode)) { + /* + * Keyboard navigation events should not be handled while we are + * waiting for a response. This avoids flickering, disappearing + * items, wrongly interpreted responses and more. + */ + debug("Ignoring " + keyCode + + " because we are waiting for a filtering response"); + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + event.stopPropagation(); + return; + } + + if (suggestionPopup.isAttached()) { + debug("Keycode " + keyCode + " target is popup"); + popupKeyDown(event); + } else { + debug("Keycode " + keyCode + " target is text field"); + inputFieldKeyDown(event); + } + } + } + + private void debug(String string) { + if (enableDebug) { + VConsole.error(string); + } + } + + /** + * Triggered when a key is pressed in the text box + * + * @param event + * The KeyDownEvent + */ + private void inputFieldKeyDown(KeyDownEvent event) { + switch (event.getNativeKeyCode()) { + case KeyCodes.KEY_DOWN: + case KeyCodes.KEY_UP: + case KeyCodes.KEY_PAGEDOWN: + case KeyCodes.KEY_PAGEUP: + // open popup as from gadget + filterOptions(-1, ""); + lastFilter = ""; + tb.selectAll(); + break; + case KeyCodes.KEY_ENTER: + /* + * This only handles the case when new items is allowed, a text is + * entered, the popup opener button is clicked to close the popup + * and enter is then pressed (see #7560). + */ + if (!allowNewItem) { + return; + } + + if (currentSuggestion != null + && tb.getText().equals( + currentSuggestion.getReplacementString())) { + // Retain behavior from #6686 by returning without stopping + // propagation if there's nothing to do + return; + } + suggestionPopup.menu.doSelectedItemAction(); + + event.stopPropagation(); + break; + } + + } + + /** + * Triggered when a key was pressed in the suggestion popup. + * + * @param event + * The KeyDownEvent of the key + */ + private void popupKeyDown(KeyDownEvent event) { + // Propagation of handled events is stopped so other handlers such as + // shortcut key handlers do not also handle the same events. + switch (event.getNativeKeyCode()) { + case KeyCodes.KEY_DOWN: + suggestionPopup.selectNextItem(); + suggestionPopup.menu.setKeyboardSelectedItem(suggestionPopup.menu + .getSelectedItem()); + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + event.stopPropagation(); + break; + case KeyCodes.KEY_UP: + suggestionPopup.selectPrevItem(); + suggestionPopup.menu.setKeyboardSelectedItem(suggestionPopup.menu + .getSelectedItem()); + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + event.stopPropagation(); + break; + case KeyCodes.KEY_PAGEDOWN: + if (hasNextPage()) { + filterOptions(currentPage + 1, lastFilter); + } + event.stopPropagation(); + break; + case KeyCodes.KEY_PAGEUP: + if (currentPage > 0) { + filterOptions(currentPage - 1, lastFilter); + } + event.stopPropagation(); + break; + case KeyCodes.KEY_TAB: + tabPressedWhenPopupOpen = true; + filterOptions(currentPage); + // onBlur() takes care of the rest + break; + case KeyCodes.KEY_ESCAPE: + reset(); + event.stopPropagation(); + break; + case KeyCodes.KEY_ENTER: + if (suggestionPopup.menu.getKeyboardSelectedItem() == null) { + /* + * Nothing selected using up/down. Happens e.g. when entering a + * text (causes popup to open) and then pressing enter. + */ + if (!allowNewItem) { + /* + * New items are not allowed: If there is only one + * suggestion, select that. Otherwise do nothing. + */ + if (currentSuggestions.size() == 1) { + onSuggestionSelected(currentSuggestions.get(0)); + } + } else { + // Handle addition of new items. + suggestionPopup.menu.doSelectedItemAction(); + } + } else { + /* + * Get the suggestion that was navigated to using up/down. + */ + currentSuggestion = ((FilterSelectSuggestion) suggestionPopup.menu + .getKeyboardSelectedItem().getCommand()); + onSuggestionSelected(currentSuggestion); + } + + event.stopPropagation(); + break; + } + + } + + /** + * Triggered when a key was depressed + * + * @param event + * The KeyUpEvent of the key depressed + */ + + @Override + public void onKeyUp(KeyUpEvent event) { + if (enabled && !readonly) { + switch (event.getNativeKeyCode()) { + case KeyCodes.KEY_ENTER: + case KeyCodes.KEY_TAB: + case KeyCodes.KEY_SHIFT: + case KeyCodes.KEY_CTRL: + case KeyCodes.KEY_ALT: + case KeyCodes.KEY_DOWN: + case KeyCodes.KEY_UP: + case KeyCodes.KEY_PAGEDOWN: + case KeyCodes.KEY_PAGEUP: + case KeyCodes.KEY_ESCAPE: + ; // NOP + break; + default: + if (textInputEnabled) { + filterOptions(currentPage); + } + break; + } + } + } + + /** + * Resets the Select to its initial state + */ + private void reset() { + if (currentSuggestion != null) { + String text = currentSuggestion.getReplacementString(); + setPromptingOff(text); + selectedOptionKey = currentSuggestion.key; + } else { + if (focused) { + setPromptingOff(""); + } else { + setPromptingOn(); + } + selectedOptionKey = null; + } + lastFilter = ""; + suggestionPopup.hide(); + } + + /** + * Listener for popupopener + */ + + @Override + public void onClick(ClickEvent event) { + if (textInputEnabled + && event.getNativeEvent().getEventTarget().cast() == tb + .getElement()) { + // Don't process clicks on the text field if text input is enabled + return; + } + if (enabled && !readonly) { + // ask suggestionPopup if it was just closed, we are using GWT + // Popup's auto close feature + if (!suggestionPopup.isJustClosed()) { + // If a focus event is not going to be sent, send the options + // request immediately; otherwise queue in the same burst as the + // focus event. Fixes #8321. + boolean immediate = focused + || !client.hasEventListeners(this, EventId.FOCUS); + filterOptions(-1, "", immediate); + popupOpenerClicked = true; + lastFilter = ""; + } + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + focus(); + tb.selectAll(); + } + } + + /** + * Calculate minimum width for FilterSelect textarea + */ + protected native int minWidth(String captions) + /*-{ + if(!captions || captions.length <= 0) + return 0; + captions = captions.split("|"); + var d = $wnd.document.createElement("div"); + var html = ""; + for(var i=0; i < captions.length; i++) { + html += "<div>" + captions[i] + "</div>"; + // TODO apply same CSS classname as in suggestionmenu + } + d.style.position = "absolute"; + d.style.top = "0"; + d.style.left = "0"; + d.style.visibility = "hidden"; + d.innerHTML = html; + $wnd.document.body.appendChild(d); + var w = d.offsetWidth; + $wnd.document.body.removeChild(d); + return w; + }-*/; + + /** + * A flag which prevents a focus event from taking place + */ + boolean iePreventNextFocus = false; + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + + @Override + public void onFocus(FocusEvent event) { + + /* + * When we disable a blur event in ie we need to refocus the textfield. + * This will cause a focus event we do not want to process, so in that + * case we just ignore it. + */ + if (BrowserInfo.get().isIE() && iePreventNextFocus) { + iePreventNextFocus = false; + return; + } + + focused = true; + if (prompting && !readonly) { + setPromptingOff(""); + } + addStyleDependentName("focus"); + + if (client.hasEventListeners(this, EventId.FOCUS)) { + client.updateVariable(paintableId, EventId.FOCUS, "", true); + } + } + + /** + * A flag which cancels the blur event and sets the focus back to the + * textfield if the Browser is IE + */ + boolean preventNextBlurEventInIE = false; + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event + * .dom.client.BlurEvent) + */ + + @Override + public void onBlur(BlurEvent event) { + + if (BrowserInfo.get().isIE() && preventNextBlurEventInIE) { + /* + * Clicking in the suggestion popup or on the popup button in IE + * causes a blur event to be sent for the field. In other browsers + * this is prevented by canceling/preventing default behavior for + * the focus event, in IE we handle it here by refocusing the text + * field and ignoring the resulting focus event for the textfield + * (in onFocus). + */ + preventNextBlurEventInIE = false; + + Element focusedElement = Util.getIEFocusedElement(); + if (getElement().isOrHasChild(focusedElement) + || suggestionPopup.getElement() + .isOrHasChild(focusedElement)) { + + // IF the suggestion popup or another part of the VFilterSelect + // was focused, move the focus back to the textfield and prevent + // the triggered focus event (in onFocus). + iePreventNextFocus = true; + tb.setFocus(true); + return; + } + } + + focused = false; + if (!readonly) { + // much of the TAB handling takes place here + if (tabPressedWhenPopupOpen) { + tabPressedWhenPopupOpen = false; + suggestionPopup.menu.doSelectedItemAction(); + suggestionPopup.hide(); + } else if (!suggestionPopup.isAttached() + || suggestionPopup.isJustClosed()) { + suggestionPopup.menu.doSelectedItemAction(); + } + if (selectedOptionKey == null) { + setPromptingOn(); + } else if (currentSuggestion != null) { + setPromptingOff(currentSuggestion.caption); + } + } + removeStyleDependentName("focus"); + + if (client.hasEventListeners(this, EventId.BLUR)) { + client.updateVariable(paintableId, EventId.BLUR, "", true); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Focusable#focus() + */ + + @Override + public void focus() { + focused = true; + if (prompting && !readonly) { + setPromptingOff(""); + } + tb.setFocus(true); + } + + /** + * Calculates the width of the select if the select has undefined width. + * Should be called when the width changes or when the icon changes. + */ + protected void updateRootWidth() { + ComponentConnector paintable = ConnectorMap.get(client).getConnector( + this); + if (paintable.isUndefinedWidth()) { + + /* + * When the select has a undefined with we need to check that we are + * only setting the text box width relative to the first page width + * of the items. If this is not done the text box width will change + * when the popup is used to view longer items than the text box is + * wide. + */ + int w = Util.getRequiredWidth(this); + if ((!initDone || currentPage + 1 < 0) + && suggestionPopupMinWidth > w) { + /* + * We want to compensate for the paddings just to preserve the + * exact size as in Vaadin 6.x, but we get here before + * MeasuredSize has been initialized. + * Util.measureHorizontalPaddingAndBorder does not work with + * border-box, so we must do this the hard way. + */ + Style style = getElement().getStyle(); + String originalPadding = style.getPadding(); + String originalBorder = style.getBorderWidth(); + style.setPaddingLeft(0, Unit.PX); + style.setBorderWidth(0, Unit.PX); + int offset = w - Util.getRequiredWidth(this); + style.setProperty("padding", originalPadding); + style.setProperty("borderWidth", originalBorder); + + setWidth(suggestionPopupMinWidth + offset + "px"); + } + + /* + * Lock the textbox width to its current value if it's not already + * locked + */ + if (!tb.getElement().getStyle().getWidth().endsWith("px")) { + tb.setWidth((tb.getOffsetWidth() - selectedItemIcon + .getOffsetWidth()) + "px"); + } + } + } + + /** + * Get the width of the select in pixels where the text area and icon has + * been included. + * + * @return The width in pixels + */ + private int getMainWidth() { + return getOffsetWidth(); + } + + @Override + public void setWidth(String width) { + super.setWidth(width); + if (width.length() != 0) { + tb.setWidth("100%"); + } + } + + /** + * Handles special behavior of the mouse down event + * + * @param event + */ + private void handleMouseDownEvent(Event event) { + /* + * Prevent the keyboard focus from leaving the textfield by preventing + * the default behaviour of the browser. Fixes #4285. + */ + if (event.getTypeInt() == Event.ONMOUSEDOWN) { + event.preventDefault(); + event.stopPropagation(); + + /* + * In IE the above wont work, the blur event will still trigger. So, + * we set a flag here to prevent the next blur event from happening. + * This is not needed if do not already have focus, in that case + * there will not be any blur event and we should not cancel the + * next blur. + */ + if (BrowserInfo.get().isIE() && focused) { + preventNextBlurEventInIE = true; + } + } + } + + @Override + protected void onDetach() { + super.onDetach(); + suggestionPopup.hide(); + } + + @Override + public Element getSubPartElement(String subPart) { + if ("textbox".equals(subPart)) { + return tb.getElement(); + } else if ("button".equals(subPart)) { + return popupOpener.getElement(); + } + return null; + } + + @Override + public String getSubPartName(Element subElement) { + if (tb.getElement().isOrHasChild(subElement)) { + return "textbox"; + } else if (popupOpener.getElement().isOrHasChild(subElement)) { + return "button"; + } + return null; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/CssLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/CssLayoutConnector.java new file mode 100644 index 0000000000..47c2049a67 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/CssLayoutConnector.java @@ -0,0 +1,178 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.csslayout; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gwt.dom.client.Style; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.LayoutClickRpc; +import com.vaadin.shared.ui.VMarginInfo; +import com.vaadin.shared.ui.csslayout.CssLayoutServerRpc; +import com.vaadin.shared.ui.csslayout.CssLayoutState; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractLayoutConnector; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; +import com.vaadin.terminal.gwt.client.ui.csslayout.VCssLayout.FlowPane; +import com.vaadin.ui.CssLayout; + +@Connect(CssLayout.class) +public class CssLayoutConnector extends AbstractLayoutConnector { + + private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler( + this) { + + @Override + protected ComponentConnector getChildComponent(Element element) { + return Util.getConnectorForElement(getConnection(), getWidget(), + element); + } + + @Override + protected LayoutClickRpc getLayoutClickRPC() { + return rpc; + }; + }; + + private CssLayoutServerRpc rpc; + + private Map<ComponentConnector, VCaption> childToCaption = new HashMap<ComponentConnector, VCaption>(); + + @Override + protected void init() { + super.init(); + rpc = RpcProxy.create(CssLayoutServerRpc.class, this); + } + + @Override + public CssLayoutState getState() { + return (CssLayoutState) super.getState(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + getWidget().setMarginStyles( + new VMarginInfo(getState().getMarginsBitmask())); + + for (ComponentConnector child : getChildComponents()) { + if (!getState().getChildCss().containsKey(child)) { + continue; + } + String css = getState().getChildCss().get(child); + Style style = child.getWidget().getElement().getStyle(); + // should we remove styles also? How can we know what we have added + // as it is added directly to the child component? + String[] cssRules = css.split(";"); + for (String cssRule : cssRules) { + String parts[] = cssRule.split(":"); + if (parts.length == 2) { + style.setProperty(makeCamelCase(parts[0].trim()), + parts[1].trim()); + } + } + } + + } + + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + super.onConnectorHierarchyChange(event); + + clickEventHandler.handleEventHandlerRegistration(); + + int index = 0; + FlowPane cssLayoutWidgetContainer = getWidget().panel; + for (ComponentConnector child : getChildComponents()) { + VCaption childCaption = childToCaption.get(child); + if (childCaption != null) { + cssLayoutWidgetContainer.addOrMove(childCaption, index++); + } + cssLayoutWidgetContainer.addOrMove(child.getWidget(), index++); + } + + // Detach old child widgets and possibly their caption + for (ComponentConnector child : event.getOldChildren()) { + if (child.getParent() == this) { + // Skip current children + continue; + } + cssLayoutWidgetContainer.remove(child.getWidget()); + VCaption vCaption = childToCaption.remove(child); + if (vCaption != null) { + cssLayoutWidgetContainer.remove(vCaption); + } + } + } + + private static final String makeCamelCase(String cssProperty) { + // TODO this might be cleaner to implement with regexp + while (cssProperty.contains("-")) { + int indexOf = cssProperty.indexOf("-"); + cssProperty = cssProperty.substring(0, indexOf) + + String.valueOf(cssProperty.charAt(indexOf + 1)) + .toUpperCase() + cssProperty.substring(indexOf + 2); + } + if ("float".equals(cssProperty)) { + if (BrowserInfo.get().isIE()) { + return "styleFloat"; + } else { + return "cssFloat"; + } + } + return cssProperty; + } + + @Override + public VCssLayout getWidget() { + return (VCssLayout) super.getWidget(); + } + + @Override + public void updateCaption(ComponentConnector child) { + Widget childWidget = child.getWidget(); + FlowPane cssLayoutWidgetContainer = getWidget().panel; + int widgetPosition = cssLayoutWidgetContainer + .getWidgetIndex(childWidget); + + VCaption caption = childToCaption.get(child); + if (VCaption.isNeeded(child.getState())) { + if (caption == null) { + caption = new VCaption(child, getConnection()); + childToCaption.put(child, caption); + } + if (!caption.isAttached()) { + // Insert caption at widget index == before widget + cssLayoutWidgetContainer.insert(caption, widgetPosition); + } + caption.updateCaption(); + } else if (caption != null) { + childToCaption.remove(child); + cssLayoutWidgetContainer.remove(caption); + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/VCssLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/VCssLayout.java new file mode 100644 index 0000000000..813e95e3ed --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/VCssLayout.java @@ -0,0 +1,84 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.csslayout; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.VMarginInfo; +import com.vaadin.terminal.gwt.client.StyleConstants; + +public class VCssLayout extends SimplePanel { + public static final String TAGNAME = "csslayout"; + public static final String CLASSNAME = "v-" + TAGNAME; + + FlowPane panel = new FlowPane(); + + Element margin = DOM.createDiv(); + + public VCssLayout() { + super(); + getElement().appendChild(margin); + setStyleName(CLASSNAME); + margin.setClassName(CLASSNAME + "-margin"); + setWidget(panel); + } + + @Override + protected Element getContainerElement() { + return margin; + } + + public class FlowPane extends FlowPanel { + + public FlowPane() { + super(); + setStyleName(CLASSNAME + "-container"); + } + + void addOrMove(Widget child, int index) { + if (child.getParent() == this) { + int currentIndex = getWidgetIndex(child); + if (index == currentIndex) { + return; + } + } + insert(child, index); + } + + } + + /** + * Sets CSS classes for margin based on the given parameters. + * + * @param margins + * A {@link VMarginInfo} object that provides info on + * top/left/bottom/right margins + */ + protected void setMarginStyles(VMarginInfo margins) { + setStyleName(margin, VCssLayout.CLASSNAME + "-" + + StyleConstants.MARGIN_TOP, margins.hasTop()); + setStyleName(margin, VCssLayout.CLASSNAME + "-" + + StyleConstants.MARGIN_RIGHT, margins.hasRight()); + setStyleName(margin, VCssLayout.CLASSNAME + "-" + + StyleConstants.MARGIN_BOTTOM, margins.hasBottom()); + setStyleName(margin, VCssLayout.CLASSNAME + "-" + + StyleConstants.MARGIN_LEFT, margins.hasLeft()); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/CustomComponentConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/CustomComponentConnector.java new file mode 100644 index 0000000000..0557b10437 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/CustomComponentConnector.java @@ -0,0 +1,52 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.customcomponent; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector; +import com.vaadin.ui.CustomComponent; + +@Connect(value = CustomComponent.class, loadStyle = LoadStyle.EAGER) +public class CustomComponentConnector extends + AbstractComponentContainerConnector { + + @Override + public VCustomComponent getWidget() { + return (VCustomComponent) super.getWidget(); + } + + @Override + public void updateCaption(ComponentConnector component) { + // NOP, custom component dont render composition roots caption + } + + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + super.onConnectorHierarchyChange(event); + + ComponentConnector newChild = null; + if (getChildComponents().size() == 1) { + newChild = getChildComponents().get(0); + } + + VCustomComponent customComponent = getWidget(); + customComponent.setWidget(newChild.getWidget()); + + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/VCustomComponent.java b/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/VCustomComponent.java new file mode 100644 index 0000000000..854f7c161e --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/VCustomComponent.java @@ -0,0 +1,30 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.customcomponent; + +import com.google.gwt.user.client.ui.SimplePanel; + +public class VCustomComponent extends SimplePanel { + + private static final String CLASSNAME = "v-customcomponent"; + + public VCustomComponent() { + super(); + setStyleName(CLASSNAME); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/customfield/CustomFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/customfield/CustomFieldConnector.java new file mode 100644 index 0000000000..4120168f62 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/customfield/CustomFieldConnector.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.customfield; + +import com.google.gwt.core.client.GWT; +import com.vaadin.shared.AbstractFieldState; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ui.customcomponent.CustomComponentConnector; +import com.vaadin.ui.CustomField; + +@Connect(value = CustomField.class) +public class CustomFieldConnector extends CustomComponentConnector { + @Override + protected SharedState createState() { + // Workaround as CustomFieldConnector does not extend + // AbstractFieldConnector. + return GWT.create(AbstractFieldState.class); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/CustomLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/CustomLayoutConnector.java new file mode 100644 index 0000000000..1e5cbd0502 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/CustomLayoutConnector.java @@ -0,0 +1,136 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.customlayout; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.customlayout.CustomLayoutState; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractLayoutConnector; +import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout; +import com.vaadin.ui.CustomLayout; + +@Connect(CustomLayout.class) +public class CustomLayoutConnector extends AbstractLayoutConnector implements + SimpleManagedLayout, Paintable { + + @Override + public CustomLayoutState getState() { + return (CustomLayoutState) super.getState(); + } + + @Override + protected void init() { + super.init(); + getWidget().client = getConnection(); + getWidget().pid = getConnectorId(); + + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + // Evaluate scripts + VCustomLayout.eval(getWidget().scripts); + getWidget().scripts = null; + + } + + private void updateHtmlTemplate() { + if (getWidget().hasTemplate()) { + // We (currently) only do this once. You can't change the template + // later on. + return; + } + String templateName = getState().getTemplateName(); + String templateContents = getState().getTemplateContents(); + + if (templateName != null) { + // Get the HTML-template from client. Overrides templateContents + // (even though both can never be given at the same time) + templateContents = getConnection().getResource( + "layouts/" + templateName + ".html"); + if (templateContents == null) { + templateContents = "<em>Layout file layouts/" + + templateName + + ".html is missing. Components will be drawn for debug purposes.</em>"; + } + } + + getWidget().initializeHTML(templateContents, + getConnection().getThemeUri()); + } + + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + super.onConnectorHierarchyChange(event); + + // Must do this once here so the HTML has been set up before we start + // adding child widgets. + + updateHtmlTemplate(); + + // For all contained widgets + for (ComponentConnector child : getChildComponents()) { + String location = getState().getChildLocations().get(child); + try { + getWidget().setWidget(child.getWidget(), location); + } catch (final IllegalArgumentException e) { + // If no location is found, this component is not visible + } + } + for (ComponentConnector oldChild : event.getOldChildren()) { + if (oldChild.getParent() == this) { + // Connector still a child of this + continue; + } + Widget oldChildWidget = oldChild.getWidget(); + if (oldChildWidget.isAttached()) { + // slot of this widget is emptied, remove it + getWidget().remove(oldChildWidget); + } + } + + } + + @Override + public VCustomLayout getWidget() { + return (VCustomLayout) super.getWidget(); + } + + @Override + public void updateCaption(ComponentConnector paintable) { + getWidget().updateCaption(paintable); + } + + @Override + public void layout() { + getWidget().iLayoutJS(DOM.getFirstChild(getWidget().getElement())); + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + // Not interested in anything from the UIDL - just implementing the + // interface to avoid some warning (#8688) + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/VCustomLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/VCustomLayout.java new file mode 100644 index 0000000000..6fd8b19e7c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/VCustomLayout.java @@ -0,0 +1,420 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.customlayout; + +import java.util.HashMap; +import java.util.Iterator; + +import com.google.gwt.dom.client.ImageElement; +import com.google.gwt.dom.client.NodeList; +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.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.VCaptionWrapper; + +/** + * Custom Layout implements complex layout defined with HTML template. + * + * @author Vaadin Ltd + * + */ +public class VCustomLayout extends ComplexPanel { + + public static final String CLASSNAME = "v-customlayout"; + + /** Location-name to containing element in DOM map */ + private final HashMap<String, Element> locationToElement = new HashMap<String, Element>(); + + /** Location-name to contained widget map */ + final HashMap<String, Widget> locationToWidget = new HashMap<String, Widget>(); + + /** Widget to captionwrapper map */ + private final HashMap<Widget, VCaptionWrapper> childWidgetToCaptionWrapper = new HashMap<Widget, VCaptionWrapper>(); + + /** Name of the currently rendered style */ + String currentTemplateName; + + /** Unexecuted scripts loaded from the template */ + String scripts = ""; + + /** Paintable ID of this paintable */ + String pid; + + ApplicationConnection client; + + private boolean htmlInitialized = false; + + private Element elementWithNativeResizeFunction; + + private String height = ""; + + private String width = ""; + + public VCustomLayout() { + setElement(DOM.createDiv()); + // Clear any unwanted styling + DOM.setStyleAttribute(getElement(), "border", "none"); + DOM.setStyleAttribute(getElement(), "margin", "0"); + DOM.setStyleAttribute(getElement(), "padding", "0"); + + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(getElement(), "position", "relative"); + } + + setStyleName(CLASSNAME); + } + + /** + * Sets widget to given location. + * + * If location already contains a widget it will be removed. + * + * @param widget + * Widget to be set into location. + * @param location + * location name where widget will be added + * + * @throws IllegalArgumentException + * if no such location is found in the layout. + */ + public void setWidget(Widget widget, String location) { + + if (widget == null) { + return; + } + + // If no given location is found in the layout, and exception is throws + Element elem = locationToElement.get(location); + if (elem == null && hasTemplate()) { + throw new IllegalArgumentException("No location " + location + + " found"); + } + + // Get previous widget + final Widget previous = locationToWidget.get(location); + // NOP if given widget already exists in this location + if (previous == widget) { + return; + } + + if (previous != null) { + remove(previous); + } + + // if template is missing add element in order + if (!hasTemplate()) { + elem = getElement(); + } + + // Add widget to location + super.add(widget, elem); + locationToWidget.put(location, widget); + } + + /** Initialize HTML-layout. */ + public void initializeHTML(String template, String themeUri) { + + // Connect body of the template to DOM + template = extractBodyAndScriptsFromTemplate(template); + + // TODO prefix img src:s here with a regeps, cannot work further with IE + + String relImgPrefix = themeUri + "/layouts/"; + + // prefix all relative image elements to point to theme dir with a + // regexp search + template = template.replaceAll( + "<((?:img)|(?:IMG))\\s([^>]*)src=\"((?![a-z]+:)[^/][^\"]+)\"", + "<$1 $2src=\"" + relImgPrefix + "$3\""); + // also support src attributes without quotes + template = template + .replaceAll( + "<((?:img)|(?:IMG))\\s([^>]*)src=[^\"]((?![a-z]+:)[^/][^ />]+)[ />]", + "<$1 $2src=\"" + relImgPrefix + "$3\""); + // also prefix relative style="...url(...)..." + template = template + .replaceAll( + "(<[^>]+style=\"[^\"]*url\\()((?![a-z]+:)[^/][^\"]+)(\\)[^>]*>)", + "$1 " + relImgPrefix + "$2 $3"); + + getElement().setInnerHTML(template); + + // Remap locations to elements + locationToElement.clear(); + scanForLocations(getElement()); + + initImgElements(); + + elementWithNativeResizeFunction = DOM.getFirstChild(getElement()); + if (elementWithNativeResizeFunction == null) { + elementWithNativeResizeFunction = getElement(); + } + publishResizedFunction(elementWithNativeResizeFunction); + + htmlInitialized = true; + } + + private native boolean uriEndsWithSlash() + /*-{ + var path = $wnd.location.pathname; + if(path.charAt(path.length - 1) == "/") + return true; + return false; + }-*/; + + boolean hasTemplate() { + return htmlInitialized; + } + + /** Collect locations from template */ + private void scanForLocations(Element elem) { + + final String location = elem.getAttribute("location"); + if (!"".equals(location)) { + locationToElement.put(location, elem); + elem.setInnerHTML(""); + + } else { + final int len = DOM.getChildCount(elem); + for (int i = 0; i < len; i++) { + scanForLocations(DOM.getChild(elem, i)); + } + } + } + + /** Evaluate given script in browser document */ + static native void eval(String script) + /*-{ + try { + if (script != null) + eval("{ var document = $doc; var window = $wnd; "+ script + "}"); + } catch (e) { + } + }-*/; + + /** + * Img elements needs some special handling in custom layout. Img elements + * will get their onload events sunk. This way custom layout can notify + * parent about possible size change. + */ + private void initImgElements() { + NodeList<com.google.gwt.dom.client.Element> nodeList = getElement() + .getElementsByTagName("IMG"); + for (int i = 0; i < nodeList.getLength(); i++) { + com.google.gwt.dom.client.ImageElement img = (ImageElement) nodeList + .getItem(i); + DOM.sinkEvents((Element) img.cast(), Event.ONLOAD); + } + } + + /** + * Extract body part and script tags from raw html-template. + * + * Saves contents of all script-tags to private property: scripts. Returns + * contents of the body part for the html without script-tags. Also replaces + * all _UID_ tags with an unique id-string. + * + * @param html + * Original HTML-template received from server + * @return html that is used to create the HTMLPanel. + */ + private String extractBodyAndScriptsFromTemplate(String html) { + + // Replace UID:s + html = html.replaceAll("_UID_", pid + "__"); + + // Exctract script-tags + scripts = ""; + int endOfPrevScript = 0; + int nextPosToCheck = 0; + String lc = html.toLowerCase(); + String res = ""; + int scriptStart = lc.indexOf("<script", nextPosToCheck); + while (scriptStart > 0) { + res += html.substring(endOfPrevScript, scriptStart); + scriptStart = lc.indexOf(">", scriptStart); + final int j = lc.indexOf("</script>", scriptStart); + scripts += html.substring(scriptStart + 1, j) + ";"; + nextPosToCheck = endOfPrevScript = j + "</script>".length(); + scriptStart = lc.indexOf("<script", nextPosToCheck); + } + res += html.substring(endOfPrevScript); + + // Extract body + html = res; + lc = html.toLowerCase(); + int startOfBody = lc.indexOf("<body"); + if (startOfBody < 0) { + res = html; + } else { + res = ""; + startOfBody = lc.indexOf(">", startOfBody) + 1; + final int endOfBody = lc.indexOf("</body>", startOfBody); + if (endOfBody > startOfBody) { + res = html.substring(startOfBody, endOfBody); + } else { + res = html.substring(startOfBody); + } + } + + return res; + } + + /** Update caption for given widget */ + public void updateCaption(ComponentConnector paintable) { + Widget widget = paintable.getWidget(); + VCaptionWrapper wrapper = childWidgetToCaptionWrapper.get(widget); + if (VCaption.isNeeded(paintable.getState())) { + if (wrapper == null) { + // Add a wrapper between the layout and the child widget + final String loc = getLocation(widget); + super.remove(widget); + wrapper = new VCaptionWrapper(paintable, client); + super.add(wrapper, locationToElement.get(loc)); + childWidgetToCaptionWrapper.put(widget, wrapper); + } + wrapper.updateCaption(); + } else { + if (wrapper != null) { + // Remove the wrapper and add the widget directly to the layout + final String loc = getLocation(widget); + super.remove(wrapper); + super.add(widget, locationToElement.get(loc)); + childWidgetToCaptionWrapper.remove(widget); + } + } + } + + /** Get the location of an widget */ + public String getLocation(Widget w) { + for (final Iterator<String> i = locationToWidget.keySet().iterator(); i + .hasNext();) { + final String location = i.next(); + if (locationToWidget.get(location) == w) { + return location; + } + } + return null; + } + + /** Removes given widget from the layout */ + @Override + public boolean remove(Widget w) { + final String location = getLocation(w); + if (location != null) { + locationToWidget.remove(location); + } + final VCaptionWrapper cw = childWidgetToCaptionWrapper.get(w); + if (cw != null) { + childWidgetToCaptionWrapper.remove(w); + return super.remove(cw); + } else if (w != null) { + return super.remove(w); + } + return false; + } + + /** Adding widget without specifying location is not supported */ + @Override + public void add(Widget w) { + throw new UnsupportedOperationException(); + } + + /** Clear all widgets from the layout */ + @Override + public void clear() { + super.clear(); + locationToWidget.clear(); + childWidgetToCaptionWrapper.clear(); + } + + /** + * This method is published to JS side with the same name into first DOM + * node of custom layout. This way if one implements some resizeable + * containers in custom layout he/she can notify children after resize. + */ + public void notifyChildrenOfSizeChange() { + client.runDescendentsLayout(this); + } + + @Override + public void onDetach() { + super.onDetach(); + if (elementWithNativeResizeFunction != null) { + detachResizedFunction(elementWithNativeResizeFunction); + } + } + + private native void detachResizedFunction(Element element) + /*-{ + element.notifyChildrenOfSizeChange = null; + }-*/; + + private native void publishResizedFunction(Element element) + /*-{ + var self = this; + element.notifyChildrenOfSizeChange = $entry(function() { + self.@com.vaadin.terminal.gwt.client.ui.customlayout.VCustomLayout::notifyChildrenOfSizeChange()(); + }); + }-*/; + + /** + * In custom layout one may want to run layout functions made with + * JavaScript. This function tests if one exists (with name "iLayoutJS" in + * layouts first DOM node) and runs et. Return value is used to determine if + * children needs to be notified of size changes. + * + * Note! When implementing a JS layout function you most likely want to call + * notifyChildrenOfSizeChange() function on your custom layouts main + * element. That method is used to control whether child components layout + * functions are to be run. + * + * @param el + * @return true if layout function exists and was run successfully, else + * false. + */ + native boolean iLayoutJS(Element el) + /*-{ + if(el && el.iLayoutJS) { + try { + el.iLayoutJS(); + return true; + } catch (e) { + return false; + } + } else { + return false; + } + }-*/; + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + event.cancelBubble(true); + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/AbstractDateFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/AbstractDateFieldConnector.java new file mode 100644 index 0000000000..791849b067 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/AbstractDateFieldConnector.java @@ -0,0 +1,124 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.datefield; + +import java.util.Date; + +import com.vaadin.shared.ui.datefield.DateFieldConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.LocaleNotLoadedException; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector; + +public class AbstractDateFieldConnector extends AbstractFieldConnector + implements Paintable { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (!isRealUpdate(uidl)) { + return; + } + + // Save details + getWidget().client = client; + getWidget().paintableId = uidl.getId(); + getWidget().immediate = getState().isImmediate(); + + getWidget().readonly = isReadOnly(); + getWidget().enabled = isEnabled(); + + if (uidl.hasAttribute("locale")) { + final String locale = uidl.getStringAttribute("locale"); + try { + getWidget().dts.setLocale(locale); + getWidget().currentLocale = locale; + } catch (final LocaleNotLoadedException e) { + getWidget().currentLocale = getWidget().dts.getLocale(); + VConsole.error("Tried to use an unloaded locale \"" + locale + + "\". Using default locale (" + + getWidget().currentLocale + ")."); + VConsole.error(e); + } + } + + // We show week numbers only if the week starts with Monday, as ISO 8601 + // specifies + getWidget().showISOWeekNumbers = uidl + .getBooleanAttribute(DateFieldConstants.ATTR_WEEK_NUMBERS) + && getWidget().dts.getFirstDayOfWeek() == 1; + + int newResolution; + if (uidl.hasVariable("sec")) { + newResolution = VDateField.RESOLUTION_SEC; + } else if (uidl.hasVariable("min")) { + newResolution = VDateField.RESOLUTION_MIN; + } else if (uidl.hasVariable("hour")) { + newResolution = VDateField.RESOLUTION_HOUR; + } else if (uidl.hasVariable("day")) { + newResolution = VDateField.RESOLUTION_DAY; + } else if (uidl.hasVariable("month")) { + newResolution = VDateField.RESOLUTION_MONTH; + } else { + newResolution = VDateField.RESOLUTION_YEAR; + } + + // Remove old stylename that indicates current resolution + setWidgetStyleName( + VDateField.CLASSNAME + + "-" + + VDateField + .resolutionToString(getWidget().currentResolution), + false); + + getWidget().currentResolution = newResolution; + + // Add stylename that indicates current resolution + setWidgetStyleName( + VDateField.CLASSNAME + + "-" + + VDateField + .resolutionToString(getWidget().currentResolution), + true); + + final int year = uidl.getIntVariable("year"); + final int month = (getWidget().currentResolution >= VDateField.RESOLUTION_MONTH) ? uidl + .getIntVariable("month") : -1; + final int day = (getWidget().currentResolution >= VDateField.RESOLUTION_DAY) ? uidl + .getIntVariable("day") : -1; + final int hour = (getWidget().currentResolution >= VDateField.RESOLUTION_HOUR) ? uidl + .getIntVariable("hour") : 0; + final int min = (getWidget().currentResolution >= VDateField.RESOLUTION_MIN) ? uidl + .getIntVariable("min") : 0; + final int sec = (getWidget().currentResolution >= VDateField.RESOLUTION_SEC) ? uidl + .getIntVariable("sec") : 0; + + // Construct new date for this datefield (only if not null) + if (year > -1) { + getWidget().setCurrentDate( + new Date((long) getWidget().getTime(year, month, day, hour, + min, sec, 0))); + } else { + getWidget().setCurrentDate(null); + } + } + + @Override + public VDateField getWidget() { + return (VDateField) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/InlineDateFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/InlineDateFieldConnector.java new file mode 100644 index 0000000000..52f10348b7 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/InlineDateFieldConnector.java @@ -0,0 +1,111 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.datefield; + +import java.util.Date; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.DateTimeService; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.FocusChangeListener; +import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.TimeChangeListener; +import com.vaadin.ui.InlineDateField; + +@Connect(InlineDateField.class) +public class InlineDateFieldConnector extends AbstractDateFieldConnector { + + @Override + @SuppressWarnings("deprecation") + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + super.updateFromUIDL(uidl, client); + if (!isRealUpdate(uidl)) { + return; + } + + getWidget().calendarPanel.setShowISOWeekNumbers(getWidget() + .isShowISOWeekNumbers()); + getWidget().calendarPanel.setDateTimeService(getWidget() + .getDateTimeService()); + getWidget().calendarPanel.setResolution(getWidget() + .getCurrentResolution()); + Date currentDate = getWidget().getCurrentDate(); + if (currentDate != null) { + getWidget().calendarPanel.setDate(new Date(currentDate.getTime())); + } else { + getWidget().calendarPanel.setDate(null); + } + + if (getWidget().currentResolution > VDateField.RESOLUTION_DAY) { + getWidget().calendarPanel + .setTimeChangeListener(new TimeChangeListener() { + @Override + public void changed(int hour, int min, int sec, int msec) { + Date d = getWidget().getDate(); + if (d == null) { + // date currently null, use the value from + // calendarPanel + // (~ client time at the init of the widget) + d = (Date) getWidget().calendarPanel.getDate() + .clone(); + } + d.setHours(hour); + d.setMinutes(min); + d.setSeconds(sec); + DateTimeService.setMilliseconds(d, msec); + + // Always update time changes to the server + getWidget().calendarPanel.setDate(d); + getWidget().updateValueFromPanel(); + } + }); + } + + if (getWidget().currentResolution <= VDateField.RESOLUTION_MONTH) { + getWidget().calendarPanel + .setFocusChangeListener(new FocusChangeListener() { + @Override + public void focusChanged(Date date) { + Date date2 = new Date(); + if (getWidget().calendarPanel.getDate() != null) { + date2.setTime(getWidget().calendarPanel + .getDate().getTime()); + } + /* + * Update the value of calendarPanel + */ + date2.setYear(date.getYear()); + date2.setMonth(date.getMonth()); + getWidget().calendarPanel.setDate(date2); + /* + * Then update the value from panel to server + */ + getWidget().updateValueFromPanel(); + } + }); + } else { + getWidget().calendarPanel.setFocusChangeListener(null); + } + + // Update possible changes + getWidget().calendarPanel.renderCalendar(); + } + + @Override + public VDateFieldCalendar getWidget() { + return (VDateFieldCalendar) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/PopupDateFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/PopupDateFieldConnector.java new file mode 100644 index 0000000000..6c4ec40694 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/PopupDateFieldConnector.java @@ -0,0 +1,149 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.datefield; + +import java.util.Date; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.DateTimeService; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.FocusChangeListener; +import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.TimeChangeListener; +import com.vaadin.ui.DateField; + +@Connect(DateField.class) +public class PopupDateFieldConnector extends TextualDateConnector { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.VTextualDate#updateFromUIDL(com.vaadin + * .terminal.gwt.client.UIDL, + * com.vaadin.terminal.gwt.client.ApplicationConnection) + */ + @Override + @SuppressWarnings("deprecation") + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + boolean lastReadOnlyState = getWidget().readonly; + boolean lastEnabledState = getWidget().isEnabled(); + + getWidget().parsable = uidl.getBooleanAttribute("parsable"); + + super.updateFromUIDL(uidl, client); + + getWidget().calendar.setDateTimeService(getWidget() + .getDateTimeService()); + getWidget().calendar.setShowISOWeekNumbers(getWidget() + .isShowISOWeekNumbers()); + if (getWidget().calendar.getResolution() != getWidget().currentResolution) { + getWidget().calendar.setResolution(getWidget().currentResolution); + if (getWidget().calendar.getDate() != null) { + getWidget().calendar.setDate((Date) getWidget() + .getCurrentDate().clone()); + // force re-render when changing resolution only + getWidget().calendar.renderCalendar(); + } + } + getWidget().calendarToggle.setEnabled(getWidget().enabled); + + if (getWidget().currentResolution <= VPopupCalendar.RESOLUTION_MONTH) { + getWidget().calendar + .setFocusChangeListener(new FocusChangeListener() { + @Override + public void focusChanged(Date date) { + getWidget().updateValue(date); + getWidget().buildDate(); + Date date2 = getWidget().calendar.getDate(); + date2.setYear(date.getYear()); + date2.setMonth(date.getMonth()); + } + }); + } else { + getWidget().calendar.setFocusChangeListener(null); + } + + if (getWidget().currentResolution > VPopupCalendar.RESOLUTION_DAY) { + getWidget().calendar + .setTimeChangeListener(new TimeChangeListener() { + @Override + public void changed(int hour, int min, int sec, int msec) { + Date d = getWidget().getDate(); + if (d == null) { + // date currently null, use the value from + // calendarPanel + // (~ client time at the init of the widget) + d = (Date) getWidget().calendar.getDate() + .clone(); + } + d.setHours(hour); + d.setMinutes(min); + d.setSeconds(sec); + DateTimeService.setMilliseconds(d, msec); + + // Always update time changes to the server + getWidget().updateValue(d); + + // Update text field + getWidget().buildDate(); + } + }); + } + + if (getWidget().readonly) { + getWidget().calendarToggle.addStyleName(VPopupCalendar.CLASSNAME + + "-button-readonly"); + } else { + getWidget().calendarToggle.removeStyleName(VPopupCalendar.CLASSNAME + + "-button-readonly"); + } + + getWidget().calendarToggle.setEnabled(true); + } + + @Override + public VPopupCalendar getWidget() { + return (VPopupCalendar) super.getWidget(); + } + + @Override + protected void setWidgetStyleName(String styleName, boolean add) { + super.setWidgetStyleName(styleName, add); + + // update the style change to popup calendar widget + getWidget().popup.setStyleName(styleName, add); + } + + @Override + protected void setWidgetStyleNameWithPrefix(String prefix, + String styleName, boolean add) { + super.setWidgetStyleNameWithPrefix(prefix, styleName, add); + + // update the style change to popup calendar widget with the correct + // prefix + if (!styleName.startsWith("-")) { + getWidget().popup.setStyleName( + VPopupCalendar.POPUP_PRIMARY_STYLE_NAME + "-" + styleName, + add); + } else { + getWidget().popup.setStyleName( + VPopupCalendar.POPUP_PRIMARY_STYLE_NAME + styleName, add); + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/TextualDateConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/TextualDateConnector.java new file mode 100644 index 0000000000..01c1529429 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/TextualDateConnector.java @@ -0,0 +1,61 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.datefield; + +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.UIDL; + +public class TextualDateConnector extends AbstractDateFieldConnector { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + int origRes = getWidget().currentResolution; + String oldLocale = getWidget().currentLocale; + super.updateFromUIDL(uidl, client); + if (origRes != getWidget().currentResolution + || oldLocale != getWidget().currentLocale) { + // force recreating format string + getWidget().formatStr = null; + } + if (uidl.hasAttribute("format")) { + getWidget().formatStr = uidl.getStringAttribute("format"); + } + + getWidget().inputPrompt = uidl + .getStringAttribute(VTextualDate.ATTR_INPUTPROMPT); + + getWidget().lenient = !uidl.getBooleanAttribute("strict"); + + getWidget().buildDate(); + // not a FocusWidget -> needs own tabindex handling + if (uidl.hasAttribute("tabindex")) { + getWidget().text.setTabIndex(uidl.getIntAttribute("tabindex")); + } + + if (getWidget().readonly) { + getWidget().text.addStyleDependentName("readonly"); + } else { + getWidget().text.removeStyleDependentName("readonly"); + } + + } + + @Override + public VTextualDate getWidget() { + return (VTextualDate) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VCalendarPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VCalendarPanel.java new file mode 100644 index 0000000000..b61ce5dbf3 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VCalendarPanel.java @@ -0,0 +1,1769 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.datefield; + +import java.util.Date; +import java.util.Iterator; + +import com.google.gwt.dom.client.Node; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.DomEvent; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.dom.client.MouseDownEvent; +import com.google.gwt.event.dom.client.MouseDownHandler; +import com.google.gwt.event.dom.client.MouseOutEvent; +import com.google.gwt.event.dom.client.MouseOutHandler; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlexTable; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.InlineHTML; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.DateTimeService; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.FocusableFlexTable; +import com.vaadin.terminal.gwt.client.ui.SubPartAware; +import com.vaadin.terminal.gwt.client.ui.label.VLabel; +import com.vaadin.terminal.gwt.client.ui.nativeselect.VNativeSelect; + +@SuppressWarnings("deprecation") +public class VCalendarPanel extends FocusableFlexTable implements + KeyDownHandler, KeyPressHandler, MouseOutHandler, MouseDownHandler, + MouseUpHandler, BlurHandler, FocusHandler, SubPartAware { + + public interface SubmitListener { + + /** + * Called when calendar user triggers a submitting operation in calendar + * panel. Eg. clicking on day or hitting enter. + */ + void onSubmit(); + + /** + * On eg. ESC key. + */ + void onCancel(); + } + + /** + * Blur listener that listens to blur event from the panel + */ + public interface FocusOutListener { + /** + * @return true if the calendar panel is not used after focus moves out + */ + boolean onFocusOut(DomEvent<?> event); + } + + /** + * FocusChangeListener is notified when the panel changes its _focused_ + * value. + */ + public interface FocusChangeListener { + void focusChanged(Date focusedDate); + } + + /** + * Dispatches an event when the panel when time is changed + */ + public interface TimeChangeListener { + + void changed(int hour, int min, int sec, int msec); + } + + /** + * Represents a Date button in the calendar + */ + private class VEventButton extends Button { + public VEventButton() { + addMouseDownHandler(VCalendarPanel.this); + addMouseOutHandler(VCalendarPanel.this); + addMouseUpHandler(VCalendarPanel.this); + } + } + + private static final String CN_FOCUSED = "focused"; + + private static final String CN_TODAY = "today"; + + private static final String CN_SELECTED = "selected"; + + private static final String CN_OFFMONTH = "offmonth"; + + /** + * Represents a click handler for when a user selects a value by using the + * mouse + */ + private ClickHandler dayClickHandler = new ClickHandler() { + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt + * .event.dom.client.ClickEvent) + */ + @Override + public void onClick(ClickEvent event) { + Day day = (Day) event.getSource(); + focusDay(day.getDate()); + selectFocused(); + onSubmit(); + } + }; + + private VEventButton prevYear; + + private VEventButton nextYear; + + private VEventButton prevMonth; + + private VEventButton nextMonth; + + private VTime time; + + private FlexTable days = new FlexTable(); + + private int resolution = VDateField.RESOLUTION_YEAR; + + private int focusedRow; + + private Timer mouseTimer; + + private Date value; + + private boolean enabled = true; + + private boolean readonly = false; + + private DateTimeService dateTimeService; + + private boolean showISOWeekNumbers; + + private Date displayedMonth; + + private Date focusedDate; + + private Day selectedDay; + + private Day focusedDay; + + private FocusOutListener focusOutListener; + + private SubmitListener submitListener; + + private FocusChangeListener focusChangeListener; + + private TimeChangeListener timeChangeListener; + + private boolean hasFocus = false; + + public VCalendarPanel() { + + setStyleName(VDateField.CLASSNAME + "-calendarpanel"); + + /* + * Firefox auto-repeat works correctly only if we use a key press + * handler, other browsers handle it correctly when using a key down + * handler + */ + if (BrowserInfo.get().isGecko()) { + addKeyPressHandler(this); + } else { + addKeyDownHandler(this); + } + addFocusHandler(this); + addBlurHandler(this); + + } + + /** + * Sets the focus to given date in the current view. Used when moving in the + * calendar with the keyboard. + * + * @param date + * A Date representing the day of month to be focused. Must be + * one of the days currently visible. + */ + private void focusDay(Date date) { + // Only used when calender body is present + if (resolution > VDateField.RESOLUTION_MONTH) { + if (focusedDay != null) { + focusedDay.removeStyleDependentName(CN_FOCUSED); + } + + if (date != null && focusedDate != null) { + focusedDate.setTime(date.getTime()); + int rowCount = days.getRowCount(); + for (int i = 0; i < rowCount; i++) { + int cellCount = days.getCellCount(i); + for (int j = 0; j < cellCount; j++) { + Widget widget = days.getWidget(i, j); + if (widget != null && widget instanceof Day) { + Day curday = (Day) widget; + if (curday.getDate().equals(date)) { + curday.addStyleDependentName(CN_FOCUSED); + focusedDay = curday; + focusedRow = i; + return; + } + } + } + } + } + } + } + + /** + * Sets the selection highlight to a given day in the current view + * + * @param date + * A Date representing the day of month to be selected. Must be + * one of the days currently visible. + * + */ + private void selectDate(Date date) { + if (selectedDay != null) { + selectedDay.removeStyleDependentName(CN_SELECTED); + } + + int rowCount = days.getRowCount(); + for (int i = 0; i < rowCount; i++) { + int cellCount = days.getCellCount(i); + for (int j = 0; j < cellCount; j++) { + Widget widget = days.getWidget(i, j); + if (widget != null && widget instanceof Day) { + Day curday = (Day) widget; + if (curday.getDate().equals(date)) { + curday.addStyleDependentName(CN_SELECTED); + selectedDay = curday; + return; + } + } + } + } + } + + /** + * Updates year, month, day from focusedDate to value + */ + private void selectFocused() { + if (focusedDate != null) { + if (value == null) { + // No previously selected value (set to null on server side). + // Create a new date using current date and time + value = new Date(); + } + /* + * #5594 set Date (day) to 1 in order to prevent any kind of + * wrapping of months when later setting the month. (e.g. 31 -> + * month with 30 days -> wraps to the 1st of the following month, + * e.g. 31st of May -> 31st of April = 1st of May) + */ + value.setDate(1); + if (value.getYear() != focusedDate.getYear()) { + value.setYear(focusedDate.getYear()); + } + if (value.getMonth() != focusedDate.getMonth()) { + value.setMonth(focusedDate.getMonth()); + } + if (value.getDate() != focusedDate.getDate()) { + } + // We always need to set the date, even if it hasn't changed, since + // it was forced to 1 above. + value.setDate(focusedDate.getDate()); + + selectDate(focusedDate); + } else { + VConsole.log("Trying to select a the focused date which is NULL!"); + } + } + + protected boolean onValueChange() { + return false; + } + + public int getResolution() { + return resolution; + } + + public void setResolution(int resolution) { + this.resolution = resolution; + if (time != null) { + time.removeFromParent(); + time = null; + } + } + + private boolean isReadonly() { + return readonly; + } + + private boolean isEnabled() { + return enabled; + } + + private void clearCalendarBody(boolean remove) { + if (!remove) { + // Leave the cells in place but clear their contents + + // This has the side effect of ensuring that the calendar always + // contain 7 rows. + for (int row = 1; row < 7; row++) { + for (int col = 0; col < 8; col++) { + days.setHTML(row, col, " "); + } + } + } else if (getRowCount() > 1) { + removeRow(1); + days.clear(); + } + } + + /** + * Builds the top buttons and current month and year header. + * + * @param needsMonth + * Should the month buttons be visible? + */ + private void buildCalendarHeader(boolean needsMonth) { + + getRowFormatter().addStyleName(0, + VDateField.CLASSNAME + "-calendarpanel-header"); + + if (prevMonth == null && needsMonth) { + prevMonth = new VEventButton(); + prevMonth.setHTML("‹"); + prevMonth.setStyleName("v-button-prevmonth"); + prevMonth.setTabIndex(-1); + nextMonth = new VEventButton(); + nextMonth.setHTML("›"); + nextMonth.setStyleName("v-button-nextmonth"); + nextMonth.setTabIndex(-1); + getFlexCellFormatter().setStyleName(0, 3, + VDateField.CLASSNAME + "-calendarpanel-nextmonth"); + getFlexCellFormatter().setStyleName(0, 1, + VDateField.CLASSNAME + "-calendarpanel-prevmonth"); + + setWidget(0, 3, nextMonth); + setWidget(0, 1, prevMonth); + } else if (prevMonth != null && !needsMonth) { + // Remove month traverse buttons + remove(prevMonth); + remove(nextMonth); + prevMonth = null; + nextMonth = null; + } + + if (prevYear == null) { + prevYear = new VEventButton(); + prevYear.setHTML("«"); + prevYear.setStyleName("v-button-prevyear"); + prevYear.setTabIndex(-1); + nextYear = new VEventButton(); + nextYear.setHTML("»"); + nextYear.setStyleName("v-button-nextyear"); + nextYear.setTabIndex(-1); + setWidget(0, 0, prevYear); + setWidget(0, 4, nextYear); + getFlexCellFormatter().setStyleName(0, 0, + VDateField.CLASSNAME + "-calendarpanel-prevyear"); + getFlexCellFormatter().setStyleName(0, 4, + VDateField.CLASSNAME + "-calendarpanel-nextyear"); + } + + final String monthName = needsMonth ? getDateTimeService().getMonth( + focusedDate.getMonth()) : ""; + final int year = focusedDate.getYear() + 1900; + getFlexCellFormatter().setStyleName(0, 2, + VDateField.CLASSNAME + "-calendarpanel-month"); + setHTML(0, 2, "<span class=\"" + VDateField.CLASSNAME + + "-calendarpanel-month\">" + monthName + " " + year + + "</span>"); + } + + private DateTimeService getDateTimeService() { + return dateTimeService; + } + + public void setDateTimeService(DateTimeService dateTimeService) { + this.dateTimeService = dateTimeService; + } + + /** + * Returns whether ISO 8601 week numbers should be shown in the value + * selector or not. ISO 8601 defines that a week always starts with a Monday + * so the week numbers are only shown if this is the case. + * + * @return true if week number should be shown, false otherwise + */ + public boolean isShowISOWeekNumbers() { + return showISOWeekNumbers; + } + + public void setShowISOWeekNumbers(boolean showISOWeekNumbers) { + this.showISOWeekNumbers = showISOWeekNumbers; + } + + /** + * Builds the day and time selectors of the calendar. + */ + private void buildCalendarBody() { + + final int weekColumn = 0; + final int firstWeekdayColumn = 1; + final int headerRow = 0; + + setWidget(1, 0, days); + setCellPadding(0); + setCellSpacing(0); + getFlexCellFormatter().setColSpan(1, 0, 5); + getFlexCellFormatter().setStyleName(1, 0, + VDateField.CLASSNAME + "-calendarpanel-body"); + + days.getFlexCellFormatter().setStyleName(headerRow, weekColumn, + "v-week"); + days.setHTML(headerRow, weekColumn, "<strong></strong>"); + // Hide the week column if week numbers are not to be displayed. + days.getFlexCellFormatter().setVisible(headerRow, weekColumn, + isShowISOWeekNumbers()); + + days.getRowFormatter().setStyleName(headerRow, + VDateField.CLASSNAME + "-calendarpanel-weekdays"); + + if (isShowISOWeekNumbers()) { + days.getFlexCellFormatter().setStyleName(headerRow, weekColumn, + "v-first"); + days.getFlexCellFormatter().setStyleName(headerRow, + firstWeekdayColumn, ""); + days.getRowFormatter().addStyleName(headerRow, + VDateField.CLASSNAME + "-calendarpanel-weeknumbers"); + } else { + days.getFlexCellFormatter().setStyleName(headerRow, weekColumn, ""); + days.getFlexCellFormatter().setStyleName(headerRow, + firstWeekdayColumn, "v-first"); + } + + days.getFlexCellFormatter().setStyleName(headerRow, + firstWeekdayColumn + 6, "v-last"); + + // Print weekday names + final int firstDay = getDateTimeService().getFirstDayOfWeek(); + for (int i = 0; i < 7; i++) { + int day = i + firstDay; + if (day > 6) { + day = 0; + } + if (getResolution() > VDateField.RESOLUTION_MONTH) { + days.setHTML(headerRow, firstWeekdayColumn + i, "<strong>" + + getDateTimeService().getShortDay(day) + "</strong>"); + } else { + days.setHTML(headerRow, firstWeekdayColumn + i, ""); + } + } + + // Zero out hours, minutes, seconds, and milliseconds to compare dates + // without time part + final Date tmp = new Date(); + final Date today = new Date(tmp.getYear(), tmp.getMonth(), + tmp.getDate()); + + final Date selectedDate = value == null ? null : new Date( + value.getYear(), value.getMonth(), value.getDate()); + + final int startWeekDay = getDateTimeService().getStartWeekDay( + displayedMonth); + final Date curr = (Date) displayedMonth.clone(); + // Start from the first day of the week that at least partially belongs + // to the current month + curr.setDate(1 - startWeekDay); + + // No month has more than 6 weeks so 6 is a safe maximum for rows. + for (int weekOfMonth = 1; weekOfMonth < 7; weekOfMonth++) { + for (int dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) { + + // Actually write the day of month + Day day = new Day((Date) curr.clone()); + + if (curr.equals(selectedDate)) { + day.addStyleDependentName(CN_SELECTED); + selectedDay = day; + } + if (curr.equals(today)) { + day.addStyleDependentName(CN_TODAY); + } + if (curr.equals(focusedDate)) { + focusedDay = day; + focusedRow = weekOfMonth; + if (hasFocus) { + day.addStyleDependentName(CN_FOCUSED); + } + } + if (curr.getMonth() != displayedMonth.getMonth()) { + day.addStyleDependentName(CN_OFFMONTH); + } + + days.setWidget(weekOfMonth, firstWeekdayColumn + dayOfWeek, day); + + // ISO week numbers if requested + days.getCellFormatter().setVisible(weekOfMonth, weekColumn, + isShowISOWeekNumbers()); + if (isShowISOWeekNumbers()) { + final String baseCssClass = VDateField.CLASSNAME + + "-calendarpanel-weeknumber"; + String weekCssClass = baseCssClass; + + int weekNumber = DateTimeService.getISOWeekNumber(curr); + + days.setHTML(weekOfMonth, 0, "<span class=\"" + + weekCssClass + "\"" + ">" + weekNumber + + "</span>"); + } + curr.setDate(curr.getDate() + 1); + } + } + } + + /** + * Do we need the time selector + * + * @return True if it is required + */ + private boolean isTimeSelectorNeeded() { + return getResolution() > VDateField.RESOLUTION_DAY; + } + + /** + * Updates the calendar and text field with the selected dates. + */ + public void renderCalendar() { + if (focusedDate == null) { + Date now = new Date(); + // focusedDate must have zero hours, mins, secs, millisecs + focusedDate = new Date(now.getYear(), now.getMonth(), now.getDate()); + displayedMonth = new Date(now.getYear(), now.getMonth(), 1); + } + + if (getResolution() <= VDateField.RESOLUTION_MONTH + && focusChangeListener != null) { + focusChangeListener.focusChanged(new Date(focusedDate.getTime())); + } + + final boolean needsMonth = getResolution() > VDateField.RESOLUTION_YEAR; + boolean needsBody = getResolution() >= VDateField.RESOLUTION_DAY; + buildCalendarHeader(needsMonth); + clearCalendarBody(!needsBody); + if (needsBody) { + buildCalendarBody(); + } + + if (isTimeSelectorNeeded() && time == null) { + time = new VTime(); + setWidget(2, 0, time); + getFlexCellFormatter().setColSpan(2, 0, 5); + getFlexCellFormatter().setStyleName(2, 0, + VDateField.CLASSNAME + "-calendarpanel-time"); + } else if (isTimeSelectorNeeded()) { + time.updateTimes(); + } else if (time != null) { + remove(time); + } + } + + /** + * Moves the focus forward the given number of days. + */ + private void focusNextDay(int days) { + int oldMonth = focusedDate.getMonth(); + focusedDate.setDate(focusedDate.getDate() + days); + + if (focusedDate.getMonth() == oldMonth) { + // Month did not change, only move the selection + focusDay(focusedDate); + } else { + // If the month changed we need to re-render the calendar + displayedMonth.setMonth(focusedDate.getMonth()); + renderCalendar(); + } + } + + /** + * Moves the focus backward the given number of days. + */ + private void focusPreviousDay(int days) { + focusNextDay(-days); + } + + /** + * Selects the next month + */ + private void focusNextMonth() { + + int currentMonth = focusedDate.getMonth(); + focusedDate.setMonth(currentMonth + 1); + int requestedMonth = (currentMonth + 1) % 12; + + /* + * If the selected value was e.g. 31.3 the new value would be 31.4 but + * this value is invalid so the new value will be 1.5. This is taken + * care of by decreasing the value until we have the correct month. + */ + while (focusedDate.getMonth() != requestedMonth) { + focusedDate.setDate(focusedDate.getDate() - 1); + } + displayedMonth.setMonth(displayedMonth.getMonth() + 1); + + renderCalendar(); + } + + /** + * Selects the previous month + */ + private void focusPreviousMonth() { + int currentMonth = focusedDate.getMonth(); + focusedDate.setMonth(currentMonth - 1); + + /* + * If the selected value was e.g. 31.12 the new value would be 31.11 but + * this value is invalid so the new value will be 1.12. This is taken + * care of by decreasing the value until we have the correct month. + */ + while (focusedDate.getMonth() == currentMonth) { + focusedDate.setDate(focusedDate.getDate() - 1); + } + displayedMonth.setMonth(displayedMonth.getMonth() - 1); + + renderCalendar(); + } + + /** + * Selects the previous year + */ + private void focusPreviousYear(int years) { + int currentMonth = focusedDate.getMonth(); + focusedDate.setYear(focusedDate.getYear() - years); + displayedMonth.setYear(displayedMonth.getYear() - years); + /* + * If the focused date was a leap day (Feb 29), the new date becomes Mar + * 1 if the new year is not also a leap year. Set it to Feb 28 instead. + */ + if (focusedDate.getMonth() != currentMonth) { + focusedDate.setDate(0); + } + renderCalendar(); + } + + /** + * Selects the next year + */ + private void focusNextYear(int years) { + int currentMonth = focusedDate.getMonth(); + focusedDate.setYear(focusedDate.getYear() + years); + displayedMonth.setYear(displayedMonth.getYear() + years); + /* + * If the focused date was a leap day (Feb 29), the new date becomes Mar + * 1 if the new year is not also a leap year. Set it to Feb 28 instead. + */ + if (focusedDate.getMonth() != currentMonth) { + focusedDate.setDate(0); + } + renderCalendar(); + } + + /** + * Handles a user click on the component + * + * @param sender + * The component that was clicked + * @param updateVariable + * Should the value field be updated + * + */ + private void processClickEvent(Widget sender) { + if (!isEnabled() || isReadonly()) { + return; + } + if (sender == prevYear) { + focusPreviousYear(1); + } else if (sender == nextYear) { + focusNextYear(1); + } else if (sender == prevMonth) { + focusPreviousMonth(); + } else if (sender == nextMonth) { + focusNextMonth(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt + * .event.dom.client.KeyDownEvent) + */ + @Override + public void onKeyDown(KeyDownEvent event) { + handleKeyPress(event); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google + * .gwt.event.dom.client.KeyPressEvent) + */ + @Override + public void onKeyPress(KeyPressEvent event) { + handleKeyPress(event); + } + + /** + * Handles the keypress from both the onKeyPress event and the onKeyDown + * event + * + * @param event + * The keydown/keypress event + */ + private void handleKeyPress(DomEvent<?> event) { + if (time != null + && time.getElement().isOrHasChild( + (Node) event.getNativeEvent().getEventTarget().cast())) { + int nativeKeyCode = event.getNativeEvent().getKeyCode(); + if (nativeKeyCode == getSelectKey()) { + onSubmit(); // submit happens if enter key hit down on listboxes + event.preventDefault(); + event.stopPropagation(); + } + return; + } + + // Check tabs + int keycode = event.getNativeEvent().getKeyCode(); + if (keycode == KeyCodes.KEY_TAB && event.getNativeEvent().getShiftKey()) { + if (onTabOut(event)) { + return; + } + } + + // Handle the navigation + if (handleNavigation(keycode, event.getNativeEvent().getCtrlKey() + || event.getNativeEvent().getMetaKey(), event.getNativeEvent() + .getShiftKey())) { + event.preventDefault(); + } + + } + + /** + * Notifies submit-listeners of a submit event + */ + private void onSubmit() { + if (getSubmitListener() != null) { + getSubmitListener().onSubmit(); + } + } + + /** + * Notifies submit-listeners of a cancel event + */ + private void onCancel() { + if (getSubmitListener() != null) { + getSubmitListener().onCancel(); + } + } + + /** + * Handles the keyboard navigation when the resolution is set to years. + * + * @param keycode + * The keycode to process + * @param ctrl + * Is ctrl pressed? + * @param shift + * is shift pressed + * @return Returns true if the keycode was processed, else false + */ + protected boolean handleNavigationYearMode(int keycode, boolean ctrl, + boolean shift) { + + // Ctrl and Shift selection not supported + if (ctrl || shift) { + return false; + } + + else if (keycode == getPreviousKey()) { + focusNextYear(10); // Add 10 years + return true; + } + + else if (keycode == getForwardKey()) { + focusNextYear(1); // Add 1 year + return true; + } + + else if (keycode == getNextKey()) { + focusPreviousYear(10); // Subtract 10 years + return true; + } + + else if (keycode == getBackwardKey()) { + focusPreviousYear(1); // Subtract 1 year + return true; + + } else if (keycode == getSelectKey()) { + value = (Date) focusedDate.clone(); + onSubmit(); + return true; + + } else if (keycode == getResetKey()) { + // Restore showing value the selected value + focusedDate.setTime(value.getTime()); + renderCalendar(); + return true; + + } else if (keycode == getCloseKey()) { + // TODO fire listener, on users responsibility?? + + return true; + } + return false; + } + + /** + * Handle the keyboard navigation when the resolution is set to MONTH + * + * @param keycode + * The keycode to handle + * @param ctrl + * Was the ctrl key pressed? + * @param shift + * Was the shift key pressed? + * @return + */ + protected boolean handleNavigationMonthMode(int keycode, boolean ctrl, + boolean shift) { + + // Ctrl selection not supported + if (ctrl) { + return false; + + } else if (keycode == getPreviousKey()) { + focusNextYear(1); // Add 1 year + return true; + + } else if (keycode == getForwardKey()) { + focusNextMonth(); // Add 1 month + return true; + + } else if (keycode == getNextKey()) { + focusPreviousYear(1); // Subtract 1 year + return true; + + } else if (keycode == getBackwardKey()) { + focusPreviousMonth(); // Subtract 1 month + return true; + + } else if (keycode == getSelectKey()) { + value = (Date) focusedDate.clone(); + onSubmit(); + return true; + + } else if (keycode == getResetKey()) { + // Restore showing value the selected value + focusedDate.setTime(value.getTime()); + renderCalendar(); + return true; + + } else if (keycode == getCloseKey() || keycode == KeyCodes.KEY_TAB) { + + // TODO fire close event + + return true; + } + + return false; + } + + /** + * Handle keyboard navigation what the resolution is set to DAY + * + * @param keycode + * The keycode to handle + * @param ctrl + * Was the ctrl key pressed? + * @param shift + * Was the shift key pressed? + * @return Return true if the key press was handled by the method, else + * return false. + */ + protected boolean handleNavigationDayMode(int keycode, boolean ctrl, + boolean shift) { + + // Ctrl key is not in use + if (ctrl) { + return false; + } + + /* + * Jumps to the next day. + */ + if (keycode == getForwardKey() && !shift) { + focusNextDay(1); + return true; + + /* + * Jumps to the previous day + */ + } else if (keycode == getBackwardKey() && !shift) { + focusPreviousDay(1); + return true; + + /* + * Jumps one week forward in the calendar + */ + } else if (keycode == getNextKey() && !shift) { + focusNextDay(7); + return true; + + /* + * Jumps one week back in the calendar + */ + } else if (keycode == getPreviousKey() && !shift) { + focusPreviousDay(7); + return true; + + /* + * Selects the value that is chosen + */ + } else if (keycode == getSelectKey() && !shift) { + selectFocused(); + onSubmit(); // submit + return true; + + } else if (keycode == getCloseKey()) { + onCancel(); + // TODO close event + + return true; + + /* + * Jumps to the next month + */ + } else if (shift && keycode == getForwardKey()) { + focusNextMonth(); + return true; + + /* + * Jumps to the previous month + */ + } else if (shift && keycode == getBackwardKey()) { + focusPreviousMonth(); + return true; + + /* + * Jumps to the next year + */ + } else if (shift && keycode == getPreviousKey()) { + focusNextYear(1); + return true; + + /* + * Jumps to the previous year + */ + } else if (shift && keycode == getNextKey()) { + focusPreviousYear(1); + return true; + + /* + * Resets the selection + */ + } else if (keycode == getResetKey() && !shift) { + // Restore showing value the selected value + focusedDate = new Date(value.getYear(), value.getMonth(), + value.getDate()); + displayedMonth = new Date(value.getYear(), value.getMonth(), 1); + renderCalendar(); + return true; + } + + return false; + } + + /** + * Handles the keyboard navigation + * + * @param keycode + * The key code that was pressed + * @param ctrl + * Was the ctrl key pressed + * @param shift + * Was the shift key pressed + * @return Return true if key press was handled by the component, else + * return false + */ + protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { + if (!isEnabled() || isReadonly()) { + return false; + } + + else if (resolution == VDateField.RESOLUTION_YEAR) { + return handleNavigationYearMode(keycode, ctrl, shift); + } + + else if (resolution == VDateField.RESOLUTION_MONTH) { + return handleNavigationMonthMode(keycode, ctrl, shift); + } + + else if (resolution == VDateField.RESOLUTION_DAY) { + return handleNavigationDayMode(keycode, ctrl, shift); + } + + else { + return handleNavigationDayMode(keycode, ctrl, shift); + } + + } + + /** + * Returns the reset key which will reset the calendar to the previous + * selection. By default this is backspace but it can be overriden to change + * the key to whatever you want. + * + * @return + */ + protected int getResetKey() { + return KeyCodes.KEY_BACKSPACE; + } + + /** + * Returns the select key which selects the value. By default this is the + * enter key but it can be changed to whatever you like by overriding this + * method. + * + * @return + */ + protected int getSelectKey() { + return KeyCodes.KEY_ENTER; + } + + /** + * Returns the key that closes the popup window if this is a VPopopCalendar. + * Else this does nothing. By default this is the Escape key but you can + * change the key to whatever you want by overriding this method. + * + * @return + */ + protected int getCloseKey() { + return KeyCodes.KEY_ESCAPE; + } + + /** + * The key that selects the next day in the calendar. By default this is the + * right arrow key but by overriding this method it can be changed to + * whatever you like. + * + * @return + */ + protected int getForwardKey() { + return KeyCodes.KEY_RIGHT; + } + + /** + * The key that selects the previous day in the calendar. By default this is + * the left arrow key but by overriding this method it can be changed to + * whatever you like. + * + * @return + */ + protected int getBackwardKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * The key that selects the next week in the calendar. By default this is + * the down arrow key but by overriding this method it can be changed to + * whatever you like. + * + * @return + */ + protected int getNextKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * The key that selects the previous week in the calendar. By default this + * is the up arrow key but by overriding this method it can be changed to + * whatever you like. + * + * @return + */ + protected int getPreviousKey() { + return KeyCodes.KEY_UP; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.MouseOutHandler#onMouseOut(com.google + * .gwt.event.dom.client.MouseOutEvent) + */ + @Override + public void onMouseOut(MouseOutEvent event) { + if (mouseTimer != null) { + mouseTimer.cancel(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.MouseDownHandler#onMouseDown(com.google + * .gwt.event.dom.client.MouseDownEvent) + */ + @Override + public void onMouseDown(MouseDownEvent event) { + // Allow user to click-n-hold for fast-forward or fast-rewind. + // Timer is first used for a 500ms delay after mousedown. After that has + // elapsed, another timer is triggered to go off every 150ms. Both + // timers are cancelled on mouseup or mouseout. + if (event.getSource() instanceof VEventButton) { + final VEventButton sender = (VEventButton) event.getSource(); + processClickEvent(sender); + mouseTimer = new Timer() { + @Override + public void run() { + mouseTimer = new Timer() { + @Override + public void run() { + processClickEvent(sender); + } + }; + mouseTimer.scheduleRepeating(150); + } + }; + mouseTimer.schedule(500); + } + + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.MouseUpHandler#onMouseUp(com.google.gwt + * .event.dom.client.MouseUpEvent) + */ + @Override + public void onMouseUp(MouseUpEvent event) { + if (mouseTimer != null) { + mouseTimer.cancel(); + } + } + + /** + * Sets the data of the Panel. + * + * @param currentDate + * The date to set + */ + public void setDate(Date currentDate) { + + // Check that we are not re-rendering an already active date + if (currentDate == value && currentDate != null) { + return; + } + + Date oldDisplayedMonth = displayedMonth; + value = currentDate; + + if (value == null) { + focusedDate = displayedMonth = null; + } else { + focusedDate = new Date(value.getYear(), value.getMonth(), + value.getDate()); + displayedMonth = new Date(value.getYear(), value.getMonth(), 1); + } + + // Re-render calendar if the displayed month is changed, + // or if a time selector is needed but does not exist. + if ((isTimeSelectorNeeded() && time == null) + || oldDisplayedMonth == null || value == null + || oldDisplayedMonth.getYear() != value.getYear() + || oldDisplayedMonth.getMonth() != value.getMonth()) { + renderCalendar(); + } else { + focusDay(focusedDate); + selectFocused(); + if (isTimeSelectorNeeded()) { + time.updateTimes(); + } + } + + if (!hasFocus) { + focusDay(null); + } + } + + /** + * TimeSelector is a widget consisting of list boxes that modifie the Date + * object that is given for. + * + */ + public class VTime extends FlowPanel implements ChangeHandler { + + private ListBox hours; + + private ListBox mins; + + private ListBox sec; + + private ListBox ampm; + + /** + * Constructor + */ + public VTime() { + super(); + setStyleName(VDateField.CLASSNAME + "-time"); + buildTime(); + } + + private ListBox createListBox() { + ListBox lb = new ListBox(); + lb.setStyleName(VNativeSelect.CLASSNAME); + lb.addChangeHandler(this); + lb.addBlurHandler(VCalendarPanel.this); + lb.addFocusHandler(VCalendarPanel.this); + return lb; + } + + /** + * Constructs the ListBoxes and updates their value + * + * @param redraw + * Should new instances of the listboxes be created + */ + private void buildTime() { + clear(); + + hours = createListBox(); + if (getDateTimeService().isTwelveHourClock()) { + hours.addItem("12"); + for (int i = 1; i < 12; i++) { + hours.addItem((i < 10) ? "0" + i : "" + i); + } + } else { + for (int i = 0; i < 24; i++) { + hours.addItem((i < 10) ? "0" + i : "" + i); + } + } + + hours.addChangeHandler(this); + if (getDateTimeService().isTwelveHourClock()) { + ampm = createListBox(); + final String[] ampmText = getDateTimeService().getAmPmStrings(); + ampm.addItem(ampmText[0]); + ampm.addItem(ampmText[1]); + ampm.addChangeHandler(this); + } + + if (getResolution() >= VDateField.RESOLUTION_MIN) { + mins = createListBox(); + for (int i = 0; i < 60; i++) { + mins.addItem((i < 10) ? "0" + i : "" + i); + } + mins.addChangeHandler(this); + } + if (getResolution() >= VDateField.RESOLUTION_SEC) { + sec = createListBox(); + for (int i = 0; i < 60; i++) { + sec.addItem((i < 10) ? "0" + i : "" + i); + } + sec.addChangeHandler(this); + } + + final String delimiter = getDateTimeService().getClockDelimeter(); + if (isReadonly()) { + int h = 0; + if (value != null) { + h = value.getHours(); + } + if (getDateTimeService().isTwelveHourClock()) { + h -= h < 12 ? 0 : 12; + } + add(new VLabel(h < 10 ? "0" + h : "" + h)); + } else { + add(hours); + } + + if (getResolution() >= VDateField.RESOLUTION_MIN) { + add(new VLabel(delimiter)); + if (isReadonly()) { + final int m = mins.getSelectedIndex(); + add(new VLabel(m < 10 ? "0" + m : "" + m)); + } else { + add(mins); + } + } + if (getResolution() >= VDateField.RESOLUTION_SEC) { + add(new VLabel(delimiter)); + if (isReadonly()) { + final int s = sec.getSelectedIndex(); + add(new VLabel(s < 10 ? "0" + s : "" + s)); + } else { + add(sec); + } + } + if (getResolution() == VDateField.RESOLUTION_HOUR) { + add(new VLabel(delimiter + "00")); // o'clock + } + if (getDateTimeService().isTwelveHourClock()) { + add(new VLabel(" ")); + if (isReadonly()) { + int i = 0; + if (value != null) { + i = (value.getHours() < 12) ? 0 : 1; + } + add(new VLabel(ampm.getItemText(i))); + } else { + add(ampm); + } + } + + if (isReadonly()) { + return; + } + + // Update times + updateTimes(); + + ListBox lastDropDown = getLastDropDown(); + lastDropDown.addKeyDownHandler(new KeyDownHandler() { + @Override + public void onKeyDown(KeyDownEvent event) { + boolean shiftKey = event.getNativeEvent().getShiftKey(); + if (shiftKey) { + return; + } else { + int nativeKeyCode = event.getNativeKeyCode(); + if (nativeKeyCode == KeyCodes.KEY_TAB) { + onTabOut(event); + } + } + } + }); + + } + + private ListBox getLastDropDown() { + int i = getWidgetCount() - 1; + while (i >= 0) { + Widget widget = getWidget(i); + if (widget instanceof ListBox) { + return (ListBox) widget; + } + i--; + } + return null; + } + + /** + * 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(); + ampm.setSelectedIndex(h < 12 ? 0 : 1); + h -= ampm.getSelectedIndex() * 12; + hours.setSelectedIndex(h); + } else { + hours.setSelectedIndex(value.getHours()); + } + if (getResolution() >= VDateField.RESOLUTION_MIN) { + mins.setSelectedIndex(value.getMinutes()); + } + if (getResolution() >= VDateField.RESOLUTION_SEC) { + sec.setSelectedIndex(value.getSeconds()); + } + if (getDateTimeService().isTwelveHourClock()) { + ampm.setSelectedIndex(value.getHours() < 12 ? 0 : 1); + } + + hours.setEnabled(isEnabled()); + if (mins != null) { + mins.setEnabled(isEnabled()); + } + if (sec != null) { + sec.setEnabled(isEnabled()); + } + if (ampm != null) { + ampm.setEnabled(isEnabled()); + } + + } + + private int getMilliseconds() { + return DateTimeService.getMilliseconds(value); + } + + private DateTimeService getDateTimeService() { + if (dateTimeService == null) { + dateTimeService = new DateTimeService(); + } + return dateTimeService; + } + + /* + * (non-Javadoc) VT + * + * @see + * com.google.gwt.event.dom.client.ChangeHandler#onChange(com.google.gwt + * .event.dom.client.ChangeEvent) + */ + @Override + public void onChange(ChangeEvent event) { + /* + * Value from dropdowns gets always set for the value. Like year and + * month when resolution is month or year. + */ + if (event.getSource() == hours) { + int h = hours.getSelectedIndex(); + if (getDateTimeService().isTwelveHourClock()) { + h = h + ampm.getSelectedIndex() * 12; + } + value.setHours(h); + if (timeChangeListener != null) { + timeChangeListener.changed(h, value.getMinutes(), + value.getSeconds(), + DateTimeService.getMilliseconds(value)); + } + event.preventDefault(); + event.stopPropagation(); + } else if (event.getSource() == mins) { + final int m = mins.getSelectedIndex(); + value.setMinutes(m); + if (timeChangeListener != null) { + timeChangeListener.changed(value.getHours(), m, + value.getSeconds(), + DateTimeService.getMilliseconds(value)); + } + event.preventDefault(); + event.stopPropagation(); + } else if (event.getSource() == sec) { + final int s = sec.getSelectedIndex(); + value.setSeconds(s); + if (timeChangeListener != null) { + timeChangeListener.changed(value.getHours(), + value.getMinutes(), s, + DateTimeService.getMilliseconds(value)); + } + event.preventDefault(); + event.stopPropagation(); + } else if (event.getSource() == ampm) { + final int h = hours.getSelectedIndex() + + (ampm.getSelectedIndex() * 12); + value.setHours(h); + if (timeChangeListener != null) { + timeChangeListener.changed(h, value.getMinutes(), + value.getSeconds(), + DateTimeService.getMilliseconds(value)); + } + event.preventDefault(); + event.stopPropagation(); + } + } + + } + + /** + * A widget representing a single day in the calendar panel. + */ + private class Day extends InlineHTML { + private static final String BASECLASS = VDateField.CLASSNAME + + "-calendarpanel-day"; + private final Date date; + + Day(Date date) { + super("" + date.getDate()); + setStyleName(BASECLASS); + this.date = date; + addClickHandler(dayClickHandler); + } + + public Date getDate() { + return date; + } + } + + public Date getDate() { + return value; + } + + /** + * If true should be returned if the panel will not be used after this + * event. + * + * @param event + * @return + */ + protected boolean onTabOut(DomEvent<?> event) { + if (focusOutListener != null) { + return focusOutListener.onFocusOut(event); + } + return false; + } + + /** + * A focus out listener is triggered when the panel loosed focus. This can + * happen either after a user clicks outside the panel or tabs out. + * + * @param listener + * The listener to trigger + */ + public void setFocusOutListener(FocusOutListener listener) { + focusOutListener = listener; + } + + /** + * The submit listener is called when the user selects a value from the + * calender either by clicking the day or selects it by keyboard. + * + * @param submitListener + * The listener to trigger + */ + public void setSubmitListener(SubmitListener submitListener) { + this.submitListener = submitListener; + } + + /** + * The given FocusChangeListener is notified when the focused date changes + * by user either clicking on a new date or by using the keyboard. + * + * @param listener + * The FocusChangeListener to be notified + */ + public void setFocusChangeListener(FocusChangeListener listener) { + focusChangeListener = listener; + } + + /** + * The time change listener is triggered when the user changes the time. + * + * @param listener + */ + public void setTimeChangeListener(TimeChangeListener listener) { + timeChangeListener = listener; + } + + /** + * Returns the submit listener that listens to selection made from the panel + * + * @return The listener or NULL if no listener has been set + */ + public SubmitListener getSubmitListener() { + return submitListener; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event + * .dom.client.BlurEvent) + */ + @Override + public void onBlur(final BlurEvent event) { + if (event.getSource() instanceof VCalendarPanel) { + hasFocus = false; + focusDay(null); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + @Override + public void onFocus(FocusEvent event) { + if (event.getSource() instanceof VCalendarPanel) { + hasFocus = true; + + // Focuses the current day if the calendar shows the days + if (focusedDay != null) { + focusDay(focusedDate); + } + } + } + + private static final String SUBPART_NEXT_MONTH = "nextmon"; + private static final String SUBPART_PREV_MONTH = "prevmon"; + + private static final String SUBPART_NEXT_YEAR = "nexty"; + private static final String SUBPART_PREV_YEAR = "prevy"; + 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"; + + @Override + public String getSubPartName(Element subElement) { + if (contains(nextMonth, subElement)) { + return SUBPART_NEXT_MONTH; + } else if (contains(prevMonth, subElement)) { + return SUBPART_PREV_MONTH; + } else if (contains(nextYear, subElement)) { + return SUBPART_NEXT_YEAR; + } else if (contains(prevYear, subElement)) { + return SUBPART_PREV_YEAR; + } else if (contains(days, subElement)) { + // Day, find out which dayOfMonth and use that as the identifier + Day day = Util.findWidget(subElement, Day.class); + if (day != null) { + Date date = day.getDate(); + int id = date.getDate(); + // Zero or negative ids map to days of the preceding month, + // past-the-end-of-month ids to days of the following month + if (date.getMonth() < displayedMonth.getMonth()) { + id -= DateTimeService.getNumberOfDaysInMonth(date); + } else if (date.getMonth() > displayedMonth.getMonth()) { + id += DateTimeService + .getNumberOfDaysInMonth(displayedMonth); + } + return SUBPART_DAY + id; + } + } else if (time != null) { + if (contains(time.hours, subElement)) { + return SUBPART_HOUR_SELECT; + } else if (contains(time.mins, subElement)) { + return SUBPART_MINUTE_SELECT; + } else if (contains(time.sec, subElement)) { + return SUBPART_SECS_SELECT; + } else if (contains(time.ampm, subElement)) { + return SUBPART_AMPM_SELECT; + + } + } else if (getCellFormatter().getElement(0, 2).isOrHasChild(subElement)) { + return SUBPART_MONTH_YEAR_HEADER; + } + + return null; + } + + /** + * Checks if subElement is inside the widget DOM hierarchy. + * + * @param w + * @param subElement + * @return true if {@code w} is a parent of subElement, false otherwise. + */ + private boolean contains(Widget w, Element subElement) { + if (w == null || w.getElement() == null) { + return false; + } + + return w.getElement().isOrHasChild(subElement); + } + + @Override + public Element getSubPartElement(String subPart) { + if (SUBPART_NEXT_MONTH.equals(subPart)) { + return nextMonth.getElement(); + } + if (SUBPART_PREV_MONTH.equals(subPart)) { + return prevMonth.getElement(); + } + if (SUBPART_NEXT_YEAR.equals(subPart)) { + return nextYear.getElement(); + } + if (SUBPART_PREV_YEAR.equals(subPart)) { + return prevYear.getElement(); + } + if (SUBPART_HOUR_SELECT.equals(subPart)) { + return time.hours.getElement(); + } + if (SUBPART_MINUTE_SELECT.equals(subPart)) { + return time.mins.getElement(); + } + if (SUBPART_SECS_SELECT.equals(subPart)) { + return time.sec.getElement(); + } + if (SUBPART_AMPM_SELECT.equals(subPart)) { + return time.ampm.getElement(); + } + if (subPart.startsWith(SUBPART_DAY)) { + // Zero or negative ids map to days in the preceding month, + // past-the-end-of-month ids to days in the following month + int dayOfMonth = Integer.parseInt(subPart.substring(SUBPART_DAY + .length())); + Date date = new Date(displayedMonth.getYear(), + displayedMonth.getMonth(), dayOfMonth); + Iterator<Widget> iter = days.iterator(); + while (iter.hasNext()) { + Widget w = iter.next(); + if (w instanceof Day) { + Day day = (Day) w; + if (day.getDate().equals(date)) { + return day.getElement(); + } + } + } + } + + if (SUBPART_MONTH_YEAR_HEADER.equals(subPart)) { + return (Element) getCellFormatter().getElement(0, 2).getChild(0); + } + return null; + } + + @Override + protected void onDetach() { + super.onDetach(); + if (mouseTimer != null) { + mouseTimer.cancel(); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateField.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateField.java new file mode 100644 index 0000000000..a339fb36d0 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateField.java @@ -0,0 +1,195 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.datefield; + +import java.util.Date; + +import com.google.gwt.user.client.ui.FlowPanel; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.DateTimeService; +import com.vaadin.terminal.gwt.client.ui.Field; + +public class VDateField extends FlowPanel implements Field { + + public static final String CLASSNAME = "v-datefield"; + + protected String paintableId; + + protected ApplicationConnection client; + + protected boolean immediate; + + public static final int RESOLUTION_YEAR = 1; + public static final int RESOLUTION_MONTH = 2; + public static final int RESOLUTION_DAY = 4; + public static final int RESOLUTION_HOUR = 8; + public static final int RESOLUTION_MIN = 16; + public static final int RESOLUTION_SEC = 32; + + static String resolutionToString(int res) { + if (res > RESOLUTION_DAY) { + return "full"; + } + if (res == RESOLUTION_DAY) { + return "day"; + } + if (res == RESOLUTION_MONTH) { + return "month"; + } + return "year"; + } + + protected int currentResolution = RESOLUTION_YEAR; + + protected String currentLocale; + + protected boolean readonly; + + protected boolean enabled; + + /** + * The date that is selected in the date field. Null if an invalid date is + * specified. + */ + private Date date = null; + + protected DateTimeService dts; + + protected boolean showISOWeekNumbers = false; + + public VDateField() { + setStyleName(CLASSNAME); + dts = new DateTimeService(); + } + + /* + * We need this redundant native function because Java's Date object doesn't + * have a setMilliseconds method. + */ + protected static native double getTime(int y, int m, int d, int h, int mi, + int s, int ms) + /*-{ + try { + var date = new Date(2000,1,1,1); // don't use current date here + if(y && y >= 0) date.setFullYear(y); + if(m && m >= 1) date.setMonth(m-1); + if(d && d >= 0) date.setDate(d); + if(h >= 0) date.setHours(h); + if(mi >= 0) date.setMinutes(mi); + if(s >= 0) date.setSeconds(s); + if(ms >= 0) date.setMilliseconds(ms); + return date.getTime(); + } catch (e) { + // TODO print some error message on the console + //console.log(e); + return (new Date()).getTime(); + } + }-*/; + + public int getMilliseconds() { + return DateTimeService.getMilliseconds(date); + } + + public void setMilliseconds(int ms) { + DateTimeService.setMilliseconds(date, ms); + } + + public int getCurrentResolution() { + return currentResolution; + } + + public void setCurrentResolution(int currentResolution) { + this.currentResolution = currentResolution; + } + + public String getCurrentLocale() { + return currentLocale; + } + + public void setCurrentLocale(String currentLocale) { + this.currentLocale = currentLocale; + } + + public Date getCurrentDate() { + return date; + } + + public void setCurrentDate(Date date) { + this.date = date; + } + + public boolean isImmediate() { + return immediate; + } + + public boolean isReadonly() { + return readonly; + } + + public boolean isEnabled() { + return enabled; + } + + public DateTimeService getDateTimeService() { + return dts; + } + + public String getId() { + return paintableId; + } + + public ApplicationConnection getClient() { + return client; + } + + /** + * Returns whether ISO 8601 week numbers should be shown in the date + * selector or not. ISO 8601 defines that a week always starts with a Monday + * so the week numbers are only shown if this is the case. + * + * @return true if week number should be shown, false otherwise + */ + public boolean isShowISOWeekNumbers() { + return showISOWeekNumbers; + } + + /** + * Returns a copy of the current date. Modifying the returned date will not + * modify the value of this VDateField. Use {@link #setDate(Date)} to change + * the current date. + * + * @return A copy of the current date + */ + protected Date getDate() { + Date current = getCurrentDate(); + if (current == null) { + return null; + } else { + return (Date) getCurrentDate().clone(); + } + } + + /** + * Sets the current date for this VDateField. + * + * @param date + * The new date to use + */ + protected void setDate(Date date) { + this.date = date; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateFieldCalendar.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateFieldCalendar.java new file mode 100644 index 0000000000..42640f0c0b --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateFieldCalendar.java @@ -0,0 +1,109 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.datefield; + +import java.util.Date; + +import com.google.gwt.event.dom.client.DomEvent; +import com.vaadin.terminal.gwt.client.DateTimeService; +import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.FocusOutListener; +import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.SubmitListener; + +/** + * A client side implementation for InlineDateField + */ +public class VDateFieldCalendar extends VDateField { + + protected final VCalendarPanel calendarPanel; + + public VDateFieldCalendar() { + super(); + calendarPanel = new VCalendarPanel(); + add(calendarPanel); + calendarPanel.setSubmitListener(new SubmitListener() { + @Override + public void onSubmit() { + updateValueFromPanel(); + } + + @Override + public void onCancel() { + // TODO Auto-generated method stub + + } + }); + calendarPanel.setFocusOutListener(new FocusOutListener() { + @Override + public boolean onFocusOut(DomEvent<?> event) { + updateValueFromPanel(); + return false; + } + }); + } + + /** + * TODO refactor: almost same method as in VPopupCalendar.updateValue + */ + @SuppressWarnings("deprecation") + protected void updateValueFromPanel() { + + // If field is invisible at the beginning, client can still be null when + // this function is called. + if (getClient() == null) { + return; + } + + Date date2 = calendarPanel.getDate(); + Date currentDate = getCurrentDate(); + if (currentDate == null || date2.getTime() != currentDate.getTime()) { + setCurrentDate((Date) date2.clone()); + getClient().updateVariable(getId(), "year", date2.getYear() + 1900, + false); + if (getCurrentResolution() > VDateField.RESOLUTION_YEAR) { + getClient().updateVariable(getId(), "month", + date2.getMonth() + 1, false); + if (getCurrentResolution() > RESOLUTION_MONTH) { + getClient().updateVariable(getId(), "day", date2.getDate(), + false); + if (getCurrentResolution() > RESOLUTION_DAY) { + getClient().updateVariable(getId(), "hour", + date2.getHours(), false); + if (getCurrentResolution() > RESOLUTION_HOUR) { + getClient().updateVariable(getId(), "min", + date2.getMinutes(), false); + if (getCurrentResolution() > RESOLUTION_MIN) { + getClient().updateVariable(getId(), "sec", + date2.getSeconds(), false); + if (getCurrentResolution() > RESOLUTION_SEC) { + getClient().updateVariable( + getId(), + "msec", + DateTimeService + .getMilliseconds(date2), + false); + } + } + } + } + } + } + if (isImmediate()) { + getClient().sendPendingVariableChanges(); + } + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VPopupCalendar.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VPopupCalendar.java new file mode 100644 index 0000000000..b7e0a2fb4e --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VPopupCalendar.java @@ -0,0 +1,385 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.datefield; + +import java.util.Date; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.DomEvent; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +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.Window; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.PopupPanel.PositionCallback; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.Field; +import com.vaadin.terminal.gwt.client.ui.SubPartAware; +import com.vaadin.terminal.gwt.client.ui.VOverlay; +import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.FocusOutListener; +import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.SubmitListener; + +/** + * Represents a date selection component with a text field and a popup date + * selector. + * + * <b>Note:</b> To change the keyboard assignments used in the popup dialog you + * should extend <code>com.vaadin.terminal.gwt.client.ui.VCalendarPanel</code> + * and then pass set it by calling the + * <code>setCalendarPanel(VCalendarPanel panel)</code> method. + * + */ +public class VPopupCalendar extends VTextualDate implements Field, + ClickHandler, CloseHandler<PopupPanel>, SubPartAware { + + protected static final String POPUP_PRIMARY_STYLE_NAME = VDateField.CLASSNAME + + "-popup"; + + protected final Button calendarToggle; + + protected VCalendarPanel calendar; + + protected final VOverlay popup; + private boolean open = false; + protected boolean parsable = true; + + public VPopupCalendar() { + super(); + + calendarToggle = new Button(); + calendarToggle.setStyleName(CLASSNAME + "-button"); + calendarToggle.setText(""); + calendarToggle.addClickHandler(this); + // -2 instead of -1 to avoid FocusWidget.onAttach to reset it + calendarToggle.getElement().setTabIndex(-2); + add(calendarToggle); + + calendar = GWT.create(VCalendarPanel.class); + calendar.setFocusOutListener(new FocusOutListener() { + @Override + public boolean onFocusOut(DomEvent<?> event) { + event.preventDefault(); + closeCalendarPanel(); + return true; + } + }); + + calendar.setSubmitListener(new SubmitListener() { + @Override + public void onSubmit() { + // Update internal value and send valuechange event if immediate + updateValue(calendar.getDate()); + + // Update text field (a must when not immediate). + buildDate(true); + + closeCalendarPanel(); + } + + @Override + public void onCancel() { + closeCalendarPanel(); + } + }); + + popup = new VOverlay(true, true, true); + popup.setStyleName(POPUP_PRIMARY_STYLE_NAME); + popup.setWidget(calendar); + popup.addCloseHandler(this); + + DOM.setElementProperty(calendar.getElement(), "id", + "PID_VAADIN_POPUPCAL"); + + sinkEvents(Event.ONKEYDOWN); + + } + + @SuppressWarnings("deprecation") + protected void updateValue(Date newDate) { + Date currentDate = getCurrentDate(); + if (currentDate == null || newDate.getTime() != currentDate.getTime()) { + setCurrentDate((Date) newDate.clone()); + getClient().updateVariable(getId(), "year", + newDate.getYear() + 1900, false); + if (getCurrentResolution() > VDateField.RESOLUTION_YEAR) { + getClient().updateVariable(getId(), "month", + newDate.getMonth() + 1, false); + if (getCurrentResolution() > RESOLUTION_MONTH) { + getClient().updateVariable(getId(), "day", + newDate.getDate(), false); + if (getCurrentResolution() > RESOLUTION_DAY) { + getClient().updateVariable(getId(), "hour", + newDate.getHours(), false); + if (getCurrentResolution() > RESOLUTION_HOUR) { + getClient().updateVariable(getId(), "min", + newDate.getMinutes(), false); + if (getCurrentResolution() > RESOLUTION_MIN) { + getClient().updateVariable(getId(), "sec", + newDate.getSeconds(), false); + } + } + } + } + } + if (isImmediate()) { + getClient().sendPendingVariableChanges(); + } + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.UIObject#setStyleName(java.lang.String) + */ + @Override + public void setStyleName(String style) { + // make sure the style is there before size calculation + super.setStyleName(style + " " + CLASSNAME + "-popupcalendar"); + } + + /** + * Opens the calendar panel popup + */ + public void openCalendarPanel() { + + if (!open && !readonly) { + open = true; + + if (getCurrentDate() != null) { + calendar.setDate((Date) getCurrentDate().clone()); + } else { + calendar.setDate(new Date()); + } + + // clear previous values + popup.setWidth(""); + popup.setHeight(""); + popup.setPopupPositionAndShow(new PositionCallback() { + @Override + public void setPosition(int offsetWidth, int offsetHeight) { + final int w = offsetWidth; + final int h = offsetHeight; + final int browserWindowWidth = Window.getClientWidth() + + Window.getScrollLeft(); + final int browserWindowHeight = Window.getClientHeight() + + Window.getScrollTop(); + int t = calendarToggle.getAbsoluteTop(); + int l = calendarToggle.getAbsoluteLeft(); + + // Add a little extra space to the right to avoid + // problems with IE7 scrollbars and to make it look + // nicer. + int extraSpace = 30; + + boolean overflowRight = false; + if (l + +w + extraSpace > browserWindowWidth) { + overflowRight = true; + // Part of the popup is outside the browser window + // (to the right) + l = browserWindowWidth - w - extraSpace; + } + + if (t + h + calendarToggle.getOffsetHeight() + 30 > browserWindowHeight) { + // Part of the popup is outside the browser window + // (below) + t = browserWindowHeight - h + - calendarToggle.getOffsetHeight() - 30; + if (!overflowRight) { + // Show to the right of the popup button unless we + // are in the lower right corner of the screen + l += calendarToggle.getOffsetWidth(); + } + } + + // fix size + popup.setWidth(w + "px"); + popup.setHeight(h + "px"); + + popup.setPopupPosition(l, + t + calendarToggle.getOffsetHeight() + 2); + + /* + * We have to wait a while before focusing since the popup + * needs to be opened before we can focus + */ + Timer focusTimer = new Timer() { + @Override + public void run() { + setFocus(true); + } + }; + + focusTimer.schedule(100); + } + }); + } else { + VConsole.error("Cannot reopen popup, it is already open!"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event + * .dom.client.ClickEvent) + */ + @Override + public void onClick(ClickEvent event) { + if (event.getSource() == calendarToggle && isEnabled()) { + openCalendarPanel(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google.gwt + * .event.logical.shared.CloseEvent) + */ + @Override + public void onClose(CloseEvent<PopupPanel> event) { + if (event.getSource() == popup) { + buildDate(); + if (!BrowserInfo.get().isTouchDevice()) { + /* + * Move focus to textbox, unless on touch device (avoids opening + * virtual keyboard). + */ + focus(); + } + + // TODO resolve what the "Sigh." is all about and document it here + // Sigh. + Timer t = new Timer() { + @Override + public void run() { + open = false; + } + }; + t.schedule(100); + } + } + + /** + * Sets focus to Calendar panel. + * + * @param focus + */ + public void setFocus(boolean focus) { + calendar.setFocus(focus); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ui.VTextualDate#buildDate() + */ + @Override + protected void buildDate() { + // Save previous value + String previousValue = getText(); + super.buildDate(); + + // Restore previous value if the input could not be parsed + if (!parsable) { + setText(previousValue); + } + } + + /** + * Update the text field contents from the date. See {@link #buildDate()}. + * + * @param forceValid + * true to force the text field to be updated, false to only + * update if the parsable flag is true. + */ + protected void buildDate(boolean forceValid) { + if (forceValid) { + parsable = true; + } + buildDate(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.VDateField#onBrowserEvent(com.google + * .gwt.user.client.Event) + */ + @Override + public void onBrowserEvent(com.google.gwt.user.client.Event event) { + super.onBrowserEvent(event); + if (DOM.eventGetType(event) == Event.ONKEYDOWN + && event.getKeyCode() == getOpenCalenderPanelKey()) { + openCalendarPanel(); + event.preventDefault(); + } + } + + /** + * Get the key code that opens the calendar panel. By default it is the down + * key but you can override this to be whatever you like + * + * @return + */ + protected int getOpenCalenderPanelKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Closes the open popup panel + */ + public void closeCalendarPanel() { + if (open) { + popup.hide(true); + } + } + + private final String CALENDAR_TOGGLE_ID = "popupButton"; + + @Override + public Element getSubPartElement(String subPart) { + if (subPart.equals(CALENDAR_TOGGLE_ID)) { + return calendarToggle.getElement(); + } + + return super.getSubPartElement(subPart); + } + + @Override + public String getSubPartName(Element subElement) { + if (calendarToggle.getElement().isOrHasChild(subElement)) { + return CALENDAR_TOGGLE_ID; + } + + return super.getSubPartName(subElement); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VTextualDate.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VTextualDate.java new file mode 100644 index 0000000000..4e82058f68 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VTextualDate.java @@ -0,0 +1,352 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.datefield; + +import java.util.Date; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.TextBox; +import com.vaadin.shared.EventId; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.LocaleNotLoadedException; +import com.vaadin.terminal.gwt.client.LocaleService; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.Field; +import com.vaadin.terminal.gwt.client.ui.SubPartAware; +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; + +public class VTextualDate extends VDateField implements Field, ChangeHandler, + Focusable, SubPartAware { + + private static final String PARSE_ERROR_CLASSNAME = CLASSNAME + + "-parseerror"; + + protected final TextBox text; + + protected String formatStr; + + protected boolean lenient; + + private static final String CLASSNAME_PROMPT = "prompt"; + protected static final String ATTR_INPUTPROMPT = "prompt"; + protected String inputPrompt = ""; + private boolean prompting = false; + + public VTextualDate() { + + super(); + text = new TextBox(); + // use normal textfield styles as a basis + text.setStyleName(VTextField.CLASSNAME); + // add datefield spesific style name also + text.addStyleName(CLASSNAME + "-textfield"); + text.addChangeHandler(this); + text.addFocusHandler(new FocusHandler() { + @Override + public void onFocus(FocusEvent event) { + text.addStyleName(VTextField.CLASSNAME + "-" + + VTextField.CLASSNAME_FOCUS); + if (prompting) { + text.setText(""); + setPrompting(false); + } + if (getClient() != null + && getClient().hasEventListeners(VTextualDate.this, + EventId.FOCUS)) { + getClient() + .updateVariable(getId(), EventId.FOCUS, "", true); + } + } + }); + text.addBlurHandler(new BlurHandler() { + @Override + public void onBlur(BlurEvent event) { + text.removeStyleName(VTextField.CLASSNAME + "-" + + VTextField.CLASSNAME_FOCUS); + String value = getText(); + setPrompting(inputPrompt != null + && (value == null || "".equals(value))); + if (prompting) { + text.setText(readonly ? "" : inputPrompt); + } + if (getClient() != null + && getClient().hasEventListeners(VTextualDate.this, + EventId.BLUR)) { + getClient().updateVariable(getId(), EventId.BLUR, "", true); + } + } + }); + add(text); + } + + protected String getFormatString() { + if (formatStr == null) { + if (currentResolution == RESOLUTION_YEAR) { + formatStr = "yyyy"; // force full year + } else { + + try { + String frmString = LocaleService + .getDateFormat(currentLocale); + frmString = cleanFormat(frmString); + // String delim = LocaleService + // .getClockDelimiter(currentLocale); + + if (currentResolution >= RESOLUTION_HOUR) { + if (dts.isTwelveHourClock()) { + frmString += " hh"; + } else { + frmString += " HH"; + } + if (currentResolution >= RESOLUTION_MIN) { + frmString += ":mm"; + if (currentResolution >= RESOLUTION_SEC) { + frmString += ":ss"; + } + } + if (dts.isTwelveHourClock()) { + frmString += " aaa"; + } + + } + + formatStr = frmString; + } catch (LocaleNotLoadedException e) { + // TODO should die instead? Can the component survive + // without format string? + VConsole.error(e); + } + } + } + return formatStr; + } + + /** + * Updates the text field according to the current date (provided by + * {@link #getDate()}). Takes care of updating text, enabling and disabling + * the field, setting/removing readonly status and updating readonly styles. + * + * TODO: Split part of this into a method that only updates the text as this + * is what usually is needed except for updateFromUIDL. + */ + protected void buildDate() { + removeStyleName(PARSE_ERROR_CLASSNAME); + // Create the initial text for the textfield + String dateText; + Date currentDate = getDate(); + if (currentDate != null) { + dateText = getDateTimeService().formatDate(currentDate, + getFormatString()); + } else { + dateText = ""; + } + + setText(dateText); + text.setEnabled(enabled); + text.setReadOnly(readonly); + + if (readonly) { + text.addStyleName("v-readonly"); + } else { + text.removeStyleName("v-readonly"); + } + + } + + protected void setPrompting(boolean prompting) { + this.prompting = prompting; + if (prompting) { + addStyleDependentName(CLASSNAME_PROMPT); + } else { + removeStyleDependentName(CLASSNAME_PROMPT); + } + } + + @Override + @SuppressWarnings("deprecation") + public void onChange(ChangeEvent event) { + if (!text.getText().equals("")) { + try { + String enteredDate = text.getText(); + + setDate(getDateTimeService().parseDate(enteredDate, + getFormatString(), lenient)); + + if (lenient) { + // If date value was leniently parsed, normalize text + // presentation. + // FIXME: Add a description/example here of when this is + // needed + text.setValue( + getDateTimeService().formatDate(getDate(), + getFormatString()), false); + } + + // remove possibly added invalid value indication + removeStyleName(PARSE_ERROR_CLASSNAME); + } catch (final Exception e) { + VConsole.log(e); + + addStyleName(PARSE_ERROR_CLASSNAME); + // this is a hack that may eventually be removed + getClient().updateVariable(getId(), "lastInvalidDateString", + text.getText(), false); + setDate(null); + } + } else { + setDate(null); + // remove possibly added invalid value indication + removeStyleName(PARSE_ERROR_CLASSNAME); + } + // always send the date string + getClient() + .updateVariable(getId(), "dateString", text.getText(), false); + + // Update variables + // (only the smallest defining resolution needs to be + // immediate) + Date currentDate = getDate(); + getClient().updateVariable(getId(), "year", + currentDate != null ? currentDate.getYear() + 1900 : -1, + currentResolution == VDateField.RESOLUTION_YEAR && immediate); + if (currentResolution >= VDateField.RESOLUTION_MONTH) { + getClient().updateVariable( + getId(), + "month", + currentDate != null ? currentDate.getMonth() + 1 : -1, + currentResolution == VDateField.RESOLUTION_MONTH + && immediate); + } + if (currentResolution >= VDateField.RESOLUTION_DAY) { + getClient() + .updateVariable( + getId(), + "day", + currentDate != null ? currentDate.getDate() : -1, + currentResolution == VDateField.RESOLUTION_DAY + && immediate); + } + if (currentResolution >= VDateField.RESOLUTION_HOUR) { + getClient().updateVariable( + getId(), + "hour", + currentDate != null ? currentDate.getHours() : -1, + currentResolution == VDateField.RESOLUTION_HOUR + && immediate); + } + if (currentResolution >= VDateField.RESOLUTION_MIN) { + getClient() + .updateVariable( + getId(), + "min", + currentDate != null ? currentDate.getMinutes() : -1, + currentResolution == VDateField.RESOLUTION_MIN + && immediate); + } + if (currentResolution >= VDateField.RESOLUTION_SEC) { + getClient() + .updateVariable( + getId(), + "sec", + currentDate != null ? currentDate.getSeconds() : -1, + currentResolution == VDateField.RESOLUTION_SEC + && immediate); + } + + } + + private String cleanFormat(String format) { + // Remove unnecessary d & M if resolution is too low + if (currentResolution < VDateField.RESOLUTION_DAY) { + format = format.replaceAll("d", ""); + } + if (currentResolution < VDateField.RESOLUTION_MONTH) { + format = format.replaceAll("M", ""); + } + + // Remove unsupported patterns + // TODO support for 'G', era designator (used at least in Japan) + format = format.replaceAll("[GzZwWkK]", ""); + + // Remove extra delimiters ('/' and '.') + while (format.startsWith("/") || format.startsWith(".") + || format.startsWith("-")) { + format = format.substring(1); + } + while (format.endsWith("/") || format.endsWith(".") + || format.endsWith("-")) { + format = format.substring(0, format.length() - 1); + } + + // Remove duplicate delimiters + format = format.replaceAll("//", "/"); + format = format.replaceAll("\\.\\.", "."); + format = format.replaceAll("--", "-"); + + return format.trim(); + } + + @Override + public void focus() { + text.setFocus(true); + } + + protected String getText() { + if (prompting) { + return ""; + } + return text.getText(); + } + + protected void setText(String text) { + if (inputPrompt != null && (text == null || "".equals(text))) { + text = readonly ? "" : inputPrompt; + setPrompting(true); + } else { + setPrompting(false); + } + + this.text.setText(text); + } + + private final String TEXTFIELD_ID = "field"; + + @Override + public Element getSubPartElement(String subPart) { + if (subPart.equals(TEXTFIELD_ID)) { + return text.getElement(); + } + + return null; + } + + @Override + public String getSubPartName(Element subElement) { + if (text.getElement().isOrHasChild(subElement)) { + return TEXTFIELD_ID; + } + + return null; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/DDUtil.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/DDUtil.java new file mode 100644 index 0000000000..b6012eded1 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/DDUtil.java @@ -0,0 +1,112 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Window; +import com.vaadin.shared.ui.dd.HorizontalDropLocation; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.terminal.gwt.client.Util; + +public class DDUtil { + + /** + * @deprecated use the version with the actual event instead of detected + * clientY value + * + * @param element + * @param clientY + * @param topBottomRatio + * @return + */ + @Deprecated + public static VerticalDropLocation getVerticalDropLocation(Element element, + int clientY, double topBottomRatio) { + int offsetHeight = element.getOffsetHeight(); + return getVerticalDropLocation(element, offsetHeight, clientY, + topBottomRatio); + } + + public static VerticalDropLocation getVerticalDropLocation(Element element, + NativeEvent event, double topBottomRatio) { + int offsetHeight = element.getOffsetHeight(); + return getVerticalDropLocation(element, offsetHeight, event, + topBottomRatio); + } + + public static VerticalDropLocation getVerticalDropLocation(Element element, + int offsetHeight, NativeEvent event, double topBottomRatio) { + int clientY = Util.getTouchOrMouseClientY(event); + return getVerticalDropLocation(element, offsetHeight, clientY, + topBottomRatio); + } + + public static VerticalDropLocation getVerticalDropLocation(Element element, + int offsetHeight, int clientY, double topBottomRatio) { + + // Event coordinates are relative to the viewport, element absolute + // position is relative to the document. Make element position relative + // to viewport by adjusting for viewport scrolling. See #6021 + int elementTop = element.getAbsoluteTop() - Window.getScrollTop(); + int fromTop = clientY - elementTop; + + float percentageFromTop = (fromTop / (float) offsetHeight); + if (percentageFromTop < topBottomRatio) { + return VerticalDropLocation.TOP; + } else if (percentageFromTop > 1 - topBottomRatio) { + return VerticalDropLocation.BOTTOM; + } else { + return VerticalDropLocation.MIDDLE; + } + } + + public static HorizontalDropLocation getHorizontalDropLocation( + Element element, NativeEvent event, double leftRightRatio) { + int touchOrMouseClientX = Util.getTouchOrMouseClientX(event); + return getHorizontalDropLocation(element, touchOrMouseClientX, + leftRightRatio); + } + + /** + * @deprecated use the version with the actual event + * @param element + * @param clientX + * @param leftRightRatio + * @return + */ + @Deprecated + public static HorizontalDropLocation getHorizontalDropLocation( + Element element, int clientX, double leftRightRatio) { + + // Event coordinates are relative to the viewport, element absolute + // position is relative to the document. Make element position relative + // to viewport by adjusting for viewport scrolling. See #6021 + int elementLeft = element.getAbsoluteLeft() - Window.getScrollLeft(); + int offsetWidth = element.getOffsetWidth(); + int fromLeft = clientX - elementLeft; + + float percentageFromLeft = (fromLeft / (float) offsetWidth); + if (percentageFromLeft < leftRightRatio) { + return HorizontalDropLocation.LEFT; + } else if (percentageFromLeft > 1 - leftRightRatio) { + return HorizontalDropLocation.RIGHT; + } else { + return HorizontalDropLocation.CENTER; + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAbstractDropHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAbstractDropHandler.java new file mode 100644 index 0000000000..bf0ab24c32 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAbstractDropHandler.java @@ -0,0 +1,154 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import java.util.Iterator; + +import com.google.gwt.user.client.Command; +import com.vaadin.event.Transferable; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.UIDL; + +public abstract class VAbstractDropHandler implements VDropHandler { + + private UIDL criterioUIDL; + private VAcceptCriterion acceptCriteria = new VAcceptAll(); + + /** + * Implementor/user of {@link VAbstractDropHandler} must pass the UIDL + * painted by {@link AcceptCriterion} to this method. Practically the + * details about {@link AcceptCriterion} are saved. + * + * @param uidl + */ + public void updateAcceptRules(UIDL uidl) { + criterioUIDL = uidl; + /* + * supports updating the accept rule root directly or so that it is + * contained in given uidl node + */ + if (!uidl.getTag().equals("-ac")) { + Iterator<Object> childIterator = uidl.getChildIterator(); + while (!uidl.getTag().equals("-ac") && childIterator.hasNext()) { + uidl = (UIDL) childIterator.next(); + } + } + acceptCriteria = VAcceptCriteria.get(uidl.getStringAttribute("name")); + if (acceptCriteria == null) { + throw new IllegalArgumentException( + "No accept criteria found with given name " + + uidl.getStringAttribute("name")); + } + } + + /** + * Default implementation does nothing. + */ + @Override + public void dragOver(VDragEvent drag) { + + } + + /** + * Default implementation does nothing. Implementors should clean possible + * emphasis or drag icons here. + */ + @Override + public void dragLeave(VDragEvent drag) { + + } + + /** + * The default implementation in {@link VAbstractDropHandler} checks if the + * Transferable is accepted. + * <p> + * If transferable is accepted (either via server visit or client side + * rules) the default implementation calls abstract + * {@link #dragAccepted(VDragEvent)} method. + * <p> + * If drop handler has distinct places where some parts may accept the + * {@link Transferable} and others don't, one should use similar validation + * logic in dragOver method and replace this method with empty + * implementation. + * + */ + @Override + public void dragEnter(final VDragEvent drag) { + validate(new VAcceptCallback() { + @Override + public void accepted(VDragEvent event) { + dragAccepted(drag); + } + }, drag); + } + + /** + * This method is called when a valid drop location was found with + * {@link AcceptCriterion} either via client or server side check. + * <p> + * Implementations can set some hints for users here to highlight that the + * drag is on a valid drop location. + * + * @param drag + */ + abstract protected void dragAccepted(VDragEvent drag); + + protected void validate(final VAcceptCallback cb, final VDragEvent event) { + Command checkCriteria = new Command() { + @Override + public void execute() { + acceptCriteria.accept(event, criterioUIDL, cb); + } + }; + + VDragAndDropManager.get().executeWhenReady(checkCriteria); + } + + boolean validated = false; + + /** + * The default implemmentation visits server if {@link AcceptCriterion} + * can't be verified on client or if {@link AcceptCriterion} are met on + * client. + */ + @Override + public boolean drop(VDragEvent drag) { + if (acceptCriteria.needsServerSideCheck(drag, criterioUIDL)) { + return true; + } else { + validated = false; + acceptCriteria.accept(drag, criterioUIDL, new VAcceptCallback() { + @Override + public void accepted(VDragEvent event) { + validated = true; + } + }); + return validated; + } + + } + + /** + * Returns the Paintable who owns this {@link VAbstractDropHandler}. Server + * side counterpart of the Paintable is expected to implement + * {@link DropTarget} interface. + */ + @Override + public abstract ComponentConnector getConnector(); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptAll.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptAll.java new file mode 100644 index 0000000000..661e47c506 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptAll.java @@ -0,0 +1,32 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.event.dd.acceptcriteria.AcceptAll; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.UIDL; + +@AcceptCriterion(AcceptAll.class) +final public class VAcceptAll extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + return true; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCallback.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCallback.java new file mode 100644 index 0000000000..a8b3518406 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCallback.java @@ -0,0 +1,29 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +public interface VAcceptCallback { + + /** + * This method is called by {@link VDragAndDropManager} if the + * {@link VDragEvent} is still active. Developer can update for example drag + * icon or empahsis the target if the target accepts the transferable. If + * the drag and drop operation ends or the {@link VAbstractDropHandler} has + * changed before response arrives, the method is never called. + */ + public void accepted(VDragEvent event); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriteria.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriteria.java new file mode 100644 index 0000000000..05330c2187 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriteria.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.google.gwt.core.client.GWT; + +/** + * A class via all AcceptCriteria instances are fetched by an identifier. + */ +public class VAcceptCriteria { + private static VAcceptCriterionFactory impl; + + static { + impl = GWT.create(VAcceptCriterionFactory.class); + } + + public static VAcceptCriterion get(String name) { + return impl.get(name); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterion.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterion.java new file mode 100644 index 0000000000..6d3bda9676 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterion.java @@ -0,0 +1,57 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.terminal.gwt.client.UIDL; + +public abstract class VAcceptCriterion { + + /** + * Checks if current drag event has valid drop target and target accepts the + * transferable. If drop target is valid, callback is used. + * + * @param drag + * @param configuration + * @param callback + */ + public void accept(final VDragEvent drag, UIDL configuration, + final VAcceptCallback callback) { + if (needsServerSideCheck(drag, configuration)) { + VDragEventServerCallback acceptCallback = new VDragEventServerCallback() { + @Override + public void handleResponse(boolean accepted, UIDL response) { + if (accepted) { + callback.accepted(drag); + } + } + }; + VDragAndDropManager.get().visitServer(acceptCallback); + } else { + boolean validates = accept(drag, configuration); + if (validates) { + callback.accepted(drag); + } + } + + } + + protected abstract boolean accept(VDragEvent drag, UIDL configuration); + + public boolean needsServerSideCheck(VDragEvent drag, UIDL criterioUIDL) { + return false; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterionFactory.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterionFactory.java new file mode 100644 index 0000000000..a0ff9ecfdf --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterionFactory.java @@ -0,0 +1,25 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +/** + * Generated by + * {@link com.vaadin.terminal.gwt.widgetsetutils.AcceptCriteriaFactoryGenerator} + */ +public abstract class VAcceptCriterionFactory { + + public abstract VAcceptCriterion get(String name); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAnd.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAnd.java new file mode 100644 index 0000000000..8eb72712ef --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAnd.java @@ -0,0 +1,54 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.event.dd.acceptcriteria.And; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.UIDL; + +@AcceptCriterion(And.class) +final public class VAnd extends VAcceptCriterion implements VAcceptCallback { + private boolean b1; + + static VAcceptCriterion getCriteria(VDragEvent drag, UIDL configuration, + int i) { + UIDL childUIDL = configuration.getChildUIDL(i); + return VAcceptCriteria.get(childUIDL.getStringAttribute("name")); + } + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + int childCount = configuration.getChildCount(); + for (int i = 0; i < childCount; i++) { + VAcceptCriterion crit = getCriteria(drag, configuration, i); + b1 = false; + crit.accept(drag, configuration.getChildUIDL(i), this); + if (!b1) { + return false; + } + } + return true; + } + + @Override + public void accepted(VDragEvent event) { + b1 = true; + } + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VContainsDataFlavor.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VContainsDataFlavor.java new file mode 100644 index 0000000000..f06aa7eb15 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VContainsDataFlavor.java @@ -0,0 +1,33 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.event.dd.acceptcriteria.ContainsDataFlavor; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.UIDL; + +@AcceptCriterion(ContainsDataFlavor.class) +final public class VContainsDataFlavor extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + String name = configuration.getStringAttribute("p"); + return drag.getTransferable().getDataFlavors().contains(name); + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragAndDropManager.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragAndDropManager.java new file mode 100644 index 0000000000..82592d1846 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragAndDropManager.java @@ -0,0 +1,752 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.RepeatingCommand; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.EventTarget; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.dd.DragEventType; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ValueMap; + +/** + * Helper class to manage the state of drag and drop event on Vaadin client + * side. Can be used to implement most of the drag and drop operation + * automatically via cross-browser event preview method or just as a helper when + * implementing own low level drag and drop operation (like with HTML5 api). + * <p> + * Singleton. Only one drag and drop operation can be active anyways. Use + * {@link #get()} to get instance. + * + * TODO cancel drag and drop if more than one touches !? + */ +public class VDragAndDropManager { + + public static final String ACTIVE_DRAG_SOURCE_STYLENAME = "v-active-drag-source"; + + private final class DefaultDragAndDropEventHandler implements + NativePreviewHandler { + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + NativeEvent nativeEvent = event.getNativeEvent(); + + int typeInt = event.getTypeInt(); + if (typeInt == Event.ONKEYDOWN) { + int keyCode = event.getNativeEvent().getKeyCode(); + if (keyCode == KeyCodes.KEY_ESCAPE) { + // end drag if ESC is hit + interruptDrag(); + event.cancel(); + event.getNativeEvent().preventDefault(); + } + // no use for handling for any key down event + return; + } + + currentDrag.setCurrentGwtEvent(nativeEvent); + updateDragImagePosition(); + + Element targetElement = Element.as(nativeEvent.getEventTarget()); + if (Util.isTouchEvent(nativeEvent) + || (dragElement != null && dragElement + .isOrHasChild(targetElement))) { + // to detect the "real" target, hide dragelement temporary and + // use elementFromPoint + String display = dragElement.getStyle().getDisplay(); + dragElement.getStyle().setDisplay(Display.NONE); + try { + int x = Util.getTouchOrMouseClientX(nativeEvent); + int y = Util.getTouchOrMouseClientY(nativeEvent); + // Util.browserDebugger(); + targetElement = Util.getElementFromPoint(x, y); + if (targetElement == null) { + // ApplicationConnection.getConsole().log( + // "Event on dragImage, ignored"); + event.cancel(); + nativeEvent.stopPropagation(); + return; + + } else { + // ApplicationConnection.getConsole().log( + // "Event on dragImage, target changed"); + // special handling for events over dragImage + // pretty much all events are mousemove althout below + // kind of happens mouseover + switch (typeInt) { + case Event.ONMOUSEOVER: + case Event.ONMOUSEOUT: + // ApplicationConnection + // .getConsole() + // .log( + // "IGNORING proxy image event, fired because of hack or not significant"); + return; + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + VDropHandler findDragTarget = findDragTarget(targetElement); + if (findDragTarget != currentDropHandler) { + // dragleave on old + if (currentDropHandler != null) { + currentDropHandler.dragLeave(currentDrag); + currentDrag.getDropDetails().clear(); + serverCallback = null; + } + // dragenter on new + currentDropHandler = findDragTarget; + if (findDragTarget != null) { + // ApplicationConnection.getConsole().log( + // "DropHandler now" + // + currentDropHandler + // .getPaintable()); + } + + if (currentDropHandler != null) { + currentDrag + .setElementOver((com.google.gwt.user.client.Element) targetElement); + currentDropHandler.dragEnter(currentDrag); + } + } else if (findDragTarget != null) { + currentDrag + .setElementOver((com.google.gwt.user.client.Element) targetElement); + currentDropHandler.dragOver(currentDrag); + } + // prevent text selection on IE + nativeEvent.preventDefault(); + return; + default: + // just update element over and let the actual + // handling code do the thing + // ApplicationConnection.getConsole().log( + // "Target just modified on " + // + event.getType()); + currentDrag + .setElementOver((com.google.gwt.user.client.Element) targetElement); + break; + } + + } + } catch (RuntimeException e) { + // ApplicationConnection.getConsole().log( + // "ERROR during elementFromPoint hack."); + throw e; + } finally { + dragElement.getStyle().setProperty("display", display); + } + } + + switch (typeInt) { + case Event.ONMOUSEOVER: + VDropHandler target = findDragTarget(targetElement); + + if (target != null && target != currentDropHandler) { + if (currentDropHandler != null) { + currentDropHandler.dragLeave(currentDrag); + currentDrag.getDropDetails().clear(); + } + + currentDropHandler = target; + // ApplicationConnection.getConsole().log( + // "DropHandler now" + // + currentDropHandler.getPaintable()); + currentDrag + .setElementOver((com.google.gwt.user.client.Element) targetElement); + target.dragEnter(currentDrag); + } else if (target == null && currentDropHandler != null) { + // ApplicationConnection.getConsole().log("Invalid state!?"); + currentDropHandler.dragLeave(currentDrag); + currentDrag.getDropDetails().clear(); + currentDropHandler = null; + } + break; + case Event.ONMOUSEOUT: + Element relatedTarget = Element.as(nativeEvent + .getRelatedEventTarget()); + VDropHandler newDragHanler = findDragTarget(relatedTarget); + if (dragElement != null + && dragElement.isOrHasChild(relatedTarget)) { + // ApplicationConnection.getConsole().log( + // "Mouse out of dragImage, ignored"); + return; + } + + if (currentDropHandler != null + && currentDropHandler != newDragHanler) { + currentDropHandler.dragLeave(currentDrag); + currentDrag.getDropDetails().clear(); + currentDropHandler = null; + serverCallback = null; + } + break; + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + if (currentDropHandler != null) { + currentDrag + .setElementOver((com.google.gwt.user.client.Element) targetElement); + currentDropHandler.dragOver(currentDrag); + } + nativeEvent.preventDefault(); + + break; + + case Event.ONTOUCHEND: + /* Avoid simulated event on drag end */ + event.getNativeEvent().preventDefault(); + case Event.ONMOUSEUP: + endDrag(); + break; + + default: + break; + } + + } + + } + + private static VDragAndDropManager instance; + private HandlerRegistration handlerRegistration; + private VDragEvent currentDrag; + + /** + * If dragging is currently on a drophandler, this field has reference to it + */ + private VDropHandler currentDropHandler; + + public VDropHandler getCurrentDropHandler() { + return currentDropHandler; + } + + /** + * If drag and drop operation is not handled by {@link VDragAndDropManager}s + * internal handler, this can be used to update current {@link VDropHandler} + * . + * + * @param currentDropHandler + */ + public void setCurrentDropHandler(VDropHandler currentDropHandler) { + this.currentDropHandler = currentDropHandler; + } + + private VDragEventServerCallback serverCallback; + + private HandlerRegistration deferredStartRegistration; + + public static VDragAndDropManager get() { + if (instance == null) { + instance = GWT.create(VDragAndDropManager.class); + } + return instance; + } + + /* Singleton */ + private VDragAndDropManager() { + } + + private NativePreviewHandler defaultDragAndDropEventHandler = new DefaultDragAndDropEventHandler(); + + /** + * Flag to indicate if drag operation has really started or not. Null check + * of currentDrag field is not enough as a lazy start may be pending. + */ + private boolean isStarted; + + /** + * This method is used to start Vaadin client side drag and drop operation. + * Operation may be started by virtually any Widget. + * <p> + * Cancels possible existing drag. TODO figure out if this is always a bug + * if one is active. Maybe a good and cheap lifesaver thought. + * <p> + * If possible, method automatically detects current {@link VDropHandler} + * and fires {@link VDropHandler#dragEnter(VDragEvent)} event on it. + * <p> + * May also be used to control the drag and drop operation. If this option + * is used, {@link VDropHandler} is searched on mouse events and appropriate + * methods on it called automatically. + * + * @param transferable + * @param nativeEvent + * @param handleDragEvents + * if true, {@link VDragAndDropManager} handles the drag and drop + * operation GWT event preview. + * @return + */ + public VDragEvent startDrag(VTransferable transferable, + final NativeEvent startEvent, final boolean handleDragEvents) { + interruptDrag(); + isStarted = false; + + currentDrag = new VDragEvent(transferable, startEvent); + currentDrag.setCurrentGwtEvent(startEvent); + + final Command startDrag = new Command() { + + @Override + public void execute() { + isStarted = true; + addActiveDragSourceStyleName(); + VDropHandler dh = null; + if (startEvent != null) { + dh = findDragTarget(Element.as(currentDrag + .getCurrentGwtEvent().getEventTarget())); + } + if (dh != null) { + // drag has started on a DropHandler, kind of drag over + // happens + currentDropHandler = dh; + dh.dragEnter(currentDrag); + } + + if (handleDragEvents) { + handlerRegistration = Event + .addNativePreviewHandler(defaultDragAndDropEventHandler); + if (dragElement != null + && dragElement.getParentElement() == null) { + // deferred attaching drag image is on going, we can + // hurry with it now + lazyAttachDragElement.cancel(); + lazyAttachDragElement.run(); + } + } + // just capture something to prevent text selection in IE + Event.setCapture(RootPanel.getBodyElement()); + } + + private void addActiveDragSourceStyleName() { + ComponentConnector dragSource = currentDrag.getTransferable() + .getDragSource(); + dragSource.getWidget().addStyleName( + ACTIVE_DRAG_SOURCE_STYLENAME); + } + }; + + final int eventType = Event.as(startEvent).getTypeInt(); + if (handleDragEvents + && (eventType == Event.ONMOUSEDOWN || eventType == Event.ONTOUCHSTART)) { + // only really start drag event on mousemove + deferredStartRegistration = Event + .addNativePreviewHandler(new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent( + NativePreviewEvent event) { + int typeInt = event.getTypeInt(); + switch (typeInt) { + case Event.ONMOUSEOVER: + if (dragElement == null) { + break; + } + EventTarget currentEventTarget = event + .getNativeEvent() + .getCurrentEventTarget(); + if (Node.is(currentEventTarget) + && !dragElement.isOrHasChild(Node + .as(currentEventTarget))) { + // drag image appeared below, ignore + break; + } + case Event.ONKEYDOWN: + case Event.ONKEYPRESS: + case Event.ONKEYUP: + case Event.ONBLUR: + case Event.ONFOCUS: + // don't cancel possible drag start + break; + case Event.ONMOUSEOUT: + + if (dragElement == null) { + break; + } + EventTarget relatedEventTarget = event + .getNativeEvent() + .getRelatedEventTarget(); + if (Node.is(relatedEventTarget) + && !dragElement.isOrHasChild(Node + .as(relatedEventTarget))) { + // drag image appeared below, ignore + break; + } + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + if (deferredStartRegistration != null) { + deferredStartRegistration.removeHandler(); + deferredStartRegistration = null; + } + currentDrag.setCurrentGwtEvent(event + .getNativeEvent()); + startDrag.execute(); + break; + default: + // on any other events, clean up the + // deferred drag start + if (deferredStartRegistration != null) { + deferredStartRegistration.removeHandler(); + deferredStartRegistration = null; + } + currentDrag = null; + clearDragElement(); + break; + } + } + + }); + + } else { + startDrag.execute(); + } + + return currentDrag; + } + + private void updateDragImagePosition() { + if (currentDrag.getCurrentGwtEvent() != null && dragElement != null) { + Style style = dragElement.getStyle(); + int clientY = Util.getTouchOrMouseClientY(currentDrag + .getCurrentGwtEvent()); + int clientX = Util.getTouchOrMouseClientX(currentDrag + .getCurrentGwtEvent()); + style.setTop(clientY, Unit.PX); + style.setLeft(clientX, Unit.PX); + } + } + + /** + * First seeks the widget from this element, then iterates widgets until one + * implement HasDropHandler. Returns DropHandler from that. + * + * @param element + * @return + */ + private VDropHandler findDragTarget(Element element) { + try { + Widget w = Util.findWidget( + (com.google.gwt.user.client.Element) element, null); + if (w == null) { + return null; + } + while (!(w instanceof VHasDropHandler)) { + w = w.getParent(); + if (w == null) { + break; + } + } + if (w == null) { + return null; + } else { + VDropHandler dh = ((VHasDropHandler) w).getDropHandler(); + return dh; + } + + } catch (Exception e) { + // ApplicationConnection.getConsole().log( + // "FIXME: Exception when detecting drop handler"); + // e.printStackTrace(); + return null; + } + + } + + /** + * Drag is ended (drop happened) on current drop handler. Calls drop method + * on current drop handler and does appropriate cleanup. + */ + public void endDrag() { + endDrag(true); + } + + /** + * The drag and drop operation is ended, but drop did not happen. If + * operation is currently on a drop handler, its dragLeave method is called + * and appropriate cleanup happens. + */ + public void interruptDrag() { + endDrag(false); + } + + private void endDrag(boolean doDrop) { + if (handlerRegistration != null) { + handlerRegistration.removeHandler(); + handlerRegistration = null; + } + boolean sendTransferableToServer = false; + if (currentDropHandler != null) { + if (doDrop) { + // we have dropped on a drop target + sendTransferableToServer = currentDropHandler.drop(currentDrag); + if (sendTransferableToServer) { + doRequest(DragEventType.DROP); + /* + * Clean active source class name deferred until response is + * handled. E.g. hidden on start, removed in drophandler -> + * would flicker in case removed eagerly. + */ + final ComponentConnector dragSource = currentDrag + .getTransferable().getDragSource(); + final ApplicationConnection client = currentDropHandler + .getApplicationConnection(); + Scheduler.get().scheduleFixedDelay(new RepeatingCommand() { + @Override + public boolean execute() { + if (!client.hasActiveRequest()) { + removeActiveDragSourceStyleName(dragSource); + return false; + } + return true; + } + + }, 30); + + } + } else { + currentDrag.setCurrentGwtEvent(null); + currentDropHandler.dragLeave(currentDrag); + } + currentDropHandler = null; + serverCallback = null; + visitId = 0; // reset to ignore ongoing server check + } + + /* + * Remove class name indicating drag source when server visit is done + * iff server visit was not initiated. Otherwise it will be removed once + * the server visit is done. + */ + if (!sendTransferableToServer && currentDrag != null) { + removeActiveDragSourceStyleName(currentDrag.getTransferable() + .getDragSource()); + } + + currentDrag = null; + + clearDragElement(); + + // release the capture (set to prevent text selection in IE) + Event.releaseCapture(RootPanel.getBodyElement()); + + } + + private void removeActiveDragSourceStyleName(ComponentConnector dragSource) { + dragSource.getWidget().removeStyleName(ACTIVE_DRAG_SOURCE_STYLENAME); + } + + private void clearDragElement() { + if (dragElement != null) { + if (dragElement.getParentElement() != null) { + RootPanel.getBodyElement().removeChild(dragElement); + } + dragElement = null; + } + } + + private int visitId = 0; + private Element dragElement; + + /** + * Visits server during drag and drop procedure. Transferable and event type + * is given to server side counterpart of DropHandler. + * + * If another server visit is started before the current is received, the + * current is just dropped. TODO consider if callback should have + * interrupted() method for cleanup. + * + * @param acceptCallback + */ + public void visitServer(VDragEventServerCallback acceptCallback) { + doRequest(DragEventType.ENTER); + serverCallback = acceptCallback; + } + + private void doRequest(DragEventType drop) { + if (currentDropHandler == null) { + return; + } + ComponentConnector paintable = currentDropHandler.getConnector(); + ApplicationConnection client = currentDropHandler + .getApplicationConnection(); + /* + * For drag events we are using special id that are routed to + * "drag service" which then again finds the corresponding DropHandler + * on server side. + * + * TODO add rest of the data in Transferable + * + * TODO implement partial updates to Transferable (currently the whole + * Transferable is sent on each request) + */ + visitId++; + client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID, + "visitId", visitId, false); + client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID, + "eventId", currentDrag.getEventId(), false); + client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID, + "dhowner", paintable, false); + + VTransferable transferable = currentDrag.getTransferable(); + + client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID, + "component", transferable.getDragSource(), false); + + client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID, + "type", drop.ordinal(), false); + + if (currentDrag.getCurrentGwtEvent() != null) { + try { + MouseEventDetails mouseEventDetails = MouseEventDetailsBuilder + .buildMouseEventDetails(currentDrag + .getCurrentGwtEvent()); + currentDrag.getDropDetails().put("mouseEvent", + mouseEventDetails.serialize()); + } catch (Exception e) { + // NOP, (at least oophm on Safari) can't serialize html dd event + // to mouseevent + } + } else { + currentDrag.getDropDetails().put("mouseEvent", null); + } + client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID, + "evt", currentDrag.getDropDetails(), false); + + client.updateVariable(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID, + "tra", transferable.getVariableMap(), true); + + } + + public void handleServerResponse(ValueMap valueMap) { + if (serverCallback == null) { + return; + } + UIDL uidl = (UIDL) valueMap.cast(); + int visitId = uidl.getIntAttribute("visitId"); + + if (this.visitId == visitId) { + serverCallback.handleResponse(uidl.getBooleanAttribute("accepted"), + uidl); + serverCallback = null; + } + runDeferredCommands(); + } + + private void runDeferredCommands() { + if (deferredCommand != null) { + Command command = deferredCommand; + deferredCommand = null; + command.execute(); + if (!isBusy()) { + runDeferredCommands(); + } + } + } + + void setDragElement(Element node) { + if (currentDrag != null) { + if (dragElement != null && dragElement != node) { + clearDragElement(); + } else if (node == dragElement) { + return; + } + + dragElement = node; + dragElement.addClassName("v-drag-element"); + updateDragImagePosition(); + + if (isStarted) { + lazyAttachDragElement.run(); + } else { + /* + * To make our default dnd handler as compatible as possible, we + * need to defer the appearance of dragElement. Otherwise events + * that are derived from sequences of other events might not + * fire as domchanged will fire between them or mouse up might + * happen on dragElement. + */ + lazyAttachDragElement.schedule(300); + } + } + } + + Element getDragElement() { + return dragElement; + } + + private final Timer lazyAttachDragElement = new Timer() { + + @Override + public void run() { + if (dragElement != null && dragElement.getParentElement() == null) { + RootPanel.getBodyElement().appendChild(dragElement); + } + + } + }; + + private Command deferredCommand; + + private boolean isBusy() { + return serverCallback != null; + } + + /** + * Method to que tasks until all dd related server visits are done + * + * @param command + */ + private void defer(Command command) { + deferredCommand = command; + } + + /** + * Method to execute commands when all existing dd related tasks are + * completed (some may require server visit). + * <p> + * Using this method may be handy if criterion that uses lazy initialization + * are used. Check + * <p> + * TODO Optimization: consider if we actually only need to keep the last + * command in queue here. + * + * @param command + */ + public void executeWhenReady(Command command) { + if (isBusy()) { + defer(command); + } else { + command.execute(); + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEvent.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEvent.java new file mode 100644 index 0000000000..7b3950f64f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEvent.java @@ -0,0 +1,201 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.TableElement; +import com.google.gwt.dom.client.TableSectionElement; +import com.google.gwt.event.dom.client.MouseOverEvent; +import com.google.gwt.user.client.Element; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.Util; + +/** + * DragEvent used by Vaadin client side engine. Supports components, items, + * properties and custom payload (HTML5 style). + * + * + */ +public class VDragEvent { + + private static final int DEFAULT_OFFSET = 10; + + private static int eventId = 0; + + private VTransferable transferable; + + private NativeEvent currentGwtEvent; + + private NativeEvent startEvent; + + private int id; + + private HashMap<String, Object> dropDetails = new HashMap<String, Object>(); + + private Element elementOver; + + VDragEvent(VTransferable t, NativeEvent startEvent) { + transferable = t; + this.startEvent = startEvent; + id = eventId++; + } + + public VTransferable getTransferable() { + return transferable; + } + + /** + * Returns the the latest {@link NativeEvent} that relates to this drag and + * drop operation. For example on {@link VDropHandler#dragEnter(VDragEvent)} + * this is commonly a {@link MouseOverEvent}. + * + * @return + */ + public NativeEvent getCurrentGwtEvent() { + return currentGwtEvent; + } + + public void setCurrentGwtEvent(NativeEvent event) { + currentGwtEvent = event; + } + + int getEventId() { + return id; + } + + /** + * Detecting the element on which the the event is happening may be + * problematic during drag and drop operation. This is especially the case + * if a drag image (often called also drag proxy) is kept under the mouse + * cursor (see {@link #createDragImage(Element, boolean)}. Drag and drop + * event handlers (like the one provided by {@link VDragAndDropManager} ) + * should set elmentOver field to reflect the the actual element on which + * the pointer currently is (drag image excluded). {@link VDropHandler}s can + * then more easily react properly on drag events by reading the element via + * this method. + * + * @return the element in {@link VDropHandler} on which mouse cursor is on + */ + public Element getElementOver() { + if (elementOver != null) { + return elementOver; + } else if (currentGwtEvent != null) { + return currentGwtEvent.getEventTarget().cast(); + } + return null; + } + + public void setElementOver(Element targetElement) { + elementOver = targetElement; + } + + /** + * Sets the drag image used for current drag and drop operation. Drag image + * is displayed next to mouse cursor during drag and drop. + * <p> + * The element to be used as drag image will automatically get CSS style + * name "v-drag-element". + * + * TODO decide if this method should be here or in {@link VTransferable} (in + * HTML5 it is in DataTransfer) or {@link VDragAndDropManager} + * + * TODO should be possible to override behavior. Like to proxy the element + * to HTML5 DataTransfer + * + * @param node + */ + public void setDragImage(Element node) { + setDragImage(node, DEFAULT_OFFSET, DEFAULT_OFFSET); + } + + /** + * TODO consider using similar smaller (than map) api as in Transferable + * + * TODO clean up when drop handler changes + * + * @return + */ + public Map<String, Object> getDropDetails() { + return dropDetails; + } + + /** + * Sets the drag image used for current drag and drop operation. Drag image + * is displayed next to mouse cursor during drag and drop. + * <p> + * The element to be used as drag image will automatically get CSS style + * name "v-drag-element". + * + * @param element + * the dom element to be positioned next to mouse cursor + * @param offsetX + * the horizontal offset of drag image from mouse cursor + * @param offsetY + * the vertical offset of drag image from mouse cursor + */ + public void setDragImage(Element element, int offsetX, int offsetY) { + element.getStyle().setMarginLeft(offsetX, Unit.PX); + element.getStyle().setMarginTop(offsetY, Unit.PX); + VDragAndDropManager.get().setDragElement(element); + } + + /** + * @return the current Element used as a drag image (aka drag proxy) or null + * if drag image is not currently set for this drag operation. + */ + public Element getDragImage() { + return (Element) VDragAndDropManager.get().getDragElement(); + } + + /** + * Automatically tries to create a proxy image from given element. + * + * @param element + * @param alignImageToEvent + * if true, proxy image is aligned to start event, else next to + * mouse cursor + */ + public void createDragImage(Element element, boolean alignImageToEvent) { + Element cloneNode = (Element) element.cloneNode(true); + if (BrowserInfo.get().isIE()) { + if (cloneNode.getTagName().toLowerCase().equals("tr")) { + TableElement table = Document.get().createTableElement(); + TableSectionElement tbody = Document.get().createTBodyElement(); + table.appendChild(tbody); + tbody.appendChild(cloneNode); + cloneNode = table.cast(); + } + } + if (alignImageToEvent) { + int absoluteTop = element.getAbsoluteTop(); + int absoluteLeft = element.getAbsoluteLeft(); + int clientX = Util.getTouchOrMouseClientX(startEvent); + int clientY = Util.getTouchOrMouseClientY(startEvent); + int offsetX = absoluteLeft - clientX; + int offsetY = absoluteTop - clientY; + setDragImage(cloneNode, offsetX, offsetY); + } else { + setDragImage(cloneNode); + } + + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEventServerCallback.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEventServerCallback.java new file mode 100644 index 0000000000..00fe2d2659 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEventServerCallback.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.terminal.gwt.client.UIDL; + +public interface VDragEventServerCallback { + + public void handleResponse(boolean accepted, UIDL response); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragSourceIs.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragSourceIs.java new file mode 100644 index 0000000000..e812ca8117 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragSourceIs.java @@ -0,0 +1,54 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.event.dd.acceptcriteria.SourceIs; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.UIDL; + +/** + * TODO Javadoc! + * + * @since 6.3 + */ +@AcceptCriterion(SourceIs.class) +final public class VDragSourceIs extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + try { + ComponentConnector component = drag.getTransferable() + .getDragSource(); + int c = configuration.getIntAttribute("c"); + for (int i = 0; i < c; i++) { + String requiredPid = configuration + .getStringAttribute("component" + i); + VDropHandler currentDropHandler = VDragAndDropManager.get() + .getCurrentDropHandler(); + ComponentConnector paintable = (ComponentConnector) ConnectorMap + .get(currentDropHandler.getApplicationConnection()) + .getConnector(requiredPid); + if (paintable == component) { + return true; + } + } + } catch (Exception e) { + } + return false; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDropHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDropHandler.java new file mode 100644 index 0000000000..ddc3af9931 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDropHandler.java @@ -0,0 +1,85 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; + +/** + * Vaadin Widgets that want to receive something via drag and drop implement + * this interface. + */ +public interface VDropHandler { + + /** + * Called by DragAndDropManager when a drag operation is in progress and the + * cursor enters the area occupied by this Paintable. + * + * @param dragEvent + * DragEvent which contains the transferable and other + * information for the operation + */ + public void dragEnter(VDragEvent dragEvent); + + /** + * Called by DragAndDropManager when a drag operation is in progress and the + * cursor leaves the area occupied by this Paintable. + * + * @param dragEvent + * DragEvent which contains the transferable and other + * information for the operation + */ + public void dragLeave(VDragEvent dragEvent); + + /** + * Called by DragAndDropManager when a drag operation was in progress and a + * drop was performed on this Paintable. + * + * + * @param dragEvent + * DragEvent which contains the transferable and other + * information for the operation + * + * @return true if the Tranferrable of this drag event needs to be sent to + * the server, false if drop is rejected or no server side event + * should be sent + */ + public boolean drop(VDragEvent drag); + + /** + * When drag is over current drag handler. + * + * With drag implementation by {@link VDragAndDropManager} will be called + * when mouse is moved. HTML5 implementations call this continuously even + * though mouse is not moved. + * + * @param currentDrag + */ + public void dragOver(VDragEvent currentDrag); + + /** + * Returns the ComponentConnector with which this DropHandler is associated + */ + public ComponentConnector getConnector(); + + /** + * Returns the application connection to which this {@link VDropHandler} + * belongs to. DragAndDropManager uses this fucction to send Transferable to + * server side. + */ + public ApplicationConnection getApplicationConnection(); + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHasDropHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHasDropHandler.java new file mode 100644 index 0000000000..01c57741d8 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHasDropHandler.java @@ -0,0 +1,29 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.terminal.gwt.client.ComponentConnector; + +/** + * Used to detect Widget from widget tree that has {@link #getDropHandler()} + * + * Decide whether to get rid of this class. If so, {@link VAbstractDropHandler} + * must extend {@link ComponentConnector}. + * + */ +public interface VHasDropHandler { + public VDropHandler getDropHandler(); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent.java new file mode 100644 index 0000000000..32abc787da --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent.java @@ -0,0 +1,96 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.google.gwt.core.client.JsArrayString; +import com.google.gwt.dom.client.NativeEvent; + +/** + * Helper class to access html5 style drag events. + * + * TODO Gears support ? + */ +public class VHtml5DragEvent extends NativeEvent { + protected VHtml5DragEvent() { + } + + public final native JsArrayString getTypes() + /*-{ + // IE does not support types, return some basic values + return this.dataTransfer.types ? this.dataTransfer.types : ["Text","Url","Html"]; + }-*/; + + public final native String getDataAsText(String type) + /*-{ + var v = this.dataTransfer.getData(type); + return v; + }-*/; + + /** + * Works on FF 3.6 and possibly with gears. + * + * @param index + * @return + */ + public final native String getFileAsString(int index) + /*-{ + if(this.dataTransfer.files.length > 0 && this.dataTransfer.files[0].getAsText) { + return this.dataTransfer.files[index].getAsText("UTF-8"); + } + return null; + }-*/; + + /** + * @deprecated As of Vaadin 6.8, replaced by {@link #setDropEffect(String)}. + */ + @Deprecated + public final void setDragEffect(String effect) { + setDropEffect(effect); + } + + public final native void setDropEffect(String effect) + /*-{ + try { + this.dataTransfer.dropEffect = effect; + } catch (e){} + }-*/; + + public final native String getEffectAllowed() + /*-{ + return this.dataTransfer.effectAllowed; + }-*/; + + public final native void setEffectAllowed(String effect) + /*-{ + this.dataTransfer.effectAllowed = effect; + }-*/; + + public final native int getFileCount() + /*-{ + return this.dataTransfer.files ? this.dataTransfer.files.length : 0; + }-*/; + + public final native VHtml5File getFile(int fileIndex) + /*-{ + return this.dataTransfer.files[fileIndex]; + }-*/; + + public final native void setHtml5DataFlavor(String flavor, String data) + /*-{ + this.dataTransfer.setData(flavor, data); + }-*/; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5File.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5File.java new file mode 100644 index 0000000000..961008c860 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5File.java @@ -0,0 +1,43 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.google.gwt.core.client.JavaScriptObject; + +/** + * Wrapper for html5 File object. + */ +public class VHtml5File extends JavaScriptObject { + + protected VHtml5File() { + }; + + public native final String getName() + /*-{ + return this.name; + }-*/; + + public native final String getType() + /*-{ + return this.type; + }-*/; + + public native final int getSize() + /*-{ + return this.size ? this.size : 0; + }-*/; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VIsOverId.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VIsOverId.java new file mode 100644 index 0000000000..c9bf630658 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VIsOverId.java @@ -0,0 +1,57 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.ui.AbstractSelect; + +@AcceptCriterion(AbstractSelect.TargetItemIs.class) +final public class VIsOverId extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + try { + + String pid = configuration.getStringAttribute("s"); + VDropHandler currentDropHandler = VDragAndDropManager.get() + .getCurrentDropHandler(); + ComponentConnector dropHandlerConnector = currentDropHandler + .getConnector(); + ConnectorMap paintableMap = ConnectorMap.get(currentDropHandler + .getApplicationConnection()); + + String pid2 = dropHandlerConnector.getConnectorId(); + if (pid2.equals(pid)) { + Object searchedId = drag.getDropDetails().get("itemIdOver"); + String[] stringArrayAttribute = configuration + .getStringArrayAttribute("keys"); + for (String string : stringArrayAttribute) { + if (string.equals(searchedId)) { + return true; + } + } + } + } catch (Exception e) { + } + return false; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VItemIdIs.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VItemIdIs.java new file mode 100644 index 0000000000..4f6aca082b --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VItemIdIs.java @@ -0,0 +1,52 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.ui.AbstractSelect; + +@AcceptCriterion(AbstractSelect.AcceptItem.class) +final public class VItemIdIs extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + try { + 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"); + String[] stringArrayAttribute = configuration + .getStringArrayAttribute("keys"); + for (String string : stringArrayAttribute) { + if (string.equals(searchedId)) { + return true; + } + } + } + } catch (Exception e) { + } + return false; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VLazyInitItemIdentifiers.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VLazyInitItemIdentifiers.java new file mode 100644 index 0000000000..ad028b7198 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VLazyInitItemIdentifiers.java @@ -0,0 +1,93 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import java.util.HashSet; + +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.ui.Table; +import com.vaadin.ui.Tree; + +/** + * + */ +public class VLazyInitItemIdentifiers extends VAcceptCriterion { + private boolean loaded = false; + private HashSet<String> hashSet; + private VDragEvent lastDragEvent; + + @AcceptCriterion(Table.TableDropCriterion.class) + final public static class VTableLazyInitItemIdentifiers extends + VLazyInitItemIdentifiers { + // all logic in superclass + } + + @AcceptCriterion(Tree.TreeDropCriterion.class) + final public static class VTreeLazyInitItemIdentifiers extends + VLazyInitItemIdentifiers { + // all logic in superclass + } + + @Override + public void accept(final VDragEvent drag, UIDL configuration, + final VAcceptCallback callback) { + if (lastDragEvent == null || lastDragEvent != drag) { + loaded = false; + lastDragEvent = drag; + } + if (loaded) { + Object object = drag.getDropDetails().get("itemIdOver"); + if (hashSet.contains(object)) { + callback.accepted(drag); + } + } else { + + VDragEventServerCallback acceptCallback = new VDragEventServerCallback() { + + @Override + public void handleResponse(boolean accepted, UIDL response) { + hashSet = new HashSet<String>(); + String[] stringArrayAttribute = response + .getStringArrayAttribute("allowedIds"); + for (int i = 0; i < stringArrayAttribute.length; i++) { + hashSet.add(stringArrayAttribute[i]); + } + loaded = true; + if (accepted) { + callback.accepted(drag); + } + } + }; + + VDragAndDropManager.get().visitServer(acceptCallback); + } + + } + + @Override + public boolean needsServerSideCheck(VDragEvent drag, UIDL criterioUIDL) { + return loaded; + } + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + return false; // not used is this implementation + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VNot.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VNot.java new file mode 100644 index 0000000000..662b1c2da2 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VNot.java @@ -0,0 +1,76 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.event.dd.acceptcriteria.Not; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.VConsole; + +/** + * TODO implementation could now be simplified/optimized + * + */ +@AcceptCriterion(Not.class) +final public class VNot extends VAcceptCriterion { + private boolean b1; + private VAcceptCriterion crit1; + + @Override + public void accept(VDragEvent drag, UIDL configuration, + VAcceptCallback callback) { + if (crit1 == null) { + crit1 = getCriteria(drag, configuration, 0); + if (crit1 == null) { + VConsole.log("Not criteria didn't found a child criteria"); + return; + } + } + + b1 = false; + + VAcceptCallback accept1cb = new VAcceptCallback() { + @Override + public void accepted(VDragEvent event) { + b1 = true; + } + }; + + crit1.accept(drag, configuration.getChildUIDL(0), accept1cb); + if (!b1) { + callback.accepted(drag); + } + } + + private VAcceptCriterion getCriteria(VDragEvent drag, UIDL configuration, + int i) { + UIDL childUIDL = configuration.getChildUIDL(i); + return VAcceptCriteria.get(childUIDL.getStringAttribute("name")); + } + + @Override + public boolean needsServerSideCheck(VDragEvent drag, UIDL criterioUIDL) { + return false; // TODO enforce on server side + } + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + return false; // not used + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOr.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOr.java new file mode 100644 index 0000000000..b51800e31f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOr.java @@ -0,0 +1,62 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.event.dd.acceptcriteria.Or; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.UIDL; + +/** + * + */ +@AcceptCriterion(Or.class) +final public class VOr extends VAcceptCriterion implements VAcceptCallback { + private boolean accepted; + + @Override + public void accept(VDragEvent drag, UIDL configuration, + VAcceptCallback callback) { + int childCount = configuration.getChildCount(); + accepted = false; + for (int i = 0; i < childCount; i++) { + VAcceptCriterion crit = VAnd.getCriteria(drag, configuration, i); + crit.accept(drag, configuration.getChildUIDL(i), this); + if (accepted == true) { + callback.accepted(drag); + return; + } + } + } + + @Override + public boolean needsServerSideCheck(VDragEvent drag, UIDL criterioUIDL) { + return false; + } + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + return false; // not used here + } + + @Override + public void accepted(VDragEvent event) { + accepted = true; + } + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOverTreeNode.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOverTreeNode.java new file mode 100644 index 0000000000..77fd89c123 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOverTreeNode.java @@ -0,0 +1,31 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.terminal.gwt.client.UIDL; + +final public class VOverTreeNode extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + Boolean containsKey = (Boolean) drag.getDropDetails().get( + "itemIdOverIsNode"); + return containsKey != null && containsKey.booleanValue(); + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VServerAccept.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VServerAccept.java new file mode 100644 index 0000000000..eee0f47a91 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VServerAccept.java @@ -0,0 +1,51 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.UIDL; + +@AcceptCriterion(ServerSideCriterion.class) +final public class VServerAccept extends VAcceptCriterion { + @Override + public void accept(final VDragEvent drag, UIDL configuration, + final VAcceptCallback callback) { + + VDragEventServerCallback acceptCallback = new VDragEventServerCallback() { + @Override + public void handleResponse(boolean accepted, UIDL response) { + if (accepted) { + callback.accepted(drag); + } + } + }; + VDragAndDropManager.get().visitServer(acceptCallback); + } + + @Override + public boolean needsServerSideCheck(VDragEvent drag, UIDL criterioUIDL) { + return true; + } + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + return false; // not used + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VSourceIsTarget.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VSourceIsTarget.java new file mode 100644 index 0000000000..21e6ab130a --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VSourceIsTarget.java @@ -0,0 +1,37 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.event.dd.acceptcriteria.SourceIsTarget; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.UIDL; + +@AcceptCriterion(SourceIsTarget.class) +final public class VSourceIsTarget extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + ComponentConnector dragSource = drag.getTransferable().getDragSource(); + ComponentConnector paintable = VDragAndDropManager.get() + .getCurrentDropHandler().getConnector(); + + return paintable == dragSource; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetDetailIs.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetDetailIs.java new file mode 100644 index 0000000000..8c6c522be4 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetDetailIs.java @@ -0,0 +1,51 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.vaadin.event.dd.acceptcriteria.TargetDetailIs; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.UIDL; + +@AcceptCriterion(TargetDetailIs.class) +final public class VTargetDetailIs extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + String name = configuration.getStringAttribute("p"); + String t = configuration.hasAttribute("t") ? configuration + .getStringAttribute("t").intern() : "s"; + Object value = null; + if (t == "s") { + value = configuration.getStringAttribute("v"); + } else if (t == "b") { + value = configuration.getBooleanAttribute("v"); + } + if (value != null) { + Object object = drag.getDropDetails().get(name); + if (object instanceof Enum) { + return ((Enum<?>) object).name().equals(value); + } else { + return value.equals(object); + } + } else { + return false; + } + + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetInSubtree.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetInSubtree.java new file mode 100644 index 0000000000..56421a6ed7 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetInSubtree.java @@ -0,0 +1,56 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.tree.VTree; +import com.vaadin.terminal.gwt.client.ui.tree.VTree.TreeNode; +import com.vaadin.ui.Tree; + +@AcceptCriterion(Tree.TargetInSubtree.class) +final public class VTargetInSubtree extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + + VTree tree = (VTree) VDragAndDropManager.get().getCurrentDropHandler() + .getConnector(); + TreeNode treeNode = tree.getNodeByKey((String) drag.getDropDetails() + .get("itemIdOver")); + if (treeNode != null) { + Widget parent2 = treeNode; + int depth = configuration.getIntAttribute("depth"); + if (depth < 0) { + depth = Integer.MAX_VALUE; + } + final String searchedKey = configuration.getStringAttribute("key"); + for (int i = 0; i <= depth && parent2 instanceof TreeNode; i++) { + if (searchedKey.equals(((TreeNode) parent2).key)) { + return true; + } + // panel -> next level node + parent2 = parent2.getParent().getParent(); + } + } + + return false; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTransferable.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTransferable.java new file mode 100644 index 0000000000..12cd1ed598 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTransferable.java @@ -0,0 +1,81 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.dd; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.event.dd.DragSource; +import com.vaadin.terminal.gwt.client.ComponentConnector; + +/** + * Client side counterpart for Transferable in com.vaadin.event.Transferable + * + */ +public class VTransferable { + + private ComponentConnector component; + + private final Map<String, Object> variables = new HashMap<String, Object>(); + + /** + * Returns the component from which the transferable is created (eg. a tree + * which node is dragged). + * + * @return the component + */ + public ComponentConnector getDragSource() { + return component; + } + + /** + * Sets the component currently being dragged or from which the transferable + * is created (eg. a tree which node is dragged). + * <p> + * The server side counterpart of the component may implement + * {@link DragSource} interface if it wants to translate or complement the + * server side instance of this Transferable. + * + * @param component + * the component to set + */ + public void setDragSource(ComponentConnector component) { + this.component = component; + } + + public Object getData(String dataFlavor) { + return variables.get(dataFlavor); + } + + public void setData(String dataFlavor, Object value) { + variables.put(dataFlavor, value); + } + + public Collection<String> getDataFlavors() { + return variables.keySet(); + } + + /** + * This helper method should only be called by {@link VDragAndDropManager}. + * + * @return data in this Transferable that needs to be moved to server. + */ + Map<String, Object> getVariableMap() { + return variables; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png Binary files differnew file mode 100644 index 0000000000..49b918ec0c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.png Binary files differnew file mode 100644 index 0000000000..9fd6635765 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png Binary files differnew file mode 100644 index 0000000000..7cd07369dc --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png Binary files differnew file mode 100644 index 0000000000..c2e1f49efe --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png Binary files differnew file mode 100644 index 0000000000..417c9aecfd --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png Binary files differnew file mode 100644 index 0000000000..2f1e461b0a --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png Binary files differnew file mode 100644 index 0000000000..63984cdee7 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png Binary files differnew file mode 100644 index 0000000000..1e730c072b --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png Binary files differnew file mode 100644 index 0000000000..34e47d1551 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png Binary files differnew file mode 100644 index 0000000000..99e3709acc --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png Binary files differnew file mode 100644 index 0000000000..be9a4cd8c5 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png Binary files differnew file mode 100644 index 0000000000..0b555ad1e7 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png Binary files differnew file mode 100644 index 0000000000..8ff42ed0f4 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/DragAndDropWrapperConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/DragAndDropWrapperConnector.java new file mode 100644 index 0000000000..7542754e58 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/DragAndDropWrapperConnector.java @@ -0,0 +1,85 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.draganddropwrapper; + +import java.util.HashMap; +import java.util.Set; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.draganddropwrapper.DragAndDropWrapperConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.customcomponent.CustomComponentConnector; +import com.vaadin.ui.DragAndDropWrapper; + +@Connect(DragAndDropWrapper.class) +public class DragAndDropWrapperConnector extends CustomComponentConnector + implements Paintable { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + getWidget().client = client; + if (isRealUpdate(uidl) && !uidl.hasAttribute("hidden")) { + UIDL acceptCrit = uidl.getChildByTagName("-ac"); + if (acceptCrit == null) { + getWidget().dropHandler = null; + } else { + if (getWidget().dropHandler == null) { + getWidget().dropHandler = getWidget().new CustomDropHandler(); + } + getWidget().dropHandler.updateAcceptRules(acceptCrit); + } + + Set<String> variableNames = uidl.getVariableNames(); + for (String fileId : variableNames) { + if (fileId.startsWith("rec-")) { + String receiverUrl = uidl.getStringVariable(fileId); + fileId = fileId.substring(4); + if (getWidget().fileIdToReceiver == null) { + getWidget().fileIdToReceiver = new HashMap<String, String>(); + } + if ("".equals(receiverUrl)) { + Integer id = Integer.parseInt(fileId); + int indexOf = getWidget().fileIds.indexOf(id); + if (indexOf != -1) { + getWidget().files.remove(indexOf); + getWidget().fileIds.remove(indexOf); + } + } else { + getWidget().fileIdToReceiver.put(fileId, receiverUrl); + } + } + } + getWidget().startNextUpload(); + + getWidget().dragStartMode = uidl + .getIntAttribute(DragAndDropWrapperConstants.DRAG_START_MODE); + getWidget().initDragStartMode(); + getWidget().html5DataFlavors = uidl + .getMapAttribute(DragAndDropWrapperConstants.HTML5_DATA_FLAVORS); + + // Used to prevent wrapper from stealing tooltips when not defined + getWidget().hasTooltip = getState().hasDescription(); + } + } + + @Override + public VDragAndDropWrapper getWidget() { + return (VDragAndDropWrapper) super.getWidget(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java new file mode 100644 index 0000000000..7d38624f22 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java @@ -0,0 +1,606 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.draganddropwrapper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.JsArrayString; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.event.dom.client.MouseDownEvent; +import com.google.gwt.event.dom.client.MouseDownHandler; +import com.google.gwt.event.dom.client.TouchStartEvent; +import com.google.gwt.event.dom.client.TouchStartHandler; +import com.google.gwt.user.client.Command; +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.Widget; +import com.google.gwt.xhr.client.ReadyStateChangeHandler; +import com.google.gwt.xhr.client.XMLHttpRequest; +import com.vaadin.shared.ui.dd.HorizontalDropLocation; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ValueMap; +import com.vaadin.terminal.gwt.client.ui.customcomponent.VCustomComponent; +import com.vaadin.terminal.gwt.client.ui.dd.DDUtil; +import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler; +import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback; +import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager; +import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent; +import com.vaadin.terminal.gwt.client.ui.dd.VDropHandler; +import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler; +import com.vaadin.terminal.gwt.client.ui.dd.VHtml5DragEvent; +import com.vaadin.terminal.gwt.client.ui.dd.VHtml5File; +import com.vaadin.terminal.gwt.client.ui.dd.VTransferable; + +/** + * + * Must have features pending: + * + * drop details: locations + sizes in document hierarchy up to wrapper + * + */ +public class VDragAndDropWrapper extends VCustomComponent implements + VHasDropHandler { + + private static final String CLASSNAME = "v-ddwrapper"; + protected static final String DRAGGABLE = "draggable"; + + boolean hasTooltip = false; + + public VDragAndDropWrapper() { + super(); + + hookHtml5Events(getElement()); + setStyleName(CLASSNAME); + addDomHandler(new MouseDownHandler() { + + @Override + public void onMouseDown(MouseDownEvent event) { + if (startDrag(event.getNativeEvent())) { + event.preventDefault(); // prevent text selection + } + } + }, MouseDownEvent.getType()); + + addDomHandler(new TouchStartHandler() { + + @Override + public void onTouchStart(TouchStartEvent event) { + if (startDrag(event.getNativeEvent())) { + /* + * Dont let eg. panel start scrolling. + */ + event.stopPropagation(); + } + } + }, TouchStartEvent.getType()); + + sinkEvents(Event.TOUCHEVENTS); + } + + /** + * Starts a drag and drop operation from mousedown or touchstart event if + * required conditions are met. + * + * @param event + * @return true if the event was handled as a drag start event + */ + private boolean startDrag(NativeEvent event) { + if (dragStartMode == WRAPPER || dragStartMode == COMPONENT) { + VTransferable transferable = new VTransferable(); + transferable.setDragSource(ConnectorMap.get(client).getConnector( + VDragAndDropWrapper.this)); + + ComponentConnector paintable = Util.findPaintable(client, + (Element) event.getEventTarget().cast()); + Widget widget = paintable.getWidget(); + transferable.setData("component", paintable); + VDragEvent dragEvent = VDragAndDropManager.get().startDrag( + transferable, event, true); + + transferable.setData("mouseDown", MouseEventDetailsBuilder + .buildMouseEventDetails(event).serialize()); + + if (dragStartMode == WRAPPER) { + dragEvent.createDragImage(getElement(), true); + } else { + dragEvent.createDragImage(widget.getElement(), true); + } + return true; + } + return false; + } + + protected final static int NONE = 0; + protected final static int COMPONENT = 1; + protected final static int WRAPPER = 2; + protected final static int HTML5 = 3; + + protected int dragStartMode; + + ApplicationConnection client; + VAbstractDropHandler dropHandler; + private VDragEvent vaadinDragEvent; + + int filecounter = 0; + Map<String, String> fileIdToReceiver; + ValueMap html5DataFlavors; + private Element dragStartElement; + + protected void initDragStartMode() { + Element div = getElement(); + if (dragStartMode == HTML5) { + if (dragStartElement == null) { + dragStartElement = getDragStartElement(); + dragStartElement.setPropertyBoolean(DRAGGABLE, true); + VConsole.log("draggable = " + + dragStartElement.getPropertyBoolean(DRAGGABLE)); + hookHtml5DragStart(dragStartElement); + VConsole.log("drag start listeners hooked."); + } + } else { + dragStartElement = null; + if (div.hasAttribute(DRAGGABLE)) { + div.removeAttribute(DRAGGABLE); + } + } + } + + protected Element getDragStartElement() { + return getElement(); + } + + private boolean uploading; + + private ReadyStateChangeHandler readyStateChangeHandler = new ReadyStateChangeHandler() { + + @Override + public void onReadyStateChange(XMLHttpRequest xhr) { + if (xhr.getReadyState() == XMLHttpRequest.DONE) { + // visit server for possible + // variable changes + client.sendPendingVariableChanges(); + uploading = false; + startNextUpload(); + xhr.clearOnReadyStateChange(); + } + } + }; + private Timer dragleavetimer; + + void startNextUpload() { + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + if (!uploading) { + if (fileIds.size() > 0) { + + uploading = true; + final Integer fileId = fileIds.remove(0); + VHtml5File file = files.remove(0); + final String receiverUrl = client + .translateVaadinUri(fileIdToReceiver + .remove(fileId.toString())); + ExtendedXHR extendedXHR = (ExtendedXHR) ExtendedXHR + .create(); + extendedXHR + .setOnReadyStateChange(readyStateChangeHandler); + extendedXHR.open("POST", receiverUrl); + extendedXHR.postFile(file); + } + } + + } + }); + + } + + public boolean html5DragStart(VHtml5DragEvent event) { + if (dragStartMode == HTML5) { + /* + * Populate html5 payload with dataflavors from the serverside + */ + JsArrayString flavors = html5DataFlavors.getKeyArray(); + for (int i = 0; i < flavors.length(); i++) { + String flavor = flavors.get(i); + event.setHtml5DataFlavor(flavor, + html5DataFlavors.getString(flavor)); + } + event.setEffectAllowed("copy"); + return true; + } + return false; + } + + public boolean html5DragEnter(VHtml5DragEvent event) { + if (dropHandler == null) { + return true; + } + try { + if (dragleavetimer != null) { + // returned quickly back to wrapper + dragleavetimer.cancel(); + dragleavetimer = null; + } + if (VDragAndDropManager.get().getCurrentDropHandler() != getDropHandler()) { + VTransferable transferable = new VTransferable(); + transferable.setDragSource(ConnectorMap.get(client) + .getConnector(this)); + + vaadinDragEvent = VDragAndDropManager.get().startDrag( + transferable, event, false); + VDragAndDropManager.get().setCurrentDropHandler( + getDropHandler()); + } + try { + event.preventDefault(); + event.stopPropagation(); + } catch (Exception e) { + // VConsole.log("IE9 fails"); + } + return false; + } catch (Exception e) { + GWT.getUncaughtExceptionHandler().onUncaughtException(e); + return true; + } + } + + public boolean html5DragLeave(VHtml5DragEvent event) { + if (dropHandler == null) { + return true; + } + + try { + dragleavetimer = new Timer() { + + @Override + public void run() { + // Yes, dragleave happens before drop. Makes no sense to me. + // IMO shouldn't fire leave at all if drop happens (I guess + // this + // is what IE does). + // In Vaadin we fire it only if drop did not happen. + if (vaadinDragEvent != null + && VDragAndDropManager.get() + .getCurrentDropHandler() == getDropHandler()) { + VDragAndDropManager.get().interruptDrag(); + } + } + }; + dragleavetimer.schedule(350); + try { + event.preventDefault(); + event.stopPropagation(); + } catch (Exception e) { + // VConsole.log("IE9 fails"); + } + return false; + } catch (Exception e) { + GWT.getUncaughtExceptionHandler().onUncaughtException(e); + return true; + } + } + + public boolean html5DragOver(VHtml5DragEvent event) { + if (dropHandler == null) { + return true; + } + + if (dragleavetimer != null) { + // returned quickly back to wrapper + dragleavetimer.cancel(); + dragleavetimer = null; + } + + vaadinDragEvent.setCurrentGwtEvent(event); + getDropHandler().dragOver(vaadinDragEvent); + + String s = event.getEffectAllowed(); + if ("all".equals(s) || s.contains("opy")) { + event.setDropEffect("copy"); + } else { + event.setDropEffect(s); + } + + try { + event.preventDefault(); + event.stopPropagation(); + } catch (Exception e) { + // VConsole.log("IE9 fails"); + } + return false; + } + + public boolean html5DragDrop(VHtml5DragEvent event) { + if (dropHandler == null || !currentlyValid) { + return true; + } + try { + + VTransferable transferable = vaadinDragEvent.getTransferable(); + + JsArrayString types = event.getTypes(); + for (int i = 0; i < types.length(); i++) { + String type = types.get(i); + if (isAcceptedType(type)) { + String data = event.getDataAsText(type); + if (data != null) { + transferable.setData(type, data); + } + } + } + + int fileCount = event.getFileCount(); + if (fileCount > 0) { + transferable.setData("filecount", fileCount); + for (int i = 0; i < fileCount; i++) { + final int fileId = filecounter++; + final VHtml5File file = event.getFile(i); + transferable.setData("fi" + i, "" + fileId); + transferable.setData("fn" + i, file.getName()); + transferable.setData("ft" + i, file.getType()); + transferable.setData("fs" + i, file.getSize()); + queueFilePost(fileId, file); + } + + } + + VDragAndDropManager.get().endDrag(); + vaadinDragEvent = null; + try { + event.preventDefault(); + event.stopPropagation(); + } catch (Exception e) { + // VConsole.log("IE9 fails"); + } + return false; + } catch (Exception e) { + GWT.getUncaughtExceptionHandler().onUncaughtException(e); + return true; + } + + } + + protected String[] acceptedTypes = new String[] { "Text", "Url", + "text/html", "text/plain", "text/rtf" }; + + private boolean isAcceptedType(String type) { + for (String t : acceptedTypes) { + if (t.equals(type)) { + return true; + } + } + return false; + } + + static class ExtendedXHR extends XMLHttpRequest { + + protected ExtendedXHR() { + } + + public final native void postFile(VHtml5File file) + /*-{ + + this.setRequestHeader('Content-Type', 'multipart/form-data'); + this.send(file); + }-*/; + + } + + /** + * Currently supports only FF36 as no other browser supports natively File + * api. + * + * @param fileId + * @param data + */ + List<Integer> fileIds = new ArrayList<Integer>(); + List<VHtml5File> files = new ArrayList<VHtml5File>(); + + private void queueFilePost(final int fileId, final VHtml5File file) { + fileIds.add(fileId); + files.add(file); + } + + @Override + public VDropHandler getDropHandler() { + return dropHandler; + } + + protected VerticalDropLocation verticalDropLocation; + protected HorizontalDropLocation horizontalDropLocation; + private VerticalDropLocation emphasizedVDrop; + private HorizontalDropLocation emphasizedHDrop; + + /** + * Flag used by html5 dd + */ + private boolean currentlyValid; + + private static final String OVER_STYLE = "v-ddwrapper-over"; + + public class CustomDropHandler extends VAbstractDropHandler { + + @Override + public void dragEnter(VDragEvent drag) { + updateDropDetails(drag); + currentlyValid = false; + super.dragEnter(drag); + } + + @Override + public void dragLeave(VDragEvent drag) { + deEmphasis(true); + dragleavetimer = null; + } + + @Override + public void dragOver(final VDragEvent drag) { + boolean detailsChanged = updateDropDetails(drag); + if (detailsChanged) { + currentlyValid = false; + validate(new VAcceptCallback() { + + @Override + public void accepted(VDragEvent event) { + dragAccepted(drag); + } + }, drag); + } + } + + @Override + public boolean drop(VDragEvent drag) { + deEmphasis(true); + + Map<String, Object> dd = drag.getDropDetails(); + + // this is absolute layout based, and we may want to set + // component + // relatively to where the drag ended. + // need to add current location of the drop area + + int absoluteLeft = getAbsoluteLeft(); + int absoluteTop = getAbsoluteTop(); + + dd.put("absoluteLeft", absoluteLeft); + dd.put("absoluteTop", absoluteTop); + + if (verticalDropLocation != null) { + dd.put("verticalLocation", verticalDropLocation.toString()); + dd.put("horizontalLocation", horizontalDropLocation.toString()); + } + + return super.drop(drag); + } + + @Override + protected void dragAccepted(VDragEvent drag) { + currentlyValid = true; + emphasis(drag); + } + + @Override + public ComponentConnector getConnector() { + return ConnectorMap.get(client).getConnector( + VDragAndDropWrapper.this); + } + + @Override + public ApplicationConnection getApplicationConnection() { + return client; + } + + } + + protected native void hookHtml5DragStart(Element el) + /*-{ + var me = this; + el.addEventListener("dragstart", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragStart(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); + }), false); + }-*/; + + /** + * Prototype code, memory leak risk. + * + * @param el + */ + protected native void hookHtml5Events(Element el) + /*-{ + var me = this; + + el.addEventListener("dragenter", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragEnter(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); + }), false); + + el.addEventListener("dragleave", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragLeave(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); + }), false); + + el.addEventListener("dragover", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragOver(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); + }), false); + + el.addEventListener("drop", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragDrop(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); + }), false); + }-*/; + + public boolean updateDropDetails(VDragEvent drag) { + VerticalDropLocation oldVL = verticalDropLocation; + verticalDropLocation = DDUtil.getVerticalDropLocation(getElement(), + drag.getCurrentGwtEvent(), 0.2); + drag.getDropDetails().put("verticalLocation", + verticalDropLocation.toString()); + HorizontalDropLocation oldHL = horizontalDropLocation; + horizontalDropLocation = DDUtil.getHorizontalDropLocation(getElement(), + drag.getCurrentGwtEvent(), 0.2); + drag.getDropDetails().put("horizontalLocation", + horizontalDropLocation.toString()); + if (oldHL != horizontalDropLocation || oldVL != verticalDropLocation) { + return true; + } else { + return false; + } + } + + protected void deEmphasis(boolean doLayout) { + if (emphasizedVDrop != null) { + VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE, false); + VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-" + + emphasizedVDrop.toString().toLowerCase(), false); + VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-" + + emphasizedHDrop.toString().toLowerCase(), false); + } + if (doLayout) { + notifySizePotentiallyChanged(); + } + } + + private void notifySizePotentiallyChanged() { + LayoutManager.get(client).setNeedsMeasure( + ConnectorMap.get(client).getConnector(getElement())); + } + + protected void emphasis(VDragEvent drag) { + deEmphasis(false); + VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE, true); + VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-" + + verticalDropLocation.toString().toLowerCase(), true); + VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-" + + horizontalDropLocation.toString().toLowerCase(), true); + emphasizedVDrop = verticalDropLocation; + emphasizedHDrop = horizontalDropLocation; + + // TODO build (to be an example) an emphasis mode where drag image + // is fitted before or after the content + notifySizePotentiallyChanged(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapperIE.java b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapperIE.java new file mode 100644 index 0000000000..fc9829d387 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapperIE.java @@ -0,0 +1,81 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.draganddropwrapper; + +import com.google.gwt.dom.client.AnchorElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.Element; +import com.vaadin.terminal.gwt.client.VConsole; + +public class VDragAndDropWrapperIE extends VDragAndDropWrapper { + private AnchorElement anchor = null; + + @Override + protected Element getDragStartElement() { + VConsole.log("IE get drag start element..."); + Element div = getElement(); + if (dragStartMode == HTML5) { + if (anchor == null) { + anchor = Document.get().createAnchorElement(); + anchor.setHref("#"); + anchor.setClassName("drag-start"); + div.appendChild(anchor); + } + VConsole.log("IE get drag start element..."); + return (Element) anchor.cast(); + } else { + if (anchor != null) { + div.removeChild(anchor); + anchor = null; + } + return div; + } + } + + @Override + protected native void hookHtml5DragStart(Element el) + /*-{ + var me = this; + + el.attachEvent("ondragstart", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragStart(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); + })); + }-*/; + + @Override + protected native void hookHtml5Events(Element el) + /*-{ + var me = this; + + el.attachEvent("ondragenter", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragEnter(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); + })); + + el.attachEvent("ondragleave", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragLeave(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); + })); + + el.attachEvent("ondragover", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragOver(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); + })); + + el.attachEvent("ondrop", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragDrop(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); + })); + }-*/; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/embedded/EmbeddedConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/embedded/EmbeddedConnector.java new file mode 100644 index 0000000000..5c95f9f554 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/embedded/EmbeddedConnector.java @@ -0,0 +1,235 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.embedded; + +import java.util.Map; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.ObjectElement; +import com.google.gwt.dom.client.Style; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.embedded.EmbeddedConstants; +import com.vaadin.shared.ui.embedded.EmbeddedServerRpc; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.VTooltip; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector; +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler; +import com.vaadin.ui.Embedded; + +@Connect(Embedded.class) +public class EmbeddedConnector extends AbstractComponentConnector implements + Paintable { + + EmbeddedServerRpc rpc; + + @Override + protected void init() { + super.init(); + rpc = RpcProxy.create(EmbeddedServerRpc.class, this); + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (!isRealUpdate(uidl)) { + return; + } + + // Save details + getWidget().client = client; + + boolean clearBrowserElement = true; + + clickEventHandler.handleEventHandlerRegistration(); + + if (uidl.hasAttribute("type")) { + // remove old style name related to type + if (getWidget().type != null) { + getWidget().removeStyleName( + VEmbedded.CLASSNAME + "-" + getWidget().type); + } + // remove old style name related to mime type + if (getWidget().mimetype != null) { + getWidget().removeStyleName( + VEmbedded.CLASSNAME + "-" + getWidget().mimetype); + } + getWidget().type = uidl.getStringAttribute("type"); + if (getWidget().type.equals("image")) { + getWidget().addStyleName(VEmbedded.CLASSNAME + "-image"); + Element el = null; + boolean created = false; + NodeList<Node> nodes = getWidget().getElement().getChildNodes(); + if (nodes != null && nodes.getLength() == 1) { + Node n = nodes.getItem(0); + if (n.getNodeType() == Node.ELEMENT_NODE) { + Element e = (Element) n; + if (e.getTagName().equals("IMG")) { + el = e; + } + } + } + if (el == null) { + getWidget().setHTML(""); + el = DOM.createImg(); + created = true; + DOM.sinkEvents(el, Event.ONLOAD); + } + + // Set attributes + Style style = el.getStyle(); + style.setProperty("width", getState().getWidth()); + style.setProperty("height", getState().getHeight()); + + DOM.setElementProperty(el, "src", + getWidget().getSrc(uidl, client)); + + if (uidl.hasAttribute(EmbeddedConstants.ALTERNATE_TEXT)) { + el.setPropertyString( + EmbeddedConstants.ALTERNATE_TEXT, + uidl.getStringAttribute(EmbeddedConstants.ALTERNATE_TEXT)); + } + + if (created) { + // insert in dom late + getWidget().getElement().appendChild(el); + } + + /* + * Sink tooltip events so tooltip is displayed when hovering the + * image. + */ + getWidget().sinkEvents(VTooltip.TOOLTIP_EVENTS); + + } else if (getWidget().type.equals("browser")) { + getWidget().addStyleName(VEmbedded.CLASSNAME + "-browser"); + if (getWidget().browserElement == null) { + getWidget().setHTML( + "<iframe width=\"100%\" height=\"100%\" frameborder=\"0\"" + + " allowTransparency=\"true\" src=\"\"" + + " name=\"" + uidl.getId() + + "\"></iframe>"); + getWidget().browserElement = DOM.getFirstChild(getWidget() + .getElement()); + } + DOM.setElementAttribute(getWidget().browserElement, "src", + getWidget().getSrc(uidl, client)); + clearBrowserElement = false; + } else { + VConsole.log("Unknown Embedded type '" + getWidget().type + "'"); + } + } else if (uidl.hasAttribute("mimetype")) { + // remove old style name related to type + if (getWidget().type != null) { + getWidget().removeStyleName( + VEmbedded.CLASSNAME + "-" + getWidget().type); + } + // remove old style name related to mime type + if (getWidget().mimetype != null) { + getWidget().removeStyleName( + VEmbedded.CLASSNAME + "-" + getWidget().mimetype); + } + final String mime = uidl.getStringAttribute("mimetype"); + if (mime.equals("application/x-shockwave-flash")) { + getWidget().mimetype = "flash"; + // Handle embedding of Flash + getWidget().addStyleName(VEmbedded.CLASSNAME + "-flash"); + getWidget().setHTML(getWidget().createFlashEmbed(uidl)); + + } else if (mime.equals("image/svg+xml")) { + getWidget().mimetype = "svg"; + getWidget().addStyleName(VEmbedded.CLASSNAME + "-svg"); + String data; + Map<String, String> parameters = VEmbedded.getParameters(uidl); + if (parameters.get("data") == null) { + data = getWidget().getSrc(uidl, client); + } else { + data = "data:image/svg+xml," + parameters.get("data"); + } + getWidget().setHTML(""); + ObjectElement obj = Document.get().createObjectElement(); + obj.setType(mime); + obj.setData(data); + if (!isUndefinedWidth()) { + obj.getStyle().setProperty("width", "100%"); + } + if (!isUndefinedHeight()) { + obj.getStyle().setProperty("height", "100%"); + } + if (uidl.hasAttribute("classid")) { + obj.setAttribute("classid", + uidl.getStringAttribute("classid")); + } + if (uidl.hasAttribute("codebase")) { + obj.setAttribute("codebase", + uidl.getStringAttribute("codebase")); + } + if (uidl.hasAttribute("codetype")) { + obj.setAttribute("codetype", + uidl.getStringAttribute("codetype")); + } + if (uidl.hasAttribute("archive")) { + obj.setAttribute("archive", + uidl.getStringAttribute("archive")); + } + if (uidl.hasAttribute("standby")) { + obj.setAttribute("standby", + uidl.getStringAttribute("standby")); + } + getWidget().getElement().appendChild(obj); + if (uidl.hasAttribute(EmbeddedConstants.ALTERNATE_TEXT)) { + obj.setInnerText(uidl + .getStringAttribute(EmbeddedConstants.ALTERNATE_TEXT)); + } + } else { + VConsole.log("Unknown Embedded mimetype '" + mime + "'"); + } + } else { + VConsole.log("Unknown Embedded; no type or mimetype attribute"); + } + + if (clearBrowserElement) { + getWidget().browserElement = null; + } + } + + @Override + public VEmbedded getWidget() { + return (VEmbedded) super.getWidget(); + } + + protected final ClickEventHandler clickEventHandler = new ClickEventHandler( + this) { + + @Override + protected void fireClick(NativeEvent event, + MouseEventDetails mouseDetails) { + rpc.click(mouseDetails); + } + + }; + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/embedded/VEmbedded.java b/client/src/com/vaadin/terminal/gwt/client/ui/embedded/VEmbedded.java new file mode 100644 index 0000000000..dca4686a5c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/embedded/VEmbedded.java @@ -0,0 +1,251 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.embedded; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +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.ui.HTML; +import com.vaadin.shared.ui.embedded.EmbeddedConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; + +public class VEmbedded extends HTML { + public static String CLASSNAME = "v-embedded"; + + protected Element browserElement; + + protected String type; + protected String mimetype; + + protected ApplicationConnection client; + + public VEmbedded() { + setStyleName(CLASSNAME); + } + + /** + * Creates the Object and Embed tags for the Flash plugin so it works + * cross-browser + * + * @param uidl + * The UIDL + * @return Tags concatenated into a string + */ + protected String createFlashEmbed(UIDL uidl) { + /* + * To ensure cross-browser compatibility we are using the twice-cooked + * method to embed flash i.e. we add a OBJECT tag for IE ActiveX and + * inside it a EMBED for all other browsers. + */ + + StringBuilder html = new StringBuilder(); + + // Start the object tag + html.append("<object "); + + /* + * Add classid required for ActiveX to recognize the flash. This is a + * predefined value which ActiveX recognizes and must be the given + * value. More info can be found on + * http://kb2.adobe.com/cps/415/tn_4150.html. Allow user to override + * this by setting his own classid. + */ + if (uidl.hasAttribute("classid")) { + html.append("classid=\"" + + Util.escapeAttribute(uidl.getStringAttribute("classid")) + + "\" "); + } else { + html.append("classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" "); + } + + /* + * Add codebase required for ActiveX and must be exactly this according + * to http://kb2.adobe.com/cps/415/tn_4150.html to work with the above + * given classid. Again, see more info on + * http://kb2.adobe.com/cps/415/tn_4150.html. Limiting Flash version to + * 6.0.0.0 and above. Allow user to override this by setting his own + * codebase + */ + if (uidl.hasAttribute("codebase")) { + html.append("codebase=\"" + + Util.escapeAttribute(uidl.getStringAttribute("codebase")) + + "\" "); + } else { + html.append("codebase=\"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0\" "); + } + + ComponentConnector paintable = ConnectorMap.get(client).getConnector( + this); + String height = paintable.getState().getHeight(); + String width = paintable.getState().getWidth(); + + // Add width and height + html.append("width=\"" + Util.escapeAttribute(width) + "\" "); + html.append("height=\"" + Util.escapeAttribute(height) + "\" "); + html.append("type=\"application/x-shockwave-flash\" "); + + // Codetype + if (uidl.hasAttribute("codetype")) { + html.append("codetype=\"" + + Util.escapeAttribute(uidl.getStringAttribute("codetype")) + + "\" "); + } + + // Standby + if (uidl.hasAttribute("standby")) { + html.append("standby=\"" + + Util.escapeAttribute(uidl.getStringAttribute("standby")) + + "\" "); + } + + // Archive + if (uidl.hasAttribute("archive")) { + html.append("archive=\"" + + Util.escapeAttribute(uidl.getStringAttribute("archive")) + + "\" "); + } + + // End object tag + html.append(">"); + + // Ensure we have an movie parameter + Map<String, String> parameters = getParameters(uidl); + if (parameters.get("movie") == null) { + parameters.put("movie", getSrc(uidl, client)); + } + + // Add parameters to OBJECT + for (String name : parameters.keySet()) { + html.append("<param "); + html.append("name=\"" + Util.escapeAttribute(name) + "\" "); + html.append("value=\"" + Util.escapeAttribute(parameters.get(name)) + + "\" "); + html.append("/>"); + } + + // Build inner EMBED tag + html.append("<embed "); + html.append("src=\"" + Util.escapeAttribute(getSrc(uidl, client)) + + "\" "); + html.append("width=\"" + Util.escapeAttribute(width) + "\" "); + html.append("height=\"" + Util.escapeAttribute(height) + "\" "); + html.append("type=\"application/x-shockwave-flash\" "); + + // Add the parameters to the Embed + for (String name : parameters.keySet()) { + html.append(Util.escapeAttribute(name)); + html.append("="); + html.append("\"" + Util.escapeAttribute(parameters.get(name)) + + "\""); + } + + // End embed tag + html.append("></embed>"); + + if (uidl.hasAttribute(EmbeddedConstants.ALTERNATE_TEXT)) { + html.append(uidl + .getStringAttribute(EmbeddedConstants.ALTERNATE_TEXT)); + } + + // End object tag + html.append("</object>"); + + return html.toString(); + } + + /** + * Returns a map (name -> value) of all parameters in the UIDL. + * + * @param uidl + * @return + */ + protected static Map<String, String> getParameters(UIDL uidl) { + Map<String, String> parameters = new HashMap<String, String>(); + + Iterator<Object> childIterator = uidl.getChildIterator(); + while (childIterator.hasNext()) { + + Object child = childIterator.next(); + if (child instanceof UIDL) { + + UIDL childUIDL = (UIDL) child; + if (childUIDL.getTag().equals("embeddedparam")) { + String name = childUIDL.getStringAttribute("name"); + String value = childUIDL.getStringAttribute("value"); + parameters.put(name, value); + } + } + + } + + return parameters; + } + + /** + * Helper to return translated src-attribute from embedded's UIDL + * + * @param uidl + * @param client + * @return + */ + protected String getSrc(UIDL uidl, ApplicationConnection client) { + String url = client.translateVaadinUri(uidl.getStringAttribute("src")); + if (url == null) { + return ""; + } + return url; + } + + @Override + protected void onDetach() { + if (BrowserInfo.get().isIE()) { + // Force browser to fire unload event when component is detached + // from the view (IE doesn't do this automatically) + if (browserElement != null) { + /* + * src was previously set to javascript:false, but this was not + * enough to overcome a bug when detaching an iframe with a pdf + * loaded in IE9. about:blank seems to cause the adobe reader + * plugin to unload properly before the iframe is removed. See + * #7855 + */ + DOM.setElementAttribute(browserElement, "src", "about:blank"); + } + } + super.onDetach(); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (DOM.eventGetType(event) == Event.ONLOAD) { + VConsole.log("Embeddable onload"); + Util.notifyParentOfSizeChange(this, true); + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/form/FormConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/form/FormConnector.java new file mode 100644 index 0000000000..a38a67f84b --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/form/FormConnector.java @@ -0,0 +1,219 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.form; + +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.form.FormState; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ui.Icon; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler; +import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeEvent; +import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeListener; +import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren; +import com.vaadin.ui.Form; + +@Connect(Form.class) +public class FormConnector extends AbstractComponentContainerConnector + implements Paintable, MayScrollChildren { + + private final ElementResizeListener footerResizeListener = new ElementResizeListener() { + @Override + public void onElementResize(ElementResizeEvent e) { + VForm form = getWidget(); + + int footerHeight; + if (form.footer != null) { + LayoutManager lm = getLayoutManager(); + footerHeight = lm.getOuterHeight(form.footer.getElement()); + } else { + footerHeight = 0; + } + + form.fieldContainer.getStyle().setPaddingBottom(footerHeight, + Unit.PX); + form.footerContainer.getStyle() + .setMarginTop(-footerHeight, Unit.PX); + } + }; + + @Override + public void onUnregister() { + VForm form = getWidget(); + if (form.footer != null) { + getLayoutManager().removeElementResizeListener( + form.footer.getElement(), footerResizeListener); + } + } + + @Override + public boolean delegateCaptionHandling() { + return false; + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + getWidget().client = client; + getWidget().id = uidl.getId(); + + if (!isRealUpdate(uidl)) { + return; + } + + boolean legendEmpty = true; + if (getState().getCaption() != null) { + getWidget().caption.setInnerText(getState().getCaption()); + legendEmpty = false; + } else { + getWidget().caption.setInnerText(""); + } + if (getState().getIcon() != null) { + if (getWidget().icon == null) { + getWidget().icon = new Icon(client); + getWidget().legend.insertFirst(getWidget().icon.getElement()); + } + getWidget().icon.setUri(getState().getIcon().getURL()); + legendEmpty = false; + } else { + if (getWidget().icon != null) { + getWidget().legend.removeChild(getWidget().icon.getElement()); + } + } + if (legendEmpty) { + getWidget().addStyleDependentName("nocaption"); + } else { + getWidget().removeStyleDependentName("nocaption"); + } + + if (null != getState().getErrorMessage()) { + getWidget().errorMessage + .updateMessage(getState().getErrorMessage()); + getWidget().errorMessage.setVisible(true); + } else { + getWidget().errorMessage.setVisible(false); + } + + if (getState().hasDescription()) { + getWidget().desc.setInnerHTML(getState().getDescription()); + if (getWidget().desc.getParentElement() == null) { + getWidget().fieldSet.insertAfter(getWidget().desc, + getWidget().legend); + } + } else { + getWidget().desc.setInnerHTML(""); + if (getWidget().desc.getParentElement() != null) { + getWidget().fieldSet.removeChild(getWidget().desc); + } + } + + // first render footer so it will be easier to handle relative height of + // main layout + if (getState().getFooter() != null) { + // render footer + ComponentConnector newFooter = (ComponentConnector) getState() + .getFooter(); + Widget newFooterWidget = newFooter.getWidget(); + if (getWidget().footer == null) { + getLayoutManager().addElementResizeListener( + newFooterWidget.getElement(), footerResizeListener); + getWidget().add(newFooter.getWidget(), + getWidget().footerContainer); + getWidget().footer = newFooterWidget; + } else if (newFooter != getWidget().footer) { + getLayoutManager().removeElementResizeListener( + getWidget().footer.getElement(), footerResizeListener); + getLayoutManager().addElementResizeListener( + newFooterWidget.getElement(), footerResizeListener); + getWidget().remove(getWidget().footer); + getWidget().add(newFooter.getWidget(), + getWidget().footerContainer); + } + getWidget().footer = newFooterWidget; + } else { + if (getWidget().footer != null) { + getLayoutManager().removeElementResizeListener( + getWidget().footer.getElement(), footerResizeListener); + getWidget().remove(getWidget().footer); + getWidget().footer = null; + } + } + + ComponentConnector newLayout = (ComponentConnector) getState() + .getLayout(); + Widget newLayoutWidget = newLayout.getWidget(); + if (getWidget().lo == null) { + // Layout not rendered before + getWidget().lo = newLayoutWidget; + getWidget().add(newLayoutWidget, getWidget().fieldContainer); + } else if (getWidget().lo != newLayoutWidget) { + // Layout has changed + getWidget().remove(getWidget().lo); + getWidget().lo = newLayoutWidget; + getWidget().add(newLayoutWidget, getWidget().fieldContainer); + } + + // also recalculates size of the footer if undefined size form - see + // #3710 + client.runDescendentsLayout(getWidget()); + + // We may have actions attached + if (uidl.getChildCount() >= 1) { + UIDL childUidl = uidl.getChildByTagName("actions"); + if (childUidl != null) { + if (getWidget().shortcutHandler == null) { + getWidget().shortcutHandler = new ShortcutActionHandler( + getConnectorId(), client); + getWidget().keyDownRegistration = getWidget() + .addDomHandler(getWidget(), KeyDownEvent.getType()); + } + getWidget().shortcutHandler.updateActionMap(childUidl); + } + } else if (getWidget().shortcutHandler != null) { + getWidget().keyDownRegistration.removeHandler(); + getWidget().shortcutHandler = null; + getWidget().keyDownRegistration = null; + } + } + + @Override + public void updateCaption(ComponentConnector component) { + // NOP form don't render caption for neither field layout nor footer + // layout + } + + @Override + public VForm getWidget() { + return (VForm) super.getWidget(); + } + + @Override + public boolean isReadOnly() { + return super.isReadOnly() || getState().isPropertyReadOnly(); + } + + @Override + public FormState getState() { + return (FormState) super.getState(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/form/VForm.java b/client/src/com/vaadin/terminal/gwt/client/ui/form/VForm.java new file mode 100644 index 0000000000..8cd7139eed --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/form/VForm.java @@ -0,0 +1,88 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.form; + +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.shared.HandlerRegistration; +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.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.VErrorMessage; +import com.vaadin.terminal.gwt.client.ui.Icon; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler; + +public class VForm extends ComplexPanel implements KeyDownHandler { + + protected String id; + + public static final String CLASSNAME = "v-form"; + + Widget lo; + Element legend = DOM.createLegend(); + Element caption = DOM.createSpan(); + Element desc = DOM.createDiv(); + Icon icon; + VErrorMessage errorMessage = new VErrorMessage(); + + Element fieldContainer = DOM.createDiv(); + + Element footerContainer = DOM.createDiv(); + + Element fieldSet = DOM.createFieldSet(); + + Widget footer; + + ApplicationConnection client; + + ShortcutActionHandler shortcutHandler; + + HandlerRegistration keyDownRegistration; + + public VForm() { + setElement(DOM.createDiv()); + getElement().appendChild(fieldSet); + setStyleName(CLASSNAME); + fieldSet.appendChild(legend); + legend.appendChild(caption); + desc.setClassName("v-form-description"); + fieldSet.appendChild(desc); // Adding description for initial padding + // measurements, removed later if no + // description is set + fieldContainer.setClassName(CLASSNAME + "-content"); + fieldSet.appendChild(fieldContainer); + errorMessage.setVisible(false); + errorMessage.setStyleName(CLASSNAME + "-errormessage"); + fieldSet.appendChild(errorMessage.getElement()); + fieldSet.appendChild(footerContainer); + } + + @Override + public void onKeyDown(KeyDownEvent event) { + shortcutHandler.handleKeyboardEvent(Event.as(event.getNativeEvent())); + } + + @Override + protected void add(Widget child, Element container) { + // Overridden to allow VFormPaintable to call this. Should be removed + // once functionality from VFormPaintable is moved to VForm. + super.add(child, container); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/FormLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/FormLayoutConnector.java new file mode 100644 index 0000000000..7d5edfadf5 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/FormLayoutConnector.java @@ -0,0 +1,147 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.formlayout; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.VMarginInfo; +import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.TooltipInfo; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector; +import com.vaadin.terminal.gwt.client.ui.AbstractLayoutConnector; +import com.vaadin.terminal.gwt.client.ui.formlayout.VFormLayout.Caption; +import com.vaadin.terminal.gwt.client.ui.formlayout.VFormLayout.ErrorFlag; +import com.vaadin.terminal.gwt.client.ui.formlayout.VFormLayout.VFormLayoutTable; +import com.vaadin.ui.FormLayout; + +@Connect(FormLayout.class) +public class FormLayoutConnector extends AbstractLayoutConnector { + + @Override + public AbstractOrderedLayoutState getState() { + return (AbstractOrderedLayoutState) super.getState(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + VFormLayoutTable formLayoutTable = getWidget().table; + + formLayoutTable.setMargins(new VMarginInfo(getState() + .getMarginsBitmask())); + formLayoutTable.setSpacing(getState().isSpacing()); + + } + + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + super.onConnectorHierarchyChange(event); + + VFormLayout formLayout = getWidget(); + VFormLayoutTable formLayoutTable = getWidget().table; + + int childId = 0; + + formLayoutTable.setRowCount(getChildComponents().size()); + + for (ComponentConnector child : getChildComponents()) { + Widget childWidget = child.getWidget(); + + Caption caption = formLayoutTable.getCaption(childWidget); + if (caption == null) { + caption = formLayout.new Caption(child); + caption.addClickHandler(formLayoutTable); + } + + ErrorFlag error = formLayoutTable.getError(childWidget); + if (error == null) { + error = formLayout.new ErrorFlag(child); + } + + formLayoutTable.setChild(childId, childWidget, caption, error); + childId++; + } + + for (ComponentConnector oldChild : event.getOldChildren()) { + if (oldChild.getParent() == this) { + continue; + } + + formLayoutTable.cleanReferences(oldChild.getWidget()); + } + + } + + @Override + public void updateCaption(ComponentConnector component) { + getWidget().table.updateCaption(component.getWidget(), + component.getState(), component.isEnabled()); + boolean hideErrors = false; + + // FIXME This incorrectly depends on AbstractFieldConnector + if (component instanceof AbstractFieldConnector) { + hideErrors = ((AbstractFieldConnector) component).getState() + .isHideErrors(); + } + + getWidget().table.updateError(component.getWidget(), component + .getState().getErrorMessage(), hideErrors); + } + + @Override + public VFormLayout getWidget() { + return (VFormLayout) super.getWidget(); + } + + @Override + public TooltipInfo getTooltipInfo(Element element) { + TooltipInfo info = null; + + if (element != getWidget().getElement()) { + Object node = Util.findWidget( + (com.google.gwt.user.client.Element) element, + VFormLayout.Caption.class); + + if (node != null) { + VFormLayout.Caption caption = (VFormLayout.Caption) node; + info = caption.getOwner().getTooltipInfo(element); + } else { + + node = Util.findWidget( + (com.google.gwt.user.client.Element) element, + VFormLayout.ErrorFlag.class); + + if (node != null) { + VFormLayout.ErrorFlag flag = (VFormLayout.ErrorFlag) node; + info = flag.getOwner().getTooltipInfo(element); + } + } + } + + if (info == null) { + info = super.getTooltipInfo(element); + } + + return info; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/VFormLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/VFormLayout.java new file mode 100644 index 0000000000..d3ce6f3d3f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/VFormLayout.java @@ -0,0 +1,378 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.formlayout; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.FlexTable; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ComponentState; +import com.vaadin.shared.ui.VMarginInfo; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.StyleConstants; +import com.vaadin.terminal.gwt.client.VTooltip; +import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector; +import com.vaadin.terminal.gwt.client.ui.Icon; + +/** + * Two col Layout that places caption on left col and field on right col + */ +public class VFormLayout extends SimplePanel { + + private final static String CLASSNAME = "v-formlayout"; + + VFormLayoutTable table; + + public VFormLayout() { + super(); + setStyleName(CLASSNAME); + table = new VFormLayoutTable(); + setWidget(table); + } + + /** + * Parses the stylenames from shared state + * + * @param state + * shared state of the component + * @param enabled + * @return An array of stylenames + */ + private String[] getStylesFromState(ComponentState state, boolean enabled) { + List<String> styles = new ArrayList<String>(); + if (state.hasStyles()) { + for (String name : state.getStyles()) { + styles.add(name); + } + } + + if (!enabled) { + styles.add(ApplicationConnection.DISABLED_CLASSNAME); + } + + return styles.toArray(new String[styles.size()]); + } + + public class VFormLayoutTable extends FlexTable implements ClickHandler { + + private static final int COLUMN_CAPTION = 0; + private static final int COLUMN_ERRORFLAG = 1; + private static final int COLUMN_WIDGET = 2; + + private HashMap<Widget, Caption> widgetToCaption = new HashMap<Widget, Caption>(); + private HashMap<Widget, ErrorFlag> widgetToError = new HashMap<Widget, ErrorFlag>(); + + public VFormLayoutTable() { + DOM.setElementProperty(getElement(), "cellPadding", "0"); + DOM.setElementProperty(getElement(), "cellSpacing", "0"); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt + * .event.dom.client.ClickEvent) + */ + @Override + public void onClick(ClickEvent event) { + Caption caption = (Caption) event.getSource(); + if (caption.getOwner() != null) { + if (caption.getOwner() instanceof Focusable) { + ((Focusable) caption.getOwner()).focus(); + } else if (caption.getOwner() instanceof com.google.gwt.user.client.ui.Focusable) { + ((com.google.gwt.user.client.ui.Focusable) caption + .getOwner()).setFocus(true); + } + } + } + + public void setMargins(VMarginInfo margins) { + Element margin = getElement(); + setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_TOP, + margins.hasTop()); + setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_RIGHT, + margins.hasRight()); + setStyleName(margin, + CLASSNAME + "-" + StyleConstants.MARGIN_BOTTOM, + margins.hasBottom()); + setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_LEFT, + margins.hasLeft()); + + } + + public void setSpacing(boolean spacing) { + setStyleName(getElement(), CLASSNAME + "-" + "spacing", spacing); + + } + + public void setRowCount(int rowNr) { + for (int i = 0; i < rowNr; i++) { + prepareCell(i, COLUMN_CAPTION); + getCellFormatter().setStyleName(i, COLUMN_CAPTION, + CLASSNAME + "-captioncell"); + + prepareCell(i, 1); + getCellFormatter().setStyleName(i, COLUMN_ERRORFLAG, + CLASSNAME + "-errorcell"); + + prepareCell(i, 2); + getCellFormatter().setStyleName(i, COLUMN_WIDGET, + CLASSNAME + "-contentcell"); + + String rowstyles = CLASSNAME + "-row"; + if (i == 0) { + rowstyles += " " + CLASSNAME + "-firstrow"; + } + if (i == rowNr - 1) { + rowstyles += " " + CLASSNAME + "-lastrow"; + } + + getRowFormatter().setStyleName(i, rowstyles); + + } + while (getRowCount() != rowNr) { + removeRow(rowNr); + } + } + + public void setChild(int rowNr, Widget childWidget, Caption caption, + ErrorFlag error) { + setWidget(rowNr, COLUMN_WIDGET, childWidget); + setWidget(rowNr, COLUMN_CAPTION, caption); + setWidget(rowNr, COLUMN_ERRORFLAG, error); + + widgetToCaption.put(childWidget, caption); + widgetToError.put(childWidget, error); + + } + + public Caption getCaption(Widget childWidget) { + return widgetToCaption.get(childWidget); + } + + public ErrorFlag getError(Widget childWidget) { + return widgetToError.get(childWidget); + } + + public void cleanReferences(Widget oldChildWidget) { + widgetToError.remove(oldChildWidget); + widgetToCaption.remove(oldChildWidget); + + } + + public void updateCaption(Widget widget, ComponentState state, + boolean enabled) { + final Caption c = widgetToCaption.get(widget); + if (c != null) { + c.updateCaption(state, enabled); + } + } + + public void updateError(Widget widget, String errorMessage, + boolean hideErrors) { + final ErrorFlag e = widgetToError.get(widget); + if (e != null) { + e.updateError(errorMessage, hideErrors); + } + + } + + } + + // TODO why duplicated here? + public class Caption extends HTML { + + public static final String CLASSNAME = "v-caption"; + + private final ComponentConnector owner; + + private Element requiredFieldIndicator; + + private Icon icon; + + private Element captionText; + + /** + * + * @param component + * optional owner of caption. If not set, getOwner will + * return null + */ + public Caption(ComponentConnector component) { + super(); + owner = component; + } + + private void setStyles(String[] styles) { + String styleName = CLASSNAME; + + if (styles != null) { + for (String style : styles) { + if (ApplicationConnection.DISABLED_CLASSNAME.equals(style)) { + // Add v-disabled also without classname prefix so + // generic v-disabled CSS rules work + styleName += " " + style; + } + + styleName += " " + CLASSNAME + "-" + style; + } + } + + setStyleName(styleName); + } + + public void updateCaption(ComponentState state, boolean enabled) { + // Update styles as they might have changed when the caption changed + setStyles(getStylesFromState(state, enabled)); + + boolean isEmpty = true; + + if (state.getIcon() != null) { + if (icon == null) { + icon = new Icon(owner.getConnection()); + + DOM.insertChild(getElement(), icon.getElement(), 0); + } + icon.setUri(state.getIcon().getURL()); + isEmpty = false; + } else { + if (icon != null) { + DOM.removeChild(getElement(), icon.getElement()); + icon = null; + } + + } + + if (state.getCaption() != null) { + if (captionText == null) { + captionText = DOM.createSpan(); + DOM.insertChild(getElement(), captionText, icon == null ? 0 + : 1); + } + String c = state.getCaption(); + if (c == null) { + c = ""; + } else { + isEmpty = false; + } + DOM.setInnerText(captionText, c); + } else { + // TODO should span also be removed + } + + if (state.hasDescription() && captionText != null) { + addStyleDependentName("hasdescription"); + } else { + removeStyleDependentName("hasdescription"); + } + + boolean required = owner instanceof AbstractFieldConnector + && ((AbstractFieldConnector) owner).isRequired(); + if (required) { + if (requiredFieldIndicator == null) { + requiredFieldIndicator = DOM.createSpan(); + DOM.setInnerText(requiredFieldIndicator, "*"); + DOM.setElementProperty(requiredFieldIndicator, "className", + "v-required-field-indicator"); + DOM.appendChild(getElement(), requiredFieldIndicator); + } + } else { + if (requiredFieldIndicator != null) { + DOM.removeChild(getElement(), requiredFieldIndicator); + requiredFieldIndicator = null; + } + } + + // Workaround for IE weirdness, sometimes returns bad height in some + // circumstances when Caption is empty. See #1444 + // IE7 bugs more often. I wonder what happens when IE8 arrives... + // FIXME: This could be unnecessary for IE8+ + if (BrowserInfo.get().isIE()) { + if (isEmpty) { + setHeight("0px"); + DOM.setStyleAttribute(getElement(), "overflow", "hidden"); + } else { + setHeight(""); + DOM.setStyleAttribute(getElement(), "overflow", ""); + } + + } + + } + + /** + * Returns Paintable for which this Caption belongs to. + * + * @return owner Widget + */ + public ComponentConnector getOwner() { + return owner; + } + } + + class ErrorFlag extends HTML { + private static final String CLASSNAME = VFormLayout.CLASSNAME + + "-error-indicator"; + Element errorIndicatorElement; + + private ComponentConnector owner; + + public ErrorFlag(ComponentConnector owner) { + setStyleName(CLASSNAME); + sinkEvents(VTooltip.TOOLTIP_EVENTS); + this.owner = owner; + } + + public ComponentConnector getOwner() { + return owner; + } + + public void updateError(String errorMessage, boolean hideErrors) { + boolean showError = null != errorMessage; + if (hideErrors) { + showError = false; + } + + if (showError) { + if (errorIndicatorElement == null) { + errorIndicatorElement = DOM.createDiv(); + DOM.setInnerHTML(errorIndicatorElement, " "); + DOM.setElementProperty(errorIndicatorElement, "className", + "v-errorindicator"); + DOM.appendChild(getElement(), errorIndicatorElement); + } + + } else if (errorIndicatorElement != null) { + DOM.removeChild(getElement(), errorIndicatorElement); + errorIndicatorElement = null; + } + } + + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/GridLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/GridLayoutConnector.java new file mode 100644 index 0000000000..520afb778d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/GridLayoutConnector.java @@ -0,0 +1,252 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.gridlayout; + +import java.util.Iterator; + +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.AlignmentInfo; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.LayoutClickRpc; +import com.vaadin.shared.ui.VMarginInfo; +import com.vaadin.shared.ui.gridlayout.GridLayoutServerRpc; +import com.vaadin.shared.ui.gridlayout.GridLayoutState; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.DirectionalManagedLayout; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; +import com.vaadin.terminal.gwt.client.ui.gridlayout.VGridLayout.Cell; +import com.vaadin.terminal.gwt.client.ui.layout.VLayoutSlot; +import com.vaadin.ui.GridLayout; + +@Connect(GridLayout.class) +public class GridLayoutConnector extends AbstractComponentContainerConnector + implements Paintable, DirectionalManagedLayout { + + private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler( + this) { + + @Override + protected ComponentConnector getChildComponent(Element element) { + return getWidget().getComponent(element); + } + + @Override + protected LayoutClickRpc getLayoutClickRPC() { + return rpc; + }; + + }; + + private GridLayoutServerRpc rpc; + private boolean needCaptionUpdate = false; + + @Override + public void init() { + super.init(); + rpc = RpcProxy.create(GridLayoutServerRpc.class, this); + getLayoutManager().registerDependency(this, + getWidget().spacingMeasureElement); + } + + @Override + public void onUnregister() { + VGridLayout layout = getWidget(); + getLayoutManager().unregisterDependency(this, + layout.spacingMeasureElement); + + // Unregister caption size dependencies + for (ComponentConnector child : getChildComponents()) { + Cell cell = layout.widgetToCell.get(child.getWidget()); + cell.slot.setCaption(null); + } + } + + @Override + public GridLayoutState getState() { + return (GridLayoutState) super.getState(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + clickEventHandler.handleEventHandlerRegistration(); + + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + VGridLayout layout = getWidget(); + layout.client = client; + + if (!isRealUpdate(uidl)) { + return; + } + + int cols = getState().getColumns(); + int rows = getState().getRows(); + + layout.columnWidths = new int[cols]; + layout.rowHeights = new int[rows]; + + layout.setSize(rows, cols); + + final int[] alignments = uidl.getIntArrayAttribute("alignments"); + int alignmentIndex = 0; + + for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) { + final UIDL r = (UIDL) i.next(); + if ("gr".equals(r.getTag())) { + for (final Iterator<?> j = r.getChildIterator(); j.hasNext();) { + final UIDL cellUidl = (UIDL) j.next(); + if ("gc".equals(cellUidl.getTag())) { + int row = cellUidl.getIntAttribute("y"); + int col = cellUidl.getIntAttribute("x"); + + Widget previousWidget = null; + + Cell cell = layout.getCell(row, col); + if (cell != null && cell.slot != null) { + // This is an update. Track if the widget changes + // and update the caption if that happens. This + // workaround can be removed once the DOM update is + // done in onContainerHierarchyChange + previousWidget = cell.slot.getWidget(); + } + + cell = layout.createCell(row, col); + + cell.updateFromUidl(cellUidl); + + if (cell.hasContent()) { + cell.setAlignment(new AlignmentInfo( + alignments[alignmentIndex++])); + if (cell.slot.getWidget() != previousWidget) { + // Widget changed or widget moved from another + // slot. Update its caption as the widget might + // have called updateCaption when the widget was + // still in its old slot. This workaround can be + // removed once the DOM update + // is done in onContainerHierarchyChange + updateCaption(ConnectorMap.get(getConnection()) + .getConnector(cell.slot.getWidget())); + } + } + } + } + } + } + + layout.colExpandRatioArray = uidl.getIntArrayAttribute("colExpand"); + layout.rowExpandRatioArray = uidl.getIntArrayAttribute("rowExpand"); + + layout.updateMarginStyleNames(new VMarginInfo(getState() + .getMarginsBitmask())); + + layout.updateSpacingStyleName(getState().isSpacing()); + + if (needCaptionUpdate) { + needCaptionUpdate = false; + + for (ComponentConnector child : getChildComponents()) { + updateCaption(child); + } + } + getLayoutManager().setNeedsLayout(this); + } + + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + super.onConnectorHierarchyChange(event); + + VGridLayout layout = getWidget(); + + // clean non rendered components + for (ComponentConnector oldChild : event.getOldChildren()) { + if (oldChild.getParent() == this) { + continue; + } + + Widget childWidget = oldChild.getWidget(); + layout.remove(childWidget); + + Cell cell = layout.widgetToCell.remove(childWidget); + cell.slot.setCaption(null); + cell.slot.getWrapperElement().removeFromParent(); + cell.slot = null; + } + + } + + @Override + public void updateCaption(ComponentConnector childConnector) { + if (!childConnector.delegateCaptionHandling()) { + // Check not required by interface but by workarounds in this class + // when updateCaption is explicitly called for all children. + return; + } + + VGridLayout layout = getWidget(); + Cell cell = layout.widgetToCell.get(childConnector.getWidget()); + if (cell == null) { + // workaround before updateFromUidl is removed. We currently update + // the captions at the end of updateFromUidl instead of immediately + // because the DOM has not been set up at this point (as it is done + // in updateFromUidl) + needCaptionUpdate = true; + return; + } + if (VCaption.isNeeded(childConnector.getState())) { + VLayoutSlot layoutSlot = cell.slot; + VCaption caption = layoutSlot.getCaption(); + if (caption == null) { + caption = new VCaption(childConnector, getConnection()); + + Widget widget = childConnector.getWidget(); + + layout.setCaption(widget, caption); + } + caption.updateCaption(); + } else { + layout.setCaption(childConnector.getWidget(), null); + } + } + + @Override + public VGridLayout getWidget() { + return (VGridLayout) super.getWidget(); + } + + @Override + public void layoutVertically() { + getWidget().updateHeight(); + } + + @Override + public void layoutHorizontally() { + getWidget().updateWidth(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/VGridLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/VGridLayout.java new file mode 100644 index 0000000000..25d7de6ee6 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/VGridLayout.java @@ -0,0 +1,714 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.gridlayout; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.AlignmentInfo; +import com.vaadin.shared.ui.VMarginInfo; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.ui.layout.ComponentConnectorLayoutSlot; +import com.vaadin.terminal.gwt.client.ui.layout.VLayoutSlot; + +public class VGridLayout extends ComplexPanel { + + public static final String CLASSNAME = "v-gridlayout"; + + ApplicationConnection client; + + HashMap<Widget, Cell> widgetToCell = new HashMap<Widget, Cell>(); + + int[] columnWidths; + int[] rowHeights; + + int[] colExpandRatioArray; + + int[] rowExpandRatioArray; + + int[] minColumnWidths; + + private int[] minRowHeights; + + DivElement spacingMeasureElement; + + public VGridLayout() { + super(); + setElement(Document.get().createDivElement()); + + spacingMeasureElement = Document.get().createDivElement(); + Style spacingStyle = spacingMeasureElement.getStyle(); + spacingStyle.setPosition(Position.ABSOLUTE); + getElement().appendChild(spacingMeasureElement); + + setStyleName(CLASSNAME); + } + + private GridLayoutConnector getConnector() { + return (GridLayoutConnector) ConnectorMap.get(client) + .getConnector(this); + } + + /** + * Returns the column widths measured in pixels + * + * @return + */ + protected int[] getColumnWidths() { + return columnWidths; + } + + /** + * Returns the row heights measured in pixels + * + * @return + */ + protected int[] getRowHeights() { + return rowHeights; + } + + /** + * Returns the spacing between the cells horizontally in pixels + * + * @return + */ + protected int getHorizontalSpacing() { + return LayoutManager.get(client).getOuterWidth(spacingMeasureElement); + } + + /** + * Returns the spacing between the cells vertically in pixels + * + * @return + */ + protected int getVerticalSpacing() { + return LayoutManager.get(client).getOuterHeight(spacingMeasureElement); + } + + static int[] cloneArray(int[] toBeCloned) { + int[] clone = new int[toBeCloned.length]; + for (int i = 0; i < clone.length; i++) { + clone[i] = toBeCloned[i] * 1; + } + return clone; + } + + void expandRows() { + if (!isUndefinedHeight()) { + int usedSpace = minRowHeights[0]; + int verticalSpacing = getVerticalSpacing(); + for (int i = 1; i < minRowHeights.length; i++) { + usedSpace += verticalSpacing + minRowHeights[i]; + } + int availableSpace = LayoutManager.get(client).getInnerHeight( + getElement()); + int excessSpace = availableSpace - usedSpace; + int distributed = 0; + if (excessSpace > 0) { + for (int i = 0; i < rowHeights.length; i++) { + int ew = excessSpace * rowExpandRatioArray[i] / 1000; + rowHeights[i] = minRowHeights[i] + ew; + distributed += ew; + } + excessSpace -= distributed; + int c = 0; + while (excessSpace > 0) { + rowHeights[c % rowHeights.length]++; + excessSpace--; + c++; + } + } + } + } + + void updateHeight() { + // Detect minimum heights & calculate spans + detectRowHeights(); + + // Expand + expandRows(); + + // Position + layoutCellsVertically(); + } + + void updateWidth() { + // Detect widths & calculate spans + detectColWidths(); + // Expand + expandColumns(); + // Position + layoutCellsHorizontally(); + + } + + void expandColumns() { + if (!isUndefinedWidth()) { + int usedSpace = minColumnWidths[0]; + int horizontalSpacing = getHorizontalSpacing(); + for (int i = 1; i < minColumnWidths.length; i++) { + usedSpace += horizontalSpacing + minColumnWidths[i]; + } + + int availableSpace = LayoutManager.get(client).getInnerWidth( + getElement()); + int excessSpace = availableSpace - usedSpace; + int distributed = 0; + if (excessSpace > 0) { + for (int i = 0; i < columnWidths.length; i++) { + int ew = excessSpace * colExpandRatioArray[i] / 1000; + columnWidths[i] = minColumnWidths[i] + ew; + distributed += ew; + } + excessSpace -= distributed; + int c = 0; + while (excessSpace > 0) { + columnWidths[c % columnWidths.length]++; + excessSpace--; + c++; + } + } + } + } + + void layoutCellsVertically() { + int verticalSpacing = getVerticalSpacing(); + LayoutManager layoutManager = LayoutManager.get(client); + Element element = getElement(); + int paddingTop = layoutManager.getPaddingTop(element); + int paddingBottom = layoutManager.getPaddingBottom(element); + int y = paddingTop; + + for (int i = 0; i < cells.length; i++) { + y = paddingTop; + for (int j = 0; j < cells[i].length; j++) { + Cell cell = cells[i][j]; + if (cell != null) { + int reservedMargin; + if (cell.rowspan + j >= cells[i].length) { + // Make room for layout padding for cells reaching the + // bottom of the layout + reservedMargin = paddingBottom; + } else { + reservedMargin = 0; + } + cell.layoutVertically(y, reservedMargin); + } + y += rowHeights[j] + verticalSpacing; + } + } + + if (isUndefinedHeight()) { + int outerHeight = y - verticalSpacing + + layoutManager.getPaddingBottom(element) + + layoutManager.getBorderHeight(element); + element.getStyle().setHeight(outerHeight, Unit.PX); + getConnector().getLayoutManager().reportOuterHeight(getConnector(), + outerHeight); + } + } + + void layoutCellsHorizontally() { + LayoutManager layoutManager = LayoutManager.get(client); + Element element = getElement(); + int x = layoutManager.getPaddingLeft(element); + int paddingRight = layoutManager.getPaddingRight(element); + int horizontalSpacing = getHorizontalSpacing(); + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < cells[i].length; j++) { + Cell cell = cells[i][j]; + if (cell != null) { + int reservedMargin; + // Make room for layout padding for cells reaching the + // right edge of the layout + if (i + cell.colspan >= cells.length) { + reservedMargin = paddingRight; + } else { + reservedMargin = 0; + } + cell.layoutHorizontally(x, reservedMargin); + } + } + x += columnWidths[i] + horizontalSpacing; + } + + if (isUndefinedWidth()) { + int outerWidth = x - horizontalSpacing + + layoutManager.getPaddingRight(element) + + layoutManager.getBorderWidth(element); + element.getStyle().setWidth(outerWidth, Unit.PX); + getConnector().getLayoutManager().reportOuterWidth(getConnector(), + outerWidth); + } + } + + private boolean isUndefinedHeight() { + return getConnector().isUndefinedHeight(); + } + + private boolean isUndefinedWidth() { + return getConnector().isUndefinedWidth(); + } + + private void detectRowHeights() { + for (int i = 0; i < rowHeights.length; i++) { + rowHeights[i] = 0; + } + + // collect min rowheight from non-rowspanned cells + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < cells[i].length; j++) { + Cell cell = cells[i][j]; + if (cell != null) { + if (cell.rowspan == 1) { + if (!cell.hasRelativeHeight() + && rowHeights[j] < cell.getHeight()) { + rowHeights[j] = cell.getHeight(); + } + } else { + storeRowSpannedCell(cell); + } + } + } + } + + distributeRowSpanHeights(); + + minRowHeights = cloneArray(rowHeights); + } + + private void detectColWidths() { + // collect min colwidths from non-colspanned cells + for (int i = 0; i < columnWidths.length; i++) { + columnWidths[i] = 0; + } + + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < cells[i].length; j++) { + Cell cell = cells[i][j]; + if (cell != null) { + if (cell.colspan == 1) { + if (!cell.hasRelativeWidth() + && columnWidths[i] < cell.getWidth()) { + columnWidths[i] = cell.getWidth(); + } + } else { + storeColSpannedCell(cell); + } + } + } + } + + distributeColSpanWidths(); + + minColumnWidths = cloneArray(columnWidths); + } + + private void storeRowSpannedCell(Cell cell) { + SpanList l = null; + for (SpanList list : rowSpans) { + if (list.span < cell.rowspan) { + continue; + } else { + // insert before this + l = list; + break; + } + } + if (l == null) { + l = new SpanList(cell.rowspan); + rowSpans.add(l); + } else if (l.span != cell.rowspan) { + SpanList newL = new SpanList(cell.rowspan); + rowSpans.add(rowSpans.indexOf(l), newL); + l = newL; + } + l.cells.add(cell); + } + + /** + * Iterates colspanned cells, ensures cols have enough space to accommodate + * them + */ + void distributeColSpanWidths() { + for (SpanList list : colSpans) { + for (Cell cell : list.cells) { + // cells with relative content may return non 0 here if on + // subsequent renders + int width = cell.hasRelativeWidth() ? 0 : cell.getWidth(); + distributeSpanSize(columnWidths, cell.col, cell.colspan, + getHorizontalSpacing(), width, colExpandRatioArray); + } + } + } + + /** + * Iterates rowspanned cells, ensures rows have enough space to accommodate + * them + */ + private void distributeRowSpanHeights() { + for (SpanList list : rowSpans) { + for (Cell cell : list.cells) { + // cells with relative content may return non 0 here if on + // subsequent renders + int height = cell.hasRelativeHeight() ? 0 : cell.getHeight(); + distributeSpanSize(rowHeights, cell.row, cell.rowspan, + getVerticalSpacing(), height, rowExpandRatioArray); + } + } + } + + private static void distributeSpanSize(int[] dimensions, + int spanStartIndex, int spanSize, int spacingSize, int size, + int[] expansionRatios) { + int allocated = dimensions[spanStartIndex]; + for (int i = 1; i < spanSize; i++) { + allocated += spacingSize + dimensions[spanStartIndex + i]; + } + if (allocated < size) { + // dimensions needs to be expanded due spanned cell + int neededExtraSpace = size - allocated; + int allocatedExtraSpace = 0; + + // Divide space according to expansion ratios if any span has a + // ratio + int totalExpansion = 0; + for (int i = 0; i < spanSize; i++) { + int itemIndex = spanStartIndex + i; + totalExpansion += expansionRatios[itemIndex]; + } + + for (int i = 0; i < spanSize; i++) { + int itemIndex = spanStartIndex + i; + int expansion; + if (totalExpansion == 0) { + // Divide equally among all cells if there are no + // expansion ratios + expansion = neededExtraSpace / spanSize; + } else { + expansion = neededExtraSpace * expansionRatios[itemIndex] + / totalExpansion; + } + dimensions[itemIndex] += expansion; + allocatedExtraSpace += expansion; + } + + // We might still miss a couple of pixels because of + // rounding errors... + if (neededExtraSpace > allocatedExtraSpace) { + for (int i = 0; i < spanSize; i++) { + // Add one pixel to every cell until we have + // compensated for any rounding error + int itemIndex = spanStartIndex + i; + dimensions[itemIndex] += 1; + allocatedExtraSpace += 1; + if (neededExtraSpace == allocatedExtraSpace) { + break; + } + } + } + } + } + + private LinkedList<SpanList> colSpans = new LinkedList<SpanList>(); + private LinkedList<SpanList> rowSpans = new LinkedList<SpanList>(); + + private class SpanList { + final int span; + List<Cell> cells = new LinkedList<Cell>(); + + public SpanList(int span) { + this.span = span; + } + } + + void storeColSpannedCell(Cell cell) { + SpanList l = null; + for (SpanList list : colSpans) { + if (list.span < cell.colspan) { + continue; + } else { + // insert before this + l = list; + break; + } + } + if (l == null) { + l = new SpanList(cell.colspan); + colSpans.add(l); + } else if (l.span != cell.colspan) { + + SpanList newL = new SpanList(cell.colspan); + colSpans.add(colSpans.indexOf(l), newL); + l = newL; + } + l.cells.add(cell); + } + + Cell[][] cells; + + /** + * Private helper class. + */ + class Cell { + public Cell(int row, int col) { + this.row = row; + this.col = col; + } + + public boolean hasContent() { + return hasContent; + } + + public boolean hasRelativeHeight() { + if (slot != null) { + return slot.getChild().isRelativeHeight(); + } else { + return true; + } + } + + /** + * @return total of spanned cols + */ + private int getAvailableWidth() { + int width = columnWidths[col]; + for (int i = 1; i < colspan; i++) { + width += getHorizontalSpacing() + columnWidths[col + i]; + } + return width; + } + + /** + * @return total of spanned rows + */ + private int getAvailableHeight() { + int height = rowHeights[row]; + for (int i = 1; i < rowspan; i++) { + height += getVerticalSpacing() + rowHeights[row + i]; + } + return height; + } + + public void layoutHorizontally(int x, int marginRight) { + if (slot != null) { + slot.positionHorizontally(x, getAvailableWidth(), marginRight); + } + } + + public void layoutVertically(int y, int marginBottom) { + if (slot != null) { + slot.positionVertically(y, getAvailableHeight(), marginBottom); + } + } + + public int getWidth() { + if (slot != null) { + return slot.getUsedWidth(); + } else { + return 0; + } + } + + public int getHeight() { + if (slot != null) { + return slot.getUsedHeight(); + } else { + return 0; + } + } + + protected boolean hasRelativeWidth() { + if (slot != null) { + return slot.getChild().isRelativeWidth(); + } else { + return true; + } + } + + final int row; + final int col; + int colspan = 1; + int rowspan = 1; + + private boolean hasContent; + + private AlignmentInfo alignment; + + ComponentConnectorLayoutSlot slot; + + public void updateFromUidl(UIDL cellUidl) { + // Set cell width + colspan = cellUidl.hasAttribute("w") ? cellUidl + .getIntAttribute("w") : 1; + // Set cell height + rowspan = cellUidl.hasAttribute("h") ? cellUidl + .getIntAttribute("h") : 1; + // ensure we will lose reference to old cells, now overlapped by + // this cell + for (int i = 0; i < colspan; i++) { + for (int j = 0; j < rowspan; j++) { + if (i > 0 || j > 0) { + cells[col + i][row + j] = null; + } + } + } + + UIDL childUidl = cellUidl.getChildUIDL(0); // we are interested + // about childUidl + hasContent = childUidl != null; + if (hasContent) { + ComponentConnector childConnector = client + .getPaintable(childUidl); + + if (slot == null || slot.getChild() != childConnector) { + slot = new ComponentConnectorLayoutSlot(CLASSNAME, + childConnector, getConnector()); + if (childConnector.isRelativeWidth()) { + slot.getWrapperElement().getStyle() + .setWidth(100, Unit.PCT); + } + Element slotWrapper = slot.getWrapperElement(); + getElement().appendChild(slotWrapper); + + Widget widget = childConnector.getWidget(); + insert(widget, slotWrapper, getWidgetCount(), false); + Cell oldCell = widgetToCell.put(widget, this); + if (oldCell != null) { + oldCell.slot.getWrapperElement().removeFromParent(); + oldCell.slot = null; + } + } + + } + } + + public void setAlignment(AlignmentInfo alignmentInfo) { + slot.setAlignment(alignmentInfo); + } + } + + Cell getCell(int row, int col) { + return cells[col][row]; + } + + /** + * Creates a new Cell with the given coordinates. If an existing cell was + * found, returns that one. + * + * @param row + * @param col + * @return + */ + Cell createCell(int row, int col) { + Cell cell = getCell(row, col); + if (cell == null) { + cell = new Cell(row, col); + cells[col][row] = cell; + } + return cell; + } + + /** + * Returns the deepest nested child component which contains "element". The + * child component is also returned if "element" is part of its caption. + * + * @param element + * An element that is a nested sub element of the root element in + * this layout + * @return The Paintable which the element is a part of. Null if the element + * belongs to the layout and not to a child. + */ + ComponentConnector getComponent(Element element) { + return Util.getConnectorForElement(client, this, element); + } + + void setCaption(Widget widget, VCaption caption) { + VLayoutSlot slot = widgetToCell.get(widget).slot; + + if (caption != null) { + // Logical attach. + getChildren().add(caption); + } + + // Physical attach if not null, also removes old caption + slot.setCaption(caption); + + if (caption != null) { + // Adopt. + adopt(caption); + } + } + + private void togglePrefixedStyleName(String name, boolean enabled) { + if (enabled) { + addStyleDependentName(name); + } else { + removeStyleDependentName(name); + } + } + + void updateMarginStyleNames(VMarginInfo marginInfo) { + togglePrefixedStyleName("margin-top", marginInfo.hasTop()); + togglePrefixedStyleName("margin-right", marginInfo.hasRight()); + togglePrefixedStyleName("margin-bottom", marginInfo.hasBottom()); + togglePrefixedStyleName("margin-left", marginInfo.hasLeft()); + } + + void updateSpacingStyleName(boolean spacingEnabled) { + String styleName = getStylePrimaryName(); + if (spacingEnabled) { + spacingMeasureElement.addClassName(styleName + "-spacing-on"); + spacingMeasureElement.removeClassName(styleName + "-spacing-off"); + } else { + spacingMeasureElement.removeClassName(styleName + "-spacing-on"); + spacingMeasureElement.addClassName(styleName + "-spacing-off"); + } + } + + public void setSize(int rows, int cols) { + if (cells == null) { + cells = new Cell[cols][rows]; + } else if (cells.length != cols || cells[0].length != rows) { + Cell[][] newCells = new Cell[cols][rows]; + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < cells[i].length; j++) { + if (i < cols && j < rows) { + newCells[i][j] = cells[i][j]; + } + } + } + cells = newCells; + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/label/LabelConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/label/LabelConnector.java new file mode 100644 index 0000000000..4280db8bc9 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/label/LabelConnector.java @@ -0,0 +1,81 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.label; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.PreElement; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.label.LabelState; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector; +import com.vaadin.ui.Label; + +@Connect(value = Label.class, loadStyle = LoadStyle.EAGER) +public class LabelConnector extends AbstractComponentConnector { + + @Override + public LabelState getState() { + return (LabelState) super.getState(); + } + + @Override + protected void init() { + super.init(); + getWidget().setConnection(getConnection()); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + boolean sinkOnloads = false; + switch (getState().getContentMode()) { + case PREFORMATTED: + PreElement preElement = Document.get().createPreElement(); + preElement.setInnerText(getState().getText()); + // clear existing content + getWidget().setHTML(""); + // add preformatted text to dom + getWidget().getElement().appendChild(preElement); + break; + + case TEXT: + getWidget().setText(getState().getText()); + break; + + case XHTML: + case RAW: + sinkOnloads = true; + case XML: + getWidget().setHTML(getState().getText()); + break; + default: + getWidget().setText(""); + break; + + } + if (sinkOnloads) { + Util.sinkOnloadForImages(getWidget().getElement()); + } + } + + @Override + public VLabel getWidget() { + return (VLabel) super.getWidget(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/label/VLabel.java b/client/src/com/vaadin/terminal/gwt/client/ui/label/VLabel.java new file mode 100644 index 0000000000..dc23807166 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/label/VLabel.java @@ -0,0 +1,81 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.label; + +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VTooltip; + +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); + sinkEvents(VTooltip.TOOLTIP_EVENTS); + } + + public VLabel(String text) { + super(text); + setStyleName(CLASSNAME); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + event.stopPropagation(); + return; + } + } + + @Override + public void setWidth(String width) { + super.setWidth(width); + if (width == null || width.equals("")) { + setStyleName(getElement(), CLASSNAME_UNDEFINED_WIDTH, true); + getElement().getStyle().setDisplay(Display.INLINE_BLOCK); + } else { + setStyleName(getElement(), CLASSNAME_UNDEFINED_WIDTH, false); + getElement().getStyle().clearDisplay(); + } + } + + @Override + public void setText(String text) { + if (BrowserInfo.get().isIE8()) { + // #3983 - IE8 incorrectly replaces \n with <br> so we do the + // escaping manually and set as HTML + super.setHTML(Util.escapeHTML(text)); + } else { + super.setText(text); + } + } + + void setConnection(ApplicationConnection client) { + connection = client; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/ComponentConnectorLayoutSlot.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ComponentConnectorLayoutSlot.java new file mode 100644 index 0000000000..b1f381c33d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ComponentConnectorLayoutSlot.java @@ -0,0 +1,111 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.layout; + +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.ui.ManagedLayout; + +public class ComponentConnectorLayoutSlot extends VLayoutSlot { + + final ComponentConnector child; + final ManagedLayout layout; + + public ComponentConnectorLayoutSlot(String baseClassName, + ComponentConnector child, ManagedLayout layout) { + super(baseClassName, child.getWidget()); + this.child = child; + this.layout = layout; + } + + public ComponentConnector getChild() { + return child; + } + + @Override + protected int getCaptionHeight() { + VCaption caption = getCaption(); + return caption != null ? getLayoutManager().getOuterHeight( + caption.getElement()) : 0; + } + + @Override + protected int getCaptionWidth() { + VCaption caption = getCaption(); + return caption != null ? getLayoutManager().getOuterWidth( + caption.getElement()) : 0; + } + + public LayoutManager getLayoutManager() { + return layout.getLayoutManager(); + } + + @Override + public void setCaption(VCaption caption) { + VCaption oldCaption = getCaption(); + if (oldCaption != null) { + getLayoutManager().unregisterDependency(layout, + oldCaption.getElement()); + } + super.setCaption(caption); + if (caption != null) { + getLayoutManager().registerDependency( + (ManagedLayout) child.getParent(), caption.getElement()); + } + } + + @Override + protected void reportActualRelativeHeight(int allocatedHeight) { + getLayoutManager().reportOuterHeight(child, allocatedHeight); + } + + @Override + protected void reportActualRelativeWidth(int allocatedWidth) { + getLayoutManager().reportOuterWidth(child, allocatedWidth); + } + + @Override + public int getWidgetHeight() { + return getLayoutManager() + .getOuterHeight(child.getWidget().getElement()); + } + + @Override + public int getWidgetWidth() { + return getLayoutManager().getOuterWidth(child.getWidget().getElement()); + } + + @Override + public boolean isUndefinedHeight() { + return child.isUndefinedHeight(); + } + + @Override + public boolean isUndefinedWidth() { + return child.isUndefinedWidth(); + } + + @Override + public boolean isRelativeHeight() { + return child.isRelativeHeight(); + } + + @Override + public boolean isRelativeWidth() { + return child.isRelativeWidth(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeEvent.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeEvent.java new file mode 100644 index 0000000000..d9733875f3 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeEvent.java @@ -0,0 +1,37 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.layout; + +import com.google.gwt.dom.client.Element; +import com.vaadin.terminal.gwt.client.LayoutManager; + +public class ElementResizeEvent { + private final Element element; + private final LayoutManager layoutManager; + + public ElementResizeEvent(LayoutManager layoutManager, Element element) { + this.layoutManager = layoutManager; + this.element = element; + } + + public Element getElement() { + return element; + } + + public LayoutManager getLayoutManager() { + return layoutManager; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeListener.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeListener.java new file mode 100644 index 0000000000..15b3a5517d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeListener.java @@ -0,0 +1,21 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.layout; + +public interface ElementResizeListener { + public void onElementResize(ElementResizeEvent e); +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/LayoutDependencyTree.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/LayoutDependencyTree.java new file mode 100644 index 0000000000..2bf789fd50 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/LayoutDependencyTree.java @@ -0,0 +1,532 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.layout; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.vaadin.shared.ComponentState; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ServerConnector; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.ManagedLayout; + +public class LayoutDependencyTree { + private class LayoutDependency { + private final ComponentConnector connector; + private final int direction; + + private boolean needsLayout = false; + private boolean needsMeasure = false; + + private boolean scrollingParentCached = false; + private ComponentConnector scrollingBoundary = null; + + private Set<ComponentConnector> measureBlockers = new HashSet<ComponentConnector>(); + private Set<ComponentConnector> layoutBlockers = new HashSet<ComponentConnector>(); + + public LayoutDependency(ComponentConnector connector, int direction) { + this.connector = connector; + this.direction = direction; + } + + private void addLayoutBlocker(ComponentConnector blocker) { + boolean blockerAdded = layoutBlockers.add(blocker); + if (blockerAdded && layoutBlockers.size() == 1) { + if (needsLayout) { + getLayoutQueue(direction).remove(connector); + } else { + // Propagation already done if needsLayout is set + propagatePotentialLayout(); + } + } + } + + private void removeLayoutBlocker(ComponentConnector blocker) { + boolean removed = layoutBlockers.remove(blocker); + if (removed && layoutBlockers.isEmpty()) { + if (needsLayout) { + getLayoutQueue(direction).add((ManagedLayout) connector); + } else { + propagateNoUpcomingLayout(); + } + } + } + + private void addMeasureBlocker(ComponentConnector blocker) { + boolean blockerAdded = measureBlockers.add(blocker); + if (blockerAdded && measureBlockers.size() == 1) { + if (needsMeasure) { + getMeasureQueue(direction).remove(connector); + } else { + propagatePotentialResize(); + } + } + } + + private void removeMeasureBlocker(ComponentConnector blocker) { + boolean removed = measureBlockers.remove(blocker); + if (removed && measureBlockers.isEmpty()) { + if (needsMeasure) { + getMeasureQueue(direction).add(connector); + } else { + propagateNoUpcomingResize(); + } + } + } + + public void setNeedsMeasure(boolean needsMeasure) { + if (needsMeasure && !this.needsMeasure) { + // If enabling needsMeasure + this.needsMeasure = needsMeasure; + + if (measureBlockers.isEmpty()) { + // Add to queue if there are no blockers + getMeasureQueue(direction).add(connector); + // Only need to propagate if not already propagated when + // setting blockers + propagatePotentialResize(); + } + } else if (!needsMeasure && this.needsMeasure + && measureBlockers.isEmpty()) { + // Only disable if there are no blockers (elements gets measured + // in both directions even if there is a blocker in one + // direction) + this.needsMeasure = needsMeasure; + getMeasureQueue(direction).remove(connector); + propagateNoUpcomingResize(); + } + } + + public void setNeedsLayout(boolean needsLayout) { + if (!(connector instanceof ManagedLayout)) { + throw new IllegalStateException( + "Only managed layouts can need layout, layout attempted for " + + Util.getConnectorString(connector)); + } + if (needsLayout && !this.needsLayout) { + // If enabling needsLayout + this.needsLayout = needsLayout; + + if (layoutBlockers.isEmpty()) { + // Add to queue if there are no blockers + getLayoutQueue(direction).add((ManagedLayout) connector); + // Only need to propagate if not already propagated when + // setting blockers + propagatePotentialLayout(); + } + } else if (!needsLayout && this.needsLayout + && layoutBlockers.isEmpty()) { + // Only disable if there are no layout blockers + // (SimpleManagedLayout gets layouted in both directions + // even if there is a blocker in one direction) + this.needsLayout = needsLayout; + getLayoutQueue(direction).remove(connector); + propagateNoUpcomingLayout(); + } + } + + private void propagatePotentialResize() { + for (ComponentConnector needsSize : getNeedsSizeForLayout()) { + LayoutDependency layoutDependency = getDependency(needsSize, + direction); + layoutDependency.addLayoutBlocker(connector); + } + } + + private Collection<ComponentConnector> getNeedsSizeForLayout() { + // Find all connectors that need the size of this connector for + // layouting + + // Parent needs size if it isn't relative? + // Connector itself needs size if it isn't undefined? + // Children doesn't care? + + ArrayList<ComponentConnector> needsSize = new ArrayList<ComponentConnector>(); + + if (!isUndefinedInDirection(connector, direction)) { + needsSize.add(connector); + } + if (!isRelativeInDirection(connector, direction)) { + ServerConnector parent = connector.getParent(); + if (parent instanceof ComponentConnector) { + needsSize.add((ComponentConnector) parent); + } + } + + return needsSize; + } + + private void propagateNoUpcomingResize() { + for (ComponentConnector mightNeedLayout : getNeedsSizeForLayout()) { + LayoutDependency layoutDependency = getDependency( + mightNeedLayout, direction); + layoutDependency.removeLayoutBlocker(connector); + } + } + + private void propagatePotentialLayout() { + for (ComponentConnector sizeMightChange : getResizedByLayout()) { + LayoutDependency layoutDependency = getDependency( + sizeMightChange, direction); + layoutDependency.addMeasureBlocker(connector); + } + } + + private Collection<ComponentConnector> getResizedByLayout() { + // Components that might get resized by a layout of this component + + // Parent never resized + // Connector itself resized if undefined + // Children resized if relative + + ArrayList<ComponentConnector> resized = new ArrayList<ComponentConnector>(); + if (isUndefinedInDirection(connector, direction)) { + resized.add(connector); + } + + if (connector instanceof ComponentContainerConnector) { + ComponentContainerConnector container = (ComponentContainerConnector) connector; + for (ComponentConnector child : container.getChildComponents()) { + if (isRelativeInDirection(child, direction)) { + resized.add(child); + } + } + } + + return resized; + } + + private void propagateNoUpcomingLayout() { + for (ComponentConnector sizeMightChange : getResizedByLayout()) { + LayoutDependency layoutDependency = getDependency( + sizeMightChange, direction); + layoutDependency.removeMeasureBlocker(connector); + } + } + + public void markSizeAsChanged() { + // When the size has changed, all that use that size should be + // layouted + for (ComponentConnector connector : getNeedsSizeForLayout()) { + LayoutDependency layoutDependency = getDependency(connector, + direction); + if (connector instanceof ManagedLayout) { + layoutDependency.setNeedsLayout(true); + } else { + // Should simulate setNeedsLayout(true) + markAsLayouted -> + // propagate needs measure + layoutDependency.propagatePostLayoutMeasure(); + } + } + + // Should also go through the hierarchy to discover appeared or + // disappeared scrollbars + ComponentConnector scrollingBoundary = getScrollingBoundary(connector); + if (scrollingBoundary != null) { + getDependency(scrollingBoundary, getOppositeDirection()) + .setNeedsMeasure(true); + } + + } + + /** + * Go up the hierarchy to find a component whose size might have changed + * in the other direction because changes to this component causes + * scrollbars to appear or disappear. + * + * @return + */ + private LayoutDependency findPotentiallyChangedScrollbar() { + ComponentConnector currentConnector = connector; + while (true) { + ServerConnector parent = currentConnector.getParent(); + if (!(parent instanceof ComponentConnector)) { + return null; + } + if (parent instanceof MayScrollChildren) { + return getDependency(currentConnector, + getOppositeDirection()); + } + currentConnector = (ComponentConnector) parent; + } + } + + private int getOppositeDirection() { + return direction == HORIZONTAL ? VERTICAL : HORIZONTAL; + } + + public void markAsLayouted() { + if (!layoutBlockers.isEmpty()) { + // Don't do anything if there are layout blockers (SimpleLayout + // gets layouted in both directions even if one direction is + // blocked) + return; + } + setNeedsLayout(false); + propagatePostLayoutMeasure(); + } + + private void propagatePostLayoutMeasure() { + for (ComponentConnector resized : getResizedByLayout()) { + LayoutDependency layoutDependency = getDependency(resized, + direction); + layoutDependency.setNeedsMeasure(true); + } + + // Special case for e.g. wrapping texts + if (direction == HORIZONTAL && !connector.isUndefinedWidth() + && connector.isUndefinedHeight()) { + LayoutDependency dependency = getDependency(connector, VERTICAL); + dependency.setNeedsMeasure(true); + } + } + + @Override + public String toString() { + String s = getCompactConnectorString(connector) + "\n"; + if (direction == VERTICAL) { + s += "Vertical"; + } else { + s += "Horizontal"; + } + ComponentState state = connector.getState(); + s += " sizing: " + + getSizeDefinition(direction == VERTICAL ? state + .getHeight() : state.getWidth()) + "\n"; + + if (needsLayout) { + s += "Needs layout\n"; + } + if (getLayoutQueue(direction).contains(connector)) { + s += "In layout queue\n"; + } + s += "Layout blockers: " + blockersToString(layoutBlockers) + "\n"; + + if (needsMeasure) { + s += "Needs measure\n"; + } + if (getMeasureQueue(direction).contains(connector)) { + s += "In measure queue\n"; + } + s += "Measure blockers: " + blockersToString(measureBlockers); + + return s; + } + + public boolean noMoreChangesExpected() { + return !needsLayout && !needsMeasure && layoutBlockers.isEmpty() + && measureBlockers.isEmpty(); + } + + } + + private static final int HORIZONTAL = 0; + private static final int VERTICAL = 1; + + private final Map<?, ?>[] dependenciesInDirection = new Map<?, ?>[] { + new HashMap<ComponentConnector, LayoutDependency>(), + new HashMap<ComponentConnector, LayoutDependency>() }; + + private final Collection<?>[] measureQueueInDirection = new HashSet<?>[] { + new HashSet<ComponentConnector>(), + new HashSet<ComponentConnector>() }; + + private final Collection<?>[] layoutQueueInDirection = new HashSet<?>[] { + new HashSet<ComponentConnector>(), + new HashSet<ComponentConnector>() }; + + public void setNeedsMeasure(ComponentConnector connector, + boolean needsMeasure) { + setNeedsHorizontalMeasure(connector, needsMeasure); + setNeedsVerticalMeasure(connector, needsMeasure); + } + + public void setNeedsHorizontalMeasure(ComponentConnector connector, + boolean needsMeasure) { + LayoutDependency dependency = getDependency(connector, HORIZONTAL); + dependency.setNeedsMeasure(needsMeasure); + } + + public void setNeedsVerticalMeasure(ComponentConnector connector, + boolean needsMeasure) { + LayoutDependency dependency = getDependency(connector, VERTICAL); + dependency.setNeedsMeasure(needsMeasure); + } + + private LayoutDependency getDependency(ComponentConnector connector, + int direction) { + @SuppressWarnings("unchecked") + Map<ComponentConnector, LayoutDependency> dependencies = (Map<ComponentConnector, LayoutDependency>) dependenciesInDirection[direction]; + LayoutDependency dependency = dependencies.get(connector); + if (dependency == null) { + dependency = new LayoutDependency(connector, direction); + dependencies.put(connector, dependency); + } + return dependency; + } + + @SuppressWarnings("unchecked") + private Collection<ManagedLayout> getLayoutQueue(int direction) { + return (Collection<ManagedLayout>) layoutQueueInDirection[direction]; + } + + @SuppressWarnings("unchecked") + private Collection<ComponentConnector> getMeasureQueue(int direction) { + return (Collection<ComponentConnector>) measureQueueInDirection[direction]; + } + + public void setNeedsHorizontalLayout(ManagedLayout layout, + boolean needsLayout) { + LayoutDependency dependency = getDependency(layout, HORIZONTAL); + dependency.setNeedsLayout(needsLayout); + } + + public void setNeedsVerticalLayout(ManagedLayout layout, boolean needsLayout) { + LayoutDependency dependency = getDependency(layout, VERTICAL); + dependency.setNeedsLayout(needsLayout); + } + + public void markAsHorizontallyLayouted(ManagedLayout layout) { + LayoutDependency dependency = getDependency(layout, HORIZONTAL); + dependency.markAsLayouted(); + } + + public void markAsVerticallyLayouted(ManagedLayout layout) { + LayoutDependency dependency = getDependency(layout, VERTICAL); + dependency.markAsLayouted(); + } + + public void markHeightAsChanged(ComponentConnector connector) { + LayoutDependency dependency = getDependency(connector, VERTICAL); + dependency.markSizeAsChanged(); + } + + public void markWidthAsChanged(ComponentConnector connector) { + LayoutDependency dependency = getDependency(connector, HORIZONTAL); + dependency.markSizeAsChanged(); + } + + private static boolean isRelativeInDirection(ComponentConnector connector, + int direction) { + if (direction == HORIZONTAL) { + return connector.isRelativeWidth(); + } else { + return connector.isRelativeHeight(); + } + } + + private static boolean isUndefinedInDirection(ComponentConnector connector, + int direction) { + if (direction == VERTICAL) { + return connector.isUndefinedHeight(); + } else { + return connector.isUndefinedWidth(); + } + } + + private static String getCompactConnectorString(ComponentConnector connector) { + return Util.getSimpleName(connector) + " (" + + connector.getConnectorId() + ")"; + } + + private static String getSizeDefinition(String size) { + if (size == null || size.length() == 0) { + return "undefined"; + } else if (size.endsWith("%")) { + return "relative"; + } else { + return "fixed"; + } + } + + private static String blockersToString( + Collection<ComponentConnector> blockers) { + StringBuilder b = new StringBuilder("["); + for (ComponentConnector blocker : blockers) { + if (b.length() != 1) { + b.append(", "); + } + b.append(getCompactConnectorString(blocker)); + } + b.append(']'); + return b.toString(); + } + + public boolean hasConnectorsToMeasure() { + return !measureQueueInDirection[HORIZONTAL].isEmpty() + || !measureQueueInDirection[VERTICAL].isEmpty(); + } + + public boolean hasHorizontalConnectorToLayout() { + return !getLayoutQueue(HORIZONTAL).isEmpty(); + } + + public boolean hasVerticaConnectorToLayout() { + return !getLayoutQueue(VERTICAL).isEmpty(); + } + + public ManagedLayout[] getHorizontalLayoutTargets() { + Collection<ManagedLayout> queue = getLayoutQueue(HORIZONTAL); + return queue.toArray(new ManagedLayout[queue.size()]); + } + + public ManagedLayout[] getVerticalLayoutTargets() { + Collection<ManagedLayout> queue = getLayoutQueue(VERTICAL); + return queue.toArray(new ManagedLayout[queue.size()]); + } + + public Collection<ComponentConnector> getMeasureTargets() { + Collection<ComponentConnector> measureTargets = new HashSet<ComponentConnector>( + getMeasureQueue(HORIZONTAL)); + measureTargets.addAll(getMeasureQueue(VERTICAL)); + return measureTargets; + } + + public void logDependencyStatus(ComponentConnector connector) { + VConsole.log("===="); + VConsole.log(getDependency(connector, HORIZONTAL).toString()); + VConsole.log(getDependency(connector, VERTICAL).toString()); + } + + public boolean noMoreChangesExpected(ComponentConnector connector) { + return getDependency(connector, HORIZONTAL).noMoreChangesExpected() + && getDependency(connector, VERTICAL).noMoreChangesExpected(); + } + + public ComponentConnector getScrollingBoundary(ComponentConnector connector) { + LayoutDependency dependency = getDependency(connector, HORIZONTAL); + if (!dependency.scrollingParentCached) { + ServerConnector parent = dependency.connector.getParent(); + if (parent instanceof MayScrollChildren) { + dependency.scrollingBoundary = connector; + } else if (parent instanceof ComponentConnector) { + dependency.scrollingBoundary = getScrollingBoundary((ComponentConnector) parent); + } else { + // No scrolling parent + } + + dependency.scrollingParentCached = true; + } + return dependency.scrollingBoundary; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java new file mode 100644 index 0000000000..21e35409a0 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java @@ -0,0 +1,98 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.layout; + +public class Margins { + + private int marginTop; + private int marginBottom; + private int marginLeft; + private int marginRight; + + private int horizontal = 0; + private int vertical = 0; + + public Margins(int marginTop, int marginBottom, int marginLeft, + int marginRight) { + super(); + this.marginTop = marginTop; + this.marginBottom = marginBottom; + this.marginLeft = marginLeft; + this.marginRight = marginRight; + + updateHorizontal(); + updateVertical(); + } + + public int getMarginTop() { + return marginTop; + } + + public int getMarginBottom() { + return marginBottom; + } + + public int getMarginLeft() { + return marginLeft; + } + + public int getMarginRight() { + return marginRight; + } + + public int getHorizontal() { + return horizontal; + } + + public int getVertical() { + return vertical; + } + + public void setMarginTop(int marginTop) { + this.marginTop = marginTop; + updateVertical(); + } + + public void setMarginBottom(int marginBottom) { + this.marginBottom = marginBottom; + updateVertical(); + } + + public void setMarginLeft(int marginLeft) { + this.marginLeft = marginLeft; + updateHorizontal(); + } + + public void setMarginRight(int marginRight) { + this.marginRight = marginRight; + updateHorizontal(); + } + + private void updateVertical() { + vertical = marginTop + marginBottom; + } + + private void updateHorizontal() { + horizontal = marginLeft + marginRight; + } + + @Override + public String toString() { + return "Margins [marginLeft=" + marginLeft + ",marginTop=" + marginTop + + ",marginRight=" + marginRight + ",marginBottom=" + + marginBottom + "]"; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/MayScrollChildren.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/MayScrollChildren.java new file mode 100644 index 0000000000..336021dbf4 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/MayScrollChildren.java @@ -0,0 +1,22 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.layout; + +import com.vaadin.terminal.gwt.client.ComponentContainerConnector; + +public interface MayScrollChildren extends ComponentContainerConnector { + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/VLayoutSlot.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/VLayoutSlot.java new file mode 100644 index 0000000000..715a24749e --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/VLayoutSlot.java @@ -0,0 +1,299 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.layout; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.AlignmentInfo; +import com.vaadin.terminal.gwt.client.VCaption; + +public abstract class VLayoutSlot { + + private final Element wrapper = Document.get().createDivElement().cast(); + + private AlignmentInfo alignment; + private VCaption caption; + private final Widget widget; + + private double expandRatio; + + public VLayoutSlot(String baseClassName, Widget widget) { + this.widget = widget; + + wrapper.setClassName(baseClassName + "-slot"); + } + + public VCaption getCaption() { + return caption; + } + + public void setCaption(VCaption caption) { + if (this.caption != null) { + this.caption.removeFromParent(); + } + this.caption = caption; + if (caption != null) { + // Physical attach. + DOM.insertBefore(wrapper, caption.getElement(), widget.getElement()); + Style style = caption.getElement().getStyle(); + style.setPosition(Position.ABSOLUTE); + style.setTop(0, Unit.PX); + } + } + + public AlignmentInfo getAlignment() { + return alignment; + } + + public Widget getWidget() { + return widget; + } + + public void setAlignment(AlignmentInfo alignment) { + this.alignment = alignment; + } + + public void positionHorizontally(double currentLocation, + double allocatedSpace, double marginRight) { + Style style = wrapper.getStyle(); + + double availableWidth = allocatedSpace; + + VCaption caption = getCaption(); + Style captionStyle = caption != null ? caption.getElement().getStyle() + : null; + int captionWidth = getCaptionWidth(); + + boolean captionAboveCompnent; + if (caption == null) { + captionAboveCompnent = false; + style.clearPaddingRight(); + } else { + captionAboveCompnent = !caption.shouldBePlacedAfterComponent(); + if (!captionAboveCompnent) { + availableWidth -= captionWidth; + captionStyle.clearLeft(); + captionStyle.setRight(0, Unit.PX); + style.setPaddingRight(captionWidth, Unit.PX); + } else { + captionStyle.setLeft(0, Unit.PX); + captionStyle.clearRight(); + style.clearPaddingRight(); + } + } + + if (marginRight > 0) { + style.setMarginRight(marginRight, Unit.PX); + } else { + style.clearMarginRight(); + } + + if (isRelativeWidth()) { + style.setPropertyPx("width", (int) availableWidth); + } else { + style.clearProperty("width"); + } + + double allocatedContentWidth = 0; + if (isRelativeWidth()) { + String percentWidth = getWidget().getElement().getStyle() + .getWidth(); + double percentage = parsePercent(percentWidth); + allocatedContentWidth = availableWidth * (percentage / 100); + reportActualRelativeWidth(Math.round((float) allocatedContentWidth)); + } + + AlignmentInfo alignment = getAlignment(); + if (!alignment.isLeft()) { + double usedWidth; + if (isRelativeWidth()) { + usedWidth = allocatedContentWidth; + } else { + usedWidth = getWidgetWidth(); + } + if (alignment.isHorizontalCenter()) { + currentLocation += (allocatedSpace - usedWidth) / 2d; + if (captionAboveCompnent) { + captionStyle.setLeft( + Math.round(usedWidth - captionWidth) / 2, Unit.PX); + } + } else { + currentLocation += (allocatedSpace - usedWidth); + if (captionAboveCompnent) { + captionStyle.setLeft(Math.round(usedWidth - captionWidth), + Unit.PX); + } + } + } else { + if (captionAboveCompnent) { + captionStyle.setLeft(0, Unit.PX); + } + } + + style.setLeft(Math.round(currentLocation), Unit.PX); + } + + private double parsePercent(String size) { + return Double.parseDouble(size.replaceAll("%", "")); + } + + public void positionVertically(double currentLocation, + double allocatedSpace, double marginBottom) { + Style style = wrapper.getStyle(); + + double contentHeight = allocatedSpace; + + int captionHeight; + VCaption caption = getCaption(); + if (caption == null || caption.shouldBePlacedAfterComponent()) { + style.clearPaddingTop(); + captionHeight = 0; + } else { + captionHeight = getCaptionHeight(); + contentHeight -= captionHeight; + if (contentHeight < 0) { + contentHeight = 0; + } + style.setPaddingTop(captionHeight, Unit.PX); + } + + if (marginBottom > 0) { + style.setMarginBottom(marginBottom, Unit.PX); + } else { + style.clearMarginBottom(); + } + + if (isRelativeHeight()) { + style.setHeight(contentHeight, Unit.PX); + } else { + style.clearHeight(); + } + + double allocatedContentHeight = 0; + if (isRelativeHeight()) { + String height = getWidget().getElement().getStyle().getHeight(); + double percentage = parsePercent(height); + allocatedContentHeight = contentHeight * (percentage / 100); + reportActualRelativeHeight(Math + .round((float) allocatedContentHeight)); + } + + AlignmentInfo alignment = getAlignment(); + if (!alignment.isTop()) { + double usedHeight; + if (isRelativeHeight()) { + usedHeight = captionHeight + allocatedContentHeight; + } else { + usedHeight = getUsedHeight(); + } + if (alignment.isVerticalCenter()) { + currentLocation += (allocatedSpace - usedHeight) / 2d; + } else { + currentLocation += (allocatedSpace - usedHeight); + } + } + + style.setTop(currentLocation, Unit.PX); + } + + protected void reportActualRelativeHeight(int allocatedHeight) { + // Default implementation does nothing + } + + protected void reportActualRelativeWidth(int allocatedWidth) { + // Default implementation does nothing + } + + public void positionInDirection(double currentLocation, + double allocatedSpace, double endingMargin, boolean isVertical) { + if (isVertical) { + positionVertically(currentLocation, allocatedSpace, endingMargin); + } else { + positionHorizontally(currentLocation, allocatedSpace, endingMargin); + } + } + + public int getWidgetSizeInDirection(boolean isVertical) { + return isVertical ? getWidgetHeight() : getWidgetWidth(); + } + + public int getUsedWidth() { + int widgetWidth = getWidgetWidth(); + if (caption == null) { + return widgetWidth; + } else if (caption.shouldBePlacedAfterComponent()) { + return widgetWidth + getCaptionWidth(); + } else { + return Math.max(widgetWidth, getCaptionWidth()); + } + } + + public int getUsedHeight() { + int widgetHeight = getWidgetHeight(); + if (caption == null) { + return widgetHeight; + } else if (caption.shouldBePlacedAfterComponent()) { + return Math.max(widgetHeight, getCaptionHeight()); + } else { + return widgetHeight + getCaptionHeight(); + } + } + + public int getUsedSizeInDirection(boolean isVertical) { + return isVertical ? getUsedHeight() : getUsedWidth(); + } + + protected abstract int getCaptionHeight(); + + protected abstract int getCaptionWidth(); + + public abstract int getWidgetHeight(); + + public abstract int getWidgetWidth(); + + public abstract boolean isUndefinedHeight(); + + public abstract boolean isUndefinedWidth(); + + public boolean isUndefinedInDirection(boolean isVertical) { + return isVertical ? isUndefinedHeight() : isUndefinedWidth(); + } + + public abstract boolean isRelativeHeight(); + + public abstract boolean isRelativeWidth(); + + public boolean isRelativeInDirection(boolean isVertical) { + return isVertical ? isRelativeHeight() : isRelativeWidth(); + } + + public Element getWrapperElement() { + return wrapper; + } + + public void setExpandRatio(double expandRatio) { + this.expandRatio = expandRatio; + } + + public double getExpandRatio() { + return expandRatio; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/link/LinkConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/link/LinkConnector.java new file mode 100644 index 0000000000..c4bbcd34f7 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/link/LinkConnector.java @@ -0,0 +1,105 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.link; + +import com.google.gwt.user.client.DOM; +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector; +import com.vaadin.terminal.gwt.client.ui.Icon; +import com.vaadin.ui.Link; + +@Connect(Link.class) +public class LinkConnector extends AbstractComponentConnector implements + Paintable { + + @Override + public boolean delegateCaptionHandling() { + return false; + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + + if (!isRealUpdate(uidl)) { + return; + } + + getWidget().client = client; + + getWidget().enabled = isEnabled(); + + if (uidl.hasAttribute("name")) { + getWidget().target = uidl.getStringAttribute("name"); + getWidget().anchor.setAttribute("target", getWidget().target); + } + if (uidl.hasAttribute("src")) { + getWidget().src = client.translateVaadinUri(uidl + .getStringAttribute("src")); + getWidget().anchor.setAttribute("href", getWidget().src); + } + + if (uidl.hasAttribute("border")) { + if ("none".equals(uidl.getStringAttribute("border"))) { + getWidget().borderStyle = VLink.BORDER_STYLE_NONE; + } else { + getWidget().borderStyle = VLink.BORDER_STYLE_MINIMAL; + } + } else { + getWidget().borderStyle = VLink.BORDER_STYLE_DEFAULT; + } + + getWidget().targetHeight = uidl.hasAttribute("targetHeight") ? uidl + .getIntAttribute("targetHeight") : -1; + getWidget().targetWidth = uidl.hasAttribute("targetWidth") ? uidl + .getIntAttribute("targetWidth") : -1; + + // Set link caption + getWidget().captionElement.setInnerText(getState().getCaption()); + + // handle error + if (null != getState().getErrorMessage()) { + if (getWidget().errorIndicatorElement == null) { + getWidget().errorIndicatorElement = DOM.createDiv(); + DOM.setElementProperty(getWidget().errorIndicatorElement, + "className", "v-errorindicator"); + } + DOM.insertChild(getWidget().getElement(), + getWidget().errorIndicatorElement, 0); + } else if (getWidget().errorIndicatorElement != null) { + DOM.setStyleAttribute(getWidget().errorIndicatorElement, "display", + "none"); + } + + if (getState().getIcon() != null) { + if (getWidget().icon == null) { + getWidget().icon = new Icon(client); + getWidget().anchor.insertBefore(getWidget().icon.getElement(), + getWidget().captionElement); + } + getWidget().icon.setUri(getState().getIcon().getURL()); + } + + } + + @Override + public VLink getWidget() { + return (VLink) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/link/VLink.java b/client/src/com/vaadin/terminal/gwt/client/ui/link/VLink.java new file mode 100644 index 0000000000..e312d4d489 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/link/VLink.java @@ -0,0 +1,125 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.link; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +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.Window; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.Icon; + +public class VLink extends HTML implements ClickHandler { + + public static final String CLASSNAME = "v-link"; + + protected static final int BORDER_STYLE_DEFAULT = 0; + protected static final int BORDER_STYLE_MINIMAL = 1; + protected static final int BORDER_STYLE_NONE = 2; + + protected String src; + + protected String target; + + protected int borderStyle = BORDER_STYLE_DEFAULT; + + protected boolean enabled; + + protected int targetWidth; + + protected int targetHeight; + + protected Element errorIndicatorElement; + + protected final Element anchor = DOM.createAnchor(); + + protected final Element captionElement = DOM.createSpan(); + + protected Icon icon; + + protected ApplicationConnection client; + + public VLink() { + super(); + getElement().appendChild(anchor); + anchor.appendChild(captionElement); + addClickHandler(this); + setStyleName(CLASSNAME); + } + + @Override + public void onClick(ClickEvent event) { + if (enabled) { + if (target == null) { + target = "_self"; + } + String features; + switch (borderStyle) { + case BORDER_STYLE_NONE: + features = "menubar=no,location=no,status=no"; + break; + case BORDER_STYLE_MINIMAL: + features = "menubar=yes,location=no,status=no"; + break; + default: + features = ""; + break; + } + + if (targetWidth > 0) { + features += (features.length() > 0 ? "," : "") + "width=" + + targetWidth; + } + if (targetHeight > 0) { + features += (features.length() > 0 ? "," : "") + "height=" + + targetHeight; + } + + if (features.length() > 0) { + // if 'special features' are set, use window.open(), unless + // a modifier key is held (ctrl to open in new tab etc) + Event e = DOM.eventGetCurrentEvent(); + if (!e.getCtrlKey() && !e.getAltKey() && !e.getShiftKey() + && !e.getMetaKey()) { + Window.open(src, target, features); + e.preventDefault(); + } + } + } + } + + @Override + public void onBrowserEvent(Event event) { + final Element target = DOM.eventGetTarget(event); + if (event.getTypeInt() == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + } + if (target == captionElement || target == anchor + || (icon != null && target == icon.getElement())) { + super.onBrowserEvent(event); + } + if (!enabled) { + event.preventDefault(); + } + + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/listselect/ListSelectConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/listselect/ListSelectConnector.java new file mode 100644 index 0000000000..4d34c21546 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/listselect/ListSelectConnector.java @@ -0,0 +1,30 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.listselect; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ui.optiongroup.OptionGroupBaseConnector; +import com.vaadin.ui.ListSelect; + +@Connect(ListSelect.class) +public class ListSelectConnector extends OptionGroupBaseConnector { + + @Override + public VListSelect getWidget() { + return (VListSelect) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/listselect/VListSelect.java b/client/src/com/vaadin/terminal/gwt/client/ui/listselect/VListSelect.java new file mode 100644 index 0000000000..06099d296c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/listselect/VListSelect.java @@ -0,0 +1,124 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.listselect; + +import java.util.ArrayList; +import java.util.Iterator; + +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.user.client.ui.ListBox; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.optiongroup.VOptionGroupBase; + +public class VListSelect extends VOptionGroupBase { + + public static final String CLASSNAME = "v-select"; + + private static final int VISIBLE_COUNT = 10; + + protected ListBox select; + + private int lastSelectedIndex = -1; + + public VListSelect() { + super(new ListBox(true), CLASSNAME); + select = getOptionsContainer(); + select.addChangeHandler(this); + select.addClickHandler(this); + select.setStyleName(CLASSNAME + "-select"); + select.setVisibleItemCount(VISIBLE_COUNT); + } + + protected ListBox getOptionsContainer() { + return (ListBox) optionsContainer; + } + + @Override + protected void buildOptions(UIDL uidl) { + select.setMultipleSelect(isMultiselect()); + select.setEnabled(!isDisabled() && !isReadonly()); + select.clear(); + if (!isMultiselect() && isNullSelectionAllowed() + && !isNullSelectionItemAvailable()) { + // can't unselect last item in singleselect mode + select.addItem("", (String) null); + } + for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) { + final UIDL optionUidl = (UIDL) i.next(); + select.addItem(optionUidl.getStringAttribute("caption"), + optionUidl.getStringAttribute("key")); + if (optionUidl.hasAttribute("selected")) { + int itemIndex = select.getItemCount() - 1; + select.setItemSelected(itemIndex, true); + lastSelectedIndex = itemIndex; + } + } + if (getRows() > 0) { + select.setVisibleItemCount(getRows()); + } + } + + @Override + protected String[] getSelectedItems() { + final ArrayList<String> selectedItemKeys = new ArrayList<String>(); + for (int i = 0; i < select.getItemCount(); i++) { + if (select.isItemSelected(i)) { + selectedItemKeys.add(select.getValue(i)); + } + } + return selectedItemKeys.toArray(new String[selectedItemKeys.size()]); + } + + @Override + public void onChange(ChangeEvent event) { + final int si = select.getSelectedIndex(); + if (si == -1 && !isNullSelectionAllowed()) { + select.setSelectedIndex(lastSelectedIndex); + } else { + lastSelectedIndex = si; + if (isMultiselect()) { + client.updateVariable(paintableId, "selected", + getSelectedItems(), isImmediate()); + } else { + client.updateVariable(paintableId, "selected", + new String[] { "" + getSelectedItem() }, isImmediate()); + } + } + } + + @Override + public void setHeight(String height) { + select.setHeight(height); + super.setHeight(height); + } + + @Override + public void setWidth(String width) { + select.setWidth(width); + super.setWidth(width); + } + + @Override + protected void setTabIndex(int tabIndex) { + getOptionsContainer().setTabIndex(tabIndex); + } + + @Override + public void focus() { + select.setFocus(true); + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBar.java b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBar.java new file mode 100644 index 0000000000..c69845d33b --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBar.java @@ -0,0 +1,532 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.menubar; + +/* + * Copyright 2007 Google Inc. + * + * 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. + */ + +// COPIED HERE DUE package privates in GWT +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.user.client.Command; +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.ui.PopupListener; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ui.VOverlay; + +/** + * A standard menu bar widget. A menu bar can contain any number of menu items, + * each of which can either fire a {@link com.google.gwt.user.client.Command} or + * open a cascaded menu bar. + * + * <p> + * <img class='gallery' src='MenuBar.png'/> + * </p> + * + * <h3>CSS Style Rules</h3> + * <ul class='css'> + * <li>.gwt-MenuBar { the menu bar itself }</li> + * <li>.gwt-MenuBar .gwt-MenuItem { menu items }</li> + * <li> + * .gwt-MenuBar .gwt-MenuItem-selected { selected menu items }</li> + * </ul> + * + * <p> + * <h3>Example</h3> + * {@example com.google.gwt.examples.MenuBarExample} + * </p> + * + * @deprecated + */ +@Deprecated +public class MenuBar extends Widget implements PopupListener { + + private final Element body; + private final ArrayList<MenuItem> items = new ArrayList<MenuItem>(); + private MenuBar parentMenu; + private PopupPanel popup; + private MenuItem selectedItem; + private MenuBar shownChildMenu; + private final boolean vertical; + private boolean autoOpen; + + /** + * Creates an empty horizontal menu bar. + */ + public MenuBar() { + this(false); + } + + /** + * Creates an empty menu bar. + * + * @param vertical + * <code>true</code> to orient the menu bar vertically + */ + public MenuBar(boolean vertical) { + super(); + + final Element table = DOM.createTable(); + body = DOM.createTBody(); + DOM.appendChild(table, body); + + if (!vertical) { + final Element tr = DOM.createTR(); + DOM.appendChild(body, tr); + } + + this.vertical = vertical; + + final Element outer = DOM.createDiv(); + DOM.appendChild(outer, table); + setElement(outer); + + sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT); + setStyleName("gwt-MenuBar"); + } + + /** + * Adds a menu item to the bar. + * + * @param item + * the item to be added + */ + public void addItem(MenuItem item) { + Element tr; + if (vertical) { + tr = DOM.createTR(); + DOM.appendChild(body, tr); + } else { + tr = DOM.getChild(body, 0); + } + + DOM.appendChild(tr, item.getElement()); + + item.setParentMenu(this); + item.setSelectionStyle(false); + items.add(item); + } + + /** + * Adds a menu item to the bar, that will fire the given command when it is + * selected. + * + * @param text + * the item's text + * @param asHTML + * <code>true</code> to treat the specified text as html + * @param cmd + * the command to be fired + * @return the {@link MenuItem} object created + */ + public MenuItem addItem(String text, boolean asHTML, Command cmd) { + final MenuItem item = new MenuItem(text, asHTML, cmd); + addItem(item); + return item; + } + + /** + * Adds a menu item to the bar, that will open the specified menu when it is + * selected. + * + * @param text + * the item's text + * @param asHTML + * <code>true</code> to treat the specified text as html + * @param popup + * the menu to be cascaded from it + * @return the {@link MenuItem} object created + */ + public MenuItem addItem(String text, boolean asHTML, MenuBar popup) { + final MenuItem item = new MenuItem(text, asHTML, popup); + addItem(item); + return item; + } + + /** + * Adds a menu item to the bar, that will fire the given command when it is + * selected. + * + * @param text + * the item's text + * @param cmd + * the command to be fired + * @return the {@link MenuItem} object created + */ + public MenuItem addItem(String text, Command cmd) { + final MenuItem item = new MenuItem(text, cmd); + addItem(item); + return item; + } + + /** + * Adds a menu item to the bar, that will open the specified menu when it is + * selected. + * + * @param text + * the item's text + * @param popup + * the menu to be cascaded from it + * @return the {@link MenuItem} object created + */ + public MenuItem addItem(String text, MenuBar popup) { + final MenuItem item = new MenuItem(text, popup); + addItem(item); + return item; + } + + /** + * Removes all menu items from this menu bar. + */ + public void clearItems() { + final Element container = getItemContainerElement(); + while (DOM.getChildCount(container) > 0) { + DOM.removeChild(container, DOM.getChild(container, 0)); + } + items.clear(); + } + + /** + * Gets whether this menu bar's child menus will open when the mouse is + * moved over it. + * + * @return <code>true</code> if child menus will auto-open + */ + public boolean getAutoOpen() { + return autoOpen; + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + + final MenuItem item = findItem(DOM.eventGetTarget(event)); + switch (DOM.eventGetType(event)) { + case Event.ONCLICK: { + // Fire an item's command when the user clicks on it. + if (item != null) { + doItemAction(item, true); + } + break; + } + + case Event.ONMOUSEOVER: { + if (item != null) { + itemOver(item); + } + break; + } + + case Event.ONMOUSEOUT: { + if (item != null) { + itemOver(null); + } + break; + } + } + } + + @Override + public void onPopupClosed(PopupPanel sender, boolean autoClosed) { + // If the menu popup was auto-closed, close all of its parents as well. + if (autoClosed) { + closeAllParents(); + } + + // When the menu popup closes, remember that no item is + // currently showing a popup menu. + onHide(); + shownChildMenu = null; + popup = null; + } + + /** + * Removes the specified menu item from the bar. + * + * @param item + * the item to be removed + */ + public void removeItem(MenuItem item) { + final int idx = items.indexOf(item); + if (idx == -1) { + return; + } + + final Element container = getItemContainerElement(); + DOM.removeChild(container, DOM.getChild(container, idx)); + items.remove(idx); + } + + /** + * Sets whether this menu bar's child menus will open when the mouse is + * moved over it. + * + * @param autoOpen + * <code>true</code> to cause child menus to auto-open + */ + public void setAutoOpen(boolean autoOpen) { + this.autoOpen = autoOpen; + } + + /** + * Returns a list containing the <code>MenuItem</code> objects in the menu + * bar. If there are no items in the menu bar, then an empty + * <code>List</code> object will be returned. + * + * @return a list containing the <code>MenuItem</code> objects in the menu + * bar + */ + public List<MenuItem> getItems() { + return items; + } + + /** + * Returns the <code>MenuItem</code> that is currently selected + * (highlighted) by the user. If none of the items in the menu are currently + * selected, then <code>null</code> will be returned. + * + * @return the <code>MenuItem</code> that is currently selected, or + * <code>null</code> if no items are currently selected + */ + public MenuItem getSelectedItem() { + return selectedItem; + } + + @Override + protected void onDetach() { + // When the menu is detached, make sure to close all of its children. + if (popup != null) { + popup.hide(); + } + + super.onDetach(); + } + + /* + * Closes all parent menu popups. + */ + void closeAllParents() { + MenuBar curMenu = this; + while (curMenu != null) { + curMenu.close(); + + if ((curMenu.parentMenu == null) && (curMenu.selectedItem != null)) { + curMenu.selectedItem.setSelectionStyle(false); + curMenu.selectedItem = null; + } + + curMenu = curMenu.parentMenu; + } + } + + /* + * Performs the action associated with the given menu item. If the item has + * a popup associated with it, the popup will be shown. If it has a command + * associated with it, and 'fireCommand' is true, then the command will be + * fired. Popups associated with other items will be hidden. + * + * @param item the item whose popup is to be shown. @param fireCommand + * <code>true</code> if the item's command should be fired, + * <code>false</code> otherwise. + */ + protected void doItemAction(final MenuItem item, boolean fireCommand) { + // If the given item is already showing its menu, we're done. + if ((shownChildMenu != null) && (item.getSubMenu() == shownChildMenu)) { + return; + } + + // If another item is showing its menu, then hide it. + if (shownChildMenu != null) { + shownChildMenu.onHide(); + popup.hide(); + } + + // If the item has no popup, optionally fire its command. + if (item.getSubMenu() == null) { + if (fireCommand) { + // Close this menu and all of its parents. + closeAllParents(); + + // Fire the item's command. + final Command cmd = item.getCommand(); + if (cmd != null) { + Scheduler.get().scheduleDeferred(cmd); + } + } + return; + } + + // Ensure that the item is selected. + selectItem(item); + + // Create a new popup for this item, and position it next to + // the item (below if this is a horizontal menu bar, to the + // right if it's a vertical bar). + popup = new VOverlay(true) { + { + setWidget(item.getSubMenu()); + item.getSubMenu().onShow(); + } + + @Override + public boolean onEventPreview(Event event) { + // Hook the popup panel's event preview. We use this to keep it + // from + // auto-hiding when the parent menu is clicked. + switch (DOM.eventGetType(event)) { + case Event.ONCLICK: + // If the event target is part of the parent menu, suppress + // the + // event altogether. + final Element target = DOM.eventGetTarget(event); + final Element parentMenuElement = item.getParentMenu() + .getElement(); + if (DOM.isOrHasChild(parentMenuElement, target)) { + return false; + } + break; + } + + return super.onEventPreview(event); + } + }; + popup.addPopupListener(this); + + if (vertical) { + popup.setPopupPosition( + item.getAbsoluteLeft() + item.getOffsetWidth(), + item.getAbsoluteTop()); + } else { + popup.setPopupPosition(item.getAbsoluteLeft(), + item.getAbsoluteTop() + item.getOffsetHeight()); + } + + shownChildMenu = item.getSubMenu(); + item.getSubMenu().parentMenu = this; + + // Show the popup, ensuring that the menubar's event preview remains on + // top + // of the popup's. + popup.show(); + } + + void itemOver(MenuItem item) { + if (item == null) { + // Don't clear selection if the currently selected item's menu is + // showing. + if ((selectedItem != null) + && (shownChildMenu == selectedItem.getSubMenu())) { + return; + } + } + + // Style the item selected when the mouse enters. + selectItem(item); + + // If child menus are being shown, or this menu is itself + // a child menu, automatically show an item's child menu + // when the mouse enters. + if (item != null) { + if ((shownChildMenu != null) || (parentMenu != null) || autoOpen) { + doItemAction(item, false); + } + } + } + + public void selectItem(MenuItem item) { + if (item == selectedItem) { + return; + } + + if (selectedItem != null) { + selectedItem.setSelectionStyle(false); + } + + if (item != null) { + item.setSelectionStyle(true); + } + + selectedItem = item; + } + + /** + * Closes this menu (if it is a popup). + */ + private void close() { + if (parentMenu != null) { + parentMenu.popup.hide(); + } + } + + private MenuItem findItem(Element hItem) { + for (int i = 0; i < items.size(); ++i) { + final MenuItem item = items.get(i); + if (DOM.isOrHasChild(item.getElement(), hItem)) { + return item; + } + } + + return null; + } + + private Element getItemContainerElement() { + if (vertical) { + return body; + } else { + return DOM.getChild(body, 0); + } + } + + /* + * This method is called when a menu bar is hidden, so that it can hide any + * child popups that are currently being shown. + */ + private void onHide() { + if (shownChildMenu != null) { + shownChildMenu.onHide(); + popup.hide(); + } + } + + /* + * This method is called when a menu bar is shown. + */ + private void onShow() { + // Select the first item when a menu is shown. + if (items.size() > 0) { + selectItem(items.get(0)); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBarConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBarConnector.java new file mode 100644 index 0000000000..372a420203 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBarConnector.java @@ -0,0 +1,204 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.menubar; + +import java.util.Iterator; +import java.util.Stack; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.Command; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.menubar.MenuBarConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.TooltipInfo; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector; +import com.vaadin.terminal.gwt.client.ui.Icon; +import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout; + +@Connect(value = com.vaadin.ui.MenuBar.class, loadStyle = LoadStyle.LAZY) +public class MenuBarConnector extends AbstractComponentConnector implements + Paintable, SimpleManagedLayout { + + /** + * This method must be implemented to update the client-side component from + * UIDL data received from server. + * + * This method is called when the page is loaded for the first time, and + * every time UI changes in the component are received from the server. + */ + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (!isRealUpdate(uidl)) { + return; + } + + getWidget().htmlContentAllowed = uidl + .hasAttribute(MenuBarConstants.HTML_CONTENT_ALLOWED); + + getWidget().openRootOnHover = uidl + .getBooleanAttribute(MenuBarConstants.OPEN_ROOT_MENU_ON_HOWER); + + getWidget().enabled = isEnabled(); + + // For future connections + getWidget().client = client; + getWidget().uidlId = uidl.getId(); + + // Empty the menu every time it receives new information + if (!getWidget().getItems().isEmpty()) { + getWidget().clearItems(); + } + + UIDL options = uidl.getChildUIDL(0); + + if (null != getState() && !getState().isUndefinedWidth()) { + UIDL moreItemUIDL = options.getChildUIDL(0); + StringBuffer itemHTML = new StringBuffer(); + + if (moreItemUIDL.hasAttribute("icon")) { + itemHTML.append("<img src=\"" + + Util.escapeAttribute(client + .translateVaadinUri(moreItemUIDL + .getStringAttribute("icon"))) + + "\" class=\"" + Icon.CLASSNAME + "\" alt=\"\" />"); + } + + String moreItemText = moreItemUIDL.getStringAttribute("text"); + if ("".equals(moreItemText)) { + moreItemText = "►"; + } + itemHTML.append(moreItemText); + + getWidget().moreItem = GWT.create(VMenuBar.CustomMenuItem.class); + getWidget().moreItem.setHTML(itemHTML.toString()); + getWidget().moreItem.setCommand(VMenuBar.emptyCommand); + + getWidget().collapsedRootItems = new VMenuBar(true, getWidget()); + getWidget().moreItem.setSubMenu(getWidget().collapsedRootItems); + getWidget().moreItem.addStyleName(VMenuBar.CLASSNAME + + "-more-menuitem"); + } + + UIDL uidlItems = uidl.getChildUIDL(1); + Iterator<Object> itr = uidlItems.getChildIterator(); + Stack<Iterator<Object>> iteratorStack = new Stack<Iterator<Object>>(); + Stack<VMenuBar> menuStack = new Stack<VMenuBar>(); + VMenuBar currentMenu = getWidget(); + + while (itr.hasNext()) { + UIDL item = (UIDL) itr.next(); + VMenuBar.CustomMenuItem currentItem = null; + + final int itemId = item.getIntAttribute("id"); + + boolean itemHasCommand = item.hasAttribute("command"); + boolean itemIsCheckable = item + .hasAttribute(MenuBarConstants.ATTRIBUTE_CHECKED); + + String itemHTML = getWidget().buildItemHTML(item); + + Command cmd = null; + if (!item.hasAttribute("separator")) { + if (itemHasCommand || itemIsCheckable) { + // Construct a command that fires onMenuClick(int) with the + // item's id-number + cmd = new Command() { + @Override + public void execute() { + getWidget().hostReference.onMenuClick(itemId); + } + }; + } + } + + currentItem = currentMenu.addItem(itemHTML.toString(), cmd); + currentItem.updateFromUIDL(item, client); + + if (item.getChildCount() > 0) { + menuStack.push(currentMenu); + iteratorStack.push(itr); + itr = item.getChildIterator(); + currentMenu = new VMenuBar(true, currentMenu); + client.getVTooltip().connectHandlersToWidget(currentMenu); + // this is the top-level style that also propagates to items - + // any item specific styles are set above in + // currentItem.updateFromUIDL(item, client) + if (getState().hasStyles()) { + for (String style : getState().getStyles()) { + currentMenu.addStyleDependentName(style); + } + } + currentItem.setSubMenu(currentMenu); + } + + while (!itr.hasNext() && !iteratorStack.empty()) { + boolean hasCheckableItem = false; + for (VMenuBar.CustomMenuItem menuItem : currentMenu.getItems()) { + hasCheckableItem = hasCheckableItem + || menuItem.isCheckable(); + } + if (hasCheckableItem) { + currentMenu.addStyleDependentName("check-column"); + } else { + currentMenu.removeStyleDependentName("check-column"); + } + + itr = iteratorStack.pop(); + currentMenu = menuStack.pop(); + } + }// while + + getLayoutManager().setNeedsHorizontalLayout(this); + + }// updateFromUIDL + + @Override + public VMenuBar getWidget() { + return (VMenuBar) super.getWidget(); + } + + @Override + public void layout() { + getWidget().iLayout(); + } + + @Override + public TooltipInfo getTooltipInfo(Element element) { + TooltipInfo info = null; + + // Check content of widget to find tooltip for element + if (element != getWidget().getElement()) { + + VMenuBar.CustomMenuItem item = getWidget().getMenuItemWithElement( + (com.google.gwt.user.client.Element) element); + if (item != null) { + info = item.getTooltip(); + } + } + + // Use default tooltip if nothing found from DOM three + if (info == null) { + info = super.getTooltipInfo(element); + } + + return info; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuItem.java b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuItem.java new file mode 100644 index 0000000000..9579f5a9b0 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuItem.java @@ -0,0 +1,205 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.menubar; + +/* + * Copyright 2007 Google Inc. + * + * 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. + */ + +// COPIED HERE DUE package privates in GWT +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.HasHTML; +import com.google.gwt.user.client.ui.UIObject; + +/** + * A widget that can be placed in a + * {@link com.google.gwt.user.client.ui.MenuBar}. Menu items can either fire a + * {@link com.google.gwt.user.client.Command} when they are clicked, or open a + * cascading sub-menu. + * + * @deprecated + */ +@Deprecated +public class MenuItem extends UIObject implements HasHTML { + + private static final String DEPENDENT_STYLENAME_SELECTED_ITEM = "selected"; + + private Command command; + private MenuBar parentMenu, subMenu; + + /** + * Constructs a new menu item that fires a command when it is selected. + * + * @param text + * the item's text + * @param cmd + * the command to be fired when it is selected + */ + public MenuItem(String text, Command cmd) { + this(text, false); + setCommand(cmd); + } + + /** + * Constructs a new menu item that fires a command when it is selected. + * + * @param text + * the item's text + * @param asHTML + * <code>true</code> to treat the specified text as html + * @param cmd + * the command to be fired when it is selected + */ + public MenuItem(String text, boolean asHTML, Command cmd) { + this(text, asHTML); + setCommand(cmd); + } + + /** + * Constructs a new menu item that cascades to a sub-menu when it is + * selected. + * + * @param text + * the item's text + * @param subMenu + * the sub-menu to be displayed when it is selected + */ + public MenuItem(String text, MenuBar subMenu) { + this(text, false); + setSubMenu(subMenu); + } + + /** + * Constructs a new menu item that cascades to a sub-menu when it is + * selected. + * + * @param text + * the item's text + * @param asHTML + * <code>true</code> to treat the specified text as html + * @param subMenu + * the sub-menu to be displayed when it is selected + */ + public MenuItem(String text, boolean asHTML, MenuBar subMenu) { + this(text, asHTML); + setSubMenu(subMenu); + } + + MenuItem(String text, boolean asHTML) { + setElement(DOM.createTD()); + setSelectionStyle(false); + + if (asHTML) { + setHTML(text); + } else { + setText(text); + } + setStyleName("gwt-MenuItem"); + } + + /** + * Gets the command associated with this item. + * + * @return this item's command, or <code>null</code> if none exists + */ + public Command getCommand() { + return command; + } + + @Override + public String getHTML() { + return DOM.getInnerHTML(getElement()); + } + + /** + * Gets the menu that contains this item. + * + * @return the parent menu, or <code>null</code> if none exists. + */ + public MenuBar getParentMenu() { + return parentMenu; + } + + /** + * Gets the sub-menu associated with this item. + * + * @return this item's sub-menu, or <code>null</code> if none exists + */ + public MenuBar getSubMenu() { + return subMenu; + } + + @Override + public String getText() { + return DOM.getInnerText(getElement()); + } + + /** + * Sets the command associated with this item. + * + * @param cmd + * the command to be associated with this item + */ + public void setCommand(Command cmd) { + command = cmd; + } + + @Override + public void setHTML(String html) { + DOM.setInnerHTML(getElement(), html); + } + + /** + * Sets the sub-menu associated with this item. + * + * @param subMenu + * this item's new sub-menu + */ + public void setSubMenu(MenuBar subMenu) { + this.subMenu = subMenu; + } + + @Override + public void setText(String text) { + DOM.setInnerText(getElement(), text); + } + + void setParentMenu(MenuBar parentMenu) { + this.parentMenu = parentMenu; + } + + void setSelectionStyle(boolean selected) { + if (selected) { + addStyleDependentName(DEPENDENT_STYLENAME_SELECTED_ITEM); + } else { + removeStyleDependentName(DEPENDENT_STYLENAME_SELECTED_ITEM); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/menubar/VMenuBar.java b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/VMenuBar.java new file mode 100644 index 0000000000..9f17b81691 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/VMenuBar.java @@ -0,0 +1,1463 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.menubar; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +import com.google.gwt.user.client.Command; +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.HasHTML; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.menubar.MenuBarConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.TooltipInfo; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.Icon; +import com.vaadin.terminal.gwt.client.ui.SimpleFocusablePanel; +import com.vaadin.terminal.gwt.client.ui.SubPartAware; +import com.vaadin.terminal.gwt.client.ui.VLazyExecutor; +import com.vaadin.terminal.gwt.client.ui.VOverlay; + +public class VMenuBar extends SimpleFocusablePanel implements + CloseHandler<PopupPanel>, KeyPressHandler, KeyDownHandler, + FocusHandler, SubPartAware { + + // The hierarchy of VMenuBar is a bit weird as VMenuBar is the Paintable, + // used for the root menu but also used for the sub menus. + + /** Set the CSS class name to allow styling. */ + public static final String CLASSNAME = "v-menubar"; + + /** For server connections **/ + protected String uidlId; + protected ApplicationConnection client; + + protected final VMenuBar hostReference = this; + protected CustomMenuItem moreItem = null; + + // Only used by the root menu bar + protected VMenuBar collapsedRootItems; + + // Construct an empty command to be used when the item has no command + // associated + protected static final Command emptyCommand = null; + + /** Widget fields **/ + protected boolean subMenu; + protected ArrayList<CustomMenuItem> items; + protected Element containerElement; + protected VOverlay popup; + protected VMenuBar visibleChildMenu; + protected boolean menuVisible = false; + protected VMenuBar parentMenu; + protected CustomMenuItem selected; + + boolean enabled = true; + + private VLazyExecutor iconLoadedExecutioner = new VLazyExecutor(100, + new ScheduledCommand() { + + @Override + public void execute() { + iLayout(true); + } + }); + + boolean openRootOnHover; + + boolean htmlContentAllowed; + + public VMenuBar() { + // Create an empty horizontal menubar + this(false, null); + + // Navigation is only handled by the root bar + addFocusHandler(this); + + /* + * Firefox auto-repeat works correctly only if we use a key press + * handler, other browsers handle it correctly when using a key down + * handler + */ + if (BrowserInfo.get().isGecko()) { + addKeyPressHandler(this); + } else { + addKeyDownHandler(this); + } + } + + public VMenuBar(boolean subMenu, VMenuBar parentMenu) { + + items = new ArrayList<CustomMenuItem>(); + popup = null; + visibleChildMenu = null; + + containerElement = getElement(); + + if (!subMenu) { + setStyleName(CLASSNAME); + } else { + setStyleName(CLASSNAME + "-submenu"); + this.parentMenu = parentMenu; + } + this.subMenu = subMenu; + + sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT + | Event.ONLOAD); + } + + @Override + protected void onDetach() { + super.onDetach(); + if (!subMenu) { + setSelected(null); + hideChildren(); + menuVisible = false; + } + } + + void updateSize() { + // Take from setWidth + if (!subMenu) { + // Only needed for root level menu + hideChildren(); + setSelected(null); + menuVisible = false; + } + } + + /** + * Build the HTML content for a menu item. + * + * @param item + * @return + */ + protected String buildItemHTML(UIDL item) { + // Construct html from the text and the optional icon + StringBuffer itemHTML = new StringBuffer(); + if (item.hasAttribute("separator")) { + itemHTML.append("<span>---</span>"); + } else { + // Add submenu indicator + if (item.getChildCount() > 0) { + String bgStyle = ""; + itemHTML.append("<span class=\"" + CLASSNAME + + "-submenu-indicator\"" + bgStyle + ">►</span>"); + } + + itemHTML.append("<span class=\"" + CLASSNAME + + "-menuitem-caption\">"); + if (item.hasAttribute("icon")) { + itemHTML.append("<img src=\"" + + Util.escapeAttribute(client.translateVaadinUri(item + .getStringAttribute("icon"))) + "\" class=\"" + + Icon.CLASSNAME + "\" alt=\"\" />"); + } + String itemText = item.getStringAttribute("text"); + if (!htmlContentAllowed) { + itemText = Util.escapeHTML(itemText); + } + itemHTML.append(itemText); + itemHTML.append("</span>"); + } + return itemHTML.toString(); + } + + /** + * This is called by the items in the menu and it communicates the + * information to the server + * + * @param clickedItemId + * id of the item that was clicked + */ + public void onMenuClick(int clickedItemId) { + // Updating the state to the server can not be done before + // the server connection is known, i.e., before updateFromUIDL() + // has been called. + if (uidlId != null && client != null) { + // Communicate the user interaction parameters to server. This call + // will initiate an AJAX request to the server. + client.updateVariable(uidlId, "clickedId", clickedItemId, true); + } + } + + /** Widget methods **/ + + /** + * Returns a list of items in this menu + */ + public List<CustomMenuItem> getItems() { + return items; + } + + /** + * Remove all the items in this menu + */ + public void clearItems() { + Element e = getContainerElement(); + while (DOM.getChildCount(e) > 0) { + DOM.removeChild(e, DOM.getChild(e, 0)); + } + items.clear(); + } + + /** + * Returns the containing element of the menu + * + * @return + */ + @Override + public Element getContainerElement() { + return containerElement; + } + + /** + * Add a new item to this menu + * + * @param html + * items text + * @param cmd + * items command + * @return the item created + */ + public CustomMenuItem addItem(String html, Command cmd) { + CustomMenuItem item = GWT.create(CustomMenuItem.class); + item.setHTML(html); + item.setCommand(cmd); + + addItem(item); + return item; + } + + /** + * Add a new item to this menu + * + * @param item + */ + public void addItem(CustomMenuItem item) { + if (items.contains(item)) { + return; + } + DOM.appendChild(getContainerElement(), item.getElement()); + item.setParentMenu(this); + item.setSelected(false); + items.add(item); + } + + public void addItem(CustomMenuItem item, int index) { + if (items.contains(item)) { + return; + } + DOM.insertChild(getContainerElement(), item.getElement(), index); + item.setParentMenu(this); + item.setSelected(false); + items.add(index, item); + } + + /** + * Remove the given item from this menu + * + * @param item + */ + public void removeItem(CustomMenuItem item) { + if (items.contains(item)) { + int index = items.indexOf(item); + + DOM.removeChild(getContainerElement(), + DOM.getChild(getContainerElement(), index)); + items.remove(index); + } + } + + /* + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user + * .client.Event) + */ + @Override + public void onBrowserEvent(Event e) { + super.onBrowserEvent(e); + + // Handle onload events (icon loaded, size changes) + if (DOM.eventGetType(e) == Event.ONLOAD) { + VMenuBar parent = getParentMenu(); + if (parent != null) { + // The onload event for an image in a popup should be sent to + // the parent, which owns the popup + parent.iconLoaded(); + } else { + // Onload events for images in the root menu are handled by the + // root menu itself + iconLoaded(); + } + return; + } + + Element targetElement = DOM.eventGetTarget(e); + CustomMenuItem targetItem = null; + for (int i = 0; i < items.size(); i++) { + CustomMenuItem item = items.get(i); + if (DOM.isOrHasChild(item.getElement(), targetElement)) { + targetItem = item; + } + } + + if (targetItem != null) { + switch (DOM.eventGetType(e)) { + + case Event.ONCLICK: + if (isEnabled() && targetItem.isEnabled()) { + itemClick(targetItem); + } + if (subMenu) { + // Prevent moving keyboard focus to child menus + VMenuBar parent = parentMenu; + while (parent.getParentMenu() != null) { + parent = parent.getParentMenu(); + } + parent.setFocus(true); + } + + break; + + case Event.ONMOUSEOVER: + LazyCloser.cancelClosing(); + + if (isEnabled() && targetItem.isEnabled()) { + itemOver(targetItem); + } + break; + + case Event.ONMOUSEOUT: + itemOut(targetItem); + LazyCloser.schedule(); + break; + } + } else if (subMenu && DOM.eventGetType(e) == Event.ONCLICK && subMenu) { + // Prevent moving keyboard focus to child menus + VMenuBar parent = parentMenu; + while (parent.getParentMenu() != null) { + parent = parent.getParentMenu(); + } + parent.setFocus(true); + } + } + + private boolean isEnabled() { + return enabled; + } + + private void iconLoaded() { + iconLoadedExecutioner.trigger(); + } + + /** + * When an item is clicked + * + * @param item + */ + public void itemClick(CustomMenuItem item) { + if (item.getCommand() != null) { + setSelected(null); + + if (visibleChildMenu != null) { + visibleChildMenu.hideChildren(); + } + + hideParents(true); + menuVisible = false; + Scheduler.get().scheduleDeferred(item.getCommand()); + + } else { + if (item.getSubMenu() != null + && item.getSubMenu() != visibleChildMenu) { + setSelected(item); + showChildMenu(item); + menuVisible = true; + } else if (!subMenu) { + setSelected(null); + hideChildren(); + menuVisible = false; + } + } + } + + /** + * When the user hovers the mouse over the item + * + * @param item + */ + public void itemOver(CustomMenuItem item) { + if ((openRootOnHover || subMenu || menuVisible) && !item.isSeparator()) { + setSelected(item); + if (!subMenu && openRootOnHover && !menuVisible) { + menuVisible = true; // start opening menus + LazyCloser.prepare(this); + } + } + + if (menuVisible && visibleChildMenu != item.getSubMenu() + && popup != null) { + popup.hide(); + } + + if (menuVisible && item.getSubMenu() != null + && visibleChildMenu != item.getSubMenu()) { + showChildMenu(item); + } + } + + /** + * When the mouse is moved away from an item + * + * @param item + */ + public void itemOut(CustomMenuItem item) { + if (visibleChildMenu != item.getSubMenu()) { + hideChildMenu(item); + setSelected(null); + } else if (visibleChildMenu == null) { + setSelected(null); + } + } + + /** + * Used to autoclose submenus when they the menu is in a mode which opens + * root menus on mouse hover. + */ + private static class LazyCloser extends Timer { + static LazyCloser INSTANCE; + private VMenuBar activeRoot; + + @Override + public void run() { + activeRoot.hideChildren(); + activeRoot.setSelected(null); + activeRoot.menuVisible = false; + activeRoot = null; + } + + public static void cancelClosing() { + if (INSTANCE != null) { + INSTANCE.cancel(); + } + } + + public static void prepare(VMenuBar vMenuBar) { + if (INSTANCE == null) { + INSTANCE = new LazyCloser(); + } + if (INSTANCE.activeRoot == vMenuBar) { + INSTANCE.cancel(); + } else if (INSTANCE.activeRoot != null) { + INSTANCE.cancel(); + INSTANCE.run(); + } + INSTANCE.activeRoot = vMenuBar; + } + + public static void schedule() { + if (INSTANCE != null && INSTANCE.activeRoot != null) { + INSTANCE.schedule(750); + } + } + + } + + /** + * Shows the child menu of an item. The caller must ensure that the item has + * a submenu. + * + * @param item + */ + public void showChildMenu(CustomMenuItem item) { + + int left = 0; + int top = 0; + if (subMenu) { + left = item.getParentMenu().getAbsoluteLeft() + + item.getParentMenu().getOffsetWidth(); + top = item.getAbsoluteTop(); + } else { + left = item.getAbsoluteLeft(); + top = item.getParentMenu().getAbsoluteTop() + + item.getParentMenu().getOffsetHeight(); + } + showChildMenuAt(item, top, left); + } + + protected void showChildMenuAt(CustomMenuItem item, int top, int left) { + final int shadowSpace = 10; + + popup = new VOverlay(true, false, true); + + // Setting owner and handlers to support tooltips. Needed for tooltip + // handling of overlay widgets (will direct queries to parent menu) + if (parentMenu == null) { + popup.setOwner(this); + } else { + VMenuBar parent = parentMenu; + while (parent.getParentMenu() != null) { + parent = parent.getParentMenu(); + } + popup.setOwner(parent); + } + if (client != null) { + client.getVTooltip().connectHandlersToWidget(popup); + } + + popup.setStyleName(CLASSNAME + "-popup"); + popup.setWidget(item.getSubMenu()); + popup.addCloseHandler(this); + popup.addAutoHidePartner(item.getElement()); + + // at 0,0 because otherwise IE7 add extra scrollbars (#5547) + popup.setPopupPosition(0, 0); + + item.getSubMenu().onShow(); + visibleChildMenu = item.getSubMenu(); + item.getSubMenu().setParentMenu(this); + + popup.show(); + + if (left + popup.getOffsetWidth() >= RootPanel.getBodyElement() + .getOffsetWidth() - shadowSpace) { + if (subMenu) { + left = item.getParentMenu().getAbsoluteLeft() + - popup.getOffsetWidth() - shadowSpace; + } else { + left = RootPanel.getBodyElement().getOffsetWidth() + - popup.getOffsetWidth() - shadowSpace; + } + // Accommodate space for shadow + if (left < shadowSpace) { + left = shadowSpace; + } + } + + top = adjustPopupHeight(top, shadowSpace); + + popup.setPopupPosition(left, top); + + } + + private int adjustPopupHeight(int top, final int shadowSpace) { + // Check that the popup will fit the screen + int availableHeight = RootPanel.getBodyElement().getOffsetHeight() + - top - shadowSpace; + int missingHeight = popup.getOffsetHeight() - availableHeight; + if (missingHeight > 0) { + // First move the top of the popup to get more space + // Don't move above top of screen, don't move more than needed + int moveUpBy = Math.min(top - shadowSpace, missingHeight); + + // Update state + top -= moveUpBy; + missingHeight -= moveUpBy; + availableHeight += moveUpBy; + + if (missingHeight > 0) { + int contentWidth = visibleChildMenu.getOffsetWidth(); + + // If there's still not enough room, limit height to fit and add + // a scroll bar + Style style = popup.getElement().getStyle(); + style.setHeight(availableHeight, Unit.PX); + style.setOverflowY(Overflow.SCROLL); + + // Make room for the scroll bar by adjusting the width of the + // popup + style.setWidth(contentWidth + Util.getNativeScrollbarSize(), + Unit.PX); + popup.sizeOrPositionUpdated(); + } + } + return top; + } + + /** + * Hides the submenu of an item + * + * @param item + */ + public void hideChildMenu(CustomMenuItem item) { + if (visibleChildMenu != null + && !(visibleChildMenu == item.getSubMenu())) { + popup.hide(); + } + } + + /** + * When the menu is shown. + */ + public void onShow() { + // remove possible previous selection + if (selected != null) { + selected.setSelected(false); + selected = null; + } + menuVisible = true; + } + + /** + * Listener method, fired when this menu is closed + */ + @Override + public void onClose(CloseEvent<PopupPanel> event) { + hideChildren(); + if (event.isAutoClosed()) { + hideParents(true); + menuVisible = false; + } + visibleChildMenu = null; + popup = null; + } + + /** + * Recursively hide all child menus + */ + public void hideChildren() { + if (visibleChildMenu != null) { + visibleChildMenu.hideChildren(); + popup.hide(); + } + } + + /** + * Recursively hide all parent menus + */ + public void hideParents(boolean autoClosed) { + if (visibleChildMenu != null) { + popup.hide(); + setSelected(null); + menuVisible = !autoClosed; + } + + if (getParentMenu() != null) { + getParentMenu().hideParents(autoClosed); + } + } + + /** + * Returns the parent menu of this menu, or null if this is the top-level + * menu + * + * @return + */ + public VMenuBar getParentMenu() { + return parentMenu; + } + + /** + * Set the parent menu of this menu + * + * @param parent + */ + public void setParentMenu(VMenuBar parent) { + parentMenu = parent; + } + + /** + * Returns the currently selected item of this menu, or null if nothing is + * selected + * + * @return + */ + public CustomMenuItem getSelected() { + return selected; + } + + /** + * Set the currently selected item of this menu + * + * @param item + */ + public void setSelected(CustomMenuItem item) { + // If we had something selected, unselect + if (item != selected && selected != null) { + selected.setSelected(false); + } + // If we have a valid selection, select it + if (item != null) { + item.setSelected(true); + } + + selected = item; + } + + /** + * + * A class to hold information on menu items + * + */ + public static class CustomMenuItem extends Widget implements HasHTML { + + protected String html = null; + protected Command command = null; + protected VMenuBar subMenu = null; + protected VMenuBar parentMenu = null; + protected boolean enabled = true; + protected boolean isSeparator = false; + protected boolean checkable = false; + protected boolean checked = false; + protected String description = null; + + /** + * Default menu item {@link Widget} constructor for GWT.create(). + * + * Use {@link #setHTML(String)} and {@link #setCommand(Command)} after + * constructing a menu item. + */ + public CustomMenuItem() { + this("", null); + } + + /** + * Creates a menu item {@link Widget}. + * + * @param html + * @param cmd + * @deprecated use the default constructor and {@link #setHTML(String)} + * and {@link #setCommand(Command)} instead + */ + @Deprecated + public CustomMenuItem(String html, Command cmd) { + // We need spans to allow inline-block in IE + setElement(DOM.createSpan()); + + setHTML(html); + setCommand(cmd); + setSelected(false); + setStyleName(CLASSNAME + "-menuitem"); + + } + + public void setSelected(boolean selected) { + if (selected && isSelectable()) { + addStyleDependentName("selected"); + // needed for IE6 to have a single style name to match for an + // element + // TODO Can be optimized now that IE6 is not supported any more + if (checkable) { + if (checked) { + removeStyleDependentName("selected-unchecked"); + addStyleDependentName("selected-checked"); + } else { + removeStyleDependentName("selected-checked"); + addStyleDependentName("selected-unchecked"); + } + } + } else { + removeStyleDependentName("selected"); + // needed for IE6 to have a single style name to match for an + // element + removeStyleDependentName("selected-checked"); + removeStyleDependentName("selected-unchecked"); + } + } + + public void setChecked(boolean checked) { + if (checkable && !isSeparator) { + this.checked = checked; + + if (checked) { + addStyleDependentName("checked"); + removeStyleDependentName("unchecked"); + } else { + addStyleDependentName("unchecked"); + removeStyleDependentName("checked"); + } + } else { + this.checked = false; + } + } + + public boolean isChecked() { + return checked; + } + + public void setCheckable(boolean checkable) { + if (checkable && !isSeparator) { + this.checkable = true; + } else { + setChecked(false); + this.checkable = false; + } + } + + public boolean isCheckable() { + return checkable; + } + + /* + * setters and getters for the fields + */ + + public void setSubMenu(VMenuBar subMenu) { + this.subMenu = subMenu; + } + + public VMenuBar getSubMenu() { + return subMenu; + } + + public void setParentMenu(VMenuBar parentMenu) { + this.parentMenu = parentMenu; + } + + public VMenuBar getParentMenu() { + return parentMenu; + } + + public void setCommand(Command command) { + this.command = command; + } + + public Command getCommand() { + return command; + } + + @Override + public String getHTML() { + return html; + } + + @Override + public void setHTML(String html) { + this.html = html; + DOM.setInnerHTML(getElement(), html); + + // Sink the onload event for any icons. The onload + // events are handled by the parent VMenuBar. + Util.sinkOnloadForImages(getElement()); + } + + @Override + public String getText() { + return html; + } + + @Override + public void setText(String text) { + setHTML(Util.escapeHTML(text)); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if (enabled) { + removeStyleDependentName("disabled"); + } else { + addStyleDependentName("disabled"); + } + } + + public boolean isEnabled() { + return enabled; + } + + private void setSeparator(boolean separator) { + isSeparator = separator; + if (separator) { + setStyleName(CLASSNAME + "-separator"); + } else { + setStyleName(CLASSNAME + "-menuitem"); + setEnabled(enabled); + } + } + + public boolean isSeparator() { + return isSeparator; + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + setSeparator(uidl.hasAttribute("separator")); + setEnabled(!uidl + .hasAttribute(MenuBarConstants.ATTRIBUTE_ITEM_DISABLED)); + + if (!isSeparator() + && uidl.hasAttribute(MenuBarConstants.ATTRIBUTE_CHECKED)) { + // if the selected attribute is present (either true or false), + // the item is selectable + setCheckable(true); + setChecked(uidl + .getBooleanAttribute(MenuBarConstants.ATTRIBUTE_CHECKED)); + } else { + setCheckable(false); + } + + if (uidl.hasAttribute(MenuBarConstants.ATTRIBUTE_ITEM_STYLE)) { + String itemStyle = uidl + .getStringAttribute(MenuBarConstants.ATTRIBUTE_ITEM_STYLE); + addStyleDependentName(itemStyle); + } + + if (uidl.hasAttribute(MenuBarConstants.ATTRIBUTE_ITEM_DESCRIPTION)) { + description = uidl + .getStringAttribute(MenuBarConstants.ATTRIBUTE_ITEM_DESCRIPTION); + } + } + + public TooltipInfo getTooltip() { + if (description == null) { + return null; + } + + return new TooltipInfo(description); + } + + /** + * Checks if the item can be selected. + * + * @return true if it is possible to select this item, false otherwise + */ + public boolean isSelectable() { + return !isSeparator() && isEnabled(); + } + + } + + /** + * @author Jouni Koivuviita / Vaadin Ltd. + */ + public void iLayout() { + iLayout(false); + updateSize(); + } + + public void iLayout(boolean iconLoadEvent) { + // Only collapse if there is more than one item in the root menu and the + // menu has an explicit size + if ((getItems().size() > 1 || (collapsedRootItems != null && collapsedRootItems + .getItems().size() > 0)) + && getElement().getStyle().getProperty("width") != null + && moreItem != null) { + + // Measure the width of the "more" item + final boolean morePresent = getItems().contains(moreItem); + addItem(moreItem); + final int moreItemWidth = moreItem.getOffsetWidth(); + if (!morePresent) { + removeItem(moreItem); + } + + int availableWidth = LayoutManager.get(client).getInnerWidth( + getElement()); + + // Used width includes the "more" item if present + int usedWidth = getConsumedWidth(); + int diff = availableWidth - usedWidth; + removeItem(moreItem); + + if (diff < 0) { + // Too many items: collapse last items from root menu + int widthNeeded = usedWidth - availableWidth; + if (!morePresent) { + widthNeeded += moreItemWidth; + } + int widthReduced = 0; + + while (widthReduced < widthNeeded && getItems().size() > 0) { + // Move last root menu item to collapsed menu + CustomMenuItem collapse = getItems().get( + getItems().size() - 1); + widthReduced += collapse.getOffsetWidth(); + removeItem(collapse); + collapsedRootItems.addItem(collapse, 0); + } + } else if (collapsedRootItems.getItems().size() > 0) { + // Space available for items: expand first items from collapsed + // menu + int widthAvailable = diff + moreItemWidth; + int widthGrowth = 0; + + while (widthAvailable > widthGrowth + && collapsedRootItems.getItems().size() > 0) { + // Move first item from collapsed menu to the root menu + CustomMenuItem expand = collapsedRootItems.getItems() + .get(0); + collapsedRootItems.removeItem(expand); + addItem(expand); + widthGrowth += expand.getOffsetWidth(); + if (collapsedRootItems.getItems().size() > 0) { + widthAvailable -= moreItemWidth; + } + if (widthGrowth > widthAvailable) { + removeItem(expand); + collapsedRootItems.addItem(expand, 0); + } else { + widthAvailable = diff + moreItemWidth; + } + } + } + if (collapsedRootItems.getItems().size() > 0) { + addItem(moreItem); + } + } + + // If a popup is open we might need to adjust the shadow as well if an + // icon shown in that popup was loaded + if (popup != null) { + // Forces a recalculation of the shadow size + popup.show(); + } + if (iconLoadEvent) { + // Size have changed if the width is undefined + Util.notifyParentOfSizeChange(this, false); + } + } + + private int getConsumedWidth() { + int w = 0; + for (CustomMenuItem item : getItems()) { + if (!collapsedRootItems.getItems().contains(item)) { + w += item.getOffsetWidth(); + } + } + return w; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google + * .gwt.event.dom.client.KeyPressEvent) + */ + @Override + public void onKeyPress(KeyPressEvent event) { + if (handleNavigation(event.getNativeEvent().getKeyCode(), + event.isControlKeyDown() || event.isMetaKeyDown(), + event.isShiftKeyDown())) { + event.preventDefault(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt + * .event.dom.client.KeyDownEvent) + */ + @Override + public void onKeyDown(KeyDownEvent event) { + if (handleNavigation(event.getNativeEvent().getKeyCode(), + event.isControlKeyDown() || event.isMetaKeyDown(), + event.isShiftKeyDown())) { + event.preventDefault(); + } + } + + /** + * Get the key that moves the selection upwards. By default it is the up + * arrow key but by overriding this you can change the key to whatever you + * want. + * + * @return The keycode of the key + */ + protected int getNavigationUpKey() { + return KeyCodes.KEY_UP; + } + + /** + * Get the key that moves the selection downwards. By default it is the down + * arrow key but by overriding this you can change the key to whatever you + * want. + * + * @return The keycode of the key + */ + protected int getNavigationDownKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Get the key that moves the selection left. By default it is the left + * arrow key but by overriding this you can change the key to whatever you + * want. + * + * @return The keycode of the key + */ + protected int getNavigationLeftKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * Get the key that moves the selection right. By default it is the right + * arrow key but by overriding this you can change the key to whatever you + * want. + * + * @return The keycode of the key + */ + protected int getNavigationRightKey() { + return KeyCodes.KEY_RIGHT; + } + + /** + * Get the key that selects a menu item. By default it is the Enter key but + * by overriding this you can change the key to whatever you want. + * + * @return + */ + protected int getNavigationSelectKey() { + return KeyCodes.KEY_ENTER; + } + + /** + * Get the key that closes the menu. By default it is the escape key but by + * overriding this yoy can change the key to whatever you want. + * + * @return + */ + protected int getCloseMenuKey() { + return KeyCodes.KEY_ESCAPE; + } + + /** + * Handles the keyboard events handled by the MenuBar + * + * @param event + * The keyboard event received + * @return true iff the navigation event was handled + */ + public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { + + // If tab or shift+tab close menus + if (keycode == KeyCodes.KEY_TAB) { + setSelected(null); + hideChildren(); + menuVisible = false; + return false; + } + + if (ctrl || shift || !isEnabled()) { + // Do not handle tab key, nor ctrl keys + return false; + } + + if (keycode == getNavigationLeftKey()) { + if (getSelected() == null) { + // If nothing is selected then select the last item + setSelected(items.get(items.size() - 1)); + if (!getSelected().isSelectable()) { + handleNavigation(keycode, ctrl, shift); + } + } else if (visibleChildMenu == null && getParentMenu() == null) { + // If this is the root menu then move to the left + int idx = items.indexOf(getSelected()); + if (idx > 0) { + setSelected(items.get(idx - 1)); + } else { + setSelected(items.get(items.size() - 1)); + } + + if (!getSelected().isSelectable()) { + handleNavigation(keycode, ctrl, shift); + } + } else if (visibleChildMenu != null) { + // Redirect all navigation to the submenu + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + + } else if (getParentMenu().getParentMenu() == null) { + // Inside a sub menu, whose parent is a root menu item + VMenuBar root = getParentMenu(); + + root.getSelected().getSubMenu().setSelected(null); + root.hideChildren(); + + // Get the root menus items and select the previous one + int idx = root.getItems().indexOf(root.getSelected()); + idx = idx > 0 ? idx : root.getItems().size(); + CustomMenuItem selected = root.getItems().get(--idx); + + while (selected.isSeparator() || !selected.isEnabled()) { + idx = idx > 0 ? idx : root.getItems().size(); + selected = root.getItems().get(--idx); + } + + root.setSelected(selected); + openMenuAndFocusFirstIfPossible(selected); + } else { + getParentMenu().getSelected().getSubMenu().setSelected(null); + getParentMenu().hideChildren(); + } + + return true; + + } else if (keycode == getNavigationRightKey()) { + + if (getSelected() == null) { + // If nothing is selected then select the first item + setSelected(items.get(0)); + if (!getSelected().isSelectable()) { + handleNavigation(keycode, ctrl, shift); + } + } else if (visibleChildMenu == null && getParentMenu() == null) { + // If this is the root menu then move to the right + int idx = items.indexOf(getSelected()); + + if (idx < items.size() - 1) { + setSelected(items.get(idx + 1)); + } else { + setSelected(items.get(0)); + } + + if (!getSelected().isSelectable()) { + handleNavigation(keycode, ctrl, shift); + } + } else if (visibleChildMenu == null + && getSelected().getSubMenu() != null) { + // If the item has a submenu then show it and move the selection + // there + showChildMenu(getSelected()); + menuVisible = true; + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + } else if (visibleChildMenu == null) { + + // Get the root menu + VMenuBar root = getParentMenu(); + while (root.getParentMenu() != null) { + root = root.getParentMenu(); + } + + // Hide the submenu + root.hideChildren(); + + // Get the root menus items and select the next one + int idx = root.getItems().indexOf(root.getSelected()); + idx = idx < root.getItems().size() - 1 ? idx : -1; + CustomMenuItem selected = root.getItems().get(++idx); + + while (selected.isSeparator() || !selected.isEnabled()) { + idx = idx < root.getItems().size() - 1 ? idx : -1; + selected = root.getItems().get(++idx); + } + + root.setSelected(selected); + openMenuAndFocusFirstIfPossible(selected); + } else if (visibleChildMenu != null) { + // Redirect all navigation to the submenu + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + } + + return true; + + } else if (keycode == getNavigationUpKey()) { + + if (getSelected() == null) { + // If nothing is selected then select the last item + setSelected(items.get(items.size() - 1)); + if (!getSelected().isSelectable()) { + handleNavigation(keycode, ctrl, shift); + } + } else if (visibleChildMenu != null) { + // Redirect all navigation to the submenu + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + } else { + // Select the previous item if possible or loop to the last item + int idx = items.indexOf(getSelected()); + if (idx > 0) { + setSelected(items.get(idx - 1)); + } else { + setSelected(items.get(items.size() - 1)); + } + + if (!getSelected().isSelectable()) { + handleNavigation(keycode, ctrl, shift); + } + } + + return true; + + } else if (keycode == getNavigationDownKey()) { + + if (getSelected() == null) { + // If nothing is selected then select the first item + selectFirstItem(); + } else if (visibleChildMenu == null && getParentMenu() == null) { + // If this is the root menu the show the child menu with arrow + // down, if there is a child menu + openMenuAndFocusFirstIfPossible(getSelected()); + } else if (visibleChildMenu != null) { + // Redirect all navigation to the submenu + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + } else { + // Select the next item if possible or loop to the first item + int idx = items.indexOf(getSelected()); + if (idx < items.size() - 1) { + setSelected(items.get(idx + 1)); + } else { + setSelected(items.get(0)); + } + + if (!getSelected().isSelectable()) { + handleNavigation(keycode, ctrl, shift); + } + } + return true; + + } else if (keycode == getCloseMenuKey()) { + setSelected(null); + hideChildren(); + menuVisible = false; + + } else if (keycode == getNavigationSelectKey()) { + if (getSelected() == null) { + // If nothing is selected then select the first item + selectFirstItem(); + } else if (visibleChildMenu != null) { + // Redirect all navigation to the submenu + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + menuVisible = false; + } else if (visibleChildMenu == null + && getSelected().getSubMenu() != null) { + // If the item has a sub menu then show it and move the + // selection there + openMenuAndFocusFirstIfPossible(getSelected()); + } else { + Command command = getSelected().getCommand(); + if (command != null) { + command.execute(); + } + + setSelected(null); + hideParents(true); + } + } + + return false; + } + + private void selectFirstItem() { + for (int i = 0; i < items.size(); i++) { + CustomMenuItem item = items.get(i); + if (item.isSelectable()) { + setSelected(item); + break; + } + } + } + + private void openMenuAndFocusFirstIfPossible(CustomMenuItem menuItem) { + VMenuBar subMenu = menuItem.getSubMenu(); + if (subMenu == null) { + // No child menu? Nothing to do + return; + } + + VMenuBar parentMenu = menuItem.getParentMenu(); + parentMenu.showChildMenu(menuItem); + + menuVisible = true; + // Select the first item in the newly open submenu + subMenu.selectFirstItem(); + + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + @Override + public void onFocus(FocusEvent event) { + + } + + private final String SUBPART_PREFIX = "item"; + + @Override + public Element getSubPartElement(String subPart) { + int index = Integer + .parseInt(subPart.substring(SUBPART_PREFIX.length())); + CustomMenuItem item = getItems().get(index); + + return item.getElement(); + } + + @Override + public String getSubPartName(Element subElement) { + if (!getElement().isOrHasChild(subElement)) { + return null; + } + + Element menuItemRoot = subElement; + while (menuItemRoot != null && menuItemRoot.getParentElement() != null + && menuItemRoot.getParentElement() != getElement()) { + menuItemRoot = menuItemRoot.getParentElement().cast(); + } + // "menuItemRoot" is now the root of the menu item + + final int itemCount = getItems().size(); + for (int i = 0; i < itemCount; i++) { + if (getItems().get(i).getElement() == menuItemRoot) { + String name = SUBPART_PREFIX + i; + return name; + } + } + return null; + } + + /** + * Get menu item with given DOM element + * + * @param element + * Element used in search + * @return Menu item or null if not found + */ + public CustomMenuItem getMenuItemWithElement(Element element) { + for (int i = 0; i < items.size(); i++) { + CustomMenuItem item = items.get(i); + if (DOM.isOrHasChild(item.getElement(), element)) { + return item; + } + + if (item.getSubMenu() != null) { + item = item.getSubMenu().getMenuItemWithElement(element); + if (item != null) { + return item; + } + } + } + + return null; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/NativeButtonConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/NativeButtonConnector.java new file mode 100644 index 0000000000..2f2c66d1ed --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/NativeButtonConnector.java @@ -0,0 +1,136 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.nativebutton; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.button.ButtonServerRpc; +import com.vaadin.shared.ui.button.ButtonState; +import com.vaadin.terminal.gwt.client.EventHelper; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector; +import com.vaadin.terminal.gwt.client.ui.Icon; +import com.vaadin.ui.NativeButton; + +@Connect(NativeButton.class) +public class NativeButtonConnector extends AbstractComponentConnector implements + BlurHandler, FocusHandler { + + private HandlerRegistration focusHandlerRegistration; + private HandlerRegistration blurHandlerRegistration; + + private FocusAndBlurServerRpc focusBlurRpc = RpcProxy.create( + FocusAndBlurServerRpc.class, this); + + @Override + public void init() { + super.init(); + + getWidget().buttonRpcProxy = RpcProxy.create(ButtonServerRpc.class, + this); + getWidget().client = getConnection(); + getWidget().paintableId = getConnectorId(); + } + + @Override + public boolean delegateCaptionHandling() { + return false; + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + getWidget().disableOnClick = getState().isDisableOnClick(); + focusHandlerRegistration = EventHelper.updateFocusHandler(this, + focusHandlerRegistration); + blurHandlerRegistration = EventHelper.updateBlurHandler(this, + blurHandlerRegistration); + + // Set text + if (getState().isHtmlContentAllowed()) { + getWidget().setHTML(getState().getCaption()); + } else { + getWidget().setText(getState().getCaption()); + } + + // handle error + if (null != getState().getErrorMessage()) { + if (getWidget().errorIndicatorElement == null) { + getWidget().errorIndicatorElement = DOM.createSpan(); + getWidget().errorIndicatorElement + .setClassName("v-errorindicator"); + } + getWidget().getElement().insertBefore( + getWidget().errorIndicatorElement, + getWidget().captionElement); + + } else if (getWidget().errorIndicatorElement != null) { + getWidget().getElement().removeChild( + getWidget().errorIndicatorElement); + getWidget().errorIndicatorElement = null; + } + + if (getState().getIcon() != null) { + if (getWidget().icon == null) { + getWidget().icon = new Icon(getConnection()); + getWidget().getElement().insertBefore( + getWidget().icon.getElement(), + getWidget().captionElement); + } + getWidget().icon.setUri(getState().getIcon().getURL()); + } else { + if (getWidget().icon != null) { + getWidget().getElement().removeChild( + getWidget().icon.getElement()); + getWidget().icon = null; + } + } + + } + + @Override + public VNativeButton getWidget() { + return (VNativeButton) super.getWidget(); + } + + @Override + public ButtonState getState() { + return (ButtonState) super.getState(); + } + + @Override + public void onFocus(FocusEvent event) { + // EventHelper.updateFocusHandler ensures that this is called only when + // there is a listener on server side + focusBlurRpc.focus(); + } + + @Override + public void onBlur(BlurEvent event) { + // EventHelper.updateFocusHandler ensures that this is called only when + // there is a listener on server side + focusBlurRpc.blur(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/VNativeButton.java b/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/VNativeButton.java new file mode 100644 index 0000000000..1ab16eccc4 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/VNativeButton.java @@ -0,0 +1,137 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.nativebutton; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.Button; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.button.ButtonServerRpc; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.Icon; + +public class VNativeButton extends Button implements ClickHandler { + + public static final String CLASSNAME = "v-nativebutton"; + + protected String width = null; + + protected String paintableId; + + protected ApplicationConnection client; + + ButtonServerRpc buttonRpcProxy; + + protected Element errorIndicatorElement; + + protected final Element captionElement = DOM.createSpan(); + + protected Icon icon; + + /** + * Helper flag to handle special-case where the button is moved from under + * mouse while clicking it. In this case mouse leaves the button without + * moving. + */ + private boolean clickPending; + + protected boolean disableOnClick = false; + + public VNativeButton() { + setStyleName(CLASSNAME); + + getElement().appendChild(captionElement); + captionElement.setClassName(getStyleName() + "-caption"); + + addClickHandler(this); + + sinkEvents(Event.ONMOUSEDOWN); + sinkEvents(Event.ONMOUSEUP); + } + + @Override + public void setText(String text) { + captionElement.setInnerText(text); + } + + @Override + public void setHTML(String html) { + captionElement.setInnerHTML(html); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + + if (DOM.eventGetType(event) == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + + } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN + && event.getButton() == Event.BUTTON_LEFT) { + clickPending = true; + } else if (DOM.eventGetType(event) == Event.ONMOUSEMOVE) { + clickPending = false; + } else if (DOM.eventGetType(event) == Event.ONMOUSEOUT) { + if (clickPending) { + click(); + } + clickPending = false; + } + } + + @Override + public void setWidth(String width) { + this.width = width; + super.setWidth(width); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event + * .dom.client.ClickEvent) + */ + @Override + public void onClick(ClickEvent event) { + if (paintableId == null || client == null) { + return; + } + + if (BrowserInfo.get().isSafari()) { + VNativeButton.this.setFocus(true); + } + if (disableOnClick) { + setEnabled(false); + buttonRpcProxy.disableOnClick(); + } + + // Add mouse details + MouseEventDetails details = MouseEventDetailsBuilder + .buildMouseEventDetails(event.getNativeEvent(), getElement()); + buttonRpcProxy.click(details); + + clickPending = false; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/NativeSelectConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/NativeSelectConnector.java new file mode 100644 index 0000000000..e88ed8b2f3 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/NativeSelectConnector.java @@ -0,0 +1,30 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.nativeselect; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ui.optiongroup.OptionGroupBaseConnector; +import com.vaadin.ui.NativeSelect; + +@Connect(NativeSelect.class) +public class NativeSelectConnector extends OptionGroupBaseConnector { + + @Override + public VNativeSelect getWidget() { + return (VNativeSelect) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/VNativeSelect.java b/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/VNativeSelect.java new file mode 100644 index 0000000000..eb77f5f113 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/VNativeSelect.java @@ -0,0 +1,127 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.nativeselect; + +import java.util.ArrayList; +import java.util.Iterator; + +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.user.client.ui.ListBox; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.Field; +import com.vaadin.terminal.gwt.client.ui.optiongroup.VOptionGroupBase; + +public class VNativeSelect extends VOptionGroupBase implements Field { + + public static final String CLASSNAME = "v-select"; + + protected ListBox select; + + private boolean firstValueIsTemporaryNullItem = false; + + public VNativeSelect() { + super(new ListBox(false), CLASSNAME); + select = getOptionsContainer(); + select.setVisibleItemCount(1); + select.addChangeHandler(this); + select.setStyleName(CLASSNAME + "-select"); + + } + + protected ListBox getOptionsContainer() { + return (ListBox) optionsContainer; + } + + @Override + protected void buildOptions(UIDL uidl) { + select.setEnabled(!isDisabled() && !isReadonly()); + select.clear(); + firstValueIsTemporaryNullItem = false; + + if (isNullSelectionAllowed() && !isNullSelectionItemAvailable()) { + // can't unselect last item in singleselect mode + select.addItem("", (String) null); + } + boolean selected = false; + for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) { + final UIDL optionUidl = (UIDL) i.next(); + select.addItem(optionUidl.getStringAttribute("caption"), + optionUidl.getStringAttribute("key")); + if (optionUidl.hasAttribute("selected")) { + select.setItemSelected(select.getItemCount() - 1, true); + selected = true; + } + } + if (!selected && !isNullSelectionAllowed()) { + // null-select not allowed, but value not selected yet; add null and + // remove when something is selected + select.insertItem("", (String) null, 0); + select.setItemSelected(0, true); + firstValueIsTemporaryNullItem = true; + } + } + + @Override + protected String[] getSelectedItems() { + final ArrayList<String> selectedItemKeys = new ArrayList<String>(); + for (int i = 0; i < select.getItemCount(); i++) { + if (select.isItemSelected(i)) { + selectedItemKeys.add(select.getValue(i)); + } + } + return selectedItemKeys.toArray(new String[selectedItemKeys.size()]); + } + + @Override + public void onChange(ChangeEvent event) { + + if (select.isMultipleSelect()) { + client.updateVariable(paintableId, "selected", getSelectedItems(), + isImmediate()); + } else { + client.updateVariable(paintableId, "selected", new String[] { "" + + getSelectedItem() }, isImmediate()); + } + if (firstValueIsTemporaryNullItem) { + // remove temporary empty item + select.removeItem(0); + firstValueIsTemporaryNullItem = false; + } + } + + @Override + public void setHeight(String height) { + select.setHeight(height); + super.setHeight(height); + } + + @Override + public void setWidth(String width) { + select.setWidth(width); + super.setWidth(width); + } + + @Override + protected void setTabIndex(int tabIndex) { + getOptionsContainer().setTabIndex(tabIndex); + } + + @Override + public void focus() { + select.setFocus(true); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/notification/VNotification.java b/client/src/com/vaadin/terminal/gwt/client/ui/notification/VNotification.java new file mode 100644 index 0000000000..451e6badbe --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/notification/VNotification.java @@ -0,0 +1,468 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.notification; + +import java.util.ArrayList; +import java.util.Date; +import java.util.EventObject; +import java.util.Iterator; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.KeyCodes; +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.HTML; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.root.RootConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.VOverlay; + +public class VNotification extends VOverlay { + + public static final int CENTERED = 1; + public static final int CENTERED_TOP = 2; + public static final int CENTERED_BOTTOM = 3; + public static final int TOP_LEFT = 4; + public static final int TOP_RIGHT = 5; + public static final int BOTTOM_LEFT = 6; + public static final int BOTTOM_RIGHT = 7; + + public static final int DELAY_FOREVER = -1; + public static final int DELAY_NONE = 0; + + private static final String STYLENAME = "v-Notification"; + private static final int mouseMoveThreshold = 7; + private static final int Z_INDEX_BASE = 20000; + public static final String STYLE_SYSTEM = "system"; + private static final int FADE_ANIMATION_INTERVAL = 50; // == 20 fps + + private static final ArrayList<VNotification> notifications = new ArrayList<VNotification>(); + + private int startOpacity = 90; + private int fadeMsec = 400; + private int delayMsec = 1000; + + private Timer fader; + private Timer delay; + + private int x = -1; + private int y = -1; + + private String temporaryStyle; + + private ArrayList<EventListener> listeners; + private static final int TOUCH_DEVICE_IDLE_DELAY = 1000; + + /** + * Default constructor. You should use GWT.create instead. + */ + public VNotification() { + setStyleName(STYLENAME); + sinkEvents(Event.ONCLICK); + DOM.setStyleAttribute(getElement(), "zIndex", "" + Z_INDEX_BASE); + } + + /** + * @deprecated Use static {@link #createNotification(int)} instead to enable + * GWT deferred binding. + * + * @param delayMsec + */ + @Deprecated + public VNotification(int delayMsec) { + this(); + this.delayMsec = delayMsec; + if (BrowserInfo.get().isTouchDevice()) { + new Timer() { + @Override + public void run() { + if (isAttached()) { + fade(); + } + } + }.schedule(delayMsec + TOUCH_DEVICE_IDLE_DELAY); + } + } + + /** + * @deprecated Use static {@link #createNotification(int, int, int)} instead + * to enable GWT deferred binding. + * + * @param delayMsec + * @param fadeMsec + * @param startOpacity + */ + @Deprecated + public VNotification(int delayMsec, int fadeMsec, int startOpacity) { + this(delayMsec); + this.fadeMsec = fadeMsec; + this.startOpacity = startOpacity; + } + + public void startDelay() { + DOM.removeEventPreview(this); + if (delayMsec > 0) { + if (delay == null) { + delay = new Timer() { + @Override + public void run() { + fade(); + } + }; + delay.schedule(delayMsec); + } + } else if (delayMsec == 0) { + fade(); + } + } + + @Override + public void show() { + show(CENTERED); + } + + public void show(String style) { + show(CENTERED, style); + } + + public void show(int position) { + show(position, null); + } + + public void show(Widget widget, int position, String style) { + setWidget(widget); + show(position, style); + } + + public void show(String html, int position, String style) { + setWidget(new HTML(html)); + show(position, style); + } + + public void show(int position, String style) { + setOpacity(getElement(), startOpacity); + if (style != null) { + temporaryStyle = style; + addStyleName(style); + addStyleDependentName(style); + } + super.show(); + notifications.add(this); + setPosition(position); + sizeOrPositionUpdated(); + /** + * Android 4 fails to render notifications correctly without a little + * nudge (#8551) + */ + if (BrowserInfo.get().isAndroid()) { + Util.setStyleTemporarily(getElement(), "display", "none"); + } + } + + @Override + public void hide() { + DOM.removeEventPreview(this); + cancelDelay(); + cancelFade(); + if (temporaryStyle != null) { + removeStyleName(temporaryStyle); + removeStyleDependentName(temporaryStyle); + temporaryStyle = null; + } + super.hide(); + notifications.remove(this); + fireEvent(new HideEvent(this)); + } + + public void fade() { + DOM.removeEventPreview(this); + cancelDelay(); + if (fader == null) { + fader = new Timer() { + private final long start = new Date().getTime(); + + @Override + public void run() { + /* + * To make animation smooth, don't count that event happens + * on time. Reduce opacity according to the actual time + * spent instead of fixed decrement. + */ + long now = new Date().getTime(); + long timeEplaced = now - start; + float remainingFraction = 1 - timeEplaced + / (float) fadeMsec; + int opacity = (int) (startOpacity * remainingFraction); + if (opacity <= 0) { + cancel(); + hide(); + if (BrowserInfo.get().isOpera()) { + // tray notification on opera needs to explicitly + // define + // size, reset it + DOM.setStyleAttribute(getElement(), "width", ""); + DOM.setStyleAttribute(getElement(), "height", ""); + } + } else { + setOpacity(getElement(), opacity); + } + } + }; + fader.scheduleRepeating(FADE_ANIMATION_INTERVAL); + } + } + + public void setPosition(int position) { + final Element el = getElement(); + DOM.setStyleAttribute(el, "top", ""); + DOM.setStyleAttribute(el, "left", ""); + DOM.setStyleAttribute(el, "bottom", ""); + DOM.setStyleAttribute(el, "right", ""); + switch (position) { + case TOP_LEFT: + DOM.setStyleAttribute(el, "top", "0px"); + DOM.setStyleAttribute(el, "left", "0px"); + break; + case TOP_RIGHT: + DOM.setStyleAttribute(el, "top", "0px"); + DOM.setStyleAttribute(el, "right", "0px"); + break; + case BOTTOM_RIGHT: + DOM.setStyleAttribute(el, "position", "absolute"); + if (BrowserInfo.get().isOpera()) { + // tray notification on opera needs explicitly defined size + DOM.setStyleAttribute(el, "width", getOffsetWidth() + "px"); + DOM.setStyleAttribute(el, "height", getOffsetHeight() + "px"); + } + DOM.setStyleAttribute(el, "bottom", "0px"); + DOM.setStyleAttribute(el, "right", "0px"); + break; + case BOTTOM_LEFT: + DOM.setStyleAttribute(el, "bottom", "0px"); + DOM.setStyleAttribute(el, "left", "0px"); + break; + case CENTERED_TOP: + center(); + DOM.setStyleAttribute(el, "top", "0px"); + break; + case CENTERED_BOTTOM: + center(); + DOM.setStyleAttribute(el, "top", ""); + DOM.setStyleAttribute(el, "bottom", "0px"); + break; + default: + case CENTERED: + center(); + break; + } + } + + private void cancelFade() { + if (fader != null) { + fader.cancel(); + fader = null; + } + } + + private void cancelDelay() { + if (delay != null) { + delay.cancel(); + delay = null; + } + } + + private void setOpacity(Element el, int opacity) { + DOM.setStyleAttribute(el, "opacity", "" + (opacity / 100.0)); + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(el, "filter", "Alpha(opacity=" + opacity + + ")"); + } + } + + @Override + public void onBrowserEvent(Event event) { + DOM.removeEventPreview(this); + if (fader == null) { + fade(); + } + } + + @Override + public boolean onEventPreview(Event event) { + int type = DOM.eventGetType(event); + // "modal" + if (delayMsec == -1 || temporaryStyle == STYLE_SYSTEM) { + if (type == Event.ONCLICK) { + if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) { + fade(); + return false; + } + } else if (type == Event.ONKEYDOWN + && event.getKeyCode() == KeyCodes.KEY_ESCAPE) { + fade(); + return false; + } + if (temporaryStyle == STYLE_SYSTEM) { + return true; + } else { + return false; + } + } + // default + switch (type) { + case Event.ONMOUSEMOVE: + + if (x < 0) { + x = DOM.eventGetClientX(event); + y = DOM.eventGetClientY(event); + } else if (Math.abs(DOM.eventGetClientX(event) - x) > mouseMoveThreshold + || Math.abs(DOM.eventGetClientY(event) - y) > mouseMoveThreshold) { + startDelay(); + } + break; + case Event.ONMOUSEDOWN: + case Event.ONMOUSEWHEEL: + case Event.ONSCROLL: + startDelay(); + break; + case Event.ONKEYDOWN: + if (event.getRepeat()) { + return true; + } + startDelay(); + break; + default: + break; + } + return true; + } + + public void addEventListener(EventListener listener) { + if (listeners == null) { + listeners = new ArrayList<EventListener>(); + } + listeners.add(listener); + } + + public void removeEventListener(EventListener listener) { + if (listeners == null) { + return; + } + listeners.remove(listener); + } + + private void fireEvent(HideEvent event) { + if (listeners != null) { + for (Iterator<EventListener> it = listeners.iterator(); it + .hasNext();) { + EventListener l = it.next(); + l.notificationHidden(event); + } + } + } + + public static void showNotification(ApplicationConnection client, + final UIDL notification) { + boolean onlyPlainText = notification + .hasAttribute(RootConstants.NOTIFICATION_HTML_CONTENT_NOT_ALLOWED); + String html = ""; + if (notification + .hasAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_ICON)) { + final String parsedUri = client + .translateVaadinUri(notification + .getStringAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_ICON)); + html += "<img src=\"" + Util.escapeAttribute(parsedUri) + "\" />"; + } + if (notification + .hasAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_CAPTION)) { + String caption = notification + .getStringAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_CAPTION); + if (onlyPlainText) { + caption = Util.escapeHTML(caption); + caption = caption.replaceAll("\\n", "<br />"); + } + html += "<h1>" + caption + "</h1>"; + } + if (notification + .hasAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_MESSAGE)) { + String message = notification + .getStringAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_MESSAGE); + if (onlyPlainText) { + message = Util.escapeHTML(message); + message = message.replaceAll("\\n", "<br />"); + } + html += "<p>" + message + "</p>"; + } + + final String style = notification + .hasAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_STYLE) ? notification + .getStringAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_STYLE) + : null; + final int position = notification + .getIntAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_POSITION); + final int delay = notification + .getIntAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_DELAY); + createNotification(delay).show(html, position, style); + } + + public static VNotification createNotification(int delayMsec) { + final VNotification notification = GWT.create(VNotification.class); + notification.delayMsec = delayMsec; + if (BrowserInfo.get().isTouchDevice()) { + new Timer() { + @Override + public void run() { + if (notification.isAttached()) { + notification.fade(); + } + } + }.schedule(notification.delayMsec + TOUCH_DEVICE_IDLE_DELAY); + } + return notification; + } + + public class HideEvent extends EventObject { + + public HideEvent(Object source) { + super(source); + } + } + + public interface EventListener extends java.util.EventListener { + public void notificationHidden(HideEvent event); + } + + /** + * Moves currently visible notifications to the top of the event preview + * stack. Can be called when opening other overlays such as subwindows to + * ensure the notifications receive the events they need and don't linger + * indefinitely. See #7136. + * + * TODO Should this be a generic Overlay feature instead? + */ + public static void bringNotificationsToFront() { + for (VNotification notification : notifications) { + DOM.removeEventPreview(notification); + DOM.addEventPreview(notification); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupBaseConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupBaseConnector.java new file mode 100644 index 0000000000..4eabdabb36 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupBaseConnector.java @@ -0,0 +1,106 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.optiongroup; + +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector; +import com.vaadin.terminal.gwt.client.ui.nativebutton.VNativeButton; +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; + +public abstract class OptionGroupBaseConnector extends AbstractFieldConnector + implements Paintable { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + + // Save details + getWidget().client = client; + getWidget().paintableId = uidl.getId(); + + if (!isRealUpdate(uidl)) { + return; + } + + getWidget().selectedKeys = uidl.getStringArrayVariableAsSet("selected"); + + getWidget().readonly = isReadOnly(); + getWidget().disabled = !isEnabled(); + getWidget().multiselect = "multi".equals(uidl + .getStringAttribute("selectmode")); + getWidget().immediate = getState().isImmediate(); + getWidget().nullSelectionAllowed = uidl + .getBooleanAttribute("nullselect"); + getWidget().nullSelectionItemAvailable = uidl + .getBooleanAttribute("nullselectitem"); + + if (uidl.hasAttribute("cols")) { + getWidget().cols = uidl.getIntAttribute("cols"); + } + if (uidl.hasAttribute("rows")) { + getWidget().rows = uidl.getIntAttribute("rows"); + } + + final UIDL ops = uidl.getChildUIDL(0); + + if (getWidget().getColumns() > 0) { + getWidget().container.setWidth(getWidget().getColumns() + "em"); + if (getWidget().container != getWidget().optionsContainer) { + getWidget().optionsContainer.setWidth("100%"); + } + } + + getWidget().buildOptions(ops); + + if (uidl.getBooleanAttribute("allownewitem")) { + if (getWidget().newItemField == null) { + getWidget().newItemButton = new VNativeButton(); + getWidget().newItemButton.setText("+"); + getWidget().newItemButton.addClickHandler(getWidget()); + getWidget().newItemField = new VTextField(); + getWidget().newItemField.addKeyPressHandler(getWidget()); + } + getWidget().newItemField.setEnabled(!getWidget().disabled + && !getWidget().readonly); + getWidget().newItemButton.setEnabled(!getWidget().disabled + && !getWidget().readonly); + + if (getWidget().newItemField == null + || getWidget().newItemField.getParent() != getWidget().container) { + getWidget().container.add(getWidget().newItemField); + getWidget().container.add(getWidget().newItemButton); + final int w = getWidget().container.getOffsetWidth() + - getWidget().newItemButton.getOffsetWidth(); + getWidget().newItemField.setWidth(Math.max(w, 0) + "px"); + } + } else if (getWidget().newItemField != null) { + getWidget().container.remove(getWidget().newItemField); + getWidget().container.remove(getWidget().newItemButton); + } + + getWidget().setTabIndex( + uidl.hasAttribute("tabindex") ? uidl + .getIntAttribute("tabindex") : 0); + + } + + @Override + public VOptionGroupBase getWidget() { + return (VOptionGroupBase) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupConnector.java new file mode 100644 index 0000000000..a15ad3b163 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupConnector.java @@ -0,0 +1,80 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.optiongroup; + +import java.util.ArrayList; + +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.EventId; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.optiongroup.OptionGroupConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.ui.OptionGroup; + +@Connect(OptionGroup.class) +public class OptionGroupConnector extends OptionGroupBaseConnector { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + getWidget().htmlContentAllowed = uidl + .hasAttribute(OptionGroupConstants.HTML_CONTENT_ALLOWED); + + super.updateFromUIDL(uidl, client); + + getWidget().sendFocusEvents = client.hasEventListeners(this, + EventId.FOCUS); + getWidget().sendBlurEvents = client.hasEventListeners(this, + EventId.BLUR); + + if (getWidget().focusHandlers != null) { + for (HandlerRegistration reg : getWidget().focusHandlers) { + reg.removeHandler(); + } + getWidget().focusHandlers.clear(); + getWidget().focusHandlers = null; + + for (HandlerRegistration reg : getWidget().blurHandlers) { + reg.removeHandler(); + } + getWidget().blurHandlers.clear(); + getWidget().blurHandlers = null; + } + + if (getWidget().sendFocusEvents || getWidget().sendBlurEvents) { + getWidget().focusHandlers = new ArrayList<HandlerRegistration>(); + getWidget().blurHandlers = new ArrayList<HandlerRegistration>(); + + // add focus and blur handlers to checkboxes / radio buttons + for (Widget wid : getWidget().panel) { + if (wid instanceof CheckBox) { + getWidget().focusHandlers.add(((CheckBox) wid) + .addFocusHandler(getWidget())); + getWidget().blurHandlers.add(((CheckBox) wid) + .addBlurHandler(getWidget())); + } + } + } + } + + @Override + public VOptionGroup getWidget() { + return (VOptionGroup) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroup.java b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroup.java new file mode 100644 index 0000000000..710a1c8e63 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroup.java @@ -0,0 +1,211 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.optiongroup; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.LoadEvent; +import com.google.gwt.event.dom.client.LoadHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.FocusWidget; +import com.google.gwt.user.client.ui.Focusable; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.RadioButton; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.EventId; +import com.vaadin.shared.ui.optiongroup.OptionGroupConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.Icon; +import com.vaadin.terminal.gwt.client.ui.checkbox.VCheckBox; + +public class VOptionGroup extends VOptionGroupBase implements FocusHandler, + BlurHandler { + + public static final String CLASSNAME = "v-select-optiongroup"; + + protected final Panel panel; + + private final Map<CheckBox, String> optionsToKeys; + + protected boolean sendFocusEvents = false; + protected boolean sendBlurEvents = false; + protected List<HandlerRegistration> focusHandlers = null; + protected List<HandlerRegistration> blurHandlers = null; + + private final LoadHandler iconLoadHandler = new LoadHandler() { + @Override + public void onLoad(LoadEvent event) { + Util.notifyParentOfSizeChange(VOptionGroup.this, true); + } + }; + + /** + * used to check whether a blur really was a blur of the complete + * optiongroup: if a control inside this optiongroup gains focus right after + * blur of another control inside this optiongroup (meaning: if onFocus + * fires after onBlur has fired), the blur and focus won't be sent to the + * server side as only a focus change inside this optiongroup occured + */ + private boolean blurOccured = false; + + protected boolean htmlContentAllowed = false; + + public VOptionGroup() { + super(CLASSNAME); + panel = (Panel) optionsContainer; + optionsToKeys = new HashMap<CheckBox, String>(); + } + + /* + * Return true if no elements were changed, false otherwise. + */ + @Override + protected void buildOptions(UIDL uidl) { + panel.clear(); + for (final Iterator<?> it = uidl.getChildIterator(); it.hasNext();) { + final UIDL opUidl = (UIDL) it.next(); + CheckBox op; + + String itemHtml = opUidl.getStringAttribute("caption"); + if (!htmlContentAllowed) { + itemHtml = Util.escapeHTML(itemHtml); + } + + String icon = opUidl.getStringAttribute("icon"); + if (icon != null && icon.length() != 0) { + String iconUrl = client.translateVaadinUri(icon); + itemHtml = "<img src=\"" + iconUrl + "\" class=\"" + + Icon.CLASSNAME + "\" alt=\"\" />" + itemHtml; + } + + if (isMultiselect()) { + op = new VCheckBox(); + op.setHTML(itemHtml); + } else { + op = new RadioButton(paintableId, itemHtml, true); + op.setStyleName("v-radiobutton"); + } + + if (icon != null && icon.length() != 0) { + Util.sinkOnloadForImages(op.getElement()); + op.addHandler(iconLoadHandler, LoadEvent.getType()); + } + + op.addStyleName(CLASSNAME_OPTION); + op.setValue(opUidl.getBooleanAttribute("selected")); + boolean enabled = !opUidl + .getBooleanAttribute(OptionGroupConstants.ATTRIBUTE_OPTION_DISABLED) + && !isReadonly() && !isDisabled(); + op.setEnabled(enabled); + setStyleName(op.getElement(), + ApplicationConnection.DISABLED_CLASSNAME, !enabled); + op.addClickHandler(this); + optionsToKeys.put(op, opUidl.getStringAttribute("key")); + panel.add(op); + } + } + + @Override + protected String[] getSelectedItems() { + return selectedKeys.toArray(new String[selectedKeys.size()]); + } + + @Override + public void onClick(ClickEvent event) { + super.onClick(event); + if (event.getSource() instanceof CheckBox) { + final boolean selected = ((CheckBox) event.getSource()).getValue(); + final String key = optionsToKeys.get(event.getSource()); + if (!isMultiselect()) { + selectedKeys.clear(); + } + if (selected) { + selectedKeys.add(key); + } else { + selectedKeys.remove(key); + } + client.updateVariable(paintableId, "selected", getSelectedItems(), + isImmediate()); + } + } + + @Override + protected void setTabIndex(int tabIndex) { + for (Iterator<Widget> iterator = panel.iterator(); iterator.hasNext();) { + FocusWidget widget = (FocusWidget) iterator.next(); + widget.setTabIndex(tabIndex); + } + } + + @Override + public void focus() { + Iterator<Widget> iterator = panel.iterator(); + if (iterator.hasNext()) { + ((Focusable) iterator.next()).setFocus(true); + } + } + + @Override + public void onFocus(FocusEvent arg0) { + if (!blurOccured) { + // no blur occured before this focus event + // panel was blurred => fire the event to the server side if + // requested by server side + if (sendFocusEvents) { + client.updateVariable(paintableId, EventId.FOCUS, "", true); + } + } else { + // blur occured before this focus event + // another control inside the panel (checkbox / radio box) was + // blurred => do not fire the focus and set blurOccured to false, so + // blur will not be fired, too + blurOccured = false; + } + } + + @Override + public void onBlur(BlurEvent arg0) { + blurOccured = true; + if (sendBlurEvents) { + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + // check whether blurOccured still is true and then send the + // event out to the server + if (blurOccured) { + client.updateVariable(paintableId, EventId.BLUR, "", + true); + blurOccured = false; + } + } + }); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroupBase.java b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroupBase.java new file mode 100644 index 0000000000..e416e85696 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroupBase.java @@ -0,0 +1,183 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.optiongroup; + +import java.util.Set; + +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +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.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.Field; +import com.vaadin.terminal.gwt.client.ui.nativebutton.VNativeButton; +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; + +public abstract class VOptionGroupBase extends Composite implements Field, + ClickHandler, ChangeHandler, KeyPressHandler, Focusable { + + public static final String CLASSNAME_OPTION = "v-select-option"; + + protected ApplicationConnection client; + + protected String paintableId; + + protected Set<String> selectedKeys; + + protected boolean immediate; + + protected boolean multiselect; + + protected boolean disabled; + + protected boolean readonly; + + protected int cols = 0; + + protected int rows = 0; + + protected boolean nullSelectionAllowed = true; + + protected boolean nullSelectionItemAvailable = false; + + /** + * Widget holding the different options (e.g. ListBox or Panel for radio + * buttons) (optional, fallbacks to container Panel) + */ + protected Widget optionsContainer; + + /** + * Panel containing the component + */ + protected final Panel container; + + protected VTextField newItemField; + + protected VNativeButton newItemButton; + + public VOptionGroupBase(String classname) { + container = new FlowPanel(); + initWidget(container); + optionsContainer = container; + container.setStyleName(classname); + immediate = false; + multiselect = false; + } + + /* + * Call this if you wish to specify your own container for the option + * elements (e.g. SELECT) + */ + public VOptionGroupBase(Widget w, String classname) { + this(classname); + optionsContainer = w; + container.add(optionsContainer); + } + + protected boolean isImmediate() { + return immediate; + } + + protected boolean isMultiselect() { + return multiselect; + } + + protected boolean isDisabled() { + return disabled; + } + + protected boolean isReadonly() { + return readonly; + } + + protected boolean isNullSelectionAllowed() { + return nullSelectionAllowed; + } + + protected boolean isNullSelectionItemAvailable() { + return nullSelectionItemAvailable; + } + + /** + * @return "cols" specified in uidl, 0 if not specified + */ + protected int getColumns() { + return cols; + } + + /** + * @return "rows" specified in uidl, 0 if not specified + */ + + protected int getRows() { + return rows; + } + + abstract protected void setTabIndex(int tabIndex); + + @Override + public void onClick(ClickEvent event) { + if (event.getSource() == newItemButton + && !newItemField.getText().equals("")) { + client.updateVariable(paintableId, "newitem", + newItemField.getText(), true); + newItemField.setText(""); + } + } + + @Override + public void onChange(ChangeEvent event) { + if (multiselect) { + client.updateVariable(paintableId, "selected", getSelectedItems(), + immediate); + } else { + client.updateVariable(paintableId, "selected", new String[] { "" + + getSelectedItem() }, immediate); + } + } + + @Override + public void onKeyPress(KeyPressEvent event) { + if (event.getSource() == newItemField + && event.getCharCode() == KeyCodes.KEY_ENTER) { + newItemButton.click(); + } + } + + protected abstract void buildOptions(UIDL uidl); + + protected abstract String[] getSelectedItems(); + + protected String getSelectedItem() { + final String[] sel = getSelectedItems(); + if (sel.length > 0) { + return sel[0]; + } else { + return null; + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java new file mode 100644 index 0000000000..5da01bf127 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java @@ -0,0 +1,334 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.orderedlayout; + +import java.util.List; + +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.AlignmentInfo; +import com.vaadin.shared.ui.LayoutClickRpc; +import com.vaadin.shared.ui.VMarginInfo; +import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutServerRpc; +import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.DirectionalManagedLayout; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractLayoutConnector; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; +import com.vaadin.terminal.gwt.client.ui.layout.ComponentConnectorLayoutSlot; +import com.vaadin.terminal.gwt.client.ui.layout.VLayoutSlot; + +public abstract class AbstractOrderedLayoutConnector extends + AbstractLayoutConnector implements DirectionalManagedLayout { + + AbstractOrderedLayoutServerRpc rpc; + + private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler( + this) { + + @Override + protected ComponentConnector getChildComponent(Element element) { + return Util.getConnectorForElement(getConnection(), getWidget(), + element); + } + + @Override + protected LayoutClickRpc getLayoutClickRPC() { + return rpc; + }; + + }; + + @Override + public void init() { + super.init(); + rpc = RpcProxy.create(AbstractOrderedLayoutServerRpc.class, this); + getLayoutManager().registerDependency(this, + getWidget().spacingMeasureElement); + } + + @Override + public void onUnregister() { + LayoutManager lm = getLayoutManager(); + + VMeasuringOrderedLayout layout = getWidget(); + lm.unregisterDependency(this, layout.spacingMeasureElement); + + // Unregister child caption listeners + for (ComponentConnector child : getChildComponents()) { + VLayoutSlot slot = layout.getSlotForChild(child.getWidget()); + slot.setCaption(null); + } + } + + @Override + public AbstractOrderedLayoutState getState() { + return (AbstractOrderedLayoutState) super.getState(); + } + + @Override + public void updateCaption(ComponentConnector component) { + VMeasuringOrderedLayout layout = getWidget(); + if (VCaption.isNeeded(component.getState())) { + VLayoutSlot layoutSlot = layout.getSlotForChild(component + .getWidget()); + VCaption caption = layoutSlot.getCaption(); + if (caption == null) { + caption = new VCaption(component, getConnection()); + + Widget widget = component.getWidget(); + + layout.setCaption(widget, caption); + } + caption.updateCaption(); + } else { + layout.setCaption(component.getWidget(), null); + getLayoutManager().setNeedsLayout(this); + } + } + + @Override + public VMeasuringOrderedLayout getWidget() { + return (VMeasuringOrderedLayout) super.getWidget(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + clickEventHandler.handleEventHandlerRegistration(); + + VMeasuringOrderedLayout layout = getWidget(); + + for (ComponentConnector child : getChildComponents()) { + VLayoutSlot slot = layout.getSlotForChild(child.getWidget()); + + AlignmentInfo alignment = new AlignmentInfo(getState() + .getChildData().get(child).getAlignmentBitmask()); + slot.setAlignment(alignment); + + double expandRatio = getState().getChildData().get(child) + .getExpandRatio(); + slot.setExpandRatio(expandRatio); + } + + layout.updateMarginStyleNames(new VMarginInfo(getState() + .getMarginsBitmask())); + + layout.updateSpacingStyleName(getState().isSpacing()); + + getLayoutManager().setNeedsLayout(this); + } + + private int getSizeForInnerSize(int size, boolean isVertical) { + LayoutManager layoutManager = getLayoutManager(); + Element element = getWidget().getElement(); + if (isVertical) { + return size + layoutManager.getBorderHeight(element) + + layoutManager.getPaddingHeight(element); + } else { + return size + layoutManager.getBorderWidth(element) + + layoutManager.getPaddingWidth(element); + } + } + + private static String getSizeProperty(boolean isVertical) { + return isVertical ? "height" : "width"; + } + + private boolean isUndefinedInDirection(boolean isVertical) { + if (isVertical) { + return isUndefinedHeight(); + } else { + return isUndefinedWidth(); + } + } + + private int getInnerSizeInDirection(boolean isVertical) { + if (isVertical) { + return getLayoutManager().getInnerHeight(getWidget().getElement()); + } else { + return getLayoutManager().getInnerWidth(getWidget().getElement()); + } + } + + private void layoutPrimaryDirection() { + VMeasuringOrderedLayout layout = getWidget(); + boolean isVertical = layout.isVertical; + boolean isUndefined = isUndefinedInDirection(isVertical); + + int startPadding = getStartPadding(isVertical); + int endPadding = getEndPadding(isVertical); + int spacingSize = getSpacingInDirection(isVertical); + int allocatedSize; + + if (isUndefined) { + allocatedSize = -1; + } else { + allocatedSize = getInnerSizeInDirection(isVertical); + } + + allocatedSize = layout.layoutPrimaryDirection(spacingSize, + allocatedSize, startPadding, endPadding); + + Style ownStyle = getWidget().getElement().getStyle(); + if (isUndefined) { + int outerSize = getSizeForInnerSize(allocatedSize, isVertical); + ownStyle.setPropertyPx(getSizeProperty(isVertical), outerSize); + reportUndefinedSize(outerSize, isVertical); + } else { + ownStyle.setProperty(getSizeProperty(isVertical), + getDefinedSize(isVertical)); + } + } + + private void reportUndefinedSize(int outerSize, boolean isVertical) { + if (isVertical) { + getLayoutManager().reportOuterHeight(this, outerSize); + } else { + getLayoutManager().reportOuterWidth(this, outerSize); + } + } + + private int getSpacingInDirection(boolean isVertical) { + if (isVertical) { + return getLayoutManager().getOuterHeight( + getWidget().spacingMeasureElement); + } else { + return getLayoutManager().getOuterWidth( + getWidget().spacingMeasureElement); + } + } + + private void layoutSecondaryDirection() { + VMeasuringOrderedLayout layout = getWidget(); + boolean isVertical = layout.isVertical; + boolean isUndefined = isUndefinedInDirection(!isVertical); + + int startPadding = getStartPadding(!isVertical); + int endPadding = getEndPadding(!isVertical); + + int allocatedSize; + if (isUndefined) { + allocatedSize = -1; + } else { + allocatedSize = getInnerSizeInDirection(!isVertical); + } + + allocatedSize = layout.layoutSecondaryDirection(allocatedSize, + startPadding, endPadding); + + Style ownStyle = getWidget().getElement().getStyle(); + + if (isUndefined) { + int outerSize = getSizeForInnerSize(allocatedSize, + !getWidget().isVertical); + ownStyle.setPropertyPx(getSizeProperty(!getWidget().isVertical), + outerSize); + reportUndefinedSize(outerSize, !isVertical); + } else { + ownStyle.setProperty(getSizeProperty(!getWidget().isVertical), + getDefinedSize(!getWidget().isVertical)); + } + } + + private String getDefinedSize(boolean isVertical) { + if (isVertical) { + return getState().getHeight(); + } else { + return getState().getWidth(); + } + } + + private int getStartPadding(boolean isVertical) { + if (isVertical) { + return getLayoutManager().getPaddingTop(getWidget().getElement()); + } else { + return getLayoutManager().getPaddingLeft(getWidget().getElement()); + } + } + + private int getEndPadding(boolean isVertical) { + if (isVertical) { + return getLayoutManager() + .getPaddingBottom(getWidget().getElement()); + } else { + return getLayoutManager().getPaddingRight(getWidget().getElement()); + } + } + + @Override + public void layoutHorizontally() { + if (getWidget().isVertical) { + layoutSecondaryDirection(); + } else { + layoutPrimaryDirection(); + } + } + + @Override + public void layoutVertically() { + if (getWidget().isVertical) { + layoutPrimaryDirection(); + } else { + layoutSecondaryDirection(); + } + } + + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + super.onConnectorHierarchyChange(event); + List<ComponentConnector> previousChildren = event.getOldChildren(); + int currentIndex = 0; + VMeasuringOrderedLayout layout = getWidget(); + + for (ComponentConnector child : getChildComponents()) { + Widget childWidget = child.getWidget(); + VLayoutSlot slot = layout.getSlotForChild(childWidget); + + if (childWidget.getParent() != layout) { + // If the child widget was previously attached to another + // AbstractOrderedLayout a slot might be found that belongs to + // another AbstractOrderedLayout. In this case we discard it and + // create a new slot. + slot = new ComponentConnectorLayoutSlot(getWidget() + .getStylePrimaryName(), child, this); + } + layout.addOrMove(slot, currentIndex++); + if (child.isRelativeWidth()) { + slot.getWrapperElement().getStyle().setWidth(100, Unit.PCT); + } + } + + for (ComponentConnector child : previousChildren) { + if (child.getParent() != this) { + // Remove slot if the connector is no longer a child of this + // layout + layout.removeSlotForWidget(child.getWidget()); + } + } + + }; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/HorizontalLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/HorizontalLayoutConnector.java new file mode 100644 index 0000000000..195622854d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/HorizontalLayoutConnector.java @@ -0,0 +1,30 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.orderedlayout; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.ui.HorizontalLayout; + +@Connect(value = HorizontalLayout.class, loadStyle = LoadStyle.EAGER) +public class HorizontalLayoutConnector extends AbstractOrderedLayoutConnector { + + @Override + public VHorizontalLayout getWidget() { + return (VHorizontalLayout) super.getWidget(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VHorizontalLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VHorizontalLayout.java new file mode 100644 index 0000000000..917384e72d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VHorizontalLayout.java @@ -0,0 +1,26 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.orderedlayout; + +public class VHorizontalLayout extends VMeasuringOrderedLayout { + + public static final String CLASSNAME = "v-horizontallayout"; + + public VHorizontalLayout() { + super(CLASSNAME, false); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VMeasuringOrderedLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VMeasuringOrderedLayout.java new file mode 100644 index 0000000000..ec2c4afa97 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VMeasuringOrderedLayout.java @@ -0,0 +1,253 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.orderedlayout; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.user.client.ui.WidgetCollection; +import com.vaadin.shared.ui.VMarginInfo; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.ui.layout.VLayoutSlot; + +public class VMeasuringOrderedLayout extends ComplexPanel { + + final boolean isVertical; + + final DivElement spacingMeasureElement; + + private Map<Widget, VLayoutSlot> widgetToSlot = new HashMap<Widget, VLayoutSlot>(); + + protected VMeasuringOrderedLayout(String className, boolean isVertical) { + DivElement element = Document.get().createDivElement(); + setElement(element); + + spacingMeasureElement = Document.get().createDivElement(); + Style spacingStyle = spacingMeasureElement.getStyle(); + spacingStyle.setPosition(Position.ABSOLUTE); + getElement().appendChild(spacingMeasureElement); + + setStyleName(className); + this.isVertical = isVertical; + } + + public void addOrMove(VLayoutSlot layoutSlot, int index) { + Widget widget = layoutSlot.getWidget(); + Element wrapperElement = layoutSlot.getWrapperElement(); + + Element containerElement = getElement(); + Node childAtIndex = containerElement.getChild(index); + if (childAtIndex != wrapperElement) { + // Insert at correct location not attached or at wrong location + containerElement.insertBefore(wrapperElement, childAtIndex); + insert(widget, wrapperElement, index, false); + } + + widgetToSlot.put(widget, layoutSlot); + } + + private void togglePrefixedStyleName(String name, boolean enabled) { + if (enabled) { + addStyleDependentName(name); + } else { + removeStyleDependentName(name); + } + } + + void updateMarginStyleNames(VMarginInfo marginInfo) { + togglePrefixedStyleName("margin-top", marginInfo.hasTop()); + togglePrefixedStyleName("margin-right", marginInfo.hasRight()); + togglePrefixedStyleName("margin-bottom", marginInfo.hasBottom()); + togglePrefixedStyleName("margin-left", marginInfo.hasLeft()); + } + + void updateSpacingStyleName(boolean spacingEnabled) { + String styleName = getStylePrimaryName(); + if (spacingEnabled) { + spacingMeasureElement.addClassName(styleName + "-spacing-on"); + spacingMeasureElement.removeClassName(styleName + "-spacing-off"); + } else { + spacingMeasureElement.removeClassName(styleName + "-spacing-on"); + spacingMeasureElement.addClassName(styleName + "-spacing-off"); + } + } + + public void removeSlotForWidget(Widget widget) { + VLayoutSlot slot = getSlotForChild(widget); + VCaption caption = slot.getCaption(); + if (caption != null) { + // Must remove using setCaption to ensure dependencies (layout -> + // caption) are unregistered + slot.setCaption(null); + } + + remove(slot.getWidget()); + getElement().removeChild(slot.getWrapperElement()); + widgetToSlot.remove(widget); + } + + public VLayoutSlot getSlotForChild(Widget widget) { + return widgetToSlot.get(widget); + } + + public void setCaption(Widget child, VCaption caption) { + VLayoutSlot slot = getSlotForChild(child); + + if (caption != null) { + // Logical attach. + getChildren().add(caption); + } + + // Physical attach if not null, also removes old caption + slot.setCaption(caption); + + if (caption != null) { + // Adopt. + adopt(caption); + } + } + + public int layoutPrimaryDirection(int spacingSize, int allocatedSize, + int startPadding, int endPadding) { + int actuallyAllocated = 0; + double totalExpand = 0; + + int childCount = 0; + for (Widget child : this) { + if (child instanceof VCaption) { + continue; + } + childCount++; + + VLayoutSlot slot = getSlotForChild(child); + totalExpand += slot.getExpandRatio(); + + if (!slot.isRelativeInDirection(isVertical)) { + actuallyAllocated += slot.getUsedSizeInDirection(isVertical); + } + } + + actuallyAllocated += spacingSize * (childCount - 1); + + if (allocatedSize == -1) { + allocatedSize = actuallyAllocated; + } + + double unallocatedSpace = Math + .max(0, allocatedSize - actuallyAllocated); + + double currentLocation = startPadding; + + WidgetCollection children = getChildren(); + for (int i = 0; i < children.size(); i++) { + Widget child = children.get(i); + if (child instanceof VCaption) { + continue; + } + + VLayoutSlot slot = getSlotForChild(child); + + double childExpandRatio; + if (totalExpand == 0) { + childExpandRatio = 1d / childCount; + } else { + childExpandRatio = slot.getExpandRatio() / totalExpand; + } + + double extraPixels = unallocatedSpace * childExpandRatio; + double endLocation = currentLocation + extraPixels; + if (!slot.isRelativeInDirection(isVertical)) { + endLocation += slot.getUsedSizeInDirection(isVertical); + } + + /* + * currentLocation and allocatedSpace are used with full precision + * to avoid missing pixels in the end. The pixel dimensions passed + * to the DOM are still rounded. Otherwise e.g. 10.5px start + * position + 10.5px space might be cause the component to go 1px + * beyond the edge as the effect of the browser's rounding may cause + * something similar to 11px + 11px. + * + * It's most efficient to use doubles all the way because native + * javascript emulates other number types using doubles. + */ + double roundedLocation = Math.round(currentLocation); + + /* + * Space is calculated as the difference between rounded start and + * end locations. Just rounding the space would cause e.g. 10.5px + + * 10.5px = 21px -> 11px + 11px = 22px but in this way we get 11px + + * 10px = 21px. + */ + double roundedSpace = Math.round(endLocation) - roundedLocation; + + // Reserve room for the padding if we're at the end + double slotEndMargin; + if (i == children.size() - 1) { + slotEndMargin = endPadding; + } else { + slotEndMargin = 0; + } + + slot.positionInDirection(roundedLocation, roundedSpace, + slotEndMargin, isVertical); + + currentLocation = endLocation + spacingSize; + } + + return allocatedSize; + } + + public int layoutSecondaryDirection(int allocatedSize, int startPadding, + int endPadding) { + int maxSize = 0; + for (Widget child : this) { + if (child instanceof VCaption) { + continue; + } + + VLayoutSlot slot = getSlotForChild(child); + if (!slot.isRelativeInDirection(!isVertical)) { + maxSize = Math.max(maxSize, + slot.getUsedSizeInDirection(!isVertical)); + } + } + + if (allocatedSize == -1) { + allocatedSize = maxSize; + } + + for (Widget child : this) { + if (child instanceof VCaption) { + continue; + } + + VLayoutSlot slot = getSlotForChild(child); + slot.positionInDirection(startPadding, allocatedSize, endPadding, + !isVertical); + } + + return allocatedSize; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VVerticalLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VVerticalLayout.java new file mode 100644 index 0000000000..5c396882f9 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VVerticalLayout.java @@ -0,0 +1,26 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.orderedlayout; + +public class VVerticalLayout extends VMeasuringOrderedLayout { + + public static final String CLASSNAME = "v-verticallayout"; + + public VVerticalLayout() { + super(CLASSNAME, true); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VerticalLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VerticalLayoutConnector.java new file mode 100644 index 0000000000..441ba9c156 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VerticalLayoutConnector.java @@ -0,0 +1,30 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.orderedlayout; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.ui.VerticalLayout; + +@Connect(value = VerticalLayout.class, loadStyle = LoadStyle.EAGER) +public class VerticalLayoutConnector extends AbstractOrderedLayoutConnector { + + @Override + public VVerticalLayout getWidget() { + return (VVerticalLayout) super.getWidget(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/panel/PanelConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/panel/PanelConnector.java new file mode 100644 index 0000000000..c6a695bc2e --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/panel/PanelConnector.java @@ -0,0 +1,258 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.panel; + +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.panel.PanelServerRpc; +import com.vaadin.shared.ui.panel.PanelState; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler; +import com.vaadin.terminal.gwt.client.ui.PostLayoutListener; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler; +import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout; +import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren; +import com.vaadin.ui.Panel; + +@Connect(Panel.class) +public class PanelConnector extends AbstractComponentContainerConnector + implements Paintable, SimpleManagedLayout, PostLayoutListener, + MayScrollChildren { + + private Integer uidlScrollTop; + + private ClickEventHandler clickEventHandler = new ClickEventHandler(this) { + + @Override + protected void fireClick(NativeEvent event, + MouseEventDetails mouseDetails) { + rpc.click(mouseDetails); + } + }; + + private Integer uidlScrollLeft; + + private PanelServerRpc rpc; + + @Override + public void init() { + super.init(); + rpc = RpcProxy.create(PanelServerRpc.class, this); + VPanel panel = getWidget(); + LayoutManager layoutManager = getLayoutManager(); + + layoutManager.registerDependency(this, panel.captionNode); + layoutManager.registerDependency(this, panel.bottomDecoration); + layoutManager.registerDependency(this, panel.contentNode); + } + + @Override + public void onUnregister() { + VPanel panel = getWidget(); + LayoutManager layoutManager = getLayoutManager(); + + layoutManager.unregisterDependency(this, panel.captionNode); + layoutManager.unregisterDependency(this, panel.bottomDecoration); + layoutManager.unregisterDependency(this, panel.contentNode); + } + + @Override + public boolean delegateCaptionHandling() { + return false; + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (isRealUpdate(uidl)) { + + // Handle caption displaying and style names, prior generics. + // Affects size calculations + + // Restore default stylenames + getWidget().contentNode.setClassName(VPanel.CLASSNAME + "-content"); + getWidget().bottomDecoration.setClassName(VPanel.CLASSNAME + + "-deco"); + getWidget().captionNode.setClassName(VPanel.CLASSNAME + "-caption"); + boolean hasCaption = false; + if (getState().getCaption() != null + && !"".equals(getState().getCaption())) { + getWidget().setCaption(getState().getCaption()); + hasCaption = true; + } else { + getWidget().setCaption(""); + getWidget().captionNode.setClassName(VPanel.CLASSNAME + + "-nocaption"); + } + + // Add proper stylenames for all elements. This way we can prevent + // unwanted CSS selector inheritance. + final String captionBaseClass = VPanel.CLASSNAME + + (hasCaption ? "-caption" : "-nocaption"); + final String contentBaseClass = VPanel.CLASSNAME + "-content"; + final String decoBaseClass = VPanel.CLASSNAME + "-deco"; + String captionClass = captionBaseClass; + String contentClass = contentBaseClass; + String decoClass = decoBaseClass; + if (getState().hasStyles()) { + for (String style : getState().getStyles()) { + captionClass += " " + captionBaseClass + "-" + style; + contentClass += " " + contentBaseClass + "-" + style; + decoClass += " " + decoBaseClass + "-" + style; + } + } + getWidget().captionNode.setClassName(captionClass); + getWidget().contentNode.setClassName(contentClass); + getWidget().bottomDecoration.setClassName(decoClass); + + getWidget().makeScrollable(); + } + + if (!isRealUpdate(uidl)) { + return; + } + + clickEventHandler.handleEventHandlerRegistration(); + + getWidget().client = client; + getWidget().id = uidl.getId(); + + if (getState().getIcon() != null) { + getWidget().setIconUri(getState().getIcon().getURL(), client); + } else { + getWidget().setIconUri(null, client); + } + + getWidget().setErrorIndicatorVisible( + null != getState().getErrorMessage()); + + // We may have actions attached to this panel + if (uidl.getChildCount() > 0) { + final int cnt = uidl.getChildCount(); + for (int i = 0; i < cnt; i++) { + UIDL childUidl = uidl.getChildUIDL(i); + if (childUidl.getTag().equals("actions")) { + if (getWidget().shortcutHandler == null) { + getWidget().shortcutHandler = new ShortcutActionHandler( + getConnectorId(), client); + } + getWidget().shortcutHandler.updateActionMap(childUidl); + } + } + } + + if (getState().getScrollTop() != getWidget().scrollTop) { + // Sizes are not yet up to date, so changing the scroll position + // is deferred to after the layout phase + uidlScrollTop = getState().getScrollTop(); + } + + if (getState().getScrollLeft() != getWidget().scrollLeft) { + // Sizes are not yet up to date, so changing the scroll position + // is deferred to after the layout phase + uidlScrollLeft = getState().getScrollLeft(); + } + + // And apply tab index + getWidget().contentNode.setTabIndex(getState().getTabIndex()); + } + + @Override + public void updateCaption(ComponentConnector component) { + // NOP: layouts caption, errors etc not rendered in Panel + } + + @Override + public VPanel getWidget() { + return (VPanel) super.getWidget(); + } + + @Override + public void layout() { + updateSizes(); + } + + void updateSizes() { + VPanel panel = getWidget(); + + LayoutManager layoutManager = getLayoutManager(); + int top = layoutManager.getOuterHeight(panel.captionNode); + int bottom = layoutManager.getInnerHeight(panel.bottomDecoration); + + Style style = panel.getElement().getStyle(); + panel.captionNode.getParentElement().getStyle() + .setMarginTop(-top, Unit.PX); + panel.bottomDecoration.getStyle().setMarginBottom(-bottom, Unit.PX); + style.setPaddingTop(top, Unit.PX); + style.setPaddingBottom(bottom, Unit.PX); + + // Update scroll positions + panel.contentNode.setScrollTop(panel.scrollTop); + panel.contentNode.setScrollLeft(panel.scrollLeft); + // Read actual value back to ensure update logic is correct + panel.scrollTop = panel.contentNode.getScrollTop(); + panel.scrollLeft = panel.contentNode.getScrollLeft(); + } + + @Override + public void postLayout() { + VPanel panel = getWidget(); + if (uidlScrollTop != null) { + panel.contentNode.setScrollTop(uidlScrollTop.intValue()); + // Read actual value back to ensure update logic is correct + // TODO Does this trigger reflows? + panel.scrollTop = panel.contentNode.getScrollTop(); + uidlScrollTop = null; + } + + if (uidlScrollLeft != null) { + panel.contentNode.setScrollLeft(uidlScrollLeft.intValue()); + // Read actual value back to ensure update logic is correct + // TODO Does this trigger reflows? + panel.scrollLeft = panel.contentNode.getScrollLeft(); + uidlScrollLeft = null; + } + } + + @Override + public PanelState getState() { + return (PanelState) super.getState(); + } + + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + super.onConnectorHierarchyChange(event); + // We always have 1 child, unless the child is hidden + Widget newChildWidget = null; + if (getChildComponents().size() == 1) { + ComponentConnector newChild = getChildComponents().get(0); + newChildWidget = newChild.getWidget(); + } + + getWidget().setWidget(newChildWidget); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/panel/VPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/panel/VPanel.java new file mode 100644 index 0000000000..b7a9f3f7c2 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/panel/VPanel.java @@ -0,0 +1,200 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.panel; + +import com.google.gwt.dom.client.DivElement; +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.ui.SimplePanel; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.ui.Icon; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate.TouchScrollHandler; + +public class VPanel extends SimplePanel implements ShortcutActionHandlerOwner, + Focusable { + + public static final String CLASSNAME = "v-panel"; + + ApplicationConnection client; + + String id; + + final Element captionNode = DOM.createDiv(); + + private final Element captionText = DOM.createSpan(); + + private Icon icon; + + final Element bottomDecoration = DOM.createDiv(); + + final Element contentNode = DOM.createDiv(); + + private Element errorIndicatorElement; + + ShortcutActionHandler shortcutHandler; + + int scrollTop; + + int scrollLeft; + + private TouchScrollHandler touchScrollHandler; + + public VPanel() { + super(); + DivElement captionWrap = Document.get().createDivElement(); + captionWrap.appendChild(captionNode); + captionNode.appendChild(captionText); + + captionWrap.setClassName(CLASSNAME + "-captionwrap"); + captionNode.setClassName(CLASSNAME + "-caption"); + contentNode.setClassName(CLASSNAME + "-content"); + bottomDecoration.setClassName(CLASSNAME + "-deco"); + + getElement().appendChild(captionWrap); + + /* + * Make contentNode focusable only by using the setFocus() method. This + * behaviour can be changed by invoking setTabIndex() in the serverside + * implementation + */ + contentNode.setTabIndex(-1); + + getElement().appendChild(contentNode); + + getElement().appendChild(bottomDecoration); + setStyleName(CLASSNAME); + DOM.sinkEvents(getElement(), Event.ONKEYDOWN); + DOM.sinkEvents(contentNode, Event.ONSCROLL | Event.TOUCHEVENTS); + + contentNode.getStyle().setProperty("position", "relative"); + getElement().getStyle().setProperty("overflow", "hidden"); + + makeScrollable(); + } + + /** + * Sets the keyboard focus on the Panel + * + * @param focus + * Should the panel have focus or not. + */ + public void setFocus(boolean focus) { + if (focus) { + getContainerElement().focus(); + } else { + getContainerElement().blur(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Focusable#focus() + */ + + @Override + public void focus() { + setFocus(true); + + } + + @Override + protected Element getContainerElement() { + return contentNode; + } + + void setCaption(String text) { + DOM.setInnerHTML(captionText, text); + } + + void setErrorIndicatorVisible(boolean showError) { + if (showError) { + if (errorIndicatorElement == null) { + errorIndicatorElement = DOM.createSpan(); + DOM.setElementProperty(errorIndicatorElement, "className", + "v-errorindicator"); + DOM.sinkEvents(errorIndicatorElement, Event.MOUSEEVENTS); + sinkEvents(Event.MOUSEEVENTS); + } + DOM.insertBefore(captionNode, errorIndicatorElement, captionText); + } else if (errorIndicatorElement != null) { + DOM.removeChild(captionNode, errorIndicatorElement); + errorIndicatorElement = null; + } + } + + void setIconUri(String iconUri, ApplicationConnection client) { + if (iconUri == null) { + if (icon != null) { + DOM.removeChild(captionNode, icon.getElement()); + icon = null; + } + } else { + if (icon == null) { + icon = new Icon(client); + DOM.insertChild(captionNode, icon.getElement(), 0); + } + icon.setUri(iconUri); + } + } + + @Override + 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); + return; + } + if (type == Event.ONSCROLL) { + int newscrollTop = DOM.getElementPropertyInt(contentNode, + "scrollTop"); + int newscrollLeft = DOM.getElementPropertyInt(contentNode, + "scrollLeft"); + if (client != null + && (newscrollLeft != scrollLeft || newscrollTop != scrollTop)) { + scrollLeft = newscrollLeft; + scrollTop = newscrollTop; + client.updateVariable(id, "scrollTop", scrollTop, false); + client.updateVariable(id, "scrollLeft", scrollLeft, false); + } + } + } + + @Override + public ShortcutActionHandler getShortcutActionHandler() { + return shortcutHandler; + } + + /** + * Ensures the panel is scrollable eg. after style name changes + */ + void makeScrollable() { + if (touchScrollHandler == null) { + touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this); + } + touchScrollHandler.addElement(contentNode); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/PasswordFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/PasswordFieldConnector.java new file mode 100644 index 0000000000..e8aa32f78b --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/PasswordFieldConnector.java @@ -0,0 +1,30 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.passwordfield; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ui.textfield.TextFieldConnector; +import com.vaadin.ui.PasswordField; + +@Connect(PasswordField.class) +public class PasswordFieldConnector extends TextFieldConnector { + + @Override + public VPasswordField getWidget() { + return (VPasswordField) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/VPasswordField.java b/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/VPasswordField.java new file mode 100644 index 0000000000..fc131909b2 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/VPasswordField.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.passwordfield; + +import com.google.gwt.user.client.DOM; +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; + +/** + * This class represents a password field. + * + * @author Vaadin Ltd. + * + */ +public class VPasswordField extends VTextField { + + public VPasswordField() { + super(DOM.createInputPassword()); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/popupview/PopupViewConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/popupview/PopupViewConnector.java new file mode 100644 index 0000000000..398e257375 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/popupview/PopupViewConnector.java @@ -0,0 +1,129 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.popupview; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.VCaptionWrapper; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ui.PostLayoutListener; +import com.vaadin.ui.PopupView; + +@Connect(PopupView.class) +public class PopupViewConnector extends AbstractComponentContainerConnector + implements Paintable, PostLayoutListener { + + private boolean centerAfterLayout = false; + + @Override + public boolean delegateCaptionHandling() { + return false; + } + + /** + * + * + * @see com.vaadin.terminal.gwt.client.ComponentConnector#updateFromUIDL(com.vaadin.terminal.gwt.client.UIDL, + * com.vaadin.terminal.gwt.client.ApplicationConnection) + */ + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (!isRealUpdate(uidl)) { + return; + } + // These are for future server connections + getWidget().client = client; + getWidget().uidlId = uidl.getId(); + + getWidget().hostPopupVisible = uidl + .getBooleanVariable("popupVisibility"); + + getWidget().setHTML(uidl.getStringAttribute("html")); + + if (uidl.hasAttribute("hideOnMouseOut")) { + getWidget().popup.setHideOnMouseOut(uidl + .getBooleanAttribute("hideOnMouseOut")); + } + + // Render the popup if visible and show it. + if (getWidget().hostPopupVisible) { + UIDL popupUIDL = uidl.getChildUIDL(0); + + // showPopupOnTop(popup, hostReference); + getWidget().preparePopup(getWidget().popup); + getWidget().popup.updateFromUIDL(popupUIDL, client); + if (getState().hasStyles()) { + final StringBuffer styleBuf = new StringBuffer(); + final String primaryName = getWidget().popup + .getStylePrimaryName(); + styleBuf.append(primaryName); + for (String style : getState().getStyles()) { + styleBuf.append(" "); + styleBuf.append(primaryName); + styleBuf.append("-"); + styleBuf.append(style); + } + getWidget().popup.setStyleName(styleBuf.toString()); + } else { + getWidget().popup.setStyleName(getWidget().popup + .getStylePrimaryName()); + } + getWidget().showPopup(getWidget().popup); + centerAfterLayout = true; + + // The popup shouldn't be visible, try to hide it. + } else { + getWidget().popup.hide(); + } + }// updateFromUIDL + + @Override + public void updateCaption(ComponentConnector component) { + if (VCaption.isNeeded(component.getState())) { + if (getWidget().popup.captionWrapper != null) { + getWidget().popup.captionWrapper.updateCaption(); + } else { + getWidget().popup.captionWrapper = new VCaptionWrapper( + component, getConnection()); + getWidget().popup.setWidget(getWidget().popup.captionWrapper); + getWidget().popup.captionWrapper.updateCaption(); + } + } else { + if (getWidget().popup.captionWrapper != null) { + getWidget().popup + .setWidget(getWidget().popup.popupComponentWidget); + } + } + } + + @Override + public VPopupView getWidget() { + return (VPopupView) super.getWidget(); + } + + @Override + public void postLayout() { + if (centerAfterLayout) { + centerAfterLayout = false; + getWidget().center(); + } + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/popupview/VPopupView.java b/client/src/com/vaadin/terminal/gwt/client/ui/popupview/VPopupView.java new file mode 100644 index 0000000000..df373c59ba --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/popupview/VPopupView.java @@ -0,0 +1,387 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.popupview; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +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.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +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.ui.Focusable; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.VCaptionWrapper; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; +import com.vaadin.terminal.gwt.client.ui.VOverlay; +import com.vaadin.terminal.gwt.client.ui.richtextarea.VRichTextArea; + +public class VPopupView extends HTML { + + public static final String CLASSNAME = "v-popupview"; + + /** For server-client communication */ + String uidlId; + ApplicationConnection client; + + /** This variable helps to communicate popup visibility to the server */ + boolean hostPopupVisible; + + final CustomPopup popup; + private final Label loading = new Label(); + + /** + * loading constructor + */ + public VPopupView() { + super(); + popup = new CustomPopup(); + + setStyleName(CLASSNAME); + popup.setStyleName(CLASSNAME + "-popup"); + loading.setStyleName(CLASSNAME + "-loading"); + + setHTML(""); + popup.setWidget(loading); + + // When we click to open the popup... + addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + updateState(true); + } + }); + + // ..and when we close it + popup.addCloseHandler(new CloseHandler<PopupPanel>() { + @Override + public void onClose(CloseEvent<PopupPanel> event) { + updateState(false); + } + }); + + popup.setAnimationEnabled(true); + } + + /** + * Update popup visibility to server + * + * @param visibility + */ + private void updateState(boolean visible) { + // If we know the server connection + // then update the current situation + if (uidlId != null && client != null && isAttached()) { + client.updateVariable(uidlId, "popupVisibility", visible, true); + } + } + + void preparePopup(final CustomPopup popup) { + popup.setVisible(false); + popup.show(); + } + + /** + * Determines the correct position for a popup and displays the popup at + * that position. + * + * By default, the popup is shown centered relative to its host component, + * ensuring it is visible on the screen if possible. + * + * Can be overridden to customize the popup position. + * + * @param popup + */ + protected void showPopup(final CustomPopup popup) { + popup.setPopupPosition(0, 0); + + popup.setVisible(true); + } + + void center() { + int windowTop = RootPanel.get().getAbsoluteTop(); + int windowLeft = RootPanel.get().getAbsoluteLeft(); + int windowRight = windowLeft + RootPanel.get().getOffsetWidth(); + int windowBottom = windowTop + RootPanel.get().getOffsetHeight(); + + int offsetWidth = popup.getOffsetWidth(); + int offsetHeight = popup.getOffsetHeight(); + + int hostHorizontalCenter = VPopupView.this.getAbsoluteLeft() + + VPopupView.this.getOffsetWidth() / 2; + int hostVerticalCenter = VPopupView.this.getAbsoluteTop() + + VPopupView.this.getOffsetHeight() / 2; + + int left = hostHorizontalCenter - offsetWidth / 2; + int top = hostVerticalCenter - offsetHeight / 2; + + // Don't show the popup outside the screen. + if ((left + offsetWidth) > windowRight) { + left -= (left + offsetWidth) - windowRight; + } + + if ((top + offsetHeight) > windowBottom) { + top -= (top + offsetHeight) - windowBottom; + } + + if (left < 0) { + left = 0; + } + + if (top < 0) { + top = 0; + } + + popup.setPopupPosition(left, top); + } + + /** + * Make sure that we remove the popup when the main widget is removed. + * + * @see com.google.gwt.user.client.ui.Widget#onUnload() + */ + @Override + protected void onDetach() { + popup.hide(); + super.onDetach(); + } + + private static native void nativeBlur(Element e) + /*-{ + if(e && e.blur) { + e.blur(); + } + }-*/; + + /** + * This class is only protected to enable overriding showPopup, and is + * currently not intended to be extended or otherwise used directly. Its API + * (other than it being a VOverlay) is to be considered private and + * potentially subject to change. + */ + protected class CustomPopup extends VOverlay { + + private ComponentConnector popupComponentPaintable = null; + Widget popupComponentWidget = null; + VCaptionWrapper captionWrapper = null; + + private boolean hasHadMouseOver = false; + private boolean hideOnMouseOut = true; + private final Set<Element> activeChildren = new HashSet<Element>(); + private boolean hiding = false; + + private ShortcutActionHandler shortcutActionHandler; + + public CustomPopup() { + super(true, false, true); // autoHide, not modal, dropshadow + + // Delegate popup keyboard events to the relevant handler. The + // events do not propagate automatically because the popup is + // directly attached to the RootPanel. + addDomHandler(new KeyDownHandler() { + @Override + public void onKeyDown(KeyDownEvent event) { + if (shortcutActionHandler != null) { + shortcutActionHandler.handleKeyboardEvent(Event + .as(event.getNativeEvent())); + } + } + }, KeyDownEvent.getType()); + } + + // For some reason ONMOUSEOUT events are not always received, so we have + // to use ONMOUSEMOVE that doesn't target the popup + @Override + public boolean onEventPreview(Event event) { + Element target = DOM.eventGetTarget(event); + boolean eventTargetsPopup = DOM.isOrHasChild(getElement(), target); + int type = DOM.eventGetType(event); + + // Catch children that use keyboard, so we can unfocus them when + // hiding + if (eventTargetsPopup && type == Event.ONKEYPRESS) { + activeChildren.add(target); + } + + if (eventTargetsPopup && type == Event.ONMOUSEMOVE) { + hasHadMouseOver = true; + } + + if (!eventTargetsPopup && type == Event.ONMOUSEMOVE) { + if (hasHadMouseOver && hideOnMouseOut) { + hide(); + return true; + } + } + + // Was the TAB key released outside of our popup? + if (!eventTargetsPopup && type == Event.ONKEYUP + && event.getKeyCode() == KeyCodes.KEY_TAB) { + // Should we hide on focus out (mouse out)? + if (hideOnMouseOut) { + hide(); + return true; + } + } + + return super.onEventPreview(event); + } + + @Override + public void hide(boolean autoClosed) { + VConsole.log("Hiding popupview"); + hiding = true; + syncChildren(); + if (popupComponentWidget != null && popupComponentWidget != loading) { + remove(popupComponentWidget); + } + hasHadMouseOver = false; + shortcutActionHandler = null; + super.hide(autoClosed); + } + + @Override + public void show() { + hiding = false; + + // Find the shortcut action handler that should handle keyboard + // events from the popup. The events do not propagate automatically + // because the popup is directly attached to the RootPanel. + Widget widget = VPopupView.this; + while (shortcutActionHandler == null && widget != null) { + if (widget instanceof ShortcutActionHandlerOwner) { + shortcutActionHandler = ((ShortcutActionHandlerOwner) widget) + .getShortcutActionHandler(); + } + widget = widget.getParent(); + } + + super.show(); + } + + /** + * Try to sync all known active child widgets to server + */ + public void syncChildren() { + // Notify children with focus + if ((popupComponentWidget instanceof Focusable)) { + ((Focusable) popupComponentWidget).setFocus(false); + } else { + + checkForRTE(popupComponentWidget); + } + + // Notify children that have used the keyboard + for (Element e : activeChildren) { + try { + nativeBlur(e); + } catch (Exception ignored) { + } + } + activeChildren.clear(); + } + + private void checkForRTE(Widget popupComponentWidget2) { + if (popupComponentWidget2 instanceof VRichTextArea) { + ((VRichTextArea) popupComponentWidget2) + .synchronizeContentToServer(); + } else if (popupComponentWidget2 instanceof HasWidgets) { + HasWidgets hw = (HasWidgets) popupComponentWidget2; + Iterator<Widget> iterator = hw.iterator(); + while (iterator.hasNext()) { + checkForRTE(iterator.next()); + } + } + } + + @Override + public boolean remove(Widget w) { + + popupComponentPaintable = null; + popupComponentWidget = null; + captionWrapper = null; + + return super.remove(w); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + + ComponentConnector newPopupComponent = client.getPaintable(uidl + .getChildUIDL(0)); + + if (newPopupComponent != popupComponentPaintable) { + Widget newWidget = newPopupComponent.getWidget(); + setWidget(newWidget); + popupComponentWidget = newWidget; + popupComponentPaintable = newPopupComponent; + } + + } + + public void setHideOnMouseOut(boolean hideOnMouseOut) { + this.hideOnMouseOut = hideOnMouseOut; + } + + /* + * + * We need a hack make popup act as a child of VPopupView in Vaadin's + * component tree, but work in default GWT manner when closing or + * opening. + * + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.Widget#getParent() + */ + @Override + public Widget getParent() { + if (!isAttached() || hiding) { + return super.getParent(); + } else { + return VPopupView.this; + } + } + + @Override + protected void onDetach() { + super.onDetach(); + hiding = false; + } + + @Override + public Element getContainerElement() { + return super.getContainerElement(); + } + + }// class CustomPopup + +}// class VPopupView diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/ProgressIndicatorConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/ProgressIndicatorConnector.java new file mode 100644 index 0000000000..e99a03f01d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/ProgressIndicatorConnector.java @@ -0,0 +1,72 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.progressindicator; + +import com.google.gwt.user.client.DOM; +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector; +import com.vaadin.ui.ProgressIndicator; + +@Connect(ProgressIndicator.class) +public class ProgressIndicatorConnector extends AbstractFieldConnector + implements Paintable { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + + if (!isRealUpdate(uidl)) { + return; + } + + // Save details + getWidget().client = client; + + getWidget().indeterminate = uidl.getBooleanAttribute("indeterminate"); + + if (getWidget().indeterminate) { + String basename = VProgressIndicator.CLASSNAME + "-indeterminate"; + getWidget().addStyleName(basename); + if (!isEnabled()) { + getWidget().addStyleName(basename + "-disabled"); + } else { + getWidget().removeStyleName(basename + "-disabled"); + } + } else { + try { + final float f = Float.parseFloat(uidl + .getStringAttribute("state")); + final int size = Math.round(100 * f); + DOM.setStyleAttribute(getWidget().indicator, "width", size + + "%"); + } catch (final Exception e) { + } + } + + if (isEnabled()) { + getWidget().interval = uidl.getIntAttribute("pollinginterval"); + getWidget().poller.scheduleRepeating(getWidget().interval); + } + } + + @Override + public VProgressIndicator getWidget() { + return (VProgressIndicator) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/VProgressIndicator.java b/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/VProgressIndicator.java new file mode 100644 index 0000000000..5d39389d5b --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/VProgressIndicator.java @@ -0,0 +1,83 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.progressindicator; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Util; + +public class VProgressIndicator extends Widget { + + public static final String CLASSNAME = "v-progressindicator"; + Element wrapper = DOM.createDiv(); + Element indicator = DOM.createDiv(); + protected ApplicationConnection client; + protected final Poller poller; + protected boolean indeterminate = false; + private boolean pollerSuspendedDueDetach; + protected int interval; + + public VProgressIndicator() { + setElement(DOM.createDiv()); + getElement().appendChild(wrapper); + setStyleName(CLASSNAME); + wrapper.appendChild(indicator); + indicator.setClassName(CLASSNAME + "-indicator"); + wrapper.setClassName(CLASSNAME + "-wrapper"); + poller = new Poller(); + } + + @Override + protected void onAttach() { + super.onAttach(); + if (pollerSuspendedDueDetach) { + poller.scheduleRepeating(interval); + } + } + + @Override + protected void onDetach() { + super.onDetach(); + if (interval > 0) { + poller.cancel(); + pollerSuspendedDueDetach = true; + } + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (!visible) { + poller.cancel(); + } + } + + class Poller extends Timer { + + @Override + public void run() { + if (!client.hasActiveRequest() + && Util.isAttachedAndDisplayed(VProgressIndicator.this)) { + client.sendPendingVariableChanges(); + } + } + + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/RichTextAreaConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/RichTextAreaConnector.java new file mode 100644 index 0000000000..9726d43297 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/RichTextAreaConnector.java @@ -0,0 +1,85 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.richtextarea; + +import com.google.gwt.user.client.Event; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.BeforeShortcutActionListener; +import com.vaadin.ui.RichTextArea; + +@Connect(value = RichTextArea.class, loadStyle = LoadStyle.LAZY) +public class RichTextAreaConnector extends AbstractFieldConnector implements + Paintable, BeforeShortcutActionListener { + + @Override + public void updateFromUIDL(final UIDL uidl, ApplicationConnection client) { + getWidget().client = client; + getWidget().id = uidl.getId(); + + if (uidl.hasVariable("text")) { + getWidget().currentValue = uidl.getStringVariable("text"); + if (getWidget().rta.isAttached()) { + getWidget().rta.setHTML(getWidget().currentValue); + } else { + getWidget().html.setHTML(getWidget().currentValue); + } + } + if (isRealUpdate(uidl)) { + getWidget().setEnabled(isEnabled()); + } + + if (!isRealUpdate(uidl)) { + return; + } + + getWidget().setReadOnly(isReadOnly()); + getWidget().immediate = getState().isImmediate(); + int newMaxLength = uidl.hasAttribute("maxLength") ? uidl + .getIntAttribute("maxLength") : -1; + if (newMaxLength >= 0) { + if (getWidget().maxLength == -1) { + getWidget().keyPressHandler = getWidget().rta + .addKeyPressHandler(getWidget()); + } + getWidget().maxLength = newMaxLength; + } else if (getWidget().maxLength != -1) { + getWidget().getElement().setAttribute("maxlength", ""); + getWidget().maxLength = -1; + getWidget().keyPressHandler.removeHandler(); + } + + if (uidl.hasAttribute("selectAll")) { + getWidget().selectAll(); + } + + } + + @Override + public void onBeforeShortcutAction(Event e) { + getWidget().synchronizeContentToServer(); + } + + @Override + public VRichTextArea getWidget() { + return (VRichTextArea) super.getWidget(); + }; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextArea.java b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextArea.java new file mode 100644 index 0000000000..cdd5025e64 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextArea.java @@ -0,0 +1,373 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.richtextarea; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Focusable; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.RichTextArea; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.Field; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate; + +/** + * This class implements a basic client side rich text editor component. + * + * @author Vaadin Ltd. + * + */ +public class VRichTextArea extends Composite implements Field, ChangeHandler, + BlurHandler, KeyPressHandler, KeyDownHandler, Focusable { + + /** + * The input node CSS classname. + */ + public static final String CLASSNAME = "v-richtextarea"; + + protected String id; + + protected ApplicationConnection client; + + boolean immediate = false; + + RichTextArea rta; + + private VRichTextToolbar formatter; + + HTML html = new HTML(); + + private final FlowPanel fp = new FlowPanel(); + + private boolean enabled = true; + + private int extraHorizontalPixels = -1; + private int extraVerticalPixels = -1; + + int maxLength = -1; + + private int toolbarNaturalWidth = 500; + + HandlerRegistration keyPressHandler; + + private ShortcutActionHandlerOwner hasShortcutActionHandler; + + String currentValue = ""; + + private boolean readOnly = false; + + public VRichTextArea() { + createRTAComponents(); + fp.add(formatter); + fp.add(rta); + + initWidget(fp); + setStyleName(CLASSNAME); + + TouchScrollDelegate.enableTouchScrolling(html, html.getElement()); + } + + private void createRTAComponents() { + rta = new RichTextArea(); + rta.setWidth("100%"); + rta.addBlurHandler(this); + rta.addKeyDownHandler(this); + formatter = new VRichTextToolbar(rta); + } + + public void setEnabled(boolean enabled) { + if (this.enabled != enabled) { + // rta.setEnabled(enabled); + swapEditableArea(); + this.enabled = enabled; + } + } + + /** + * Swaps html to rta and visa versa. + */ + private void swapEditableArea() { + if (html.isAttached()) { + fp.remove(html); + if (BrowserInfo.get().isWebkit()) { + fp.remove(formatter); + createRTAComponents(); // recreate new RTA to bypass #5379 + fp.add(formatter); + } + rta.setHTML(currentValue); + fp.add(rta); + } else { + html.setHTML(currentValue); + fp.remove(rta); + fp.add(html); + } + } + + void selectAll() { + /* + * There is a timing issue if trying to select all immediately on first + * render. Simple deferred command is not enough. Using Timer with + * moderated timeout. If this appears to fail on many (most likely slow) + * environments, consider increasing the timeout. + * + * FF seems to require the most time to stabilize its RTA. On Vaadin + * tiergarden test machines, 200ms was not enough always (about 50% + * success rate) - 300 ms was 100% successful. This however was not + * enough on a sluggish old non-virtualized XP test machine. A bullet + * proof solution would be nice, GWT 2.1 might however solve these. At + * least setFocus has a workaround for this kind of issue. + */ + new Timer() { + @Override + public void run() { + rta.getFormatter().selectAll(); + } + }.schedule(320); + } + + void setReadOnly(boolean b) { + if (isReadOnly() != b) { + swapEditableArea(); + readOnly = b; + } + // reset visibility in case enabled state changed and the formatter was + // recreated + formatter.setVisible(!readOnly); + } + + private boolean isReadOnly() { + return readOnly; + } + + // TODO is this really used, or does everything go via onBlur() only? + @Override + public void onChange(ChangeEvent event) { + synchronizeContentToServer(); + } + + /** + * Method is public to let popupview force synchronization on close. + */ + public void synchronizeContentToServer() { + if (client != null && id != null) { + final String html = rta.getHTML(); + if (!html.equals(currentValue)) { + client.updateVariable(id, "text", html, immediate); + currentValue = html; + } + } + } + + @Override + public void onBlur(BlurEvent event) { + synchronizeContentToServer(); + // TODO notify possible server side blur/focus listeners + } + + /** + * @return space used by components paddings and borders + */ + private int getExtraHorizontalPixels() { + if (extraHorizontalPixels < 0) { + detectExtraSizes(); + } + return extraHorizontalPixels; + } + + /** + * @return space used by components paddings and borders + */ + private int getExtraVerticalPixels() { + if (extraVerticalPixels < 0) { + detectExtraSizes(); + } + return extraVerticalPixels; + } + + /** + * Detects space used by components paddings and borders. + */ + private void detectExtraSizes() { + Element clone = Util.cloneNode(getElement(), false); + DOM.setElementAttribute(clone, "id", ""); + DOM.setStyleAttribute(clone, "visibility", "hidden"); + DOM.setStyleAttribute(clone, "position", "absolute"); + // due FF3 bug set size to 10px and later subtract it from extra pixels + DOM.setStyleAttribute(clone, "width", "10px"); + DOM.setStyleAttribute(clone, "height", "10px"); + DOM.appendChild(DOM.getParent(getElement()), clone); + extraHorizontalPixels = DOM.getElementPropertyInt(clone, "offsetWidth") - 10; + extraVerticalPixels = DOM.getElementPropertyInt(clone, "offsetHeight") - 10; + + DOM.removeChild(DOM.getParent(getElement()), clone); + } + + @Override + public void setHeight(String height) { + if (height.endsWith("px")) { + int h = Integer.parseInt(height.substring(0, height.length() - 2)); + h -= getExtraVerticalPixels(); + if (h < 0) { + h = 0; + } + + super.setHeight(h + "px"); + } else { + super.setHeight(height); + } + + if (height == null || height.equals("")) { + rta.setHeight(""); + } else { + /* + * The formatter height will be initially calculated wrong so we + * delay the height setting so the DOM has had time to stabilize. + */ + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + int editorHeight = getOffsetHeight() + - getExtraVerticalPixels() + - formatter.getOffsetHeight(); + if (editorHeight < 0) { + editorHeight = 0; + } + rta.setHeight(editorHeight + "px"); + } + }); + } + } + + @Override + public void setWidth(String width) { + if (width.endsWith("px")) { + int w = Integer.parseInt(width.substring(0, width.length() - 2)); + w -= getExtraHorizontalPixels(); + if (w < 0) { + w = 0; + } + + super.setWidth(w + "px"); + } else if (width.equals("")) { + /* + * IE cannot calculate the width of the 100% iframe correctly if + * there is no width specified for the parent. In this case we would + * use the toolbar but IE cannot calculate the width of that one + * correctly either in all cases. So we end up using a default width + * for a RichTextArea with no width definition in all browsers (for + * compatibility). + */ + + super.setWidth(toolbarNaturalWidth + "px"); + } else { + super.setWidth(width); + } + } + + @Override + public void onKeyPress(KeyPressEvent event) { + if (maxLength >= 0) { + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + if (rta.getHTML().length() > maxLength) { + rta.setHTML(rta.getHTML().substring(0, maxLength)); + } + } + }); + } + } + + @Override + public void onKeyDown(KeyDownEvent event) { + // delegate to closest shortcut action handler + // throw event from the iframe forward to the shortcuthandler + ShortcutActionHandler shortcutHandler = getShortcutHandlerOwner() + .getShortcutActionHandler(); + if (shortcutHandler != null) { + shortcutHandler + .handleKeyboardEvent(com.google.gwt.user.client.Event + .as(event.getNativeEvent()), + ConnectorMap.get(client).getConnector(this)); + } + } + + private ShortcutActionHandlerOwner getShortcutHandlerOwner() { + if (hasShortcutActionHandler == null) { + Widget parent = getParent(); + while (parent != null) { + if (parent instanceof ShortcutActionHandlerOwner) { + break; + } + parent = parent.getParent(); + } + hasShortcutActionHandler = (ShortcutActionHandlerOwner) parent; + } + return hasShortcutActionHandler; + } + + @Override + public int getTabIndex() { + return rta.getTabIndex(); + } + + @Override + public void setAccessKey(char key) { + rta.setAccessKey(key); + } + + @Override + public void setFocus(boolean focused) { + /* + * Similar issue as with selectAll. Focusing must happen before possible + * selectall, so keep the timeout here lower. + */ + new Timer() { + + @Override + public void run() { + rta.setFocus(true); + } + }.schedule(300); + } + + @Override + public void setTabIndex(int index) { + rta.setTabIndex(index); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar$Strings.properties b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar$Strings.properties new file mode 100644 index 0000000000..363b704584 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar$Strings.properties @@ -0,0 +1,35 @@ +bold = Toggle Bold +createLink = Create Link +hr = Insert Horizontal Rule +indent = Indent Right +insertImage = Insert Image +italic = Toggle Italic +justifyCenter = Center +justifyLeft = Left Justify +justifyRight = Right Justify +ol = Insert Ordered List +outdent = Indent Left +removeFormat = Remove Formatting +removeLink = Remove Link +strikeThrough = Toggle Strikethrough +subscript = Toggle Subscript +superscript = Toggle Superscript +ul = Insert Unordered List +underline = Toggle Underline +color = Color +black = Black +white = White +red = Red +green = Green +yellow = Yellow +blue = Blue +font = Font +normal = Normal +size = Size +xxsmall = XX-Small +xsmall = X-Small +small = Small +medium = Medium +large = Large +xlarge = X-Large +xxlarge = XX-Large
\ No newline at end of file diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar.java b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar.java new file mode 100644 index 0000000000..3128b4d842 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar.java @@ -0,0 +1,476 @@ +/* + * Copyright 2011 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. + */ +/* + * Copyright 2007 Google Inc. + * + * 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.terminal.gwt.client.ui.richtextarea; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.event.dom.client.KeyUpHandler; +import com.google.gwt.i18n.client.Constants; +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.PushButton; +import com.google.gwt.user.client.ui.RichTextArea; +import com.google.gwt.user.client.ui.ToggleButton; + +/** + * A modified version of sample toolbar for use with {@link RichTextArea}. It + * provides a simple UI for all rich text formatting, dynamically displayed only + * for the available functionality. + */ +public class VRichTextToolbar extends Composite { + + /** + * This {@link ClientBundle} is used for all the button icons. Using a + * bundle allows all of these images to be packed into a single image, which + * saves a lot of HTTP requests, drastically improving startup time. + */ + public interface Images extends ClientBundle { + + ImageResource bold(); + + ImageResource createLink(); + + ImageResource hr(); + + ImageResource indent(); + + ImageResource insertImage(); + + ImageResource italic(); + + ImageResource justifyCenter(); + + ImageResource justifyLeft(); + + ImageResource justifyRight(); + + ImageResource ol(); + + ImageResource outdent(); + + ImageResource removeFormat(); + + ImageResource removeLink(); + + ImageResource strikeThrough(); + + ImageResource subscript(); + + ImageResource superscript(); + + ImageResource ul(); + + ImageResource underline(); + } + + /** + * This {@link Constants} interface is used to make the toolbar's strings + * internationalizable. + */ + public interface Strings extends Constants { + + String black(); + + String blue(); + + String bold(); + + String color(); + + String createLink(); + + String font(); + + String green(); + + String hr(); + + String indent(); + + String insertImage(); + + String italic(); + + String justifyCenter(); + + String justifyLeft(); + + String justifyRight(); + + String large(); + + String medium(); + + String normal(); + + String ol(); + + String outdent(); + + String red(); + + String removeFormat(); + + String removeLink(); + + String size(); + + String small(); + + String strikeThrough(); + + String subscript(); + + String superscript(); + + String ul(); + + String underline(); + + String white(); + + String xlarge(); + + String xsmall(); + + String xxlarge(); + + String xxsmall(); + + String yellow(); + } + + /** + * We use an inner EventHandler class to avoid exposing event methods on the + * RichTextToolbar itself. + */ + private class EventHandler implements ClickHandler, ChangeHandler, + KeyUpHandler { + + @Override + @SuppressWarnings("deprecation") + public void onChange(ChangeEvent event) { + Object sender = event.getSource(); + if (sender == backColors) { + basic.setBackColor(backColors.getValue(backColors + .getSelectedIndex())); + backColors.setSelectedIndex(0); + } else if (sender == foreColors) { + basic.setForeColor(foreColors.getValue(foreColors + .getSelectedIndex())); + foreColors.setSelectedIndex(0); + } else if (sender == fonts) { + basic.setFontName(fonts.getValue(fonts.getSelectedIndex())); + fonts.setSelectedIndex(0); + } else if (sender == fontSizes) { + basic.setFontSize(fontSizesConstants[fontSizes + .getSelectedIndex() - 1]); + fontSizes.setSelectedIndex(0); + } + } + + @Override + @SuppressWarnings("deprecation") + public void onClick(ClickEvent event) { + Object sender = event.getSource(); + if (sender == bold) { + basic.toggleBold(); + } else if (sender == italic) { + basic.toggleItalic(); + } else if (sender == underline) { + basic.toggleUnderline(); + } else if (sender == subscript) { + basic.toggleSubscript(); + } else if (sender == superscript) { + basic.toggleSuperscript(); + } else if (sender == strikethrough) { + extended.toggleStrikethrough(); + } else if (sender == indent) { + extended.rightIndent(); + } else if (sender == outdent) { + extended.leftIndent(); + } else if (sender == justifyLeft) { + basic.setJustification(RichTextArea.Justification.LEFT); + } else if (sender == justifyCenter) { + basic.setJustification(RichTextArea.Justification.CENTER); + } else if (sender == justifyRight) { + basic.setJustification(RichTextArea.Justification.RIGHT); + } else if (sender == insertImage) { + final String url = Window.prompt("Enter an image URL:", + "http://"); + if (url != null) { + extended.insertImage(url); + } + } else if (sender == createLink) { + final String url = Window + .prompt("Enter a link URL:", "http://"); + if (url != null) { + extended.createLink(url); + } + } else if (sender == removeLink) { + extended.removeLink(); + } else if (sender == hr) { + extended.insertHorizontalRule(); + } else if (sender == ol) { + extended.insertOrderedList(); + } else if (sender == ul) { + extended.insertUnorderedList(); + } else if (sender == removeFormat) { + extended.removeFormat(); + } else if (sender == richText) { + // We use the RichTextArea's onKeyUp event to update the toolbar + // status. This will catch any cases where the user moves the + // cursur using the keyboard, or uses one of the browser's + // built-in keyboard shortcuts. + updateStatus(); + } + } + + @Override + public void onKeyUp(KeyUpEvent event) { + if (event.getSource() == richText) { + // We use the RichTextArea's onKeyUp event to update the toolbar + // status. This will catch any cases where the user moves the + // cursor using the keyboard, or uses one of the browser's + // built-in keyboard shortcuts. + updateStatus(); + } + } + } + + private static final RichTextArea.FontSize[] fontSizesConstants = new RichTextArea.FontSize[] { + RichTextArea.FontSize.XX_SMALL, RichTextArea.FontSize.X_SMALL, + RichTextArea.FontSize.SMALL, RichTextArea.FontSize.MEDIUM, + RichTextArea.FontSize.LARGE, RichTextArea.FontSize.X_LARGE, + RichTextArea.FontSize.XX_LARGE }; + + private final Images images = (Images) GWT.create(Images.class); + private final Strings strings = (Strings) GWT.create(Strings.class); + private final EventHandler handler = new EventHandler(); + + private final RichTextArea richText; + @SuppressWarnings("deprecation") + private final RichTextArea.BasicFormatter basic; + @SuppressWarnings("deprecation") + private final RichTextArea.ExtendedFormatter extended; + + private final FlowPanel outer = new FlowPanel(); + private final FlowPanel topPanel = new FlowPanel(); + private final FlowPanel bottomPanel = new FlowPanel(); + private ToggleButton bold; + private ToggleButton italic; + private ToggleButton underline; + private ToggleButton subscript; + private ToggleButton superscript; + private ToggleButton strikethrough; + private PushButton indent; + private PushButton outdent; + private PushButton justifyLeft; + private PushButton justifyCenter; + private PushButton justifyRight; + private PushButton hr; + private PushButton ol; + private PushButton ul; + private PushButton insertImage; + private PushButton createLink; + private PushButton removeLink; + private PushButton removeFormat; + + private ListBox backColors; + private ListBox foreColors; + private ListBox fonts; + private ListBox fontSizes; + + /** + * Creates a new toolbar that drives the given rich text area. + * + * @param richText + * the rich text area to be controlled + */ + @SuppressWarnings("deprecation") + public VRichTextToolbar(RichTextArea richText) { + this.richText = richText; + basic = richText.getBasicFormatter(); + extended = richText.getExtendedFormatter(); + + outer.add(topPanel); + outer.add(bottomPanel); + topPanel.setStyleName("gwt-RichTextToolbar-top"); + bottomPanel.setStyleName("gwt-RichTextToolbar-bottom"); + + initWidget(outer); + setStyleName("gwt-RichTextToolbar"); + + if (basic != null) { + topPanel.add(bold = createToggleButton(images.bold(), + strings.bold())); + topPanel.add(italic = createToggleButton(images.italic(), + strings.italic())); + topPanel.add(underline = createToggleButton(images.underline(), + strings.underline())); + topPanel.add(subscript = createToggleButton(images.subscript(), + strings.subscript())); + topPanel.add(superscript = createToggleButton(images.superscript(), + strings.superscript())); + topPanel.add(justifyLeft = createPushButton(images.justifyLeft(), + strings.justifyLeft())); + topPanel.add(justifyCenter = createPushButton( + images.justifyCenter(), strings.justifyCenter())); + topPanel.add(justifyRight = createPushButton(images.justifyRight(), + strings.justifyRight())); + } + + if (extended != null) { + topPanel.add(strikethrough = createToggleButton( + images.strikeThrough(), strings.strikeThrough())); + topPanel.add(indent = createPushButton(images.indent(), + strings.indent())); + topPanel.add(outdent = createPushButton(images.outdent(), + strings.outdent())); + topPanel.add(hr = createPushButton(images.hr(), strings.hr())); + topPanel.add(ol = createPushButton(images.ol(), strings.ol())); + topPanel.add(ul = createPushButton(images.ul(), strings.ul())); + topPanel.add(insertImage = createPushButton(images.insertImage(), + strings.insertImage())); + topPanel.add(createLink = createPushButton(images.createLink(), + strings.createLink())); + topPanel.add(removeLink = createPushButton(images.removeLink(), + strings.removeLink())); + topPanel.add(removeFormat = createPushButton(images.removeFormat(), + strings.removeFormat())); + } + + if (basic != null) { + bottomPanel.add(backColors = createColorList("Background")); + bottomPanel.add(foreColors = createColorList("Foreground")); + bottomPanel.add(fonts = createFontList()); + bottomPanel.add(fontSizes = createFontSizes()); + + // We only use these handlers for updating status, so don't hook + // them up unless at least basic editing is supported. + richText.addKeyUpHandler(handler); + richText.addClickHandler(handler); + } + } + + private ListBox createColorList(String caption) { + final ListBox lb = new ListBox(); + lb.addChangeHandler(handler); + lb.setVisibleItemCount(1); + + lb.addItem(caption); + lb.addItem(strings.white(), "white"); + lb.addItem(strings.black(), "black"); + lb.addItem(strings.red(), "red"); + lb.addItem(strings.green(), "green"); + lb.addItem(strings.yellow(), "yellow"); + lb.addItem(strings.blue(), "blue"); + lb.setTabIndex(-1); + return lb; + } + + private ListBox createFontList() { + final ListBox lb = new ListBox(); + lb.addChangeHandler(handler); + lb.setVisibleItemCount(1); + + lb.addItem(strings.font(), ""); + lb.addItem(strings.normal(), "inherit"); + lb.addItem("Times New Roman", "Times New Roman"); + lb.addItem("Arial", "Arial"); + lb.addItem("Courier New", "Courier New"); + lb.addItem("Georgia", "Georgia"); + lb.addItem("Trebuchet", "Trebuchet"); + lb.addItem("Verdana", "Verdana"); + lb.setTabIndex(-1); + return lb; + } + + private ListBox createFontSizes() { + final ListBox lb = new ListBox(); + lb.addChangeHandler(handler); + lb.setVisibleItemCount(1); + + lb.addItem(strings.size()); + lb.addItem(strings.xxsmall()); + lb.addItem(strings.xsmall()); + lb.addItem(strings.small()); + lb.addItem(strings.medium()); + lb.addItem(strings.large()); + lb.addItem(strings.xlarge()); + lb.addItem(strings.xxlarge()); + lb.setTabIndex(-1); + return lb; + } + + private PushButton createPushButton(ImageResource img, String tip) { + final PushButton pb = new PushButton(new Image(img)); + pb.addClickHandler(handler); + pb.setTitle(tip); + pb.setTabIndex(-1); + return pb; + } + + private ToggleButton createToggleButton(ImageResource img, String tip) { + final ToggleButton tb = new ToggleButton(new Image(img)); + tb.addClickHandler(handler); + tb.setTitle(tip); + tb.setTabIndex(-1); + return tb; + } + + /** + * Updates the status of all the stateful buttons. + */ + @SuppressWarnings("deprecation") + private void updateStatus() { + if (basic != null) { + bold.setDown(basic.isBold()); + italic.setDown(basic.isItalic()); + underline.setDown(basic.isUnderlined()); + subscript.setDown(basic.isSubscript()); + superscript.setDown(basic.isSuperscript()); + } + + if (extended != null) { + strikethrough.setDown(extended.isStrikethrough()); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif Binary files differnew file mode 100644 index 0000000000..ddfc1cea2c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif Binary files differnew file mode 100644 index 0000000000..7c22eaac68 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif Binary files differnew file mode 100644 index 0000000000..1a1412fe0e --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif Binary files differnew file mode 100644 index 0000000000..c2f4c8cb21 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif Binary files differnew file mode 100644 index 0000000000..1629cabb78 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif Binary files differnew file mode 100644 index 0000000000..2bb89ef189 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png Binary files differnew file mode 100644 index 0000000000..80728186d8 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif Binary files differnew file mode 100644 index 0000000000..d507082cf1 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif Binary files differnew file mode 100644 index 0000000000..905421ed76 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif Binary files differnew file mode 100644 index 0000000000..394ec432a5 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif Binary files differnew file mode 100644 index 0000000000..ffe0e97284 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif Binary files differnew file mode 100644 index 0000000000..f7d4c4693d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif Binary files differnew file mode 100644 index 0000000000..bc37a3ed5a --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif Binary files differnew file mode 100644 index 0000000000..892d569384 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif Binary files differnew file mode 100644 index 0000000000..54f8e4f551 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif Binary files differnew file mode 100644 index 0000000000..78fd1b5722 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif Binary files differnew file mode 100644 index 0000000000..cf92c9774f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif Binary files differnew file mode 100644 index 0000000000..40721a7bca --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif Binary files differnew file mode 100644 index 0000000000..a7a233c023 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif Binary files differnew file mode 100644 index 0000000000..58b6fbb816 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif Binary files differnew file mode 100644 index 0000000000..a6270f6e21 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif Binary files differnew file mode 100644 index 0000000000..83f1562bcb --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif Binary files differnew file mode 100644 index 0000000000..06f0200fdd --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/root/RootConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/root/RootConnector.java new file mode 100644 index 0000000000..0a0e0e5082 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/root/RootConnector.java @@ -0,0 +1,483 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.root; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Position; +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.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.History; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.web.bindery.event.shared.HandlerRegistration; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.root.PageClientRpc; +import com.vaadin.shared.ui.root.RootConstants; +import com.vaadin.shared.ui.root.RootServerRpc; +import com.vaadin.shared.ui.root.RootState; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent.StateChangeHandler; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler; +import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren; +import com.vaadin.terminal.gwt.client.ui.notification.VNotification; +import com.vaadin.terminal.gwt.client.ui.window.WindowConnector; +import com.vaadin.ui.Root; + +@Connect(value = Root.class, loadStyle = LoadStyle.EAGER) +public class RootConnector extends AbstractComponentContainerConnector + implements Paintable, MayScrollChildren { + + private RootServerRpc rpc = RpcProxy.create(RootServerRpc.class, this); + + private HandlerRegistration childStateChangeHandlerRegistration; + + private final StateChangeHandler childStateChangeHandler = new StateChangeHandler() { + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + // TODO Should use a more specific handler that only reacts to + // size changes + onChildSizeChange(); + } + }; + + @Override + protected void init() { + super.init(); + registerRpc(PageClientRpc.class, new PageClientRpc() { + @Override + public void setTitle(String title) { + com.google.gwt.user.client.Window.setTitle(title); + } + }); + final int heartbeatInterval = getState().getHeartbeatInterval(); + new Timer() { + @Override + public void run() { + sendHeartbeat(); + schedule(heartbeatInterval); + } + }.schedule(heartbeatInterval); + } + + @Override + public void updateFromUIDL(final UIDL uidl, ApplicationConnection client) { + ConnectorMap paintableMap = ConnectorMap.get(getConnection()); + getWidget().rendering = true; + getWidget().id = getConnectorId(); + boolean firstPaint = getWidget().connection == null; + getWidget().connection = client; + + getWidget().immediate = getState().isImmediate(); + getWidget().resizeLazy = uidl.hasAttribute(RootConstants.RESIZE_LAZY); + String newTheme = uidl.getStringAttribute("theme"); + if (getWidget().theme != null && !newTheme.equals(getWidget().theme)) { + // Complete page refresh is needed due css can affect layout + // calculations etc + getWidget().reloadHostPage(); + } else { + getWidget().theme = newTheme; + } + // this also implicitly removes old styles + String styles = ""; + styles += getWidget().getStylePrimaryName() + " "; + if (getState().hasStyles()) { + for (String style : getState().getStyles()) { + styles += style + " "; + } + } + if (!client.getConfiguration().isStandalone()) { + styles += getWidget().getStylePrimaryName() + "-embedded"; + } + getWidget().setStyleName(styles.trim()); + + getWidget().makeScrollable(); + + clickEventHandler.handleEventHandlerRegistration(); + + // Process children + int childIndex = 0; + + // Open URL:s + boolean isClosed = false; // was this window closed? + while (childIndex < uidl.getChildCount() + && "open".equals(uidl.getChildUIDL(childIndex).getTag())) { + final UIDL open = uidl.getChildUIDL(childIndex); + final String url = client.translateVaadinUri(open + .getStringAttribute("src")); + final String target = open.getStringAttribute("name"); + if (target == null) { + // source will be opened to this browser window, but we may have + // to finish rendering this window in case this is a download + // (and window stays open). + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + VRoot.goTo(url); + } + }); + } else if ("_self".equals(target)) { + // This window is closing (for sure). Only other opens are + // relevant in this change. See #3558, #2144 + isClosed = true; + VRoot.goTo(url); + } else { + String options; + if (open.hasAttribute("border")) { + if (open.getStringAttribute("border").equals("minimal")) { + options = "menubar=yes,location=no,status=no"; + } else { + options = "menubar=no,location=no,status=no"; + } + + } else { + options = "resizable=yes,menubar=yes,toolbar=yes,directories=yes,location=yes,scrollbars=yes,status=yes"; + } + + if (open.hasAttribute("width")) { + int w = open.getIntAttribute("width"); + options += ",width=" + w; + } + if (open.hasAttribute("height")) { + int h = open.getIntAttribute("height"); + options += ",height=" + h; + } + + Window.open(url, target, options); + } + childIndex++; + } + if (isClosed) { + // don't render the content, something else will be opened to this + // browser view + getWidget().rendering = false; + return; + } + + // Handle other UIDL children + UIDL childUidl; + while ((childUidl = uidl.getChildUIDL(childIndex++)) != null) { + String tag = childUidl.getTag().intern(); + if (tag == "actions") { + if (getWidget().actionHandler == null) { + getWidget().actionHandler = new ShortcutActionHandler( + getWidget().id, client); + } + getWidget().actionHandler.updateActionMap(childUidl); + } else if (tag == "notifications") { + for (final Iterator<?> it = childUidl.getChildIterator(); it + .hasNext();) { + final UIDL notification = (UIDL) it.next(); + VNotification.showNotification(client, notification); + } + } + } + + if (uidl.hasAttribute("focused")) { + // set focused component when render phase is finished + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + ComponentConnector paintable = (ComponentConnector) uidl + .getPaintableAttribute("focused", getConnection()); + + final Widget toBeFocused = paintable.getWidget(); + /* + * Two types of Widgets can be focused, either implementing + * GWT HasFocus of a thinner Vaadin specific Focusable + * interface. + */ + if (toBeFocused instanceof com.google.gwt.user.client.ui.Focusable) { + final com.google.gwt.user.client.ui.Focusable toBeFocusedWidget = (com.google.gwt.user.client.ui.Focusable) toBeFocused; + toBeFocusedWidget.setFocus(true); + } else if (toBeFocused instanceof Focusable) { + ((Focusable) toBeFocused).focus(); + } else { + VConsole.log("Could not focus component"); + } + } + }); + } + + // Add window listeners on first paint, to prevent premature + // variablechanges + if (firstPaint) { + Window.addWindowClosingHandler(getWidget()); + Window.addResizeHandler(getWidget()); + } + + // finally set scroll position from UIDL + if (uidl.hasVariable("scrollTop")) { + getWidget().scrollable = true; + getWidget().scrollTop = uidl.getIntVariable("scrollTop"); + DOM.setElementPropertyInt(getWidget().getElement(), "scrollTop", + getWidget().scrollTop); + getWidget().scrollLeft = uidl.getIntVariable("scrollLeft"); + DOM.setElementPropertyInt(getWidget().getElement(), "scrollLeft", + getWidget().scrollLeft); + } else { + getWidget().scrollable = false; + } + + if (uidl.hasAttribute("scrollTo")) { + final ComponentConnector connector = (ComponentConnector) uidl + .getPaintableAttribute("scrollTo", getConnection()); + scrollIntoView(connector); + } + + if (uidl.hasAttribute(RootConstants.FRAGMENT_VARIABLE)) { + getWidget().currentFragment = uidl + .getStringAttribute(RootConstants.FRAGMENT_VARIABLE); + if (!getWidget().currentFragment.equals(History.getToken())) { + History.newItem(getWidget().currentFragment, true); + } + } else { + // Initial request for which the server doesn't yet have a fragment + // (and haven't shown any interest in getting one) + getWidget().currentFragment = History.getToken(); + + // Include current fragment in the next request + client.updateVariable(getWidget().id, + RootConstants.FRAGMENT_VARIABLE, + getWidget().currentFragment, false); + } + + if (firstPaint) { + // Queue the initial window size to be sent with the following + // request. + getWidget().sendClientResized(); + } + getWidget().rendering = false; + } + + public void init(String rootPanelId, + ApplicationConnection applicationConnection) { + DOM.sinkEvents(getWidget().getElement(), Event.ONKEYDOWN + | Event.ONSCROLL); + + // iview is focused when created so element needs tabIndex + // 1 due 0 is at the end of natural tabbing order + DOM.setElementProperty(getWidget().getElement(), "tabIndex", "1"); + + RootPanel root = RootPanel.get(rootPanelId); + + // Remove the v-app-loading or any splash screen added inside the div by + // the user + root.getElement().setInnerHTML(""); + + root.addStyleName("v-theme-" + + applicationConnection.getConfiguration().getThemeName()); + + root.add(getWidget()); + + if (applicationConnection.getConfiguration().isStandalone()) { + // set focus to iview element by default to listen possible keyboard + // shortcuts. For embedded applications this is unacceptable as we + // don't want to steal focus from the main page nor we don't want + // side-effects from focusing (scrollIntoView). + getWidget().getElement().focus(); + } + } + + private ClickEventHandler clickEventHandler = new ClickEventHandler(this) { + + @Override + protected void fireClick(NativeEvent event, + MouseEventDetails mouseDetails) { + rpc.click(mouseDetails); + } + + }; + + @Override + public void updateCaption(ComponentConnector component) { + // NOP The main view never draws caption for its layout + } + + @Override + public VRoot getWidget() { + return (VRoot) super.getWidget(); + } + + protected ComponentConnector getContent() { + return (ComponentConnector) getState().getContent(); + } + + protected void onChildSizeChange() { + ComponentConnector child = getContent(); + Style childStyle = child.getWidget().getElement().getStyle(); + /* + * Must set absolute position if the child has relative height and + * there's a chance of horizontal scrolling as some browsers will + * otherwise not take the scrollbar into account when calculating the + * height. Assuming v-view does not have an undefined width for now, see + * #8460. + */ + if (child.isRelativeHeight() && !BrowserInfo.get().isIE9()) { + childStyle.setPosition(Position.ABSOLUTE); + } else { + childStyle.clearPosition(); + } + } + + /** + * Checks if the given sub window is a child of this Root Connector + * + * @deprecated Should be replaced by a more generic mechanism for getting + * non-ComponentConnector children + * @param wc + * @return + */ + @Deprecated + public boolean hasSubWindow(WindowConnector wc) { + return getChildComponents().contains(wc); + } + + /** + * Return an iterator for current subwindows. This method is meant for + * testing purposes only. + * + * @return + */ + public List<WindowConnector> getSubWindows() { + ArrayList<WindowConnector> windows = new ArrayList<WindowConnector>(); + for (ComponentConnector child : getChildComponents()) { + if (child instanceof WindowConnector) { + windows.add((WindowConnector) child); + } + } + return windows; + } + + @Override + public RootState getState() { + return (RootState) super.getState(); + } + + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + super.onConnectorHierarchyChange(event); + + ComponentConnector oldChild = null; + ComponentConnector newChild = getContent(); + + for (ComponentConnector c : event.getOldChildren()) { + if (!(c instanceof WindowConnector)) { + oldChild = c; + break; + } + } + + if (oldChild != newChild) { + if (childStateChangeHandlerRegistration != null) { + childStateChangeHandlerRegistration.removeHandler(); + childStateChangeHandlerRegistration = null; + } + getWidget().setWidget(newChild.getWidget()); + childStateChangeHandlerRegistration = newChild + .addStateChangeHandler(childStateChangeHandler); + // Must handle new child here as state change events are already + // fired + onChildSizeChange(); + } + + for (ComponentConnector c : getChildComponents()) { + if (c instanceof WindowConnector) { + WindowConnector wc = (WindowConnector) c; + wc.setWindowOrderAndPosition(); + } + } + + // Close removed sub windows + for (ComponentConnector c : event.getOldChildren()) { + if (c.getParent() != this && c instanceof WindowConnector) { + ((WindowConnector) c).getWidget().hide(); + } + } + } + + /** + * Tries to scroll the viewport so that the given connector is in view. + * + * @param componentConnector + * The connector which should be visible + * + */ + public void scrollIntoView(final ComponentConnector componentConnector) { + if (componentConnector == null) { + return; + } + + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + componentConnector.getWidget().getElement().scrollIntoView(); + } + }); + } + + private void sendHeartbeat() { + RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, "url"); + + rb.setCallback(new RequestCallback() { + + @Override + public void onResponseReceived(Request request, Response response) { + // TODO Auto-generated method stub + + } + + @Override + public void onError(Request request, Throwable exception) { + // TODO Auto-generated method stub + + } + }); + + try { + rb.send(); + } catch (RequestException re) { + + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/root/VRoot.java b/client/src/com/vaadin/terminal/gwt/client/ui/root/VRoot.java new file mode 100644 index 0000000000..a473bf4846 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/root/VRoot.java @@ -0,0 +1,461 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.root; + +import java.util.ArrayList; + +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.logical.shared.ResizeEvent; +import com.google.gwt.event.logical.shared.ResizeHandler; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.History; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.SimplePanel; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.ui.root.RootConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate.TouchScrollHandler; +import com.vaadin.terminal.gwt.client.ui.VLazyExecutor; +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; + +/** + * + */ +public class VRoot extends SimplePanel implements ResizeHandler, + Window.ClosingHandler, ShortcutActionHandlerOwner, Focusable { + + private static final String CLASSNAME = "v-view"; + + private static int MONITOR_PARENT_TIMER_INTERVAL = 1000; + + String theme; + + String id; + + ShortcutActionHandler actionHandler; + + /* + * Last known window size used to detect whether VView should be layouted + * again. Detection must check window size, because the VView size might be + * fixed and thus not automatically adapt to changed window sizes. + */ + private int windowWidth; + private int windowHeight; + + /* + * Last know view size used to detect whether new dimensions should be sent + * to the server. + */ + private int viewWidth; + private int viewHeight; + + ApplicationConnection connection; + + /** + * Keep track of possible parent size changes when an embedded application. + * + * Uses {@link #parentWidth} and {@link #parentHeight} as an optimization to + * keep track of when there is a real change. + */ + private Timer resizeTimer; + + /** stored width of parent for embedded application auto-resize */ + private int parentWidth; + + /** stored height of parent for embedded application auto-resize */ + private int parentHeight; + + int scrollTop; + + int scrollLeft; + + boolean rendering; + + boolean scrollable; + + boolean immediate; + + boolean resizeLazy = false; + + private HandlerRegistration historyHandlerRegistration; + + private TouchScrollHandler touchScrollHandler; + + /** + * The current URI fragment, used to avoid sending updates if nothing has + * changed. + */ + String currentFragment; + + /** + * Listener for URI fragment changes. Notifies the server of the new value + * whenever the value changes. + */ + private final ValueChangeHandler<String> historyChangeHandler = new ValueChangeHandler<String>() { + + @Override + public void onValueChange(ValueChangeEvent<String> event) { + String newFragment = event.getValue(); + + // Send the new fragment to the server if it has changed + if (!newFragment.equals(currentFragment) && connection != null) { + currentFragment = newFragment; + connection.updateVariable(id, RootConstants.FRAGMENT_VARIABLE, + newFragment, true); + } + } + }; + + private VLazyExecutor delayedResizeExecutor = new VLazyExecutor(200, + new ScheduledCommand() { + + @Override + public void execute() { + performSizeCheck(); + } + + }); + + public VRoot() { + super(); + setStyleName(CLASSNAME); + + // Allow focusing the view by using the focus() method, the view + // should not be in the document focus flow + getElement().setTabIndex(-1); + makeScrollable(); + } + + /** + * Start to periodically monitor for parent element resizes if embedded + * application (e.g. portlet). + */ + @Override + protected void onLoad() { + super.onLoad(); + if (isMonitoringParentSize()) { + resizeTimer = new Timer() { + + @Override + public void run() { + // trigger check to see if parent size has changed, + // recalculate layouts + performSizeCheck(); + resizeTimer.schedule(MONITOR_PARENT_TIMER_INTERVAL); + } + }; + resizeTimer.schedule(MONITOR_PARENT_TIMER_INTERVAL); + } + } + + @Override + protected void onAttach() { + super.onAttach(); + historyHandlerRegistration = History + .addValueChangeHandler(historyChangeHandler); + currentFragment = History.getToken(); + } + + @Override + protected void onDetach() { + super.onDetach(); + historyHandlerRegistration.removeHandler(); + historyHandlerRegistration = null; + } + + /** + * Stop monitoring for parent element resizes. + */ + + @Override + protected void onUnload() { + if (resizeTimer != null) { + resizeTimer.cancel(); + resizeTimer = null; + } + super.onUnload(); + } + + /** + * Called when the window or parent div might have been resized. + * + * This immediately checks the sizes of the window and the parent div (if + * monitoring it) and triggers layout recalculation if they have changed. + */ + protected void performSizeCheck() { + windowSizeMaybeChanged(Window.getClientWidth(), + Window.getClientHeight()); + } + + /** + * Called when the window or parent div might have been resized. + * + * This immediately checks the sizes of the window and the parent div (if + * monitoring it) and triggers layout recalculation if they have changed. + * + * @param newWindowWidth + * The new width of the window + * @param newWindowHeight + * The new height of the window + * + * @deprecated use {@link #performSizeCheck()} + */ + @Deprecated + protected void windowSizeMaybeChanged(int newWindowWidth, + int newWindowHeight) { + boolean changed = false; + ComponentConnector connector = ConnectorMap.get(connection) + .getConnector(this); + if (windowWidth != newWindowWidth) { + windowWidth = newWindowWidth; + changed = true; + connector.getLayoutManager().reportOuterWidth(connector, + newWindowWidth); + VConsole.log("New window width: " + windowWidth); + } + if (windowHeight != newWindowHeight) { + windowHeight = newWindowHeight; + changed = true; + connector.getLayoutManager().reportOuterHeight(connector, + newWindowHeight); + VConsole.log("New window height: " + windowHeight); + } + Element parentElement = getElement().getParentElement(); + if (isMonitoringParentSize() && parentElement != null) { + // check also for parent size changes + int newParentWidth = parentElement.getClientWidth(); + int newParentHeight = parentElement.getClientHeight(); + if (parentWidth != newParentWidth) { + parentWidth = newParentWidth; + changed = true; + VConsole.log("New parent width: " + parentWidth); + } + if (parentHeight != newParentHeight) { + parentHeight = newParentHeight; + changed = true; + VConsole.log("New parent height: " + parentHeight); + } + } + if (changed) { + /* + * If the window size has changed, layout the VView again and send + * new size to the server if the size changed. (Just checking VView + * size would cause us to ignore cases when a relatively sized VView + * should shrink as the content's size is fixed and would thus not + * automatically shrink.) + */ + VConsole.log("Running layout functions due to window or parent resize"); + + // update size to avoid (most) redundant re-layout passes + // there can still be an extra layout recalculation if webkit + // overflow fix updates the size in a deferred block + if (isMonitoringParentSize() && parentElement != null) { + parentWidth = parentElement.getClientWidth(); + parentHeight = parentElement.getClientHeight(); + } + + sendClientResized(); + + connector.getLayoutManager().layoutNow(); + } + } + + public String getTheme() { + return theme; + } + + /** + * Used to reload host page on theme changes. + */ + static native void reloadHostPage() + /*-{ + $wnd.location.reload(); + }-*/; + + /** + * Returns true if the body is NOT generated, i.e if someone else has made + * the page that we're running in. Otherwise we're in charge of the whole + * page. + * + * @return true if we're running embedded + */ + public boolean isEmbedded() { + return !getElement().getOwnerDocument().getBody().getClassName() + .contains(ApplicationConstants.GENERATED_BODY_CLASSNAME); + } + + /** + * Returns true if the size of the parent should be checked periodically and + * the application should react to its changes. + * + * @return true if size of parent should be tracked + */ + protected boolean isMonitoringParentSize() { + // could also perform a more specific check (Liferay portlet) + return isEmbedded(); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + int type = DOM.eventGetType(event); + if (type == Event.ONKEYDOWN && actionHandler != null) { + actionHandler.handleKeyboardEvent(event); + return; + } else if (scrollable && type == Event.ONSCROLL) { + updateScrollPosition(); + } + } + + /** + * Updates scroll position from DOM and saves variables to server. + */ + private void updateScrollPosition() { + int oldTop = scrollTop; + int oldLeft = scrollLeft; + scrollTop = DOM.getElementPropertyInt(getElement(), "scrollTop"); + scrollLeft = DOM.getElementPropertyInt(getElement(), "scrollLeft"); + if (connection != null && !rendering) { + if (oldTop != scrollTop) { + connection.updateVariable(id, "scrollTop", scrollTop, false); + } + if (oldLeft != scrollLeft) { + connection.updateVariable(id, "scrollLeft", scrollLeft, false); + } + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.logical.shared.ResizeHandler#onResize(com.google + * .gwt.event.logical.shared.ResizeEvent) + */ + + @Override + public void onResize(ResizeEvent event) { + triggerSizeChangeCheck(); + } + + /** + * Called when a resize event is received. + * + * This may trigger a lazy refresh or perform the size check immediately + * depending on the browser used and whether the server side requests + * resizes to be lazy. + */ + private void triggerSizeChangeCheck() { + /* + * IE (pre IE9 at least) will give us some false resize events due to + * problems with scrollbars. Firefox 3 might also produce some extra + * events. We postpone both the re-layouting and the server side event + * for a while to deal with these issues. + * + * We may also postpone these events to avoid slowness when resizing the + * browser window. Constantly recalculating the layout causes the resize + * operation to be really slow with complex layouts. + */ + boolean lazy = resizeLazy || BrowserInfo.get().isIE8(); + + if (lazy) { + delayedResizeExecutor.trigger(); + } else { + performSizeCheck(); + } + } + + /** + * Send new dimensions to the server. + */ + void sendClientResized() { + Element parentElement = getElement().getParentElement(); + int viewHeight = parentElement.getClientHeight(); + int viewWidth = parentElement.getClientWidth(); + + connection.updateVariable(id, "height", viewHeight, false); + connection.updateVariable(id, "width", viewWidth, false); + + int windowWidth = Window.getClientWidth(); + int windowHeight = Window.getClientHeight(); + + connection.updateVariable(id, RootConstants.BROWSER_WIDTH_VAR, + windowWidth, false); + connection.updateVariable(id, RootConstants.BROWSER_HEIGHT_VAR, + windowHeight, immediate); + } + + public native static void goTo(String url) + /*-{ + $wnd.location = url; + }-*/; + + @Override + public void onWindowClosing(Window.ClosingEvent event) { + // Change focus on this window in order to ensure that all state is + // collected from textfields + // TODO this is a naive hack, that only works with text fields and may + // cause some odd issues. Should be replaced with a decent solution, see + // also related BeforeShortcutActionListener interface. Same interface + // might be usable here. + VTextField.flushChangesFromFocusedTextField(); + } + + private native static void loadAppIdListFromDOM(ArrayList<String> list) + /*-{ + var j; + for(j in $wnd.vaadin.vaadinConfigurations) { + // $entry not needed as function is not exported + list.@java.util.Collection::add(Ljava/lang/Object;)(j); + } + }-*/; + + @Override + public ShortcutActionHandler getShortcutActionHandler() { + return actionHandler; + } + + @Override + public void focus() { + getElement().focus(); + } + + /** + * Ensures the root is scrollable eg. after style name changes. + */ + void makeScrollable() { + if (touchScrollHandler == null) { + touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this); + } + touchScrollHandler.addElement(getElement()); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/slider/SliderConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/slider/SliderConnector.java new file mode 100644 index 0000000000..7e0617b7dc --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/slider/SliderConnector.java @@ -0,0 +1,84 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.slider; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.user.client.Command; +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector; +import com.vaadin.ui.Slider; + +@Connect(Slider.class) +public class SliderConnector extends AbstractFieldConnector implements + Paintable { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + + getWidget().client = client; + getWidget().id = uidl.getId(); + + if (!isRealUpdate(uidl)) { + return; + } + + getWidget().immediate = getState().isImmediate(); + getWidget().disabled = !isEnabled(); + getWidget().readonly = isReadOnly(); + + getWidget().vertical = uidl.hasAttribute("vertical"); + + // TODO should style names be used? + + if (getWidget().vertical) { + getWidget().addStyleName(VSlider.CLASSNAME + "-vertical"); + } else { + getWidget().removeStyleName(VSlider.CLASSNAME + "-vertical"); + } + + getWidget().min = uidl.getDoubleAttribute("min"); + getWidget().max = uidl.getDoubleAttribute("max"); + getWidget().resolution = uidl.getIntAttribute("resolution"); + getWidget().value = new Double(uidl.getDoubleVariable("value")); + + getWidget().setFeedbackValue(getWidget().value); + + getWidget().buildBase(); + + if (!getWidget().vertical) { + // Draw handle with a delay to allow base to gain maximum width + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + getWidget().buildHandle(); + getWidget().setValue(getWidget().value, false); + } + }); + } else { + getWidget().buildHandle(); + getWidget().setValue(getWidget().value, false); + } + } + + @Override + public VSlider getWidget() { + return (VSlider) super.getWidget(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/slider/VSlider.java b/client/src/com/vaadin/terminal/gwt/client/ui/slider/VSlider.java new file mode 100644 index 0000000000..9667522eb3 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/slider/VSlider.java @@ -0,0 +1,542 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.slider; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.user.client.Command; +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.Window; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ContainerResizedListener; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.Field; +import com.vaadin.terminal.gwt.client.ui.SimpleFocusablePanel; +import com.vaadin.terminal.gwt.client.ui.VLazyExecutor; +import com.vaadin.terminal.gwt.client.ui.VOverlay; + +public class VSlider extends SimpleFocusablePanel implements Field, + ContainerResizedListener { + + public static final String CLASSNAME = "v-slider"; + + /** + * Minimum size (width or height, depending on orientation) of the slider + * base. + */ + private static final int MIN_SIZE = 50; + + ApplicationConnection client; + + String id; + + boolean immediate; + boolean disabled; + boolean readonly; + + private int acceleration = 1; + double min; + double max; + int resolution; + Double value; + boolean vertical; + + private final HTML feedback = new HTML("", false); + private final VOverlay feedbackPopup = new VOverlay(true, false, true) { + + @Override + public void show() { + super.show(); + updateFeedbackPosition(); + } + }; + + /* DOM element for slider's base */ + private final Element base; + private final int BASE_BORDER_WIDTH = 1; + + /* DOM element for slider's handle */ + private final Element handle; + + /* DOM element for decrement arrow */ + private final Element smaller; + + /* DOM element for increment arrow */ + private final Element bigger; + + /* Temporary dragging/animation variables */ + private boolean dragging = false; + + private VLazyExecutor delayedValueUpdater = new VLazyExecutor(100, + new ScheduledCommand() { + + @Override + public void execute() { + updateValueToServer(); + acceleration = 1; + } + }); + + public VSlider() { + super(); + + base = DOM.createDiv(); + handle = DOM.createDiv(); + smaller = DOM.createDiv(); + bigger = DOM.createDiv(); + + setStyleName(CLASSNAME); + DOM.setElementProperty(base, "className", CLASSNAME + "-base"); + DOM.setElementProperty(handle, "className", CLASSNAME + "-handle"); + DOM.setElementProperty(smaller, "className", CLASSNAME + "-smaller"); + DOM.setElementProperty(bigger, "className", CLASSNAME + "-bigger"); + + DOM.appendChild(getElement(), bigger); + DOM.appendChild(getElement(), smaller); + DOM.appendChild(getElement(), base); + DOM.appendChild(base, handle); + + // Hide initially + DOM.setStyleAttribute(smaller, "display", "none"); + DOM.setStyleAttribute(bigger, "display", "none"); + DOM.setStyleAttribute(handle, "visibility", "hidden"); + + sinkEvents(Event.MOUSEEVENTS | Event.ONMOUSEWHEEL | Event.KEYEVENTS + | Event.FOCUSEVENTS | Event.TOUCHEVENTS); + + feedbackPopup.addStyleName(CLASSNAME + "-feedback"); + feedbackPopup.setWidget(feedback); + } + + void setFeedbackValue(double value) { + String currentValue = "" + value; + if (resolution == 0) { + currentValue = "" + new Double(value).intValue(); + } + feedback.setText(currentValue); + } + + private void updateFeedbackPosition() { + if (vertical) { + feedbackPopup.setPopupPosition( + DOM.getAbsoluteLeft(handle) + handle.getOffsetWidth(), + DOM.getAbsoluteTop(handle) + handle.getOffsetHeight() / 2 + - feedbackPopup.getOffsetHeight() / 2); + } else { + feedbackPopup.setPopupPosition( + DOM.getAbsoluteLeft(handle) + handle.getOffsetWidth() / 2 + - feedbackPopup.getOffsetWidth() / 2, + DOM.getAbsoluteTop(handle) + - feedbackPopup.getOffsetHeight()); + } + } + + void buildBase() { + final String styleAttribute = vertical ? "height" : "width"; + final String oppositeStyleAttribute = vertical ? "width" : "height"; + final String domProperty = vertical ? "offsetHeight" : "offsetWidth"; + + // clear unnecessary opposite style attribute + DOM.setStyleAttribute(base, oppositeStyleAttribute, ""); + + final Element p = DOM.getParent(getElement()); + if (DOM.getElementPropertyInt(p, domProperty) > 50) { + if (vertical) { + setHeight(); + } else { + DOM.setStyleAttribute(base, styleAttribute, ""); + } + } else { + // Set minimum size and adjust after all components have + // (supposedly) been drawn completely. + DOM.setStyleAttribute(base, styleAttribute, MIN_SIZE + "px"); + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + final Element p = DOM.getParent(getElement()); + if (DOM.getElementPropertyInt(p, domProperty) > (MIN_SIZE + 5)) { + if (vertical) { + setHeight(); + } else { + DOM.setStyleAttribute(base, styleAttribute, ""); + } + // Ensure correct position + setValue(value, false); + } + } + }); + } + + // TODO attach listeners for focusing and arrow keys + } + + void buildHandle() { + final String handleAttribute = vertical ? "marginTop" : "marginLeft"; + final String oppositeHandleAttribute = vertical ? "marginLeft" + : "marginTop"; + + DOM.setStyleAttribute(handle, handleAttribute, "0"); + + // clear unnecessary opposite handle attribute + DOM.setStyleAttribute(handle, oppositeHandleAttribute, ""); + + // Restore visibility + DOM.setStyleAttribute(handle, "visibility", "visible"); + + } + + void setValue(Double value, boolean updateToServer) { + if (value == null) { + return; + } + + if (value < min) { + value = min; + } else if (value > max) { + value = max; + } + + // Update handle position + final String styleAttribute = vertical ? "marginTop" : "marginLeft"; + final String domProperty = vertical ? "offsetHeight" : "offsetWidth"; + final int handleSize = Integer.parseInt(DOM.getElementProperty(handle, + domProperty)); + final int baseSize = Integer.parseInt(DOM.getElementProperty(base, + domProperty)) - (2 * BASE_BORDER_WIDTH); + + final int range = baseSize - handleSize; + double v = value.doubleValue(); + + // Round value to resolution + if (resolution > 0) { + v = Math.round(v * Math.pow(10, resolution)); + v = v / Math.pow(10, resolution); + } else { + v = Math.round(v); + } + final double valueRange = max - min; + double p = 0; + if (valueRange > 0) { + p = range * ((v - min) / valueRange); + } + if (p < 0) { + p = 0; + } + if (vertical) { + p = range - p; + } + final double pos = p; + + DOM.setStyleAttribute(handle, styleAttribute, (Math.round(pos)) + "px"); + + // Update value + this.value = new Double(v); + setFeedbackValue(v); + + if (updateToServer) { + updateValueToServer(); + } + } + + @Override + public void onBrowserEvent(Event event) { + if (disabled || readonly) { + return; + } + final Element targ = DOM.eventGetTarget(event); + + if (DOM.eventGetType(event) == Event.ONMOUSEWHEEL) { + processMouseWheelEvent(event); + } else if (dragging || targ == handle) { + processHandleEvent(event); + } else if (targ == smaller) { + decreaseValue(true); + } else if (targ == bigger) { + increaseValue(true); + } else if (DOM.eventGetType(event) == Event.MOUSEEVENTS) { + processBaseEvent(event); + } else if ((BrowserInfo.get().isGecko() && DOM.eventGetType(event) == Event.ONKEYPRESS) + || (!BrowserInfo.get().isGecko() && DOM.eventGetType(event) == Event.ONKEYDOWN)) { + + if (handleNavigation(event.getKeyCode(), event.getCtrlKey(), + event.getShiftKey())) { + + feedbackPopup.show(); + + delayedValueUpdater.trigger(); + + DOM.eventPreventDefault(event); + DOM.eventCancelBubble(event, true); + } + } else if (targ.equals(getElement()) + && DOM.eventGetType(event) == Event.ONFOCUS) { + feedbackPopup.show(); + } else if (targ.equals(getElement()) + && DOM.eventGetType(event) == Event.ONBLUR) { + feedbackPopup.hide(); + } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) { + feedbackPopup.show(); + } + if (Util.isTouchEvent(event)) { + event.preventDefault(); // avoid simulated events + event.stopPropagation(); + } + } + + private void processMouseWheelEvent(final Event event) { + final int dir = DOM.eventGetMouseWheelVelocityY(event); + + if (dir < 0) { + increaseValue(false); + } else { + decreaseValue(false); + } + + delayedValueUpdater.trigger(); + + DOM.eventPreventDefault(event); + DOM.eventCancelBubble(event, true); + } + + private void processHandleEvent(Event event) { + switch (DOM.eventGetType(event)) { + case Event.ONMOUSEDOWN: + case Event.ONTOUCHSTART: + if (!disabled && !readonly) { + focus(); + feedbackPopup.show(); + dragging = true; + DOM.setElementProperty(handle, "className", CLASSNAME + + "-handle " + CLASSNAME + "-handle-active"); + DOM.setCapture(getElement()); + DOM.eventPreventDefault(event); // prevent selecting text + DOM.eventCancelBubble(event, true); + event.stopPropagation(); + VConsole.log("Slider move start"); + } + break; + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + if (dragging) { + VConsole.log("Slider move"); + setValueByEvent(event, false); + updateFeedbackPosition(); + event.stopPropagation(); + } + break; + case Event.ONTOUCHEND: + feedbackPopup.hide(); + case Event.ONMOUSEUP: + // feedbackPopup.hide(); + VConsole.log("Slider move end"); + dragging = false; + DOM.setElementProperty(handle, "className", CLASSNAME + "-handle"); + DOM.releaseCapture(getElement()); + setValueByEvent(event, true); + event.stopPropagation(); + break; + default: + break; + } + } + + private void processBaseEvent(Event event) { + if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) { + if (!disabled && !readonly && !dragging) { + setValueByEvent(event, true); + DOM.eventCancelBubble(event, true); + } + } + } + + private void decreaseValue(boolean updateToServer) { + setValue(new Double(value.doubleValue() - Math.pow(10, -resolution)), + updateToServer); + } + + private void increaseValue(boolean updateToServer) { + setValue(new Double(value.doubleValue() + Math.pow(10, -resolution)), + updateToServer); + } + + private void setValueByEvent(Event event, boolean updateToServer) { + double v = min; // Fallback to min + + final int coord = getEventPosition(event); + + final int handleSize, baseSize, baseOffset; + if (vertical) { + handleSize = handle.getOffsetHeight(); + baseSize = base.getOffsetHeight(); + baseOffset = base.getAbsoluteTop() - Window.getScrollTop() + - handleSize / 2; + } else { + handleSize = handle.getOffsetWidth(); + baseSize = base.getOffsetWidth(); + baseOffset = base.getAbsoluteLeft() - Window.getScrollLeft() + + handleSize / 2; + } + + if (vertical) { + v = ((baseSize - (coord - baseOffset)) / (double) (baseSize - handleSize)) + * (max - min) + min; + } else { + v = ((coord - baseOffset) / (double) (baseSize - handleSize)) + * (max - min) + min; + } + + if (v < min) { + v = min; + } else if (v > max) { + v = max; + } + + setValue(v, updateToServer); + } + + /** + * TODO consider extracting touches support to an impl class specific for + * webkit (only browser that really supports touches). + * + * @param event + * @return + */ + protected int getEventPosition(Event event) { + if (vertical) { + return Util.getTouchOrMouseClientY(event); + } else { + return Util.getTouchOrMouseClientX(event); + } + } + + @Override + public void iLayout() { + if (vertical) { + setHeight(); + } + // Update handle position + setValue(value, false); + } + + private void setHeight() { + // Calculate decoration size + DOM.setStyleAttribute(base, "height", "0"); + DOM.setStyleAttribute(base, "overflow", "hidden"); + int h = DOM.getElementPropertyInt(getElement(), "offsetHeight"); + if (h < MIN_SIZE) { + h = MIN_SIZE; + } + DOM.setStyleAttribute(base, "height", h + "px"); + DOM.setStyleAttribute(base, "overflow", ""); + } + + private void updateValueToServer() { + client.updateVariable(id, "value", value.doubleValue(), immediate); + } + + /** + * Handles the keyboard events handled by the Slider + * + * @param event + * The keyboard event received + * @return true iff the navigation event was handled + */ + public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { + + // No support for ctrl moving + if (ctrl) { + return false; + } + + if ((keycode == getNavigationUpKey() && vertical) + || (keycode == getNavigationRightKey() && !vertical)) { + if (shift) { + for (int a = 0; a < acceleration; a++) { + increaseValue(false); + } + acceleration++; + } else { + increaseValue(false); + } + return true; + } else if (keycode == getNavigationDownKey() && vertical + || (keycode == getNavigationLeftKey() && !vertical)) { + if (shift) { + for (int a = 0; a < acceleration; a++) { + decreaseValue(false); + } + acceleration++; + } else { + decreaseValue(false); + } + return true; + } + + return false; + } + + /** + * Get the key that increases the vertical slider. By default it is the up + * arrow key but by overriding this you can change the key to whatever you + * want. + * + * @return The keycode of the key + */ + protected int getNavigationUpKey() { + return KeyCodes.KEY_UP; + } + + /** + * Get the key that decreases the vertical slider. By default it is the down + * arrow key but by overriding this you can change the key to whatever you + * want. + * + * @return The keycode of the key + */ + protected int getNavigationDownKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Get the key that decreases the horizontal slider. By default it is the + * left arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationLeftKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * Get the key that increases the horizontal slider. By default it is the + * right arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationRightKey() { + return KeyCodes.KEY_RIGHT; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/AbstractSplitPanelConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/AbstractSplitPanelConnector.java new file mode 100644 index 0000000000..9f4df02380 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/AbstractSplitPanelConnector.java @@ -0,0 +1,227 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.splitpanel; + +import java.util.LinkedList; +import java.util.List; + +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.event.dom.client.DomEvent; +import com.google.gwt.event.dom.client.DomEvent.Type; +import com.google.gwt.event.shared.EventHandler; +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.ui.Widget; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelRpc; +import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState; +import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState.SplitterState; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler; +import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout; +import com.vaadin.terminal.gwt.client.ui.splitpanel.VAbstractSplitPanel.SplitterMoveHandler; +import com.vaadin.terminal.gwt.client.ui.splitpanel.VAbstractSplitPanel.SplitterMoveHandler.SplitterMoveEvent; + +public abstract class AbstractSplitPanelConnector extends + AbstractComponentContainerConnector implements SimpleManagedLayout { + + private AbstractSplitPanelRpc rpc; + + @Override + protected void init() { + super.init(); + rpc = RpcProxy.create(AbstractSplitPanelRpc.class, this); + // TODO Remove + getWidget().client = getConnection(); + + getWidget().addHandler(new SplitterMoveHandler() { + + @Override + public void splitterMoved(SplitterMoveEvent event) { + String position = getWidget().getSplitterPosition(); + float pos = 0; + if (position.indexOf("%") > 0) { + // Send % values as a fraction to avoid that the splitter + // "jumps" when server responds with the integer pct value + // (e.g. dragged 16.6% -> should not jump to 17%) + pos = Float.valueOf(position.substring(0, + position.length() - 1)); + } else { + pos = Integer.parseInt(position.substring(0, + position.length() - 2)); + } + + rpc.setSplitterPosition(pos); + } + + }, SplitterMoveEvent.TYPE); + } + + @Override + public void updateCaption(ComponentConnector component) { + // TODO Implement caption handling + } + + ClickEventHandler clickEventHandler = new ClickEventHandler(this) { + + @Override + protected <H extends EventHandler> HandlerRegistration registerHandler( + H handler, Type<H> type) { + if ((Event.getEventsSunk(getWidget().splitter) & Event + .getTypeInt(type.getName())) != 0) { + // If we are already sinking the event for the splitter we do + // not want to additionally sink it for the root element + return getWidget().addHandler(handler, type); + } else { + return getWidget().addDomHandler(handler, type); + } + } + + @Override + protected boolean shouldFireEvent(DomEvent<?> event) { + Element target = event.getNativeEvent().getEventTarget().cast(); + if (!getWidget().splitter.isOrHasChild(target)) { + return false; + } + + return super.shouldFireEvent(event); + }; + + @Override + protected Element getRelativeToElement() { + return getWidget().splitter; + }; + + @Override + protected void fireClick(NativeEvent event, + MouseEventDetails mouseDetails) { + rpc.splitterClick(mouseDetails); + } + + }; + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + getWidget().immediate = getState().isImmediate(); + + getWidget().setEnabled(isEnabled()); + + clickEventHandler.handleEventHandlerRegistration(); + + if (getState().hasStyles()) { + getWidget().componentStyleNames = getState().getStyles(); + } else { + getWidget().componentStyleNames = new LinkedList<String>(); + } + + // Splitter updates + SplitterState splitterState = getState().getSplitterState(); + + getWidget().setLocked(splitterState.isLocked()); + getWidget().setPositionReversed(splitterState.isPositionReversed()); + + getWidget().setStylenames(); + + getWidget().minimumPosition = splitterState.getMinPosition() + + splitterState.getMinPositionUnit(); + + getWidget().maximumPosition = splitterState.getMaxPosition() + + splitterState.getMaxPositionUnit(); + + getWidget().position = splitterState.getPosition() + + splitterState.getPositionUnit(); + + // This is needed at least for cases like #3458 to take + // appearing/disappearing scrollbars into account. + getConnection().runDescendentsLayout(getWidget()); + + getLayoutManager().setNeedsLayout(this); + + getWidget().makeScrollable(); + } + + @Override + public void layout() { + VAbstractSplitPanel splitPanel = getWidget(); + splitPanel.setSplitPosition(splitPanel.position); + splitPanel.updateSizes(); + // Report relative sizes in other direction for quicker propagation + List<ComponentConnector> children = getChildComponents(); + for (ComponentConnector child : children) { + reportOtherDimension(child); + } + } + + private void reportOtherDimension(ComponentConnector child) { + LayoutManager layoutManager = getLayoutManager(); + if (this instanceof HorizontalSplitPanelConnector) { + if (child.isRelativeHeight()) { + int height = layoutManager.getInnerHeight(getWidget() + .getElement()); + layoutManager.reportHeightAssignedToRelative(child, height); + } + } else { + if (child.isRelativeWidth()) { + int width = layoutManager.getInnerWidth(getWidget() + .getElement()); + layoutManager.reportWidthAssignedToRelative(child, width); + } + } + } + + @Override + public VAbstractSplitPanel getWidget() { + return (VAbstractSplitPanel) super.getWidget(); + } + + @Override + public AbstractSplitPanelState getState() { + return (AbstractSplitPanelState) super.getState(); + } + + private ComponentConnector getFirstChild() { + return (ComponentConnector) getState().getFirstChild(); + } + + private ComponentConnector getSecondChild() { + return (ComponentConnector) getState().getSecondChild(); + } + + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + super.onConnectorHierarchyChange(event); + + Widget newFirstChildWidget = null; + if (getFirstChild() != null) { + newFirstChildWidget = getFirstChild().getWidget(); + } + getWidget().setFirstWidget(newFirstChildWidget); + + Widget newSecondChildWidget = null; + if (getSecondChild() != null) { + newSecondChildWidget = getSecondChild().getWidget(); + } + getWidget().setSecondWidget(newSecondChildWidget); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/HorizontalSplitPanelConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/HorizontalSplitPanelConnector.java new file mode 100644 index 0000000000..3165e13407 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/HorizontalSplitPanelConnector.java @@ -0,0 +1,30 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.splitpanel; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.ui.HorizontalSplitPanel; + +@Connect(value = HorizontalSplitPanel.class, loadStyle = LoadStyle.EAGER) +public class HorizontalSplitPanelConnector extends AbstractSplitPanelConnector { + + @Override + public VSplitPanelHorizontal getWidget() { + return (VSplitPanelHorizontal) super.getWidget(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VAbstractSplitPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VAbstractSplitPanel.java new file mode 100644 index 0000000000..b83108b34c --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VAbstractSplitPanel.java @@ -0,0 +1,784 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.splitpanel; + +import java.util.Collections; +import java.util.List; + +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.Style; +import com.google.gwt.event.dom.client.TouchCancelEvent; +import com.google.gwt.event.dom.client.TouchCancelHandler; +import com.google.gwt.event.dom.client.TouchEndEvent; +import com.google.gwt.event.dom.client.TouchEndHandler; +import com.google.gwt.event.dom.client.TouchMoveEvent; +import com.google.gwt.event.dom.client.TouchMoveHandler; +import com.google.gwt.event.dom.client.TouchStartEvent; +import com.google.gwt.event.dom.client.TouchStartHandler; +import com.google.gwt.event.shared.EventHandler; +import com.google.gwt.event.shared.GwtEvent; +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.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate.TouchScrollHandler; +import com.vaadin.terminal.gwt.client.ui.VOverlay; +import com.vaadin.terminal.gwt.client.ui.splitpanel.VAbstractSplitPanel.SplitterMoveHandler.SplitterMoveEvent; + +public class VAbstractSplitPanel extends ComplexPanel { + + private boolean enabled = false; + + public static final String CLASSNAME = "v-splitpanel"; + + public static final int ORIENTATION_HORIZONTAL = 0; + + public static final int ORIENTATION_VERTICAL = 1; + + private static final int MIN_SIZE = 30; + + private int orientation = ORIENTATION_HORIZONTAL; + + Widget firstChild; + + Widget secondChild; + + private final Element wrapper = DOM.createDiv(); + + private final Element firstContainer = DOM.createDiv(); + + private final Element secondContainer = DOM.createDiv(); + + final Element splitter = DOM.createDiv(); + + private boolean resizing; + + private boolean resized = false; + + private int origX; + + private int origY; + + private int origMouseX; + + private int origMouseY; + + private boolean locked = false; + + private boolean positionReversed = false; + + List<String> componentStyleNames = Collections.emptyList(); + + private Element draggingCurtain; + + ApplicationConnection client; + + boolean immediate; + + /* The current position of the split handle in either percentages or pixels */ + String position; + + String maximumPosition; + + String minimumPosition; + + private TouchScrollHandler touchScrollHandler; + + protected Element scrolledContainer; + + protected int origScrollTop; + + public VAbstractSplitPanel() { + this(ORIENTATION_HORIZONTAL); + } + + public VAbstractSplitPanel(int orientation) { + setElement(DOM.createDiv()); + switch (orientation) { + case ORIENTATION_HORIZONTAL: + setStyleName(CLASSNAME + "-horizontal"); + break; + case ORIENTATION_VERTICAL: + default: + setStyleName(CLASSNAME + "-vertical"); + break; + } + // size below will be overridden in update from uidl, initial size + // needed to keep IE alive + setWidth(MIN_SIZE + "px"); + setHeight(MIN_SIZE + "px"); + constructDom(); + setOrientation(orientation); + sinkEvents(Event.MOUSEEVENTS); + + makeScrollable(); + + addDomHandler(new TouchCancelHandler() { + @Override + public void onTouchCancel(TouchCancelEvent event) { + // TODO When does this actually happen?? + VConsole.log("TOUCH CANCEL"); + } + }, TouchCancelEvent.getType()); + addDomHandler(new TouchStartHandler() { + @Override + public void onTouchStart(TouchStartEvent event) { + Node target = event.getTouches().get(0).getTarget().cast(); + if (splitter.isOrHasChild(target)) { + onMouseDown(Event.as(event.getNativeEvent())); + } + } + }, TouchStartEvent.getType()); + addDomHandler(new TouchMoveHandler() { + @Override + public void onTouchMove(TouchMoveEvent event) { + if (resizing) { + onMouseMove(Event.as(event.getNativeEvent())); + } + } + }, TouchMoveEvent.getType()); + addDomHandler(new TouchEndHandler() { + @Override + public void onTouchEnd(TouchEndEvent event) { + if (resizing) { + onMouseUp(Event.as(event.getNativeEvent())); + } + } + }, TouchEndEvent.getType()); + + } + + protected void constructDom() { + DOM.appendChild(splitter, DOM.createDiv()); // for styling + DOM.appendChild(getElement(), wrapper); + DOM.setStyleAttribute(wrapper, "position", "relative"); + DOM.setStyleAttribute(wrapper, "width", "100%"); + DOM.setStyleAttribute(wrapper, "height", "100%"); + + DOM.appendChild(wrapper, secondContainer); + DOM.appendChild(wrapper, firstContainer); + DOM.appendChild(wrapper, splitter); + + DOM.setStyleAttribute(splitter, "position", "absolute"); + DOM.setStyleAttribute(secondContainer, "position", "absolute"); + + setStylenames(); + } + + private void setOrientation(int orientation) { + this.orientation = orientation; + if (orientation == ORIENTATION_HORIZONTAL) { + DOM.setStyleAttribute(splitter, "height", "100%"); + DOM.setStyleAttribute(splitter, "top", "0"); + DOM.setStyleAttribute(firstContainer, "height", "100%"); + DOM.setStyleAttribute(secondContainer, "height", "100%"); + } else { + DOM.setStyleAttribute(splitter, "width", "100%"); + DOM.setStyleAttribute(splitter, "left", "0"); + DOM.setStyleAttribute(firstContainer, "width", "100%"); + DOM.setStyleAttribute(secondContainer, "width", "100%"); + } + } + + @Override + public boolean remove(Widget w) { + boolean removed = super.remove(w); + if (removed) { + if (firstChild == w) { + firstChild = null; + } else { + secondChild = null; + } + } + return removed; + } + + void setLocked(boolean newValue) { + if (locked != newValue) { + locked = newValue; + splitterSize = -1; + setStylenames(); + } + } + + void setPositionReversed(boolean reversed) { + if (positionReversed != reversed) { + if (orientation == ORIENTATION_HORIZONTAL) { + DOM.setStyleAttribute(splitter, "right", ""); + DOM.setStyleAttribute(splitter, "left", ""); + } else if (orientation == ORIENTATION_VERTICAL) { + DOM.setStyleAttribute(splitter, "top", ""); + DOM.setStyleAttribute(splitter, "bottom", ""); + } + + positionReversed = reversed; + } + } + + /** + * Converts given split position string (in pixels or percentage) to a + * floating point pixel value. + * + * @param pos + * @return + */ + private float convertToPixels(String pos) { + float posAsFloat; + if (pos.indexOf("%") > 0) { + posAsFloat = Math.round(Float.parseFloat(pos.substring(0, + pos.length() - 1)) + / 100 + * (orientation == ORIENTATION_HORIZONTAL ? getOffsetWidth() + : getOffsetHeight())); + } else { + posAsFloat = Float.parseFloat(pos.substring(0, pos.length() - 2)); + } + return posAsFloat; + } + + /** + * Converts given split position string (in pixels or percentage) to a float + * percentage value. + * + * @param pos + * @return + */ + private float convertToPercentage(String pos) { + if (pos.endsWith("px")) { + float pixelPosition = Float.parseFloat(pos.substring(0, + pos.length() - 2)); + int offsetLength = orientation == ORIENTATION_HORIZONTAL ? getOffsetWidth() + : getOffsetHeight(); + + // Take splitter size into account at the edge + if (pixelPosition + getSplitterSize() >= offsetLength) { + return 100; + } + + return pixelPosition / offsetLength * 100; + } else { + assert pos.endsWith("%"); + return Float.parseFloat(pos.substring(0, pos.length() - 1)); + } + } + + /** + * Returns the given position clamped to the range between current minimum + * and maximum positions. + * + * TODO Should this be in the connector? + * + * @param pos + * Position of the splitter as a CSS string, either pixels or a + * percentage. + * @return minimumPosition if pos is less than minimumPosition; + * maximumPosition if pos is greater than maximumPosition; pos + * otherwise. + */ + private String checkSplitPositionLimits(String pos) { + float positionAsFloat = convertToPixels(pos); + + if (maximumPosition != null + && convertToPixels(maximumPosition) < positionAsFloat) { + pos = maximumPosition; + } else if (minimumPosition != null + && convertToPixels(minimumPosition) > positionAsFloat) { + pos = minimumPosition; + } + return pos; + } + + /** + * Converts given string to the same units as the split position is. + * + * @param pos + * position to be converted + * @return converted position string + */ + private String convertToPositionUnits(String pos) { + if (position.indexOf("%") != -1 && pos.indexOf("%") == -1) { + // position is in percentage, pos in pixels + pos = convertToPercentage(pos) + "%"; + } else if (position.indexOf("px") > 0 && pos.indexOf("px") == -1) { + // position is in pixels and pos in percentage + pos = convertToPixels(pos) + "px"; + } + + return pos; + } + + void setSplitPosition(String pos) { + if (pos == null) { + return; + } + + pos = checkSplitPositionLimits(pos); + if (!pos.equals(position)) { + position = convertToPositionUnits(pos); + } + + // Convert percentage values to pixels + if (pos.indexOf("%") > 0) { + int size = orientation == ORIENTATION_HORIZONTAL ? getOffsetWidth() + : getOffsetHeight(); + float percentage = Float.parseFloat(pos.substring(0, + pos.length() - 1)); + pos = percentage / 100 * size + "px"; + } + + String attributeName; + if (orientation == ORIENTATION_HORIZONTAL) { + if (positionReversed) { + attributeName = "right"; + } else { + attributeName = "left"; + } + } else { + if (positionReversed) { + attributeName = "bottom"; + } else { + attributeName = "top"; + } + } + + Style style = splitter.getStyle(); + if (!pos.equals(style.getProperty(attributeName))) { + style.setProperty(attributeName, pos); + updateSizes(); + } + } + + void updateSizes() { + if (!isAttached()) { + return; + } + + int wholeSize; + int pixelPosition; + + switch (orientation) { + case ORIENTATION_HORIZONTAL: + wholeSize = DOM.getElementPropertyInt(wrapper, "clientWidth"); + pixelPosition = DOM.getElementPropertyInt(splitter, "offsetLeft"); + + // reposition splitter in case it is out of box + if ((pixelPosition > 0 && pixelPosition + getSplitterSize() > wholeSize) + || (positionReversed && pixelPosition < 0)) { + pixelPosition = wholeSize - getSplitterSize(); + if (pixelPosition < 0) { + pixelPosition = 0; + } + setSplitPosition(pixelPosition + "px"); + return; + } + + DOM.setStyleAttribute(firstContainer, "width", pixelPosition + "px"); + int secondContainerWidth = (wholeSize - pixelPosition - getSplitterSize()); + if (secondContainerWidth < 0) { + secondContainerWidth = 0; + } + DOM.setStyleAttribute(secondContainer, "width", + secondContainerWidth + "px"); + DOM.setStyleAttribute(secondContainer, "left", + (pixelPosition + getSplitterSize()) + "px"); + + LayoutManager layoutManager = LayoutManager.get(client); + ConnectorMap connectorMap = ConnectorMap.get(client); + if (firstChild != null) { + ComponentConnector connector = connectorMap + .getConnector(firstChild); + if (connector.isRelativeWidth()) { + layoutManager.reportWidthAssignedToRelative(connector, + pixelPosition); + } else { + layoutManager.setNeedsMeasure(connector); + } + } + if (secondChild != null) { + ComponentConnector connector = connectorMap + .getConnector(secondChild); + if (connector.isRelativeWidth()) { + layoutManager.reportWidthAssignedToRelative(connector, + secondContainerWidth); + } else { + layoutManager.setNeedsMeasure(connector); + } + } + break; + case ORIENTATION_VERTICAL: + wholeSize = DOM.getElementPropertyInt(wrapper, "clientHeight"); + pixelPosition = DOM.getElementPropertyInt(splitter, "offsetTop"); + + // reposition splitter in case it is out of box + if ((pixelPosition > 0 && pixelPosition + getSplitterSize() > wholeSize) + || (positionReversed && pixelPosition < 0)) { + pixelPosition = wholeSize - getSplitterSize(); + if (pixelPosition < 0) { + pixelPosition = 0; + } + setSplitPosition(pixelPosition + "px"); + return; + } + + DOM.setStyleAttribute(firstContainer, "height", pixelPosition + + "px"); + int secondContainerHeight = (wholeSize - pixelPosition - getSplitterSize()); + if (secondContainerHeight < 0) { + secondContainerHeight = 0; + } + DOM.setStyleAttribute(secondContainer, "height", + secondContainerHeight + "px"); + DOM.setStyleAttribute(secondContainer, "top", + (pixelPosition + getSplitterSize()) + "px"); + + layoutManager = LayoutManager.get(client); + connectorMap = ConnectorMap.get(client); + if (firstChild != null) { + ComponentConnector connector = connectorMap + .getConnector(firstChild); + if (connector.isRelativeHeight()) { + layoutManager.reportHeightAssignedToRelative(connector, + pixelPosition); + } else { + layoutManager.setNeedsMeasure(connector); + } + } + if (secondChild != null) { + ComponentConnector connector = connectorMap + .getConnector(secondChild); + if (connector.isRelativeHeight()) { + layoutManager.reportHeightAssignedToRelative(connector, + secondContainerHeight); + } else { + layoutManager.setNeedsMeasure(connector); + } + } + break; + } + } + + void setFirstWidget(Widget w) { + if (firstChild != null) { + firstChild.removeFromParent(); + } + if (w != null) { + super.add(w, firstContainer); + } + firstChild = w; + } + + void setSecondWidget(Widget w) { + if (secondChild != null) { + secondChild.removeFromParent(); + } + if (w != null) { + super.add(w, secondContainer); + } + secondChild = w; + } + + @Override + public void onBrowserEvent(Event event) { + switch (DOM.eventGetType(event)) { + case Event.ONMOUSEMOVE: + // case Event.ONTOUCHMOVE: + if (resizing) { + onMouseMove(event); + } + break; + case Event.ONMOUSEDOWN: + // case Event.ONTOUCHSTART: + onMouseDown(event); + break; + case Event.ONMOUSEOUT: + // Dragging curtain interferes with click events if added in + // mousedown so we add it only when needed i.e., if the mouse moves + // outside the splitter. + if (resizing) { + showDraggingCurtain(); + } + break; + case Event.ONMOUSEUP: + // case Event.ONTOUCHEND: + if (resizing) { + onMouseUp(event); + } + break; + case Event.ONCLICK: + resizing = false; + break; + } + // Only fire click event listeners if the splitter isn't moved + if (Util.isTouchEvent(event) || !resized) { + super.onBrowserEvent(event); + } else if (DOM.eventGetType(event) == Event.ONMOUSEUP) { + // Reset the resized flag after a mouseup has occured so the next + // mousedown/mouseup can be interpreted as a click. + resized = false; + } + } + + public void onMouseDown(Event event) { + if (locked || !isEnabled()) { + return; + } + final Element trg = event.getEventTarget().cast(); + if (trg == splitter || trg == DOM.getChild(splitter, 0)) { + resizing = true; + DOM.setCapture(getElement()); + origX = DOM.getElementPropertyInt(splitter, "offsetLeft"); + origY = DOM.getElementPropertyInt(splitter, "offsetTop"); + origMouseX = Util.getTouchOrMouseClientX(event); + origMouseY = Util.getTouchOrMouseClientY(event); + event.stopPropagation(); + event.preventDefault(); + } + } + + public void onMouseMove(Event event) { + switch (orientation) { + case ORIENTATION_HORIZONTAL: + final int x = Util.getTouchOrMouseClientX(event); + onHorizontalMouseMove(x); + break; + case ORIENTATION_VERTICAL: + default: + final int y = Util.getTouchOrMouseClientY(event); + onVerticalMouseMove(y); + break; + } + + } + + private void onHorizontalMouseMove(int x) { + int newX = origX + x - origMouseX; + if (newX < 0) { + newX = 0; + } + if (newX + getSplitterSize() > getOffsetWidth()) { + newX = getOffsetWidth() - getSplitterSize(); + } + + if (position.indexOf("%") > 0) { + position = convertToPositionUnits(newX + "px"); + } else { + // Reversed position + if (positionReversed) { + position = (getOffsetWidth() - newX - getSplitterSize()) + "px"; + } else { + position = newX + "px"; + } + } + + if (origX != newX) { + resized = true; + } + + // Reversed position + if (positionReversed) { + newX = getOffsetWidth() - newX - getSplitterSize(); + } + + setSplitPosition(newX + "px"); + } + + private void onVerticalMouseMove(int y) { + int newY = origY + y - origMouseY; + if (newY < 0) { + newY = 0; + } + + if (newY + getSplitterSize() > getOffsetHeight()) { + newY = getOffsetHeight() - getSplitterSize(); + } + + if (position.indexOf("%") > 0) { + position = convertToPositionUnits(newY + "px"); + } else { + // Reversed position + if (positionReversed) { + position = (getOffsetHeight() - newY - getSplitterSize()) + + "px"; + } else { + position = newY + "px"; + } + } + + if (origY != newY) { + resized = true; + } + + // Reversed position + if (positionReversed) { + newY = getOffsetHeight() - newY - getSplitterSize(); + } + + setSplitPosition(newY + "px"); + } + + public void onMouseUp(Event event) { + DOM.releaseCapture(getElement()); + hideDraggingCurtain(); + resizing = false; + if (!Util.isTouchEvent(event)) { + onMouseMove(event); + } + fireEvent(new SplitterMoveEvent(this)); + } + + public interface SplitterMoveHandler extends EventHandler { + public void splitterMoved(SplitterMoveEvent event); + + public static class SplitterMoveEvent extends + GwtEvent<SplitterMoveHandler> { + + public static final Type<SplitterMoveHandler> TYPE = new Type<SplitterMoveHandler>(); + + private Widget splitPanel; + + public SplitterMoveEvent(Widget splitPanel) { + this.splitPanel = splitPanel; + } + + @Override + public com.google.gwt.event.shared.GwtEvent.Type<SplitterMoveHandler> getAssociatedType() { + return TYPE; + } + + @Override + protected void dispatch(SplitterMoveHandler handler) { + handler.splitterMoved(this); + } + + } + } + + String getSplitterPosition() { + return position; + } + + /** + * Used in FF to avoid losing mouse capture when pointer is moved on an + * iframe. + */ + private void showDraggingCurtain() { + if (!isDraggingCurtainRequired()) { + return; + } + if (draggingCurtain == null) { + draggingCurtain = DOM.createDiv(); + DOM.setStyleAttribute(draggingCurtain, "position", "absolute"); + DOM.setStyleAttribute(draggingCurtain, "top", "0px"); + DOM.setStyleAttribute(draggingCurtain, "left", "0px"); + DOM.setStyleAttribute(draggingCurtain, "width", "100%"); + DOM.setStyleAttribute(draggingCurtain, "height", "100%"); + DOM.setStyleAttribute(draggingCurtain, "zIndex", "" + + VOverlay.Z_INDEX); + + DOM.appendChild(wrapper, draggingCurtain); + } + } + + /** + * A dragging curtain is required in Gecko and Webkit. + * + * @return true if the browser requires a dragging curtain + */ + private boolean isDraggingCurtainRequired() { + return (BrowserInfo.get().isGecko() || BrowserInfo.get().isWebkit()); + } + + /** + * Hides dragging curtain + */ + private void hideDraggingCurtain() { + if (draggingCurtain != null) { + DOM.removeChild(wrapper, draggingCurtain); + draggingCurtain = null; + } + } + + private int splitterSize = -1; + + private int getSplitterSize() { + if (splitterSize < 0) { + if (isAttached()) { + switch (orientation) { + case ORIENTATION_HORIZONTAL: + splitterSize = DOM.getElementPropertyInt(splitter, + "offsetWidth"); + break; + + default: + splitterSize = DOM.getElementPropertyInt(splitter, + "offsetHeight"); + break; + } + } + } + return splitterSize; + } + + void setStylenames() { + final String splitterClass = CLASSNAME + + (orientation == ORIENTATION_HORIZONTAL ? "-hsplitter" + : "-vsplitter"); + final String firstContainerClass = CLASSNAME + "-first-container"; + final String secondContainerClass = CLASSNAME + "-second-container"; + final String lockedSuffix = locked ? "-locked" : ""; + + splitter.setClassName(splitterClass + lockedSuffix); + firstContainer.setClassName(firstContainerClass); + secondContainer.setClassName(secondContainerClass); + + for (String styleName : componentStyleNames) { + splitter.addClassName(splitterClass + "-" + styleName + + lockedSuffix); + firstContainer.addClassName(firstContainerClass + "-" + styleName); + secondContainer + .addClassName(secondContainerClass + "-" + styleName); + } + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + /** + * Ensures the panels are scrollable eg. after style name changes + */ + void makeScrollable() { + if (touchScrollHandler == null) { + touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this); + } + touchScrollHandler.addElement(firstContainer); + touchScrollHandler.addElement(secondContainer); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelHorizontal.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelHorizontal.java new file mode 100644 index 0000000000..7016620e2d --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelHorizontal.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.splitpanel; + +public class VSplitPanelHorizontal extends VAbstractSplitPanel { + + public VSplitPanelHorizontal() { + super(VAbstractSplitPanel.ORIENTATION_HORIZONTAL); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelVertical.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelVertical.java new file mode 100644 index 0000000000..02397ea4c6 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelVertical.java @@ -0,0 +1,24 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.splitpanel; + +public class VSplitPanelVertical extends VAbstractSplitPanel { + + public VSplitPanelVertical() { + super(VAbstractSplitPanel.ORIENTATION_VERTICAL); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VerticalSplitPanelConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VerticalSplitPanelConnector.java new file mode 100644 index 0000000000..ab4ea350ce --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VerticalSplitPanelConnector.java @@ -0,0 +1,31 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.splitpanel; + +import com.google.gwt.core.client.GWT; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.ui.VerticalSplitPanel; + +@Connect(value = VerticalSplitPanel.class, loadStyle = LoadStyle.EAGER) +public class VerticalSplitPanelConnector extends AbstractSplitPanelConnector { + + @Override + public VSplitPanelVertical getWidget() { + return (VSplitPanelVertical) super.getWidget(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/table/TableConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/table/TableConnector.java new file mode 100644 index 0000000000..77d3fa61d5 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/table/TableConnector.java @@ -0,0 +1,390 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.table; + +import java.util.Iterator; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.AbstractFieldState; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.table.TableConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.DirectionalManagedLayout; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.ServerConnector; +import com.vaadin.terminal.gwt.client.TooltipInfo; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ui.PostLayoutListener; +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.ContextMenuDetails; +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.VScrollTableBody.VScrollTableRow; + +@Connect(com.vaadin.ui.Table.class) +public class TableConnector extends AbstractComponentContainerConnector + implements Paintable, DirectionalManagedLayout, PostLayoutListener { + + @Override + protected void init() { + super.init(); + getWidget().init(getConnection()); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin.terminal + * .gwt.client.UIDL, com.vaadin.terminal.gwt.client.ApplicationConnection) + */ + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + getWidget().rendering = true; + + // If a row has an open context menu, it will be closed as the row is + // detached. Retain a reference here so we can restore the menu if + // required. + ContextMenuDetails contextMenuBeforeUpdate = getWidget().contextMenu; + + if (uidl.hasAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_FIRST)) { + getWidget().serverCacheFirst = uidl + .getIntAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_FIRST); + getWidget().serverCacheLast = uidl + .getIntAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_LAST); + } else { + getWidget().serverCacheFirst = -1; + getWidget().serverCacheLast = -1; + } + /* + * We need to do this before updateComponent since updateComponent calls + * this.setHeight() which will calculate a new body height depending on + * the space available. + */ + if (uidl.hasAttribute("colfooters")) { + getWidget().showColFooters = uidl.getBooleanAttribute("colfooters"); + } + + getWidget().tFoot.setVisible(getWidget().showColFooters); + + if (!isRealUpdate(uidl)) { + getWidget().rendering = false; + return; + } + + getWidget().enabled = isEnabled(); + + if (BrowserInfo.get().isIE8() && !getWidget().enabled) { + /* + * The disabled shim will not cover the table body if it is relative + * in IE8. See #7324 + */ + getWidget().scrollBodyPanel.getElement().getStyle() + .setPosition(Position.STATIC); + } else if (BrowserInfo.get().isIE8()) { + getWidget().scrollBodyPanel.getElement().getStyle() + .setPosition(Position.RELATIVE); + } + + getWidget().paintableId = uidl.getStringAttribute("id"); + getWidget().immediate = getState().isImmediate(); + + int previousTotalRows = getWidget().totalRows; + getWidget().updateTotalRows(uidl); + boolean totalRowsChanged = (getWidget().totalRows != previousTotalRows); + + getWidget().updateDragMode(uidl); + + getWidget().updateSelectionProperties(uidl, getState(), isReadOnly()); + + if (uidl.hasAttribute("alb")) { + getWidget().bodyActionKeys = uidl.getStringArrayAttribute("alb"); + } else { + // Need to clear the actions if the action handlers have been + // removed + getWidget().bodyActionKeys = null; + } + + getWidget().setCacheRateFromUIDL(uidl); + + getWidget().recalcWidths = uidl.hasAttribute("recalcWidths"); + if (getWidget().recalcWidths) { + getWidget().tHead.clear(); + getWidget().tFoot.clear(); + } + + getWidget().updatePageLength(uidl); + + getWidget().updateFirstVisibleAndScrollIfNeeded(uidl); + + getWidget().showRowHeaders = uidl.getBooleanAttribute("rowheaders"); + getWidget().showColHeaders = uidl.getBooleanAttribute("colheaders"); + + getWidget().updateSortingProperties(uidl); + + boolean keyboardSelectionOverRowFetchInProgress = getWidget() + .selectSelectedRows(uidl); + + getWidget().updateActionMap(uidl); + + getWidget().updateColumnProperties(uidl); + + UIDL ac = uidl.getChildByTagName("-ac"); + if (ac == null) { + if (getWidget().dropHandler != null) { + // remove dropHandler if not present anymore + getWidget().dropHandler = null; + } + } else { + if (getWidget().dropHandler == null) { + getWidget().dropHandler = getWidget().new VScrollTableDropHandler(); + } + getWidget().dropHandler.updateAcceptRules(ac); + } + + UIDL partialRowAdditions = uidl.getChildByTagName("prows"); + UIDL partialRowUpdates = uidl.getChildByTagName("urows"); + if (partialRowUpdates != null || partialRowAdditions != null) { + // we may have pending cache row fetch, cancel it. See #2136 + getWidget().rowRequestHandler.cancel(); + + getWidget().updateRowsInBody(partialRowUpdates); + getWidget().addAndRemoveRows(partialRowAdditions); + } else { + UIDL rowData = uidl.getChildByTagName("rows"); + if (rowData != null) { + // we may have pending cache row fetch, cancel it. See #2136 + getWidget().rowRequestHandler.cancel(); + + if (!getWidget().recalcWidths + && getWidget().initializedAndAttached) { + getWidget().updateBody(rowData, + uidl.getIntAttribute("firstrow"), + uidl.getIntAttribute("rows")); + if (getWidget().headerChangedDuringUpdate) { + getWidget().triggerLazyColumnAdjustment(true); + } else if (!getWidget().isScrollPositionVisible() + || totalRowsChanged + || getWidget().lastRenderedHeight != getWidget().scrollBody + .getOffsetHeight()) { + // webkits may still bug with their disturbing scrollbar + // bug, see #3457 + // Run overflow fix for the scrollable area + // #6698 - If there's a scroll going on, don't abort it + // by changing overflows as the length of the contents + // *shouldn't* have changed (unless the number of rows + // or the height of the widget has also changed) + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + Util.runWebkitOverflowAutoFix(getWidget().scrollBodyPanel + .getElement()); + } + }); + } + } else { + getWidget().initializeRows(uidl, rowData); + } + } + } + + // 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. + showSavedContextMenu(contextMenuBeforeUpdate); + + if (!getWidget().isSelectable()) { + getWidget().scrollBody.addStyleName(VScrollTable.CLASSNAME + + "-body-noselection"); + } else { + getWidget().scrollBody.removeStyleName(VScrollTable.CLASSNAME + + "-body-noselection"); + } + + getWidget().hideScrollPositionAnnotation(); + + // selection is no in sync with server, avoid excessive server visits by + // clearing to flag used during the normal operation + if (!keyboardSelectionOverRowFetchInProgress) { + getWidget().selectionChanged = false; + } + + /* + * This is called when the Home or page up button has been pressed in + * selectable mode and the next selected row was not yet rendered in the + * client + */ + if (getWidget().selectFirstItemInNextRender + || getWidget().focusFirstItemInNextRender) { + getWidget().selectFirstRenderedRowInViewPort( + getWidget().focusFirstItemInNextRender); + getWidget().selectFirstItemInNextRender = getWidget().focusFirstItemInNextRender = false; + } + + /* + * This is called when the page down or end button has been pressed in + * selectable mode and the next selected row was not yet rendered in the + * client + */ + if (getWidget().selectLastItemInNextRender + || getWidget().focusLastItemInNextRender) { + getWidget().selectLastRenderedRowInViewPort( + getWidget().focusLastItemInNextRender); + getWidget().selectLastItemInNextRender = getWidget().focusLastItemInNextRender = false; + } + getWidget().multiselectPending = false; + + if (getWidget().focusedRow != null) { + if (!getWidget().focusedRow.isAttached() + && !getWidget().rowRequestHandler.isRunning()) { + // focused row has been orphaned, can't focus + getWidget().focusRowFromBody(); + } + } + + /* + * If the server has (re)initialized the rows, our selectionRangeStart + * row will point to an index that the server knows nothing about, + * causing problems if doing multi selection with shift. The field will + * be cleared a little later when the row focus has been restored. + * (#8584) + */ + if (uidl.hasAttribute(TableConstants.ATTRIBUTE_KEY_MAPPER_RESET) + && uidl.getBooleanAttribute(TableConstants.ATTRIBUTE_KEY_MAPPER_RESET) + && getWidget().selectionRangeStart != null) { + assert !getWidget().selectionRangeStart.isAttached(); + getWidget().selectionRangeStart = getWidget().focusedRow; + } + + getWidget().tabIndex = uidl.hasAttribute("tabindex") ? uidl + .getIntAttribute("tabindex") : 0; + getWidget().setProperTabIndex(); + + getWidget().resizeSortedColumnForSortIndicator(); + + // Remember this to detect situations where overflow hack might be + // needed during scrolling + getWidget().lastRenderedHeight = getWidget().scrollBody + .getOffsetHeight(); + + getWidget().rendering = false; + getWidget().headerChangedDuringUpdate = false; + + } + + @Override + public VScrollTable getWidget() { + return (VScrollTable) super.getWidget(); + } + + @Override + public void updateCaption(ComponentConnector component) { + // NOP, not rendered + } + + @Override + public void layoutVertically() { + getWidget().updateHeight(); + } + + @Override + public void layoutHorizontally() { + getWidget().updateWidth(); + } + + @Override + public void postLayout() { + VScrollTable table = getWidget(); + if (table.sizeNeedsInit) { + table.sizeInit(); + Scheduler.get().scheduleFinally(new ScheduledCommand() { + @Override + public void execute() { + getLayoutManager().setNeedsMeasure(TableConnector.this); + ServerConnector parent = getParent(); + if (parent instanceof ComponentConnector) { + getLayoutManager().setNeedsMeasure( + (ComponentConnector) parent); + } + getLayoutManager().setNeedsVerticalLayout( + TableConnector.this); + getLayoutManager().layoutNow(); + } + }); + } + } + + @Override + public boolean isReadOnly() { + return super.isReadOnly() || getState().isPropertyReadOnly(); + } + + @Override + public AbstractFieldState getState() { + return (AbstractFieldState) super.getState(); + } + + /** + * Shows a saved row context menu if the row for the context menu is still + * visible. Does nothing if a context menu has not been saved. + * + * @param savedContextMenu + */ + public void showSavedContextMenu(ContextMenuDetails savedContextMenu) { + if (isEnabled() && savedContextMenu != null) { + Iterator<Widget> iterator = getWidget().scrollBody.iterator(); + while (iterator.hasNext()) { + Widget w = iterator.next(); + VScrollTableRow row = (VScrollTableRow) w; + if (row.getKey().equals(savedContextMenu.rowKey)) { + getWidget().contextMenu = savedContextMenu; + getConnection().getContextMenu().showAt(row, + savedContextMenu.left, savedContextMenu.top); + } + } + } + } + + @Override + public TooltipInfo getTooltipInfo(Element element) { + + TooltipInfo info = null; + + if (element != getWidget().getElement()) { + Object node = Util.findWidget( + (com.google.gwt.user.client.Element) element, + VScrollTableRow.class); + + if (node != null) { + VScrollTableRow row = (VScrollTableRow) node; + info = row.getTooltip(element); + } + } + + if (info == null) { + info = super.getTooltipInfo(element); + } + + return info; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java b/client/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java new file mode 100644 index 0000000000..aa7da488d8 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java @@ -0,0 +1,6917 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.table; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gwt.core.client.JavaScriptObject; +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.Node; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.Style.Visibility; +import com.google.gwt.dom.client.TableCellElement; +import com.google.gwt.dom.client.TableRowElement; +import com.google.gwt.dom.client.TableSectionElement; +import com.google.gwt.dom.client.Touch; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.event.dom.client.ContextMenuHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.event.dom.client.KeyUpHandler; +import com.google.gwt.event.dom.client.ScrollEvent; +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.user.client.Command; +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.Window; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.UIObject; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ComponentState; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.shared.ui.table.TableConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder; +import com.vaadin.terminal.gwt.client.TooltipInfo; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.VTooltip; +import com.vaadin.terminal.gwt.client.ui.Action; +import com.vaadin.terminal.gwt.client.ui.ActionOwner; +import com.vaadin.terminal.gwt.client.ui.FocusableScrollPanel; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate; +import com.vaadin.terminal.gwt.client.ui.TreeAction; +import com.vaadin.terminal.gwt.client.ui.dd.DDUtil; +import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler; +import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback; +import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager; +import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent; +import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler; +import com.vaadin.terminal.gwt.client.ui.dd.VTransferable; +import com.vaadin.terminal.gwt.client.ui.embedded.VEmbedded; +import com.vaadin.terminal.gwt.client.ui.label.VLabel; +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.VScrollTableBody.VScrollTableRow; +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; + +/** + * VScrollTable + * + * VScrollTable is a FlowPanel having two widgets in it: * TableHead component * + * ScrollPanel + * + * TableHead contains table's header and widgets + logic for resizing, + * reordering and hiding columns. + * + * ScrollPanel contains VScrollTableBody object which handles content. To save + * some bandwidth and to improve clients responsiveness with loads of data, in + * VScrollTableBody all rows are not necessary rendered. There are "spacers" in + * VScrollTableBody to use the exact same space as non-rendered rows would use. + * This way we can use seamlessly traditional scrollbars and scrolling to fetch + * more rows instead of "paging". + * + * In VScrollTable we listen to scroll events. On horizontal scrolling we also + * update TableHeads scroll position which has its scrollbars hidden. On + * vertical scroll events we will check if we are reaching the end of area where + * we have rows rendered and + * + * TODO implement unregistering for child components in Cells + */ +public class VScrollTable extends FlowPanel implements HasWidgets, + ScrollHandler, VHasDropHandler, FocusHandler, BlurHandler, Focusable, + ActionOwner { + + public enum SelectMode { + NONE(0), SINGLE(1), MULTI(2); + private int id; + + private SelectMode(int id) { + this.id = id; + } + + public int getId() { + return id; + } + } + + private static final String ROW_HEADER_COLUMN_KEY = "0"; + + public static final String CLASSNAME = "v-table"; + public static final String CLASSNAME_SELECTION_FOCUS = CLASSNAME + "-focus"; + + private static final double CACHE_RATE_DEFAULT = 2; + + /** + * The default multi select mode where simple left clicks only selects one + * item, CTRL+left click selects multiple items and SHIFT-left click selects + * a range of items. + */ + private static final int MULTISELECT_MODE_DEFAULT = 0; + + /** + * The simple multiselect mode is what the table used to have before + * ctrl/shift selections were added. That is that when this is set clicking + * on an item selects/deselects the item and no ctrl/shift selections are + * available. + */ + private static final int MULTISELECT_MODE_SIMPLE = 1; + + /** + * multiple of pagelength which component will cache when requesting more + * rows + */ + private double cache_rate = CACHE_RATE_DEFAULT; + /** + * fraction of pageLenght which can be scrolled without making new request + */ + private double cache_react_rate = 0.75 * cache_rate; + + public static final char ALIGN_CENTER = 'c'; + public static final char ALIGN_LEFT = 'b'; + public static final char ALIGN_RIGHT = 'e'; + private static final int CHARCODE_SPACE = 32; + private int firstRowInViewPort = 0; + private int pageLength = 15; + private int lastRequestedFirstvisible = 0; // to detect "serverside scroll" + + protected boolean showRowHeaders = false; + + private String[] columnOrder; + + protected ApplicationConnection client; + protected String paintableId; + + boolean immediate; + private boolean nullSelectionAllowed = true; + + private SelectMode selectMode = SelectMode.NONE; + + private final HashSet<String> selectedRowKeys = new HashSet<String>(); + + /* + * When scrolling and selecting at the same time, the selections are not in + * sync with the server while retrieving new rows (until key is released). + */ + private HashSet<Object> unSyncedselectionsBeforeRowFetch; + + /* + * These are used when jumping between pages when pressing Home and End + */ + boolean selectLastItemInNextRender = false; + boolean selectFirstItemInNextRender = false; + boolean focusFirstItemInNextRender = false; + boolean focusLastItemInNextRender = false; + + /* + * The currently focused row + */ + VScrollTableRow focusedRow; + + /* + * Helper to store selection range start in when using the keyboard + */ + VScrollTableRow selectionRangeStart; + + /* + * Flag for notifying when the selection has changed and should be sent to + * the server + */ + boolean selectionChanged = false; + + /* + * The speed (in pixels) which the scrolling scrolls vertically/horizontally + */ + private int scrollingVelocity = 10; + + private Timer scrollingVelocityTimer = null; + + String[] bodyActionKeys; + + private boolean enableDebug = false; + + private static final boolean hasNativeTouchScrolling = BrowserInfo.get() + .isTouchDevice() + && !BrowserInfo.get().requiresTouchScrollDelegate(); + + private Set<String> noncollapsibleColumns; + + /** + * The last known row height used to preserve the height of a table with + * custom row heights and a fixed page length after removing the last row + * from the table. + * + * A new VScrollTableBody instance is created every time the number of rows + * changes causing {@link VScrollTableBody#rowHeight} to be discarded and + * the height recalculated by {@link VScrollTableBody#getRowHeight(boolean)} + * to avoid some rounding problems, e.g. round(2 * 19.8) / 2 = 20 but + * round(3 * 19.8) / 3 = 19.66. + */ + private double lastKnownRowHeight = Double.NaN; + + /** + * Represents a select range of rows + */ + private class SelectionRange { + private VScrollTableRow startRow; + private final int length; + + /** + * Constuctor. + */ + public SelectionRange(VScrollTableRow row1, VScrollTableRow row2) { + VScrollTableRow endRow; + if (row2.isBefore(row1)) { + startRow = row2; + endRow = row1; + } else { + startRow = row1; + endRow = row2; + } + length = endRow.getIndex() - startRow.getIndex() + 1; + } + + public SelectionRange(VScrollTableRow row, int length) { + startRow = row; + this.length = length; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + @Override + public String toString() { + return startRow.getKey() + "-" + length; + } + + private boolean inRange(VScrollTableRow row) { + return row.getIndex() >= startRow.getIndex() + && row.getIndex() < startRow.getIndex() + length; + } + + public Collection<SelectionRange> split(VScrollTableRow row) { + assert row.isAttached(); + ArrayList<SelectionRange> ranges = new ArrayList<SelectionRange>(2); + + int endOfFirstRange = row.getIndex() - 1; + if (!(endOfFirstRange - startRow.getIndex() < 0)) { + // create range of first part unless its length is < 1 + ranges.add(new SelectionRange(startRow, endOfFirstRange + - startRow.getIndex() + 1)); + } + int startOfSecondRange = row.getIndex() + 1; + if (!(getEndIndex() - startOfSecondRange < 0)) { + // create range of second part unless its length is < 1 + VScrollTableRow startOfRange = scrollBody + .getRowByRowIndex(startOfSecondRange); + ranges.add(new SelectionRange(startOfRange, getEndIndex() + - startOfSecondRange + 1)); + } + return ranges; + } + + private int getEndIndex() { + return startRow.getIndex() + length - 1; + } + + }; + + private final HashSet<SelectionRange> selectedRowRanges = new HashSet<SelectionRange>(); + + boolean initializedAndAttached = false; + + /** + * Flag to indicate if a column width recalculation is needed due update. + */ + boolean headerChangedDuringUpdate = false; + + protected final TableHead tHead = new TableHead(); + + final TableFooter tFoot = new TableFooter(); + + final FocusableScrollPanel scrollBodyPanel = new FocusableScrollPanel(true); + + private KeyPressHandler navKeyPressHandler = new KeyPressHandler() { + + @Override + public void onKeyPress(KeyPressEvent keyPressEvent) { + // This is used for Firefox only, since Firefox auto-repeat + // works correctly only if we use a key press handler, other + // browsers handle it correctly when using a key down handler + if (!BrowserInfo.get().isGecko()) { + return; + } + + NativeEvent event = keyPressEvent.getNativeEvent(); + if (!enabled) { + // Cancel default keyboard events on a disabled Table + // (prevents scrolling) + event.preventDefault(); + } else if (hasFocus) { + // Key code in Firefox/onKeyPress is present only for + // special keys, otherwise 0 is returned + int keyCode = event.getKeyCode(); + if (keyCode == 0 && event.getCharCode() == ' ') { + // Provide a keyCode for space to be compatible with + // FireFox keypress event + keyCode = CHARCODE_SPACE; + } + + if (handleNavigation(keyCode, + event.getCtrlKey() || event.getMetaKey(), + event.getShiftKey())) { + event.preventDefault(); + } + + startScrollingVelocityTimer(); + } + } + + }; + + private KeyUpHandler navKeyUpHandler = new KeyUpHandler() { + + @Override + public void onKeyUp(KeyUpEvent keyUpEvent) { + NativeEvent event = keyUpEvent.getNativeEvent(); + int keyCode = event.getKeyCode(); + + if (!isFocusable()) { + cancelScrollingVelocityTimer(); + } else if (isNavigationKey(keyCode)) { + if (keyCode == getNavigationDownKey() + || keyCode == getNavigationUpKey()) { + /* + * in multiselect mode the server may still have value from + * previous page. Clear it unless doing multiselection or + * just moving focus. + */ + if (!event.getShiftKey() && !event.getCtrlKey()) { + instructServerToForgetPreviousSelections(); + } + sendSelectedRows(); + } + cancelScrollingVelocityTimer(); + navKeyDown = false; + } + } + }; + + private KeyDownHandler navKeyDownHandler = new KeyDownHandler() { + + @Override + public void onKeyDown(KeyDownEvent keyDownEvent) { + NativeEvent event = keyDownEvent.getNativeEvent(); + // This is not used for Firefox + if (BrowserInfo.get().isGecko()) { + return; + } + + if (!enabled) { + // Cancel default keyboard events on a disabled Table + // (prevents scrolling) + event.preventDefault(); + } else if (hasFocus) { + if (handleNavigation(event.getKeyCode(), event.getCtrlKey() + || event.getMetaKey(), event.getShiftKey())) { + navKeyDown = true; + event.preventDefault(); + } + + startScrollingVelocityTimer(); + } + } + }; + int totalRows; + + private Set<String> collapsedColumns; + + final RowRequestHandler rowRequestHandler; + VScrollTableBody scrollBody; + private int firstvisible = 0; + private boolean sortAscending; + private String sortColumn; + private String oldSortColumn; + private boolean columnReordering; + + /** + * This map contains captions and icon urls for actions like: * "33_c" -> + * "Edit" * "33_i" -> "http://dom.com/edit.png" + */ + private final HashMap<Object, String> actionMap = new HashMap<Object, String>(); + private String[] visibleColOrder; + private boolean initialContentReceived = false; + private Element scrollPositionElement; + boolean enabled; + boolean showColHeaders; + boolean showColFooters; + + /** flag to indicate that table body has changed */ + private boolean isNewBody = true; + + /* + * Read from the "recalcWidths" -attribute. When it is true, the table will + * recalculate the widths for columns - desirable in some cases. For #1983, + * marked experimental. + */ + boolean recalcWidths = false; + + boolean rendering = false; + private boolean hasFocus = false; + private int dragmode; + + private int multiselectmode; + int tabIndex; + private TouchScrollDelegate touchScrollDelegate; + + int lastRenderedHeight; + + /** + * Values (serverCacheFirst+serverCacheLast) sent by server that tells which + * rows (indexes) are in the server side cache (page buffer). -1 means + * unknown. The server side cache row MUST MATCH the client side cache rows. + * + * If the client side cache contains additional rows with e.g. buttons, it + * will cause out of sync when such a button is pressed. + * + * If the server side cache contains additional rows with e.g. buttons, + * scrolling in the client will cause empty buttons to be rendered + * (cached=true request for non-existing components) + */ + int serverCacheFirst = -1; + int serverCacheLast = -1; + + boolean sizeNeedsInit = true; + + /** + * Used to recall the position of an open context menu if we need to close + * and reopen it during a row update. + */ + class ContextMenuDetails { + String rowKey; + int left; + int top; + + ContextMenuDetails(String rowKey, int left, int top) { + this.rowKey = rowKey; + this.left = left; + this.top = top; + } + } + + protected ContextMenuDetails contextMenu = null; + + public VScrollTable() { + setMultiSelectMode(MULTISELECT_MODE_DEFAULT); + + scrollBodyPanel.addStyleName(CLASSNAME + "-body-wrapper"); + scrollBodyPanel.addFocusHandler(this); + scrollBodyPanel.addBlurHandler(this); + + scrollBodyPanel.addScrollHandler(this); + scrollBodyPanel.addStyleName(CLASSNAME + "-body"); + + /* + * Firefox auto-repeat works correctly only if we use a key press + * handler, other browsers handle it correctly when using a key down + * handler + */ + if (BrowserInfo.get().isGecko()) { + scrollBodyPanel.addKeyPressHandler(navKeyPressHandler); + } else { + scrollBodyPanel.addKeyDownHandler(navKeyDownHandler); + } + scrollBodyPanel.addKeyUpHandler(navKeyUpHandler); + + scrollBodyPanel.sinkEvents(Event.TOUCHEVENTS); + + scrollBodyPanel.sinkEvents(Event.ONCONTEXTMENU); + scrollBodyPanel.addDomHandler(new ContextMenuHandler() { + + @Override + public void onContextMenu(ContextMenuEvent event) { + handleBodyContextMenu(event); + } + }, ContextMenuEvent.getType()); + + setStyleName(CLASSNAME); + + add(tHead); + add(scrollBodyPanel); + add(tFoot); + + rowRequestHandler = new RowRequestHandler(); + } + + public void init(ApplicationConnection client) { + this.client = client; + // Add a handler to clear saved context menu details when the menu + // closes. See #8526. + client.getContextMenu().addCloseHandler(new CloseHandler<PopupPanel>() { + + @Override + public void onClose(CloseEvent<PopupPanel> event) { + contextMenu = null; + } + }); + } + + private void handleBodyContextMenu(ContextMenuEvent event) { + if (enabled && bodyActionKeys != null) { + int left = Util.getTouchOrMouseClientX(event.getNativeEvent()); + int top = Util.getTouchOrMouseClientY(event.getNativeEvent()); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + client.getContextMenu().showAt(this, left, top); + + // Only prevent browser context menu if there are action handlers + // registered + event.stopPropagation(); + event.preventDefault(); + } + } + + /** + * Fires a column resize event which sends the resize information to the + * server. + * + * @param columnId + * The columnId of the column which was resized + * @param originalWidth + * The width in pixels of the column before the resize event + * @param newWidth + * The width in pixels of the column after the resize event + */ + private void fireColumnResizeEvent(String columnId, int originalWidth, + int newWidth) { + client.updateVariable(paintableId, "columnResizeEventColumn", columnId, + false); + client.updateVariable(paintableId, "columnResizeEventPrev", + originalWidth, false); + client.updateVariable(paintableId, "columnResizeEventCurr", newWidth, + immediate); + + } + + /** + * Non-immediate variable update of column widths for a collection of + * columns. + * + * @param columns + * the columns to trigger the events for. + */ + private void sendColumnWidthUpdates(Collection<HeaderCell> columns) { + String[] newSizes = new String[columns.size()]; + int ix = 0; + for (HeaderCell cell : columns) { + newSizes[ix++] = cell.getColKey() + ":" + cell.getWidth(); + } + client.updateVariable(paintableId, "columnWidthUpdates", newSizes, + false); + } + + /** + * Moves the focus one step down + * + * @return Returns true if succeeded + */ + private boolean moveFocusDown() { + return moveFocusDown(0); + } + + /** + * Moves the focus down by 1+offset rows + * + * @return Returns true if succeeded, else false if the selection could not + * be move downwards + */ + private boolean moveFocusDown(int offset) { + if (isSelectable()) { + if (focusedRow == null && scrollBody.iterator().hasNext()) { + // FIXME should focus first visible from top, not first rendered + // ?? + return setRowFocus((VScrollTableRow) scrollBody.iterator() + .next()); + } else { + VScrollTableRow next = getNextRow(focusedRow, offset); + if (next != null) { + return setRowFocus(next); + } + } + } + + return false; + } + + /** + * Moves the selection one step up + * + * @return Returns true if succeeded + */ + private boolean moveFocusUp() { + return moveFocusUp(0); + } + + /** + * Moves the focus row upwards + * + * @return Returns true if succeeded, else false if the selection could not + * be move upwards + * + */ + private boolean moveFocusUp(int offset) { + if (isSelectable()) { + if (focusedRow == null && scrollBody.iterator().hasNext()) { + // FIXME logic is exactly the same as in moveFocusDown, should + // be the opposite?? + return setRowFocus((VScrollTableRow) scrollBody.iterator() + .next()); + } else { + VScrollTableRow prev = getPreviousRow(focusedRow, offset); + if (prev != null) { + return setRowFocus(prev); + } else { + VConsole.log("no previous available"); + } + } + } + + return false; + } + + /** + * Selects a row where the current selection head is + * + * @param ctrlSelect + * Is the selection a ctrl+selection + * @param shiftSelect + * Is the selection a shift+selection + * @return Returns truw + */ + private void selectFocusedRow(boolean ctrlSelect, boolean shiftSelect) { + if (focusedRow != null) { + // Arrows moves the selection and clears previous selections + if (isSelectable() && !ctrlSelect && !shiftSelect) { + deselectAll(); + focusedRow.toggleSelection(); + selectionRangeStart = focusedRow; + } else if (isSelectable() && ctrlSelect && !shiftSelect) { + // Ctrl+arrows moves selection head + selectionRangeStart = focusedRow; + // No selection, only selection head is moved + } else if (isMultiSelectModeAny() && !ctrlSelect && shiftSelect) { + // Shift+arrows selection selects a range + focusedRow.toggleShiftSelection(shiftSelect); + } + } + } + + /** + * Sends the selection to the server if changed since the last update/visit. + */ + protected void sendSelectedRows() { + sendSelectedRows(immediate); + } + + /** + * Sends the selection to the server if it has been changed since the last + * update/visit. + * + * @param immediately + * set to true to immediately send the rows + */ + protected void sendSelectedRows(boolean immediately) { + // Don't send anything if selection has not changed + if (!selectionChanged) { + return; + } + + // Reset selection changed flag + selectionChanged = false; + + // Note: changing the immediateness of this might require changes to + // "clickEvent" immediateness also. + if (isMultiSelectModeDefault()) { + // Convert ranges to a set of strings + Set<String> ranges = new HashSet<String>(); + for (SelectionRange range : selectedRowRanges) { + ranges.add(range.toString()); + } + + // Send the selected row ranges + client.updateVariable(paintableId, "selectedRanges", + ranges.toArray(new String[selectedRowRanges.size()]), false); + + // clean selectedRowKeys so that they don't contain excess values + for (Iterator<String> iterator = selectedRowKeys.iterator(); iterator + .hasNext();) { + String key = iterator.next(); + VScrollTableRow renderedRowByKey = getRenderedRowByKey(key); + if (renderedRowByKey != null) { + for (SelectionRange range : selectedRowRanges) { + if (range.inRange(renderedRowByKey)) { + iterator.remove(); + } + } + } else { + // orphaned selected key, must be in a range, ignore + iterator.remove(); + } + + } + } + + // Send the selected rows + client.updateVariable(paintableId, "selected", + selectedRowKeys.toArray(new String[selectedRowKeys.size()]), + immediately); + + } + + /** + * Get the key that moves the selection head upwards. By default it is the + * up arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationUpKey() { + return KeyCodes.KEY_UP; + } + + /** + * Get the key that moves the selection head downwards. By default it is the + * down arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationDownKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Get the key that scrolls to the left in the table. By default it is the + * left arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationLeftKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * Get the key that scroll to the right on the table. By default it is the + * right arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationRightKey() { + return KeyCodes.KEY_RIGHT; + } + + /** + * Get the key that selects an item in the table. By default it is the space + * bar key but by overriding this you can change the key to whatever you + * want. + * + * @return + */ + protected int getNavigationSelectKey() { + return CHARCODE_SPACE; + } + + /** + * Get the key the moves the selection one page up in the table. By default + * this is the Page Up key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationPageUpKey() { + return KeyCodes.KEY_PAGEUP; + } + + /** + * Get the key the moves the selection one page down in the table. By + * default this is the Page Down key but by overriding this you can change + * the key to whatever you want. + * + * @return + */ + protected int getNavigationPageDownKey() { + return KeyCodes.KEY_PAGEDOWN; + } + + /** + * Get the key the moves the selection to the beginning of the table. By + * default this is the Home key but by overriding this you can change the + * key to whatever you want. + * + * @return + */ + protected int getNavigationStartKey() { + return KeyCodes.KEY_HOME; + } + + /** + * Get the key the moves the selection to the end of the table. By default + * this is the End key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationEndKey() { + return KeyCodes.KEY_END; + } + + void initializeRows(UIDL uidl, UIDL rowData) { + if (scrollBody != null) { + scrollBody.removeFromParent(); + } + scrollBody = createScrollBody(); + + scrollBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"), + uidl.getIntAttribute("rows")); + scrollBodyPanel.add(scrollBody); + + // New body starts scrolled to the left, make sure the header and footer + // are also scrolled to the left + tHead.setHorizontalScrollPosition(0); + tFoot.setHorizontalScrollPosition(0); + + initialContentReceived = true; + sizeNeedsInit = true; + scrollBody.restoreRowVisibility(); + } + + void updateColumnProperties(UIDL uidl) { + updateColumnOrder(uidl); + + updateCollapsedColumns(uidl); + + UIDL vc = uidl.getChildByTagName("visiblecolumns"); + if (vc != null) { + tHead.updateCellsFromUIDL(vc); + tFoot.updateCellsFromUIDL(vc); + } + + updateHeader(uidl.getStringArrayAttribute("vcolorder")); + updateFooter(uidl.getStringArrayAttribute("vcolorder")); + if (uidl.hasVariable("noncollapsiblecolumns")) { + noncollapsibleColumns = uidl + .getStringArrayVariableAsSet("noncollapsiblecolumns"); + } + } + + private void updateCollapsedColumns(UIDL uidl) { + if (uidl.hasVariable("collapsedcolumns")) { + tHead.setColumnCollapsingAllowed(true); + collapsedColumns = uidl + .getStringArrayVariableAsSet("collapsedcolumns"); + } else { + tHead.setColumnCollapsingAllowed(false); + } + } + + private void updateColumnOrder(UIDL uidl) { + if (uidl.hasVariable("columnorder")) { + columnReordering = true; + columnOrder = uidl.getStringArrayVariable("columnorder"); + } else { + columnReordering = false; + columnOrder = null; + } + } + + boolean selectSelectedRows(UIDL uidl) { + boolean keyboardSelectionOverRowFetchInProgress = false; + + if (uidl.hasVariable("selected")) { + final Set<String> selectedKeys = uidl + .getStringArrayVariableAsSet("selected"); + if (scrollBody != null) { + Iterator<Widget> iterator = scrollBody.iterator(); + while (iterator.hasNext()) { + /* + * Make the focus reflect to the server side state unless we + * are currently selecting multiple rows with keyboard. + */ + VScrollTableRow row = (VScrollTableRow) iterator.next(); + boolean selected = selectedKeys.contains(row.getKey()); + if (!selected + && unSyncedselectionsBeforeRowFetch != null + && unSyncedselectionsBeforeRowFetch.contains(row + .getKey())) { + selected = true; + keyboardSelectionOverRowFetchInProgress = true; + } + if (selected != row.isSelected()) { + row.toggleSelection(); + if (!isSingleSelectMode() && !selected) { + // Update selection range in case a row is + // unselected from the middle of a range - #8076 + removeRowFromUnsentSelectionRanges(row); + } + } + } + } + } + unSyncedselectionsBeforeRowFetch = null; + return keyboardSelectionOverRowFetchInProgress; + } + + void updateSortingProperties(UIDL uidl) { + oldSortColumn = sortColumn; + if (uidl.hasVariable("sortascending")) { + sortAscending = uidl.getBooleanVariable("sortascending"); + sortColumn = uidl.getStringVariable("sortcolumn"); + } + } + + void resizeSortedColumnForSortIndicator() { + // Force recalculation of the captionContainer element inside the header + // cell to accomodate for the size of the sort arrow. + HeaderCell sortedHeader = tHead.getHeaderCell(sortColumn); + if (sortedHeader != null) { + tHead.resizeCaptionContainer(sortedHeader); + } + // Also recalculate the width of the captionContainer element in the + // previously sorted header, since this now has more room. + HeaderCell oldSortedHeader = tHead.getHeaderCell(oldSortColumn); + if (oldSortedHeader != null) { + tHead.resizeCaptionContainer(oldSortedHeader); + } + } + + void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) { + firstvisible = uidl.hasVariable("firstvisible") ? uidl + .getIntVariable("firstvisible") : 0; + if (firstvisible != lastRequestedFirstvisible && scrollBody != null) { + // received 'surprising' firstvisible from server: scroll there + firstRowInViewPort = firstvisible; + scrollBodyPanel + .setScrollPosition(measureRowHeightOffset(firstvisible)); + } + } + + protected int measureRowHeightOffset(int rowIx) { + return (int) (rowIx * scrollBody.getRowHeight()); + } + + void updatePageLength(UIDL uidl) { + int oldPageLength = pageLength; + if (uidl.hasAttribute("pagelength")) { + pageLength = uidl.getIntAttribute("pagelength"); + } else { + // pagelenght is "0" meaning scrolling is turned off + pageLength = totalRows; + } + + if (oldPageLength != pageLength && initializedAndAttached) { + // page length changed, need to update size + sizeNeedsInit = true; + } + } + + void updateSelectionProperties(UIDL uidl, ComponentState state, + boolean readOnly) { + setMultiSelectMode(uidl.hasAttribute("multiselectmode") ? uidl + .getIntAttribute("multiselectmode") : MULTISELECT_MODE_DEFAULT); + + nullSelectionAllowed = uidl.hasAttribute("nsa") ? uidl + .getBooleanAttribute("nsa") : true; + + if (uidl.hasAttribute("selectmode")) { + if (readOnly) { + selectMode = SelectMode.NONE; + } else if (uidl.getStringAttribute("selectmode").equals("multi")) { + selectMode = SelectMode.MULTI; + } else if (uidl.getStringAttribute("selectmode").equals("single")) { + selectMode = SelectMode.SINGLE; + } else { + selectMode = SelectMode.NONE; + } + } + } + + void updateDragMode(UIDL uidl) { + dragmode = uidl.hasAttribute("dragmode") ? uidl + .getIntAttribute("dragmode") : 0; + if (BrowserInfo.get().isIE()) { + if (dragmode > 0) { + getElement().setPropertyJSO("onselectstart", + getPreventTextSelectionIEHack()); + } else { + getElement().setPropertyJSO("onselectstart", null); + } + } + } + + protected void updateTotalRows(UIDL uidl) { + int newTotalRows = uidl.getIntAttribute("totalrows"); + if (newTotalRows != getTotalRows()) { + if (scrollBody != null) { + if (getTotalRows() == 0) { + tHead.clear(); + tFoot.clear(); + } + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + setTotalRows(newTotalRows); + } + } + + protected void setTotalRows(int newTotalRows) { + totalRows = newTotalRows; + } + + public int getTotalRows() { + return totalRows; + } + + void focusRowFromBody() { + if (selectedRowKeys.size() == 1) { + // try to focus a row currently selected and in viewport + String selectedRowKey = selectedRowKeys.iterator().next(); + if (selectedRowKey != null) { + VScrollTableRow renderedRow = getRenderedRowByKey(selectedRowKey); + if (renderedRow == null || !renderedRow.isInViewPort()) { + setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort)); + } else { + setRowFocus(renderedRow); + } + } + } else { + // multiselect mode + setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort)); + } + } + + protected VScrollTableBody createScrollBody() { + return new VScrollTableBody(); + } + + /** + * Selects the last row visible in the table + * + * @param focusOnly + * Should the focus only be moved to the last row + */ + void selectLastRenderedRowInViewPort(boolean focusOnly) { + int index = firstRowInViewPort + getFullyVisibleRowCount(); + VScrollTableRow lastRowInViewport = scrollBody.getRowByRowIndex(index); + if (lastRowInViewport == null) { + // this should not happen in normal situations (white space at the + // end of viewport). Select the last rendered as a fallback. + lastRowInViewport = scrollBody.getRowByRowIndex(scrollBody + .getLastRendered()); + if (lastRowInViewport == null) { + return; // empty table + } + } + setRowFocus(lastRowInViewport); + if (!focusOnly) { + selectFocusedRow(false, multiselectPending); + sendSelectedRows(); + } + } + + /** + * Selects the first row visible in the table + * + * @param focusOnly + * Should the focus only be moved to the first row + */ + void selectFirstRenderedRowInViewPort(boolean focusOnly) { + int index = firstRowInViewPort; + VScrollTableRow firstInViewport = scrollBody.getRowByRowIndex(index); + if (firstInViewport == null) { + // this should not happen in normal situations + return; + } + setRowFocus(firstInViewport); + if (!focusOnly) { + selectFocusedRow(false, multiselectPending); + sendSelectedRows(); + } + } + + void setCacheRateFromUIDL(UIDL uidl) { + setCacheRate(uidl.hasAttribute("cr") ? uidl.getDoubleAttribute("cr") + : CACHE_RATE_DEFAULT); + } + + private void setCacheRate(double d) { + if (cache_rate != d) { + cache_rate = d; + cache_react_rate = 0.75 * d; + } + } + + void updateActionMap(UIDL mainUidl) { + UIDL actionsUidl = mainUidl.getChildByTagName("actions"); + if (actionsUidl == null) { + return; + } + + final Iterator<?> it = actionsUidl.getChildIterator(); + while (it.hasNext()) { + final UIDL action = (UIDL) it.next(); + final String key = action.getStringAttribute("key"); + final String caption = action.getStringAttribute("caption"); + actionMap.put(key + "_c", caption); + if (action.hasAttribute("icon")) { + // TODO need some uri handling ?? + actionMap.put(key + "_i", client.translateVaadinUri(action + .getStringAttribute("icon"))); + } else { + actionMap.remove(key + "_i"); + } + } + + } + + public String getActionCaption(String actionKey) { + return actionMap.get(actionKey + "_c"); + } + + public String getActionIcon(String actionKey) { + return actionMap.get(actionKey + "_i"); + } + + private void updateHeader(String[] strings) { + if (strings == null) { + return; + } + + int visibleCols = strings.length; + int colIndex = 0; + if (showRowHeaders) { + tHead.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex); + visibleCols++; + visibleColOrder = new String[visibleCols]; + visibleColOrder[colIndex] = ROW_HEADER_COLUMN_KEY; + colIndex++; + } else { + visibleColOrder = new String[visibleCols]; + tHead.removeCell(ROW_HEADER_COLUMN_KEY); + } + + int i; + for (i = 0; i < strings.length; i++) { + final String cid = strings[i]; + visibleColOrder[colIndex] = cid; + tHead.enableColumn(cid, colIndex); + colIndex++; + } + + tHead.setVisible(showColHeaders); + setContainerHeight(); + + } + + /** + * Updates footers. + * <p> + * Update headers whould be called before this method is called! + * </p> + * + * @param strings + */ + private void updateFooter(String[] strings) { + if (strings == null) { + return; + } + + // Add dummy column if row headers are present + int colIndex = 0; + if (showRowHeaders) { + tFoot.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex); + colIndex++; + } else { + tFoot.removeCell(ROW_HEADER_COLUMN_KEY); + } + + int i; + for (i = 0; i < strings.length; i++) { + final String cid = strings[i]; + tFoot.enableColumn(cid, colIndex); + colIndex++; + } + + tFoot.setVisible(showColFooters); + } + + /** + * @param uidl + * which contains row data + * @param firstRow + * first row in data set + * @param reqRows + * amount of rows in data set + */ + void updateBody(UIDL uidl, int firstRow, int reqRows) { + if (uidl == null || reqRows < 1) { + // container is empty, remove possibly existing rows + if (firstRow <= 0) { + while (scrollBody.getLastRendered() > scrollBody.firstRendered) { + scrollBody.unlinkRow(false); + } + scrollBody.unlinkRow(false); + } + return; + } + + scrollBody.renderRows(uidl, firstRow, reqRows); + + discardRowsOutsideCacheWindow(); + } + + void updateRowsInBody(UIDL partialRowUpdates) { + if (partialRowUpdates == null) { + return; + } + int firstRowIx = partialRowUpdates.getIntAttribute("firsturowix"); + int count = partialRowUpdates.getIntAttribute("numurows"); + scrollBody.unlinkRows(firstRowIx, count); + scrollBody.insertRows(partialRowUpdates, firstRowIx, count); + } + + /** + * Updates the internal cache by unlinking rows that fall outside of the + * caching window. + */ + protected void discardRowsOutsideCacheWindow() { + int firstRowToKeep = (int) (firstRowInViewPort - pageLength + * cache_rate); + int lastRowToKeep = (int) (firstRowInViewPort + pageLength + pageLength + * cache_rate); + debug("Client side calculated cache rows to keep: " + firstRowToKeep + + "-" + lastRowToKeep); + + if (serverCacheFirst != -1) { + firstRowToKeep = serverCacheFirst; + lastRowToKeep = serverCacheLast; + debug("Server cache rows that override: " + serverCacheFirst + "-" + + serverCacheLast); + if (firstRowToKeep < scrollBody.getFirstRendered() + || lastRowToKeep > scrollBody.getLastRendered()) { + debug("*** Server wants us to keep " + serverCacheFirst + "-" + + serverCacheLast + " but we only have rows " + + scrollBody.getFirstRendered() + "-" + + scrollBody.getLastRendered() + " rendered!"); + } + } + discardRowsOutsideOf(firstRowToKeep, lastRowToKeep); + + scrollBody.fixSpacers(); + + scrollBody.restoreRowVisibility(); + } + + private void discardRowsOutsideOf(int optimalFirstRow, int optimalLastRow) { + /* + * firstDiscarded and lastDiscarded are only calculated for debug + * purposes + */ + int firstDiscarded = -1, lastDiscarded = -1; + boolean cont = true; + while (cont && scrollBody.getLastRendered() > optimalFirstRow + && scrollBody.getFirstRendered() < optimalFirstRow) { + if (firstDiscarded == -1) { + firstDiscarded = scrollBody.getFirstRendered(); + } + + // removing row from start + cont = scrollBody.unlinkRow(true); + } + if (firstDiscarded != -1) { + lastDiscarded = scrollBody.getFirstRendered() - 1; + debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded); + } + firstDiscarded = lastDiscarded = -1; + + cont = true; + while (cont && scrollBody.getLastRendered() > optimalLastRow) { + if (lastDiscarded == -1) { + lastDiscarded = scrollBody.getLastRendered(); + } + + // removing row from the end + cont = scrollBody.unlinkRow(false); + } + if (lastDiscarded != -1) { + firstDiscarded = scrollBody.getLastRendered() + 1; + debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded); + } + + debug("Now in cache: " + scrollBody.getFirstRendered() + "-" + + scrollBody.getLastRendered()); + } + + /** + * Inserts rows in the table body or removes them from the table body based + * on the commands in the UIDL. + * + * @param partialRowAdditions + * the UIDL containing row updates. + */ + protected void addAndRemoveRows(UIDL partialRowAdditions) { + if (partialRowAdditions == null) { + return; + } + if (partialRowAdditions.hasAttribute("hide")) { + scrollBody.unlinkAndReindexRows( + partialRowAdditions.getIntAttribute("firstprowix"), + partialRowAdditions.getIntAttribute("numprows")); + scrollBody.ensureCacheFilled(); + } else { + if (partialRowAdditions.hasAttribute("delbelow")) { + scrollBody.insertRowsDeleteBelow(partialRowAdditions, + partialRowAdditions.getIntAttribute("firstprowix"), + partialRowAdditions.getIntAttribute("numprows")); + } else { + scrollBody.insertAndReindexRows(partialRowAdditions, + partialRowAdditions.getIntAttribute("firstprowix"), + partialRowAdditions.getIntAttribute("numprows")); + } + } + + discardRowsOutsideCacheWindow(); + } + + /** + * Gives correct column index for given column key ("cid" in UIDL). + * + * @param colKey + * @return column index of visible columns, -1 if column not visible + */ + private int getColIndexByKey(String colKey) { + // return 0 if asked for rowHeaders + if (ROW_HEADER_COLUMN_KEY.equals(colKey)) { + return 0; + } + for (int i = 0; i < visibleColOrder.length; i++) { + if (visibleColOrder[i].equals(colKey)) { + return i; + } + } + return -1; + } + + private boolean isMultiSelectModeSimple() { + return selectMode == SelectMode.MULTI + && multiselectmode == MULTISELECT_MODE_SIMPLE; + } + + private boolean isSingleSelectMode() { + return selectMode == SelectMode.SINGLE; + } + + private boolean isMultiSelectModeAny() { + return selectMode == SelectMode.MULTI; + } + + private boolean isMultiSelectModeDefault() { + return selectMode == SelectMode.MULTI + && multiselectmode == MULTISELECT_MODE_DEFAULT; + } + + private void setMultiSelectMode(int multiselectmode) { + if (BrowserInfo.get().isTouchDevice()) { + // Always use the simple mode for touch devices that do not have + // shift/ctrl keys + this.multiselectmode = MULTISELECT_MODE_SIMPLE; + } else { + this.multiselectmode = multiselectmode; + } + + } + + protected boolean isSelectable() { + return selectMode.getId() > SelectMode.NONE.getId(); + } + + private boolean isCollapsedColumn(String colKey) { + if (collapsedColumns == null) { + return false; + } + if (collapsedColumns.contains(colKey)) { + return true; + } + return false; + } + + private String getColKeyByIndex(int index) { + return tHead.getHeaderCell(index).getColKey(); + } + + private void setColWidth(int colIndex, int w, boolean isDefinedWidth) { + final HeaderCell hcell = tHead.getHeaderCell(colIndex); + + // Make sure that the column grows to accommodate the sort indicator if + // necessary. + if (w < hcell.getMinWidth()) { + w = hcell.getMinWidth(); + } + + // Set header column width + hcell.setWidth(w, isDefinedWidth); + + // Ensure indicators have been taken into account + tHead.resizeCaptionContainer(hcell); + + // Set body column width + scrollBody.setColWidth(colIndex, w); + + // Set footer column width + FooterCell fcell = tFoot.getFooterCell(colIndex); + fcell.setWidth(w, isDefinedWidth); + } + + private int getColWidth(String colKey) { + return tHead.getHeaderCell(colKey).getWidth(); + } + + /** + * Get a rendered row by its key + * + * @param key + * The key to search with + * @return + */ + public VScrollTableRow getRenderedRowByKey(String key) { + if (scrollBody != null) { + final Iterator<Widget> it = scrollBody.iterator(); + VScrollTableRow r = null; + while (it.hasNext()) { + r = (VScrollTableRow) it.next(); + if (r.getKey().equals(key)) { + return r; + } + } + } + return null; + } + + /** + * Returns the next row to the given row + * + * @param row + * The row to calculate from + * + * @return The next row or null if no row exists + */ + private VScrollTableRow getNextRow(VScrollTableRow row, int offset) { + final Iterator<Widget> it = scrollBody.iterator(); + VScrollTableRow r = null; + while (it.hasNext()) { + r = (VScrollTableRow) it.next(); + if (r == row) { + r = null; + while (offset >= 0 && it.hasNext()) { + r = (VScrollTableRow) it.next(); + offset--; + } + return r; + } + } + + return null; + } + + /** + * Returns the previous row from the given row + * + * @param row + * The row to calculate from + * @return The previous row or null if no row exists + */ + private VScrollTableRow getPreviousRow(VScrollTableRow row, int offset) { + final Iterator<Widget> it = scrollBody.iterator(); + final Iterator<Widget> offsetIt = scrollBody.iterator(); + VScrollTableRow r = null; + VScrollTableRow prev = null; + while (it.hasNext()) { + r = (VScrollTableRow) it.next(); + if (offset < 0) { + prev = (VScrollTableRow) offsetIt.next(); + } + if (r == row) { + return prev; + } + offset--; + } + + return null; + } + + protected void reOrderColumn(String columnKey, int newIndex) { + + final int oldIndex = getColIndexByKey(columnKey); + + // Change header order + tHead.moveCell(oldIndex, newIndex); + + // Change body order + scrollBody.moveCol(oldIndex, newIndex); + + // Change footer order + tFoot.moveCell(oldIndex, newIndex); + + /* + * Build new columnOrder and update it to server Note that columnOrder + * also contains collapsed columns so we cannot directly build it from + * cells vector Loop the old columnOrder and append in order to new + * array unless on moved columnKey. On new index also put the moved key + * i == index on columnOrder, j == index on newOrder + */ + final String oldKeyOnNewIndex = visibleColOrder[newIndex]; + if (showRowHeaders) { + newIndex--; // columnOrder don't have rowHeader + } + // add back hidden rows, + for (int i = 0; i < columnOrder.length; i++) { + if (columnOrder[i].equals(oldKeyOnNewIndex)) { + break; // break loop at target + } + if (isCollapsedColumn(columnOrder[i])) { + newIndex++; + } + } + // finally we can build the new columnOrder for server + final String[] newOrder = new String[columnOrder.length]; + for (int i = 0, j = 0; j < newOrder.length; i++) { + if (j == newIndex) { + newOrder[j] = columnKey; + j++; + } + if (i == columnOrder.length) { + break; + } + if (columnOrder[i].equals(columnKey)) { + continue; + } + newOrder[j] = columnOrder[i]; + j++; + } + columnOrder = newOrder; + // also update visibleColumnOrder + int i = showRowHeaders ? 1 : 0; + for (int j = 0; j < newOrder.length; j++) { + final String cid = newOrder[j]; + if (!isCollapsedColumn(cid)) { + visibleColOrder[i++] = cid; + } + } + client.updateVariable(paintableId, "columnorder", columnOrder, false); + if (client.hasEventListeners(this, + TableConstants.COLUMN_REORDER_EVENT_ID)) { + client.sendPendingVariableChanges(); + } + } + + @Override + protected void onDetach() { + rowRequestHandler.cancel(); + super.onDetach(); + // ensure that scrollPosElement will be detached + if (scrollPositionElement != null) { + final Element parent = DOM.getParent(scrollPositionElement); + if (parent != null) { + DOM.removeChild(parent, scrollPositionElement); + } + } + } + + /** + * Run only once when component is attached and received its initial + * content. This function: + * + * * Syncs headers and bodys "natural widths and saves the values. + * + * * Sets proper width and height + * + * * Makes deferred request to get some cache rows + */ + void sizeInit() { + sizeNeedsInit = false; + + scrollBody.setContainerHeight(); + + /* + * We will use browsers table rendering algorithm to find proper column + * widths. If content and header take less space than available, we will + * divide extra space relatively to each column which has not width set. + * + * Overflow pixels are added to last column. + */ + + Iterator<Widget> headCells = tHead.iterator(); + Iterator<Widget> footCells = tFoot.iterator(); + int i = 0; + int totalExplicitColumnsWidths = 0; + int total = 0; + float expandRatioDivider = 0; + + final int[] widths = new int[tHead.visibleCells.size()]; + + tHead.enableBrowserIntelligence(); + tFoot.enableBrowserIntelligence(); + + // first loop: collect natural widths + while (headCells.hasNext()) { + final HeaderCell hCell = (HeaderCell) headCells.next(); + final FooterCell fCell = (FooterCell) footCells.next(); + int w = hCell.getWidth(); + if (hCell.isDefinedWidth()) { + // server has defined column width explicitly + totalExplicitColumnsWidths += w; + } else { + if (hCell.getExpandRatio() > 0) { + expandRatioDivider += hCell.getExpandRatio(); + w = 0; + } else { + // get and store greater of header width and column width, + // and + // store it as a minimumn natural col width + int headerWidth = hCell.getNaturalColumnWidth(i); + int footerWidth = fCell.getNaturalColumnWidth(i); + w = headerWidth > footerWidth ? headerWidth : footerWidth; + } + hCell.setNaturalMinimumColumnWidth(w); + fCell.setNaturalMinimumColumnWidth(w); + } + widths[i] = w; + total += w; + i++; + } + + tHead.disableBrowserIntelligence(); + tFoot.disableBrowserIntelligence(); + + boolean willHaveScrollbarz = willHaveScrollbars(); + + // fix "natural" width if width not set + if (isDynamicWidth()) { + int w = total; + w += scrollBody.getCellExtraWidth() * visibleColOrder.length; + if (willHaveScrollbarz) { + w += Util.getNativeScrollbarSize(); + } + setContentWidth(w); + } + + int availW = scrollBody.getAvailableWidth(); + if (BrowserInfo.get().isIE()) { + // Hey IE, are you really sure about this? + availW = scrollBody.getAvailableWidth(); + } + availW -= scrollBody.getCellExtraWidth() * visibleColOrder.length; + + if (willHaveScrollbarz) { + availW -= Util.getNativeScrollbarSize(); + } + + // TODO refactor this code to be the same as in resize timer + boolean needsReLayout = false; + + if (availW > total) { + // natural size is smaller than available space + final int extraSpace = availW - total; + final int totalWidthR = total - totalExplicitColumnsWidths; + int checksum = 0; + needsReLayout = true; + + if (extraSpace == 1) { + // We cannot divide one single pixel so we give it the first + // undefined column + headCells = tHead.iterator(); + i = 0; + checksum = availW; + while (headCells.hasNext()) { + HeaderCell hc = (HeaderCell) headCells.next(); + if (!hc.isDefinedWidth()) { + widths[i]++; + break; + } + i++; + } + + } else if (expandRatioDivider > 0) { + // visible columns have some active expand ratios, excess + // space is divided according to them + headCells = tHead.iterator(); + i = 0; + while (headCells.hasNext()) { + HeaderCell hCell = (HeaderCell) headCells.next(); + if (hCell.getExpandRatio() > 0) { + int w = widths[i]; + final int newSpace = Math.round((extraSpace * (hCell + .getExpandRatio() / expandRatioDivider))); + w += newSpace; + widths[i] = w; + } + checksum += widths[i]; + i++; + } + } else if (totalWidthR > 0) { + // no expand ratios defined, we will share extra space + // relatively to "natural widths" among those without + // explicit width + headCells = tHead.iterator(); + i = 0; + while (headCells.hasNext()) { + HeaderCell hCell = (HeaderCell) headCells.next(); + if (!hCell.isDefinedWidth()) { + int w = widths[i]; + final int newSpace = Math.round((float) extraSpace + * (float) w / totalWidthR); + w += newSpace; + widths[i] = w; + } + checksum += widths[i]; + i++; + } + } + + if (extraSpace > 0 && checksum != availW) { + /* + * There might be in some cases a rounding error of 1px when + * extra space is divided so if there is one then we give the + * first undefined column 1 more pixel + */ + headCells = tHead.iterator(); + i = 0; + while (headCells.hasNext()) { + HeaderCell hc = (HeaderCell) headCells.next(); + if (!hc.isDefinedWidth()) { + widths[i] += availW - checksum; + break; + } + i++; + } + } + + } else { + // bodys size will be more than available and scrollbar will appear + } + + // last loop: set possibly modified values or reset if new tBody + i = 0; + headCells = tHead.iterator(); + while (headCells.hasNext()) { + final HeaderCell hCell = (HeaderCell) headCells.next(); + if (isNewBody || hCell.getWidth() == -1) { + final int w = widths[i]; + setColWidth(i, w, false); + } + i++; + } + + initializedAndAttached = true; + + if (needsReLayout) { + scrollBody.reLayoutComponents(); + } + + updatePageLength(); + + /* + * Fix "natural" height if height is not set. This must be after width + * fixing so the components' widths have been adjusted. + */ + if (isDynamicHeight()) { + /* + * We must force an update of the row height as this point as it + * might have been (incorrectly) calculated earlier + */ + + int bodyHeight; + if (pageLength == totalRows) { + /* + * A hack to support variable height rows when paging is off. + * Generally this is not supported by scrolltable. We want to + * show all rows so the bodyHeight should be equal to the table + * height. + */ + // int bodyHeight = scrollBody.getOffsetHeight(); + bodyHeight = scrollBody.getRequiredHeight(); + } else { + bodyHeight = (int) Math.round(scrollBody.getRowHeight(true) + * pageLength); + } + boolean needsSpaceForHorizontalSrollbar = (total > availW); + if (needsSpaceForHorizontalSrollbar) { + bodyHeight += Util.getNativeScrollbarSize(); + } + scrollBodyPanel.setHeight(bodyHeight + "px"); + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); + } + + isNewBody = false; + + if (firstvisible > 0) { + // Deferred due to some Firefox oddities + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + scrollBodyPanel + .setScrollPosition(measureRowHeightOffset(firstvisible)); + firstRowInViewPort = firstvisible; + } + }); + } + + if (enabled) { + // Do we need cache rows + if (scrollBody.getLastRendered() + 1 < firstRowInViewPort + + pageLength + (int) cache_react_rate * pageLength) { + if (totalRows - 1 > scrollBody.getLastRendered()) { + // fetch cache rows + int firstInNewSet = scrollBody.getLastRendered() + 1; + rowRequestHandler.setReqFirstRow(firstInNewSet); + int lastInNewSet = (int) (firstRowInViewPort + pageLength + cache_rate + * pageLength); + if (lastInNewSet > totalRows - 1) { + lastInNewSet = totalRows - 1; + } + rowRequestHandler.setReqRows(lastInNewSet - firstInNewSet + + 1); + rowRequestHandler.deferRowFetch(1); + } + } + } + + /* + * Ensures the column alignments are correct at initial loading. <br/> + * (child components widths are correct) + */ + scrollBody.reLayoutComponents(); + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); + } + }); + } + + /** + * Note, this method is not official api although declared as protected. + * Extend at you own risk. + * + * @return true if content area will have scrollbars visible. + */ + protected boolean willHaveScrollbars() { + if (isDynamicHeight()) { + if (pageLength < totalRows) { + return true; + } + } else { + int fakeheight = (int) Math.round(scrollBody.getRowHeight() + * totalRows); + int availableHeight = scrollBodyPanel.getElement().getPropertyInt( + "clientHeight"); + if (fakeheight > availableHeight) { + return true; + } + } + return false; + } + + private void announceScrollPosition() { + if (scrollPositionElement == null) { + scrollPositionElement = DOM.createDiv(); + scrollPositionElement.setClassName(CLASSNAME + "-scrollposition"); + scrollPositionElement.getStyle().setPosition(Position.ABSOLUTE); + scrollPositionElement.getStyle().setDisplay(Display.NONE); + getElement().appendChild(scrollPositionElement); + } + + Style style = scrollPositionElement.getStyle(); + style.setMarginLeft(getElement().getOffsetWidth() / 2 - 80, Unit.PX); + style.setMarginTop(-scrollBodyPanel.getOffsetHeight(), Unit.PX); + + // indexes go from 1-totalRows, as rowheaders in index-mode indicate + int last = (firstRowInViewPort + pageLength); + if (last > totalRows) { + last = totalRows; + } + scrollPositionElement.setInnerHTML("<span>" + (firstRowInViewPort + 1) + + " – " + (last) + "..." + "</span>"); + style.setDisplay(Display.BLOCK); + } + + void hideScrollPositionAnnotation() { + if (scrollPositionElement != null) { + DOM.setStyleAttribute(scrollPositionElement, "display", "none"); + } + } + + boolean isScrollPositionVisible() { + return scrollPositionElement != null + && !scrollPositionElement.getStyle().getDisplay() + .equals(Display.NONE.toString()); + } + + class RowRequestHandler extends Timer { + + private int reqFirstRow = 0; + private int reqRows = 0; + private boolean isRunning = false; + + public void deferRowFetch() { + deferRowFetch(250); + } + + public boolean isRunning() { + return isRunning; + } + + public void deferRowFetch(int msec) { + isRunning = true; + if (reqRows > 0 && reqFirstRow < totalRows) { + schedule(msec); + + // tell scroll position to user if currently "visible" rows are + // not rendered + if (totalRows > pageLength + && ((firstRowInViewPort + pageLength > scrollBody + .getLastRendered()) || (firstRowInViewPort < scrollBody + .getFirstRendered()))) { + announceScrollPosition(); + } else { + hideScrollPositionAnnotation(); + } + } + } + + public void setReqFirstRow(int reqFirstRow) { + if (reqFirstRow < 0) { + reqFirstRow = 0; + } else if (reqFirstRow >= totalRows) { + reqFirstRow = totalRows - 1; + } + this.reqFirstRow = reqFirstRow; + } + + public void setReqRows(int reqRows) { + this.reqRows = reqRows; + } + + @Override + public void run() { + if (client.hasActiveRequest() || navKeyDown) { + // if client connection is busy, don't bother loading it more + VConsole.log("Postponed rowfetch"); + schedule(250); + } else { + + int firstToBeRendered = scrollBody.firstRendered; + if (reqFirstRow < firstToBeRendered) { + firstToBeRendered = reqFirstRow; + } else if (firstRowInViewPort - (int) (cache_rate * pageLength) > firstToBeRendered) { + firstToBeRendered = firstRowInViewPort + - (int) (cache_rate * pageLength); + if (firstToBeRendered < 0) { + firstToBeRendered = 0; + } + } + + int lastToBeRendered = scrollBody.lastRendered; + + if (reqFirstRow + reqRows - 1 > lastToBeRendered) { + lastToBeRendered = reqFirstRow + reqRows - 1; + } else if (firstRowInViewPort + pageLength + pageLength + * cache_rate < lastToBeRendered) { + lastToBeRendered = (firstRowInViewPort + pageLength + (int) (pageLength * cache_rate)); + if (lastToBeRendered >= totalRows) { + lastToBeRendered = totalRows - 1; + } + // due Safari 3.1 bug (see #2607), verify reqrows, original + // problem unknown, but this should catch the issue + if (reqFirstRow + reqRows - 1 > lastToBeRendered) { + reqRows = lastToBeRendered - reqFirstRow; + } + } + + client.updateVariable(paintableId, "firstToBeRendered", + firstToBeRendered, false); + + client.updateVariable(paintableId, "lastToBeRendered", + lastToBeRendered, false); + // remember which firstvisible we requested, in case the server + // has + // a differing opinion + lastRequestedFirstvisible = firstRowInViewPort; + client.updateVariable(paintableId, "firstvisible", + firstRowInViewPort, false); + client.updateVariable(paintableId, "reqfirstrow", reqFirstRow, + false); + client.updateVariable(paintableId, "reqrows", reqRows, true); + + if (selectionChanged) { + unSyncedselectionsBeforeRowFetch = new HashSet<Object>( + selectedRowKeys); + } + isRunning = false; + } + } + + public int getReqFirstRow() { + return reqFirstRow; + } + + /** + * Sends request to refresh content at this position. + */ + public void refreshContent() { + isRunning = true; + int first = (int) (firstRowInViewPort - pageLength * cache_rate); + int reqRows = (int) (2 * pageLength * cache_rate + pageLength); + if (first < 0) { + reqRows = reqRows + first; + first = 0; + } + setReqFirstRow(first); + setReqRows(reqRows); + run(); + } + } + + public class HeaderCell extends Widget { + + Element td = DOM.createTD(); + + Element captionContainer = DOM.createDiv(); + + Element sortIndicator = DOM.createDiv(); + + Element colResizeWidget = DOM.createDiv(); + + Element floatingCopyOfHeaderCell; + + private boolean sortable = false; + private final String cid; + private boolean dragging; + + private int dragStartX; + private int colIndex; + private int originalWidth; + + private boolean isResizing; + + private int headerX; + + private boolean moved; + + private int closestSlot; + + private int width = -1; + + private int naturalWidth = -1; + + private char align = ALIGN_LEFT; + + boolean definedWidth = false; + + private float expandRatio = 0; + + private boolean sorted; + + public void setSortable(boolean b) { + sortable = b; + } + + /** + * Makes room for the sorting indicator in case the column that the + * header cell belongs to is sorted. This is done by resizing the width + * of the caption container element by the correct amount + */ + public void resizeCaptionContainer(int rightSpacing) { + int captionContainerWidth = width + - colResizeWidget.getOffsetWidth() - rightSpacing; + + if (td.getClassName().contains("-asc") + || td.getClassName().contains("-desc")) { + // Leave room for the sort indicator + captionContainerWidth -= sortIndicator.getOffsetWidth(); + } + + if (captionContainerWidth < 0) { + rightSpacing += captionContainerWidth; + captionContainerWidth = 0; + } + + captionContainer.getStyle().setPropertyPx("width", + captionContainerWidth); + + // Apply/Remove spacing if defined + if (rightSpacing > 0) { + colResizeWidget.getStyle().setMarginLeft(rightSpacing, Unit.PX); + } else { + colResizeWidget.getStyle().clearMarginLeft(); + } + } + + public void setNaturalMinimumColumnWidth(int w) { + naturalWidth = w; + } + + public HeaderCell(String colId, String headerText) { + cid = colId; + + DOM.setElementProperty(colResizeWidget, "className", CLASSNAME + + "-resizer"); + + setText(headerText); + + DOM.appendChild(td, colResizeWidget); + + DOM.setElementProperty(sortIndicator, "className", CLASSNAME + + "-sort-indicator"); + DOM.appendChild(td, sortIndicator); + + DOM.setElementProperty(captionContainer, "className", CLASSNAME + + "-caption-container"); + + // ensure no clipping initially (problem on column additions) + DOM.setStyleAttribute(captionContainer, "overflow", "visible"); + + DOM.appendChild(td, captionContainer); + + DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK + | Event.ONCONTEXTMENU | Event.TOUCHEVENTS); + + setElement(td); + + setAlign(ALIGN_LEFT); + } + + public void disableAutoWidthCalculation() { + definedWidth = true; + expandRatio = 0; + } + + public void setWidth(int w, boolean ensureDefinedWidth) { + if (ensureDefinedWidth) { + definedWidth = true; + // on column resize expand ratio becomes zero + expandRatio = 0; + } + if (width == -1) { + // go to default mode, clip content if necessary + DOM.setStyleAttribute(captionContainer, "overflow", ""); + } + width = w; + if (w == -1) { + DOM.setStyleAttribute(captionContainer, "width", ""); + setWidth(""); + } else { + tHead.resizeCaptionContainer(this); + + /* + * if we already have tBody, set the header width properly, if + * not defer it. IE will fail with complex float in table header + * unless TD width is not explicitly set. + */ + if (scrollBody != null) { + int tdWidth = width + scrollBody.getCellExtraWidth(); + setWidth(tdWidth + "px"); + } else { + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + int tdWidth = width + + scrollBody.getCellExtraWidth(); + setWidth(tdWidth + "px"); + } + }); + } + } + } + + public void setUndefinedWidth() { + definedWidth = false; + setWidth(-1, false); + } + + /** + * Detects if width is fixed by developer on server side or resized to + * current width by user. + * + * @return true if defined, false if "natural" width + */ + public boolean isDefinedWidth() { + return definedWidth && width >= 0; + } + + public int getWidth() { + return width; + } + + public void setText(String headerText) { + DOM.setInnerHTML(captionContainer, headerText); + } + + public String getColKey() { + return cid; + } + + private void setSorted(boolean sorted) { + this.sorted = sorted; + if (sorted) { + if (sortAscending) { + this.setStyleName(CLASSNAME + "-header-cell-asc"); + } else { + this.setStyleName(CLASSNAME + "-header-cell-desc"); + } + } else { + this.setStyleName(CLASSNAME + "-header-cell"); + } + } + + /** + * Handle column reordering. + */ + + @Override + public void onBrowserEvent(Event event) { + if (enabled && event != null) { + if (isResizing + || event.getEventTarget().cast() == colResizeWidget) { + if (dragging + && (event.getTypeInt() == Event.ONMOUSEUP || event + .getTypeInt() == Event.ONTOUCHEND)) { + // Handle releasing column header on spacer #5318 + handleCaptionEvent(event); + } else { + onResizeEvent(event); + } + } else { + /* + * Ensure focus before handling caption event. Otherwise + * variables changed from caption event may be before + * variables from other components that fire variables when + * they lose focus. + */ + if (event.getTypeInt() == Event.ONMOUSEDOWN + || event.getTypeInt() == Event.ONTOUCHSTART) { + scrollBodyPanel.setFocus(true); + } + handleCaptionEvent(event); + boolean stopPropagation = true; + if (event.getTypeInt() == Event.ONCONTEXTMENU + && !client.hasEventListeners(VScrollTable.this, + TableConstants.HEADER_CLICK_EVENT_ID)) { + // Prevent showing the browser's context menu only when + // there is a header click listener. + stopPropagation = false; + } + if (stopPropagation) { + event.stopPropagation(); + event.preventDefault(); + } + } + } + } + + private void createFloatingCopy() { + floatingCopyOfHeaderCell = DOM.createDiv(); + DOM.setInnerHTML(floatingCopyOfHeaderCell, DOM.getInnerHTML(td)); + floatingCopyOfHeaderCell = DOM + .getChild(floatingCopyOfHeaderCell, 2); + DOM.setElementProperty(floatingCopyOfHeaderCell, "className", + CLASSNAME + "-header-drag"); + // otherwise might wrap or be cut if narrow column + DOM.setStyleAttribute(floatingCopyOfHeaderCell, "width", "auto"); + updateFloatingCopysPosition(DOM.getAbsoluteLeft(td), + DOM.getAbsoluteTop(td)); + DOM.appendChild(RootPanel.get().getElement(), + floatingCopyOfHeaderCell); + } + + private void updateFloatingCopysPosition(int x, int y) { + x -= DOM.getElementPropertyInt(floatingCopyOfHeaderCell, + "offsetWidth") / 2; + DOM.setStyleAttribute(floatingCopyOfHeaderCell, "left", x + "px"); + if (y > 0) { + DOM.setStyleAttribute(floatingCopyOfHeaderCell, "top", (y + 7) + + "px"); + } + } + + private void hideFloatingCopy() { + DOM.removeChild(RootPanel.get().getElement(), + floatingCopyOfHeaderCell); + floatingCopyOfHeaderCell = null; + } + + /** + * Fires a header click event after the user has clicked a column header + * cell + * + * @param event + * The click event + */ + private void fireHeaderClickedEvent(Event event) { + if (client.hasEventListeners(VScrollTable.this, + TableConstants.HEADER_CLICK_EVENT_ID)) { + MouseEventDetails details = MouseEventDetailsBuilder + .buildMouseEventDetails(event); + client.updateVariable(paintableId, "headerClickEvent", + details.toString(), false); + client.updateVariable(paintableId, "headerClickCID", cid, true); + } + } + + protected void handleCaptionEvent(Event event) { + switch (DOM.eventGetType(event)) { + case Event.ONTOUCHSTART: + case Event.ONMOUSEDOWN: + if (columnReordering + && Util.isTouchEventOrLeftMouseButton(event)) { + if (event.getTypeInt() == Event.ONTOUCHSTART) { + /* + * prevent using this event in e.g. scrolling + */ + event.stopPropagation(); + } + dragging = true; + moved = false; + colIndex = getColIndexByKey(cid); + DOM.setCapture(getElement()); + headerX = tHead.getAbsoluteLeft(); + event.preventDefault(); // prevent selecting text && + // generated touch events + } + break; + case Event.ONMOUSEUP: + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + if (columnReordering + && Util.isTouchEventOrLeftMouseButton(event)) { + dragging = false; + DOM.releaseCapture(getElement()); + if (moved) { + hideFloatingCopy(); + tHead.removeSlotFocus(); + if (closestSlot != colIndex + && closestSlot != (colIndex + 1)) { + if (closestSlot > colIndex) { + reOrderColumn(cid, closestSlot - 1); + } else { + reOrderColumn(cid, closestSlot); + } + } + } + if (Util.isTouchEvent(event)) { + /* + * Prevent using in e.g. scrolling and prevent generated + * events. + */ + event.preventDefault(); + event.stopPropagation(); + } + } + + if (!moved) { + // mouse event was a click to header -> sort column + if (sortable && Util.isTouchEventOrLeftMouseButton(event)) { + if (sortColumn.equals(cid)) { + // just toggle order + client.updateVariable(paintableId, "sortascending", + !sortAscending, false); + } else { + // set table sorted by this column + client.updateVariable(paintableId, "sortcolumn", + cid, false); + } + // get also cache columns at the same request + scrollBodyPanel.setScrollPosition(0); + firstvisible = 0; + rowRequestHandler.setReqFirstRow(0); + rowRequestHandler.setReqRows((int) (2 * pageLength + * cache_rate + pageLength)); + rowRequestHandler.deferRowFetch(); // some validation + + // defer 250ms + rowRequestHandler.cancel(); // instead of waiting + rowRequestHandler.run(); // run immediately + } + fireHeaderClickedEvent(event); + if (Util.isTouchEvent(event)) { + /* + * Prevent using in e.g. scrolling and prevent generated + * events. + */ + event.preventDefault(); + event.stopPropagation(); + } + break; + } + break; + case Event.ONDBLCLICK: + fireHeaderClickedEvent(event); + break; + case Event.ONTOUCHMOVE: + case Event.ONMOUSEMOVE: + if (dragging && Util.isTouchEventOrLeftMouseButton(event)) { + if (event.getTypeInt() == Event.ONTOUCHMOVE) { + /* + * prevent using this event in e.g. scrolling + */ + event.stopPropagation(); + } + if (!moved) { + createFloatingCopy(); + moved = true; + } + + final int clientX = Util.getTouchOrMouseClientX(event); + final int x = clientX + tHead.hTableWrapper.getScrollLeft(); + int slotX = headerX; + closestSlot = colIndex; + int closestDistance = -1; + int start = 0; + if (showRowHeaders) { + start++; + } + final int visibleCellCount = tHead.getVisibleCellCount(); + for (int i = start; i <= visibleCellCount; i++) { + if (i > 0) { + final String colKey = getColKeyByIndex(i - 1); + slotX += getColWidth(colKey); + } + final int dist = Math.abs(x - slotX); + if (closestDistance == -1 || dist < closestDistance) { + closestDistance = dist; + closestSlot = i; + } + } + tHead.focusSlot(closestSlot); + + updateFloatingCopysPosition(clientX, -1); + } + break; + default: + break; + } + } + + private void onResizeEvent(Event event) { + switch (DOM.eventGetType(event)) { + case Event.ONMOUSEDOWN: + if (!Util.isTouchEventOrLeftMouseButton(event)) { + return; + } + isResizing = true; + DOM.setCapture(getElement()); + dragStartX = DOM.eventGetClientX(event); + colIndex = getColIndexByKey(cid); + originalWidth = getWidth(); + DOM.eventPreventDefault(event); + break; + case Event.ONMOUSEUP: + if (!Util.isTouchEventOrLeftMouseButton(event)) { + return; + } + isResizing = false; + DOM.releaseCapture(getElement()); + tHead.disableAutoColumnWidthCalculation(this); + + // Ensure last header cell is taking into account possible + // column selector + HeaderCell lastCell = tHead.getHeaderCell(tHead + .getVisibleCellCount() - 1); + tHead.resizeCaptionContainer(lastCell); + triggerLazyColumnAdjustment(true); + + fireColumnResizeEvent(cid, originalWidth, getColWidth(cid)); + break; + case Event.ONMOUSEMOVE: + if (!Util.isTouchEventOrLeftMouseButton(event)) { + return; + } + if (isResizing) { + final int deltaX = DOM.eventGetClientX(event) - dragStartX; + if (deltaX == 0) { + return; + } + tHead.disableAutoColumnWidthCalculation(this); + + int newWidth = originalWidth + deltaX; + if (newWidth < getMinWidth()) { + newWidth = getMinWidth(); + } + setColWidth(colIndex, newWidth, true); + triggerLazyColumnAdjustment(false); + forceRealignColumnHeaders(); + } + break; + default: + break; + } + } + + public int getMinWidth() { + int cellExtraWidth = 0; + if (scrollBody != null) { + cellExtraWidth += scrollBody.getCellExtraWidth(); + } + return cellExtraWidth + sortIndicator.getOffsetWidth(); + } + + public String getCaption() { + return DOM.getInnerText(captionContainer); + } + + public boolean isEnabled() { + return getParent() != null; + } + + public void setAlign(char c) { + final String ALIGN_PREFIX = CLASSNAME + "-caption-container-align-"; + if (align != c) { + captionContainer.removeClassName(ALIGN_PREFIX + "center"); + captionContainer.removeClassName(ALIGN_PREFIX + "right"); + captionContainer.removeClassName(ALIGN_PREFIX + "left"); + switch (c) { + case ALIGN_CENTER: + captionContainer.addClassName(ALIGN_PREFIX + "center"); + break; + case ALIGN_RIGHT: + captionContainer.addClassName(ALIGN_PREFIX + "right"); + break; + default: + captionContainer.addClassName(ALIGN_PREFIX + "left"); + break; + } + } + align = c; + } + + public char getAlign() { + return align; + } + + /** + * Detects the natural minimum width for the column of this header cell. + * If column is resized by user or the width is defined by server the + * actual width is returned. Else the natural min width is returned. + * + * @param columnIndex + * column index hint, if -1 (unknown) it will be detected + * + * @return + */ + public int getNaturalColumnWidth(int columnIndex) { + if (isDefinedWidth()) { + return width; + } else { + if (naturalWidth < 0) { + // This is recently revealed column. Try to detect a proper + // value (greater of header and data + // cols) + + int hw = captionContainer.getOffsetWidth() + + scrollBody.getCellExtraWidth(); + if (BrowserInfo.get().isGecko()) { + hw += sortIndicator.getOffsetWidth(); + } + if (columnIndex < 0) { + columnIndex = 0; + for (Iterator<Widget> it = tHead.iterator(); it + .hasNext(); columnIndex++) { + if (it.next() == this) { + break; + } + } + } + final int cw = scrollBody.getColWidth(columnIndex); + naturalWidth = (hw > cw ? hw : cw); + } + return naturalWidth; + } + } + + public void setExpandRatio(float floatAttribute) { + if (floatAttribute != expandRatio) { + triggerLazyColumnAdjustment(false); + } + expandRatio = floatAttribute; + } + + public float getExpandRatio() { + return expandRatio; + } + + public boolean isSorted() { + return sorted; + } + } + + /** + * HeaderCell that is header cell for row headers. + * + * Reordering disabled and clicking on it resets sorting. + */ + public class RowHeadersHeaderCell extends HeaderCell { + + RowHeadersHeaderCell() { + super(ROW_HEADER_COLUMN_KEY, ""); + this.setStyleName(CLASSNAME + "-header-cell-rowheader"); + } + + @Override + protected void handleCaptionEvent(Event event) { + // NOP: RowHeaders cannot be reordered + // TODO It'd be nice to reset sorting here + } + } + + public class TableHead extends Panel implements ActionOwner { + + private static final int WRAPPER_WIDTH = 900000; + + ArrayList<Widget> visibleCells = new ArrayList<Widget>(); + + HashMap<String, HeaderCell> availableCells = new HashMap<String, HeaderCell>(); + + Element div = DOM.createDiv(); + Element hTableWrapper = DOM.createDiv(); + Element hTableContainer = DOM.createDiv(); + Element table = DOM.createTable(); + Element headerTableBody = DOM.createTBody(); + Element tr = DOM.createTR(); + + private final Element columnSelector = DOM.createDiv(); + + private int focusedSlot = -1; + + public TableHead() { + if (BrowserInfo.get().isIE()) { + table.setPropertyInt("cellSpacing", 0); + } + + DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden"); + DOM.setElementProperty(hTableWrapper, "className", CLASSNAME + + "-header"); + + // TODO move styles to CSS + DOM.setElementProperty(columnSelector, "className", CLASSNAME + + "-column-selector"); + DOM.setStyleAttribute(columnSelector, "display", "none"); + + DOM.appendChild(table, headerTableBody); + DOM.appendChild(headerTableBody, tr); + DOM.appendChild(hTableContainer, table); + DOM.appendChild(hTableWrapper, hTableContainer); + DOM.appendChild(div, hTableWrapper); + DOM.appendChild(div, columnSelector); + setElement(div); + + setStyleName(CLASSNAME + "-header-wrap"); + + DOM.sinkEvents(columnSelector, Event.ONCLICK); + + availableCells.put(ROW_HEADER_COLUMN_KEY, + new RowHeadersHeaderCell()); + } + + public void resizeCaptionContainer(HeaderCell cell) { + HeaderCell lastcell = getHeaderCell(visibleCells.size() - 1); + + // Measure column widths + int columnTotalWidth = 0; + for (Widget w : visibleCells) { + columnTotalWidth += w.getOffsetWidth(); + } + + if (cell == lastcell + && columnSelector.getOffsetWidth() > 0 + && columnTotalWidth >= div.getOffsetWidth() + - columnSelector.getOffsetWidth() + && !hasVerticalScrollbar()) { + // Ensure column caption is visible when placed under the column + // selector widget by shifting and resizing the caption. + int offset = 0; + int diff = div.getOffsetWidth() - columnTotalWidth; + if (diff < columnSelector.getOffsetWidth() && diff > 0) { + // If the difference is less than the column selectors width + // then just offset by the + // difference + offset = columnSelector.getOffsetWidth() - diff; + } else { + // Else offset by the whole column selector + offset = columnSelector.getOffsetWidth(); + } + lastcell.resizeCaptionContainer(offset); + } else { + cell.resizeCaptionContainer(0); + } + } + + @Override + public void clear() { + for (String cid : availableCells.keySet()) { + removeCell(cid); + } + availableCells.clear(); + availableCells.put(ROW_HEADER_COLUMN_KEY, + new RowHeadersHeaderCell()); + } + + public void updateCellsFromUIDL(UIDL uidl) { + Iterator<?> it = uidl.getChildIterator(); + HashSet<String> updated = new HashSet<String>(); + boolean refreshContentWidths = false; + while (it.hasNext()) { + final UIDL col = (UIDL) it.next(); + final String cid = col.getStringAttribute("cid"); + updated.add(cid); + + String caption = buildCaptionHtmlSnippet(col); + HeaderCell c = getHeaderCell(cid); + if (c == null) { + c = new HeaderCell(cid, caption); + availableCells.put(cid, c); + if (initializedAndAttached) { + // we will need a column width recalculation + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + } else { + c.setText(caption); + } + + if (col.hasAttribute("sortable")) { + c.setSortable(true); + if (cid.equals(sortColumn)) { + c.setSorted(true); + } else { + c.setSorted(false); + } + } else { + c.setSortable(false); + } + + if (col.hasAttribute("align")) { + c.setAlign(col.getStringAttribute("align").charAt(0)); + } else { + c.setAlign(ALIGN_LEFT); + + } + if (col.hasAttribute("width")) { + final String widthStr = col.getStringAttribute("width"); + // Make sure to accomodate for the sort indicator if + // necessary. + int width = Integer.parseInt(widthStr); + if (width < c.getMinWidth()) { + width = c.getMinWidth(); + } + if (width != c.getWidth() && scrollBody != null) { + // Do a more thorough update if a column is resized from + // the server *after* the header has been properly + // initialized + final int colIx = getColIndexByKey(c.cid); + final int newWidth = width; + Scheduler.get().scheduleDeferred( + new ScheduledCommand() { + + @Override + public void execute() { + setColWidth(colIx, newWidth, true); + } + }); + refreshContentWidths = true; + } else { + c.setWidth(width, true); + } + } else if (recalcWidths) { + c.setUndefinedWidth(); + } + if (col.hasAttribute("er")) { + c.setExpandRatio(col.getFloatAttribute("er")); + } + if (col.hasAttribute("collapsed")) { + // ensure header is properly removed from parent (case when + // collapsing happens via servers side api) + if (c.isAttached()) { + c.removeFromParent(); + headerChangedDuringUpdate = true; + } + } + } + + if (refreshContentWidths) { + // Recalculate the column sizings if any column has changed + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + + @Override + public void execute() { + triggerLazyColumnAdjustment(true); + } + }); + } + + // check for orphaned header cells + for (Iterator<String> cit = availableCells.keySet().iterator(); cit + .hasNext();) { + String cid = cit.next(); + if (!updated.contains(cid)) { + removeCell(cid); + cit.remove(); + // we will need a column width recalculation, since columns + // with expand ratios should expand to fill the void. + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + } + } + + public void enableColumn(String cid, int index) { + final HeaderCell c = getHeaderCell(cid); + if (!c.isEnabled() || getHeaderCell(index) != c) { + setHeaderCell(index, c); + if (initializedAndAttached) { + headerChangedDuringUpdate = true; + } + } + } + + public int getVisibleCellCount() { + return visibleCells.size(); + } + + public void setHorizontalScrollPosition(int scrollLeft) { + hTableWrapper.setScrollLeft(scrollLeft); + } + + public void setColumnCollapsingAllowed(boolean cc) { + if (cc) { + columnSelector.getStyle().setDisplay(Display.BLOCK); + } else { + columnSelector.getStyle().setDisplay(Display.NONE); + } + } + + public void disableBrowserIntelligence() { + hTableContainer.getStyle().setWidth(WRAPPER_WIDTH, Unit.PX); + } + + public void enableBrowserIntelligence() { + hTableContainer.getStyle().clearWidth(); + } + + public void setHeaderCell(int index, HeaderCell cell) { + if (cell.isEnabled()) { + // we're moving the cell + DOM.removeChild(tr, cell.getElement()); + orphan(cell); + visibleCells.remove(cell); + } + if (index < visibleCells.size()) { + // insert to right slot + DOM.insertChild(tr, cell.getElement(), index); + adopt(cell); + visibleCells.add(index, cell); + } else if (index == visibleCells.size()) { + // simply append + DOM.appendChild(tr, cell.getElement()); + adopt(cell); + visibleCells.add(cell); + } else { + throw new RuntimeException( + "Header cells must be appended in order"); + } + } + + public HeaderCell getHeaderCell(int index) { + if (index >= 0 && index < visibleCells.size()) { + return (HeaderCell) visibleCells.get(index); + } else { + return null; + } + } + + /** + * Get's HeaderCell by it's column Key. + * + * Note that this returns HeaderCell even if it is currently collapsed. + * + * @param cid + * Column key of accessed HeaderCell + * @return HeaderCell + */ + public HeaderCell getHeaderCell(String cid) { + return availableCells.get(cid); + } + + public void moveCell(int oldIndex, int newIndex) { + final HeaderCell hCell = getHeaderCell(oldIndex); + final Element cell = hCell.getElement(); + + visibleCells.remove(oldIndex); + DOM.removeChild(tr, cell); + + DOM.insertChild(tr, cell, newIndex); + visibleCells.add(newIndex, hCell); + } + + @Override + public Iterator<Widget> iterator() { + return visibleCells.iterator(); + } + + @Override + public boolean remove(Widget w) { + if (visibleCells.contains(w)) { + visibleCells.remove(w); + orphan(w); + DOM.removeChild(DOM.getParent(w.getElement()), w.getElement()); + return true; + } + return false; + } + + public void removeCell(String colKey) { + final HeaderCell c = getHeaderCell(colKey); + remove(c); + } + + private void focusSlot(int index) { + removeSlotFocus(); + if (index > 0) { + DOM.setElementProperty( + DOM.getFirstChild(DOM.getChild(tr, index - 1)), + "className", CLASSNAME + "-resizer " + CLASSNAME + + "-focus-slot-right"); + } else { + DOM.setElementProperty( + DOM.getFirstChild(DOM.getChild(tr, index)), + "className", CLASSNAME + "-resizer " + CLASSNAME + + "-focus-slot-left"); + } + focusedSlot = index; + } + + private void removeSlotFocus() { + if (focusedSlot < 0) { + return; + } + if (focusedSlot == 0) { + DOM.setElementProperty( + DOM.getFirstChild(DOM.getChild(tr, focusedSlot)), + "className", CLASSNAME + "-resizer"); + } else if (focusedSlot > 0) { + DOM.setElementProperty( + DOM.getFirstChild(DOM.getChild(tr, focusedSlot - 1)), + "className", CLASSNAME + "-resizer"); + } + focusedSlot = -1; + } + + @Override + public void onBrowserEvent(Event event) { + if (enabled) { + if (event.getEventTarget().cast() == columnSelector) { + final int left = DOM.getAbsoluteLeft(columnSelector); + final int top = DOM.getAbsoluteTop(columnSelector) + + DOM.getElementPropertyInt(columnSelector, + "offsetHeight"); + client.getContextMenu().showAt(this, left, top); + } + } + } + + @Override + protected void onDetach() { + super.onDetach(); + if (client != null) { + client.getContextMenu().ensureHidden(this); + } + } + + class VisibleColumnAction extends Action { + + String colKey; + private boolean collapsed; + private boolean noncollapsible = false; + private VScrollTableRow currentlyFocusedRow; + + public VisibleColumnAction(String colKey) { + super(VScrollTable.TableHead.this); + this.colKey = colKey; + caption = tHead.getHeaderCell(colKey).getCaption(); + currentlyFocusedRow = focusedRow; + } + + @Override + public void execute() { + if (noncollapsible) { + return; + } + client.getContextMenu().hide(); + // toggle selected column + if (collapsedColumns.contains(colKey)) { + collapsedColumns.remove(colKey); + } else { + tHead.removeCell(colKey); + collapsedColumns.add(colKey); + triggerLazyColumnAdjustment(true); + } + + // update variable to server + client.updateVariable(paintableId, "collapsedcolumns", + collapsedColumns.toArray(new String[collapsedColumns + .size()]), false); + // let rowRequestHandler determine proper rows + rowRequestHandler.refreshContent(); + lazyRevertFocusToRow(currentlyFocusedRow); + } + + public void setCollapsed(boolean b) { + collapsed = b; + } + + public void setNoncollapsible(boolean b) { + noncollapsible = b; + } + + /** + * Override default method to distinguish on/off columns + */ + + @Override + public String getHTML() { + final StringBuffer buf = new StringBuffer(); + buf.append("<span class=\""); + if (collapsed) { + buf.append("v-off"); + } else { + buf.append("v-on"); + } + if (noncollapsible) { + buf.append(" v-disabled"); + } + buf.append("\">"); + + buf.append(super.getHTML()); + buf.append("</span>"); + + return buf.toString(); + } + + } + + /* + * Returns columns as Action array for column select popup + */ + + @Override + public Action[] getActions() { + Object[] cols; + if (columnReordering && columnOrder != null) { + cols = columnOrder; + } else { + // if columnReordering is disabled, we need different way to get + // all available columns + cols = visibleColOrder; + cols = new Object[visibleColOrder.length + + collapsedColumns.size()]; + int i; + for (i = 0; i < visibleColOrder.length; i++) { + cols[i] = visibleColOrder[i]; + } + for (final Iterator<String> it = collapsedColumns.iterator(); it + .hasNext();) { + cols[i++] = it.next(); + } + } + final Action[] actions = new Action[cols.length]; + + for (int i = 0; i < cols.length; i++) { + final String cid = (String) cols[i]; + final HeaderCell c = getHeaderCell(cid); + final VisibleColumnAction a = new VisibleColumnAction( + c.getColKey()); + a.setCaption(c.getCaption()); + if (!c.isEnabled()) { + a.setCollapsed(true); + } + if (noncollapsibleColumns.contains(cid)) { + a.setNoncollapsible(true); + } + actions[i] = a; + } + return actions; + } + + @Override + public ApplicationConnection getClient() { + return client; + } + + @Override + public String getPaintableId() { + return paintableId; + } + + /** + * Returns column alignments for visible columns + */ + public char[] getColumnAlignments() { + final Iterator<Widget> it = visibleCells.iterator(); + final char[] aligns = new char[visibleCells.size()]; + int colIndex = 0; + while (it.hasNext()) { + aligns[colIndex++] = ((HeaderCell) it.next()).getAlign(); + } + return aligns; + } + + /** + * Disables the automatic calculation of all column widths by forcing + * the widths to be "defined" thus turning off expand ratios and such. + */ + public void disableAutoColumnWidthCalculation(HeaderCell source) { + for (HeaderCell cell : availableCells.values()) { + cell.disableAutoWidthCalculation(); + } + // fire column resize events for all columns but the source of the + // resize action, since an event will fire separately for this. + ArrayList<HeaderCell> columns = new ArrayList<HeaderCell>( + availableCells.values()); + columns.remove(source); + sendColumnWidthUpdates(columns); + forceRealignColumnHeaders(); + } + } + + /** + * A cell in the footer + */ + public class FooterCell extends Widget { + private final Element td = DOM.createTD(); + private final Element captionContainer = DOM.createDiv(); + private char align = ALIGN_LEFT; + private int width = -1; + private float expandRatio = 0; + private final String cid; + boolean definedWidth = false; + private int naturalWidth = -1; + + public FooterCell(String colId, String headerText) { + cid = colId; + + setText(headerText); + + DOM.setElementProperty(captionContainer, "className", CLASSNAME + + "-footer-container"); + + // ensure no clipping initially (problem on column additions) + DOM.setStyleAttribute(captionContainer, "overflow", "visible"); + + DOM.sinkEvents(captionContainer, Event.MOUSEEVENTS); + + DOM.appendChild(td, captionContainer); + + DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK + | Event.ONCONTEXTMENU); + + setElement(td); + } + + /** + * Sets the text of the footer + * + * @param footerText + * The text in the footer + */ + public void setText(String footerText) { + if (footerText == null || footerText.equals("")) { + footerText = " "; + } + + DOM.setInnerHTML(captionContainer, footerText); + } + + /** + * Set alignment of the text in the cell + * + * @param c + * The alignment which can be ALIGN_CENTER, ALIGN_LEFT, + * ALIGN_RIGHT + */ + public void setAlign(char c) { + if (align != c) { + switch (c) { + case ALIGN_CENTER: + DOM.setStyleAttribute(captionContainer, "textAlign", + "center"); + break; + case ALIGN_RIGHT: + DOM.setStyleAttribute(captionContainer, "textAlign", + "right"); + break; + default: + DOM.setStyleAttribute(captionContainer, "textAlign", ""); + break; + } + } + align = c; + } + + /** + * Get the alignment of the text int the cell + * + * @return Returns either ALIGN_CENTER, ALIGN_LEFT or ALIGN_RIGHT + */ + public char getAlign() { + return align; + } + + /** + * Sets the width of the cell + * + * @param w + * The width of the cell + * @param ensureDefinedWidth + * Ensures the the given width is not recalculated + */ + public void setWidth(int w, boolean ensureDefinedWidth) { + + if (ensureDefinedWidth) { + definedWidth = true; + // on column resize expand ratio becomes zero + expandRatio = 0; + } + if (width == w) { + return; + } + if (width == -1) { + // go to default mode, clip content if necessary + DOM.setStyleAttribute(captionContainer, "overflow", ""); + } + width = w; + if (w == -1) { + DOM.setStyleAttribute(captionContainer, "width", ""); + setWidth(""); + } else { + /* + * Reduce width with one pixel for the right border since the + * footers does not have any spacers between them. + */ + final int borderWidths = 1; + + // Set the container width (check for negative value) + captionContainer.getStyle().setPropertyPx("width", + Math.max(w - borderWidths, 0)); + + /* + * if we already have tBody, set the header width properly, if + * not defer it. IE will fail with complex float in table header + * unless TD width is not explicitly set. + */ + if (scrollBody != null) { + int tdWidth = width + scrollBody.getCellExtraWidth() + - borderWidths; + setWidth(Math.max(tdWidth, 0) + "px"); + } else { + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + int tdWidth = width + + scrollBody.getCellExtraWidth() + - borderWidths; + setWidth(Math.max(tdWidth, 0) + "px"); + } + }); + } + } + } + + /** + * Sets the width to undefined + */ + public void setUndefinedWidth() { + setWidth(-1, false); + } + + /** + * Detects if width is fixed by developer on server side or resized to + * current width by user. + * + * @return true if defined, false if "natural" width + */ + public boolean isDefinedWidth() { + return definedWidth && width >= 0; + } + + /** + * Returns the pixels width of the footer cell + * + * @return The width in pixels + */ + public int getWidth() { + return width; + } + + /** + * Sets the expand ratio of the cell + * + * @param floatAttribute + * The expand ratio + */ + public void setExpandRatio(float floatAttribute) { + expandRatio = floatAttribute; + } + + /** + * Returns the expand ration of the cell + * + * @return The expand ratio + */ + public float getExpandRatio() { + return expandRatio; + } + + /** + * Is the cell enabled? + * + * @return True if enabled else False + */ + public boolean isEnabled() { + return getParent() != null; + } + + /** + * Handle column clicking + */ + + @Override + public void onBrowserEvent(Event event) { + if (enabled && event != null) { + handleCaptionEvent(event); + + if (DOM.eventGetType(event) == Event.ONMOUSEUP) { + scrollBodyPanel.setFocus(true); + } + boolean stopPropagation = true; + if (event.getTypeInt() == Event.ONCONTEXTMENU + && !client.hasEventListeners(VScrollTable.this, + TableConstants.FOOTER_CLICK_EVENT_ID)) { + // Show browser context menu if a footer click listener is + // not present + stopPropagation = false; + } + if (stopPropagation) { + event.stopPropagation(); + event.preventDefault(); + } + } + } + + /** + * Handles a event on the captions + * + * @param event + * The event to handle + */ + protected void handleCaptionEvent(Event event) { + if (event.getTypeInt() == Event.ONMOUSEUP + || event.getTypeInt() == Event.ONDBLCLICK) { + fireFooterClickedEvent(event); + } + } + + /** + * Fires a footer click event after the user has clicked a column footer + * cell + * + * @param event + * The click event + */ + private void fireFooterClickedEvent(Event event) { + if (client.hasEventListeners(VScrollTable.this, + TableConstants.FOOTER_CLICK_EVENT_ID)) { + MouseEventDetails details = MouseEventDetailsBuilder + .buildMouseEventDetails(event); + client.updateVariable(paintableId, "footerClickEvent", + details.toString(), false); + client.updateVariable(paintableId, "footerClickCID", cid, true); + } + } + + /** + * Returns the column key of the column + * + * @return The column key + */ + public String getColKey() { + return cid; + } + + /** + * Detects the natural minimum width for the column of this header cell. + * If column is resized by user or the width is defined by server the + * actual width is returned. Else the natural min width is returned. + * + * @param columnIndex + * column index hint, if -1 (unknown) it will be detected + * + * @return + */ + public int getNaturalColumnWidth(int columnIndex) { + if (isDefinedWidth()) { + return width; + } else { + if (naturalWidth < 0) { + // This is recently revealed column. Try to detect a proper + // value (greater of header and data + // cols) + + final int hw = ((Element) getElement().getLastChild()) + .getOffsetWidth() + scrollBody.getCellExtraWidth(); + if (columnIndex < 0) { + columnIndex = 0; + for (Iterator<Widget> it = tHead.iterator(); it + .hasNext(); columnIndex++) { + if (it.next() == this) { + break; + } + } + } + final int cw = scrollBody.getColWidth(columnIndex); + naturalWidth = (hw > cw ? hw : cw); + } + return naturalWidth; + } + } + + public void setNaturalMinimumColumnWidth(int w) { + naturalWidth = w; + } + } + + /** + * HeaderCell that is header cell for row headers. + * + * Reordering disabled and clicking on it resets sorting. + */ + public class RowHeadersFooterCell extends FooterCell { + + RowHeadersFooterCell() { + super(ROW_HEADER_COLUMN_KEY, ""); + } + + @Override + protected void handleCaptionEvent(Event event) { + // NOP: RowHeaders cannot be reordered + // TODO It'd be nice to reset sorting here + } + } + + /** + * The footer of the table which can be seen in the bottom of the Table. + */ + public class TableFooter extends Panel { + + private static final int WRAPPER_WIDTH = 900000; + + ArrayList<Widget> visibleCells = new ArrayList<Widget>(); + HashMap<String, FooterCell> availableCells = new HashMap<String, FooterCell>(); + + Element div = DOM.createDiv(); + Element hTableWrapper = DOM.createDiv(); + Element hTableContainer = DOM.createDiv(); + Element table = DOM.createTable(); + Element headerTableBody = DOM.createTBody(); + Element tr = DOM.createTR(); + + public TableFooter() { + + DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden"); + DOM.setElementProperty(hTableWrapper, "className", CLASSNAME + + "-footer"); + + DOM.appendChild(table, headerTableBody); + DOM.appendChild(headerTableBody, tr); + DOM.appendChild(hTableContainer, table); + DOM.appendChild(hTableWrapper, hTableContainer); + DOM.appendChild(div, hTableWrapper); + setElement(div); + + setStyleName(CLASSNAME + "-footer-wrap"); + + availableCells.put(ROW_HEADER_COLUMN_KEY, + new RowHeadersFooterCell()); + } + + @Override + public void clear() { + for (String cid : availableCells.keySet()) { + removeCell(cid); + } + availableCells.clear(); + availableCells.put(ROW_HEADER_COLUMN_KEY, + new RowHeadersFooterCell()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Panel#remove(com.google.gwt.user.client + * .ui.Widget) + */ + + @Override + public boolean remove(Widget w) { + if (visibleCells.contains(w)) { + visibleCells.remove(w); + orphan(w); + DOM.removeChild(DOM.getParent(w.getElement()), w.getElement()); + return true; + } + return false; + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.HasWidgets#iterator() + */ + + @Override + public Iterator<Widget> iterator() { + return visibleCells.iterator(); + } + + /** + * Gets a footer cell which represents the given columnId + * + * @param cid + * The columnId + * + * @return The cell + */ + public FooterCell getFooterCell(String cid) { + return availableCells.get(cid); + } + + /** + * Gets a footer cell by using a column index + * + * @param index + * The index of the column + * @return The Cell + */ + public FooterCell getFooterCell(int index) { + if (index < visibleCells.size()) { + return (FooterCell) visibleCells.get(index); + } else { + return null; + } + } + + /** + * Updates the cells contents when updateUIDL request is received + * + * @param uidl + * The UIDL + */ + public void updateCellsFromUIDL(UIDL uidl) { + Iterator<?> columnIterator = uidl.getChildIterator(); + HashSet<String> updated = new HashSet<String>(); + while (columnIterator.hasNext()) { + final UIDL col = (UIDL) columnIterator.next(); + final String cid = col.getStringAttribute("cid"); + updated.add(cid); + + String caption = col.hasAttribute("fcaption") ? col + .getStringAttribute("fcaption") : ""; + FooterCell c = getFooterCell(cid); + if (c == null) { + c = new FooterCell(cid, caption); + availableCells.put(cid, c); + if (initializedAndAttached) { + // we will need a column width recalculation + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + } else { + c.setText(caption); + } + + if (col.hasAttribute("align")) { + c.setAlign(col.getStringAttribute("align").charAt(0)); + } else { + c.setAlign(ALIGN_LEFT); + + } + if (col.hasAttribute("width")) { + if (scrollBody == null) { + // Already updated by setColWidth called from + // TableHeads.updateCellsFromUIDL in case of a server + // side resize + final String width = col.getStringAttribute("width"); + c.setWidth(Integer.parseInt(width), true); + } + } else if (recalcWidths) { + c.setUndefinedWidth(); + } + if (col.hasAttribute("er")) { + c.setExpandRatio(col.getFloatAttribute("er")); + } + if (col.hasAttribute("collapsed")) { + // ensure header is properly removed from parent (case when + // collapsing happens via servers side api) + if (c.isAttached()) { + c.removeFromParent(); + headerChangedDuringUpdate = true; + } + } + } + + // check for orphaned header cells + for (Iterator<String> cit = availableCells.keySet().iterator(); cit + .hasNext();) { + String cid = cit.next(); + if (!updated.contains(cid)) { + removeCell(cid); + cit.remove(); + } + } + } + + /** + * Set a footer cell for a specified column index + * + * @param index + * The index + * @param cell + * The footer cell + */ + public void setFooterCell(int index, FooterCell cell) { + if (cell.isEnabled()) { + // we're moving the cell + DOM.removeChild(tr, cell.getElement()); + orphan(cell); + visibleCells.remove(cell); + } + if (index < visibleCells.size()) { + // insert to right slot + DOM.insertChild(tr, cell.getElement(), index); + adopt(cell); + visibleCells.add(index, cell); + } else if (index == visibleCells.size()) { + // simply append + DOM.appendChild(tr, cell.getElement()); + adopt(cell); + visibleCells.add(cell); + } else { + throw new RuntimeException( + "Header cells must be appended in order"); + } + } + + /** + * Remove a cell by using the columnId + * + * @param colKey + * The columnId to remove + */ + public void removeCell(String colKey) { + final FooterCell c = getFooterCell(colKey); + remove(c); + } + + /** + * Enable a column (Sets the footer cell) + * + * @param cid + * The columnId + * @param index + * The index of the column + */ + public void enableColumn(String cid, int index) { + final FooterCell c = getFooterCell(cid); + if (!c.isEnabled() || getFooterCell(index) != c) { + setFooterCell(index, c); + if (initializedAndAttached) { + headerChangedDuringUpdate = true; + } + } + } + + /** + * Disable browser measurement of the table width + */ + public void disableBrowserIntelligence() { + DOM.setStyleAttribute(hTableContainer, "width", WRAPPER_WIDTH + + "px"); + } + + /** + * Enable browser measurement of the table width + */ + public void enableBrowserIntelligence() { + DOM.setStyleAttribute(hTableContainer, "width", ""); + } + + /** + * Set the horizontal position in the cell in the footer. This is done + * when a horizontal scrollbar is present. + * + * @param scrollLeft + * The value of the leftScroll + */ + public void setHorizontalScrollPosition(int scrollLeft) { + hTableWrapper.setScrollLeft(scrollLeft); + } + + /** + * Swap cells when the column are dragged + * + * @param oldIndex + * The old index of the cell + * @param newIndex + * The new index of the cell + */ + public void moveCell(int oldIndex, int newIndex) { + final FooterCell hCell = getFooterCell(oldIndex); + final Element cell = hCell.getElement(); + + visibleCells.remove(oldIndex); + DOM.removeChild(tr, cell); + + DOM.insertChild(tr, cell, newIndex); + visibleCells.add(newIndex, hCell); + } + } + + /** + * This Panel can only contain VScrollTableRow type of widgets. This + * "simulates" very large table, keeping spacers which take room of + * unrendered rows. + * + */ + public class VScrollTableBody extends Panel { + + public static final int DEFAULT_ROW_HEIGHT = 24; + + private double rowHeight = -1; + + private final LinkedList<Widget> renderedRows = new LinkedList<Widget>(); + + /** + * Due some optimizations row height measuring is deferred and initial + * set of rows is rendered detached. Flag set on when table body has + * been attached in dom and rowheight has been measured. + */ + private boolean tBodyMeasurementsDone = false; + + Element preSpacer = DOM.createDiv(); + Element postSpacer = DOM.createDiv(); + + Element container = DOM.createDiv(); + + TableSectionElement tBodyElement = Document.get().createTBodyElement(); + Element table = DOM.createTable(); + + private int firstRendered; + private int lastRendered; + + private char[] aligns; + + protected VScrollTableBody() { + constructDOM(); + setElement(container); + } + + public VScrollTableRow getRowByRowIndex(int indexInTable) { + int internalIndex = indexInTable - firstRendered; + if (internalIndex >= 0 && internalIndex < renderedRows.size()) { + return (VScrollTableRow) renderedRows.get(internalIndex); + } else { + return null; + } + } + + /** + * @return the height of scrollable body, subpixels ceiled. + */ + public int getRequiredHeight() { + return preSpacer.getOffsetHeight() + postSpacer.getOffsetHeight() + + Util.getRequiredHeight(table); + } + + private void constructDOM() { + DOM.setElementProperty(table, "className", CLASSNAME + "-table"); + if (BrowserInfo.get().isIE()) { + table.setPropertyInt("cellSpacing", 0); + } + DOM.setElementProperty(preSpacer, "className", CLASSNAME + + "-row-spacer"); + DOM.setElementProperty(postSpacer, "className", CLASSNAME + + "-row-spacer"); + + table.appendChild(tBodyElement); + DOM.appendChild(container, preSpacer); + DOM.appendChild(container, table); + DOM.appendChild(container, postSpacer); + if (BrowserInfo.get().requiresTouchScrollDelegate()) { + NodeList<Node> childNodes = container.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Element item = (Element) childNodes.getItem(i); + item.getStyle().setProperty("webkitTransform", + "translate3d(0,0,0)"); + } + } + + } + + public int getAvailableWidth() { + int availW = scrollBodyPanel.getOffsetWidth() - getBorderWidth(); + return availW; + } + + public void renderInitialRows(UIDL rowData, int firstIndex, int rows) { + firstRendered = firstIndex; + lastRendered = firstIndex + rows - 1; + final Iterator<?> it = rowData.getChildIterator(); + aligns = tHead.getColumnAlignments(); + while (it.hasNext()) { + final VScrollTableRow row = createRow((UIDL) it.next(), aligns); + addRow(row); + } + if (isAttached()) { + fixSpacers(); + } + } + + public void renderRows(UIDL rowData, int firstIndex, int rows) { + // FIXME REVIEW + aligns = tHead.getColumnAlignments(); + final Iterator<?> it = rowData.getChildIterator(); + if (firstIndex == lastRendered + 1) { + while (it.hasNext()) { + final VScrollTableRow row = prepareRow((UIDL) it.next()); + addRow(row); + lastRendered++; + } + fixSpacers(); + } else if (firstIndex + rows == firstRendered) { + final VScrollTableRow[] rowArray = new VScrollTableRow[rows]; + int i = rows; + while (it.hasNext()) { + i--; + rowArray[i] = prepareRow((UIDL) it.next()); + } + for (i = 0; i < rows; i++) { + addRowBeforeFirstRendered(rowArray[i]); + firstRendered--; + } + } else { + // completely new set of rows + while (lastRendered + 1 > firstRendered) { + unlinkRow(false); + } + final VScrollTableRow row = prepareRow((UIDL) it.next()); + firstRendered = firstIndex; + lastRendered = firstIndex - 1; + addRow(row); + lastRendered++; + setContainerHeight(); + fixSpacers(); + while (it.hasNext()) { + addRow(prepareRow((UIDL) it.next())); + lastRendered++; + } + fixSpacers(); + } + + // this may be a new set of rows due content change, + // ensure we have proper cache rows + ensureCacheFilled(); + } + + /** + * Ensure we have the correct set of rows on client side, e.g. if the + * content on the server side has changed, or the client scroll position + * has changed since the last request. + */ + protected void ensureCacheFilled() { + int reactFirstRow = (int) (firstRowInViewPort - pageLength + * cache_react_rate); + int reactLastRow = (int) (firstRowInViewPort + pageLength + pageLength + * cache_react_rate); + if (reactFirstRow < 0) { + reactFirstRow = 0; + } + if (reactLastRow >= totalRows) { + reactLastRow = totalRows - 1; + } + if (lastRendered < reactFirstRow || firstRendered > reactLastRow) { + /* + * #8040 - scroll position is completely changed since the + * latest request, so request a new set of rows. + * + * TODO: We should probably check whether the fetched rows match + * the current scroll position right when they arrive, so as to + * not waste time rendering a set of rows that will never be + * visible... + */ + rowRequestHandler.setReqFirstRow(reactFirstRow); + rowRequestHandler.setReqRows(reactLastRow - reactFirstRow + 1); + rowRequestHandler.deferRowFetch(1); + } else if (lastRendered < reactLastRow) { + // get some cache rows below visible area + rowRequestHandler.setReqFirstRow(lastRendered + 1); + rowRequestHandler.setReqRows(reactLastRow - lastRendered); + rowRequestHandler.deferRowFetch(1); + } else if (firstRendered > reactFirstRow) { + /* + * Branch for fetching cache above visible area. + * + * If cache needed for both before and after visible area, this + * will be rendered after-cache is received and rendered. So in + * some rare situations the table may make two cache visits to + * server. + */ + rowRequestHandler.setReqFirstRow(reactFirstRow); + rowRequestHandler.setReqRows(firstRendered - reactFirstRow); + rowRequestHandler.deferRowFetch(1); + } + } + + /** + * Inserts rows as provided in the rowData starting at firstIndex. + * + * @param rowData + * @param firstIndex + * @param rows + * the number of rows + * @return a list of the rows added. + */ + protected List<VScrollTableRow> insertRows(UIDL rowData, + int firstIndex, int rows) { + aligns = tHead.getColumnAlignments(); + final Iterator<?> it = rowData.getChildIterator(); + List<VScrollTableRow> insertedRows = new ArrayList<VScrollTableRow>(); + + if (firstIndex == lastRendered + 1) { + while (it.hasNext()) { + final VScrollTableRow row = prepareRow((UIDL) it.next()); + addRow(row); + insertedRows.add(row); + lastRendered++; + } + fixSpacers(); + } else if (firstIndex + rows == firstRendered) { + final VScrollTableRow[] rowArray = new VScrollTableRow[rows]; + int i = rows; + while (it.hasNext()) { + i--; + rowArray[i] = prepareRow((UIDL) it.next()); + } + for (i = 0; i < rows; i++) { + addRowBeforeFirstRendered(rowArray[i]); + insertedRows.add(rowArray[i]); + firstRendered--; + } + } else { + // insert in the middle + int ix = firstIndex; + while (it.hasNext()) { + VScrollTableRow row = prepareRow((UIDL) it.next()); + insertRowAt(row, ix); + insertedRows.add(row); + lastRendered++; + ix++; + } + fixSpacers(); + } + return insertedRows; + } + + protected List<VScrollTableRow> insertAndReindexRows(UIDL rowData, + int firstIndex, int rows) { + List<VScrollTableRow> inserted = insertRows(rowData, firstIndex, + rows); + int actualIxOfFirstRowAfterInserted = firstIndex + rows + - firstRendered; + for (int ix = actualIxOfFirstRowAfterInserted; ix < renderedRows + .size(); ix++) { + VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix); + r.setIndex(r.getIndex() + rows); + } + setContainerHeight(); + return inserted; + } + + protected void insertRowsDeleteBelow(UIDL rowData, int firstIndex, + int rows) { + unlinkAllRowsStartingAt(firstIndex); + insertRows(rowData, firstIndex, rows); + setContainerHeight(); + } + + /** + * This method is used to instantiate new rows for this table. It + * automatically sets correct widths to rows cells and assigns correct + * client reference for child widgets. + * + * This method can be called only after table has been initialized + * + * @param uidl + */ + private VScrollTableRow prepareRow(UIDL uidl) { + final VScrollTableRow row = createRow(uidl, aligns); + row.initCellWidths(); + return row; + } + + protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) { + if (uidl.hasAttribute("gen_html")) { + // This is a generated row. + return new VScrollTableGeneratedRow(uidl, aligns2); + } + return new VScrollTableRow(uidl, aligns2); + } + + private void addRowBeforeFirstRendered(VScrollTableRow row) { + row.setIndex(firstRendered - 1); + if (row.isSelected()) { + row.addStyleName("v-selected"); + } + tBodyElement.insertBefore(row.getElement(), + tBodyElement.getFirstChild()); + adopt(row); + renderedRows.add(0, row); + } + + private void addRow(VScrollTableRow row) { + row.setIndex(firstRendered + renderedRows.size()); + if (row.isSelected()) { + row.addStyleName("v-selected"); + } + tBodyElement.appendChild(row.getElement()); + adopt(row); + renderedRows.add(row); + } + + private void insertRowAt(VScrollTableRow row, int index) { + row.setIndex(index); + if (row.isSelected()) { + row.addStyleName("v-selected"); + } + if (index > 0) { + VScrollTableRow sibling = getRowByRowIndex(index - 1); + tBodyElement + .insertAfter(row.getElement(), sibling.getElement()); + } else { + VScrollTableRow sibling = getRowByRowIndex(index); + tBodyElement.insertBefore(row.getElement(), + sibling.getElement()); + } + adopt(row); + int actualIx = index - firstRendered; + renderedRows.add(actualIx, row); + } + + @Override + public Iterator<Widget> iterator() { + return renderedRows.iterator(); + } + + /** + * @return false if couldn't remove row + */ + protected boolean unlinkRow(boolean fromBeginning) { + if (lastRendered - firstRendered < 0) { + return false; + } + int actualIx; + if (fromBeginning) { + actualIx = 0; + firstRendered++; + } else { + actualIx = renderedRows.size() - 1; + lastRendered--; + } + if (actualIx >= 0) { + unlinkRowAtActualIndex(actualIx); + fixSpacers(); + return true; + } + return false; + } + + protected void unlinkRows(int firstIndex, int count) { + if (count < 1) { + return; + } + if (firstRendered > firstIndex + && firstRendered < firstIndex + count) { + firstIndex = firstRendered; + } + int lastIndex = firstIndex + count - 1; + if (lastRendered < lastIndex) { + lastIndex = lastRendered; + } + for (int ix = lastIndex; ix >= firstIndex; ix--) { + unlinkRowAtActualIndex(actualIndex(ix)); + lastRendered--; + } + fixSpacers(); + } + + protected void unlinkAndReindexRows(int firstIndex, int count) { + unlinkRows(firstIndex, count); + int actualFirstIx = firstIndex - firstRendered; + for (int ix = actualFirstIx; ix < renderedRows.size(); ix++) { + VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix); + r.setIndex(r.getIndex() - count); + } + setContainerHeight(); + } + + protected void unlinkAllRowsStartingAt(int index) { + if (firstRendered > index) { + index = firstRendered; + } + for (int ix = renderedRows.size() - 1; ix >= index; ix--) { + unlinkRowAtActualIndex(actualIndex(ix)); + lastRendered--; + } + fixSpacers(); + } + + private int actualIndex(int index) { + return index - firstRendered; + } + + private void unlinkRowAtActualIndex(int index) { + final VScrollTableRow toBeRemoved = (VScrollTableRow) renderedRows + .get(index); + tBodyElement.removeChild(toBeRemoved.getElement()); + orphan(toBeRemoved); + renderedRows.remove(index); + } + + @Override + public boolean remove(Widget w) { + throw new UnsupportedOperationException(); + } + + /** + * Fix container blocks height according to totalRows to avoid + * "bouncing" when scrolling + */ + private void setContainerHeight() { + fixSpacers(); + DOM.setStyleAttribute(container, "height", + measureRowHeightOffset(totalRows) + "px"); + } + + private void fixSpacers() { + int prepx = measureRowHeightOffset(firstRendered); + if (prepx < 0) { + prepx = 0; + } + preSpacer.getStyle().setPropertyPx("height", prepx); + int postpx = measureRowHeightOffset(totalRows - 1) + - measureRowHeightOffset(lastRendered); + if (postpx < 0) { + postpx = 0; + } + postSpacer.getStyle().setPropertyPx("height", postpx); + } + + public double getRowHeight() { + return getRowHeight(false); + } + + public double getRowHeight(boolean forceUpdate) { + if (tBodyMeasurementsDone && !forceUpdate) { + return rowHeight; + } else { + if (tBodyElement.getRows().getLength() > 0) { + int tableHeight = getTableHeight(); + int rowCount = tBodyElement.getRows().getLength(); + rowHeight = tableHeight / (double) rowCount; + } else { + // Special cases if we can't just measure the current rows + if (!Double.isNaN(lastKnownRowHeight)) { + // Use previous value if available + if (BrowserInfo.get().isIE()) { + /* + * IE needs to reflow the table element at this + * point to work correctly (e.g. + * com.vaadin.tests.components.table. + * ContainerSizeChange) - the other code paths + * already trigger reflows, but here it must be done + * explicitly. + */ + getTableHeight(); + } + rowHeight = lastKnownRowHeight; + } else if (isAttached()) { + // measure row height by adding a dummy row + VScrollTableRow scrollTableRow = new VScrollTableRow(); + tBodyElement.appendChild(scrollTableRow.getElement()); + getRowHeight(forceUpdate); + tBodyElement.removeChild(scrollTableRow.getElement()); + } else { + // TODO investigate if this can never happen anymore + return DEFAULT_ROW_HEIGHT; + } + } + lastKnownRowHeight = rowHeight; + tBodyMeasurementsDone = true; + return rowHeight; + } + } + + public int getTableHeight() { + return table.getOffsetHeight(); + } + + /** + * Returns the width available for column content. + * + * @param columnIndex + * @return + */ + public int getColWidth(int columnIndex) { + if (tBodyMeasurementsDone) { + if (renderedRows.isEmpty()) { + // no rows yet rendered + return 0; + } + for (Widget row : renderedRows) { + if (!(row instanceof VScrollTableGeneratedRow)) { + TableRowElement tr = row.getElement().cast(); + Element wrapperdiv = tr.getCells().getItem(columnIndex) + .getFirstChildElement().cast(); + return wrapperdiv.getOffsetWidth(); + } + } + return 0; + } else { + return 0; + } + } + + /** + * Sets the content width of a column. + * + * Due IE limitation, we must set the width to a wrapper elements inside + * table cells (with overflow hidden, which does not work on td + * elements). + * + * To get this work properly crossplatform, we will also set the width + * of td. + * + * @param colIndex + * @param w + */ + public void setColWidth(int colIndex, int w) { + for (Widget row : renderedRows) { + ((VScrollTableRow) row).setCellWidth(colIndex, w); + } + } + + private int cellExtraWidth = -1; + + /** + * Method to return the space used for cell paddings + border. + */ + private int getCellExtraWidth() { + if (cellExtraWidth < 0) { + detectExtrawidth(); + } + return cellExtraWidth; + } + + private void detectExtrawidth() { + NodeList<TableRowElement> rows = tBodyElement.getRows(); + if (rows.getLength() == 0) { + /* need to temporary add empty row and detect */ + VScrollTableRow scrollTableRow = new VScrollTableRow(); + tBodyElement.appendChild(scrollTableRow.getElement()); + detectExtrawidth(); + tBodyElement.removeChild(scrollTableRow.getElement()); + } else { + boolean noCells = false; + TableRowElement item = rows.getItem(0); + TableCellElement firstTD = item.getCells().getItem(0); + if (firstTD == null) { + // content is currently empty, we need to add a fake cell + // for measuring + noCells = true; + VScrollTableRow next = (VScrollTableRow) iterator().next(); + boolean sorted = tHead.getHeaderCell(0) != null ? tHead + .getHeaderCell(0).isSorted() : false; + next.addCell(null, "", ALIGN_LEFT, "", true, sorted); + firstTD = item.getCells().getItem(0); + } + com.google.gwt.dom.client.Element wrapper = firstTD + .getFirstChildElement(); + cellExtraWidth = firstTD.getOffsetWidth() + - wrapper.getOffsetWidth(); + if (noCells) { + firstTD.getParentElement().removeChild(firstTD); + } + } + } + + private void reLayoutComponents() { + for (Widget w : this) { + VScrollTableRow r = (VScrollTableRow) w; + for (Widget widget : r) { + client.handleComponentRelativeSize(widget); + } + } + } + + public int getLastRendered() { + return lastRendered; + } + + public int getFirstRendered() { + return firstRendered; + } + + public void moveCol(int oldIndex, int newIndex) { + + // loop all rows and move given index to its new place + final Iterator<?> rows = iterator(); + while (rows.hasNext()) { + final VScrollTableRow row = (VScrollTableRow) rows.next(); + + final Element td = DOM.getChild(row.getElement(), oldIndex); + if (td != null) { + DOM.removeChild(row.getElement(), td); + + DOM.insertChild(row.getElement(), td, newIndex); + } + } + + } + + /** + * Restore row visibility which is set to "none" when the row is + * rendered (due a performance optimization). + */ + private void restoreRowVisibility() { + for (Widget row : renderedRows) { + row.getElement().getStyle().setProperty("visibility", ""); + } + } + + public class VScrollTableRow extends Panel implements ActionOwner { + + private static final int TOUCHSCROLL_TIMEOUT = 100; + private static final int DRAGMODE_MULTIROW = 2; + protected ArrayList<Widget> childWidgets = new ArrayList<Widget>(); + private boolean selected = false; + protected final int rowKey; + + private String[] actionKeys = null; + private final TableRowElement rowElement; + private int index; + private Event touchStart; + private static final String ROW_CLASSNAME_EVEN = CLASSNAME + "-row"; + private static final String ROW_CLASSNAME_ODD = CLASSNAME + + "-row-odd"; + private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500; + private Timer contextTouchTimeout; + private Timer dragTouchTimeout; + private int touchStartY; + private int touchStartX; + private TooltipInfo tooltipInfo = null; + private Map<TableCellElement, TooltipInfo> cellToolTips = new HashMap<TableCellElement, TooltipInfo>(); + private boolean isDragging = false; + + private VScrollTableRow(int rowKey) { + this.rowKey = rowKey; + rowElement = Document.get().createTRElement(); + setElement(rowElement); + DOM.sinkEvents(getElement(), Event.MOUSEEVENTS + | Event.TOUCHEVENTS | Event.ONDBLCLICK + | Event.ONCONTEXTMENU | VTooltip.TOOLTIP_EVENTS); + } + + public VScrollTableRow(UIDL uidl, char[] aligns) { + this(uidl.getIntAttribute("key")); + + /* + * Rendering the rows as hidden improves Firefox and Safari + * performance drastically. + */ + getElement().getStyle().setProperty("visibility", "hidden"); + + String rowStyle = uidl.getStringAttribute("rowstyle"); + if (rowStyle != null) { + addStyleName(CLASSNAME + "-row-" + rowStyle); + } + + String rowDescription = uidl.getStringAttribute("rowdescr"); + if (rowDescription != null && !rowDescription.equals("")) { + tooltipInfo = new TooltipInfo(rowDescription); + } else { + tooltipInfo = null; + } + + tHead.getColumnAlignments(); + int col = 0; + int visibleColumnIndex = -1; + + // row header + if (showRowHeaders) { + boolean sorted = tHead.getHeaderCell(col).isSorted(); + addCell(uidl, buildCaptionHtmlSnippet(uidl), aligns[col++], + "rowheader", true, sorted); + visibleColumnIndex++; + } + + if (uidl.hasAttribute("al")) { + actionKeys = uidl.getStringArrayAttribute("al"); + } + + addCellsFromUIDL(uidl, aligns, col, visibleColumnIndex); + + if (uidl.hasAttribute("selected") && !isSelected()) { + toggleSelection(); + } + } + + public TooltipInfo getTooltipInfo() { + return tooltipInfo; + } + + /** + * Add a dummy row, used for measurements if Table is empty. + */ + public VScrollTableRow() { + this(0); + addStyleName(CLASSNAME + "-row"); + addCell(null, "_", 'b', "", true, false); + } + + protected void initCellWidths() { + final int cells = tHead.getVisibleCellCount(); + for (int i = 0; i < cells; i++) { + int w = VScrollTable.this.getColWidth(getColKeyByIndex(i)); + if (w < 0) { + w = 0; + } + setCellWidth(i, w); + } + } + + protected void setCellWidth(int cellIx, int width) { + final Element cell = DOM.getChild(getElement(), cellIx); + cell.getFirstChildElement().getStyle() + .setPropertyPx("width", width); + cell.getStyle().setPropertyPx("width", width); + } + + protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col, + int visibleColumnIndex) { + final Iterator<?> cells = uidl.getChildIterator(); + while (cells.hasNext()) { + final Object cell = cells.next(); + visibleColumnIndex++; + + String columnId = visibleColOrder[visibleColumnIndex]; + + String style = ""; + if (uidl.hasAttribute("style-" + columnId)) { + style = uidl.getStringAttribute("style-" + columnId); + } + + String description = null; + if (uidl.hasAttribute("descr-" + columnId)) { + description = uidl.getStringAttribute("descr-" + + columnId); + } + + boolean sorted = tHead.getHeaderCell(col).isSorted(); + if (cell instanceof String) { + addCell(uidl, cell.toString(), aligns[col++], style, + isRenderHtmlInCells(), sorted, description); + } else { + final ComponentConnector cellContent = client + .getPaintable((UIDL) cell); + + addCell(uidl, cellContent.getWidget(), aligns[col++], + style, sorted); + } + } + } + + /** + * Overriding this and returning true causes all text cells to be + * rendered as HTML. + * + * @return always returns false in the default implementation + */ + protected boolean isRenderHtmlInCells() { + return false; + } + + /** + * Detects whether row is visible in tables viewport. + * + * @return + */ + public boolean isInViewPort() { + int absoluteTop = getAbsoluteTop(); + int scrollPosition = scrollBodyPanel.getScrollPosition(); + if (absoluteTop < scrollPosition) { + return false; + } + int maxVisible = scrollPosition + + scrollBodyPanel.getOffsetHeight() - getOffsetHeight(); + if (absoluteTop > maxVisible) { + return false; + } + return true; + } + + /** + * Makes a check based on indexes whether the row is before the + * compared row. + * + * @param row1 + * @return true if this rows index is smaller than in the row1 + */ + public boolean isBefore(VScrollTableRow row1) { + return getIndex() < row1.getIndex(); + } + + /** + * Sets the index of the row in the whole table. Currently used just + * to set even/odd classname + * + * @param indexInWholeTable + */ + private void setIndex(int indexInWholeTable) { + index = indexInWholeTable; + boolean isOdd = indexInWholeTable % 2 == 0; + // Inverted logic to be backwards compatible with earlier 6.4. + // It is very strange because rows 1,3,5 are considered "even" + // and 2,4,6 "odd". + // + // First remove any old styles so that both styles aren't + // applied when indexes are updated. + removeStyleName(ROW_CLASSNAME_ODD); + removeStyleName(ROW_CLASSNAME_EVEN); + if (!isOdd) { + addStyleName(ROW_CLASSNAME_ODD); + } else { + addStyleName(ROW_CLASSNAME_EVEN); + } + } + + public int getIndex() { + return index; + } + + @Override + protected void onDetach() { + super.onDetach(); + client.getContextMenu().ensureHidden(this); + } + + public String getKey() { + return String.valueOf(rowKey); + } + + public void addCell(UIDL rowUidl, String text, char align, + String style, boolean textIsHTML, boolean sorted) { + addCell(rowUidl, text, align, style, textIsHTML, sorted, null); + } + + public void addCell(UIDL rowUidl, String text, char align, + String style, boolean textIsHTML, boolean sorted, + String description) { + // String only content is optimized by not using Label widget + final TableCellElement td = DOM.createTD().cast(); + initCellWithText(text, align, style, textIsHTML, sorted, + description, td); + } + + protected void initCellWithText(String text, char align, + String style, boolean textIsHTML, boolean sorted, + String description, final TableCellElement td) { + final Element container = DOM.createDiv(); + String className = CLASSNAME + "-cell-content"; + if (style != null && !style.equals("")) { + className += " " + CLASSNAME + "-cell-content-" + style; + } + if (sorted) { + className += " " + CLASSNAME + "-cell-content-sorted"; + } + td.setClassName(className); + container.setClassName(CLASSNAME + "-cell-wrapper"); + if (textIsHTML) { + container.setInnerHTML(text); + } else { + container.setInnerText(text); + } + if (align != ALIGN_LEFT) { + switch (align) { + case ALIGN_CENTER: + container.getStyle().setProperty("textAlign", "center"); + break; + case ALIGN_RIGHT: + default: + container.getStyle().setProperty("textAlign", "right"); + break; + } + } + + if (description != null && !description.equals("")) { + TooltipInfo info = new TooltipInfo(description); + cellToolTips.put(td, info); + } else { + cellToolTips.remove(td); + } + + td.appendChild(container); + getElement().appendChild(td); + } + + public void addCell(UIDL rowUidl, Widget w, char align, + String style, boolean sorted) { + final TableCellElement td = DOM.createTD().cast(); + initCellWithWidget(w, align, style, sorted, td); + } + + protected void initCellWithWidget(Widget w, char align, + String style, boolean sorted, final TableCellElement td) { + final Element container = DOM.createDiv(); + String className = CLASSNAME + "-cell-content"; + if (style != null && !style.equals("")) { + className += " " + CLASSNAME + "-cell-content-" + style; + } + if (sorted) { + className += " " + CLASSNAME + "-cell-content-sorted"; + } + td.setClassName(className); + container.setClassName(CLASSNAME + "-cell-wrapper"); + // TODO most components work with this, but not all (e.g. + // Select) + // Old comment: make widget cells respect align. + // text-align:center for IE, margin: auto for others + if (align != ALIGN_LEFT) { + switch (align) { + case ALIGN_CENTER: + container.getStyle().setProperty("textAlign", "center"); + break; + case ALIGN_RIGHT: + default: + container.getStyle().setProperty("textAlign", "right"); + break; + } + } + td.appendChild(container); + getElement().appendChild(td); + // ensure widget not attached to another element (possible tBody + // change) + w.removeFromParent(); + container.appendChild(w.getElement()); + adopt(w); + childWidgets.add(w); + } + + @Override + public Iterator<Widget> iterator() { + return childWidgets.iterator(); + } + + @Override + public boolean remove(Widget w) { + if (childWidgets.contains(w)) { + orphan(w); + DOM.removeChild(DOM.getParent(w.getElement()), + w.getElement()); + childWidgets.remove(w); + return true; + } else { + return false; + } + } + + /** + * If there are registered click listeners, sends a click event and + * returns true. Otherwise, does nothing and returns false. + * + * @param event + * @param targetTdOrTr + * @param immediate + * Whether the event is sent immediately + * @return Whether a click event was sent + */ + private boolean handleClickEvent(Event event, Element targetTdOrTr, + boolean immediate) { + if (!client.hasEventListeners(VScrollTable.this, + TableConstants.ITEM_CLICK_EVENT_ID)) { + // Don't send an event if nobody is listening + return false; + } + + // This row was clicked + client.updateVariable(paintableId, "clickedKey", "" + rowKey, + false); + + if (getElement() == targetTdOrTr.getParentElement()) { + // A specific column was clicked + int childIndex = DOM.getChildIndex(getElement(), + targetTdOrTr); + String colKey = null; + colKey = tHead.getHeaderCell(childIndex).getColKey(); + client.updateVariable(paintableId, "clickedColKey", colKey, + false); + } + + MouseEventDetails details = MouseEventDetailsBuilder + .buildMouseEventDetails(event); + + client.updateVariable(paintableId, "clickEvent", + details.toString(), immediate); + + return true; + } + + public TooltipInfo getTooltip( + com.google.gwt.dom.client.Element target) { + + TooltipInfo info = null; + + if (target.hasTagName("TD")) { + + TableCellElement td = (TableCellElement) target.cast(); + info = cellToolTips.get(td); + } + + if (info == null) { + info = tooltipInfo; + } + + return info; + } + + /** + * Special handler for touch devices that support native scrolling + * + * @return Whether the event was handled by this method. + */ + private boolean handleTouchEvent(final Event event) { + + boolean touchEventHandled = false; + + if (enabled && hasNativeTouchScrolling) { + final Element targetTdOrTr = getEventTargetTdOrTr(event); + final int type = event.getTypeInt(); + + switch (type) { + case Event.ONTOUCHSTART: + touchEventHandled = true; + touchStart = event; + isDragging = false; + Touch touch = event.getChangedTouches().get(0); + // save position to fields, touches in events are same + // instance during the operation. + touchStartX = touch.getClientX(); + touchStartY = touch.getClientY(); + + if (dragmode != 0) { + if (dragTouchTimeout == null) { + dragTouchTimeout = new Timer() { + + @Override + public void run() { + if (touchStart != null) { + // Start a drag if a finger is held + // in place long enough, then moved + isDragging = true; + } + } + }; + } + dragTouchTimeout.schedule(TOUCHSCROLL_TIMEOUT); + } + + if (actionKeys != null) { + if (contextTouchTimeout == null) { + contextTouchTimeout = new Timer() { + + @Override + public void run() { + if (touchStart != null) { + // Open the context menu if finger + // is held in place long enough. + showContextMenu(touchStart); + event.preventDefault(); + touchStart = null; + } + } + }; + } + contextTouchTimeout + .schedule(TOUCH_CONTEXT_MENU_TIMEOUT); + } + break; + case Event.ONTOUCHMOVE: + touchEventHandled = true; + if (isSignificantMove(event)) { + if (contextTouchTimeout != null) { + // Moved finger before the context menu timer + // expired, so let the browser handle this as a + // scroll. + contextTouchTimeout.cancel(); + contextTouchTimeout = null; + } + if (!isDragging && dragTouchTimeout != null) { + // Moved finger before the drag timer expired, + // so let the browser handle this as a scroll. + dragTouchTimeout.cancel(); + dragTouchTimeout = null; + } + + if (dragmode != 0 && touchStart != null + && isDragging) { + event.preventDefault(); + event.stopPropagation(); + startRowDrag(touchStart, type, targetTdOrTr); + } + touchStart = null; + } + break; + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + touchEventHandled = true; + if (contextTouchTimeout != null) { + contextTouchTimeout.cancel(); + } + if (dragTouchTimeout != null) { + dragTouchTimeout.cancel(); + } + if (touchStart != null) { + event.preventDefault(); + event.stopPropagation(); + if (!BrowserInfo.get().isAndroid()) { + Util.simulateClickFromTouchEvent(touchStart, + this); + } + touchStart = null; + } + isDragging = false; + break; + } + } + return touchEventHandled; + } + + /* + * React on click that occur on content cells only + */ + + @Override + public void onBrowserEvent(final Event event) { + + final boolean touchEventHandled = handleTouchEvent(event); + + if (enabled && !touchEventHandled) { + final int type = event.getTypeInt(); + final Element targetTdOrTr = getEventTargetTdOrTr(event); + if (type == Event.ONCONTEXTMENU) { + showContextMenu(event); + if (enabled + && (actionKeys != null || client + .hasEventListeners( + VScrollTable.this, + TableConstants.ITEM_CLICK_EVENT_ID))) { + /* + * Prevent browser context menu only if there are + * action handlers or item click listeners + * registered + */ + event.stopPropagation(); + event.preventDefault(); + } + return; + } + + boolean targetCellOrRowFound = targetTdOrTr != null; + + switch (type) { + case Event.ONDBLCLICK: + if (targetCellOrRowFound) { + handleClickEvent(event, targetTdOrTr, true); + } + break; + case Event.ONMOUSEUP: + if (targetCellOrRowFound) { + /* + * Queue here, send at the same time as the + * corresponding value change event - see #7127 + */ + boolean clickEventSent = handleClickEvent(event, + targetTdOrTr, false); + + if (event.getButton() == Event.BUTTON_LEFT + && isSelectable()) { + + // Ctrl+Shift click + if ((event.getCtrlKey() || event.getMetaKey()) + && event.getShiftKey() + && isMultiSelectModeDefault()) { + toggleShiftSelection(false); + setRowFocus(this); + + // Ctrl click + } else if ((event.getCtrlKey() || event + .getMetaKey()) + && isMultiSelectModeDefault()) { + boolean wasSelected = isSelected(); + toggleSelection(); + setRowFocus(this); + /* + * next possible range select must start on + * this row + */ + selectionRangeStart = this; + if (wasSelected) { + removeRowFromUnsentSelectionRanges(this); + } + + } else if ((event.getCtrlKey() || event + .getMetaKey()) && isSingleSelectMode()) { + // Ctrl (or meta) click (Single selection) + if (!isSelected() + || (isSelected() && nullSelectionAllowed)) { + + if (!isSelected()) { + deselectAll(); + } + + toggleSelection(); + setRowFocus(this); + } + + } else if (event.getShiftKey() + && isMultiSelectModeDefault()) { + // Shift click + toggleShiftSelection(true); + + } else { + // click + boolean currentlyJustThisRowSelected = selectedRowKeys + .size() == 1 + && selectedRowKeys + .contains(getKey()); + + if (!currentlyJustThisRowSelected) { + if (isSingleSelectMode() + || isMultiSelectModeDefault()) { + /* + * For default multi select mode + * (ctrl/shift) and for single + * select mode we need to clear the + * previous selection before + * selecting a new one when the user + * clicks on a row. Only in + * multiselect/simple mode the old + * selection should remain after a + * normal click. + */ + deselectAll(); + } + toggleSelection(); + } else if ((isSingleSelectMode() || isMultiSelectModeSimple()) + && nullSelectionAllowed) { + toggleSelection(); + }/* + * else NOP to avoid excessive server + * visits (selection is removed with + * CTRL/META click) + */ + + selectionRangeStart = this; + setRowFocus(this); + } + + // Remove IE text selection hack + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()) + .setPropertyJSO("onselectstart", + null); + } + // Queue value change + sendSelectedRows(false); + } + /* + * Send queued click and value change events if any + * If a click event is sent, send value change with + * it regardless of the immediate flag, see #7127 + */ + if (immediate || clickEventSent) { + client.sendPendingVariableChanges(); + } + } + break; + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + if (touchStart != null) { + /* + * Touch has not been handled as neither context or + * drag start, handle it as a click. + */ + Util.simulateClickFromTouchEvent(touchStart, this); + touchStart = null; + } + if (contextTouchTimeout != null) { + contextTouchTimeout.cancel(); + } + break; + case Event.ONTOUCHMOVE: + if (isSignificantMove(event)) { + /* + * TODO figure out scroll delegate don't eat events + * if row is selected. Null check for active + * delegate is as a workaround. + */ + if (dragmode != 0 + && touchStart != null + && (TouchScrollDelegate + .getActiveScrollDelegate() == null)) { + startRowDrag(touchStart, type, targetTdOrTr); + } + if (contextTouchTimeout != null) { + contextTouchTimeout.cancel(); + } + /* + * Avoid clicks and drags by clearing touch start + * flag. + */ + touchStart = null; + } + + break; + case Event.ONTOUCHSTART: + touchStart = event; + Touch touch = event.getChangedTouches().get(0); + // save position to fields, touches in events are same + // isntance during the operation. + touchStartX = touch.getClientX(); + touchStartY = touch.getClientY(); + /* + * Prevent simulated mouse events. + */ + touchStart.preventDefault(); + if (dragmode != 0 || actionKeys != null) { + new Timer() { + + @Override + public void run() { + TouchScrollDelegate activeScrollDelegate = TouchScrollDelegate + .getActiveScrollDelegate(); + /* + * If there's a scroll delegate, check if + * we're actually scrolling and handle it. + * If no delegate, do nothing here and let + * the row handle potential drag'n'drop or + * context menu. + */ + if (activeScrollDelegate != null) { + if (activeScrollDelegate.isMoved()) { + /* + * Prevent the row from handling + * touch move/end events (the + * delegate handles those) and from + * doing drag'n'drop or opening a + * context menu. + */ + touchStart = null; + } else { + /* + * Scrolling hasn't started, so + * cancel delegate and let the row + * handle potential drag'n'drop or + * context menu. + */ + activeScrollDelegate + .stopScrolling(); + } + } + } + }.schedule(TOUCHSCROLL_TIMEOUT); + + if (contextTouchTimeout == null + && actionKeys != null) { + contextTouchTimeout = new Timer() { + + @Override + public void run() { + if (touchStart != null) { + showContextMenu(touchStart); + touchStart = null; + } + } + }; + } + if (contextTouchTimeout != null) { + contextTouchTimeout.cancel(); + contextTouchTimeout + .schedule(TOUCH_CONTEXT_MENU_TIMEOUT); + } + } + break; + case Event.ONMOUSEDOWN: + if (targetCellOrRowFound) { + setRowFocus(this); + ensureFocus(); + if (dragmode != 0 + && (event.getButton() == NativeEvent.BUTTON_LEFT)) { + startRowDrag(event, type, targetTdOrTr); + + } else if (event.getCtrlKey() + || event.getShiftKey() + || event.getMetaKey() + && isMultiSelectModeDefault()) { + + // Prevent default text selection in Firefox + event.preventDefault(); + + // Prevent default text selection in IE + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()) + .setPropertyJSO( + "onselectstart", + getPreventTextSelectionIEHack()); + } + + event.stopPropagation(); + } + } + break; + case Event.ONMOUSEOUT: + break; + default: + break; + } + } + super.onBrowserEvent(event); + } + + private boolean isSignificantMove(Event event) { + if (touchStart == null) { + // no touch start + return false; + } + /* + * TODO calculate based on real distance instead of separate + * axis checks + */ + Touch touch = event.getChangedTouches().get(0); + if (Math.abs(touch.getClientX() - touchStartX) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) { + return true; + } + if (Math.abs(touch.getClientY() - touchStartY) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) { + return true; + } + return false; + } + + protected void startRowDrag(Event event, final int type, + Element targetTdOrTr) { + VTransferable transferable = new VTransferable(); + transferable.setDragSource(ConnectorMap.get(client) + .getConnector(VScrollTable.this)); + transferable.setData("itemId", "" + rowKey); + NodeList<TableCellElement> cells = rowElement.getCells(); + for (int i = 0; i < cells.getLength(); i++) { + if (cells.getItem(i).isOrHasChild(targetTdOrTr)) { + HeaderCell headerCell = tHead.getHeaderCell(i); + transferable.setData("propertyId", headerCell.cid); + break; + } + } + + VDragEvent ev = VDragAndDropManager.get().startDrag( + transferable, event, true); + if (dragmode == DRAGMODE_MULTIROW && isMultiSelectModeAny() + && selectedRowKeys.contains("" + rowKey)) { + ev.createDragImage( + (Element) scrollBody.tBodyElement.cast(), true); + Element dragImage = ev.getDragImage(); + int i = 0; + for (Iterator<Widget> iterator = scrollBody.iterator(); iterator + .hasNext();) { + VScrollTableRow next = (VScrollTableRow) iterator + .next(); + Element child = (Element) dragImage.getChild(i++); + if (!selectedRowKeys.contains("" + next.rowKey)) { + child.getStyle().setVisibility(Visibility.HIDDEN); + } + } + } else { + ev.createDragImage(getElement(), true); + } + if (type == Event.ONMOUSEDOWN) { + event.preventDefault(); + } + event.stopPropagation(); + } + + /** + * Finds the TD that the event interacts with. Returns null if the + * target of the event should not be handled. If the event target is + * the row directly this method returns the TR element instead of + * the TD. + * + * @param event + * @return TD or TR element that the event targets (the actual event + * target is this element or a child of it) + */ + private Element getEventTargetTdOrTr(Event event) { + final Element eventTarget = event.getEventTarget().cast(); + Widget widget = Util.findWidget(eventTarget, null); + final Element thisTrElement = getElement(); + + if (widget != this) { + /* + * This is a workaround to make Labels, read only TextFields + * and Embedded in a Table clickable (see #2688). It is + * really not a fix as it does not work with a custom read + * only components (not extending VLabel/VEmbedded). + */ + while (widget != null && widget.getParent() != this) { + widget = widget.getParent(); + } + + if (!(widget instanceof VLabel) + && !(widget instanceof VEmbedded) + && !(widget instanceof VTextField && ((VTextField) widget) + .isReadOnly())) { + return null; + } + } + if (eventTarget == thisTrElement) { + // This was a click on the TR element + return thisTrElement; + } + + // Iterate upwards until we find the TR element + Element element = eventTarget; + while (element != null + && element.getParentElement().cast() != thisTrElement) { + element = element.getParentElement().cast(); + } + return element; + } + + public void showContextMenu(Event event) { + if (enabled && actionKeys != null) { + // Show context menu if there are registered action handlers + int left = Util.getTouchOrMouseClientX(event); + int top = Util.getTouchOrMouseClientY(event); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + contextMenu = new ContextMenuDetails(getKey(), left, top); + client.getContextMenu().showAt(this, left, top); + } + } + + /** + * Has the row been selected? + * + * @return Returns true if selected, else false + */ + public boolean isSelected() { + return selected; + } + + /** + * Toggle the selection of the row + */ + public void toggleSelection() { + selected = !selected; + selectionChanged = true; + if (selected) { + selectedRowKeys.add(String.valueOf(rowKey)); + addStyleName("v-selected"); + } else { + removeStyleName("v-selected"); + selectedRowKeys.remove(String.valueOf(rowKey)); + } + } + + /** + * Is called when a user clicks an item when holding SHIFT key down. + * This will select a new range from the last focused row + * + * @param deselectPrevious + * Should the previous selected range be deselected + */ + private void toggleShiftSelection(boolean deselectPrevious) { + + /* + * Ensures that we are in multiselect mode and that we have a + * previous selection which was not a deselection + */ + if (isSingleSelectMode()) { + // No previous selection found + deselectAll(); + toggleSelection(); + return; + } + + // Set the selectable range + VScrollTableRow endRow = this; + VScrollTableRow startRow = selectionRangeStart; + if (startRow == null) { + startRow = focusedRow; + // If start row is null then we have a multipage selection + // from + // above + if (startRow == null) { + startRow = (VScrollTableRow) scrollBody.iterator() + .next(); + setRowFocus(endRow); + } + } + // Deselect previous items if so desired + if (deselectPrevious) { + deselectAll(); + } + + // we'll ensure GUI state from top down even though selection + // was the opposite way + if (!startRow.isBefore(endRow)) { + VScrollTableRow tmp = startRow; + startRow = endRow; + endRow = tmp; + } + SelectionRange range = new SelectionRange(startRow, endRow); + + for (Widget w : scrollBody) { + VScrollTableRow row = (VScrollTableRow) w; + if (range.inRange(row)) { + if (!row.isSelected()) { + row.toggleSelection(); + } + selectedRowKeys.add(row.getKey()); + } + } + + // Add range + if (startRow != endRow) { + selectedRowRanges.add(range); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ui.IActionOwner#getActions () + */ + + @Override + public Action[] getActions() { + if (actionKeys == null) { + return new Action[] {}; + } + final Action[] actions = new Action[actionKeys.length]; + for (int i = 0; i < actions.length; i++) { + final String actionKey = actionKeys[i]; + final TreeAction a = new TreeAction(this, + String.valueOf(rowKey), actionKey) { + + @Override + public void execute() { + super.execute(); + lazyRevertFocusToRow(VScrollTableRow.this); + } + }; + a.setCaption(getActionCaption(actionKey)); + a.setIconUrl(getActionIcon(actionKey)); + actions[i] = a; + } + return actions; + } + + @Override + public ApplicationConnection getClient() { + return client; + } + + @Override + public String getPaintableId() { + return paintableId; + } + + private int getColIndexOf(Widget child) { + com.google.gwt.dom.client.Element widgetCell = child + .getElement().getParentElement().getParentElement(); + NodeList<TableCellElement> cells = rowElement.getCells(); + for (int i = 0; i < cells.getLength(); i++) { + if (cells.getItem(i) == widgetCell) { + return i; + } + } + return -1; + } + + public Widget getWidgetForPaintable() { + return this; + } + } + + protected class VScrollTableGeneratedRow extends VScrollTableRow { + + private boolean spanColumns; + private boolean htmlContentAllowed; + + public VScrollTableGeneratedRow(UIDL uidl, char[] aligns) { + super(uidl, aligns); + addStyleName("v-table-generated-row"); + } + + public boolean isSpanColumns() { + return spanColumns; + } + + @Override + protected void initCellWidths() { + if (spanColumns) { + setSpannedColumnWidthAfterDOMFullyInited(); + } else { + super.initCellWidths(); + } + } + + private void setSpannedColumnWidthAfterDOMFullyInited() { + // Defer setting width on spanned columns to make sure that + // they are added to the DOM before trying to calculate + // widths. + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + + @Override + public void execute() { + if (showRowHeaders) { + setCellWidth(0, tHead.getHeaderCell(0).getWidth()); + calcAndSetSpanWidthOnCell(1); + } else { + calcAndSetSpanWidthOnCell(0); + } + } + }); + } + + @Override + protected boolean isRenderHtmlInCells() { + return htmlContentAllowed; + } + + @Override + protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col, + int visibleColumnIndex) { + htmlContentAllowed = uidl.getBooleanAttribute("gen_html"); + spanColumns = uidl.getBooleanAttribute("gen_span"); + + final Iterator<?> cells = uidl.getChildIterator(); + if (spanColumns) { + int colCount = uidl.getChildCount(); + if (cells.hasNext()) { + final Object cell = cells.next(); + if (cell instanceof String) { + addSpannedCell(uidl, cell.toString(), aligns[0], + "", htmlContentAllowed, false, null, + colCount); + } else { + addSpannedCell(uidl, (Widget) cell, aligns[0], "", + false, colCount); + } + } + } else { + super.addCellsFromUIDL(uidl, aligns, col, + visibleColumnIndex); + } + } + + private void addSpannedCell(UIDL rowUidl, Widget w, char align, + String style, boolean sorted, int colCount) { + TableCellElement td = DOM.createTD().cast(); + td.setColSpan(colCount); + initCellWithWidget(w, align, style, sorted, td); + } + + private void addSpannedCell(UIDL rowUidl, String text, char align, + String style, boolean textIsHTML, boolean sorted, + String description, int colCount) { + // String only content is optimized by not using Label widget + final TableCellElement td = DOM.createTD().cast(); + td.setColSpan(colCount); + initCellWithText(text, align, style, textIsHTML, sorted, + description, td); + } + + @Override + protected void setCellWidth(int cellIx, int width) { + if (isSpanColumns()) { + if (showRowHeaders) { + if (cellIx == 0) { + super.setCellWidth(0, width); + } else { + // We need to recalculate the spanning TDs width for + // every cellIx in order to support column resizing. + calcAndSetSpanWidthOnCell(1); + } + } else { + // Same as above. + calcAndSetSpanWidthOnCell(0); + } + } else { + super.setCellWidth(cellIx, width); + } + } + + private void calcAndSetSpanWidthOnCell(final int cellIx) { + int spanWidth = 0; + for (int ix = (showRowHeaders ? 1 : 0); ix < tHead + .getVisibleCellCount(); ix++) { + spanWidth += tHead.getHeaderCell(ix).getOffsetWidth(); + } + Util.setWidthExcludingPaddingAndBorder((Element) getElement() + .getChild(cellIx), spanWidth, 13, false); + } + } + + /** + * Ensure the component has a focus. + * + * TODO the current implementation simply always calls focus for the + * component. In case the Table at some point implements focus/blur + * listeners, this method needs to be evolved to conditionally call + * focus only if not currently focused. + */ + protected void ensureFocus() { + if (!hasFocus) { + scrollBodyPanel.setFocus(true); + } + + } + + } + + /** + * Deselects all items + */ + public void deselectAll() { + for (Widget w : scrollBody) { + VScrollTableRow row = (VScrollTableRow) w; + if (row.isSelected()) { + row.toggleSelection(); + } + } + // still ensure all selects are removed from (not necessary rendered) + selectedRowKeys.clear(); + selectedRowRanges.clear(); + // also notify server that it clears all previous selections (the client + // side does not know about the invisible ones) + instructServerToForgetPreviousSelections(); + } + + /** + * Used in multiselect mode when the client side knows that all selections + * are in the next request. + */ + private void instructServerToForgetPreviousSelections() { + client.updateVariable(paintableId, "clearSelections", true, false); + } + + /** + * Determines the pagelength when the table height is fixed. + */ + public void updatePageLength() { + // Only update if visible and enabled + if (!isVisible() || !enabled) { + return; + } + + if (scrollBody == null) { + return; + } + + if (isDynamicHeight()) { + return; + } + + int rowHeight = (int) Math.round(scrollBody.getRowHeight()); + int bodyH = scrollBodyPanel.getOffsetHeight(); + int rowsAtOnce = bodyH / rowHeight; + boolean anotherPartlyVisible = ((bodyH % rowHeight) != 0); + if (anotherPartlyVisible) { + rowsAtOnce++; + } + if (pageLength != rowsAtOnce) { + pageLength = rowsAtOnce; + client.updateVariable(paintableId, "pagelength", pageLength, false); + + if (!rendering) { + int currentlyVisible = scrollBody.lastRendered + - scrollBody.firstRendered; + if (currentlyVisible < pageLength + && currentlyVisible < totalRows) { + // shake scrollpanel to fill empty space + scrollBodyPanel.setScrollPosition(scrollTop + 1); + scrollBodyPanel.setScrollPosition(scrollTop - 1); + } + + sizeNeedsInit = true; + } + } + + } + + void updateWidth() { + if (!isVisible()) { + /* + * Do not update size when the table is hidden as all column widths + * will be set to zero and they won't be recalculated when the table + * is set visible again (until the size changes again) + */ + return; + } + + if (!isDynamicWidth()) { + int innerPixels = getOffsetWidth() - getBorderWidth(); + if (innerPixels < 0) { + innerPixels = 0; + } + setContentWidth(innerPixels); + + // readjust undefined width columns + triggerLazyColumnAdjustment(false); + + } else { + + sizeNeedsInit = true; + + // readjust undefined width columns + triggerLazyColumnAdjustment(false); + } + + /* + * setting width may affect wheter the component has scrollbars -> needs + * scrolling or not + */ + setProperTabIndex(); + } + + private static final int LAZY_COLUMN_ADJUST_TIMEOUT = 300; + + private final Timer lazyAdjustColumnWidths = new Timer() { + /** + * Check for column widths, and available width, to see if we can fix + * column widths "optimally". Doing this lazily to avoid expensive + * calculation when resizing is not yet finished. + */ + + @Override + public void run() { + if (scrollBody == null) { + // Try again later if we get here before scrollBody has been + // initalized + triggerLazyColumnAdjustment(false); + return; + } + + Iterator<Widget> headCells = tHead.iterator(); + int usedMinimumWidth = 0; + int totalExplicitColumnsWidths = 0; + float expandRatioDivider = 0; + int colIndex = 0; + while (headCells.hasNext()) { + final HeaderCell hCell = (HeaderCell) headCells.next(); + if (hCell.isDefinedWidth()) { + totalExplicitColumnsWidths += hCell.getWidth(); + usedMinimumWidth += hCell.getWidth(); + } else { + usedMinimumWidth += hCell.getNaturalColumnWidth(colIndex); + expandRatioDivider += hCell.getExpandRatio(); + } + colIndex++; + } + + int availW = scrollBody.getAvailableWidth(); + // Hey IE, are you really sure about this? + availW = scrollBody.getAvailableWidth(); + int visibleCellCount = tHead.getVisibleCellCount(); + availW -= scrollBody.getCellExtraWidth() * visibleCellCount; + if (willHaveScrollbars()) { + availW -= Util.getNativeScrollbarSize(); + } + + int extraSpace = availW - usedMinimumWidth; + if (extraSpace < 0) { + extraSpace = 0; + } + + int totalUndefinedNaturalWidths = usedMinimumWidth + - totalExplicitColumnsWidths; + + // we have some space that can be divided optimally + HeaderCell hCell; + colIndex = 0; + headCells = tHead.iterator(); + int checksum = 0; + while (headCells.hasNext()) { + hCell = (HeaderCell) headCells.next(); + if (!hCell.isDefinedWidth()) { + int w = hCell.getNaturalColumnWidth(colIndex); + int newSpace; + if (expandRatioDivider > 0) { + // divide excess space by expand ratios + newSpace = Math.round((w + extraSpace + * hCell.getExpandRatio() / expandRatioDivider)); + } else { + if (totalUndefinedNaturalWidths != 0) { + // divide relatively to natural column widths + newSpace = Math.round(w + (float) extraSpace + * (float) w / totalUndefinedNaturalWidths); + } else { + newSpace = w; + } + } + checksum += newSpace; + setColWidth(colIndex, newSpace, false); + } else { + checksum += hCell.getWidth(); + } + colIndex++; + } + + if (extraSpace > 0 && checksum != availW) { + /* + * There might be in some cases a rounding error of 1px when + * extra space is divided so if there is one then we give the + * first undefined column 1 more pixel + */ + headCells = tHead.iterator(); + colIndex = 0; + while (headCells.hasNext()) { + HeaderCell hc = (HeaderCell) headCells.next(); + if (!hc.isDefinedWidth()) { + setColWidth(colIndex, + hc.getWidth() + availW - checksum, false); + break; + } + colIndex++; + } + } + + if (isDynamicHeight() && totalRows == pageLength) { + // fix body height (may vary if lazy loading is offhorizontal + // scrollbar appears/disappears) + int bodyHeight = scrollBody.getRequiredHeight(); + boolean needsSpaceForHorizontalScrollbar = (availW < usedMinimumWidth); + if (needsSpaceForHorizontalScrollbar) { + bodyHeight += Util.getNativeScrollbarSize(); + } + int heightBefore = getOffsetHeight(); + scrollBodyPanel.setHeight(bodyHeight + "px"); + if (heightBefore != getOffsetHeight()) { + Util.notifyParentOfSizeChange(VScrollTable.this, false); + } + } + scrollBody.reLayoutComponents(); + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); + } + }); + + forceRealignColumnHeaders(); + } + + }; + + private void forceRealignColumnHeaders() { + if (BrowserInfo.get().isIE()) { + /* + * IE does not fire onscroll event if scroll position is reverted to + * 0 due to the content element size growth. Ensure headers are in + * sync with content manually. Safe to use null event as we don't + * actually use the event object in listener. + */ + onScroll(null); + } + } + + /** + * helper to set pixel size of head and body part + * + * @param pixels + */ + private void setContentWidth(int pixels) { + tHead.setWidth(pixels + "px"); + scrollBodyPanel.setWidth(pixels + "px"); + tFoot.setWidth(pixels + "px"); + } + + private int borderWidth = -1; + + /** + * @return border left + border right + */ + private int getBorderWidth() { + if (borderWidth < 0) { + borderWidth = Util.measureHorizontalPaddingAndBorder( + scrollBodyPanel.getElement(), 2); + if (borderWidth < 0) { + borderWidth = 0; + } + } + return borderWidth; + } + + /** + * Ensures scrollable area is properly sized. This method is used when fixed + * size is used. + */ + private int containerHeight; + + private void setContainerHeight() { + if (!isDynamicHeight()) { + containerHeight = getOffsetHeight(); + containerHeight -= showColHeaders ? tHead.getOffsetHeight() : 0; + containerHeight -= tFoot.getOffsetHeight(); + containerHeight -= getContentAreaBorderHeight(); + if (containerHeight < 0) { + containerHeight = 0; + } + scrollBodyPanel.setHeight(containerHeight + "px"); + } + } + + private int contentAreaBorderHeight = -1; + private int scrollLeft; + private int scrollTop; + VScrollTableDropHandler dropHandler; + private boolean navKeyDown; + boolean multiselectPending; + + /** + * @return border top + border bottom of the scrollable area of table + */ + private int getContentAreaBorderHeight() { + if (contentAreaBorderHeight < 0) { + + DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow", + "hidden"); + int oh = scrollBodyPanel.getOffsetHeight(); + int ch = scrollBodyPanel.getElement() + .getPropertyInt("clientHeight"); + contentAreaBorderHeight = oh - ch; + DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow", + "auto"); + } + return contentAreaBorderHeight; + } + + @Override + public void setHeight(String height) { + if (height.length() == 0 + && getElement().getStyle().getHeight().length() != 0) { + /* + * Changing from defined to undefined size -> should do a size init + * to take page length into account again + */ + sizeNeedsInit = true; + } + super.setHeight(height); + } + + void updateHeight() { + setContainerHeight(); + + if (initializedAndAttached) { + updatePageLength(); + } + if (!rendering) { + // Webkit may sometimes get an odd rendering bug (white space + // between header and body), see bug #3875. Running + // overflow hack here to shake body element a bit. + // We must run the fix as a deferred command to prevent it from + // overwriting the scroll position with an outdated value, see + // #7607. + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); + } + }); + } + + triggerLazyColumnAdjustment(false); + + /* + * setting height may affect wheter the component has scrollbars -> + * needs scrolling or not + */ + setProperTabIndex(); + + } + + /* + * Overridden due Table might not survive of visibility change (scroll pos + * lost). Example ITabPanel just set contained components invisible and back + * when changing tabs. + */ + + @Override + public void setVisible(boolean visible) { + if (isVisible() != visible) { + super.setVisible(visible); + if (initializedAndAttached) { + if (visible) { + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + scrollBodyPanel + .setScrollPosition(measureRowHeightOffset(firstRowInViewPort)); + } + }); + } + } + } + } + + /** + * Helper function to build html snippet for column or row headers + * + * @param uidl + * possibly with values caption and icon + * @return html snippet containing possibly an icon + caption text + */ + protected String buildCaptionHtmlSnippet(UIDL uidl) { + String s = uidl.hasAttribute("caption") ? uidl + .getStringAttribute("caption") : ""; + if (uidl.hasAttribute("icon")) { + s = "<img src=\"" + + Util.escapeAttribute(client.translateVaadinUri(uidl + .getStringAttribute("icon"))) + + "\" alt=\"icon\" class=\"v-icon\">" + s; + } + return s; + } + + /** + * This method has logic which rows needs to be requested from server when + * user scrolls + */ + + @Override + public void onScroll(ScrollEvent event) { + scrollLeft = scrollBodyPanel.getElement().getScrollLeft(); + scrollTop = scrollBodyPanel.getScrollPosition(); + /* + * #6970 - IE sometimes fires scroll events for a detached table. + * + * FIXME initializedAndAttached should probably be renamed - its name + * doesn't seem to reflect its semantics. onDetach() doesn't set it to + * false, and changing that might break something else, so we need to + * check isAttached() separately. + */ + if (!initializedAndAttached || !isAttached()) { + return; + } + if (!enabled) { + scrollBodyPanel + .setScrollPosition(measureRowHeightOffset(firstRowInViewPort)); + return; + } + + rowRequestHandler.cancel(); + + if (BrowserInfo.get().isSafari() && event != null && scrollTop == 0) { + // due to the webkitoverflowworkaround, top may sometimes report 0 + // for webkit, although it really is not. Expecting to have the + // correct + // value available soon. + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + onScroll(null); + } + }); + return; + } + + // fix headers horizontal scrolling + tHead.setHorizontalScrollPosition(scrollLeft); + + // fix footers horizontal scrolling + tFoot.setHorizontalScrollPosition(scrollLeft); + + firstRowInViewPort = calcFirstRowInViewPort(); + if (firstRowInViewPort > totalRows - pageLength) { + firstRowInViewPort = totalRows - pageLength; + } + + int postLimit = (int) (firstRowInViewPort + (pageLength - 1) + pageLength + * cache_react_rate); + if (postLimit > totalRows - 1) { + postLimit = totalRows - 1; + } + int preLimit = (int) (firstRowInViewPort - pageLength + * cache_react_rate); + if (preLimit < 0) { + preLimit = 0; + } + final int lastRendered = scrollBody.getLastRendered(); + final int firstRendered = scrollBody.getFirstRendered(); + + if (postLimit <= lastRendered && preLimit >= firstRendered) { + // we're within no-react area, no need to request more rows + // remember which firstvisible we requested, in case the server has + // a differing opinion + lastRequestedFirstvisible = firstRowInViewPort; + client.updateVariable(paintableId, "firstvisible", + firstRowInViewPort, false); + return; + } + + if (firstRowInViewPort - pageLength * cache_rate > lastRendered + || firstRowInViewPort + pageLength + pageLength * cache_rate < firstRendered) { + // need a totally new set of rows + rowRequestHandler + .setReqFirstRow((firstRowInViewPort - (int) (pageLength * cache_rate))); + int last = firstRowInViewPort + (int) (cache_rate * pageLength) + + pageLength - 1; + if (last >= totalRows) { + last = totalRows - 1; + } + rowRequestHandler.setReqRows(last + - rowRequestHandler.getReqFirstRow() + 1); + rowRequestHandler.deferRowFetch(); + return; + } + if (preLimit < firstRendered) { + // need some rows to the beginning of the rendered area + rowRequestHandler + .setReqFirstRow((int) (firstRowInViewPort - pageLength + * cache_rate)); + rowRequestHandler.setReqRows(firstRendered + - rowRequestHandler.getReqFirstRow()); + rowRequestHandler.deferRowFetch(); + + return; + } + if (postLimit > lastRendered) { + // need some rows to the end of the rendered area + rowRequestHandler.setReqFirstRow(lastRendered + 1); + rowRequestHandler.setReqRows((int) ((firstRowInViewPort + + pageLength + pageLength * cache_rate) - lastRendered)); + rowRequestHandler.deferRowFetch(); + } + } + + protected int calcFirstRowInViewPort() { + return (int) Math.ceil(scrollTop / scrollBody.getRowHeight()); + } + + @Override + public VScrollTableDropHandler getDropHandler() { + return dropHandler; + } + + private static class TableDDDetails { + int overkey = -1; + VerticalDropLocation dropLocation; + String colkey; + + @Override + public boolean equals(Object obj) { + if (obj instanceof TableDDDetails) { + TableDDDetails other = (TableDDDetails) obj; + return dropLocation == other.dropLocation + && overkey == other.overkey + && ((colkey != null && colkey.equals(other.colkey)) || (colkey == null && other.colkey == null)); + } + return false; + } + + // + // public int hashCode() { + // return overkey; + // } + } + + public class VScrollTableDropHandler extends VAbstractDropHandler { + + private static final String ROWSTYLEBASE = "v-table-row-drag-"; + private TableDDDetails dropDetails; + private TableDDDetails lastEmphasized; + + @Override + public void dragEnter(VDragEvent drag) { + updateDropDetails(drag); + super.dragEnter(drag); + } + + private void updateDropDetails(VDragEvent drag) { + dropDetails = new TableDDDetails(); + Element elementOver = drag.getElementOver(); + + VScrollTableRow row = Util.findWidget(elementOver, getRowClass()); + if (row != null) { + dropDetails.overkey = row.rowKey; + Element tr = row.getElement(); + Element element = elementOver; + while (element != null && element.getParentElement() != tr) { + element = (Element) element.getParentElement(); + } + int childIndex = DOM.getChildIndex(tr, element); + dropDetails.colkey = tHead.getHeaderCell(childIndex) + .getColKey(); + dropDetails.dropLocation = DDUtil.getVerticalDropLocation( + row.getElement(), drag.getCurrentGwtEvent(), 0.2); + } + + drag.getDropDetails().put("itemIdOver", dropDetails.overkey + ""); + drag.getDropDetails().put( + "detail", + dropDetails.dropLocation != null ? dropDetails.dropLocation + .toString() : null); + + } + + private Class<? extends Widget> getRowClass() { + // get the row type this way to make dd work in derived + // implementations + return scrollBody.iterator().next().getClass(); + } + + @Override + public void dragOver(VDragEvent drag) { + TableDDDetails oldDetails = dropDetails; + updateDropDetails(drag); + if (!oldDetails.equals(dropDetails)) { + deEmphasis(); + final TableDDDetails newDetails = dropDetails; + VAcceptCallback cb = new VAcceptCallback() { + + @Override + public void accepted(VDragEvent event) { + if (newDetails.equals(dropDetails)) { + dragAccepted(event); + } + /* + * Else new target slot already defined, ignore + */ + } + }; + validate(cb, drag); + } + } + + @Override + public void dragLeave(VDragEvent drag) { + deEmphasis(); + super.dragLeave(drag); + } + + @Override + public boolean drop(VDragEvent drag) { + deEmphasis(); + return super.drop(drag); + } + + private void deEmphasis() { + UIObject.setStyleName(getElement(), CLASSNAME + "-drag", false); + if (lastEmphasized == null) { + return; + } + for (Widget w : scrollBody.renderedRows) { + VScrollTableRow row = (VScrollTableRow) w; + if (lastEmphasized != null + && row.rowKey == lastEmphasized.overkey) { + String stylename = ROWSTYLEBASE + + lastEmphasized.dropLocation.toString() + .toLowerCase(); + VScrollTableRow.setStyleName(row.getElement(), stylename, + false); + lastEmphasized = null; + return; + } + } + } + + /** + * TODO needs different drop modes ?? (on cells, on rows), now only + * supports rows + */ + private void emphasis(TableDDDetails details) { + deEmphasis(); + UIObject.setStyleName(getElement(), CLASSNAME + "-drag", true); + // iterate old and new emphasized row + for (Widget w : scrollBody.renderedRows) { + VScrollTableRow row = (VScrollTableRow) w; + if (details != null && details.overkey == row.rowKey) { + String stylename = ROWSTYLEBASE + + details.dropLocation.toString().toLowerCase(); + VScrollTableRow.setStyleName(row.getElement(), stylename, + true); + lastEmphasized = details; + return; + } + } + } + + @Override + protected void dragAccepted(VDragEvent drag) { + emphasis(dropDetails); + } + + @Override + public ComponentConnector getConnector() { + return ConnectorMap.get(client).getConnector(VScrollTable.this); + } + + @Override + public ApplicationConnection getApplicationConnection() { + return client; + } + + } + + protected VScrollTableRow getFocusedRow() { + return focusedRow; + } + + /** + * Moves the selection head to a specific row + * + * @param row + * The row to where the selection head should move + * @return Returns true if focus was moved successfully, else false + */ + public boolean setRowFocus(VScrollTableRow row) { + + if (!isSelectable()) { + return false; + } + + // Remove previous selection + if (focusedRow != null && focusedRow != row) { + focusedRow.removeStyleName(CLASSNAME_SELECTION_FOCUS); + } + + if (row != null) { + + // Apply focus style to new selection + row.addStyleName(CLASSNAME_SELECTION_FOCUS); + + /* + * Trying to set focus on already focused row + */ + if (row == focusedRow) { + return false; + } + + // Set new focused row + focusedRow = row; + + ensureRowIsVisible(row); + + return true; + } + + return false; + } + + /** + * Ensures that the row is visible + * + * @param row + * The row to ensure is visible + */ + private void ensureRowIsVisible(VScrollTableRow row) { + if (BrowserInfo.get().isTouchDevice()) { + // Skip due to android devices that have broken scrolltop will may + // get odd scrolling here. + return; + } + Util.scrollIntoViewVertically(row.getElement()); + } + + /** + * Handles the keyboard events handled by the table + * + * @param event + * The keyboard event received + * @return true iff the navigation event was handled + */ + protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { + if (keycode == KeyCodes.KEY_TAB || keycode == KeyCodes.KEY_SHIFT) { + // Do not handle tab key + return false; + } + + // Down navigation + if (!isSelectable() && keycode == getNavigationDownKey()) { + scrollBodyPanel.setScrollPosition(scrollBodyPanel + .getScrollPosition() + scrollingVelocity); + return true; + } else if (keycode == getNavigationDownKey()) { + if (isMultiSelectModeAny() && moveFocusDown()) { + selectFocusedRow(ctrl, shift); + + } else if (isSingleSelectMode() && !shift && moveFocusDown()) { + selectFocusedRow(ctrl, shift); + } + return true; + } + + // Up navigation + if (!isSelectable() && keycode == getNavigationUpKey()) { + scrollBodyPanel.setScrollPosition(scrollBodyPanel + .getScrollPosition() - scrollingVelocity); + return true; + } else if (keycode == getNavigationUpKey()) { + if (isMultiSelectModeAny() && moveFocusUp()) { + selectFocusedRow(ctrl, shift); + } else if (isSingleSelectMode() && !shift && moveFocusUp()) { + selectFocusedRow(ctrl, shift); + } + return true; + } + + if (keycode == getNavigationLeftKey()) { + // Left navigation + scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel + .getHorizontalScrollPosition() - scrollingVelocity); + return true; + + } else if (keycode == getNavigationRightKey()) { + // Right navigation + scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel + .getHorizontalScrollPosition() + scrollingVelocity); + return true; + } + + // Select navigation + if (isSelectable() && keycode == getNavigationSelectKey()) { + if (isSingleSelectMode()) { + boolean wasSelected = focusedRow.isSelected(); + deselectAll(); + if (!wasSelected || !nullSelectionAllowed) { + focusedRow.toggleSelection(); + } + } else { + focusedRow.toggleSelection(); + removeRowFromUnsentSelectionRanges(focusedRow); + } + + sendSelectedRows(); + return true; + } + + // Page Down navigation + if (keycode == getNavigationPageDownKey()) { + if (isSelectable()) { + /* + * If selectable we plagiate MSW behaviour: first scroll to the + * end of current view. If at the end, scroll down one page + * length and keep the selected row in the bottom part of + * visible area. + */ + if (!isFocusAtTheEndOfTable()) { + VScrollTableRow lastVisibleRowInViewPort = scrollBody + .getRowByRowIndex(firstRowInViewPort + + getFullyVisibleRowCount() - 1); + if (lastVisibleRowInViewPort != null + && lastVisibleRowInViewPort != focusedRow) { + // focused row is not at the end of the table, move + // focus and select the last visible row + setRowFocus(lastVisibleRowInViewPort); + selectFocusedRow(ctrl, shift); + sendSelectedRows(); + } else { + int indexOfToBeFocused = focusedRow.getIndex() + + getFullyVisibleRowCount(); + if (indexOfToBeFocused >= totalRows) { + indexOfToBeFocused = totalRows - 1; + } + VScrollTableRow toBeFocusedRow = scrollBody + .getRowByRowIndex(indexOfToBeFocused); + + if (toBeFocusedRow != null) { + /* + * if the next focused row is rendered + */ + setRowFocus(toBeFocusedRow); + selectFocusedRow(ctrl, shift); + // TODO needs scrollintoview ? + sendSelectedRows(); + } else { + // scroll down by pixels and return, to wait for + // new rows, then select the last item in the + // viewport + selectLastItemInNextRender = true; + multiselectPending = shift; + scrollByPagelenght(1); + } + } + } + } else { + /* No selections, go page down by scrolling */ + scrollByPagelenght(1); + } + return true; + } + + // Page Up navigation + if (keycode == getNavigationPageUpKey()) { + if (isSelectable()) { + /* + * If selectable we plagiate MSW behaviour: first scroll to the + * end of current view. If at the end, scroll down one page + * length and keep the selected row in the bottom part of + * visible area. + */ + if (!isFocusAtTheBeginningOfTable()) { + VScrollTableRow firstVisibleRowInViewPort = scrollBody + .getRowByRowIndex(firstRowInViewPort); + if (firstVisibleRowInViewPort != null + && firstVisibleRowInViewPort != focusedRow) { + // focus is not at the beginning of the table, move + // focus and select the first visible row + setRowFocus(firstVisibleRowInViewPort); + selectFocusedRow(ctrl, shift); + sendSelectedRows(); + } else { + int indexOfToBeFocused = focusedRow.getIndex() + - getFullyVisibleRowCount(); + if (indexOfToBeFocused < 0) { + indexOfToBeFocused = 0; + } + VScrollTableRow toBeFocusedRow = scrollBody + .getRowByRowIndex(indexOfToBeFocused); + + if (toBeFocusedRow != null) { // if the next focused row + // is rendered + setRowFocus(toBeFocusedRow); + selectFocusedRow(ctrl, shift); + // TODO needs scrollintoview ? + sendSelectedRows(); + } else { + // unless waiting for the next rowset already + // scroll down by pixels and return, to wait for + // new rows, then select the last item in the + // viewport + selectFirstItemInNextRender = true; + multiselectPending = shift; + scrollByPagelenght(-1); + } + } + } + } else { + /* No selections, go page up by scrolling */ + scrollByPagelenght(-1); + } + + return true; + } + + // Goto start navigation + if (keycode == getNavigationStartKey()) { + scrollBodyPanel.setScrollPosition(0); + if (isSelectable()) { + if (focusedRow != null && focusedRow.getIndex() == 0) { + return false; + } else { + VScrollTableRow rowByRowIndex = (VScrollTableRow) scrollBody + .iterator().next(); + if (rowByRowIndex.getIndex() == 0) { + setRowFocus(rowByRowIndex); + selectFocusedRow(ctrl, shift); + sendSelectedRows(); + } else { + // first row of table will come in next row fetch + if (ctrl) { + focusFirstItemInNextRender = true; + } else { + selectFirstItemInNextRender = true; + multiselectPending = shift; + } + } + } + } + return true; + } + + // Goto end navigation + if (keycode == getNavigationEndKey()) { + scrollBodyPanel.setScrollPosition(scrollBody.getOffsetHeight()); + if (isSelectable()) { + final int lastRendered = scrollBody.getLastRendered(); + if (lastRendered + 1 == totalRows) { + VScrollTableRow rowByRowIndex = scrollBody + .getRowByRowIndex(lastRendered); + if (focusedRow != rowByRowIndex) { + setRowFocus(rowByRowIndex); + selectFocusedRow(ctrl, shift); + sendSelectedRows(); + } + } else { + if (ctrl) { + focusLastItemInNextRender = true; + } else { + selectLastItemInNextRender = true; + multiselectPending = shift; + } + } + } + return true; + } + + return false; + } + + private boolean isFocusAtTheBeginningOfTable() { + return focusedRow.getIndex() == 0; + } + + private boolean isFocusAtTheEndOfTable() { + return focusedRow.getIndex() + 1 >= totalRows; + } + + private int getFullyVisibleRowCount() { + return (int) (scrollBodyPanel.getOffsetHeight() / scrollBody + .getRowHeight()); + } + + private void scrollByPagelenght(int i) { + int pixels = i * scrollBodyPanel.getOffsetHeight(); + int newPixels = scrollBodyPanel.getScrollPosition() + pixels; + if (newPixels < 0) { + newPixels = 0; + } // else if too high, NOP (all know browsers accept illegally big + // values here) + scrollBodyPanel.setScrollPosition(newPixels); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + + @Override + public void onFocus(FocusEvent event) { + if (isFocusable()) { + hasFocus = true; + + // Focus a row if no row is in focus + if (focusedRow == null) { + focusRowFromBody(); + } else { + setRowFocus(focusedRow); + } + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event + * .dom.client.BlurEvent) + */ + + @Override + public void onBlur(BlurEvent event) { + hasFocus = false; + navKeyDown = false; + + if (BrowserInfo.get().isIE()) { + // IE sometimes moves focus to a clicked table cell... + Element focusedElement = Util.getIEFocusedElement(); + if (Util.getConnectorForElement(client, getParent(), focusedElement) == this) { + // ..in that case, steal the focus back to the focus handler + // but not if focus is in a child component instead (#7965) + focus(); + return; + } + } + + if (isFocusable()) { + // Unfocus any row + setRowFocus(null); + } + } + + /** + * Removes a key from a range if the key is found in a selected range + * + * @param key + * The key to remove + */ + private void removeRowFromUnsentSelectionRanges(VScrollTableRow row) { + Collection<SelectionRange> newRanges = null; + for (Iterator<SelectionRange> iterator = selectedRowRanges.iterator(); iterator + .hasNext();) { + SelectionRange range = iterator.next(); + if (range.inRange(row)) { + // Split the range if given row is in range + Collection<SelectionRange> splitranges = range.split(row); + if (newRanges == null) { + newRanges = new ArrayList<SelectionRange>(); + } + newRanges.addAll(splitranges); + iterator.remove(); + } + } + if (newRanges != null) { + selectedRowRanges.addAll(newRanges); + } + } + + /** + * Can the Table be focused? + * + * @return True if the table can be focused, else false + */ + public boolean isFocusable() { + if (scrollBody != null && enabled) { + return !(!hasHorizontalScrollbar() && !hasVerticalScrollbar() && !isSelectable()); + } + return false; + } + + private boolean hasHorizontalScrollbar() { + return scrollBody.getOffsetWidth() > scrollBodyPanel.getOffsetWidth(); + } + + private boolean hasVerticalScrollbar() { + return scrollBody.getOffsetHeight() > scrollBodyPanel.getOffsetHeight(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Focusable#focus() + */ + + @Override + public void focus() { + if (isFocusable()) { + scrollBodyPanel.focus(); + } + } + + /** + * Sets the proper tabIndex for scrollBodyPanel (the focusable elemen in the + * component). + * + * If the component has no explicit tabIndex a zero is given (default + * tabbing order based on dom hierarchy) or -1 if the component does not + * need to gain focus. The component needs no focus if it has no scrollabars + * (not scrollable) and not selectable. Note that in the future shortcut + * actions may need focus. + * + */ + void setProperTabIndex() { + int storedScrollTop = 0; + int storedScrollLeft = 0; + + if (BrowserInfo.get().getOperaVersion() >= 11) { + // Workaround for Opera scroll bug when changing tabIndex (#6222) + storedScrollTop = scrollBodyPanel.getScrollPosition(); + storedScrollLeft = scrollBodyPanel.getHorizontalScrollPosition(); + } + + if (tabIndex == 0 && !isFocusable()) { + scrollBodyPanel.setTabIndex(-1); + } else { + scrollBodyPanel.setTabIndex(tabIndex); + } + + if (BrowserInfo.get().getOperaVersion() >= 11) { + // Workaround for Opera scroll bug when changing tabIndex (#6222) + scrollBodyPanel.setScrollPosition(storedScrollTop); + scrollBodyPanel.setHorizontalScrollPosition(storedScrollLeft); + } + } + + public void startScrollingVelocityTimer() { + if (scrollingVelocityTimer == null) { + scrollingVelocityTimer = new Timer() { + + @Override + public void run() { + scrollingVelocity++; + } + }; + scrollingVelocityTimer.scheduleRepeating(100); + } + } + + public void cancelScrollingVelocityTimer() { + if (scrollingVelocityTimer != null) { + // Remove velocityTimer if it exists and the Table is disabled + scrollingVelocityTimer.cancel(); + scrollingVelocityTimer = null; + scrollingVelocity = 10; + } + } + + /** + * + * @param keyCode + * @return true if the given keyCode is used by the table for navigation + */ + private boolean isNavigationKey(int keyCode) { + return keyCode == getNavigationUpKey() + || keyCode == getNavigationLeftKey() + || keyCode == getNavigationRightKey() + || keyCode == getNavigationDownKey() + || keyCode == getNavigationPageUpKey() + || keyCode == getNavigationPageDownKey() + || keyCode == getNavigationEndKey() + || keyCode == getNavigationStartKey(); + } + + public void lazyRevertFocusToRow(final VScrollTableRow currentlyFocusedRow) { + Scheduler.get().scheduleFinally(new ScheduledCommand() { + + @Override + public void execute() { + if (currentlyFocusedRow != null) { + setRowFocus(currentlyFocusedRow); + } else { + VConsole.log("no row?"); + focusRowFromBody(); + } + scrollBody.ensureFocus(); + } + }); + } + + @Override + public Action[] getActions() { + if (bodyActionKeys == null) { + return new Action[] {}; + } + final Action[] actions = new Action[bodyActionKeys.length]; + for (int i = 0; i < actions.length; i++) { + final String actionKey = bodyActionKeys[i]; + Action bodyAction = new TreeAction(this, null, actionKey); + bodyAction.setCaption(getActionCaption(actionKey)); + bodyAction.setIconUrl(getActionIcon(actionKey)); + actions[i] = bodyAction; + } + return actions; + } + + @Override + public ApplicationConnection getClient() { + return client; + } + + @Override + public String getPaintableId() { + return paintableId; + } + + /** + * Add this to the element mouse down event by using element.setPropertyJSO + * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it then again + * when the mouse is depressed in the mouse up event. + * + * @return Returns the JSO preventing text selection + */ + private static native JavaScriptObject getPreventTextSelectionIEHack() + /*-{ + return function(){ return false; }; + }-*/; + + public void triggerLazyColumnAdjustment(boolean now) { + lazyAdjustColumnWidths.cancel(); + if (now) { + lazyAdjustColumnWidths.run(); + } else { + lazyAdjustColumnWidths.schedule(LAZY_COLUMN_ADJUST_TIMEOUT); + } + } + + private boolean isDynamicWidth() { + ComponentConnector paintable = ConnectorMap.get(client).getConnector( + this); + return paintable.isUndefinedWidth(); + } + + private boolean isDynamicHeight() { + ComponentConnector paintable = ConnectorMap.get(client).getConnector( + this); + if (paintable == null) { + // This should be refactored. As isDynamicHeight can be called from + // a timer it is possible that the connector has been unregistered + // when this method is called, causing getConnector to return null. + return false; + } + return paintable.isUndefinedHeight(); + } + + private void debug(String msg) { + if (enableDebug) { + VConsole.error(msg); + } + } + + public Widget getWidgetForPaintable() { + return this; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetBaseConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetBaseConnector.java new file mode 100644 index 0000000000..e8a1c709c4 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetBaseConnector.java @@ -0,0 +1,107 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.tabsheet; + +import java.util.ArrayList; +import java.util.Iterator; + +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.tabsheet.TabsheetBaseConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector; + +public abstract class TabsheetBaseConnector extends + AbstractComponentContainerConnector implements Paintable { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + getWidget().client = client; + + if (!isRealUpdate(uidl)) { + return; + } + + // Update member references + getWidget().id = uidl.getId(); + getWidget().disabled = !isEnabled(); + + // Render content + final UIDL tabs = uidl.getChildUIDL(0); + + // Widgets in the TabSheet before update + ArrayList<Widget> oldWidgets = new ArrayList<Widget>(); + for (Iterator<Widget> iterator = getWidget().getWidgetIterator(); iterator + .hasNext();) { + oldWidgets.add(iterator.next()); + } + + // Clear previous values + getWidget().tabKeys.clear(); + getWidget().disabledTabKeys.clear(); + + int index = 0; + for (final Iterator<Object> it = tabs.getChildIterator(); it.hasNext();) { + final UIDL tab = (UIDL) it.next(); + final String key = tab.getStringAttribute("key"); + final boolean selected = tab.getBooleanAttribute("selected"); + final boolean hidden = tab.getBooleanAttribute("hidden"); + + if (tab.getBooleanAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_DISABLED)) { + getWidget().disabledTabKeys.add(key); + } + + getWidget().tabKeys.add(key); + + if (selected) { + getWidget().activeTabIndex = index; + } + getWidget().renderTab(tab, index, selected, hidden); + index++; + } + + int tabCount = getWidget().getTabCount(); + while (tabCount-- > index) { + getWidget().removeTab(index); + } + + for (int i = 0; i < getWidget().getTabCount(); i++) { + ComponentConnector p = getWidget().getTab(i); + // null for PlaceHolder widgets + if (p != null) { + oldWidgets.remove(p.getWidget()); + } + } + + // Detach any old tab widget, should be max 1 + for (Iterator<Widget> iterator = oldWidgets.iterator(); iterator + .hasNext();) { + Widget oldWidget = iterator.next(); + if (oldWidget.isAttached()) { + oldWidget.removeFromParent(); + } + } + + } + + @Override + public VTabsheetBase getWidget() { + return (VTabsheetBase) super.getWidget(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetConnector.java new file mode 100644 index 0000000000..76c56ba2ed --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetConnector.java @@ -0,0 +1,141 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.tabsheet; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.DOM; +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.TooltipInfo; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout; +import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren; +import com.vaadin.ui.TabSheet; + +@Connect(TabSheet.class) +public class TabsheetConnector extends TabsheetBaseConnector implements + SimpleManagedLayout, MayScrollChildren { + + // Can't use "style" as it's already in use + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + + if (isRealUpdate(uidl)) { + // Handle stylename changes before generics (might affect size + // calculations) + getWidget().handleStyleNames(uidl, getState()); + } + + super.updateFromUIDL(uidl, client); + if (!isRealUpdate(uidl)) { + return; + } + + // tabs; push or not + if (!isUndefinedWidth()) { + DOM.setStyleAttribute(getWidget().tabs, "overflow", "hidden"); + } else { + getWidget().showAllTabs(); + DOM.setStyleAttribute(getWidget().tabs, "width", ""); + DOM.setStyleAttribute(getWidget().tabs, "overflow", "visible"); + getWidget().updateDynamicWidth(); + } + + if (!isUndefinedHeight()) { + // Must update height after the styles have been set + getWidget().updateContentNodeHeight(); + getWidget().updateOpenTabSize(); + } + + getWidget().iLayout(); + + // Re run relative size update to ensure optimal scrollbars + // TODO isolate to situation that visible tab has undefined height + try { + client.handleComponentRelativeSize(getWidget().tp + .getWidget(getWidget().tp.getVisibleWidget())); + } catch (Exception e) { + // Ignore, most likely empty tabsheet + } + + getWidget().waitingForResponse = false; + } + + @Override + public VTabsheet getWidget() { + return (VTabsheet) super.getWidget(); + } + + @Override + public void updateCaption(ComponentConnector component) { + /* Tabsheet does not render its children's captions */ + } + + @Override + public void layout() { + VTabsheet tabsheet = getWidget(); + + tabsheet.updateContentNodeHeight(); + + if (isUndefinedWidth()) { + tabsheet.contentNode.getStyle().setProperty("width", ""); + } else { + int contentWidth = tabsheet.getOffsetWidth() + - tabsheet.getContentAreaBorderWidth(); + if (contentWidth < 0) { + contentWidth = 0; + } + tabsheet.contentNode.getStyle().setProperty("width", + contentWidth + "px"); + } + + tabsheet.updateOpenTabSize(); + if (isUndefinedWidth()) { + tabsheet.updateDynamicWidth(); + } + + tabsheet.iLayout(); + + } + + @Override + public TooltipInfo getTooltipInfo(Element element) { + + TooltipInfo info = null; + + // Find a tooltip for the tab, if the element is a tab + if (element != getWidget().getElement()) { + Object node = Util.findWidget( + (com.google.gwt.user.client.Element) element, + VTabsheet.TabCaption.class); + + if (node != null) { + VTabsheet.TabCaption caption = (VTabsheet.TabCaption) node; + info = caption.getTooltipInfo(); + } + } + + // If not tab tooltip was found, use the default + if (info == null) { + info = super.getTooltipInfo(element); + } + + return info; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheet.java b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheet.java new file mode 100644 index 0000000000..bec36aed4b --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheet.java @@ -0,0 +1,1250 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.tabsheet; + +import java.util.Iterator; +import java.util.List; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Visibility; +import com.google.gwt.dom.client.TableCellElement; +import com.google.gwt.dom.client.TableElement; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.dom.client.HasKeyDownHandlers; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Command; +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.ui.ComplexPanel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.shared.ComponentState; +import com.vaadin.shared.EventId; +import com.vaadin.shared.ui.tabsheet.TabsheetBaseConstants; +import com.vaadin.shared.ui.tabsheet.TabsheetConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.TooltipInfo; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.ui.label.VLabel; + +public class VTabsheet extends VTabsheetBase implements Focusable, + FocusHandler, BlurHandler, KeyDownHandler { + + private static class VCloseEvent { + private Tab tab; + + VCloseEvent(Tab tab) { + this.tab = tab; + } + + public Tab getTab() { + return tab; + } + + } + + private interface VCloseHandler { + public void onClose(VCloseEvent event); + } + + /** + * Representation of a single "tab" shown in the TabBar + * + */ + private static class Tab extends SimplePanel implements HasFocusHandlers, + HasBlurHandlers, HasKeyDownHandlers { + private static final String TD_CLASSNAME = CLASSNAME + "-tabitemcell"; + private static final String TD_FIRST_CLASSNAME = TD_CLASSNAME + + "-first"; + private static final String TD_SELECTED_CLASSNAME = TD_CLASSNAME + + "-selected"; + private static final String TD_SELECTED_FIRST_CLASSNAME = TD_SELECTED_CLASSNAME + + "-first"; + private static final String TD_DISABLED_CLASSNAME = TD_CLASSNAME + + "-disabled"; + + private static final String DIV_CLASSNAME = CLASSNAME + "-tabitem"; + private static final String DIV_SELECTED_CLASSNAME = DIV_CLASSNAME + + "-selected"; + + private TabCaption tabCaption; + Element td = getElement(); + private VCloseHandler closeHandler; + + private boolean enabledOnServer = true; + private Element div; + private TabBar tabBar; + private boolean hiddenOnServer = false; + + private String styleName; + + private Tab(TabBar tabBar) { + super(DOM.createTD()); + this.tabBar = tabBar; + setStyleName(td, TD_CLASSNAME); + + div = DOM.createDiv(); + focusImpl.setTabIndex(td, -1); + setStyleName(div, DIV_CLASSNAME); + + DOM.appendChild(td, div); + + tabCaption = new TabCaption(this, getTabsheet() + .getApplicationConnection()); + add(tabCaption); + + addFocusHandler(getTabsheet()); + addBlurHandler(getTabsheet()); + addKeyDownHandler(getTabsheet()); + } + + public boolean isHiddenOnServer() { + return hiddenOnServer; + } + + public void setHiddenOnServer(boolean hiddenOnServer) { + this.hiddenOnServer = hiddenOnServer; + } + + @Override + protected Element getContainerElement() { + // Attach caption element to div, not td + return div; + } + + public boolean isEnabledOnServer() { + return enabledOnServer; + } + + public void setEnabledOnServer(boolean enabled) { + enabledOnServer = enabled; + setStyleName(td, TD_DISABLED_CLASSNAME, !enabled); + if (!enabled) { + focusImpl.setTabIndex(td, -1); + } + } + + public void addClickHandler(ClickHandler handler) { + tabCaption.addClickHandler(handler); + } + + public void setCloseHandler(VCloseHandler closeHandler) { + this.closeHandler = closeHandler; + } + + /** + * Toggles the style names for the Tab + * + * @param selected + * true if the Tab is selected + * @param first + * true if the Tab is the first visible Tab + */ + public void setStyleNames(boolean selected, boolean first) { + setStyleName(td, TD_FIRST_CLASSNAME, first); + setStyleName(td, TD_SELECTED_CLASSNAME, selected); + setStyleName(td, TD_SELECTED_FIRST_CLASSNAME, selected && first); + setStyleName(div, DIV_SELECTED_CLASSNAME, selected); + } + + public void setTabulatorIndex(int tabIndex) { + focusImpl.setTabIndex(td, tabIndex); + } + + public boolean isClosable() { + return tabCaption.isClosable(); + } + + public void onClose() { + closeHandler.onClose(new VCloseEvent(this)); + } + + public VTabsheet getTabsheet() { + return tabBar.getTabsheet(); + } + + public void updateFromUIDL(UIDL tabUidl) { + tabCaption.updateCaption(tabUidl); + + // Apply the styleName set for the tab + String newStyleName = tabUidl + .getStringAttribute(TabsheetConstants.TAB_STYLE_NAME); + // Find the nth td element + if (newStyleName != null && newStyleName.length() != 0) { + if (!newStyleName.equals(styleName)) { + // If we have a new style name + if (styleName != null && styleName.length() != 0) { + // Remove old style name if present + td.removeClassName(TD_CLASSNAME + "-" + styleName); + } + // Set new style name + td.addClassName(TD_CLASSNAME + "-" + newStyleName); + styleName = newStyleName; + } + } else if (styleName != null) { + // Remove the set stylename if no stylename is present in the + // uidl + td.removeClassName(TD_CLASSNAME + "-" + styleName); + styleName = null; + } + } + + public void recalculateCaptionWidth() { + tabCaption.setWidth(tabCaption.getRequiredWidth() + "px"); + } + + @Override + public HandlerRegistration addFocusHandler(FocusHandler handler) { + return addDomHandler(handler, FocusEvent.getType()); + } + + @Override + public HandlerRegistration addBlurHandler(BlurHandler handler) { + return addDomHandler(handler, BlurEvent.getType()); + } + + @Override + public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { + return addDomHandler(handler, KeyDownEvent.getType()); + } + + public void focus() { + focusImpl.focus(td); + } + + public void blur() { + focusImpl.blur(td); + } + } + + public static class TabCaption extends VCaption { + + 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; + } + + public boolean updateCaption(UIDL uidl) { + if (uidl.hasAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_DESCRIPTION)) { + setTooltipInfo(new TooltipInfo( + uidl.getStringAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_DESCRIPTION), + uidl.getStringAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_ERROR_MESSAGE))); + } else { + setTooltipInfo(null); + } + + // TODO need to call this instead of super because the caption does + // not have an owner + boolean ret = updateCaptionWithoutOwner( + uidl.getStringAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_CAPTION), + uidl.hasAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_DISABLED), + uidl.hasAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_DESCRIPTION), + uidl.hasAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_ERROR_MESSAGE), + uidl.getStringAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_ICON)); + + setClosable(uidl.hasAttribute("closable")); + + return ret; + } + + private VTabsheet getTabsheet() { + return tab.getTabsheet(); + } + + @Override + public void onBrowserEvent(Event event) { + if (closable && event.getTypeInt() == Event.ONCLICK + && event.getEventTarget().cast() == closeButton) { + tab.onClose(); + event.stopPropagation(); + event.preventDefault(); + } + + super.onBrowserEvent(event); + + if (event.getTypeInt() == Event.ONLOAD) { + getTabsheet().tabSizeMightHaveChanged(getTab()); + } + } + + public Tab getTab() { + return tab; + } + + public void setClosable(boolean closable) { + this.closable = closable; + if (closable && closeButton == null) { + closeButton = DOM.createSpan(); + closeButton.setInnerHTML("×"); + closeButton + .setClassName(VTabsheet.CLASSNAME + "-caption-close"); + getElement().insertBefore(closeButton, + getElement().getLastChild()); + } else if (!closable && closeButton != null) { + getElement().removeChild(closeButton); + closeButton = null; + } + if (closable) { + addStyleDependentName("closable"); + } else { + removeStyleDependentName("closable"); + } + } + + public boolean isClosable() { + return closable; + } + + @Override + public int getRequiredWidth() { + int width = super.getRequiredWidth(); + if (closeButton != null) { + width += Util.getRequiredWidth(closeButton); + } + return width; + } + + public Element getCloseButton() { + return closeButton; + } + + } + + static class TabBar extends ComplexPanel implements ClickHandler, + VCloseHandler { + + private final Element tr = DOM.createTR(); + + private final Element spacerTd = DOM.createTD(); + + private Tab selected; + + private VTabsheet tabsheet; + + TabBar(VTabsheet tabsheet) { + this.tabsheet = tabsheet; + + Element el = DOM.createTable(); + Element tbody = DOM.createTBody(); + DOM.appendChild(el, tbody); + DOM.appendChild(tbody, tr); + setStyleName(spacerTd, CLASSNAME + "-spacertd"); + DOM.appendChild(tr, spacerTd); + DOM.appendChild(spacerTd, DOM.createDiv()); + setElement(el); + } + + @Override + public void onClose(VCloseEvent event) { + Tab tab = event.getTab(); + if (!tab.isEnabledOnServer()) { + return; + } + int tabIndex = getWidgetIndex(tab); + getTabsheet().sendTabClosedEvent(tabIndex); + } + + protected Element getContainerElement() { + return tr; + } + + public int getTabCount() { + return getWidgetCount(); + } + + public Tab addTab() { + Tab t = new Tab(this); + int tabIndex = getTabCount(); + + // Logical attach + insert(t, tr, tabIndex, true); + + if (tabIndex == 0) { + // Set the "first" style + t.setStyleNames(false, true); + } + + t.addClickHandler(this); + t.setCloseHandler(this); + + return t; + } + + @Override + public void onClick(ClickEvent event) { + TabCaption caption = (TabCaption) event.getSource(); + Element targetElement = event.getNativeEvent().getEventTarget() + .cast(); + // the tab should not be focused if the close button was clicked + if (targetElement == caption.getCloseButton()) { + return; + } + + int index = getWidgetIndex(caption.getParent()); + // IE needs explicit focus() + if (BrowserInfo.get().isIE()) { + getTabsheet().focus(); + } + getTabsheet().onTabSelected(index); + } + + public VTabsheet getTabsheet() { + return tabsheet; + } + + public Tab getTab(int index) { + if (index < 0 || index >= getTabCount()) { + return null; + } + return (Tab) super.getWidget(index); + } + + public void selectTab(int index) { + final Tab newSelected = getTab(index); + final Tab oldSelected = selected; + + newSelected.setStyleNames(true, isFirstVisibleTab(index)); + newSelected.setTabulatorIndex(getTabsheet().tabulatorIndex); + + if (oldSelected != null && oldSelected != newSelected) { + oldSelected.setStyleNames(false, + isFirstVisibleTab(getWidgetIndex(oldSelected))); + oldSelected.setTabulatorIndex(-1); + } + + // Update the field holding the currently selected tab + selected = newSelected; + + // The selected tab might need more (or less) space + newSelected.recalculateCaptionWidth(); + getTab(tabsheet.activeTabIndex).recalculateCaptionWidth(); + } + + public void removeTab(int i) { + Tab tab = getTab(i); + if (tab == null) { + return; + } + + remove(tab); + + /* + * If this widget was selected we need to unmark it as the last + * selected + */ + if (tab == selected) { + selected = null; + } + + // FIXME: Shouldn't something be selected instead? + } + + private boolean isFirstVisibleTab(int index) { + return getFirstVisibleTab() == index; + } + + /** + * Returns the index of the first visible tab + * + * @return + */ + private int getFirstVisibleTab() { + return getNextVisibleTab(-1); + } + + /** + * Find the next visible tab. Returns -1 if none is found. + * + * @param i + * @return + */ + private int getNextVisibleTab(int i) { + int tabs = getTabCount(); + do { + i++; + } while (i < tabs && getTab(i).isHiddenOnServer()); + + if (i == tabs) { + return -1; + } else { + return i; + } + } + + /** + * Find the previous visible tab. Returns -1 if none is found. + * + * @param i + * @return + */ + private int getPreviousVisibleTab(int i) { + do { + i--; + } while (i >= 0 && getTab(i).isHiddenOnServer()); + + return i; + + } + + public int scrollLeft(int currentFirstVisible) { + int prevVisible = getPreviousVisibleTab(currentFirstVisible); + if (prevVisible == -1) { + return -1; + } + + Tab newFirst = getTab(prevVisible); + newFirst.setVisible(true); + newFirst.recalculateCaptionWidth(); + + return prevVisible; + } + + public int scrollRight(int currentFirstVisible) { + int nextVisible = getNextVisibleTab(currentFirstVisible); + if (nextVisible == -1) { + return -1; + } + Tab currentFirst = getTab(currentFirstVisible); + currentFirst.setVisible(false); + currentFirst.recalculateCaptionWidth(); + return nextVisible; + } + } + + public static final String CLASSNAME = "v-tabsheet"; + + public static final String TABS_CLASSNAME = "v-tabsheet-tabcontainer"; + public static final String SCROLLER_CLASSNAME = "v-tabsheet-scroller"; + + final Element tabs; // tabbar and 'scroller' container + Tab focusedTab; + /** + * The tabindex property (position in the browser's focus cycle.) Named like + * this to avoid confusion with activeTabIndex. + */ + int tabulatorIndex = 0; + + private static final FocusImpl focusImpl = FocusImpl.getFocusImplForPanel(); + + private final Element scroller; // tab-scroller element + private final Element scrollerNext; // tab-scroller next button element + private final Element scrollerPrev; // tab-scroller prev button element + + /** + * The index of the first visible tab (when scrolled) + */ + private int scrollerIndex = 0; + + final TabBar tb = new TabBar(this); + final VTabsheetPanel tp = new VTabsheetPanel(); + final Element contentNode; + + private final Element deco; + + boolean waitingForResponse; + + private String currentStyle; + + /** + * @return Whether the tab could be selected or not. + */ + private boolean onTabSelected(final int tabIndex) { + Tab tab = tb.getTab(tabIndex); + if (client == null || disabled || waitingForResponse) { + return false; + } + if (!tab.isEnabledOnServer() || tab.isHiddenOnServer()) { + return false; + } + if (activeTabIndex != tabIndex) { + tb.selectTab(tabIndex); + + // If this TabSheet already has focus, set the new selected tab + // as focused. + if (focusedTab != null) { + focusedTab = tab; + } + + addStyleDependentName("loading"); + // Hide the current contents so a loading indicator can be shown + // instead + Widget currentlyDisplayedWidget = tp.getWidget(tp + .getVisibleWidget()); + currentlyDisplayedWidget.getElement().getParentElement().getStyle() + .setVisibility(Visibility.HIDDEN); + client.updateVariable(id, "selected", tabKeys.get(tabIndex) + .toString(), true); + waitingForResponse = true; + } + // Note that we return true when tabIndex == activeTabIndex; the active + // tab could be selected, it's just a no-op. + return true; + } + + public ApplicationConnection getApplicationConnection() { + return client; + } + + public void tabSizeMightHaveChanged(Tab tab) { + // icon onloads may change total width of tabsheet + if (isDynamicWidth()) { + updateDynamicWidth(); + } + updateTabScroller(); + + } + + void sendTabClosedEvent(int tabIndex) { + client.updateVariable(id, "close", tabKeys.get(tabIndex), true); + } + + boolean isDynamicWidth() { + ComponentConnector paintable = ConnectorMap.get(client).getConnector( + this); + return paintable.isUndefinedWidth(); + } + + boolean isDynamicHeight() { + ComponentConnector paintable = ConnectorMap.get(client).getConnector( + this); + return paintable.isUndefinedHeight(); + } + + public VTabsheet() { + super(CLASSNAME); + + addHandler(this, FocusEvent.getType()); + addHandler(this, BlurEvent.getType()); + + // Tab scrolling + DOM.setStyleAttribute(getElement(), "overflow", "hidden"); + tabs = DOM.createDiv(); + DOM.setElementProperty(tabs, "className", TABS_CLASSNAME); + scroller = DOM.createDiv(); + + DOM.setElementProperty(scroller, "className", SCROLLER_CLASSNAME); + scrollerPrev = DOM.createButton(); + DOM.setElementProperty(scrollerPrev, "className", SCROLLER_CLASSNAME + + "Prev"); + DOM.sinkEvents(scrollerPrev, Event.ONCLICK); + scrollerNext = DOM.createButton(); + DOM.setElementProperty(scrollerNext, "className", SCROLLER_CLASSNAME + + "Next"); + DOM.sinkEvents(scrollerNext, Event.ONCLICK); + DOM.appendChild(getElement(), tabs); + + // Tabs + tp.setStyleName(CLASSNAME + "-tabsheetpanel"); + contentNode = DOM.createDiv(); + + deco = DOM.createDiv(); + + addStyleDependentName("loading"); // Indicate initial progress + tb.setStyleName(CLASSNAME + "-tabs"); + DOM.setElementProperty(contentNode, "className", CLASSNAME + "-content"); + DOM.setElementProperty(deco, "className", CLASSNAME + "-deco"); + + add(tb, tabs); + DOM.appendChild(scroller, scrollerPrev); + DOM.appendChild(scroller, scrollerNext); + + DOM.appendChild(getElement(), contentNode); + add(tp, contentNode); + DOM.appendChild(getElement(), deco); + + DOM.appendChild(tabs, scroller); + + // TODO Use for Safari only. Fix annoying 1px first cell in TabBar. + // DOM.setStyleAttribute(DOM.getFirstChild(DOM.getFirstChild(DOM + // .getFirstChild(tb.getElement()))), "display", "none"); + + } + + @Override + public void onBrowserEvent(Event event) { + if (event.getTypeInt() == Event.ONCLICK) { + // Tab scrolling + if (isScrolledTabs() && DOM.eventGetTarget(event) == scrollerPrev) { + int newFirstIndex = tb.scrollLeft(scrollerIndex); + if (newFirstIndex != -1) { + scrollerIndex = newFirstIndex; + updateTabScroller(); + } + return; + } else if (isClippedTabs() + && DOM.eventGetTarget(event) == scrollerNext) { + int newFirstIndex = tb.scrollRight(scrollerIndex); + + if (newFirstIndex != -1) { + scrollerIndex = newFirstIndex; + updateTabScroller(); + } + return; + } + } + super.onBrowserEvent(event); + } + + /** + * Checks if the tab with the selected index has been scrolled out of the + * view (on the left side). + * + * @param index + * @return + */ + private boolean scrolledOutOfView(int index) { + return scrollerIndex > index; + } + + void handleStyleNames(UIDL uidl, ComponentState state) { + // Add proper stylenames for all elements (easier to prevent unwanted + // style inheritance) + if (state.hasStyles()) { + final List<String> styles = state.getStyles(); + if (!currentStyle.equals(styles.toString())) { + currentStyle = styles.toString(); + final String tabsBaseClass = TABS_CLASSNAME; + String tabsClass = tabsBaseClass; + final String contentBaseClass = CLASSNAME + "-content"; + String contentClass = contentBaseClass; + final String decoBaseClass = CLASSNAME + "-deco"; + String decoClass = decoBaseClass; + for (String style : styles) { + tb.addStyleDependentName(style); + tabsClass += " " + tabsBaseClass + "-" + style; + contentClass += " " + contentBaseClass + "-" + style; + decoClass += " " + decoBaseClass + "-" + style; + } + DOM.setElementProperty(tabs, "className", tabsClass); + DOM.setElementProperty(contentNode, "className", contentClass); + DOM.setElementProperty(deco, "className", decoClass); + borderW = -1; + } + } else { + tb.setStyleName(CLASSNAME + "-tabs"); + DOM.setElementProperty(tabs, "className", TABS_CLASSNAME); + DOM.setElementProperty(contentNode, "className", CLASSNAME + + "-content"); + DOM.setElementProperty(deco, "className", CLASSNAME + "-deco"); + } + + if (uidl.hasAttribute("hidetabs")) { + tb.setVisible(false); + addStyleName(CLASSNAME + "-hidetabs"); + } else { + tb.setVisible(true); + removeStyleName(CLASSNAME + "-hidetabs"); + } + } + + void updateDynamicWidth() { + // Find width consumed by tabs + TableCellElement spacerCell = ((TableElement) tb.getElement().cast()) + .getRows().getItem(0).getCells().getItem(tb.getTabCount()); + + int spacerWidth = spacerCell.getOffsetWidth(); + DivElement div = (DivElement) spacerCell.getFirstChildElement(); + + int spacerMinWidth = spacerCell.getOffsetWidth() - div.getOffsetWidth(); + + int tabsWidth = tb.getOffsetWidth() - spacerWidth + spacerMinWidth; + + // Find content width + Style style = tp.getElement().getStyle(); + String overflow = style.getProperty("overflow"); + style.setProperty("overflow", "hidden"); + style.setPropertyPx("width", tabsWidth); + + boolean hasTabs = tp.getWidgetCount() > 0; + + Style wrapperstyle = null; + if (hasTabs) { + wrapperstyle = tp.getWidget(tp.getVisibleWidget()).getElement() + .getParentElement().getStyle(); + wrapperstyle.setPropertyPx("width", tabsWidth); + } + // Get content width from actual widget + + int contentWidth = 0; + if (hasTabs) { + contentWidth = tp.getWidget(tp.getVisibleWidget()).getOffsetWidth(); + } + style.setProperty("overflow", overflow); + + // Set widths to max(tabs,content) + if (tabsWidth < contentWidth) { + tabsWidth = contentWidth; + } + + int outerWidth = tabsWidth + getContentAreaBorderWidth(); + + tabs.getStyle().setPropertyPx("width", outerWidth); + style.setPropertyPx("width", tabsWidth); + if (hasTabs) { + wrapperstyle.setPropertyPx("width", tabsWidth); + } + + contentNode.getStyle().setPropertyPx("width", tabsWidth); + super.setWidth(outerWidth + "px"); + updateOpenTabSize(); + } + + @Override + protected void renderTab(final UIDL tabUidl, int index, boolean selected, + boolean hidden) { + Tab tab = tb.getTab(index); + if (tab == null) { + tab = tb.addTab(); + } + tab.updateFromUIDL(tabUidl); + tab.setEnabledOnServer((!disabledTabKeys.contains(tabKeys.get(index)))); + tab.setHiddenOnServer(hidden); + + if (scrolledOutOfView(index)) { + // Should not set tabs visible if they are scrolled out of view + hidden = true; + } + // Set the current visibility of the tab (in the browser) + tab.setVisible(!hidden); + + /* + * Force the width of the caption container so the content will not wrap + * and tabs won't be too narrow in certain browsers + */ + tab.recalculateCaptionWidth(); + + UIDL tabContentUIDL = null; + ComponentConnector tabContentPaintable = null; + Widget tabContentWidget = null; + if (tabUidl.getChildCount() > 0) { + tabContentUIDL = tabUidl.getChildUIDL(0); + tabContentPaintable = client.getPaintable(tabContentUIDL); + tabContentWidget = tabContentPaintable.getWidget(); + } + + if (tabContentPaintable != null) { + /* This is a tab with content information */ + + int oldIndex = tp.getWidgetIndex(tabContentWidget); + if (oldIndex != -1 && oldIndex != index) { + /* + * The tab has previously been rendered in another position so + * we must move the cached content to correct position + */ + tp.insert(tabContentWidget, index); + } + } else { + /* A tab whose content has not yet been loaded */ + + /* + * Make sure there is a corresponding empty tab in tp. The same + * operation as the moving above but for not-loaded tabs. + */ + if (index < tp.getWidgetCount()) { + Widget oldWidget = tp.getWidget(index); + if (!(oldWidget instanceof PlaceHolder)) { + tp.insert(new PlaceHolder(), index); + } + } + + } + + if (selected) { + renderContent(tabContentUIDL); + tb.selectTab(index); + } else { + if (tabContentUIDL != null) { + // updating a drawn child on hidden tab + if (tp.getWidgetIndex(tabContentWidget) < 0) { + tp.insert(tabContentWidget, index); + } + } else if (tp.getWidgetCount() <= index) { + tp.add(new PlaceHolder()); + } + } + } + + public class PlaceHolder extends VLabel { + public PlaceHolder() { + super(""); + } + } + + @Override + protected void selectTab(int index, final UIDL contentUidl) { + if (index != activeTabIndex) { + activeTabIndex = index; + tb.selectTab(activeTabIndex); + } + renderContent(contentUidl); + } + + private void renderContent(final UIDL contentUIDL) { + final ComponentConnector content = client.getPaintable(contentUIDL); + Widget newWidget = content.getWidget(); + if (tp.getWidgetCount() > activeTabIndex) { + Widget old = tp.getWidget(activeTabIndex); + if (old != newWidget) { + tp.remove(activeTabIndex); + ConnectorMap paintableMap = ConnectorMap.get(client); + if (paintableMap.isConnector(old)) { + paintableMap.unregisterConnector(paintableMap + .getConnector(old)); + } + tp.insert(content.getWidget(), activeTabIndex); + } + } else { + tp.add(content.getWidget()); + } + + tp.showWidget(activeTabIndex); + + VTabsheet.this.iLayout(); + /* + * The size of a cached, relative sized component must be updated to + * report correct size to updateOpenTabSize(). + */ + if (contentUIDL.getBooleanAttribute("cached")) { + client.handleComponentRelativeSize(content.getWidget()); + } + updateOpenTabSize(); + VTabsheet.this.removeStyleDependentName("loading"); + } + + void updateContentNodeHeight() { + if (!isDynamicHeight()) { + int contentHeight = getOffsetHeight(); + contentHeight -= DOM.getElementPropertyInt(deco, "offsetHeight"); + contentHeight -= tb.getOffsetHeight(); + if (contentHeight < 0) { + contentHeight = 0; + } + + // Set proper values for content element + DOM.setStyleAttribute(contentNode, "height", contentHeight + "px"); + } else { + DOM.setStyleAttribute(contentNode, "height", ""); + } + } + + public void iLayout() { + updateTabScroller(); + } + + /** + * Sets the size of the visible tab (component). As the tab is set to + * position: absolute (to work around a firefox flickering bug) we must keep + * this up-to-date by hand. + */ + void updateOpenTabSize() { + /* + * The overflow=auto element must have a height specified, otherwise it + * will be just as high as the contents and no scrollbars will appear + */ + int height = -1; + int width = -1; + int minWidth = 0; + + if (!isDynamicHeight()) { + height = contentNode.getOffsetHeight(); + } + if (!isDynamicWidth()) { + width = contentNode.getOffsetWidth() - getContentAreaBorderWidth(); + } else { + /* + * If the tabbar is wider than the content we need to use the tabbar + * width as minimum width so scrollbars get placed correctly (at the + * right edge). + */ + minWidth = tb.getOffsetWidth() - getContentAreaBorderWidth(); + } + tp.fixVisibleTabSize(width, height, minWidth); + + } + + /** + * Layouts the tab-scroller elements, and applies styles. + */ + private void updateTabScroller() { + if (!isDynamicWidth()) { + ComponentConnector paintable = ConnectorMap.get(client) + .getConnector(this); + DOM.setStyleAttribute(tabs, "width", paintable.getState() + .getWidth()); + } + + // Make sure scrollerIndex is valid + if (scrollerIndex < 0 || scrollerIndex > tb.getTabCount()) { + scrollerIndex = tb.getFirstVisibleTab(); + } else if (tb.getTabCount() > 0 + && tb.getTab(scrollerIndex).isHiddenOnServer()) { + scrollerIndex = tb.getNextVisibleTab(scrollerIndex); + } + + boolean scrolled = isScrolledTabs(); + boolean clipped = isClippedTabs(); + if (tb.getTabCount() > 0 && tb.isVisible() && (scrolled || clipped)) { + DOM.setStyleAttribute(scroller, "display", ""); + DOM.setElementProperty(scrollerPrev, "className", + SCROLLER_CLASSNAME + (scrolled ? "Prev" : "Prev-disabled")); + DOM.setElementProperty(scrollerNext, "className", + SCROLLER_CLASSNAME + (clipped ? "Next" : "Next-disabled")); + } 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 + * com.vaadin.tests.components.tabsheet.TabSheetIcons + */ + final Style style = scroller.getStyle(); + style.setProperty("whiteSpace", "normal"); + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + style.setProperty("whiteSpace", ""); + } + }); + } + + } + + void showAllTabs() { + scrollerIndex = tb.getFirstVisibleTab(); + for (int i = 0; i < tb.getTabCount(); i++) { + Tab t = tb.getTab(i); + if (!t.isHiddenOnServer()) { + t.setVisible(true); + } + } + } + + private boolean isScrolledTabs() { + return scrollerIndex > tb.getFirstVisibleTab(); + } + + private boolean isClippedTabs() { + return (tb.getOffsetWidth() - DOM.getElementPropertyInt((Element) tb + .getContainerElement().getLastChild().cast(), "offsetWidth")) > getOffsetWidth() + - (isScrolledTabs() ? scroller.getOffsetWidth() : 0); + } + + private boolean isClipped(Tab tab) { + return tab.getAbsoluteLeft() + tab.getOffsetWidth() > getAbsoluteLeft() + + getOffsetWidth() - scroller.getOffsetWidth(); + } + + @Override + protected void clearPaintables() { + + int i = tb.getTabCount(); + while (i > 0) { + tb.removeTab(--i); + } + tp.clear(); + + } + + @Override + protected Iterator<Widget> getWidgetIterator() { + return tp.iterator(); + } + + private int borderW = -1; + + int getContentAreaBorderWidth() { + if (borderW < 0) { + borderW = Util.measureHorizontalBorder(contentNode); + } + return borderW; + } + + @Override + protected int getTabCount() { + return tb.getTabCount(); + } + + @Override + protected ComponentConnector getTab(int index) { + if (tp.getWidgetCount() > index) { + Widget widget = tp.getWidget(index); + return ConnectorMap.get(client).getConnector(widget); + } + return null; + } + + @Override + protected void removeTab(int index) { + tb.removeTab(index); + /* + * This must be checked because renderTab automatically removes the + * active tab content when it changes + */ + if (tp.getWidgetCount() > index) { + tp.remove(index); + } + } + + @Override + public void onBlur(BlurEvent event) { + if (focusedTab != null && event.getSource() instanceof Tab) { + focusedTab = null; + if (client.hasEventListeners(this, EventId.BLUR)) { + client.updateVariable(id, EventId.BLUR, "", true); + } + } + } + + @Override + public void onFocus(FocusEvent event) { + if (focusedTab == null && event.getSource() instanceof Tab) { + focusedTab = (Tab) event.getSource(); + if (client.hasEventListeners(this, EventId.FOCUS)) { + client.updateVariable(id, EventId.FOCUS, "", true); + } + } + } + + @Override + public void focus() { + tb.getTab(activeTabIndex).focus(); + } + + public void blur() { + tb.getTab(activeTabIndex).blur(); + } + + @Override + public void onKeyDown(KeyDownEvent event) { + if (event.getSource() instanceof Tab) { + int keycode = event.getNativeEvent().getKeyCode(); + + if (keycode == getPreviousTabKey()) { + selectPreviousTab(); + } else if (keycode == getNextTabKey()) { + selectNextTab(); + } else if (keycode == getCloseTabKey()) { + Tab tab = tb.getTab(activeTabIndex); + if (tab.isClosable()) { + tab.onClose(); + } + } + } + } + + /** + * @return The key code of the keyboard shortcut that selects the previous + * tab in a focused tabsheet. + */ + protected int getPreviousTabKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * @return The key code of the keyboard shortcut that selects the next tab + * in a focused tabsheet. + */ + protected int getNextTabKey() { + return KeyCodes.KEY_RIGHT; + } + + /** + * @return The key code of the keyboard shortcut that closes the currently + * selected tab in a focused tabsheet. + */ + protected int getCloseTabKey() { + return KeyCodes.KEY_DELETE; + } + + private void selectPreviousTab() { + int newTabIndex = activeTabIndex; + // Find the previous visible and enabled tab if any. + do { + newTabIndex--; + } while (newTabIndex >= 0 && !onTabSelected(newTabIndex)); + + if (newTabIndex >= 0) { + activeTabIndex = newTabIndex; + if (isScrolledTabs()) { + // Scroll until the new active tab is visible + int newScrollerIndex = scrollerIndex; + while (tb.getTab(activeTabIndex).getAbsoluteLeft() < getAbsoluteLeft() + && newScrollerIndex != -1) { + newScrollerIndex = tb.scrollLeft(newScrollerIndex); + } + scrollerIndex = newScrollerIndex; + updateTabScroller(); + } + } + } + + private void selectNextTab() { + int newTabIndex = activeTabIndex; + // Find the next visible and enabled tab if any. + do { + newTabIndex++; + } while (newTabIndex < getTabCount() && !onTabSelected(newTabIndex)); + + if (newTabIndex < getTabCount()) { + activeTabIndex = newTabIndex; + if (isClippedTabs()) { + // Scroll until the new active tab is completely visible + int newScrollerIndex = scrollerIndex; + while (isClipped(tb.getTab(activeTabIndex)) + && newScrollerIndex != -1) { + newScrollerIndex = tb.scrollRight(newScrollerIndex); + } + scrollerIndex = newScrollerIndex; + updateTabScroller(); + } + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetBase.java b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetBase.java new file mode 100644 index 0000000000..b9a68dc4af --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetBase.java @@ -0,0 +1,87 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.tabsheet; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.UIDL; + +public abstract class VTabsheetBase extends ComplexPanel { + + protected String id; + protected ApplicationConnection client; + + protected final ArrayList<String> tabKeys = new ArrayList<String>(); + protected int activeTabIndex = 0; + protected boolean disabled; + protected boolean readonly; + protected Set<String> disabledTabKeys = new HashSet<String>(); + + public VTabsheetBase(String classname) { + setElement(DOM.createDiv()); + setStyleName(classname); + } + + /** + * @return a list of currently shown Widgets + */ + abstract protected Iterator<Widget> getWidgetIterator(); + + /** + * Clears current tabs and contents + */ + abstract protected void clearPaintables(); + + /** + * Implement in extending classes. This method should render needed elements + * and set the visibility of the tab according to the 'selected' parameter. + */ + protected abstract void renderTab(final UIDL tabUidl, int index, + boolean selected, boolean hidden); + + /** + * Implement in extending classes. This method should render any previously + * non-cached content and set the activeTabIndex property to the specified + * index. + */ + protected abstract void selectTab(int index, final UIDL contentUidl); + + /** + * Implement in extending classes. This method should return the number of + * tabs currently rendered. + */ + protected abstract int getTabCount(); + + /** + * Implement in extending classes. This method should return the Paintable + * corresponding to the given index. + */ + protected abstract ComponentConnector getTab(int index); + + /** + * Implement in extending classes. This method should remove the rendered + * tab with the specified index. + */ + protected abstract void removeTab(int index); +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetPanel.java new file mode 100644 index 0000000000..e48f7ffa1f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetPanel.java @@ -0,0 +1,201 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.tabsheet; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate.TouchScrollHandler; + +/** + * A panel that displays all of its child widgets in a 'deck', where only one + * can be visible at a time. It is used by + * {@link com.vaadin.terminal.gwt.client.ui.tabsheet.VTabsheet}. + * + * This class has the same basic functionality as the GWT DeckPanel + * {@link com.google.gwt.user.client.ui.DeckPanel}, with the exception that it + * doesn't manipulate the child widgets' width and height attributes. + */ +public class VTabsheetPanel extends ComplexPanel { + + private Widget visibleWidget; + + private final TouchScrollHandler touchScrollHandler; + + /** + * Creates an empty tabsheet panel. + */ + public VTabsheetPanel() { + setElement(DOM.createDiv()); + touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this); + } + + /** + * Adds the specified widget to the deck. + * + * @param w + * the widget to be added + */ + @Override + public void add(Widget w) { + Element el = createContainerElement(); + DOM.appendChild(getElement(), el); + super.add(w, el); + } + + private Element createContainerElement() { + Element el = DOM.createDiv(); + DOM.setStyleAttribute(el, "position", "absolute"); + hide(el); + touchScrollHandler.addElement(el); + return el; + } + + /** + * Gets the index of the currently-visible widget. + * + * @return the visible widget's index + */ + public int getVisibleWidget() { + return getWidgetIndex(visibleWidget); + } + + /** + * Inserts a widget before the specified index. + * + * @param w + * the widget to be inserted + * @param beforeIndex + * the index before which it will be inserted + * @throws IndexOutOfBoundsException + * if <code>beforeIndex</code> is out of range + */ + public void insert(Widget w, int beforeIndex) { + Element el = createContainerElement(); + DOM.insertChild(getElement(), el, beforeIndex); + super.insert(w, el, beforeIndex, false); + } + + @Override + public boolean remove(Widget w) { + Element child = w.getElement(); + Element parent = null; + if (child != null) { + parent = DOM.getParent(child); + } + final boolean removed = super.remove(w); + if (removed) { + if (visibleWidget == w) { + visibleWidget = null; + } + if (parent != null) { + DOM.removeChild(getElement(), parent); + } + touchScrollHandler.removeElement(parent); + } + return removed; + } + + /** + * Shows the widget at the specified index. This causes the currently- + * visible widget to be hidden. + * + * @param index + * the index of the widget to be shown + */ + public void showWidget(int index) { + checkIndexBoundsForAccess(index); + Widget newVisible = getWidget(index); + if (visibleWidget != newVisible) { + if (visibleWidget != null) { + hide(DOM.getParent(visibleWidget.getElement())); + } + visibleWidget = newVisible; + touchScrollHandler.setElements(visibleWidget.getElement() + .getParentElement()); + } + // Always ensure the selected tab is visible. If server prevents a tab + // change we might end up here with visibleWidget == newVisible but its + // parent is still hidden. + unHide(DOM.getParent(visibleWidget.getElement())); + } + + private void hide(Element e) { + DOM.setStyleAttribute(e, "visibility", "hidden"); + DOM.setStyleAttribute(e, "top", "-100000px"); + DOM.setStyleAttribute(e, "left", "-100000px"); + } + + private void unHide(Element e) { + DOM.setStyleAttribute(e, "top", "0px"); + DOM.setStyleAttribute(e, "left", "0px"); + DOM.setStyleAttribute(e, "visibility", ""); + } + + public void fixVisibleTabSize(int width, int height, int minWidth) { + if (visibleWidget == null) { + return; + } + + boolean dynamicHeight = false; + + if (height < 0) { + height = visibleWidget.getOffsetHeight(); + dynamicHeight = true; + } + if (width < 0) { + width = visibleWidget.getOffsetWidth(); + } + if (width < minWidth) { + width = minWidth; + } + + Element wrapperDiv = (Element) visibleWidget.getElement() + .getParentElement(); + + // width first + getElement().getStyle().setPropertyPx("width", width); + wrapperDiv.getStyle().setPropertyPx("width", width); + + if (dynamicHeight) { + // height of widget might have changed due wrapping + height = visibleWidget.getOffsetHeight(); + } + // v-tabsheet-tabsheetpanel height + getElement().getStyle().setPropertyPx("height", height); + + // widget wrapper height + if (dynamicHeight) { + wrapperDiv.getStyle().clearHeight(); + } else { + // widget wrapper height + wrapperDiv.getStyle().setPropertyPx("height", height); + } + } + + public void replaceComponent(Widget oldComponent, Widget newComponent) { + boolean isVisible = (visibleWidget == oldComponent); + int widgetIndex = getWidgetIndex(oldComponent); + remove(oldComponent); + insert(newComponent, widgetIndex); + if (isVisible) { + showWidget(widgetIndex); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/textarea/TextAreaConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/textarea/TextAreaConnector.java new file mode 100644 index 0000000000..5fb7f97044 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/textarea/TextAreaConnector.java @@ -0,0 +1,45 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.textarea; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.textarea.TextAreaState; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.textfield.TextFieldConnector; +import com.vaadin.ui.TextArea; + +@Connect(TextArea.class) +public class TextAreaConnector extends TextFieldConnector { + + @Override + public TextAreaState getState() { + return (TextAreaState) super.getState(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + getWidget().setRows(getState().getRows()); + getWidget().setWordwrap(getState().isWordwrap()); + } + + @Override + public VTextArea getWidget() { + return (VTextArea) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/textarea/VTextArea.java b/client/src/com/vaadin/terminal/gwt/client/ui/textarea/VTextArea.java new file mode 100644 index 0000000000..e061cda1fa --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/textarea/VTextArea.java @@ -0,0 +1,117 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.textarea; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.dom.client.TextAreaElement; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; + +/** + * This class represents a multiline textfield (textarea). + * + * TODO consider replacing this with a RichTextArea based implementation. IE + * does not support CSS height for textareas in Strict mode :-( + * + * @author Vaadin Ltd. + * + */ +public class VTextArea extends VTextField { + public static final String CLASSNAME = "v-textarea"; + private boolean wordwrap = true; + + public VTextArea() { + super(DOM.createTextArea()); + setStyleName(CLASSNAME); + } + + public TextAreaElement getTextAreaElement() { + return super.getElement().cast(); + } + + public void setRows(int rows) { + getTextAreaElement().setRows(rows); + } + + @Override + protected void setMaxLength(int newMaxLength) { + super.setMaxLength(newMaxLength); + + boolean hasMaxLength = (newMaxLength >= 0); + + if (hasMaxLength) { + sinkEvents(Event.ONKEYUP); + } else { + unsinkEvents(Event.ONKEYUP); + } + } + + @Override + public void onBrowserEvent(Event event) { + if (getMaxLength() >= 0 && event.getTypeInt() == Event.ONKEYUP) { + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + if (getText().length() > getMaxLength()) { + setText(getText().substring(0, getMaxLength())); + } + } + }); + } + super.onBrowserEvent(event); + } + + @Override + public int getCursorPos() { + // This is needed so that TextBoxImplIE6 is used to return the correct + // position for old Internet Explorer versions where it has to be + // detected in a different way. + return getImpl().getTextAreaCursorPos(getElement()); + } + + @Override + protected void setMaxLengthToElement(int newMaxLength) { + // There is no maxlength property for textarea. The maximum length is + // enforced by the KEYUP handler + + } + + public void setWordwrap(boolean wordwrap) { + if (wordwrap == this.wordwrap) { + return; // No change + } + + if (wordwrap) { + getElement().removeAttribute("wrap"); + getElement().getStyle().clearOverflow(); + } else { + getElement().setAttribute("wrap", "off"); + getElement().getStyle().setOverflow(Overflow.AUTO); + } + if (BrowserInfo.get().isOpera()) { + // Opera fails to dynamically update the wrap attribute so we detach + // and reattach the whole TextArea. + Util.detachAttach(getElement()); + } + this.wordwrap = wordwrap; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/textfield/TextFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/textfield/TextFieldConnector.java new file mode 100644 index 0000000000..d16c01bd27 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/textfield/TextFieldConnector.java @@ -0,0 +1,119 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.textfield; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.Event; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.textfield.AbstractTextFieldState; +import com.vaadin.shared.ui.textfield.TextFieldConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.BeforeShortcutActionListener; +import com.vaadin.ui.TextField; + +@Connect(value = TextField.class, loadStyle = LoadStyle.EAGER) +public class TextFieldConnector extends AbstractFieldConnector implements + Paintable, BeforeShortcutActionListener { + + @Override + public AbstractTextFieldState getState() { + return (AbstractTextFieldState) super.getState(); + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + // Save details + getWidget().client = client; + getWidget().paintableId = uidl.getId(); + + if (!isRealUpdate(uidl)) { + return; + } + + getWidget().setReadOnly(isReadOnly()); + + getWidget().setInputPrompt(getState().getInputPrompt()); + getWidget().setMaxLength(getState().getMaxLength()); + getWidget().setImmediate(getState().isImmediate()); + + getWidget().listenTextChangeEvents = hasEventListener("ie"); + if (getWidget().listenTextChangeEvents) { + getWidget().textChangeEventMode = uidl + .getStringAttribute(TextFieldConstants.ATTR_TEXTCHANGE_EVENTMODE); + if (getWidget().textChangeEventMode + .equals(TextFieldConstants.TEXTCHANGE_MODE_EAGER)) { + getWidget().textChangeEventTimeout = 1; + } else { + getWidget().textChangeEventTimeout = uidl + .getIntAttribute(TextFieldConstants.ATTR_TEXTCHANGE_TIMEOUT); + if (getWidget().textChangeEventTimeout < 1) { + // Sanitize and allow lazy/timeout with timeout set to 0 to + // work as eager + getWidget().textChangeEventTimeout = 1; + } + } + getWidget().sinkEvents(VTextField.TEXTCHANGE_EVENTS); + getWidget().attachCutEventListener(getWidget().getElement()); + } + getWidget().setColumns(getState().getColumns()); + + final String text = getState().getText(); + + /* + * We skip the text content update if field has been repainted, but text + * has not been changed. Additional sanity check verifies there is no + * change in the que (in which case we count more on the server side + * value). + */ + if (!(uidl + .getBooleanAttribute(TextFieldConstants.ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS) + && getWidget().valueBeforeEdit != null && text + .equals(getWidget().valueBeforeEdit))) { + getWidget().updateFieldContent(text); + } + + if (uidl.hasAttribute("selpos")) { + final int pos = uidl.getIntAttribute("selpos"); + final int length = uidl.getIntAttribute("sellen"); + /* + * Gecko defers setting the text so we need to defer the selection. + */ + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + getWidget().setSelectionRange(pos, length); + } + }); + } + } + + @Override + public VTextField getWidget() { + return (VTextField) super.getWidget(); + } + + @Override + public void onBeforeShortcutAction(Event e) { + getWidget().valueChange(false); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java b/client/src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java new file mode 100644 index 0000000000..b00210cdd2 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java @@ -0,0 +1,421 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.textfield; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +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.TextBoxBase; +import com.vaadin.shared.EventId; +import com.vaadin.shared.ui.textfield.TextFieldConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.Field; + +/** + * This class represents a basic text input field with one row. + * + * @author Vaadin Ltd. + * + */ +public class VTextField extends TextBoxBase implements Field, ChangeHandler, + FocusHandler, BlurHandler, KeyDownHandler { + + /** + * The input node CSS classname. + */ + public static final String CLASSNAME = "v-textfield"; + /** + * This CSS classname is added to the input node on hover. + */ + public static final String CLASSNAME_FOCUS = "focus"; + + protected String paintableId; + + protected ApplicationConnection client; + + protected String valueBeforeEdit = null; + + /** + * Set to false if a text change event has been sent since the last value + * change event. This means that {@link #valueBeforeEdit} should not be + * trusted when determining whether a text change even should be sent. + */ + private boolean valueBeforeEditIsSynced = true; + + private boolean immediate = false; + private int maxLength = -1; + + private static final String CLASSNAME_PROMPT = "prompt"; + private static final String TEXTCHANGE_MODE_TIMEOUT = "TIMEOUT"; + + private String inputPrompt = null; + private boolean prompting = false; + private int lastCursorPos = -1; + + public VTextField() { + this(DOM.createInputText()); + } + + protected VTextField(Element node) { + super(node); + setStyleName(CLASSNAME); + addChangeHandler(this); + if (BrowserInfo.get().isIE()) { + // IE does not send change events when pressing enter in a text + // input so we handle it using a key listener instead + addKeyDownHandler(this); + } + addFocusHandler(this); + addBlurHandler(this); + } + + /* + * TODO When GWT adds ONCUT, add it there and remove workaround. See + * http://code.google.com/p/google-web-toolkit/issues/detail?id=4030 + * + * Also note that the cut/paste are not totally crossbrowsers compatible. + * E.g. in Opera mac works via context menu, but on via File->Paste/Cut. + * Opera might need the polling method for 100% working textchanceevents. + * Eager polling for a change is bit dum and heavy operation, so I guess we + * should first try to survive without. + */ + protected static final int TEXTCHANGE_EVENTS = Event.ONPASTE + | Event.KEYEVENTS | Event.ONMOUSEUP; + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + + if (listenTextChangeEvents + && (event.getTypeInt() & TEXTCHANGE_EVENTS) == event + .getTypeInt()) { + deferTextChangeEvent(); + } + + } + + /* + * TODO optimize this so that only changes are sent + make the value change + * event just a flag that moves the current text to value + */ + private String lastTextChangeString = null; + + private String getLastCommunicatedString() { + return lastTextChangeString; + } + + private void communicateTextValueToServer() { + String text = getText(); + if (prompting) { + // Input prompt visible, text is actually "" + text = ""; + } + if (!text.equals(getLastCommunicatedString())) { + if (valueBeforeEditIsSynced && text.equals(valueBeforeEdit)) { + /* + * Value change for the current text has been enqueued since the + * last text change event was sent, but we can't know that it + * has been sent to the server. Ensure that all pending changes + * are sent now. Sending a value change without a text change + * will simulate a TextChangeEvent on the server. + */ + client.sendPendingVariableChanges(); + } else { + // Default case - just send an immediate text change message + client.updateVariable(paintableId, + TextFieldConstants.VAR_CUR_TEXT, text, true); + + // Shouldn't investigate valueBeforeEdit to avoid duplicate text + // change events as the states are not in sync any more + valueBeforeEditIsSynced = false; + } + lastTextChangeString = text; + } + } + + private Timer textChangeEventTrigger = new Timer() { + + @Override + public void run() { + if (isAttached()) { + updateCursorPosition(); + communicateTextValueToServer(); + scheduled = false; + } + } + }; + private boolean scheduled = false; + protected boolean listenTextChangeEvents; + protected String textChangeEventMode; + protected int textChangeEventTimeout; + + private void deferTextChangeEvent() { + if (textChangeEventMode.equals(TEXTCHANGE_MODE_TIMEOUT) && scheduled) { + return; + } else { + textChangeEventTrigger.cancel(); + } + textChangeEventTrigger.schedule(getTextChangeEventTimeout()); + scheduled = true; + } + + private int getTextChangeEventTimeout() { + return textChangeEventTimeout; + } + + @Override + public void setReadOnly(boolean readOnly) { + boolean wasReadOnly = isReadOnly(); + + if (readOnly) { + setTabIndex(-1); + } else if (wasReadOnly && !readOnly && getTabIndex() == -1) { + /* + * Need to manually set tab index to 0 since server will not send + * the tab index if it is 0. + */ + setTabIndex(0); + } + + super.setReadOnly(readOnly); + } + + protected void updateFieldContent(final String text) { + setPrompting(inputPrompt != null && focusedTextField != this + && (text.equals(""))); + + String fieldValue; + if (prompting) { + fieldValue = isReadOnly() ? "" : inputPrompt; + addStyleDependentName(CLASSNAME_PROMPT); + } else { + fieldValue = text; + removeStyleDependentName(CLASSNAME_PROMPT); + } + setText(fieldValue); + + lastTextChangeString = valueBeforeEdit = text; + valueBeforeEditIsSynced = true; + } + + protected void onCut() { + if (listenTextChangeEvents) { + deferTextChangeEvent(); + } + } + + protected native void attachCutEventListener(Element el) + /*-{ + var me = this; + el.oncut = $entry(function() { + me.@com.vaadin.terminal.gwt.client.ui.textfield.VTextField::onCut()(); + }); + }-*/; + + protected native void detachCutEventListener(Element el) + /*-{ + el.oncut = null; + }-*/; + + @Override + protected void onDetach() { + super.onDetach(); + detachCutEventListener(getElement()); + if (focusedTextField == this) { + focusedTextField = null; + } + } + + @Override + protected void onAttach() { + super.onAttach(); + if (listenTextChangeEvents) { + detachCutEventListener(getElement()); + } + } + + protected void setMaxLength(int newMaxLength) { + if (newMaxLength >= 0) { + maxLength = newMaxLength; + } else { + maxLength = -1; + } + setMaxLengthToElement(newMaxLength); + } + + protected void setMaxLengthToElement(int newMaxLength) { + if (newMaxLength >= 0) { + getElement().setPropertyInt("maxLength", newMaxLength); + } else { + getElement().removeAttribute("maxLength"); + } + } + + public int getMaxLength() { + return maxLength; + } + + @Override + public void onChange(ChangeEvent event) { + valueChange(false); + } + + /** + * Called when the field value might have changed and/or the field was + * blurred. These are combined so the blur event is sent in the same batch + * as a possible value change event (these are often connected). + * + * @param blurred + * true if the field was blurred + */ + public void valueChange(boolean blurred) { + if (client != null && paintableId != null) { + boolean sendBlurEvent = false; + boolean sendValueChange = false; + + if (blurred && client.hasEventListeners(this, EventId.BLUR)) { + sendBlurEvent = true; + client.updateVariable(paintableId, EventId.BLUR, "", false); + } + + String newText = getText(); + if (!prompting && newText != null + && !newText.equals(valueBeforeEdit)) { + sendValueChange = immediate; + client.updateVariable(paintableId, "text", newText, false); + valueBeforeEdit = newText; + valueBeforeEditIsSynced = true; + } + + /* + * also send cursor position, no public api yet but for easier + * extension + */ + updateCursorPosition(); + + if (sendBlurEvent || sendValueChange) { + /* + * Avoid sending text change event as we will simulate it on the + * server side before value change events. + */ + textChangeEventTrigger.cancel(); + scheduled = false; + client.sendPendingVariableChanges(); + } + } + } + + /** + * Updates the cursor position variable if it has changed since the last + * update. + * + * @return true iff the value was updated + */ + protected boolean updateCursorPosition() { + if (Util.isAttachedAndDisplayed(this)) { + int cursorPos = getCursorPos(); + if (lastCursorPos != cursorPos) { + client.updateVariable(paintableId, + TextFieldConstants.VAR_CURSOR, cursorPos, false); + lastCursorPos = cursorPos; + return true; + } + } + return false; + } + + private static VTextField focusedTextField; + + public static void flushChangesFromFocusedTextField() { + if (focusedTextField != null) { + focusedTextField.onChange(null); + } + } + + @Override + public void onFocus(FocusEvent event) { + addStyleDependentName(CLASSNAME_FOCUS); + if (prompting) { + setText(""); + removeStyleDependentName(CLASSNAME_PROMPT); + setPrompting(false); + } + focusedTextField = this; + if (client.hasEventListeners(this, EventId.FOCUS)) { + client.updateVariable(paintableId, EventId.FOCUS, "", true); + } + } + + @Override + public void onBlur(BlurEvent event) { + // this is called twice on Chrome when e.g. changing tab while prompting + // field focused - do not change settings on the second time + if (focusedTextField != this) { + return; + } + removeStyleDependentName(CLASSNAME_FOCUS); + focusedTextField = null; + String text = getText(); + setPrompting(inputPrompt != null && (text == null || "".equals(text))); + if (prompting) { + setText(isReadOnly() ? "" : inputPrompt); + addStyleDependentName(CLASSNAME_PROMPT); + } + + valueChange(true); + } + + private void setPrompting(boolean prompting) { + this.prompting = prompting; + } + + public void setColumns(int columns) { + if (columns <= 0) { + return; + } + + setWidth(columns + "em"); + } + + @Override + public void onKeyDown(KeyDownEvent event) { + if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) { + valueChange(false); + } + } + + public void setImmediate(boolean immediate) { + this.immediate = immediate; + } + + public void setInputPrompt(String inputPrompt) { + this.inputPrompt = inputPrompt; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tree/TreeConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/tree/TreeConnector.java new file mode 100644 index 0000000000..a97a4b521f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/tree/TreeConnector.java @@ -0,0 +1,296 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.tree; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.google.gwt.dom.client.Element; +import com.vaadin.shared.AbstractFieldState; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.tree.TreeConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.TooltipInfo; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector; +import com.vaadin.terminal.gwt.client.ui.tree.VTree.TreeNode; +import com.vaadin.ui.Tree; + +@Connect(Tree.class) +public class TreeConnector extends AbstractComponentConnector implements + Paintable { + + protected final Map<TreeNode, TooltipInfo> tooltipMap = new HashMap<TreeNode, TooltipInfo>(); + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (!isRealUpdate(uidl)) { + return; + } + + getWidget().rendering = true; + + getWidget().client = client; + + if (uidl.hasAttribute("partialUpdate")) { + handleUpdate(uidl); + getWidget().rendering = false; + return; + } + + getWidget().paintableId = uidl.getId(); + + getWidget().immediate = getState().isImmediate(); + + getWidget().disabled = !isEnabled(); + getWidget().readonly = isReadOnly(); + + getWidget().dragMode = uidl.hasAttribute("dragMode") ? uidl + .getIntAttribute("dragMode") : 0; + + getWidget().isNullSelectionAllowed = uidl + .getBooleanAttribute("nullselect"); + + if (uidl.hasAttribute("alb")) { + getWidget().bodyActionKeys = uidl.getStringArrayAttribute("alb"); + } + + getWidget().body.clear(); + // clear out any references to nodes that no longer are attached + getWidget().clearNodeToKeyMap(); + tooltipMap.clear(); + + TreeNode childTree = null; + UIDL childUidl = null; + for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) { + childUidl = (UIDL) i.next(); + if ("actions".equals(childUidl.getTag())) { + updateActionMap(childUidl); + continue; + } else if ("-ac".equals(childUidl.getTag())) { + getWidget().updateDropHandler(childUidl); + continue; + } + childTree = getWidget().new TreeNode(); + getConnection().getVTooltip().connectHandlersToWidget(childTree); + updateNodeFromUIDL(childTree, childUidl); + getWidget().body.add(childTree); + childTree.addStyleDependentName("root"); + childTree.childNodeContainer.addStyleDependentName("root"); + } + if (childTree != null && childUidl != null) { + boolean leaf = !childUidl.getTag().equals("node"); + childTree.addStyleDependentName(leaf ? "leaf-last" : "last"); + childTree.childNodeContainer.addStyleDependentName("last"); + } + final String selectMode = uidl.getStringAttribute("selectmode"); + getWidget().selectable = !"none".equals(selectMode); + getWidget().isMultiselect = "multi".equals(selectMode); + + if (getWidget().isMultiselect) { + if (BrowserInfo.get().isTouchDevice()) { + // Always use the simple mode for touch devices that do not have + // shift/ctrl keys (#8595) + getWidget().multiSelectMode = VTree.MULTISELECT_MODE_SIMPLE; + } else { + getWidget().multiSelectMode = uidl + .getIntAttribute("multiselectmode"); + } + } + + getWidget().selectedIds = uidl.getStringArrayVariableAsSet("selected"); + + // Update lastSelection and focusedNode to point to *actual* nodes again + // after the old ones have been cleared from the body. This fixes focus + // and keyboard navigation issues as described in #7057 and other + // tickets. + if (getWidget().lastSelection != null) { + getWidget().lastSelection = getWidget().getNodeByKey( + getWidget().lastSelection.key); + } + if (getWidget().focusedNode != null) { + getWidget().setFocusedNode( + getWidget().getNodeByKey(getWidget().focusedNode.key)); + } + + if (getWidget().lastSelection == null + && getWidget().focusedNode == null + && !getWidget().selectedIds.isEmpty()) { + getWidget().setFocusedNode( + getWidget().getNodeByKey( + getWidget().selectedIds.iterator().next())); + getWidget().focusedNode.setFocused(false); + } + + getWidget().rendering = false; + + } + + @Override + public VTree getWidget() { + return (VTree) super.getWidget(); + } + + private void handleUpdate(UIDL uidl) { + final TreeNode rootNode = getWidget().getNodeByKey( + uidl.getStringAttribute("rootKey")); + if (rootNode != null) { + if (!rootNode.getState()) { + // expanding node happened server side + rootNode.setState(true, false); + } + renderChildNodes(rootNode, (Iterator) uidl.getChildIterator()); + } + } + + /** + * Registers action for the root and also for individual nodes + * + * @param uidl + */ + private void updateActionMap(UIDL uidl) { + final Iterator<?> it = uidl.getChildIterator(); + while (it.hasNext()) { + final UIDL action = (UIDL) it.next(); + final String key = action.getStringAttribute("key"); + final String caption = action + .getStringAttribute(TreeConstants.ATTRIBUTE_ACTION_CAPTION); + String iconUrl = null; + if (action.hasAttribute(TreeConstants.ATTRIBUTE_ACTION_ICON)) { + iconUrl = getConnection() + .translateVaadinUri( + action.getStringAttribute(TreeConstants.ATTRIBUTE_ACTION_ICON)); + } + getWidget().registerAction(key, caption, iconUrl); + } + + } + + public void updateNodeFromUIDL(TreeNode treeNode, UIDL uidl) { + String nodeKey = uidl.getStringAttribute("key"); + treeNode.setText(uidl + .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_CAPTION)); + treeNode.key = nodeKey; + + getWidget().registerNode(treeNode); + + if (uidl.hasAttribute("al")) { + treeNode.actionKeys = uidl.getStringArrayAttribute("al"); + } + + if (uidl.getTag().equals("node")) { + if (uidl.getChildCount() == 0) { + treeNode.childNodeContainer.setVisible(false); + } else { + renderChildNodes(treeNode, (Iterator) uidl.getChildIterator()); + treeNode.childrenLoaded = true; + } + } else { + treeNode.addStyleName(TreeNode.CLASSNAME + "-leaf"); + } + if (uidl.hasAttribute(TreeConstants.ATTRIBUTE_NODE_STYLE)) { + treeNode.setNodeStyleName(uidl + .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_STYLE)); + } + + String description = uidl.getStringAttribute("descr"); + if (description != null) { + tooltipMap.put(treeNode, new TooltipInfo(description)); + } + + if (uidl.getBooleanAttribute("expanded") && !treeNode.getState()) { + treeNode.setState(true, false); + } + + if (uidl.getBooleanAttribute("selected")) { + treeNode.setSelected(true); + // ensure that identifier is in selectedIds array (this may be a + // partial update) + getWidget().selectedIds.add(nodeKey); + } + + treeNode.setIcon(uidl + .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON)); + } + + void renderChildNodes(TreeNode containerNode, Iterator<UIDL> i) { + containerNode.childNodeContainer.clear(); + containerNode.childNodeContainer.setVisible(true); + while (i.hasNext()) { + final UIDL childUidl = i.next(); + // actions are in bit weird place, don't mix them with children, + // but current node's actions + if ("actions".equals(childUidl.getTag())) { + updateActionMap(childUidl); + continue; + } + final TreeNode childTree = getWidget().new TreeNode(); + getConnection().getVTooltip().connectHandlersToWidget(childTree); + updateNodeFromUIDL(childTree, childUidl); + containerNode.childNodeContainer.add(childTree); + if (!i.hasNext()) { + childTree + .addStyleDependentName(childTree.isLeaf() ? "leaf-last" + : "last"); + childTree.childNodeContainer.addStyleDependentName("last"); + } + } + containerNode.childrenLoaded = true; + } + + @Override + public boolean isReadOnly() { + return super.isReadOnly() || getState().isPropertyReadOnly(); + } + + @Override + public AbstractFieldState getState() { + return (AbstractFieldState) super.getState(); + } + + @Override + public TooltipInfo getTooltipInfo(Element element) { + + TooltipInfo info = null; + + // Try to find a tooltip for a node + if (element != getWidget().getElement()) { + Object node = Util.findWidget( + (com.google.gwt.user.client.Element) element, + TreeNode.class); + + if (node != null) { + TreeNode tnode = (TreeNode) node; + if (tnode.isCaptionElement(element)) { + info = tooltipMap.get(tnode); + } + } + } + + // If no tooltip found for the node or if the target was not a node, use + // the default tooltip + if (info == null) { + info = super.getTooltipInfo(element); + } + + return info; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java b/client/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java new file mode 100644 index 0000000000..9fbaa1d8bf --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java @@ -0,0 +1,2139 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.tree; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.event.dom.client.ContextMenuHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.user.client.Command; +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.Window; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.UIObject; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.shared.ui.tree.TreeConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.Action; +import com.vaadin.terminal.gwt.client.ui.ActionOwner; +import com.vaadin.terminal.gwt.client.ui.FocusElementPanel; +import com.vaadin.terminal.gwt.client.ui.Icon; +import com.vaadin.terminal.gwt.client.ui.SubPartAware; +import com.vaadin.terminal.gwt.client.ui.TreeAction; +import com.vaadin.terminal.gwt.client.ui.VLazyExecutor; +import com.vaadin.terminal.gwt.client.ui.dd.DDUtil; +import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler; +import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback; +import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager; +import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent; +import com.vaadin.terminal.gwt.client.ui.dd.VDropHandler; +import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler; +import com.vaadin.terminal.gwt.client.ui.dd.VTransferable; + +/** + * + */ +public class VTree extends FocusElementPanel implements VHasDropHandler, + FocusHandler, BlurHandler, KeyPressHandler, KeyDownHandler, + SubPartAware, ActionOwner { + + public static final String CLASSNAME = "v-tree"; + + /** + * Click selects the current node, ctrl/shift toggles multi selection + */ + public static final int MULTISELECT_MODE_DEFAULT = 0; + + /** + * Click/touch on node toggles its selected status + */ + public static final int MULTISELECT_MODE_SIMPLE = 1; + + private static final int CHARCODE_SPACE = 32; + + final FlowPanel body = new FlowPanel(); + + Set<String> selectedIds = new HashSet<String>(); + ApplicationConnection client; + String paintableId; + boolean selectable; + boolean isMultiselect; + private String currentMouseOverKey; + TreeNode lastSelection; + TreeNode focusedNode; + int multiSelectMode = MULTISELECT_MODE_DEFAULT; + + private final HashMap<String, TreeNode> keyToNode = new HashMap<String, TreeNode>(); + + /** + * This map contains captions and icon urls for actions like: * "33_c" -> + * "Edit" * "33_i" -> "http://dom.com/edit.png" + */ + private final HashMap<String, String> actionMap = new HashMap<String, String>(); + + boolean immediate; + + boolean isNullSelectionAllowed = true; + + boolean disabled = false; + + boolean readonly; + + boolean rendering; + + private VAbstractDropHandler dropHandler; + + int dragMode; + + private boolean selectionHasChanged = false; + + String[] bodyActionKeys; + + public VLazyExecutor iconLoaded = new VLazyExecutor(50, + new ScheduledCommand() { + + @Override + public void execute() { + Util.notifyParentOfSizeChange(VTree.this, true); + } + + }); + + public VTree() { + super(); + setStyleName(CLASSNAME); + add(body); + + addFocusHandler(this); + addBlurHandler(this); + + /* + * Listen to context menu events on the empty space in the tree + */ + sinkEvents(Event.ONCONTEXTMENU); + addDomHandler(new ContextMenuHandler() { + @Override + public void onContextMenu(ContextMenuEvent event) { + handleBodyContextMenu(event); + } + }, ContextMenuEvent.getType()); + + /* + * Firefox auto-repeat works correctly only if we use a key press + * handler, other browsers handle it correctly when using a key down + * handler + */ + if (BrowserInfo.get().isGecko() || BrowserInfo.get().isOpera()) { + addKeyPressHandler(this); + } else { + addKeyDownHandler(this); + } + + /* + * We need to use the sinkEvents method to catch the keyUp events so we + * can cache a single shift. KeyUpHandler cannot do this. At the same + * time we catch the mouse down and up events so we can apply the text + * selection patch in IE + */ + sinkEvents(Event.ONMOUSEDOWN | Event.ONMOUSEUP | Event.ONKEYUP); + + /* + * Re-set the tab index to make sure that the FocusElementPanel's + * (super) focus element gets the tab index and not the element + * containing the tree. + */ + setTabIndex(0); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user + * .client.Event) + */ + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONMOUSEDOWN) { + // Prevent default text selection in IE + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()).setPropertyJSO( + "onselectstart", applyDisableTextSelectionIEHack()); + } + } else if (event.getTypeInt() == Event.ONMOUSEUP) { + // Remove IE text selection hack + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()).setPropertyJSO( + "onselectstart", null); + } + } else if (event.getTypeInt() == Event.ONKEYUP) { + if (selectionHasChanged) { + if (event.getKeyCode() == getNavigationDownKey() + && !event.getShiftKey()) { + sendSelectionToServer(); + event.preventDefault(); + } else if (event.getKeyCode() == getNavigationUpKey() + && !event.getShiftKey()) { + sendSelectionToServer(); + event.preventDefault(); + } else if (event.getKeyCode() == KeyCodes.KEY_SHIFT) { + sendSelectionToServer(); + event.preventDefault(); + } else if (event.getKeyCode() == getNavigationSelectKey()) { + sendSelectionToServer(); + event.preventDefault(); + } + } + } + } + + public String getActionCaption(String actionKey) { + return actionMap.get(actionKey + "_c"); + } + + public String getActionIcon(String actionKey) { + return actionMap.get(actionKey + "_i"); + } + + /** + * Returns the first root node of the tree or null if there are no root + * nodes. + * + * @return The first root {@link TreeNode} + */ + protected TreeNode getFirstRootNode() { + if (body.getWidgetCount() == 0) { + return null; + } + return (TreeNode) body.getWidget(0); + } + + /** + * Returns the last root node of the tree or null if there are no root + * nodes. + * + * @return The last root {@link TreeNode} + */ + protected TreeNode getLastRootNode() { + if (body.getWidgetCount() == 0) { + return null; + } + return (TreeNode) body.getWidget(body.getWidgetCount() - 1); + } + + /** + * Returns a list of all root nodes in the Tree in the order they appear in + * the tree. + * + * @return A list of all root {@link TreeNode}s. + */ + protected List<TreeNode> getRootNodes() { + ArrayList<TreeNode> rootNodes = new ArrayList<TreeNode>(); + for (int i = 0; i < body.getWidgetCount(); i++) { + rootNodes.add((TreeNode) body.getWidget(i)); + } + return rootNodes; + } + + private void updateTreeRelatedDragData(VDragEvent drag) { + + currentMouseOverKey = findCurrentMouseOverKey(drag.getElementOver()); + + drag.getDropDetails().put("itemIdOver", currentMouseOverKey); + if (currentMouseOverKey != null) { + TreeNode treeNode = getNodeByKey(currentMouseOverKey); + VerticalDropLocation detail = treeNode.getDropDetail(drag + .getCurrentGwtEvent()); + Boolean overTreeNode = null; + if (treeNode != null && !treeNode.isLeaf() + && detail == VerticalDropLocation.MIDDLE) { + overTreeNode = true; + } + drag.getDropDetails().put("itemIdOverIsNode", overTreeNode); + drag.getDropDetails().put("detail", detail); + } else { + drag.getDropDetails().put("itemIdOverIsNode", null); + drag.getDropDetails().put("detail", null); + } + + } + + private String findCurrentMouseOverKey(Element elementOver) { + TreeNode treeNode = Util.findWidget(elementOver, TreeNode.class); + return treeNode == null ? null : treeNode.key; + } + + void updateDropHandler(UIDL childUidl) { + if (dropHandler == null) { + dropHandler = new VAbstractDropHandler() { + + @Override + public void dragEnter(VDragEvent drag) { + } + + @Override + protected void dragAccepted(final VDragEvent drag) { + + } + + @Override + public void dragOver(final VDragEvent currentDrag) { + final Object oldIdOver = currentDrag.getDropDetails().get( + "itemIdOver"); + final VerticalDropLocation oldDetail = (VerticalDropLocation) currentDrag + .getDropDetails().get("detail"); + + updateTreeRelatedDragData(currentDrag); + final VerticalDropLocation detail = (VerticalDropLocation) currentDrag + .getDropDetails().get("detail"); + boolean nodeHasChanged = (currentMouseOverKey != null && currentMouseOverKey != oldIdOver) + || (currentMouseOverKey == null && oldIdOver != null); + boolean detailHasChanded = (detail != null && detail != oldDetail) + || (detail == null && oldDetail != null); + + if (nodeHasChanged || detailHasChanded) { + final String newKey = currentMouseOverKey; + TreeNode treeNode = keyToNode.get(oldIdOver); + if (treeNode != null) { + // clear old styles + treeNode.emphasis(null); + } + if (newKey != null) { + validate(new VAcceptCallback() { + @Override + public void accepted(VDragEvent event) { + VerticalDropLocation curDetail = (VerticalDropLocation) event + .getDropDetails().get("detail"); + if (curDetail == detail + && newKey.equals(currentMouseOverKey)) { + getNodeByKey(newKey).emphasis(detail); + } + /* + * Else drag is already on a different + * node-detail pair, new criteria check is + * going on + */ + } + }, currentDrag); + + } + } + + } + + @Override + public void dragLeave(VDragEvent drag) { + cleanUp(); + } + + private void cleanUp() { + if (currentMouseOverKey != null) { + getNodeByKey(currentMouseOverKey).emphasis(null); + currentMouseOverKey = null; + } + } + + @Override + public boolean drop(VDragEvent drag) { + cleanUp(); + return super.drop(drag); + } + + @Override + public ComponentConnector getConnector() { + return ConnectorMap.get(client).getConnector(VTree.this); + } + + @Override + public ApplicationConnection getApplicationConnection() { + return client; + } + + }; + } + dropHandler.updateAcceptRules(childUidl); + } + + public void setSelected(TreeNode treeNode, boolean selected) { + if (selected) { + if (!isMultiselect) { + while (selectedIds.size() > 0) { + final String id = selectedIds.iterator().next(); + final TreeNode oldSelection = getNodeByKey(id); + if (oldSelection != null) { + // can be null if the node is not visible (parent + // collapsed) + oldSelection.setSelected(false); + } + selectedIds.remove(id); + } + } + treeNode.setSelected(true); + selectedIds.add(treeNode.key); + } else { + if (!isNullSelectionAllowed) { + if (!isMultiselect || selectedIds.size() == 1) { + return; + } + } + selectedIds.remove(treeNode.key); + treeNode.setSelected(false); + } + + sendSelectionToServer(); + } + + /** + * Sends the selection to the server + */ + private void sendSelectionToServer() { + Command command = new Command() { + @Override + public void execute() { + client.updateVariable(paintableId, "selected", + selectedIds.toArray(new String[selectedIds.size()]), + immediate); + selectionHasChanged = false; + } + }; + + /* + * Delaying the sending of the selection in webkit to ensure the + * selection is always sent when the tree has focus and after click + * events have been processed. This is due to the focusing + * implementation in FocusImplSafari which uses timeouts when focusing + * and blurring. + */ + if (BrowserInfo.get().isWebkit()) { + Scheduler.get().scheduleDeferred(command); + } else { + command.execute(); + } + } + + /** + * Is a node selected in the tree + * + * @param treeNode + * The node to check + * @return + */ + public boolean isSelected(TreeNode treeNode) { + return selectedIds.contains(treeNode.key); + } + + public class TreeNode extends SimplePanel implements ActionOwner { + + public static final String CLASSNAME = "v-tree-node"; + public static final String CLASSNAME_FOCUSED = CLASSNAME + "-focused"; + + public String key; + + String[] actionKeys = null; + + boolean childrenLoaded; + + Element nodeCaptionDiv; + + protected Element nodeCaptionSpan; + + FlowPanel childNodeContainer; + + private boolean open; + + private Icon icon; + + private Event mouseDownEvent; + + private int cachedHeight = -1; + + private boolean focused = false; + + public TreeNode() { + constructDom(); + sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS + | Event.TOUCHEVENTS | Event.ONCONTEXTMENU); + } + + public VerticalDropLocation getDropDetail(NativeEvent currentGwtEvent) { + if (cachedHeight < 0) { + /* + * Height is cached to avoid flickering (drop hints may change + * the reported offsetheight -> would change the drop detail) + */ + cachedHeight = nodeCaptionDiv.getOffsetHeight(); + } + VerticalDropLocation verticalDropLocation = DDUtil + .getVerticalDropLocation(nodeCaptionDiv, cachedHeight, + currentGwtEvent, 0.15); + return verticalDropLocation; + } + + protected void emphasis(VerticalDropLocation detail) { + String base = "v-tree-node-drag-"; + UIObject.setStyleName(getElement(), base + "top", + VerticalDropLocation.TOP == detail); + UIObject.setStyleName(getElement(), base + "bottom", + VerticalDropLocation.BOTTOM == detail); + UIObject.setStyleName(getElement(), base + "center", + VerticalDropLocation.MIDDLE == detail); + base = "v-tree-node-caption-drag-"; + UIObject.setStyleName(nodeCaptionDiv, base + "top", + VerticalDropLocation.TOP == detail); + UIObject.setStyleName(nodeCaptionDiv, base + "bottom", + VerticalDropLocation.BOTTOM == detail); + UIObject.setStyleName(nodeCaptionDiv, base + "center", + VerticalDropLocation.MIDDLE == detail); + + // also add classname to "folder node" into which the drag is + // targeted + + TreeNode folder = null; + /* Possible parent of this TreeNode will be stored here */ + TreeNode parentFolder = getParentNode(); + + // TODO fix my bugs + if (isLeaf()) { + folder = parentFolder; + // note, parent folder may be null if this is root node => no + // folder target exists + } else { + if (detail == VerticalDropLocation.TOP) { + folder = parentFolder; + } else { + folder = this; + } + // ensure we remove the dragfolder classname from the previous + // folder node + setDragFolderStyleName(this, false); + setDragFolderStyleName(parentFolder, false); + } + if (folder != null) { + setDragFolderStyleName(folder, detail != null); + } + + } + + private TreeNode getParentNode() { + Widget parent2 = getParent().getParent(); + if (parent2 instanceof TreeNode) { + return (TreeNode) parent2; + } + return null; + } + + private void setDragFolderStyleName(TreeNode folder, boolean add) { + if (folder != null) { + UIObject.setStyleName(folder.getElement(), + "v-tree-node-dragfolder", add); + UIObject.setStyleName(folder.nodeCaptionDiv, + "v-tree-node-caption-dragfolder", add); + } + } + + /** + * Handles mouse selection + * + * @param ctrl + * Was the ctrl-key pressed + * @param shift + * Was the shift-key pressed + * @return Returns true if event was handled, else false + */ + private boolean handleClickSelection(final boolean ctrl, + final boolean shift) { + + // always when clicking an item, focus it + setFocusedNode(this, false); + + if (!BrowserInfo.get().isOpera()) { + /* + * Ensure that the tree's focus element also gains focus + * (TreeNodes focus is faked using FocusElementPanel in browsers + * other than Opera). + */ + focus(); + } + + ScheduledCommand command = new ScheduledCommand() { + @Override + public void execute() { + + if (multiSelectMode == MULTISELECT_MODE_SIMPLE + || !isMultiselect) { + toggleSelection(); + lastSelection = TreeNode.this; + } else if (multiSelectMode == MULTISELECT_MODE_DEFAULT) { + // Handle ctrl+click + if (isMultiselect && ctrl && !shift) { + toggleSelection(); + lastSelection = TreeNode.this; + + // Handle shift+click + } else if (isMultiselect && !ctrl && shift) { + deselectAll(); + selectNodeRange(lastSelection.key, key); + sendSelectionToServer(); + + // Handle ctrl+shift click + } else if (isMultiselect && ctrl && shift) { + selectNodeRange(lastSelection.key, key); + + // Handle click + } else { + // TODO should happen only if this alone not yet + // selected, + // now sending excess server calls + deselectAll(); + toggleSelection(); + lastSelection = TreeNode.this; + } + } + } + }; + + if (BrowserInfo.get().isWebkit() && !treeHasFocus) { + /* + * Safari may need to wait for focus. See FocusImplSafari. + */ + // VConsole.log("Deferring click handling to let webkit gain focus..."); + Scheduler.get().scheduleDeferred(command); + } else { + command.execute(); + } + + return true; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt + * .user.client.Event) + */ + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + final int type = DOM.eventGetType(event); + final Element target = DOM.eventGetTarget(event); + + if (type == Event.ONLOAD && target == icon.getElement()) { + iconLoaded.trigger(); + } + + if (disabled) { + return; + } + + final boolean inCaption = isCaptionElement(target); + if (inCaption + && client.hasEventListeners(VTree.this, + TreeConstants.ITEM_CLICK_EVENT_ID) + + && (type == Event.ONDBLCLICK || type == Event.ONMOUSEUP)) { + fireClick(event); + } + if (type == Event.ONCLICK) { + if (getElement() == target) { + // state change + toggleState(); + } else if (!readonly && inCaption) { + if (selectable) { + // caption click = selection change && possible click + // event + if (handleClickSelection( + event.getCtrlKey() || event.getMetaKey(), + event.getShiftKey())) { + event.preventDefault(); + } + } else { + // Not selectable, only focus the node. + setFocusedNode(this); + } + } + event.stopPropagation(); + } else if (type == Event.ONCONTEXTMENU) { + showContextMenu(event); + } + + if (dragMode != 0 || dropHandler != null) { + if (type == Event.ONMOUSEDOWN || type == Event.ONTOUCHSTART) { + if (nodeCaptionDiv.isOrHasChild((Node) event + .getEventTarget().cast())) { + if (dragMode > 0 + && (type == Event.ONTOUCHSTART || event + .getButton() == NativeEvent.BUTTON_LEFT)) { + mouseDownEvent = event; // save event for possible + // dd operation + if (type == Event.ONMOUSEDOWN) { + event.preventDefault(); // prevent text + // selection + } else { + /* + * FIXME We prevent touch start event to be used + * as a scroll start event. Note that we cannot + * easily distinguish whether the user wants to + * drag or scroll. The same issue is in table + * that has scrollable area and has drag and + * drop enable. Some kind of timer might be used + * to resolve the issue. + */ + event.stopPropagation(); + } + } + } + } else if (type == Event.ONMOUSEMOVE + || type == Event.ONMOUSEOUT + || type == Event.ONTOUCHMOVE) { + + if (mouseDownEvent != null) { + // start actual drag on slight move when mouse is down + VTransferable t = new VTransferable(); + t.setDragSource(ConnectorMap.get(client).getConnector( + VTree.this)); + t.setData("itemId", key); + VDragEvent drag = VDragAndDropManager.get().startDrag( + t, mouseDownEvent, true); + + drag.createDragImage(nodeCaptionDiv, true); + event.stopPropagation(); + + mouseDownEvent = null; + } + } else if (type == Event.ONMOUSEUP) { + mouseDownEvent = null; + } + if (type == Event.ONMOUSEOVER) { + mouseDownEvent = null; + currentMouseOverKey = key; + event.stopPropagation(); + } + + } else if (type == Event.ONMOUSEDOWN + && event.getButton() == NativeEvent.BUTTON_LEFT) { + event.preventDefault(); // text selection + } + } + + /** + * Checks if the given element is the caption or the icon. + * + * @param target + * The element to check + * @return true if the element is the caption or the icon + */ + public boolean isCaptionElement(com.google.gwt.dom.client.Element target) { + return (target == nodeCaptionSpan || (icon != null && target == icon + .getElement())); + } + + private void fireClick(final Event evt) { + /* + * Ensure we have focus in tree before sending variables. Otherwise + * previously modified field may contain dirty variables. + */ + if (!treeHasFocus) { + if (BrowserInfo.get().isOpera()) { + if (focusedNode == null) { + getNodeByKey(key).setFocused(true); + } else { + focusedNode.setFocused(true); + } + } else { + focus(); + } + } + final MouseEventDetails details = MouseEventDetailsBuilder + .buildMouseEventDetails(evt); + ScheduledCommand command = new ScheduledCommand() { + @Override + public void execute() { + // Determine if we should send the event immediately to the + // server. We do not want to send the event if there is a + // selection event happening after this. In all other cases + // we want to send it immediately. + boolean sendClickEventNow = true; + + if (details.getButton() == NativeEvent.BUTTON_LEFT + && immediate && selectable) { + // Probably a selection that will cause a value change + // event to be sent + sendClickEventNow = false; + + // The exception is that user clicked on the + // currently selected row and null selection is not + // allowed == no selection event + if (isSelected() && selectedIds.size() == 1 + && !isNullSelectionAllowed) { + sendClickEventNow = true; + } + } + + client.updateVariable(paintableId, "clickedKey", key, false); + client.updateVariable(paintableId, "clickEvent", + details.toString(), sendClickEventNow); + } + }; + if (treeHasFocus) { + command.execute(); + } else { + /* + * Webkits need a deferring due to FocusImplSafari uses timeout + */ + Scheduler.get().scheduleDeferred(command); + } + } + + private void toggleSelection() { + if (selectable) { + VTree.this.setSelected(this, !isSelected()); + } + } + + private void toggleState() { + setState(!getState(), true); + } + + protected void constructDom() { + addStyleName(CLASSNAME); + + nodeCaptionDiv = DOM.createDiv(); + DOM.setElementProperty(nodeCaptionDiv, "className", CLASSNAME + + "-caption"); + Element wrapper = DOM.createDiv(); + nodeCaptionSpan = DOM.createSpan(); + DOM.appendChild(getElement(), nodeCaptionDiv); + DOM.appendChild(nodeCaptionDiv, wrapper); + DOM.appendChild(wrapper, nodeCaptionSpan); + + if (BrowserInfo.get().isOpera()) { + /* + * Focus the caption div of the node to get keyboard navigation + * to work without scrolling up or down when focusing a node. + */ + nodeCaptionDiv.setTabIndex(-1); + } + + childNodeContainer = new FlowPanel(); + childNodeContainer.setStyleName(CLASSNAME + "-children"); + setWidget(childNodeContainer); + } + + public boolean isLeaf() { + String[] styleNames = getStyleName().split(" "); + for (String styleName : styleNames) { + if (styleName.equals(CLASSNAME + "-leaf")) { + return true; + } + } + return false; + } + + void setState(boolean state, boolean notifyServer) { + if (open == state) { + return; + } + if (state) { + if (!childrenLoaded && notifyServer) { + client.updateVariable(paintableId, "requestChildTree", + true, false); + } + if (notifyServer) { + client.updateVariable(paintableId, "expand", + new String[] { key }, true); + } + addStyleName(CLASSNAME + "-expanded"); + childNodeContainer.setVisible(true); + + } else { + removeStyleName(CLASSNAME + "-expanded"); + childNodeContainer.setVisible(false); + if (notifyServer) { + client.updateVariable(paintableId, "collapse", + new String[] { key }, true); + } + } + open = state; + + if (!rendering) { + Util.notifyParentOfSizeChange(VTree.this, false); + } + } + + boolean getState() { + return open; + } + + void setText(String text) { + DOM.setInnerText(nodeCaptionSpan, text); + } + + public boolean isChildrenLoaded() { + return childrenLoaded; + } + + /** + * Returns the children of the node + * + * @return A set of tree nodes + */ + public List<TreeNode> getChildren() { + List<TreeNode> nodes = new LinkedList<TreeNode>(); + + if (!isLeaf() && isChildrenLoaded()) { + Iterator<Widget> iter = childNodeContainer.iterator(); + while (iter.hasNext()) { + TreeNode node = (TreeNode) iter.next(); + nodes.add(node); + } + } + return nodes; + } + + @Override + public Action[] getActions() { + if (actionKeys == null) { + return new Action[] {}; + } + final Action[] actions = new Action[actionKeys.length]; + for (int i = 0; i < actions.length; i++) { + final String actionKey = actionKeys[i]; + final TreeAction a = new TreeAction(this, String.valueOf(key), + actionKey); + a.setCaption(getActionCaption(actionKey)); + a.setIconUrl(getActionIcon(actionKey)); + actions[i] = a; + } + return actions; + } + + @Override + public ApplicationConnection getClient() { + return client; + } + + @Override + public String getPaintableId() { + return paintableId; + } + + /** + * Adds/removes Vaadin specific style name. This method ought to be + * called only from VTree. + * + * @param selected + */ + protected void setSelected(boolean selected) { + // add style name to caption dom structure only, not to subtree + setStyleName(nodeCaptionDiv, "v-tree-node-selected", selected); + } + + protected boolean isSelected() { + return VTree.this.isSelected(this); + } + + /** + * Travels up the hierarchy looking for this node + * + * @param child + * The child which grandparent this is or is not + * @return True if this is a grandparent of the child node + */ + public boolean isGrandParentOf(TreeNode child) { + TreeNode currentNode = child; + boolean isGrandParent = false; + while (currentNode != null) { + currentNode = currentNode.getParentNode(); + if (currentNode == this) { + isGrandParent = true; + break; + } + } + return isGrandParent; + } + + public boolean isSibling(TreeNode node) { + return node.getParentNode() == getParentNode(); + } + + public void showContextMenu(Event event) { + if (!readonly && !disabled) { + if (actionKeys != null) { + int left = event.getClientX(); + int top = event.getClientY(); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + client.getContextMenu().showAt(this, left, top); + } + event.stopPropagation(); + event.preventDefault(); + } + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.Widget#onDetach() + */ + @Override + protected void onDetach() { + super.onDetach(); + client.getContextMenu().ensureHidden(this); + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.UIObject#toString() + */ + @Override + public String toString() { + return nodeCaptionSpan.getInnerText(); + } + + /** + * Is the node focused? + * + * @param focused + * True if focused, false if not + */ + public void setFocused(boolean focused) { + if (!this.focused && focused) { + nodeCaptionDiv.addClassName(CLASSNAME_FOCUSED); + + this.focused = focused; + if (BrowserInfo.get().isOpera()) { + nodeCaptionDiv.focus(); + } + treeHasFocus = true; + } else if (this.focused && !focused) { + nodeCaptionDiv.removeClassName(CLASSNAME_FOCUSED); + this.focused = focused; + treeHasFocus = false; + } + } + + /** + * Scrolls the caption into view + */ + public void scrollIntoView() { + Util.scrollIntoViewVertically(nodeCaptionDiv); + } + + public void setIcon(String iconUrl) { + if (iconUrl != null) { + // Add icon if not present + if (icon == null) { + icon = new Icon(client); + DOM.insertBefore(DOM.getFirstChild(nodeCaptionDiv), + icon.getElement(), nodeCaptionSpan); + } + icon.setUri(iconUrl); + } else { + // Remove icon if present + if (icon != null) { + DOM.removeChild(DOM.getFirstChild(nodeCaptionDiv), + icon.getElement()); + icon = null; + } + } + } + + public void setNodeStyleName(String styleName) { + addStyleName(TreeNode.CLASSNAME + "-" + styleName); + setStyleName(nodeCaptionDiv, TreeNode.CLASSNAME + "-caption-" + + styleName, true); + childNodeContainer.addStyleName(TreeNode.CLASSNAME + "-children-" + + styleName); + + } + + } + + @Override + public VDropHandler getDropHandler() { + return dropHandler; + } + + public TreeNode getNodeByKey(String key) { + return keyToNode.get(key); + } + + /** + * Deselects all items in the tree + */ + public void deselectAll() { + for (String key : selectedIds) { + TreeNode node = keyToNode.get(key); + if (node != null) { + node.setSelected(false); + } + } + selectedIds.clear(); + selectionHasChanged = true; + } + + /** + * Selects a range of nodes + * + * @param startNodeKey + * The start node key + * @param endNodeKey + * The end node key + */ + private void selectNodeRange(String startNodeKey, String endNodeKey) { + + TreeNode startNode = keyToNode.get(startNodeKey); + TreeNode endNode = keyToNode.get(endNodeKey); + + // The nodes have the same parent + if (startNode.getParent() == endNode.getParent()) { + doSiblingSelection(startNode, endNode); + + // The start node is a grandparent of the end node + } else if (startNode.isGrandParentOf(endNode)) { + doRelationSelection(startNode, endNode); + + // The end node is a grandparent of the start node + } else if (endNode.isGrandParentOf(startNode)) { + doRelationSelection(endNode, startNode); + + } else { + doNoRelationSelection(startNode, endNode); + } + } + + /** + * Selects a node and deselect all other nodes + * + * @param node + * The node to select + */ + private void selectNode(TreeNode node, boolean deselectPrevious) { + if (deselectPrevious) { + deselectAll(); + } + + if (node != null) { + node.setSelected(true); + selectedIds.add(node.key); + lastSelection = node; + } + selectionHasChanged = true; + } + + /** + * Deselects a node + * + * @param node + * The node to deselect + */ + private void deselectNode(TreeNode node) { + node.setSelected(false); + selectedIds.remove(node.key); + selectionHasChanged = true; + } + + /** + * Selects all the open children to a node + * + * @param node + * The parent node + */ + private void selectAllChildren(TreeNode node, boolean includeRootNode) { + if (includeRootNode) { + node.setSelected(true); + selectedIds.add(node.key); + } + + for (TreeNode child : node.getChildren()) { + if (!child.isLeaf() && child.getState()) { + selectAllChildren(child, true); + } else { + child.setSelected(true); + selectedIds.add(child.key); + } + } + selectionHasChanged = true; + } + + /** + * Selects all children until a stop child is reached + * + * @param root + * The root not to start from + * @param stopNode + * The node to finish with + * @param includeRootNode + * Should the root node be selected + * @param includeStopNode + * Should the stop node be selected + * + * @return Returns false if the stop child was found, else true if all + * children was selected + */ + private boolean selectAllChildrenUntil(TreeNode root, TreeNode stopNode, + boolean includeRootNode, boolean includeStopNode) { + if (includeRootNode) { + root.setSelected(true); + selectedIds.add(root.key); + } + if (root.getState() && root != stopNode) { + for (TreeNode child : root.getChildren()) { + if (!child.isLeaf() && child.getState() && child != stopNode) { + if (!selectAllChildrenUntil(child, stopNode, true, + includeStopNode)) { + return false; + } + } else if (child == stopNode) { + if (includeStopNode) { + child.setSelected(true); + selectedIds.add(child.key); + } + return false; + } else { + child.setSelected(true); + selectedIds.add(child.key); + } + } + } + selectionHasChanged = true; + + return true; + } + + /** + * Select a range between two nodes which have no relation to each other + * + * @param startNode + * The start node to start the selection from + * @param endNode + * The end node to end the selection to + */ + private void doNoRelationSelection(TreeNode startNode, TreeNode endNode) { + + TreeNode commonParent = getCommonGrandParent(startNode, endNode); + TreeNode startBranch = null, endBranch = null; + + // Find the children of the common parent + List<TreeNode> children; + if (commonParent != null) { + children = commonParent.getChildren(); + } else { + children = getRootNodes(); + } + + // Find the start and end branches + for (TreeNode node : children) { + if (nodeIsInBranch(startNode, node)) { + startBranch = node; + } + if (nodeIsInBranch(endNode, node)) { + endBranch = node; + } + } + + // Swap nodes if necessary + if (children.indexOf(startBranch) > children.indexOf(endBranch)) { + TreeNode temp = startBranch; + startBranch = endBranch; + endBranch = temp; + + temp = startNode; + startNode = endNode; + endNode = temp; + } + + // Select all children under the start node + selectAllChildren(startNode, true); + TreeNode startParent = startNode.getParentNode(); + TreeNode currentNode = startNode; + while (startParent != null && startParent != commonParent) { + List<TreeNode> startChildren = startParent.getChildren(); + for (int i = startChildren.indexOf(currentNode) + 1; i < startChildren + .size(); i++) { + selectAllChildren(startChildren.get(i), true); + } + + currentNode = startParent; + startParent = startParent.getParentNode(); + } + + // Select nodes until the end node is reached + for (int i = children.indexOf(startBranch) + 1; i <= children + .indexOf(endBranch); i++) { + selectAllChildrenUntil(children.get(i), endNode, true, true); + } + + // Ensure end node was selected + endNode.setSelected(true); + selectedIds.add(endNode.key); + selectionHasChanged = true; + } + + /** + * Examines the children of the branch node and returns true if a node is in + * that branch + * + * @param node + * The node to search for + * @param branch + * The branch to search in + * @return True if found, false if not found + */ + private boolean nodeIsInBranch(TreeNode node, TreeNode branch) { + if (node == branch) { + return true; + } + for (TreeNode child : branch.getChildren()) { + if (child == node) { + return true; + } + if (!child.isLeaf() && child.getState()) { + if (nodeIsInBranch(node, child)) { + return true; + } + } + } + return false; + } + + /** + * Selects a range of items which are in direct relation with each other.<br/> + * NOTE: The start node <b>MUST</b> be before the end node! + * + * @param startNode + * + * @param endNode + */ + private void doRelationSelection(TreeNode startNode, TreeNode endNode) { + TreeNode currentNode = endNode; + while (currentNode != startNode) { + currentNode.setSelected(true); + selectedIds.add(currentNode.key); + + // Traverse children above the selection + List<TreeNode> subChildren = currentNode.getParentNode() + .getChildren(); + if (subChildren.size() > 1) { + selectNodeRange(subChildren.iterator().next().key, + currentNode.key); + } else if (subChildren.size() == 1) { + TreeNode n = subChildren.get(0); + n.setSelected(true); + selectedIds.add(n.key); + } + + currentNode = currentNode.getParentNode(); + } + startNode.setSelected(true); + selectedIds.add(startNode.key); + selectionHasChanged = true; + } + + /** + * Selects a range of items which have the same parent. + * + * @param startNode + * The start node + * @param endNode + * The end node + */ + private void doSiblingSelection(TreeNode startNode, TreeNode endNode) { + TreeNode parent = startNode.getParentNode(); + + List<TreeNode> children; + if (parent == null) { + // Topmost parent + children = getRootNodes(); + } else { + children = parent.getChildren(); + } + + // Swap start and end point if needed + if (children.indexOf(startNode) > children.indexOf(endNode)) { + TreeNode temp = startNode; + startNode = endNode; + endNode = temp; + } + + Iterator<TreeNode> childIter = children.iterator(); + boolean startFound = false; + while (childIter.hasNext()) { + TreeNode node = childIter.next(); + if (node == startNode) { + startFound = true; + } + + if (startFound && node != endNode && node.getState()) { + selectAllChildren(node, true); + } else if (startFound && node != endNode) { + node.setSelected(true); + selectedIds.add(node.key); + } + + if (node == endNode) { + node.setSelected(true); + selectedIds.add(node.key); + break; + } + } + selectionHasChanged = true; + } + + /** + * Returns the first common parent of two nodes + * + * @param node1 + * The first node + * @param node2 + * The second node + * @return The common parent or null + */ + public TreeNode getCommonGrandParent(TreeNode node1, TreeNode node2) { + // If either one does not have a grand parent then return null + if (node1.getParentNode() == null || node2.getParentNode() == null) { + return null; + } + + // If the nodes are parents of each other then return null + if (node1.isGrandParentOf(node2) || node2.isGrandParentOf(node1)) { + return null; + } + + // Get parents of node1 + List<TreeNode> parents1 = new ArrayList<TreeNode>(); + TreeNode parent1 = node1.getParentNode(); + while (parent1 != null) { + parents1.add(parent1); + parent1 = parent1.getParentNode(); + } + + // Get parents of node2 + List<TreeNode> parents2 = new ArrayList<TreeNode>(); + TreeNode parent2 = node2.getParentNode(); + while (parent2 != null) { + parents2.add(parent2); + parent2 = parent2.getParentNode(); + } + + // Search the parents for the first common parent + for (int i = 0; i < parents1.size(); i++) { + parent1 = parents1.get(i); + for (int j = 0; j < parents2.size(); j++) { + parent2 = parents2.get(j); + if (parent1 == parent2) { + return parent1; + } + } + } + + return null; + } + + /** + * Sets the node currently in focus + * + * @param node + * The node to focus or null to remove the focus completely + * @param scrollIntoView + * Scroll the node into view + */ + public void setFocusedNode(TreeNode node, boolean scrollIntoView) { + // Unfocus previously focused node + if (focusedNode != null) { + focusedNode.setFocused(false); + } + + if (node != null) { + node.setFocused(true); + } + + focusedNode = node; + + if (node != null && scrollIntoView) { + /* + * Delay scrolling the focused node into view if we are still + * rendering. #5396 + */ + if (!rendering) { + node.scrollIntoView(); + } else { + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + focusedNode.scrollIntoView(); + } + }); + } + } + } + + /** + * Focuses a node and scrolls it into view + * + * @param node + * The node to focus + */ + public void setFocusedNode(TreeNode node) { + setFocusedNode(node, true); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + @Override + public void onFocus(FocusEvent event) { + treeHasFocus = true; + // If no node has focus, focus the first item in the tree + if (focusedNode == null && lastSelection == null && selectable) { + setFocusedNode(getFirstRootNode(), false); + } else if (focusedNode != null && selectable) { + setFocusedNode(focusedNode, false); + } else if (lastSelection != null && selectable) { + setFocusedNode(lastSelection, false); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event + * .dom.client.BlurEvent) + */ + @Override + public void onBlur(BlurEvent event) { + treeHasFocus = false; + if (focusedNode != null) { + focusedNode.setFocused(false); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google + * .gwt.event.dom.client.KeyPressEvent) + */ + @Override + public void onKeyPress(KeyPressEvent event) { + NativeEvent nativeEvent = event.getNativeEvent(); + int keyCode = nativeEvent.getKeyCode(); + if (keyCode == 0 && nativeEvent.getCharCode() == ' ') { + // Provide a keyCode for space to be compatible with FireFox + // keypress event + keyCode = CHARCODE_SPACE; + } + if (handleKeyNavigation(keyCode, + event.isControlKeyDown() || event.isMetaKeyDown(), + event.isShiftKeyDown())) { + event.preventDefault(); + event.stopPropagation(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt + * .event.dom.client.KeyDownEvent) + */ + @Override + public void onKeyDown(KeyDownEvent event) { + if (handleKeyNavigation(event.getNativeEvent().getKeyCode(), + event.isControlKeyDown() || event.isMetaKeyDown(), + event.isShiftKeyDown())) { + event.preventDefault(); + event.stopPropagation(); + } + } + + /** + * Handles the keyboard navigation + * + * @param keycode + * The keycode of the pressed key + * @param ctrl + * Was ctrl pressed + * @param shift + * Was shift pressed + * @return Returns true if the key was handled, else false + */ + protected boolean handleKeyNavigation(int keycode, boolean ctrl, + boolean shift) { + // Navigate down + if (keycode == getNavigationDownKey()) { + TreeNode node = null; + // If node is open and has children then move in to the children + if (!focusedNode.isLeaf() && focusedNode.getState() + && focusedNode.getChildren().size() > 0) { + node = focusedNode.getChildren().get(0); + } + + // Else move down to the next sibling + else { + node = getNextSibling(focusedNode); + if (node == null) { + // Else jump to the parent and try to select the next + // sibling there + TreeNode current = focusedNode; + while (node == null && current.getParentNode() != null) { + node = getNextSibling(current.getParentNode()); + current = current.getParentNode(); + } + } + } + + if (node != null) { + setFocusedNode(node); + if (selectable) { + if (!ctrl && !shift) { + selectNode(node, true); + } else if (shift && isMultiselect) { + deselectAll(); + selectNodeRange(lastSelection.key, node.key); + } else if (shift) { + selectNode(node, true); + } + } + } + return true; + } + + // Navigate up + if (keycode == getNavigationUpKey()) { + TreeNode prev = getPreviousSibling(focusedNode); + TreeNode node = null; + if (prev != null) { + node = getLastVisibleChildInTree(prev); + } else if (focusedNode.getParentNode() != null) { + node = focusedNode.getParentNode(); + } + if (node != null) { + setFocusedNode(node); + if (selectable) { + if (!ctrl && !shift) { + selectNode(node, true); + } else if (shift && isMultiselect) { + deselectAll(); + selectNodeRange(lastSelection.key, node.key); + } else if (shift) { + selectNode(node, true); + } + } + } + return true; + } + + // Navigate left (close branch) + if (keycode == getNavigationLeftKey()) { + if (!focusedNode.isLeaf() && focusedNode.getState()) { + focusedNode.setState(false, true); + } else if (focusedNode.getParentNode() != null + && (focusedNode.isLeaf() || !focusedNode.getState())) { + + if (ctrl || !selectable) { + setFocusedNode(focusedNode.getParentNode()); + } else if (shift) { + doRelationSelection(focusedNode.getParentNode(), + focusedNode); + setFocusedNode(focusedNode.getParentNode()); + } else { + focusAndSelectNode(focusedNode.getParentNode()); + } + } + return true; + } + + // Navigate right (open branch) + if (keycode == getNavigationRightKey()) { + if (!focusedNode.isLeaf() && !focusedNode.getState()) { + focusedNode.setState(true, true); + } else if (!focusedNode.isLeaf()) { + if (ctrl || !selectable) { + setFocusedNode(focusedNode.getChildren().get(0)); + } else if (shift) { + setSelected(focusedNode, true); + setFocusedNode(focusedNode.getChildren().get(0)); + setSelected(focusedNode, true); + } else { + focusAndSelectNode(focusedNode.getChildren().get(0)); + } + } + return true; + } + + // Selection + if (keycode == getNavigationSelectKey()) { + if (!focusedNode.isSelected()) { + selectNode( + focusedNode, + (!isMultiselect || multiSelectMode == MULTISELECT_MODE_SIMPLE) + && selectable); + } else { + deselectNode(focusedNode); + } + return true; + } + + // Home selection + if (keycode == getNavigationStartKey()) { + TreeNode node = getFirstRootNode(); + if (ctrl || !selectable) { + setFocusedNode(node); + } else if (shift) { + deselectAll(); + selectNodeRange(focusedNode.key, node.key); + } else { + selectNode(node, true); + } + sendSelectionToServer(); + return true; + } + + // End selection + if (keycode == getNavigationEndKey()) { + TreeNode lastNode = getLastRootNode(); + TreeNode node = getLastVisibleChildInTree(lastNode); + if (ctrl || !selectable) { + setFocusedNode(node); + } else if (shift) { + deselectAll(); + selectNodeRange(focusedNode.key, node.key); + } else { + selectNode(node, true); + } + sendSelectionToServer(); + return true; + } + + return false; + } + + private void focusAndSelectNode(TreeNode node) { + /* + * Keyboard navigation doesn't work reliably if the tree is in + * multiselect mode as well as isNullSelectionAllowed = false. It first + * tries to deselect the old focused node, which fails since there must + * be at least one selection. After this the newly focused node is + * selected and we've ended up with two selected nodes even though we + * only navigated with the arrow keys. + * + * Because of this, we first select the next node and later de-select + * the old one. + */ + TreeNode oldFocusedNode = focusedNode; + setFocusedNode(node); + setSelected(focusedNode, true); + setSelected(oldFocusedNode, false); + } + + /** + * Traverses the tree to the bottom most child + * + * @param root + * The root of the tree + * @return The bottom most child + */ + private TreeNode getLastVisibleChildInTree(TreeNode root) { + if (root.isLeaf() || !root.getState() || root.getChildren().size() == 0) { + return root; + } + List<TreeNode> children = root.getChildren(); + return getLastVisibleChildInTree(children.get(children.size() - 1)); + } + + /** + * Gets the next sibling in the tree + * + * @param node + * The node to get the sibling for + * @return The sibling node or null if the node is the last sibling + */ + private TreeNode getNextSibling(TreeNode node) { + TreeNode parent = node.getParentNode(); + List<TreeNode> children; + if (parent == null) { + children = getRootNodes(); + } else { + children = parent.getChildren(); + } + + int idx = children.indexOf(node); + if (idx < children.size() - 1) { + return children.get(idx + 1); + } + + return null; + } + + /** + * Returns the previous sibling in the tree + * + * @param node + * The node to get the sibling for + * @return The sibling node or null if the node is the first sibling + */ + private TreeNode getPreviousSibling(TreeNode node) { + TreeNode parent = node.getParentNode(); + List<TreeNode> children; + if (parent == null) { + children = getRootNodes(); + } else { + children = parent.getChildren(); + } + + int idx = children.indexOf(node); + if (idx > 0) { + return children.get(idx - 1); + } + + return null; + } + + /** + * Add this to the element mouse down event by using element.setPropertyJSO + * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it then again + * when the mouse is depressed in the mouse up event. + * + * @return Returns the JSO preventing text selection + */ + private native JavaScriptObject applyDisableTextSelectionIEHack() + /*-{ + return function(){ return false; }; + }-*/; + + /** + * Get the key that moves the selection head upwards. By default it is the + * up arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationUpKey() { + return KeyCodes.KEY_UP; + } + + /** + * Get the key that moves the selection head downwards. By default it is the + * down arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationDownKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Get the key that scrolls to the left in the table. By default it is the + * left arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationLeftKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * Get the key that scroll to the right on the table. By default it is the + * right arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationRightKey() { + return KeyCodes.KEY_RIGHT; + } + + /** + * Get the key that selects an item in the table. By default it is the space + * bar key but by overriding this you can change the key to whatever you + * want. + * + * @return + */ + protected int getNavigationSelectKey() { + return CHARCODE_SPACE; + } + + /** + * Get the key the moves the selection one page up in the table. By default + * this is the Page Up key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationPageUpKey() { + return KeyCodes.KEY_PAGEUP; + } + + /** + * Get the key the moves the selection one page down in the table. By + * default this is the Page Down key but by overriding this you can change + * the key to whatever you want. + * + * @return + */ + protected int getNavigationPageDownKey() { + return KeyCodes.KEY_PAGEDOWN; + } + + /** + * Get the key the moves the selection to the beginning of the table. By + * default this is the Home key but by overriding this you can change the + * key to whatever you want. + * + * @return + */ + protected int getNavigationStartKey() { + return KeyCodes.KEY_HOME; + } + + /** + * Get the key the moves the selection to the end of the table. By default + * this is the End key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationEndKey() { + return KeyCodes.KEY_END; + } + + private final String SUBPART_NODE_PREFIX = "n"; + private final String EXPAND_IDENTIFIER = "expand"; + + /* + * In webkit, focus may have been requested for this component but not yet + * gained. Use this to trac if tree has gained the focus on webkit. See + * FocusImplSafari and #6373 + */ + private boolean treeHasFocus; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.SubPartAware#getSubPartElement(java + * .lang.String) + */ + @Override + public Element getSubPartElement(String subPart) { + if ("fe".equals(subPart)) { + if (BrowserInfo.get().isOpera() && focusedNode != null) { + return focusedNode.getElement(); + } + return getFocusElement(); + } + + if (subPart.startsWith(SUBPART_NODE_PREFIX + "[")) { + boolean expandCollapse = false; + + // Node + String[] nodes = subPart.split("/"); + TreeNode treeNode = null; + try { + for (String node : nodes) { + if (node.startsWith(SUBPART_NODE_PREFIX)) { + + // skip SUBPART_NODE_PREFIX"[" + node = node.substring(SUBPART_NODE_PREFIX.length() + 1); + // skip "]" + node = node.substring(0, node.length() - 1); + int position = Integer.parseInt(node); + if (treeNode == null) { + treeNode = getRootNodes().get(position); + } else { + treeNode = treeNode.getChildren().get(position); + } + } else if (node.startsWith(EXPAND_IDENTIFIER)) { + expandCollapse = true; + } + } + + if (expandCollapse) { + return treeNode.getElement(); + } else { + return treeNode.nodeCaptionSpan; + } + } catch (Exception e) { + // Invalid locator string or node could not be found + return null; + } + } + return null; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.SubPartAware#getSubPartName(com.google + * .gwt.user.client.Element) + */ + @Override + public String getSubPartName(Element subElement) { + // Supported identifiers: + // + // n[index]/n[index]/n[index]{/expand} + // + // Ends with "/expand" if the target is expand/collapse indicator, + // otherwise ends with the node + + boolean isExpandCollapse = false; + + if (!getElement().isOrHasChild(subElement)) { + return null; + } + + if (subElement == getFocusElement()) { + return "fe"; + } + + TreeNode treeNode = Util.findWidget(subElement, TreeNode.class); + if (treeNode == null) { + // Did not click on a node, let somebody else take care of the + // locator string + return null; + } + + if (subElement == treeNode.getElement()) { + // Targets expand/collapse arrow + isExpandCollapse = true; + } + + ArrayList<Integer> positions = new ArrayList<Integer>(); + while (treeNode.getParentNode() != null) { + positions.add(0, + treeNode.getParentNode().getChildren().indexOf(treeNode)); + treeNode = treeNode.getParentNode(); + } + positions.add(0, getRootNodes().indexOf(treeNode)); + + String locator = ""; + for (Integer i : positions) { + locator += SUBPART_NODE_PREFIX + "[" + i + "]/"; + } + + locator = locator.substring(0, locator.length() - 1); + if (isExpandCollapse) { + locator += "/" + EXPAND_IDENTIFIER; + } + return locator; + } + + @Override + public Action[] getActions() { + if (bodyActionKeys == null) { + return new Action[] {}; + } + final Action[] actions = new Action[bodyActionKeys.length]; + for (int i = 0; i < actions.length; i++) { + final String actionKey = bodyActionKeys[i]; + final TreeAction a = new TreeAction(this, null, actionKey); + a.setCaption(getActionCaption(actionKey)); + a.setIconUrl(getActionIcon(actionKey)); + actions[i] = a; + } + return actions; + } + + @Override + public ApplicationConnection getClient() { + return client; + } + + @Override + public String getPaintableId() { + return paintableId; + } + + private void handleBodyContextMenu(ContextMenuEvent event) { + if (!readonly && !disabled) { + if (bodyActionKeys != null) { + int left = event.getNativeEvent().getClientX(); + int top = event.getNativeEvent().getClientY(); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + client.getContextMenu().showAt(this, left, top); + } + event.stopPropagation(); + event.preventDefault(); + } + } + + public void registerAction(String key, String caption, String iconUrl) { + actionMap.put(key + "_c", caption); + if (iconUrl != null) { + actionMap.put(key + "_i", iconUrl); + } else { + actionMap.remove(key + "_i"); + } + + } + + public void registerNode(TreeNode treeNode) { + keyToNode.put(treeNode.key, treeNode); + } + + public void clearNodeToKeyMap() { + keyToNode.clear(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/treetable/TreeTableConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/treetable/TreeTableConnector.java new file mode 100644 index 0000000000..21f78c2356 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/treetable/TreeTableConnector.java @@ -0,0 +1,108 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.treetable; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.treetable.TreeTableConstants; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.FocusableScrollPanel; +import com.vaadin.terminal.gwt.client.ui.table.TableConnector; +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.VScrollTableBody.VScrollTableRow; +import com.vaadin.terminal.gwt.client.ui.treetable.VTreeTable.PendingNavigationEvent; +import com.vaadin.ui.TreeTable; + +@Connect(TreeTable.class) +public class TreeTableConnector extends TableConnector { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + FocusableScrollPanel widget = null; + int scrollPosition = 0; + if (getWidget().collapseRequest) { + widget = (FocusableScrollPanel) getWidget().getWidget(1); + scrollPosition = widget.getScrollPosition(); + } + getWidget().animationsEnabled = uidl.getBooleanAttribute("animate"); + getWidget().colIndexOfHierarchy = uidl + .hasAttribute(TreeTableConstants.ATTRIBUTE_HIERARCHY_COLUMN_INDEX) ? uidl + .getIntAttribute(TreeTableConstants.ATTRIBUTE_HIERARCHY_COLUMN_INDEX) + : 0; + int oldTotalRows = getWidget().getTotalRows(); + super.updateFromUIDL(uidl, client); + if (getWidget().collapseRequest) { + if (getWidget().collapsedRowKey != null + && getWidget().scrollBody != null) { + VScrollTableRow row = getWidget().getRenderedRowByKey( + getWidget().collapsedRowKey); + if (row != null) { + getWidget().setRowFocus(row); + getWidget().focus(); + } + } + + int scrollPosition2 = widget.getScrollPosition(); + if (scrollPosition != scrollPosition2) { + widget.setScrollPosition(scrollPosition); + } + + // check which rows are needed from the server and initiate a + // deferred fetch + getWidget().onScroll(null); + } + // Recalculate table size if collapse request, or if page length is zero + // (not sent by server) and row count changes (#7908). + if (getWidget().collapseRequest + || (!uidl.hasAttribute("pagelength") && getWidget() + .getTotalRows() != oldTotalRows)) { + /* + * Ensure that possibly removed/added scrollbars are considered. + * Triggers row calculations, removes cached rows etc. Basically + * cleans up state. Be careful if touching this, you will break + * pageLength=0 if you remove this. + */ + getWidget().triggerLazyColumnAdjustment(true); + + getWidget().collapseRequest = false; + } + if (uidl.hasAttribute("focusedRow")) { + String key = uidl.getStringAttribute("focusedRow"); + getWidget().setRowFocus(getWidget().getRenderedRowByKey(key)); + getWidget().focusParentResponsePending = false; + } else if (uidl.hasAttribute("clearFocusPending")) { + // Special case to detect a response to a focusParent request that + // does not return any focusedRow because the selected node has no + // parent + getWidget().focusParentResponsePending = false; + } + + while (!getWidget().collapseRequest + && !getWidget().focusParentResponsePending + && !getWidget().pendingNavigationEvents.isEmpty()) { + // Keep replaying any queued events as long as we don't have any + // potential content changes pending + PendingNavigationEvent event = getWidget().pendingNavigationEvents + .removeFirst(); + getWidget() + .handleNavigation(event.keycode, event.ctrl, event.shift); + } + } + + @Override + public VTreeTable getWidget() { + return (VTreeTable) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/treetable/VTreeTable.java b/client/src/com/vaadin/terminal/gwt/client/ui/treetable/VTreeTable.java new file mode 100644 index 0000000000..a8621190ae --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/treetable/VTreeTable.java @@ -0,0 +1,842 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.treetable; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import com.google.gwt.animation.client.Animation; +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.ImageElement; +import com.google.gwt.dom.client.SpanElement; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.Style.Visibility; +import com.google.gwt.dom.client.TableCellElement; +import com.google.gwt.event.dom.client.KeyCodes; +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.ui.Widget; +import com.vaadin.terminal.gwt.client.ComputedStyle; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable; +import com.vaadin.terminal.gwt.client.ui.treetable.VTreeTable.VTreeTableScrollBody.VTreeTableRow; + +public class VTreeTable extends VScrollTable { + + static class PendingNavigationEvent { + final int keycode; + final boolean ctrl; + final boolean shift; + + public PendingNavigationEvent(int keycode, boolean ctrl, boolean shift) { + this.keycode = keycode; + this.ctrl = ctrl; + this.shift = shift; + } + + @Override + public String toString() { + String string = "Keyboard event: " + keycode; + if (ctrl) { + string += " + ctrl"; + } + if (shift) { + string += " + shift"; + } + return string; + } + } + + boolean collapseRequest; + private boolean selectionPending; + int colIndexOfHierarchy; + String collapsedRowKey; + VTreeTableScrollBody scrollBody; + boolean animationsEnabled; + LinkedList<PendingNavigationEvent> pendingNavigationEvents = new LinkedList<VTreeTable.PendingNavigationEvent>(); + boolean focusParentResponsePending; + + @Override + protected VScrollTableBody createScrollBody() { + scrollBody = new VTreeTableScrollBody(); + return scrollBody; + } + + /* + * Overridden to allow animation of expands and collapses of nodes. + */ + @Override + protected void addAndRemoveRows(UIDL partialRowAdditions) { + if (partialRowAdditions == null) { + return; + } + + if (animationsEnabled) { + if (partialRowAdditions.hasAttribute("hide")) { + scrollBody.unlinkRowsAnimatedAndUpdateCacheWhenFinished( + partialRowAdditions.getIntAttribute("firstprowix"), + partialRowAdditions.getIntAttribute("numprows")); + } else { + scrollBody.insertRowsAnimated(partialRowAdditions, + partialRowAdditions.getIntAttribute("firstprowix"), + partialRowAdditions.getIntAttribute("numprows")); + discardRowsOutsideCacheWindow(); + } + } else { + super.addAndRemoveRows(partialRowAdditions); + } + } + + class VTreeTableScrollBody extends VScrollTable.VScrollTableBody { + private int indentWidth = -1; + + VTreeTableScrollBody() { + super(); + } + + @Override + protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) { + if (uidl.hasAttribute("gen_html")) { + // This is a generated row. + return new VTreeTableGeneratedRow(uidl, aligns2); + } + return new VTreeTableRow(uidl, aligns2); + } + + class VTreeTableRow extends + VScrollTable.VScrollTableBody.VScrollTableRow { + + private boolean isTreeCellAdded = false; + private SpanElement treeSpacer; + private boolean open; + private int depth; + private boolean canHaveChildren; + protected Widget widgetInHierarchyColumn; + + public VTreeTableRow(UIDL uidl, char[] aligns2) { + super(uidl, aligns2); + } + + @Override + public void addCell(UIDL rowUidl, String text, char align, + String style, boolean textIsHTML, boolean isSorted, + String description) { + super.addCell(rowUidl, text, align, style, textIsHTML, + isSorted, description); + + addTreeSpacer(rowUidl); + } + + protected boolean addTreeSpacer(UIDL rowUidl) { + if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) { + Element container = (Element) getElement().getLastChild() + .getFirstChild(); + + if (rowUidl.hasAttribute("icon")) { + // icons are in first content cell in TreeTable + ImageElement icon = Document.get().createImageElement(); + icon.setClassName("v-icon"); + icon.setAlt("icon"); + icon.setSrc(client.translateVaadinUri(rowUidl + .getStringAttribute("icon"))); + container.insertFirst(icon); + } + + String classname = "v-treetable-treespacer"; + if (rowUidl.getBooleanAttribute("ca")) { + canHaveChildren = true; + open = rowUidl.getBooleanAttribute("open"); + classname += open ? " v-treetable-node-open" + : " v-treetable-node-closed"; + } + + treeSpacer = Document.get().createSpanElement(); + + treeSpacer.setClassName(classname); + container.insertFirst(treeSpacer); + depth = rowUidl.hasAttribute("depth") ? rowUidl + .getIntAttribute("depth") : 0; + setIndent(); + isTreeCellAdded = true; + return true; + } + return false; + } + + private boolean cellShowsTreeHierarchy(int curColIndex) { + if (isTreeCellAdded) { + return false; + } + return curColIndex == colIndexOfHierarchy + + (showRowHeaders ? 1 : 0); + } + + @Override + public void onBrowserEvent(Event event) { + if (event.getEventTarget().cast() == treeSpacer + && treeSpacer.getClassName().contains("node")) { + if (event.getTypeInt() == Event.ONMOUSEUP) { + sendToggleCollapsedUpdate(getKey()); + } + return; + } + super.onBrowserEvent(event); + } + + @Override + public void addCell(UIDL rowUidl, Widget w, char align, + String style, boolean isSorted) { + super.addCell(rowUidl, w, align, style, isSorted); + if (addTreeSpacer(rowUidl)) { + widgetInHierarchyColumn = w; + } + + } + + private void setIndent() { + if (getIndentWidth() > 0) { + treeSpacer.getParentElement().getStyle() + .setPaddingLeft(getIndent(), Unit.PX); + treeSpacer.getStyle().setWidth(getIndent(), Unit.PX); + } + } + + @Override + protected void onAttach() { + super.onAttach(); + if (getIndentWidth() < 0) { + detectIndent(this); + } + } + + private int getHierarchyAndIconWidth() { + int consumedSpace = treeSpacer.getOffsetWidth(); + if (treeSpacer.getParentElement().getChildCount() > 2) { + // icon next to tree spacer + consumedSpace += ((com.google.gwt.dom.client.Element) treeSpacer + .getNextSibling()).getOffsetWidth(); + } + return consumedSpace; + } + + @Override + protected void setCellWidth(int cellIx, int width) { + if (cellIx == colIndexOfHierarchy + (showRowHeaders ? 1 : 0)) { + // take indentation padding into account if this is the + // hierarchy column + width = Math.max(width - getIndent(), 0); + } + super.setCellWidth(cellIx, width); + } + + private int getIndent() { + return (depth + 1) * getIndentWidth(); + } + } + + protected class VTreeTableGeneratedRow extends VTreeTableRow { + private boolean spanColumns; + private boolean htmlContentAllowed; + + public VTreeTableGeneratedRow(UIDL uidl, char[] aligns) { + super(uidl, aligns); + addStyleName("v-table-generated-row"); + } + + public boolean isSpanColumns() { + return spanColumns; + } + + @Override + protected void initCellWidths() { + if (spanColumns) { + setSpannedColumnWidthAfterDOMFullyInited(); + } else { + super.initCellWidths(); + } + } + + private void setSpannedColumnWidthAfterDOMFullyInited() { + // Defer setting width on spanned columns to make sure that + // they are added to the DOM before trying to calculate + // widths. + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + + @Override + public void execute() { + if (showRowHeaders) { + setCellWidth(0, tHead.getHeaderCell(0).getWidth()); + calcAndSetSpanWidthOnCell(1); + } else { + calcAndSetSpanWidthOnCell(0); + } + } + }); + } + + @Override + protected boolean isRenderHtmlInCells() { + return htmlContentAllowed; + } + + @Override + protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col, + int visibleColumnIndex) { + htmlContentAllowed = uidl.getBooleanAttribute("gen_html"); + spanColumns = uidl.getBooleanAttribute("gen_span"); + + final Iterator<?> cells = uidl.getChildIterator(); + if (spanColumns) { + int colCount = uidl.getChildCount(); + if (cells.hasNext()) { + final Object cell = cells.next(); + if (cell instanceof String) { + addSpannedCell(uidl, cell.toString(), aligns[0], + "", htmlContentAllowed, false, null, + colCount); + } else { + addSpannedCell(uidl, (Widget) cell, aligns[0], "", + false, colCount); + } + } + } else { + super.addCellsFromUIDL(uidl, aligns, col, + visibleColumnIndex); + } + } + + private void addSpannedCell(UIDL rowUidl, Widget w, char align, + String style, boolean sorted, int colCount) { + TableCellElement td = DOM.createTD().cast(); + td.setColSpan(colCount); + initCellWithWidget(w, align, style, sorted, td); + td.getStyle().setHeight(getRowHeight(), Unit.PX); + if (addTreeSpacer(rowUidl)) { + widgetInHierarchyColumn = w; + } + } + + private void addSpannedCell(UIDL rowUidl, String text, char align, + String style, boolean textIsHTML, boolean sorted, + String description, int colCount) { + // String only content is optimized by not using Label widget + final TableCellElement td = DOM.createTD().cast(); + td.setColSpan(colCount); + initCellWithText(text, align, style, textIsHTML, sorted, + description, td); + td.getStyle().setHeight(getRowHeight(), Unit.PX); + addTreeSpacer(rowUidl); + } + + @Override + protected void setCellWidth(int cellIx, int width) { + if (isSpanColumns()) { + if (showRowHeaders) { + if (cellIx == 0) { + super.setCellWidth(0, width); + } else { + // We need to recalculate the spanning TDs width for + // every cellIx in order to support column resizing. + calcAndSetSpanWidthOnCell(1); + } + } else { + // Same as above. + calcAndSetSpanWidthOnCell(0); + } + } else { + super.setCellWidth(cellIx, width); + } + } + + private void calcAndSetSpanWidthOnCell(final int cellIx) { + int spanWidth = 0; + for (int ix = (showRowHeaders ? 1 : 0); ix < tHead + .getVisibleCellCount(); ix++) { + spanWidth += tHead.getHeaderCell(ix).getOffsetWidth(); + } + Util.setWidthExcludingPaddingAndBorder((Element) getElement() + .getChild(cellIx), spanWidth, 13, false); + } + } + + private int getIndentWidth() { + return indentWidth; + } + + private void detectIndent(VTreeTableRow vTreeTableRow) { + indentWidth = vTreeTableRow.treeSpacer.getOffsetWidth(); + if (indentWidth == 0) { + indentWidth = -1; + return; + } + Iterator<Widget> iterator = iterator(); + while (iterator.hasNext()) { + VTreeTableRow next = (VTreeTableRow) iterator.next(); + next.setIndent(); + } + } + + protected void unlinkRowsAnimatedAndUpdateCacheWhenFinished( + final int firstIndex, final int rows) { + List<VScrollTableRow> rowsToDelete = new ArrayList<VScrollTableRow>(); + for (int ix = firstIndex; ix < firstIndex + rows; ix++) { + VScrollTableRow row = getRowByRowIndex(ix); + if (row != null) { + rowsToDelete.add(row); + } + } + if (!rowsToDelete.isEmpty()) { + // #8810 Only animate if there's something to animate + RowCollapseAnimation anim = new RowCollapseAnimation( + rowsToDelete) { + @Override + protected void onComplete() { + super.onComplete(); + // Actually unlink the rows and update the cache after + // the + // animation is done. + unlinkAndReindexRows(firstIndex, rows); + discardRowsOutsideCacheWindow(); + ensureCacheFilled(); + } + }; + anim.run(150); + } + } + + protected List<VScrollTableRow> insertRowsAnimated(UIDL rowData, + int firstIndex, int rows) { + List<VScrollTableRow> insertedRows = insertAndReindexRows(rowData, + firstIndex, rows); + if (!insertedRows.isEmpty()) { + // Only animate if there's something to animate (#8810) + RowExpandAnimation anim = new RowExpandAnimation(insertedRows); + anim.run(150); + } + return insertedRows; + } + + /** + * Prepares the table for animation by copying the background colors of + * all TR elements to their respective TD elements if the TD element is + * transparent. This is needed, since if TDs have transparent + * backgrounds, the rows sliding behind them are visible. + */ + private class AnimationPreparator { + private final int lastItemIx; + + public AnimationPreparator(int lastItemIx) { + this.lastItemIx = lastItemIx; + } + + public void prepareTableForAnimation() { + int ix = lastItemIx; + VScrollTableRow row = null; + while ((row = getRowByRowIndex(ix)) != null) { + copyTRBackgroundsToTDs(row); + --ix; + } + } + + private void copyTRBackgroundsToTDs(VScrollTableRow row) { + Element tr = row.getElement(); + ComputedStyle cs = new ComputedStyle(tr); + String backgroundAttachment = cs + .getProperty("backgroundAttachment"); + String backgroundClip = cs.getProperty("backgroundClip"); + String backgroundColor = cs.getProperty("backgroundColor"); + String backgroundImage = cs.getProperty("backgroundImage"); + String backgroundOrigin = cs.getProperty("backgroundOrigin"); + for (int ix = 0; ix < tr.getChildCount(); ix++) { + Element td = tr.getChild(ix).cast(); + if (!elementHasBackground(td)) { + td.getStyle().setProperty("backgroundAttachment", + backgroundAttachment); + td.getStyle().setProperty("backgroundClip", + backgroundClip); + td.getStyle().setProperty("backgroundColor", + backgroundColor); + td.getStyle().setProperty("backgroundImage", + backgroundImage); + td.getStyle().setProperty("backgroundOrigin", + backgroundOrigin); + } + } + } + + private boolean elementHasBackground(Element element) { + ComputedStyle cs = new ComputedStyle(element); + String clr = cs.getProperty("backgroundColor"); + String img = cs.getProperty("backgroundImage"); + return !("rgba(0, 0, 0, 0)".equals(clr.trim()) + || "transparent".equals(clr.trim()) || img == null); + } + + public void restoreTableAfterAnimation() { + int ix = lastItemIx; + VScrollTableRow row = null; + while ((row = getRowByRowIndex(ix)) != null) { + restoreStyleForTDsInRow(row); + + --ix; + } + } + + private void restoreStyleForTDsInRow(VScrollTableRow row) { + Element tr = row.getElement(); + for (int ix = 0; ix < tr.getChildCount(); ix++) { + Element td = tr.getChild(ix).cast(); + td.getStyle().clearProperty("backgroundAttachment"); + td.getStyle().clearProperty("backgroundClip"); + td.getStyle().clearProperty("backgroundColor"); + td.getStyle().clearProperty("backgroundImage"); + td.getStyle().clearProperty("backgroundOrigin"); + } + } + } + + /** + * Animates row expansion using the GWT animation framework. + * + * The idea is as follows: + * + * 1. Insert all rows normally + * + * 2. Insert a newly created DIV containing a new TABLE element below + * the DIV containing the actual scroll table body. + * + * 3. Clone the rows that were inserted in step 1 and attach the clones + * to the new TABLE element created in step 2. + * + * 4. The new DIV from step 2 is absolutely positioned so that the last + * inserted row is just behind the row that was expanded. + * + * 5. Hide the contents of the originally inserted rows by setting the + * DIV.v-table-cell-wrapper to display:none;. + * + * 6. Set the height of the originally inserted rows to 0. + * + * 7. The animation loop slides the DIV from step 2 downwards, while at + * the same pace growing the height of each of the inserted rows from 0 + * to full height. The first inserted row grows from 0 to full and after + * this the second row grows from 0 to full, etc until all rows are full + * height. + * + * 8. Remove the DIV from step 2 + * + * 9. Restore display:block; to the DIV.v-table-cell-wrapper elements. + * + * 10. DONE + */ + private class RowExpandAnimation extends Animation { + + private final List<VScrollTableRow> rows; + private Element cloneDiv; + private Element cloneTable; + private AnimationPreparator preparator; + + /** + * @param rows + * List of rows to animate. Must not be empty. + */ + public RowExpandAnimation(List<VScrollTableRow> rows) { + this.rows = rows; + buildAndInsertAnimatingDiv(); + preparator = new AnimationPreparator(rows.get(0).getIndex() - 1); + preparator.prepareTableForAnimation(); + for (VScrollTableRow row : rows) { + cloneAndAppendRow(row); + row.addStyleName("v-table-row-animating"); + setCellWrapperDivsToDisplayNone(row); + row.setHeight(getInitialHeight()); + } + } + + protected String getInitialHeight() { + return "0px"; + } + + private void cloneAndAppendRow(VScrollTableRow row) { + Element clonedTR = null; + clonedTR = row.getElement().cloneNode(true).cast(); + clonedTR.getStyle().setVisibility(Visibility.VISIBLE); + cloneTable.appendChild(clonedTR); + } + + protected double getBaseOffset() { + return rows.get(0).getAbsoluteTop() + - rows.get(0).getParent().getAbsoluteTop() + - rows.size() * getRowHeight(); + } + + private void buildAndInsertAnimatingDiv() { + cloneDiv = DOM.createDiv(); + cloneDiv.addClassName("v-treetable-animation-clone-wrapper"); + cloneTable = DOM.createTable(); + cloneTable.addClassName("v-treetable-animation-clone"); + cloneDiv.appendChild(cloneTable); + insertAnimatingDiv(); + } + + private void insertAnimatingDiv() { + Element tableBody = getElement().cast(); + Element tableBodyParent = tableBody.getParentElement().cast(); + tableBodyParent.insertAfter(cloneDiv, tableBody); + } + + @Override + protected void onUpdate(double progress) { + animateDiv(progress); + animateRowHeights(progress); + } + + private void animateDiv(double progress) { + double offset = calculateDivOffset(progress, getRowHeight()); + + cloneDiv.getStyle().setTop(getBaseOffset() + offset, Unit.PX); + } + + private void animateRowHeights(double progress) { + double rh = getRowHeight(); + double vlh = calculateHeightOfAllVisibleLines(progress, rh); + int ix = 0; + + while (ix < rows.size()) { + double height = vlh < rh ? vlh : rh; + rows.get(ix).setHeight(height + "px"); + vlh -= height; + ix++; + } + } + + protected double calculateHeightOfAllVisibleLines(double progress, + double rh) { + return rows.size() * rh * progress; + } + + protected double calculateDivOffset(double progress, double rh) { + return progress * rows.size() * rh; + } + + @Override + protected void onComplete() { + preparator.restoreTableAfterAnimation(); + for (VScrollTableRow row : rows) { + resetCellWrapperDivsDisplayProperty(row); + row.removeStyleName("v-table-row-animating"); + } + Element tableBodyParent = (Element) getElement() + .getParentElement(); + tableBodyParent.removeChild(cloneDiv); + } + + private void setCellWrapperDivsToDisplayNone(VScrollTableRow row) { + Element tr = row.getElement(); + for (int ix = 0; ix < tr.getChildCount(); ix++) { + getWrapperDiv(tr, ix).getStyle().setDisplay(Display.NONE); + } + } + + private Element getWrapperDiv(Element tr, int tdIx) { + Element td = tr.getChild(tdIx).cast(); + return td.getChild(0).cast(); + } + + private void resetCellWrapperDivsDisplayProperty(VScrollTableRow row) { + Element tr = row.getElement(); + for (int ix = 0; ix < tr.getChildCount(); ix++) { + getWrapperDiv(tr, ix).getStyle().clearProperty("display"); + } + } + + } + + /** + * This is the inverse of the RowExpandAnimation and is implemented by + * extending it and overriding the calculation of offsets and heights. + */ + private class RowCollapseAnimation extends RowExpandAnimation { + + private final List<VScrollTableRow> rows; + + /** + * @param rows + * List of rows to animate. Must not be empty. + */ + public RowCollapseAnimation(List<VScrollTableRow> rows) { + super(rows); + this.rows = rows; + } + + @Override + protected String getInitialHeight() { + return getRowHeight() + "px"; + } + + @Override + protected double getBaseOffset() { + return getRowHeight(); + } + + @Override + protected double calculateHeightOfAllVisibleLines(double progress, + double rh) { + return rows.size() * rh * (1 - progress); + } + + @Override + protected double calculateDivOffset(double progress, double rh) { + return -super.calculateDivOffset(progress, rh); + } + } + } + + /** + * Icons rendered into first actual column in TreeTable, not to row header + * cell + */ + @Override + protected String buildCaptionHtmlSnippet(UIDL uidl) { + if (uidl.getTag().equals("column")) { + return super.buildCaptionHtmlSnippet(uidl); + } else { + String s = uidl.getStringAttribute("caption"); + return s; + } + } + + @Override + protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { + if (collapseRequest || focusParentResponsePending) { + // Enqueue the event if there might be pending content changes from + // the server + if (pendingNavigationEvents.size() < 10) { + // Only keep 10 keyboard events in the queue + PendingNavigationEvent pendingNavigationEvent = new PendingNavigationEvent( + keycode, ctrl, shift); + pendingNavigationEvents.add(pendingNavigationEvent); + } + return true; + } + + VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow(); + if (focusedRow != null) { + if (focusedRow.canHaveChildren + && ((keycode == KeyCodes.KEY_RIGHT && !focusedRow.open) || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) { + if (!ctrl) { + client.updateVariable(paintableId, "selectCollapsed", true, + false); + } + sendSelectedRows(false); + sendToggleCollapsedUpdate(focusedRow.getKey()); + return true; + } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) { + // already expanded, move selection down if next is on a deeper + // level (is-a-child) + VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow + .getParent(); + Iterator<Widget> iterator = body.iterator(); + VTreeTableRow next = null; + while (iterator.hasNext()) { + next = (VTreeTableRow) iterator.next(); + if (next == focusedRow) { + next = (VTreeTableRow) iterator.next(); + break; + } + } + if (next != null) { + if (next.depth > focusedRow.depth) { + selectionPending = true; + return super.handleNavigation(getNavigationDownKey(), + ctrl, shift); + } + } else { + // Note, a minor change here for a bit false behavior if + // cache rows is disabled + last visible row + no childs for + // the node + selectionPending = true; + return super.handleNavigation(getNavigationDownKey(), ctrl, + shift); + } + } else if (keycode == KeyCodes.KEY_LEFT) { + // already collapsed move selection up to parent node + // do on the server side as the parent is not necessary + // rendered on the client, could check if parent is visible if + // a performance issue arises + + client.updateVariable(paintableId, "focusParent", + focusedRow.getKey(), true); + + // Set flag that we should enqueue navigation events until we + // get a response to this request + focusParentResponsePending = true; + + return true; + } + } + return super.handleNavigation(keycode, ctrl, shift); + } + + private void sendToggleCollapsedUpdate(String rowKey) { + collapsedRowKey = rowKey; + collapseRequest = true; + client.updateVariable(paintableId, "toggleCollapsed", rowKey, true); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONKEYUP && selectionPending) { + sendSelectedRows(); + } + } + + @Override + protected void sendSelectedRows(boolean immediately) { + super.sendSelectedRows(immediately); + selectionPending = false; + } + + @Override + protected void reOrderColumn(String columnKey, int newIndex) { + super.reOrderColumn(columnKey, newIndex); + // current impl not intelligent enough to survive without visiting the + // server to redraw content + client.sendPendingVariableChanges(); + } + + @Override + public void setStyleName(String style) { + super.setStyleName(style + " v-treetable"); + } + + @Override + protected void updateTotalRows(UIDL uidl) { + // Make sure that initializedAndAttached & al are not reset when the + // totalrows are updated on expand/collapse requests. + int newTotalRows = uidl.getIntAttribute("totalrows"); + setTotalRows(newTotalRows); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/TwinColSelectConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/TwinColSelectConnector.java new file mode 100644 index 0000000000..c4a5437149 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/TwinColSelectConnector.java @@ -0,0 +1,77 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.twincolselect; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.DirectionalManagedLayout; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.optiongroup.OptionGroupBaseConnector; +import com.vaadin.ui.TwinColSelect; + +@Connect(TwinColSelect.class) +public class TwinColSelectConnector extends OptionGroupBaseConnector implements + DirectionalManagedLayout { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + // Captions are updated before super call to ensure the widths are set + // correctly + if (isRealUpdate(uidl)) { + getWidget().updateCaptions(uidl); + getLayoutManager().setNeedsHorizontalLayout(this); + } + + super.updateFromUIDL(uidl, client); + } + + @Override + protected void init() { + super.init(); + getLayoutManager().registerDependency(this, + getWidget().captionWrapper.getElement()); + } + + @Override + public void onUnregister() { + getLayoutManager().unregisterDependency(this, + getWidget().captionWrapper.getElement()); + } + + @Override + public VTwinColSelect getWidget() { + return (VTwinColSelect) super.getWidget(); + } + + @Override + public void layoutVertically() { + if (isUndefinedHeight()) { + getWidget().clearInternalHeights(); + } else { + getWidget().setInternalHeights(); + } + } + + @Override + public void layoutHorizontally() { + if (isUndefinedWidth()) { + getWidget().clearInternalWidths(); + } else { + getWidget().setInternalWidths(); + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/VTwinColSelect.java b/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/VTwinColSelect.java new file mode 100644 index 0000000000..fef44eb502 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/VTwinColSelect.java @@ -0,0 +1,621 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.twincolselect; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.event.dom.client.ClickEvent; +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.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.MouseDownEvent; +import com.google.gwt.event.dom.client.MouseDownHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.Panel; +import com.vaadin.shared.ui.twincolselect.TwinColSelectConstants; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.SubPartAware; +import com.vaadin.terminal.gwt.client.ui.button.VButton; +import com.vaadin.terminal.gwt.client.ui.optiongroup.VOptionGroupBase; + +public class VTwinColSelect extends VOptionGroupBase implements KeyDownHandler, + MouseDownHandler, DoubleClickHandler, SubPartAware { + + private static final String CLASSNAME = "v-select-twincol"; + + private static final int VISIBLE_COUNT = 10; + + private static final int DEFAULT_COLUMN_COUNT = 10; + + private final DoubleClickListBox options; + + private final DoubleClickListBox selections; + + FlowPanel captionWrapper; + + private HTML optionsCaption = null; + + private HTML selectionsCaption = null; + + private final VButton add; + + private final VButton remove; + + private final FlowPanel buttons; + + private final Panel panel; + + /** + * A ListBox which catches double clicks + * + */ + public class DoubleClickListBox extends ListBox implements + HasDoubleClickHandlers { + public DoubleClickListBox(boolean isMultipleSelect) { + super(isMultipleSelect); + } + + public DoubleClickListBox() { + super(); + } + + @Override + public HandlerRegistration addDoubleClickHandler( + DoubleClickHandler handler) { + return addDomHandler(handler, DoubleClickEvent.getType()); + } + } + + public VTwinColSelect() { + super(CLASSNAME); + + captionWrapper = new FlowPanel(); + + options = new DoubleClickListBox(); + options.addClickHandler(this); + options.addDoubleClickHandler(this); + options.setVisibleItemCount(VISIBLE_COUNT); + options.setStyleName(CLASSNAME + "-options"); + + selections = new DoubleClickListBox(); + selections.addClickHandler(this); + selections.addDoubleClickHandler(this); + selections.setVisibleItemCount(VISIBLE_COUNT); + selections.setStyleName(CLASSNAME + "-selections"); + + buttons = new FlowPanel(); + buttons.setStyleName(CLASSNAME + "-buttons"); + add = new VButton(); + add.setText(">>"); + add.addClickHandler(this); + remove = new VButton(); + remove.setText("<<"); + remove.addClickHandler(this); + + panel = ((Panel) optionsContainer); + + panel.add(captionWrapper); + captionWrapper.getElement().getStyle().setOverflow(Overflow.HIDDEN); + // Hide until there actually is a caption to prevent IE from rendering + // extra empty space + captionWrapper.setVisible(false); + + panel.add(options); + buttons.add(add); + final HTML br = new HTML("<span/>"); + br.setStyleName(CLASSNAME + "-deco"); + buttons.add(br); + buttons.add(remove); + panel.add(buttons); + panel.add(selections); + + options.addKeyDownHandler(this); + options.addMouseDownHandler(this); + + selections.addMouseDownHandler(this); + selections.addKeyDownHandler(this); + } + + public HTML getOptionsCaption() { + if (optionsCaption == null) { + optionsCaption = new HTML(); + optionsCaption.setStyleName(CLASSNAME + "-caption-left"); + optionsCaption.getElement().getStyle() + .setFloat(com.google.gwt.dom.client.Style.Float.LEFT); + captionWrapper.add(optionsCaption); + } + + return optionsCaption; + } + + public HTML getSelectionsCaption() { + if (selectionsCaption == null) { + selectionsCaption = new HTML(); + selectionsCaption.setStyleName(CLASSNAME + "-caption-right"); + selectionsCaption.getElement().getStyle() + .setFloat(com.google.gwt.dom.client.Style.Float.RIGHT); + captionWrapper.add(selectionsCaption); + } + + return selectionsCaption; + } + + protected void updateCaptions(UIDL uidl) { + String leftCaption = (uidl + .hasAttribute(TwinColSelectConstants.ATTRIBUTE_LEFT_CAPTION) ? uidl + .getStringAttribute(TwinColSelectConstants.ATTRIBUTE_LEFT_CAPTION) + : null); + String rightCaption = (uidl + .hasAttribute(TwinColSelectConstants.ATTRIBUTE_RIGHT_CAPTION) ? uidl + .getStringAttribute(TwinColSelectConstants.ATTRIBUTE_RIGHT_CAPTION) + : null); + + boolean hasCaptions = (leftCaption != null || rightCaption != null); + + if (leftCaption == null) { + removeOptionsCaption(); + } else { + getOptionsCaption().setText(leftCaption); + + } + + if (rightCaption == null) { + removeSelectionsCaption(); + } else { + getSelectionsCaption().setText(rightCaption); + } + + captionWrapper.setVisible(hasCaptions); + } + + private void removeOptionsCaption() { + if (optionsCaption == null) { + return; + } + + if (optionsCaption.getParent() != null) { + captionWrapper.remove(optionsCaption); + } + + optionsCaption = null; + } + + private void removeSelectionsCaption() { + if (selectionsCaption == null) { + return; + } + + if (selectionsCaption.getParent() != null) { + captionWrapper.remove(selectionsCaption); + } + + selectionsCaption = null; + } + + @Override + protected void buildOptions(UIDL uidl) { + final boolean enabled = !isDisabled() && !isReadonly(); + options.setMultipleSelect(isMultiselect()); + selections.setMultipleSelect(isMultiselect()); + options.setEnabled(enabled); + selections.setEnabled(enabled); + add.setEnabled(enabled); + remove.setEnabled(enabled); + options.clear(); + selections.clear(); + for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) { + final UIDL optionUidl = (UIDL) i.next(); + if (optionUidl.hasAttribute("selected")) { + selections.addItem(optionUidl.getStringAttribute("caption"), + optionUidl.getStringAttribute("key")); + } else { + options.addItem(optionUidl.getStringAttribute("caption"), + optionUidl.getStringAttribute("key")); + } + } + + if (getRows() > 0) { + options.setVisibleItemCount(getRows()); + selections.setVisibleItemCount(getRows()); + + } + + } + + @Override + protected String[] getSelectedItems() { + final ArrayList<String> selectedItemKeys = new ArrayList<String>(); + for (int i = 0; i < selections.getItemCount(); i++) { + selectedItemKeys.add(selections.getValue(i)); + } + return selectedItemKeys.toArray(new String[selectedItemKeys.size()]); + } + + private boolean[] getSelectionBitmap(ListBox listBox) { + final boolean[] selectedIndexes = new boolean[listBox.getItemCount()]; + for (int i = 0; i < listBox.getItemCount(); i++) { + if (listBox.isItemSelected(i)) { + selectedIndexes[i] = true; + } else { + selectedIndexes[i] = false; + } + } + return selectedIndexes; + } + + private void addItem() { + Set<String> movedItems = moveSelectedItems(options, selections); + selectedKeys.addAll(movedItems); + + client.updateVariable(paintableId, "selected", + selectedKeys.toArray(new String[selectedKeys.size()]), + isImmediate()); + } + + private void removeItem() { + Set<String> movedItems = moveSelectedItems(selections, options); + selectedKeys.removeAll(movedItems); + + client.updateVariable(paintableId, "selected", + selectedKeys.toArray(new String[selectedKeys.size()]), + isImmediate()); + } + + private Set<String> moveSelectedItems(ListBox source, ListBox target) { + final boolean[] sel = getSelectionBitmap(source); + final Set<String> movedItems = new HashSet<String>(); + int lastSelected = 0; + for (int i = 0; i < sel.length; i++) { + if (sel[i]) { + final int optionIndex = i + - (sel.length - source.getItemCount()); + movedItems.add(source.getValue(optionIndex)); + + // Move selection to another column + final String text = source.getItemText(optionIndex); + final String value = source.getValue(optionIndex); + target.addItem(text, value); + target.setItemSelected(target.getItemCount() - 1, true); + source.removeItem(optionIndex); + + if (source.getItemCount() > 0) { + lastSelected = optionIndex > 0 ? optionIndex - 1 : 0; + } + } + } + + if (source.getItemCount() > 0) { + source.setSelectedIndex(lastSelected); + } + + // If no items are left move the focus to the selections + if (source.getItemCount() == 0) { + target.setFocus(true); + } else { + source.setFocus(true); + } + + return movedItems; + } + + @Override + public void onClick(ClickEvent event) { + super.onClick(event); + if (event.getSource() == add) { + addItem(); + + } else if (event.getSource() == remove) { + removeItem(); + + } else if (event.getSource() == options) { + // unselect all in other list, to avoid mistakes (i.e wrong button) + final int c = selections.getItemCount(); + for (int i = 0; i < c; i++) { + selections.setItemSelected(i, false); + } + } else if (event.getSource() == selections) { + // unselect all in other list, to avoid mistakes (i.e wrong button) + final int c = options.getItemCount(); + for (int i = 0; i < c; i++) { + options.setItemSelected(i, false); + } + } + } + + void clearInternalHeights() { + selections.setHeight(""); + options.setHeight(""); + } + + void setInternalHeights() { + int captionHeight = Util.getRequiredHeight(captionWrapper); + int totalHeight = getOffsetHeight(); + + String selectHeight = (totalHeight - captionHeight) + "px"; + + selections.setHeight(selectHeight); + options.setHeight(selectHeight); + + } + + void clearInternalWidths() { + int cols = -1; + if (getColumns() > 0) { + cols = getColumns(); + } else { + cols = DEFAULT_COLUMN_COUNT; + } + + if (cols >= 0) { + String colWidth = cols + "em"; + String containerWidth = (2 * cols + 4) + "em"; + // Caption wrapper width == optionsSelect + buttons + + // selectionsSelect + String captionWrapperWidth = (2 * cols + 4 - 0.5) + "em"; + + options.setWidth(colWidth); + if (optionsCaption != null) { + optionsCaption.setWidth(colWidth); + } + selections.setWidth(colWidth); + if (selectionsCaption != null) { + selectionsCaption.setWidth(colWidth); + } + buttons.setWidth("3.5em"); + optionsContainer.setWidth(containerWidth); + captionWrapper.setWidth(captionWrapperWidth); + } + } + + void setInternalWidths() { + DOM.setStyleAttribute(getElement(), "position", "relative"); + int bordersAndPaddings = Util.measureHorizontalPaddingAndBorder( + buttons.getElement(), 0); + + int buttonWidth = Util.getRequiredWidth(buttons); + int totalWidth = getOffsetWidth(); + + int spaceForSelect = (totalWidth - buttonWidth - bordersAndPaddings) / 2; + + options.setWidth(spaceForSelect + "px"); + if (optionsCaption != null) { + optionsCaption.setWidth(spaceForSelect + "px"); + } + + selections.setWidth(spaceForSelect + "px"); + if (selectionsCaption != null) { + selectionsCaption.setWidth(spaceForSelect + "px"); + } + captionWrapper.setWidth("100%"); + } + + @Override + protected void setTabIndex(int tabIndex) { + options.setTabIndex(tabIndex); + selections.setTabIndex(tabIndex); + add.setTabIndex(tabIndex); + remove.setTabIndex(tabIndex); + } + + @Override + public void focus() { + options.setFocus(true); + } + + /** + * Get the key that selects an item in the table. By default it is the Enter + * key but by overriding this you can change the key to whatever you want. + * + * @return + */ + protected int getNavigationSelectKey() { + return KeyCodes.KEY_ENTER; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt + * .event.dom.client.KeyDownEvent) + */ + @Override + public void onKeyDown(KeyDownEvent event) { + int keycode = event.getNativeKeyCode(); + + // Catch tab and move between select:s + if (keycode == KeyCodes.KEY_TAB && event.getSource() == options) { + // Prevent default behavior + event.preventDefault(); + + // Remove current selections + for (int i = 0; i < options.getItemCount(); i++) { + options.setItemSelected(i, false); + } + + // Focus selections + selections.setFocus(true); + } + + if (keycode == KeyCodes.KEY_TAB && event.isShiftKeyDown() + && event.getSource() == selections) { + // Prevent default behavior + event.preventDefault(); + + // Remove current selections + for (int i = 0; i < selections.getItemCount(); i++) { + selections.setItemSelected(i, false); + } + + // Focus options + options.setFocus(true); + } + + if (keycode == getNavigationSelectKey()) { + // Prevent default behavior + event.preventDefault(); + + // Decide which select the selection was made in + if (event.getSource() == options) { + // Prevents the selection to become a single selection when + // using Enter key + // as the selection key (default) + options.setFocus(false); + + addItem(); + + } else if (event.getSource() == selections) { + // Prevents the selection to become a single selection when + // using Enter key + // as the selection key (default) + selections.setFocus(false); + + removeItem(); + } + } + + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.MouseDownHandler#onMouseDown(com.google + * .gwt.event.dom.client.MouseDownEvent) + */ + @Override + public void onMouseDown(MouseDownEvent event) { + // Ensure that items are deselected when selecting + // from a different source. See #3699 for details. + if (event.getSource() == options) { + for (int i = 0; i < selections.getItemCount(); i++) { + selections.setItemSelected(i, false); + } + } else if (event.getSource() == selections) { + for (int i = 0; i < options.getItemCount(); i++) { + options.setItemSelected(i, false); + } + } + + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.DoubleClickHandler#onDoubleClick(com. + * google.gwt.event.dom.client.DoubleClickEvent) + */ + @Override + public void onDoubleClick(DoubleClickEvent event) { + if (event.getSource() == options) { + addItem(); + options.setSelectedIndex(-1); + options.setFocus(false); + } else if (event.getSource() == selections) { + removeItem(); + selections.setSelectedIndex(-1); + selections.setFocus(false); + } + + } + + private static final String SUBPART_OPTION_SELECT = "leftSelect"; + private static final String SUBPART_OPTION_SELECT_ITEM = SUBPART_OPTION_SELECT + + "-item"; + private static final String SUBPART_SELECTION_SELECT = "rightSelect"; + private static final String SUBPART_SELECTION_SELECT_ITEM = SUBPART_SELECTION_SELECT + + "-item"; + private static final String SUBPART_LEFT_CAPTION = "leftCaption"; + private static final String SUBPART_RIGHT_CAPTION = "rightCaption"; + private static final String SUBPART_ADD_BUTTON = "add"; + private static final String SUBPART_REMOVE_BUTTON = "remove"; + + @Override + public Element getSubPartElement(String subPart) { + if (SUBPART_OPTION_SELECT.equals(subPart)) { + return options.getElement(); + } else if (subPart.startsWith(SUBPART_OPTION_SELECT_ITEM)) { + String idx = subPart.substring(SUBPART_OPTION_SELECT_ITEM.length()); + return (Element) options.getElement().getChild( + Integer.parseInt(idx)); + } else if (SUBPART_SELECTION_SELECT.equals(subPart)) { + return selections.getElement(); + } else if (subPart.startsWith(SUBPART_SELECTION_SELECT_ITEM)) { + String idx = subPart.substring(SUBPART_SELECTION_SELECT_ITEM + .length()); + return (Element) selections.getElement().getChild( + Integer.parseInt(idx)); + } else if (optionsCaption != null + && SUBPART_LEFT_CAPTION.equals(subPart)) { + return optionsCaption.getElement(); + } else if (selectionsCaption != null + && SUBPART_RIGHT_CAPTION.equals(subPart)) { + return selectionsCaption.getElement(); + } else if (SUBPART_ADD_BUTTON.equals(subPart)) { + return add.getElement(); + } else if (SUBPART_REMOVE_BUTTON.equals(subPart)) { + return remove.getElement(); + } + + return null; + } + + @Override + public String getSubPartName(Element subElement) { + if (optionsCaption != null + && optionsCaption.getElement().isOrHasChild(subElement)) { + return SUBPART_LEFT_CAPTION; + } else if (selectionsCaption != null + && selectionsCaption.getElement().isOrHasChild(subElement)) { + return SUBPART_RIGHT_CAPTION; + } else if (options.getElement().isOrHasChild(subElement)) { + if (options.getElement() == subElement) { + return SUBPART_OPTION_SELECT; + } else { + int idx = Util.getChildElementIndex(subElement); + return SUBPART_OPTION_SELECT_ITEM + idx; + } + } else if (selections.getElement().isOrHasChild(subElement)) { + if (selections.getElement() == subElement) { + return SUBPART_SELECTION_SELECT; + } else { + int idx = Util.getChildElementIndex(subElement); + return SUBPART_SELECTION_SELECT_ITEM + idx; + } + } else if (add.getElement().isOrHasChild(subElement)) { + return SUBPART_ADD_BUTTON; + } else if (remove.getElement().isOrHasChild(subElement)) { + return SUBPART_REMOVE_BUTTON; + } + + return null; + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadConnector.java new file mode 100644 index 0000000000..83be123eb9 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadConnector.java @@ -0,0 +1,73 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.upload; + +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector; +import com.vaadin.ui.Upload; + +@Connect(value = Upload.class, loadStyle = LoadStyle.LAZY) +public class UploadConnector extends AbstractComponentConnector implements + Paintable { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (!isRealUpdate(uidl)) { + return; + } + if (uidl.hasAttribute("notStarted")) { + getWidget().t.schedule(400); + return; + } + if (uidl.hasAttribute("forceSubmit")) { + getWidget().submit(); + return; + } + getWidget().setImmediate(getState().isImmediate()); + getWidget().client = client; + getWidget().paintableId = uidl.getId(); + getWidget().nextUploadId = uidl.getIntAttribute("nextid"); + final String action = client.translateVaadinUri(uidl + .getStringVariable("action")); + getWidget().element.setAction(action); + if (uidl.hasAttribute("buttoncaption")) { + getWidget().submitButton.setText(uidl + .getStringAttribute("buttoncaption")); + getWidget().submitButton.setVisible(true); + } else { + getWidget().submitButton.setVisible(false); + } + getWidget().fu.setName(getWidget().paintableId + "_file"); + + if (!isEnabled() || isReadOnly()) { + getWidget().disableUpload(); + } else if (!uidl.getBooleanAttribute("state")) { + // Enable the button only if an upload is not in progress + getWidget().enableUpload(); + getWidget().ensureTargetFrame(); + } + } + + @Override + public VUpload getWidget() { + return (VUpload) super.getWidget(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategy.java b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategy.java new file mode 100644 index 0000000000..e81b4909ba --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategy.java @@ -0,0 +1,37 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.upload; + +public class UploadIFrameOnloadStrategy { + + native void hookEvents(com.google.gwt.dom.client.Element iframe, + VUpload upload) + /*-{ + iframe.onload = $entry(function() { + upload.@com.vaadin.terminal.gwt.client.ui.upload.VUpload::onSubmitComplete()(); + }); + }-*/; + + /** + * @param iframe + * the iframe whose onLoad event is to be cleaned + */ + native void unHookEvents(com.google.gwt.dom.client.Element iframe) + /*-{ + iframe.onload = null; + }-*/; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategyIE.java b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategyIE.java new file mode 100644 index 0000000000..0f885261e3 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategyIE.java @@ -0,0 +1,41 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.upload; + +import com.google.gwt.dom.client.Element; + +/** + * IE does not have onload, detect onload via readystatechange + * + */ +public class UploadIFrameOnloadStrategyIE extends UploadIFrameOnloadStrategy { + @Override + native void hookEvents(Element iframe, VUpload upload) + /*-{ + iframe.onreadystatechange = $entry(function() { + if (iframe.readyState == 'complete') { + upload.@com.vaadin.terminal.gwt.client.ui.upload.VUpload::onSubmitComplete()(); + } + }); + }-*/; + + @Override + native void unHookEvents(Element iframe) + /*-{ + iframe.onreadystatechange = null; + }-*/; + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/upload/VUpload.java b/client/src/com/vaadin/terminal/gwt/client/ui/upload/VUpload.java new file mode 100644 index 0000000000..759236ea7f --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/upload/VUpload.java @@ -0,0 +1,319 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.upload; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.FormElement; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.Command; +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.FileUpload; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.FormPanel; +import com.google.gwt.user.client.ui.Hidden; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.button.VButton; + +/** + * + * Note, we are not using GWT FormPanel as we want to listen submitcomplete + * events even though the upload component is already detached. + * + */ +public class VUpload extends SimplePanel { + + private final class MyFileUpload extends FileUpload { + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONCHANGE) { + if (immediate && fu.getFilename() != null + && !"".equals(fu.getFilename())) { + submit(); + } + } else if (BrowserInfo.get().isIE() + && event.getTypeInt() == Event.ONFOCUS) { + // IE and user has clicked on hidden textarea part of upload + // field. Manually open file selector, other browsers do it by + // default. + fireNativeClick(fu.getElement()); + // also remove focus to enable hack if user presses cancel + // button + fireNativeBlur(fu.getElement()); + } + } + } + + public static final String CLASSNAME = "v-upload"; + + /** + * FileUpload component that opens native OS dialog to select file. + */ + FileUpload fu = new MyFileUpload(); + + Panel panel = new FlowPanel(); + + UploadIFrameOnloadStrategy onloadstrategy = GWT + .create(UploadIFrameOnloadStrategy.class); + + ApplicationConnection client; + + protected String paintableId; + + /** + * Button that initiates uploading + */ + protected final VButton submitButton; + + /** + * When expecting big files, programmer may initiate some UI changes when + * uploading the file starts. Bit after submitting file we'll visit the + * server to check possible changes. + */ + protected Timer t; + + /** + * some browsers tries to send form twice if submit is called in button + * click handler, some don't submit at all without it, so we need to track + * if form is already being submitted + */ + private boolean submitted = false; + + private boolean enabled = true; + + private boolean immediate; + + private Hidden maxfilesize = new Hidden(); + + protected FormElement element; + + private com.google.gwt.dom.client.Element synthesizedFrame; + + protected int nextUploadId; + + public VUpload() { + super(com.google.gwt.dom.client.Document.get().createFormElement()); + + element = getElement().cast(); + setEncoding(getElement(), FormPanel.ENCODING_MULTIPART); + element.setMethod(FormPanel.METHOD_POST); + + setWidget(panel); + panel.add(maxfilesize); + panel.add(fu); + submitButton = new VButton(); + submitButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (immediate) { + // fire click on upload (eg. focused button and hit space) + fireNativeClick(fu.getElement()); + } else { + submit(); + } + } + }); + panel.add(submitButton); + + setStyleName(CLASSNAME); + } + + private static native void setEncoding(Element form, String encoding) + /*-{ + form.enctype = encoding; + }-*/; + + protected void setImmediate(boolean booleanAttribute) { + if (immediate != booleanAttribute) { + immediate = booleanAttribute; + if (immediate) { + fu.sinkEvents(Event.ONCHANGE); + fu.sinkEvents(Event.ONFOCUS); + } + } + setStyleName(getElement(), CLASSNAME + "-immediate", immediate); + } + + private static native void fireNativeClick(Element element) + /*-{ + element.click(); + }-*/; + + private static native void fireNativeBlur(Element element) + /*-{ + element.blur(); + }-*/; + + protected void disableUpload() { + submitButton.setEnabled(false); + if (!submitted) { + // Cannot disable the fileupload while submitting or the file won't + // be submitted at all + fu.getElement().setPropertyBoolean("disabled", true); + } + enabled = false; + } + + protected void enableUpload() { + submitButton.setEnabled(true); + fu.getElement().setPropertyBoolean("disabled", false); + enabled = true; + if (submitted) { + /* + * An old request is still in progress (most likely cancelled), + * ditching that target frame to make it possible to send a new + * file. A new target frame is created later." + */ + cleanTargetFrame(); + submitted = false; + } + } + + /** + * Re-creates file input field and populates panel. This is needed as we + * want to clear existing values from our current file input field. + */ + private void rebuildPanel() { + panel.remove(submitButton); + panel.remove(fu); + fu = new MyFileUpload(); + fu.setName(paintableId + "_file"); + fu.getElement().setPropertyBoolean("disabled", !enabled); + panel.add(fu); + panel.add(submitButton); + if (immediate) { + fu.sinkEvents(Event.ONCHANGE); + } + } + + /** + * Called by JSNI (hooked via {@link #onloadstrategy}) + */ + private void onSubmitComplete() { + /* Needs to be run dereferred to avoid various browser issues. */ + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + if (submitted) { + if (client != null) { + if (t != null) { + t.cancel(); + } + VConsole.log("VUpload:Submit complete"); + client.sendPendingVariableChanges(); + } + + rebuildPanel(); + + submitted = false; + enableUpload(); + if (!isAttached()) { + /* + * Upload is complete when upload is already abandoned. + */ + cleanTargetFrame(); + } + } + } + }); + } + + protected void submit() { + if (fu.getFilename().length() == 0 || submitted || !enabled) { + VConsole.log("Submit cancelled (disabled, no file or already submitted)"); + return; + } + // flush possibly pending variable changes, so they will be handled + // before upload + client.sendPendingVariableChanges(); + + element.submit(); + submitted = true; + VConsole.log("Submitted form"); + + disableUpload(); + + /* + * Visit server a moment after upload has started to see possible + * changes from UploadStarted event. Will be cleared on complete. + */ + t = new Timer() { + @Override + public void run() { + VConsole.log("Visiting server to see if upload started event changed UI."); + client.updateVariable(paintableId, "pollForStart", + nextUploadId, true); + } + }; + t.schedule(800); + } + + @Override + protected void onAttach() { + super.onAttach(); + if (client != null) { + ensureTargetFrame(); + } + } + + protected void ensureTargetFrame() { + if (synthesizedFrame == null) { + // Attach a hidden IFrame to the form. This is the target iframe to + // which the form will be submitted. We have to create the iframe + // using innerHTML, because setting an iframe's 'name' property + // dynamically doesn't work on most browsers. + DivElement dummy = Document.get().createDivElement(); + dummy.setInnerHTML("<iframe src=\"javascript:''\" name='" + + getFrameName() + + "' style='position:absolute;width:0;height:0;border:0'>"); + synthesizedFrame = dummy.getFirstChildElement(); + Document.get().getBody().appendChild(synthesizedFrame); + element.setTarget(getFrameName()); + onloadstrategy.hookEvents(synthesizedFrame, this); + } + } + + private String getFrameName() { + return paintableId + "_TGT_FRAME"; + } + + @Override + protected void onDetach() { + super.onDetach(); + if (!submitted) { + cleanTargetFrame(); + } + } + + private void cleanTargetFrame() { + if (synthesizedFrame != null) { + Document.get().getBody().removeChild(synthesizedFrame); + onloadstrategy.unHookEvents(synthesizedFrame); + synthesizedFrame = null; + } + } +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/video/VVideo.java b/client/src/com/vaadin/terminal/gwt/client/ui/video/VVideo.java new file mode 100644 index 0000000000..5f8d1ee574 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/video/VVideo.java @@ -0,0 +1,71 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.video; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.VideoElement; +import com.google.gwt.user.client.Element; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.VMediaBase; + +public class VVideo extends VMediaBase { + + private static String CLASSNAME = "v-video"; + + private VideoElement video; + + public VVideo() { + video = Document.get().createVideoElement(); + setMediaElement(video); + setStyleName(CLASSNAME); + + updateDimensionsWhenMetadataLoaded(getElement()); + } + + /** + * Registers a listener that updates the dimensions of the widget when the + * video metadata has been loaded. + * + * @param el + */ + private native void updateDimensionsWhenMetadataLoaded(Element el) + /*-{ + var self = this; + el.addEventListener('loadedmetadata', $entry(function(e) { + self.@com.vaadin.terminal.gwt.client.ui.video.VVideo::updateElementDynamicSize(II)(el.videoWidth, el.videoHeight); + }), false); + + }-*/; + + /** + * Updates the dimensions of the widget. + * + * @param w + * @param h + */ + private void updateElementDynamicSize(int w, int h) { + video.getStyle().setWidth(w, Unit.PX); + video.getStyle().setHeight(h, Unit.PX); + Util.notifyParentOfSizeChange(this, true); + } + + public void setPoster(String poster) { + video.setPoster(poster); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/video/VideoConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/video/VideoConnector.java new file mode 100644 index 0000000000..2b228d14ea --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/video/VideoConnector.java @@ -0,0 +1,54 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.video; + +import com.vaadin.shared.communication.URLReference; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.video.VideoState; +import com.vaadin.terminal.gwt.client.communication.StateChangeEvent; +import com.vaadin.terminal.gwt.client.ui.MediaBaseConnector; +import com.vaadin.ui.Video; + +@Connect(Video.class) +public class VideoConnector extends MediaBaseConnector { + + @Override + public VideoState getState() { + return (VideoState) super.getState(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + URLReference poster = getState().getPoster(); + if (poster != null) { + getWidget().setPoster(poster.getURL()); + } else { + getWidget().setPoster(null); + } + } + + @Override + public VVideo getWidget() { + return (VVideo) super.getWidget(); + } + + @Override + protected String getDefaultAltHtml() { + return "Your browser does not support the <code>video</code> element."; + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/window/VWindow.java b/client/src/com/vaadin/terminal/gwt/client/ui/window/VWindow.java new file mode 100644 index 0000000000..1660eee246 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/window/VWindow.java @@ -0,0 +1,932 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.window; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.ScrollEvent; +import com.google.gwt.event.dom.client.ScrollHandler; +import com.google.gwt.user.client.Command; +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.Window; +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.shared.EventId; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.Console; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.FocusableScrollPanel; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; +import com.vaadin.terminal.gwt.client.ui.VLazyExecutor; +import com.vaadin.terminal.gwt.client.ui.VOverlay; +import com.vaadin.terminal.gwt.client.ui.notification.VNotification; + +/** + * "Sub window" component. + * + * @author Vaadin Ltd + */ +public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, + ScrollHandler, KeyDownHandler, FocusHandler, BlurHandler, Focusable { + + /** + * Minimum allowed height of a window. This refers to the content area, not + * the outer borders. + */ + private static final int MIN_CONTENT_AREA_HEIGHT = 100; + + /** + * Minimum allowed width of a window. This refers to the content area, not + * the outer borders. + */ + private static final int MIN_CONTENT_AREA_WIDTH = 150; + + private static ArrayList<VWindow> windowOrder = new ArrayList<VWindow>(); + + private static boolean orderingDefered; + + public static final String CLASSNAME = "v-window"; + + private static final int STACKING_OFFSET_PIXELS = 15; + + public static final int Z_INDEX = 10000; + + ComponentConnector layout; + + Element contents; + + Element header; + + Element footer; + + private Element resizeBox; + + final FocusableScrollPanel contentPanel = new FocusableScrollPanel(); + + private boolean dragging; + + private int startX; + + private int startY; + + private int origX; + + private int origY; + + private boolean resizing; + + private int origW; + + private int origH; + + Element closeBox; + + protected ApplicationConnection client; + + String id; + + ShortcutActionHandler shortcutHandler; + + /** Last known positionx read from UIDL or updated to application connection */ + private int uidlPositionX = -1; + + /** Last known positiony read from UIDL or updated to application connection */ + private int uidlPositionY = -1; + + boolean vaadinModality = false; + + boolean resizable = true; + + private boolean draggable = true; + + boolean resizeLazy = false; + + private Element modalityCurtain; + private Element draggingCurtain; + private Element resizingCurtain; + + private Element headerText; + + private boolean closable = true; + + // If centered (via UIDL), the window should stay in the centered -mode + // until a position is received from the server, or the user moves or + // resizes the window. + boolean centered = false; + + boolean immediate; + + private Element wrapper; + + boolean visibilityChangesDisabled; + + int bringToFrontSequence = -1; + + private VLazyExecutor delayedContentsSizeUpdater = new VLazyExecutor(200, + new ScheduledCommand() { + + @Override + public void execute() { + updateContentsSize(); + } + }); + + public VWindow() { + super(false, false, true); // no autohide, not modal, shadow + // Different style of shadow for windows + setShadowStyle("window"); + + constructDOM(); + contentPanel.addScrollHandler(this); + contentPanel.addKeyDownHandler(this); + contentPanel.addFocusHandler(this); + contentPanel.addBlurHandler(this); + } + + public void bringToFront() { + int curIndex = windowOrder.indexOf(this); + if (curIndex + 1 < windowOrder.size()) { + windowOrder.remove(this); + windowOrder.add(this); + for (; curIndex < windowOrder.size(); curIndex++) { + windowOrder.get(curIndex).setWindowOrder(curIndex); + } + } + } + + /** + * Returns true if this window is the topmost VWindow + * + * @return + */ + private boolean isActive() { + return equals(getTopmostWindow()); + } + + private static VWindow getTopmostWindow() { + return windowOrder.get(windowOrder.size() - 1); + } + + void setWindowOrderAndPosition() { + // This cannot be done in the constructor as the widgets are created in + // a different order than on they should appear on screen + if (windowOrder.contains(this)) { + // Already set + return; + } + final int order = windowOrder.size(); + setWindowOrder(order); + windowOrder.add(this); + setPopupPosition(order * STACKING_OFFSET_PIXELS, order + * STACKING_OFFSET_PIXELS); + + } + + private void setWindowOrder(int order) { + setZIndex(order + Z_INDEX); + } + + @Override + protected void setZIndex(int zIndex) { + super.setZIndex(zIndex); + if (vaadinModality) { + DOM.setStyleAttribute(getModalityCurtain(), "zIndex", "" + zIndex); + } + } + + protected Element getModalityCurtain() { + if (modalityCurtain == null) { + modalityCurtain = DOM.createDiv(); + modalityCurtain.setClassName(CLASSNAME + "-modalitycurtain"); + } + return modalityCurtain; + } + + protected void constructDOM() { + setStyleName(CLASSNAME); + + header = DOM.createDiv(); + DOM.setElementProperty(header, "className", CLASSNAME + "-outerheader"); + headerText = DOM.createDiv(); + DOM.setElementProperty(headerText, "className", CLASSNAME + "-header"); + contents = DOM.createDiv(); + DOM.setElementProperty(contents, "className", CLASSNAME + "-contents"); + footer = DOM.createDiv(); + DOM.setElementProperty(footer, "className", CLASSNAME + "-footer"); + resizeBox = DOM.createDiv(); + DOM.setElementProperty(resizeBox, "className", CLASSNAME + "-resizebox"); + closeBox = DOM.createDiv(); + DOM.setElementProperty(closeBox, "className", CLASSNAME + "-closebox"); + DOM.appendChild(footer, resizeBox); + + wrapper = DOM.createDiv(); + DOM.setElementProperty(wrapper, "className", CLASSNAME + "-wrap"); + + DOM.appendChild(wrapper, header); + DOM.appendChild(wrapper, closeBox); + DOM.appendChild(header, headerText); + DOM.appendChild(wrapper, contents); + DOM.appendChild(wrapper, footer); + DOM.appendChild(super.getContainerElement(), wrapper); + + sinkEvents(Event.MOUSEEVENTS | Event.TOUCHEVENTS | Event.ONCLICK + | Event.ONLOSECAPTURE); + + setWidget(contentPanel); + + } + + /** + * Calling this method will defer ordering algorithm, to order windows based + * on servers bringToFront and modality instructions. Non changed windows + * will be left intact. + */ + static void deferOrdering() { + if (!orderingDefered) { + orderingDefered = true; + Scheduler.get().scheduleFinally(new Command() { + + @Override + public void execute() { + doServerSideOrdering(); + VNotification.bringNotificationsToFront(); + } + }); + } + } + + private static void doServerSideOrdering() { + orderingDefered = false; + VWindow[] array = windowOrder.toArray(new VWindow[windowOrder.size()]); + Arrays.sort(array, new Comparator<VWindow>() { + + @Override + public int compare(VWindow o1, VWindow o2) { + /* + * Order by modality, then by bringtofront sequence. + */ + + if (o1.vaadinModality && !o2.vaadinModality) { + return 1; + } else if (!o1.vaadinModality && o2.vaadinModality) { + return -1; + } else if (o1.bringToFrontSequence > o2.bringToFrontSequence) { + return 1; + } else if (o1.bringToFrontSequence < o2.bringToFrontSequence) { + return -1; + } else { + return 0; + } + } + }); + for (int i = 0; i < array.length; i++) { + VWindow w = array[i]; + if (w.bringToFrontSequence != -1 || w.vaadinModality) { + w.bringToFront(); + w.bringToFrontSequence = -1; + } + } + } + + @Override + public void setVisible(boolean visible) { + /* + * Visibility with VWindow works differently than with other Paintables + * in Vaadin. Invisible VWindows are not attached to DOM at all. Flag is + * used to avoid visibility call from + * ApplicationConnection.updateComponent(); + */ + if (!visibilityChangesDisabled) { + super.setVisible(visible); + } + } + + void setDraggable(boolean draggable) { + if (this.draggable == draggable) { + return; + } + + this.draggable = draggable; + + setCursorProperties(); + } + + private void setCursorProperties() { + if (!draggable) { + header.getStyle().setProperty("cursor", "default"); + footer.getStyle().setProperty("cursor", "default"); + } else { + header.getStyle().setProperty("cursor", ""); + footer.getStyle().setProperty("cursor", ""); + } + } + + /** + * Sets the closable state of the window. Additionally hides/shows the close + * button according to the new state. + * + * @param closable + * true if the window can be closed by the user + */ + protected void setClosable(boolean closable) { + if (this.closable == closable) { + return; + } + + this.closable = closable; + if (closable) { + DOM.setStyleAttribute(closeBox, "display", ""); + } else { + DOM.setStyleAttribute(closeBox, "display", "none"); + } + + } + + /** + * Returns the closable state of the sub window. If the sub window is + * closable a decoration (typically an X) is shown to the user. By clicking + * on the X the user can close the window. + * + * @return true if the sub window is closable + */ + protected boolean isClosable() { + return closable; + } + + @Override + public void show() { + if (!windowOrder.contains(this)) { + // This is needed if the window is hidden and then shown again. + // Otherwise this VWindow is added to windowOrder in the + // constructor. + windowOrder.add(this); + } + + if (vaadinModality) { + showModalityCurtain(); + } + super.show(); + } + + @Override + public void hide() { + if (vaadinModality) { + hideModalityCurtain(); + } + super.hide(); + + // Remove window from windowOrder to avoid references being left + // hanging. + windowOrder.remove(this); + } + + void setVaadinModality(boolean modality) { + vaadinModality = modality; + if (vaadinModality) { + if (isAttached()) { + showModalityCurtain(); + } + deferOrdering(); + } else { + if (modalityCurtain != null) { + if (isAttached()) { + hideModalityCurtain(); + } + modalityCurtain = null; + } + } + } + + private void showModalityCurtain() { + DOM.setStyleAttribute(getModalityCurtain(), "zIndex", + "" + (windowOrder.indexOf(this) + Z_INDEX)); + if (isShowing()) { + RootPanel.getBodyElement().insertBefore(getModalityCurtain(), + getElement()); + } else { + DOM.appendChild(RootPanel.getBodyElement(), getModalityCurtain()); + } + } + + private void hideModalityCurtain() { + DOM.removeChild(RootPanel.getBodyElement(), modalityCurtain); + } + + /* + * Shows an empty div on top of all other content; used when moving, so that + * iframes (etc) do not steal event. + */ + private void showDraggingCurtain() { + DOM.appendChild(RootPanel.getBodyElement(), getDraggingCurtain()); + } + + private void hideDraggingCurtain() { + if (draggingCurtain != null) { + DOM.removeChild(RootPanel.getBodyElement(), draggingCurtain); + } + } + + /* + * Shows an empty div on top of all other content; used when resizing, so + * that iframes (etc) do not steal event. + */ + private void showResizingCurtain() { + DOM.appendChild(RootPanel.getBodyElement(), getResizingCurtain()); + } + + private void hideResizingCurtain() { + if (resizingCurtain != null) { + DOM.removeChild(RootPanel.getBodyElement(), resizingCurtain); + } + } + + private Element getDraggingCurtain() { + if (draggingCurtain == null) { + draggingCurtain = createCurtain(); + draggingCurtain.setClassName(CLASSNAME + "-draggingCurtain"); + } + + return draggingCurtain; + } + + private Element getResizingCurtain() { + if (resizingCurtain == null) { + resizingCurtain = createCurtain(); + resizingCurtain.setClassName(CLASSNAME + "-resizingCurtain"); + } + + return resizingCurtain; + } + + private Element createCurtain() { + Element curtain = DOM.createDiv(); + + DOM.setStyleAttribute(curtain, "position", "absolute"); + DOM.setStyleAttribute(curtain, "top", "0px"); + DOM.setStyleAttribute(curtain, "left", "0px"); + DOM.setStyleAttribute(curtain, "width", "100%"); + DOM.setStyleAttribute(curtain, "height", "100%"); + DOM.setStyleAttribute(curtain, "zIndex", "" + VOverlay.Z_INDEX); + + return curtain; + } + + void setResizable(boolean resizability) { + resizable = resizability; + if (resizability) { + DOM.setElementProperty(footer, "className", CLASSNAME + "-footer"); + DOM.setElementProperty(resizeBox, "className", CLASSNAME + + "-resizebox"); + } else { + DOM.setElementProperty(footer, "className", CLASSNAME + "-footer " + + CLASSNAME + "-footer-noresize"); + DOM.setElementProperty(resizeBox, "className", CLASSNAME + + "-resizebox " + CLASSNAME + "-resizebox-disabled"); + } + } + + @Override + public void setPopupPosition(int left, int top) { + if (top < 0) { + // ensure window is not moved out of browser window from top of the + // screen + top = 0; + } + super.setPopupPosition(left, top); + if (left != uidlPositionX && client != null) { + client.updateVariable(id, "positionx", left, false); + uidlPositionX = left; + } + if (top != uidlPositionY && client != null) { + client.updateVariable(id, "positiony", top, false); + uidlPositionY = top; + } + } + + public void setCaption(String c) { + setCaption(c, null); + } + + public void setCaption(String c, String icon) { + String html = Util.escapeHTML(c); + if (icon != null) { + icon = client.translateVaadinUri(icon); + html = "<img src=\"" + Util.escapeAttribute(icon) + + "\" class=\"v-icon\" />" + html; + } + DOM.setInnerHTML(headerText, html); + } + + @Override + protected Element getContainerElement() { + // in GWT 1.5 this method is used in PopupPanel constructor + if (contents == null) { + return super.getContainerElement(); + } + return contents; + } + + @Override + public void onBrowserEvent(final Event event) { + boolean bubble = true; + + final int type = event.getTypeInt(); + + final Element target = DOM.eventGetTarget(event); + + if (resizing || resizeBox == target) { + onResizeEvent(event); + bubble = false; + } else if (isClosable() && target == closeBox) { + if (type == Event.ONCLICK) { + onCloseClick(); + } + bubble = false; + } else if (dragging || !contents.isOrHasChild(target)) { + onDragEvent(event); + bubble = false; + } else if (type == Event.ONCLICK) { + // clicked inside window, ensure to be on top + if (!isActive()) { + bringToFront(); + } + } + + /* + * If clicking on other than the content, move focus to the window. + * After that this windows e.g. gets all keyboard shortcuts. + */ + if (type == Event.ONMOUSEDOWN + && !contentPanel.getElement().isOrHasChild(target) + && target != closeBox) { + contentPanel.focus(); + } + + if (!bubble) { + event.stopPropagation(); + } else { + // Super.onBrowserEvent takes care of Handlers added by the + // ClickEventHandler + super.onBrowserEvent(event); + } + } + + private void onCloseClick() { + client.updateVariable(id, "close", true, true); + } + + private void onResizeEvent(Event event) { + if (resizable && Util.isTouchEventOrLeftMouseButton(event)) { + switch (event.getTypeInt()) { + case Event.ONMOUSEDOWN: + case Event.ONTOUCHSTART: + if (!isActive()) { + bringToFront(); + } + showResizingCurtain(); + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(resizeBox, "visibility", "hidden"); + } + resizing = true; + startX = Util.getTouchOrMouseClientX(event); + startY = Util.getTouchOrMouseClientY(event); + origW = getElement().getOffsetWidth(); + origH = getElement().getOffsetHeight(); + DOM.setCapture(getElement()); + event.preventDefault(); + break; + case Event.ONMOUSEUP: + case Event.ONTOUCHEND: + setSize(event, true); + case Event.ONTOUCHCANCEL: + DOM.releaseCapture(getElement()); + case Event.ONLOSECAPTURE: + hideResizingCurtain(); + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(resizeBox, "visibility", ""); + } + resizing = false; + break; + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + if (resizing) { + centered = false; + setSize(event, false); + event.preventDefault(); + } + break; + default: + event.preventDefault(); + break; + } + } + } + + /** + * TODO check if we need to support this with touch based devices. + * + * Checks if the cursor was inside the browser content area when the event + * happened. + * + * @param event + * The event to be checked + * @return true, if the cursor is inside the browser content area + * + * false, otherwise + */ + private boolean cursorInsideBrowserContentArea(Event event) { + if (event.getClientX() < 0 || event.getClientY() < 0) { + // Outside to the left or above + return false; + } + + if (event.getClientX() > Window.getClientWidth() + || event.getClientY() > Window.getClientHeight()) { + // Outside to the right or below + return false; + } + + return true; + } + + private void setSize(Event event, boolean updateVariables) { + if (!cursorInsideBrowserContentArea(event)) { + // Only drag while cursor is inside the browser client area + return; + } + + int w = Util.getTouchOrMouseClientX(event) - startX + origW; + int minWidth = getMinWidth(); + if (w < minWidth) { + w = minWidth; + } + + int h = Util.getTouchOrMouseClientY(event) - startY + origH; + int minHeight = getMinHeight(); + if (h < minHeight) { + h = minHeight; + } + + setWidth(w + "px"); + setHeight(h + "px"); + + if (updateVariables) { + // sending width back always as pixels, no need for unit + client.updateVariable(id, "width", w, false); + client.updateVariable(id, "height", h, immediate); + } + + if (updateVariables || !resizeLazy) { + // Resize has finished or is not lazy + updateContentsSize(); + } else { + // Lazy resize - wait for a while before re-rendering contents + delayedContentsSizeUpdater.trigger(); + } + } + + private void updateContentsSize() { + // Update child widget dimensions + if (client != null) { + client.handleComponentRelativeSize(layout.getWidget()); + client.runDescendentsLayout((HasWidgets) layout.getWidget()); + } + + LayoutManager layoutManager = LayoutManager.get(client); + layoutManager.setNeedsMeasure(ConnectorMap.get(client).getConnector( + this)); + layoutManager.layoutNow(); + } + + @Override + public void setWidth(String width) { + // Override PopupPanel which sets the width to the contents + getElement().getStyle().setProperty("width", width); + // Update v-has-width in case undefined window is resized + setStyleName("v-has-width", width != null && width.length() > 0); + } + + @Override + public void setHeight(String height) { + // Override PopupPanel which sets the height to the contents + getElement().getStyle().setProperty("height", height); + // Update v-has-height in case undefined window is resized + setStyleName("v-has-height", height != null && height.length() > 0); + } + + private void onDragEvent(Event event) { + if (!Util.isTouchEventOrLeftMouseButton(event)) { + return; + } + + switch (DOM.eventGetType(event)) { + case Event.ONTOUCHSTART: + if (event.getTouches().length() > 1) { + return; + } + case Event.ONMOUSEDOWN: + if (!isActive()) { + bringToFront(); + } + beginMovingWindow(event); + break; + case Event.ONMOUSEUP: + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + case Event.ONLOSECAPTURE: + stopMovingWindow(); + break; + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + moveWindow(event); + break; + default: + break; + } + } + + private void moveWindow(Event event) { + if (dragging) { + centered = false; + if (cursorInsideBrowserContentArea(event)) { + // Only drag while cursor is inside the browser client area + final int x = Util.getTouchOrMouseClientX(event) - startX + + origX; + final int y = Util.getTouchOrMouseClientY(event) - startY + + origY; + setPopupPosition(x, y); + } + DOM.eventPreventDefault(event); + } + } + + private void beginMovingWindow(Event event) { + if (draggable) { + showDraggingCurtain(); + dragging = true; + startX = Util.getTouchOrMouseClientX(event); + startY = Util.getTouchOrMouseClientY(event); + origX = DOM.getAbsoluteLeft(getElement()); + origY = DOM.getAbsoluteTop(getElement()); + DOM.setCapture(getElement()); + DOM.eventPreventDefault(event); + } + } + + private void stopMovingWindow() { + dragging = false; + hideDraggingCurtain(); + DOM.releaseCapture(getElement()); + } + + @Override + public boolean onEventPreview(Event event) { + if (dragging) { + onDragEvent(event); + return false; + } else if (resizing) { + onResizeEvent(event); + return false; + } + + // TODO This is probably completely unnecessary as the modality curtain + // prevents events from reaching other windows and any security check + // must be done on the server side and not here. + // The code here is also run many times as each VWindow has an event + // preview but we cannot check only the current VWindow here (e.g. + // if(isTopMost) {...}) because PopupPanel will cause all events that + // are not cancelled here and target this window to be consume():d + // meaning the event won't be sent to the rest of the preview handlers. + + if (getTopmostWindow().vaadinModality) { + // Topmost window is modal. Cancel the event if it targets something + // outside that window (except debug console...) + if (DOM.getCaptureElement() != null) { + // Allow events when capture is set + return true; + } + + final Element target = event.getEventTarget().cast(); + if (!DOM.isOrHasChild(getTopmostWindow().getElement(), target)) { + // not within the modal window, but let's see if it's in the + // debug window + Widget w = Util.findWidget(target, null); + while (w != null) { + if (w instanceof Console) { + return true; // allow debug-window clicks + } else if (ConnectorMap.get(client).isConnector(w)) { + return false; + } + w = w.getParent(); + } + return false; + } + } + return true; + } + + @Override + public void addStyleDependentName(String styleSuffix) { + // VWindow's getStyleElement() does not return the same element as + // getElement(), so we need to override this. + setStyleName(getElement(), getStylePrimaryName() + "-" + styleSuffix, + true); + } + + @Override + public ShortcutActionHandler getShortcutActionHandler() { + return shortcutHandler; + } + + @Override + public void onScroll(ScrollEvent event) { + client.updateVariable(id, "scrollTop", + contentPanel.getScrollPosition(), false); + client.updateVariable(id, "scrollLeft", + contentPanel.getHorizontalScrollPosition(), false); + + } + + @Override + public void onKeyDown(KeyDownEvent event) { + if (shortcutHandler != null) { + shortcutHandler + .handleKeyboardEvent(Event.as(event.getNativeEvent())); + return; + } + } + + @Override + public void onBlur(BlurEvent event) { + if (client.hasEventListeners(this, EventId.BLUR)) { + client.updateVariable(id, EventId.BLUR, "", true); + } + } + + @Override + public void onFocus(FocusEvent event) { + if (client.hasEventListeners(this, EventId.FOCUS)) { + client.updateVariable(id, EventId.FOCUS, "", true); + } + } + + @Override + public void focus() { + contentPanel.focus(); + } + + public int getMinHeight() { + return MIN_CONTENT_AREA_HEIGHT + getDecorationHeight(); + } + + private int getDecorationHeight() { + LayoutManager lm = layout.getLayoutManager(); + int headerHeight = lm.getOuterHeight(header); + int footerHeight = lm.getOuterHeight(footer); + return headerHeight + footerHeight; + } + + public int getMinWidth() { + return MIN_CONTENT_AREA_WIDTH + getDecorationWidth(); + } + + private int getDecorationWidth() { + LayoutManager layoutManager = layout.getLayoutManager(); + return layoutManager.getOuterWidth(getElement()) + - contentPanel.getElement().getOffsetWidth(); + } + +} diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/window/WindowConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/window/WindowConnector.java new file mode 100644 index 0000000000..a1bab91618 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/client/ui/window/WindowConnector.java @@ -0,0 +1,318 @@ +/* + * Copyright 2011 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.terminal.gwt.client.ui.window; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.window.WindowServerRpc; +import com.vaadin.shared.ui.window.WindowState; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent; +import com.vaadin.terminal.gwt.client.LayoutManager; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.communication.RpcProxy; +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector; +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler; +import com.vaadin.terminal.gwt.client.ui.PostLayoutListener; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler; +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.BeforeShortcutActionListener; +import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout; +import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren; + +@Connect(value = com.vaadin.ui.Window.class) +public class WindowConnector extends AbstractComponentContainerConnector + implements Paintable, BeforeShortcutActionListener, + SimpleManagedLayout, PostLayoutListener, MayScrollChildren { + + private ClickEventHandler clickEventHandler = new ClickEventHandler(this) { + @Override + protected void fireClick(NativeEvent event, + MouseEventDetails mouseDetails) { + rpc.click(mouseDetails); + } + }; + + private WindowServerRpc rpc; + + boolean minWidthChecked = false; + + @Override + public boolean delegateCaptionHandling() { + return false; + }; + + @Override + protected void init() { + super.init(); + rpc = RpcProxy.create(WindowServerRpc.class, this); + + getLayoutManager().registerDependency(this, + getWidget().contentPanel.getElement()); + getLayoutManager().registerDependency(this, getWidget().header); + getLayoutManager().registerDependency(this, getWidget().footer); + } + + @Override + public void onUnregister() { + LayoutManager lm = getLayoutManager(); + VWindow window = getWidget(); + lm.unregisterDependency(this, window.contentPanel.getElement()); + lm.unregisterDependency(this, window.header); + lm.unregisterDependency(this, window.footer); + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + getWidget().id = getConnectorId(); + getWidget().client = client; + + // Workaround needed for Testing Tools (GWT generates window DOM + // slightly different in different browsers). + DOM.setElementProperty(getWidget().closeBox, "id", getConnectorId() + + "_window_close"); + + if (isRealUpdate(uidl)) { + if (getState().isModal() != getWidget().vaadinModality) { + getWidget().setVaadinModality(!getWidget().vaadinModality); + } + if (!getWidget().isAttached()) { + getWidget().setVisible(false); // hide until + // possible centering + getWidget().show(); + } + if (getState().isResizable() != getWidget().resizable) { + getWidget().setResizable(getState().isResizable()); + } + getWidget().resizeLazy = getState().isResizeLazy(); + + getWidget().setDraggable(getState().isDraggable()); + + // Caption must be set before required header size is measured. If + // the caption attribute is missing the caption should be cleared. + String iconURL = null; + if (getState().getIcon() != null) { + iconURL = getState().getIcon().getURL(); + } + getWidget().setCaption(getState().getCaption(), iconURL); + } + + getWidget().visibilityChangesDisabled = true; + if (!isRealUpdate(uidl)) { + return; + } + getWidget().visibilityChangesDisabled = false; + + clickEventHandler.handleEventHandlerRegistration(); + + getWidget().immediate = getState().isImmediate(); + + getWidget().setClosable(!isReadOnly()); + + // Initialize the position form UIDL + int positionx = getState().getPositionX(); + int positiony = getState().getPositionY(); + if (positionx >= 0 || positiony >= 0) { + if (positionx < 0) { + positionx = 0; + } + if (positiony < 0) { + positiony = 0; + } + getWidget().setPopupPosition(positionx, positiony); + } + + int childIndex = 0; + + // we may have actions + for (int i = 0; i < uidl.getChildCount(); i++) { + UIDL childUidl = uidl.getChildUIDL(i); + if (childUidl.getTag().equals("actions")) { + if (getWidget().shortcutHandler == null) { + getWidget().shortcutHandler = new ShortcutActionHandler( + getConnectorId(), client); + } + getWidget().shortcutHandler.updateActionMap(childUidl); + } + + } + + // setting scrollposition must happen after children is rendered + getWidget().contentPanel.setScrollPosition(getState().getScrollTop()); + getWidget().contentPanel.setHorizontalScrollPosition(getState() + .getScrollLeft()); + + // Center this window on screen if requested + // This had to be here because we might not know the content size before + // everything is painted into the window + + // centered is this is unset on move/resize + getWidget().centered = getState().isCentered(); + getWidget().setVisible(true); + + // ensure window is not larger than browser window + if (getWidget().getOffsetWidth() > Window.getClientWidth()) { + getWidget().setWidth(Window.getClientWidth() + "px"); + } + if (getWidget().getOffsetHeight() > Window.getClientHeight()) { + getWidget().setHeight(Window.getClientHeight() + "px"); + } + + if (uidl.hasAttribute("bringToFront")) { + /* + * Focus as a side-effect. Will be overridden by + * ApplicationConnection if another component was focused by the + * server side. + */ + getWidget().contentPanel.focus(); + getWidget().bringToFrontSequence = uidl + .getIntAttribute("bringToFront"); + VWindow.deferOrdering(); + } + } + + @Override + public void updateCaption(ComponentConnector component) { + // NOP, window has own caption, layout caption not rendered + } + + @Override + public void onBeforeShortcutAction(Event e) { + // NOP, nothing to update just avoid workaround ( causes excess + // blur/focus ) + } + + @Override + public VWindow getWidget() { + return (VWindow) super.getWidget(); + } + + @Override + public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { + super.onConnectorHierarchyChange(event); + + // We always have 1 child, unless the child is hidden + Widget newChildWidget = null; + ComponentConnector newChild = null; + if (getChildComponents().size() == 1) { + newChild = getChildComponents().get(0); + newChildWidget = newChild.getWidget(); + } + + getWidget().layout = newChild; + getWidget().contentPanel.setWidget(newChildWidget); + } + + @Override + public void layout() { + LayoutManager lm = getLayoutManager(); + VWindow window = getWidget(); + ComponentConnector layout = window.layout; + Element contentElement = window.contentPanel.getElement(); + + if (!minWidthChecked) { + boolean needsMinWidth = !isUndefinedWidth() + || layout.isRelativeWidth(); + int minWidth = window.getMinWidth(); + if (needsMinWidth && lm.getInnerWidth(contentElement) < minWidth) { + minWidthChecked = true; + // Use minimum width if less than a certain size + window.setWidth(minWidth + "px"); + } + minWidthChecked = true; + } + + boolean needsMinHeight = !isUndefinedHeight() + || layout.isRelativeHeight(); + int minHeight = window.getMinHeight(); + if (needsMinHeight && lm.getInnerHeight(contentElement) < minHeight) { + // Use minimum height if less than a certain size + window.setHeight(minHeight + "px"); + } + + Style contentStyle = window.contents.getStyle(); + + int headerHeight = lm.getOuterHeight(window.header); + contentStyle.setPaddingTop(headerHeight, Unit.PX); + contentStyle.setMarginTop(-headerHeight, Unit.PX); + + int footerHeight = lm.getOuterHeight(window.footer); + contentStyle.setPaddingBottom(footerHeight, Unit.PX); + contentStyle.setMarginBottom(-footerHeight, Unit.PX); + + /* + * Must set absolute position if the child has relative height and + * there's a chance of horizontal scrolling as some browsers will + * otherwise not take the scrollbar into account when calculating the + * height. + */ + Element layoutElement = layout.getWidget().getElement(); + Style childStyle = layoutElement.getStyle(); + if (layout.isRelativeHeight() && !BrowserInfo.get().isIE9()) { + childStyle.setPosition(Position.ABSOLUTE); + + Style wrapperStyle = contentElement.getStyle(); + if (window.getElement().getStyle().getWidth().length() == 0 + && !layout.isRelativeWidth()) { + /* + * Need to lock width to make undefined width work even with + * absolute positioning + */ + int contentWidth = lm.getOuterWidth(layoutElement); + wrapperStyle.setWidth(contentWidth, Unit.PX); + } else { + wrapperStyle.clearWidth(); + } + } else { + childStyle.clearPosition(); + } + } + + @Override + public void postLayout() { + minWidthChecked = false; + VWindow window = getWidget(); + if (window.centered) { + window.center(); + } + window.sizeOrPositionUpdated(); + } + + @Override + public WindowState getState() { + return (WindowState) super.getState(); + } + + /** + * Gives the WindowConnector an order number. As a side effect, moves the + * window according to its order number so the windows are stacked. This + * method should be called for each window in the order they should appear. + */ + public void setWindowOrderAndPosition() { + getWidget().setWindowOrderAndPosition(); + } +} diff --git a/client/src/com/vaadin/terminal/gwt/public/ie6pngfix/blank.gif b/client/src/com/vaadin/terminal/gwt/public/ie6pngfix/blank.gif Binary files differnew file mode 100644 index 0000000000..3776af0784 --- /dev/null +++ b/client/src/com/vaadin/terminal/gwt/public/ie6pngfix/blank.gif |