diff options
Diffstat (limited to 'client')
105 files changed, 13701 insertions, 1974 deletions
diff --git a/client/build.xml b/client/build.xml index d0dae91dfa..a2262eed7d 100644 --- a/client/build.xml +++ b/client/build.xml @@ -20,7 +20,7 @@ --> <fileset file="${gwt.user.jar}" /> </path> - <path id="classpath.tests.custom" /> + <path id="classpath.test.custom" /> <target name="jar"> <property name="jar.file" location="${result.dir}/lib/${module.name}-${vaadin.version}.jar" /> @@ -69,8 +69,8 @@ </antcall> </target> - <target name="tests" depends="checkstyle"> - <antcall target="common.tests.run" /> + <target name="test" depends="checkstyle"> + <antcall target="common.test.run" /> </target> </project>
\ No newline at end of file diff --git a/client/ivy.xml b/client/ivy.xml index 4b56338c24..5d079537b9 100644 --- a/client/ivy.xml +++ b/client/ivy.xml @@ -11,7 +11,7 @@ <conf name="build" /> <conf name="build-provided" /> <conf name="ide" visibility="private" /> - <conf name="tests" /> + <conf name="test" /> </configurations> <publications> <artifact type="jar" ext="jar" /> @@ -25,15 +25,15 @@ <!-- LIBRARY DEPENDENCIES (compile time) --> <!-- Project modules --> <dependency org="com.vaadin" name="vaadin-shared" - rev="${vaadin.version}" conf="build,tests->build"></dependency> + rev="${vaadin.version}" conf="build,test->build"></dependency> <dependency org="com.vaadin" name="vaadin-server" - rev="${vaadin.version}" conf="build->build"></dependency> + rev="${vaadin.version}" conf="build,test->build"></dependency> <!-- gwt-user dependencies --> <dependency org="org.w3c.css" name="sac" rev="1.3" /> <dependency org="junit" name="junit" rev="4.5" - conf="tests->default" /> + conf="test->default" /> <dependency org="javax.validation" name="validation-api" rev="1.0.0.GA" conf="build->default,sources" /> diff --git a/client/src/com/vaadin/Vaadin.gwt.xml b/client/src/com/vaadin/Vaadin.gwt.xml index dcc5b0d294..a4eb88d9b4 100644 --- a/client/src/com/vaadin/Vaadin.gwt.xml +++ b/client/src/com/vaadin/Vaadin.gwt.xml @@ -12,7 +12,13 @@ <inherits name="com.google.gwt.http.HTTP" /> <inherits name="com.google.gwt.json.JSON" /> - + + <inherits name="com.google.gwt.logging.Logging" /> + <!-- Firebug handler is deprecated but for some reason still enabled by default --> + <set-property name="gwt.logging.firebugHandler" value="DISABLED" /> + <!-- Disable popup logging as we have our own popup logger --> + <set-property name="gwt.logging.popupHandler" value="DISABLED" /> + <inherits name="com.vaadin.VaadinBrowserSpecificOverrides" /> <source path="client" /> @@ -24,10 +30,6 @@ <when-type-is class="com.google.gwt.core.client.impl.SchedulerImpl" /> </replace-with> - <replace-with class="com.vaadin.client.VDebugConsole"> - <when-type-is class="com.vaadin.client.Console" /> - </replace-with> - <generate-with class="com.vaadin.server.widgetsetutils.AcceptCriteriaFactoryGenerator"> <when-type-is class="com.vaadin.client.ui.dd.VAcceptCriterionFactory" /> @@ -39,6 +41,10 @@ class="com.vaadin.client.metadata.ConnectorBundleLoader" /> </generate-with> + <replace-with class="com.vaadin.client.communication.AtmospherePushConnection"> + <when-type-is class="com.vaadin.client.communication.PushConnection" /> + </replace-with> + <!-- Set vaadin.profiler to true to include profiling support in the module --> <define-property name="vaadin.profiler" values="true,false" /> <set-property name="vaadin.profiler" value="false" /> diff --git a/client/src/com/vaadin/client/ApplicationConfiguration.java b/client/src/com/vaadin/client/ApplicationConfiguration.java index 2291f21361..adf5e1de9d 100644 --- a/client/src/com/vaadin/client/ApplicationConfiguration.java +++ b/client/src/com/vaadin/client/ApplicationConfiguration.java @@ -20,6 +20,9 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.core.client.GWT; @@ -28,8 +31,15 @@ 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.logging.client.LogConfiguration; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.Window; +import com.vaadin.client.debug.internal.ErrorNotificationHandler; +import com.vaadin.client.debug.internal.HierarchySection; +import com.vaadin.client.debug.internal.LogSection; +import com.vaadin.client.debug.internal.NetworkSection; +import com.vaadin.client.debug.internal.Section; +import com.vaadin.client.debug.internal.VDebugWindow; import com.vaadin.client.metadata.BundleLoadCallback; import com.vaadin.client.metadata.ConnectorBundleLoader; import com.vaadin.client.metadata.NoDataException; @@ -259,7 +269,16 @@ public class ApplicationConfiguration implements EntryPoint { } public String getThemeUri() { - return vaadinDirUrl + "themes/" + getThemeName(); + return getVaadinDirUrl() + "themes/" + getThemeName(); + } + + /** + * Gets the URL of the VAADIN directory on the server. + * + * @return the URL of the VAADIN directory + */ + public String getVaadinDirUrl() { + return vaadinDirUrl; } public void setAppId(String appId) { @@ -365,7 +384,6 @@ public class ApplicationConfiguration implements EntryPoint { if (jsoConfiguration.getConfigBoolean("initPending") == Boolean.FALSE) { setBrowserDetailsSent(); } - } /** @@ -546,32 +564,50 @@ public class ApplicationConfiguration implements EntryPoint { enableIOS6castFix(); } - // Prepare VConsole for debugging + // Prepare the debugging window 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() { + /* + * XXX Lots of implementation details here right now. This should be + * cleared up when an API for extending the debug window is + * implemented. + */ + VDebugWindow window = GWT.create(VDebugWindow.class); - @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 (LogConfiguration.loggingIsEnabled()) { + window.addSection((Section) GWT.create(LogSection.class)); } - }); + window.addSection((Section) GWT.create(HierarchySection.class)); + window.addSection((Section) GWT.create(NetworkSection.class)); + + if (isQuietDebugMode()) { + window.close(); + } else { + window.init(); + } + + // Connect to the legacy API + VConsole.setImplementation(window); + + Handler errorNotificationHandler = GWT + .create(ErrorNotificationHandler.class); + Logger.getLogger("").addHandler(errorNotificationHandler); + } + + if (LogConfiguration.loggingIsEnabled()) { + GWT.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + + @Override + public void onUncaughtException(Throwable e) { + /* + * If the debug window is not enabled (?debug), this will + * not show anything to normal users. "a1 is not an object" + * style errors helps nobody, especially end user. It does + * not work tells just as much. + */ + getLogger().log(Level.SEVERE, e.getMessage(), e); + } + }); + } Profiler.leave("ApplicationConfiguration.onModuleLoad"); if (SuperDevMode.enableBasedOnParameter()) { @@ -679,4 +715,8 @@ public class ApplicationConfiguration implements EntryPoint { widgetsetVersionSent = true; } + private static final Logger getLogger() { + return Logger.getLogger(ApplicationConfiguration.class.getName()); + } + } diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java index 4ddbd7c39b..4141f9f2dc 100644 --- a/client/src/com/vaadin/client/ApplicationConnection.java +++ b/client/src/com/vaadin/client/ApplicationConnection.java @@ -27,6 +27,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import com.google.gwt.aria.client.LiveValue; +import com.google.gwt.aria.client.RelevantValue; +import com.google.gwt.aria.client.Roles; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JavaScriptObject; @@ -66,6 +69,7 @@ import com.vaadin.client.communication.HasJavaScriptConnectorHelper; import com.vaadin.client.communication.JavaScriptMethodInvocation; import com.vaadin.client.communication.JsonDecoder; import com.vaadin.client.communication.JsonEncoder; +import com.vaadin.client.communication.PushConnection; import com.vaadin.client.communication.RpcManager; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.extensions.AbstractExtensionConnector; @@ -80,6 +84,7 @@ import com.vaadin.client.ui.AbstractConnector; import com.vaadin.client.ui.VContextMenu; import com.vaadin.client.ui.VNotification; import com.vaadin.client.ui.VNotification.HideEvent; +import com.vaadin.client.ui.VOverlay; import com.vaadin.client.ui.dd.VDragAndDropManager; import com.vaadin.client.ui.ui.UIConnector; import com.vaadin.client.ui.window.WindowConnector; @@ -94,7 +99,7 @@ import com.vaadin.shared.ui.ui.UIConstants; /** * This is the client side communication "engine", managing client-server * communication with its server side counterpart - * com.vaadin.server.AbstractCommunicationManager. + * com.vaadin.server.VaadinService. * * Client-side connectors receive updates from the corresponding server-side * connector (typically component) as state updates or RPC calls. The connector @@ -155,8 +160,8 @@ public class ApplicationConnection { */ public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh"; - // will hold the UIDL security key (for XSS protection) once received - private String uidlSecurityKey = "init"; + // will hold the CSRF token once received + private String csrfToken = "init"; private final HashMap<String, String> resourcesMap = new HashMap<String, String>(); @@ -177,11 +182,6 @@ public class ApplicationConnection { private VContextMenu contextMenu = null; - private Timer loadTimer; - private Timer loadTimer2; - private Timer loadTimer3; - private Element loadElement; - private final UIConnector uIConnector; protected boolean applicationRunning = false; @@ -227,6 +227,8 @@ public class ApplicationConnection { private final RpcManager rpcManager; + private PushConnection push; + /** * If responseHandlingLocks contains any objects, response handling is * suspended until the collection is empty or a timeout has occurred. @@ -378,6 +380,8 @@ public class ApplicationConnection { private CommunicationErrorHandler communicationErrorDelegate = null; + private VLoadingIndicator loadingIndicator; + public static class MultiStepDuration extends Duration { private int previousStep = elapsedMillis(); @@ -404,6 +408,8 @@ public class ApplicationConnection { layoutManager = GWT.create(LayoutManager.class); layoutManager.setConnection(this); tooltip = GWT.create(VTooltip.class); + loadingIndicator = GWT.create(VLoadingIndicator.class); + loadingIndicator.setConnection(this); } public void init(WidgetSet widgetSet, ApplicationConfiguration cnf) { @@ -436,7 +442,7 @@ public class ApplicationConnection { tooltip.setOwner(uIConnector.getWidget()); - showLoadingIndicator(); + getLoadingIndicator().show(); scheduleHeartbeat(); @@ -454,6 +460,15 @@ public class ApplicationConnection { webkitMaybeIgnoringRequests = true; } }); + + // Ensure the overlay container is added to the dom and set as a live + // area for assistive devices + Element overlayContainer = VOverlay.getOverlayContainer(this); + Roles.getAlertRole().setAriaLiveProperty(overlayContainer, + LiveValue.ASSERTIVE); + setOverlayContainerLabel(getUIConnector().getState().overlayContainerLabel); + Roles.getAlertRole().setAriaRelevantProperty(overlayContainer, + RelevantValue.ADDITIONS); } /** @@ -656,8 +671,7 @@ public class ApplicationConnection { }-*/; protected void repaintAll() { - String repainAllParameters = getRepaintAllParameters(); - makeUidlRequest("", repainAllParameters, false); + makeUidlRequest("", getRepaintAllParameters()); } /** @@ -667,7 +681,7 @@ public class ApplicationConnection { public void analyzeLayouts() { String params = getRepaintAllParameters() + "&" + ApplicationConstants.PARAM_ANALYZE_LAYOUTS + "=1"; - makeUidlRequest("", params, false); + makeUidlRequest("", params); } /** @@ -681,7 +695,7 @@ public class ApplicationConnection { String params = getRepaintAllParameters() + "&" + ApplicationConstants.PARAM_HIGHLIGHT_CONNECTOR + "=" + serverConnector.getConnectorId(); - makeUidlRequest("", params, false); + makeUidlRequest("", params); } /** @@ -694,14 +708,12 @@ public class ApplicationConnection { * 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) { + final String extraParams) { startRequest(); // Security: double cookie submission pattern - final String payload = uidlSecurityKey + VAR_BURST_SEPARATOR + final String payload = getCsrfToken() + VAR_BURST_SEPARATOR + requestData; VConsole.log("Making UIDL Request with params: " + payload); String uri = translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX @@ -713,7 +725,7 @@ public class ApplicationConnection { uri = addGetParameters(uri, UIConstants.UI_ID_PARAMETER + "=" + configuration.getUIId()); - doUidlRequest(uri, payload, forceSync); + doUidlRequest(uri, payload); } @@ -725,143 +737,127 @@ public class ApplicationConnection { * 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) { - handleCommunicationError(exception.getMessage(), -1); - } + protected void doUidlRequest(final String uri, final String payload) { + RequestCallback requestCallback = new RequestCallback() { + @Override + public void onError(Request request, Throwable exception) { + handleCommunicationError(exception.getMessage(), -1); + } - private void handleCommunicationError(String details, - int statusCode) { - if (!handleErrorInDelegate(details, statusCode)) { - showCommunicationError(details, statusCode); - } - endRequest(); + private void handleCommunicationError(String details, int statusCode) { + if (!handleErrorInDelegate(details, statusCode)) { + showCommunicationError(details, statusCode); } + 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: - if (retryCanceledActiveRequest) { - /* - * Request was most likely canceled because the - * browser is maybe navigating away from the page. - * Just send the request again without displaying - * any error in case the navigation isn't carried - * through. - */ - retryCanceledActiveRequest = false; - doUidlRequest(uri, payload, synchronous); - } else { - handleCommunicationError( - "Invalid status code 0 (server down?)", - statusCode); - } - return; + @Override + public void onResponseReceived(Request request, Response response) { + VConsole.log("Server visit took " + + String.valueOf((new Date()).getTime() + - requestStartTime.getTime()) + "ms"); - 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; + int statusCode = response.getStatusCode(); - case 503: + switch (statusCode) { + case 0: + if (retryCanceledActiveRequest) { /* - * 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. + * Request was most likely canceled because the browser + * is maybe navigating away from the page. Just send the + * request again without displaying any error in case + * the navigation isn't carried through. */ - 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; - } + retryCanceledActiveRequest = false; + doUidlRequest(uri, payload); + } else { + handleCommunicationError( + "Invalid status code 0 (server down?)", + statusCode); } + 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. - handleCommunicationError("Server error. Error code: " - + statusCode, statusCode); + 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); + } + }).schedule(Integer.parseInt(delay)); 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; - } - } + 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. + handleCommunicationError("Server error. Error code: " + + statusCode, statusCode); + return; + } - // for(;;);[realjson] - final String jsonText = response.getText().substring(9, - response.getText().length() - 1); - handleJSONText(jsonText, statusCode); + 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); + } + }; + if (push != null) { + push.push(payload); + } else { try { - doAsyncUIDLRequest(uri, payload, requestCallback); + doAjaxRequest(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(); } - } /** @@ -905,11 +901,12 @@ public class ApplicationConnection { * @throws RequestException * if the request could not be sent */ - protected void doAsyncUIDLRequest(String uri, String payload, + protected void doAjaxRequest(String uri, String payload, RequestCallback requestCallback) throws RequestException { RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri); // TODO enable timeout // rb.setTimeoutMillis(timeoutMillis); + // TODO this should be configurable rb.setHeader("Content-Type", "text/plain;charset=utf-8"); rb.setRequestData(payload); rb.setCallback(requestCallback); @@ -1008,7 +1005,7 @@ public class ApplicationConnection { */ protected boolean isCSSLoaded() { return cssLoaded - || DOM.getElementPropertyInt(loadElement, "offsetHeight") != 0; + || getLoadingIndicator().getElement().getOffsetHeight() != 0; } /** @@ -1042,7 +1039,7 @@ public class ApplicationConnection { * @param details * Optional details for debugging. */ - protected void showSessionExpiredError(String details) { + public void showSessionExpiredError(String details) { VConsole.error("Session expired: " + details); showError(details, configuration.getSessionExpiredError()); } @@ -1106,25 +1103,7 @@ public class ApplicationConnection { } 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); + loadingIndicator.trigger(); eventBus.fireEvent(new RequestStartingEvent(this)); } @@ -1145,12 +1124,13 @@ public class ApplicationConnection { checkForPendingVariableBursts(); runPostRequestHooks(configuration.getRootPanelId()); } + // deferring to avoid flickering Scheduler.get().scheduleDeferred(new Command() { @Override public void execute() { if (!hasActiveRequest()) { - hideLoadingIndicator(); + getLoadingIndicator().hide(); // If on Liferay and session expiration management is in // use, extend session duration on each request. @@ -1179,7 +1159,7 @@ public class ApplicationConnection { } LinkedHashMap<String, MethodInvocation> nextBurst = pendingBursts .remove(0); - buildAndSendVariableBurst(nextBurst, false); + buildAndSendVariableBurst(nextBurst); } } @@ -1203,54 +1183,6 @@ public class ApplicationConnection { } } - private void showLoadingIndicator() { - // show initial throbber - if (loadElement == null) { - loadElement = DOM.createDiv(); - DOM.setStyleAttribute(loadElement, "position", "absolute"); - DOM.appendChild(uIConnector.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 @@ -1273,19 +1205,24 @@ public class ApplicationConnection { } /** + * Returns the loading indicator used by this ApplicationConnection + * + * @return The loading indicator for this ApplicationConnection + */ + public VLoadingIndicator getLoadingIndicator() { + return loadingIndicator; + } + + /** * Determines whether or not the loading indicator is showing. * * @return true if the loading indicator is visible + * @deprecated As of 7.1. Use {@link #getLoadingIndicator()} and + * {@link VLoadingIndicator#isVisible()}.isVisible() instead. */ + @Deprecated public boolean isLoadingIndicatorVisible() { - if (loadElement == null) { - return false; - } - if (loadElement.getStyle().getProperty("display").equals("none")) { - return false; - } - - return true; + return getLoadingIndicator().isVisible(); } private static native ValueMap parseJSONResponse(String jsonText) @@ -1332,6 +1269,14 @@ public class ApplicationConnection { return; } + /* + * Lock response handling to avoid a situation where something pushed + * from the server gets processed while waiting for e.g. lazily loaded + * connectors that are needed for processing the current message. + */ + final Object lock = new Object(); + suspendReponseHandling(lock); + VConsole.log("Handling message from server"); eventBus.fireEvent(new ResponseHandlingStartedEvent(this)); @@ -1349,7 +1294,7 @@ public class ApplicationConnection { // Get security key if (json.containsKey(ApplicationConstants.UIDL_SECURITY_TOKEN_ID)) { - uidlSecurityKey = json + csrfToken = json .getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID); } VConsole.log(" * Handling resources from server"); @@ -1545,7 +1490,12 @@ public class ApplicationConnection { + jsonText.length() + " characters of JSON"); VConsole.log("Referenced paintables: " + connectorMap.size()); - endRequest(); + if (meta == null || !meta.containsKey("async")) { + // End the request if the received message was a response, + // not sent asynchronously + endRequest(); + } + resumeResponseHandling(lock); if (Profiler.isEnabled()) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @@ -1556,7 +1506,6 @@ public class ApplicationConnection { } }); } - } /** @@ -2417,6 +2366,23 @@ public class ApplicationConnection { } /** + * Removes any pending invocation of the given method from the queue + * + * @param invocation + * The invocation to remove + */ + public void removePendingInvocations(MethodInvocation invocation) { + Iterator<MethodInvocation> iter = pendingInvocations.values() + .iterator(); + while (iter.hasNext()) { + MethodInvocation mi = iter.next(); + if (mi.equals(invocation)) { + iter.remove(); + } + } + } + + /** * This method sends currently queued variable changes to server. It is * called when immediate variable update must happen. * @@ -2429,7 +2395,7 @@ public class ApplicationConnection { public void sendPendingVariableChanges() { if (!deferedSendPending) { deferedSendPending = true; - Scheduler.get().scheduleDeferred(sendPendingCommand); + Scheduler.get().scheduleFinally(sendPendingCommand); } } @@ -2444,7 +2410,7 @@ public class ApplicationConnection { private void doSendPendingVariableChanges() { if (applicationRunning) { - if (hasActiveRequest()) { + if (hasActiveRequest() || (push != null && !push.isActive())) { // skip empty queues if there are pending bursts to be sent if (pendingInvocations.size() > 0 || pendingBursts.size() == 0) { pendingBursts.add(pendingInvocations); @@ -2453,7 +2419,7 @@ public class ApplicationConnection { lastInvocationTag = 0; } } else { - buildAndSendVariableBurst(pendingInvocations, false); + buildAndSendVariableBurst(pendingInvocations); } } } @@ -2467,12 +2433,9 @@ public class ApplicationConnection { * * @param pendingInvocations * List of RPC method invocations to send - * @param forceSync - * Should we use synchronous request? */ private void buildAndSendVariableBurst( - LinkedHashMap<String, MethodInvocation> pendingInvocations, - boolean forceSync) { + LinkedHashMap<String, MethodInvocation> pendingInvocations) { final StringBuffer req = new StringBuffer(); while (!pendingInvocations.isEmpty()) { @@ -2526,12 +2489,6 @@ public class ApplicationConnection { pendingInvocations.clear(); // Keep tag string short lastInvocationTag = 0; - // 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 @@ -2552,7 +2509,7 @@ public class ApplicationConnection { getConfiguration().setWidgetsetVersionSent(); } - makeUidlRequest(req.toString(), extraParams, forceSync); + makeUidlRequest(req.toString(), extraParams); } private boolean isJavascriptRpc(MethodInvocation invocation) { @@ -3056,7 +3013,17 @@ public class ApplicationConnection { private ConnectorMap connectorMap = GWT.create(ConnectorMap.class); protected String getUidlSecurityKey() { - return uidlSecurityKey; + return getCsrfToken(); + } + + /** + * Gets the token (aka double submit cookie) that the server uses to protect + * against Cross Site Request Forgery attacks. + * + * @return the CSRF token string + */ + public String getCsrfToken() { + return csrfToken; } /** @@ -3309,6 +3276,14 @@ public class ApplicationConnection { Timer forceHandleMessage = new Timer() { @Override public void run() { + if (responseHandlingLocks.isEmpty()) { + /* + * Timer fired but there's nothing to clear. This can happen + * with IE8 as Timer.cancel is not always effective (see GWT + * issue 8101). + */ + return; + } VConsole.log("WARNING: reponse handling was never resumed, forcibly removing locks..."); responseHandlingLocks.clear(); handlePendingMessages(); @@ -3333,9 +3308,13 @@ public class ApplicationConnection { public void resumeResponseHandling(Object lock) { responseHandlingLocks.remove(lock); if (responseHandlingLocks.isEmpty()) { - VConsole.log("No more response handling locks, handling pending requests."); + // Cancel timer that breaks the lock forceHandleMessage.cancel(); - handlePendingMessages(); + + if (!pendingUIDLMessages.isEmpty()) { + VConsole.log("No more response handling locks, handling pending requests."); + handlePendingMessages(); + } } } @@ -3344,11 +3323,19 @@ public class ApplicationConnection { * suspended. */ private void handlePendingMessages() { - for (PendingUIDLMessage pending : pendingUIDLMessages) { - handleUIDLMessage(pending.getStart(), pending.getJsonText(), - pending.getJson()); + if (!pendingUIDLMessages.isEmpty()) { + /* + * Clear the list before processing enqueued messages to support + * reentrancy + */ + List<PendingUIDLMessage> pendingMessages = pendingUIDLMessages; + pendingUIDLMessages = new ArrayList<PendingUIDLMessage>(); + + for (PendingUIDLMessage pending : pendingMessages) { + handleReceivedJSONMessage(pending.getStart(), + pending.getJsonText(), pending.getJson()); + } } - pendingUIDLMessages.clear(); } private boolean handleErrorInDelegate(String details, int statusCode) { @@ -3406,4 +3393,58 @@ public class ApplicationConnection { return Util.getConnectorForElement(this, getUIConnector().getWidget(), focusedElement); } + + /** + * Sets the status for the push connection. + * + * @param enabled + * <code>true</code> to enable the push connection; + * <code>false</code> to disable the push connection. + */ + public void setPushEnabled(boolean enabled) { + if (enabled && push == null) { + push = GWT.create(PushConnection.class); + push.init(this); + } else if (!enabled && push != null && push.isActive()) { + push.disconnect(new Command() { + @Override + public void execute() { + push = null; + /* + * If push has been enabled again while we were waiting for + * the old connection to disconnect, now is the right time + * to open a new connection + */ + if (uIConnector.getState().pushMode.isEnabled()) { + setPushEnabled(true); + } + + /* + * Send anything that was enqueued while we waited for the + * connection to close + */ + if (pendingInvocations.size() > 0) { + sendPendingVariableChanges(); + } + } + }); + } + } + + public void handlePushMessage(String message) { + handleJSONText(message, 200); + } + + /** + * Set the label of the container element, where tooltip, notification and + * dialgs are added to. + * + * @param overlayContainerLabel + * label for the container + */ + public void setOverlayContainerLabel(String overlayContainerLabel) { + Roles.getAlertRole().setAriaLabelProperty( + VOverlay.getOverlayContainer(this), + getUIConnector().getState().overlayContainerLabel); + } } diff --git a/client/src/com/vaadin/client/BrowserInfo.java b/client/src/com/vaadin/client/BrowserInfo.java index 5185f829fe..b3490c3ca4 100644 --- a/client/src/com/vaadin/client/BrowserInfo.java +++ b/client/src/com/vaadin/client/BrowserInfo.java @@ -85,6 +85,8 @@ public class BrowserInfo { if (browserDetails.isChrome()) { touchDevice = detectChromeTouchDevice(); + } else if (browserDetails.isIE()) { + touchDevice = detectIETouchDevice(); } else { touchDevice = detectTouchDevice(); } @@ -100,6 +102,11 @@ public class BrowserInfo { return ("ontouchstart" in window); }-*/; + private native boolean detectIETouchDevice() + /*-{ + return !!navigator.msMaxTouchPoints; + }-*/; + private native int getIEDocumentMode() /*-{ var mode = $wnd.document.documentMode; @@ -331,7 +338,8 @@ public class BrowserInfo { * otherwise <code>false</code> */ public boolean requiresOverflowAutoFix() { - return (getWebkitVersion() > 0 || getOperaVersion() >= 11 || isFirefox()) + return (getWebkitVersion() > 0 || getOperaVersion() >= 11 + || getIEVersion() >= 10 || isFirefox()) && Util.getNativeScrollbarSize() > 0; } diff --git a/client/src/com/vaadin/client/ComponentConnector.java b/client/src/com/vaadin/client/ComponentConnector.java index eecc3fda0c..f923a9dade 100644 --- a/client/src/com/vaadin/client/ComponentConnector.java +++ b/client/src/com/vaadin/client/ComponentConnector.java @@ -119,6 +119,11 @@ public interface ComponentConnector extends ServerConnector { /** * Gets the tooltip info for the given element. + * <p> + * When overriding this method, {@link #hasTooltip()} should also be + * overridden to return <code>true</code> in all situations where this + * method might return a non-empty result. + * </p> * * @param element * The element to lookup a tooltip for @@ -128,6 +133,20 @@ public interface ComponentConnector extends ServerConnector { public TooltipInfo getTooltipInfo(Element element); /** + * Check whether there might be a tooltip for this component. The framework + * will only add event listeners for automatically handling tooltips (using + * {@link #getTooltipInfo(Element)}) if this method returns true. + * <p> + * This is only done to optimize performance, so in cases where the status + * is not known, it's safer to return <code>true</code> so that there will + * be a tooltip handler even though it might not be needed in all cases. + * + * @return <code>true</code> if some part of the component might have a + * tooltip, otherwise <code>false</code> + */ + public boolean hasTooltip(); + + /** * Called for the active (focused) connector when a situation occurs that * the focused connector might have buffered changes which need to be * processed before other activity takes place. diff --git a/client/src/com/vaadin/client/ComponentLocator.java b/client/src/com/vaadin/client/ComponentLocator.java index 05603e8abe..af934470c2 100644 --- a/client/src/com/vaadin/client/ComponentLocator.java +++ b/client/src/com/vaadin/client/ComponentLocator.java @@ -28,6 +28,7 @@ import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ui.SubPartAware; import com.vaadin.client.ui.VCssLayout; import com.vaadin.client.ui.VGridLayout; +import com.vaadin.client.ui.VOverlay; import com.vaadin.client.ui.VTabsheetPanel; import com.vaadin.client.ui.VUI; import com.vaadin.client.ui.VWindow; @@ -446,7 +447,10 @@ public class ComponentLocator { return null; } String elementId = w.getElement().getId(); - if (elementId != null && !elementId.isEmpty()) { + if (elementId != null && !elementId.isEmpty() + && !elementId.startsWith("gwt-uid-")) { + // Use PID_S+id if the user has set an id but do not use it for auto + // generated id:s as these might not be consistent return "PID_S" + elementId; } else if (w instanceof VUI) { return ""; @@ -575,7 +579,12 @@ public class ComponentLocator { // is always 0 which indicates the widget in the active tab widgetPosition = 0; } - + if (w instanceof VOverlay + && "VCalendarPanel".equals(widgetClassName)) { + // Vaadin 7.1 adds a wrapper for datefield popups + parent = (Iterable<?>) ((Iterable) parent).iterator() + .next(); + } /* * The new grid and ordered layotus do not contain * ChildComponentContainer widgets. This is instead simulated by diff --git a/client/src/com/vaadin/client/ConnectorHierarchyChangeEvent.java b/client/src/com/vaadin/client/ConnectorHierarchyChangeEvent.java index 56ae7c44ac..2896386933 100644 --- a/client/src/com/vaadin/client/ConnectorHierarchyChangeEvent.java +++ b/client/src/com/vaadin/client/ConnectorHierarchyChangeEvent.java @@ -67,19 +67,17 @@ public class ConnectorHierarchyChangeEvent extends } /** - * Returns the {@link HasComponentsConnector} for which this event - * occurred. + * Returns the {@link HasComponentsConnector} for which this event occurred. * - * @return The {@link HasComponentsConnector} whose child collection - * has changed. Never returns null. + * @return The {@link HasComponentsConnector} whose child collection has + * changed. Never returns null. */ public HasComponentsConnector getParent() { return parent; } /** - * Sets the {@link HasComponentsConnector} for which this event - * occurred. + * Sets the {@link HasComponentsConnector} for which this event occurred. * * @param The * {@link HasComponentsConnector} whose child collection has diff --git a/client/src/com/vaadin/client/Console.java b/client/src/com/vaadin/client/Console.java deleted file mode 100644 index aa8ef2adc5..0000000000 --- a/client/src/com/vaadin/client/Console.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2000-2013 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.vaadin.client; - -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/client/HasComponentsConnector.java b/client/src/com/vaadin/client/HasComponentsConnector.java index 0a1a7be97b..ebc6dbcd2a 100644 --- a/client/src/com/vaadin/client/HasComponentsConnector.java +++ b/client/src/com/vaadin/client/HasComponentsConnector.java @@ -21,6 +21,7 @@ import java.util.List; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.ui.HasWidgets; import com.vaadin.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler; +import com.vaadin.ui.HasComponents; /** * An interface used by client-side connectors whose widget is a component diff --git a/client/src/com/vaadin/client/NullConsole.java b/client/src/com/vaadin/client/NullConsole.java deleted file mode 100644 index 2b70454b9d..0000000000 --- a/client/src/com/vaadin/client/NullConsole.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2000-2013 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.vaadin.client; - -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) { - GWT.log(e.getMessage(), e); - } - - @Override - public void setQuietMode(boolean quietDebugMode) { - } - - @Override - public void init() { - } - -} diff --git a/client/src/com/vaadin/client/Profiler.java b/client/src/com/vaadin/client/Profiler.java index 6e8f2f5f9a..95b3232723 100644 --- a/client/src/com/vaadin/client/Profiler.java +++ b/client/src/com/vaadin/client/Profiler.java @@ -387,17 +387,12 @@ public class Profiler { StringBuilder stringBuilder = new StringBuilder(); rootNode.buildRecursiveString(stringBuilder, ""); - Console implementation = VConsole.getImplementation(); - if (implementation instanceof VDebugConsole) { - VDebugConsole console = (VDebugConsole) implementation; - SimpleTree tree = (SimpleTree) stack.getFirst().buildTree(); - tree.setText("Profiler data"); - - console.showTree(tree, stringBuilder.toString()); - } else { - VConsole.log(stringBuilder.toString()); - } + /* + * Should really output to a separate section in the debug window, but + * just dump it to the log for now. + */ + VConsole.log(stringBuilder.toString()); Map<String, Node> totals = new HashMap<String, Node>(); rootNode.sumUpTotals(totals); @@ -482,13 +477,11 @@ public class Profiler { return; } - Console implementation = VConsole.getImplementation(); - if (implementation instanceof VDebugConsole) { - VDebugConsole console = (VDebugConsole) implementation; - console.showTree(tree, stringBuilder.toString()); - } else { - VConsole.log(stringBuilder.toString()); - } + /* + * Should really output to a separate section in the debug window, + * but just dump it to the log for now. + */ + VConsole.log(stringBuilder.toString()); } } diff --git a/client/src/com/vaadin/client/SimpleTree.java b/client/src/com/vaadin/client/SimpleTree.java index d4aef4e4f7..23bdc4828f 100644 --- a/client/src/com/vaadin/client/SimpleTree.java +++ b/client/src/com/vaadin/client/SimpleTree.java @@ -25,16 +25,30 @@ 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.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.shared.HandlerManager; +import com.google.gwt.event.shared.HandlerRegistration; 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 { +/** + * @author Vaadin Ltd + * + * @deprecated as of 7.1. This class was mainly used by the old debug console + * but is retained for now for backwards compatibility. + */ +@Deprecated +public class SimpleTree extends ComplexPanel implements HasDoubleClickHandlers { private Element children = Document.get().createDivElement().cast(); private SpanElement handle = Document.get().createSpanElement(); private SpanElement text = Document.get().createSpanElement(); + private HandlerManager textDoubleClickHandlerManager; + public SimpleTree() { setElement(Document.get().createDivElement()); Style style = getElement().getStyle(); @@ -126,4 +140,24 @@ public class SimpleTree extends ComplexPanel { getElement().getStyle().setPaddingLeft(3, Unit.PX); } + /** + * {@inheritDoc} Events are not fired when double clicking child widgets. + */ + @Override + public HandlerRegistration addDoubleClickHandler(DoubleClickHandler handler) { + if (textDoubleClickHandlerManager == null) { + textDoubleClickHandlerManager = new HandlerManager(this); + addDomHandler(new DoubleClickHandler() { + @Override + public void onDoubleClick(DoubleClickEvent event) { + if (event.getNativeEvent().getEventTarget().cast() == text) { + textDoubleClickHandlerManager.fireEvent(event); + } + } + }, DoubleClickEvent.getType()); + } + return textDoubleClickHandlerManager.addHandler( + DoubleClickEvent.getType(), handler); + } + } diff --git a/client/src/com/vaadin/client/Util.java b/client/src/com/vaadin/client/Util.java index 2cd01b2dd8..8972670232 100644 --- a/client/src/com/vaadin/client/Util.java +++ b/client/src/com/vaadin/client/Util.java @@ -48,6 +48,7 @@ import com.vaadin.shared.AbstractComponentState; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.communication.MethodInvocation; import com.vaadin.shared.ui.ComponentStateUtil; +import com.vaadin.shared.util.SharedUtil; public class Util { @@ -550,12 +551,21 @@ public class Util { } } + /** + * Checks if a and b are equals using {@link #equals(Object)}. Handles null + * values as well. Does not ensure that objects are of the same type. + * Assumes that the first object's equals method handle equals properly. + * + * @param a + * The first value to compare + * @param b + * The second value to compare + * @return + * @deprecated As of 7.1 use {@link SharedUtil#equals(Object)} instead + */ + @Deprecated public static boolean equals(Object a, Object b) { - if (a == null) { - return b == null; - } - - return a.equals(b); + return SharedUtil.equals(a, b); } public static void updateRelativeChildrenAndSendSizeUpdateEvent( @@ -630,6 +640,10 @@ public class Util { /*-{ var cs = element.ownerDocument.defaultView.getComputedStyle(element); var heightPx = cs.height; + if(heightPx == 'auto'){ + // Fallback for when IE reports auto + heightPx = @com.vaadin.client.Util::getRequiredHeightBoundingClientRect(Lcom/google/gwt/dom/client/Element;)(element) + 'px'; + } var borderTopPx = cs.borderTop; var borderBottomPx = cs.borderBottom; var paddingTopPx = cs.paddingTop; @@ -646,6 +660,10 @@ public class Util { /*-{ var cs = element.ownerDocument.defaultView.getComputedStyle(element); var widthPx = cs.width; + if(widthPx == 'auto'){ + // Fallback for when IE reports auto + widthPx = @com.vaadin.client.Util::getRequiredWidthBoundingClientRect(Lcom/google/gwt/dom/client/Element;)(element) + 'px'; + } var borderLeftPx = cs.borderLeft; var borderRightPx = cs.borderRight; var paddingLeftPx = cs.paddingLeft; @@ -1339,5 +1357,4 @@ public class Util { } } - } diff --git a/client/src/com/vaadin/client/VCaption.java b/client/src/com/vaadin/client/VCaption.java index 47287636c4..d033c2b4fe 100644 --- a/client/src/com/vaadin/client/VCaption.java +++ b/client/src/com/vaadin/client/VCaption.java @@ -16,6 +16,7 @@ package com.vaadin.client; +import com.google.gwt.aria.client.Roles; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; @@ -23,6 +24,7 @@ import com.google.gwt.user.client.ui.HTML; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.AbstractFieldConnector; import com.vaadin.client.ui.Icon; +import com.vaadin.client.ui.aria.AriaHelper; import com.vaadin.shared.AbstractComponentState; import com.vaadin.shared.AbstractFieldState; import com.vaadin.shared.ComponentConstants; @@ -95,6 +97,24 @@ public class VCaption extends HTML { setStyleName(CLASSNAME); } + @Override + protected void onAttach() { + super.onAttach(); + + if (null != owner) { + AriaHelper.bindCaption(owner.getWidget(), getElement()); + } + } + + @Override + protected void onDetach() { + super.onDetach(); + + if (null != owner) { + AriaHelper.bindCaption(owner.getWidget(), null); + } + } + /** * Updates the caption from UIDL. * @@ -200,6 +220,8 @@ public class VCaption extends HTML { removeStyleDependentName("hasdescription"); } + AriaHelper.handleInputRequired(owner.getWidget(), showRequired); + if (showRequired) { if (requiredFieldIndicator == null) { requiredFieldIndicator = DOM.createDiv(); @@ -209,6 +231,10 @@ public class VCaption extends HTML { DOM.insertChild(getElement(), requiredFieldIndicator, getInsertPosition(InsertPosition.REQUIRED)); + + // Hide the required indicator from assistive device + Roles.getTextboxRole().setAriaHiddenState( + requiredFieldIndicator, true); } } else if (requiredFieldIndicator != null) { // Remove existing @@ -216,6 +242,8 @@ public class VCaption extends HTML { requiredFieldIndicator = null; } + AriaHelper.handleInputInvalid(owner.getWidget(), showError); + if (showError) { if (errorIndicatorElement == null) { errorIndicatorElement = DOM.createDiv(); @@ -225,6 +253,10 @@ public class VCaption extends HTML { DOM.insertChild(getElement(), errorIndicatorElement, getInsertPosition(InsertPosition.ERROR)); + + // Hide error indicator from assistive devices + Roles.getTextboxRole().setAriaHiddenState( + errorIndicatorElement, true); } } else if (errorIndicatorElement != null) { // Remove existing diff --git a/client/src/com/vaadin/client/VConsole.java b/client/src/com/vaadin/client/VConsole.java index db19d1a9fd..f7a7554e34 100644 --- a/client/src/com/vaadin/client/VConsole.java +++ b/client/src/com/vaadin/client/VConsole.java @@ -16,91 +16,67 @@ package com.vaadin.client; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; -import com.google.gwt.core.client.GWT; +import com.google.gwt.logging.client.LogConfiguration; +import com.vaadin.client.debug.internal.VDebugWindow; /** * 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 * + * @deprecated as of 7.1, use {@link Logger} from java.util.logging instead. */ +@Deprecated public class VConsole { - private static Console impl; + private static VDebugWindow impl; /** * Used by ApplicationConfiguration to initialize VConsole. * * @param console */ - static void setImplementation(Console console) { + static void setImplementation(VDebugWindow 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); + if (LogConfiguration.loggingIsEnabled(Level.INFO)) { + getLogger().log(Level.INFO, msg); } } public static void log(Throwable e) { - if (impl != null) { - impl.log(e); + if (LogConfiguration.loggingIsEnabled(Level.INFO)) { + getLogger().log(Level.INFO, e.getMessage(), e); } } public static void error(Throwable e) { - if (impl != null) { - impl.error(e); + if (LogConfiguration.loggingIsEnabled(Level.SEVERE)) { + getLogger().log(Level.SEVERE, e.getMessage(), e); } } public static void error(String msg) { - if (impl != null) { - impl.error(msg); + if (LogConfiguration.loggingIsEnabled(Level.SEVERE)) { + getLogger().log(Level.SEVERE, msg); } } public static void printObject(Object msg) { - if (impl != null) { - impl.printObject(msg); + String str; + if (msg == null) { + str = "null"; + } else { + str = msg.toString(); } + log(str); } public static void dirUIDL(ValueMap u, ApplicationConnection client) { if (impl != null) { - impl.dirUIDL(u, client); + impl.uidl(client, u); } } @@ -109,9 +85,12 @@ public class VConsole { Set<ComponentConnector> zeroHeightComponents, Set<ComponentConnector> zeroWidthComponents) { if (impl != null) { - impl.printLayoutProblems(meta, applicationConnection, - zeroHeightComponents, zeroWidthComponents); + impl.meta(applicationConnection, meta); } } + private static Logger getLogger() { + return Logger.getLogger(VConsole.class.getName()); + } + } diff --git a/client/src/com/vaadin/client/VDebugConsole.java b/client/src/com/vaadin/client/VDebugConsole.java deleted file mode 100644 index ee7505876d..0000000000 --- a/client/src/com/vaadin/client/VDebugConsole.java +++ /dev/null @@ -1,1041 +0,0 @@ -/* - * Copyright 2000-2013 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.vaadin.client; - -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.client.ui.VLazyExecutor; -import com.vaadin.client.ui.VNotification; -import com.vaadin.client.ui.VOverlay; -import com.vaadin.client.ui.ui.UIConnector; -import com.vaadin.client.ui.window.WindowConnector; -import com.vaadin.shared.Version; - -/** - * 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.getUIConnector().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.getUIConnector().getWidget(), eventTarget); - if (paintable == null) { - paintable = Util.getConnectorForElement(a, - RootPanel.get(), eventTarget); - } - - if (paintable != null) { - a.highlightConnector(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.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.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.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.client.Console#dirUIDL(com.vaadin.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()); - } - - /** - * Adds a {@link SimpleTree} to the console and prints a string - * representation of the tree structure to the text console. - * - * @param tree - * the simple tree to display in the console window - * @param stringRepresentation - * the string representation of the tree to output to the text - * console - */ - public void showTree(SimpleTree tree, String stringRepresentation) { - if (panel.isAttached()) { - panel.add(tree); - } - consoleLog(stringRepresentation); - } - - 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 { - Widget owner = null; - - if (!ApplicationConfiguration.getRunningApplications().isEmpty()) { - // Make a wild guess and use the first available - // ApplicationConnection. This is better than than leaving the - // exception completely unstyled... - ApplicationConnection connection = ApplicationConfiguration - .getRunningApplications().get(0); - owner = connection.getUIConnector().getWidget(); - } - VNotification - .createNotification(VNotification.DELAY_FOREVER, owner) - .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) { - UIConnector root = a.getUIConnector(); - log("================"); - log("Connector hierarchy for Root: " + root.getState().caption + " (" - + 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/client/VErrorMessage.java b/client/src/com/vaadin/client/VErrorMessage.java index a384b451dd..2e42b98a05 100644 --- a/client/src/com/vaadin/client/VErrorMessage.java +++ b/client/src/com/vaadin/client/VErrorMessage.java @@ -31,6 +31,9 @@ public class VErrorMessage extends FlowPanel { public VErrorMessage() { super(); setStyleName(CLASSNAME); + + // Needed for binding with WAI-ARIA attributes + getElement().setId(DOM.createUniqueId()); } /** diff --git a/client/src/com/vaadin/client/VLoadingIndicator.java b/client/src/com/vaadin/client/VLoadingIndicator.java new file mode 100644 index 0000000000..fcce35781d --- /dev/null +++ b/client/src/com/vaadin/client/VLoadingIndicator.java @@ -0,0 +1,292 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client; + +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Timer; + +/** + * Class representing the loading indicator for Vaadin applications. The loading + * indicator has four states: "triggered", "first", "second" and "third". When + * {@link #trigger()} is called the indicator moves to its "triggered" state and + * then transitions from one state to the next when the timeouts specified using + * the set*StateDelay methods occur. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class VLoadingIndicator { + + private static final String PRIMARY_STYLE_NAME = "v-loading-indicator"; + + private ApplicationConnection connection; + + private int firstDelay = 300; + private int secondDelay = 1500; + private int thirdDelay = 5000; + + /** + * Timer with method for checking if it has been cancelled. This class is a + * workaround for a IE8 problem which causes a timer to be fired even if it + * has been cancelled. + * + * @author Vaadin Ltd + * @since 7.1 + */ + private abstract static class LoadingIndicatorTimer extends Timer { + private boolean cancelled = false; + + @Override + public void cancel() { + super.cancel(); + cancelled = true; + } + + @Override + public void schedule(int delayMillis) { + super.schedule(delayMillis); + cancelled = false; + } + + @Override + public void scheduleRepeating(int periodMillis) { + super.scheduleRepeating(periodMillis); + cancelled = false; + } + + /** + * Checks if this timer has been cancelled. + * + * @return true if the timer has been cancelled, false otherwise + */ + public boolean isCancelled() { + return cancelled; + } + } + + private Timer firstTimer = new LoadingIndicatorTimer() { + @Override + public void run() { + if (isCancelled()) { + // IE8 does not properly cancel the timer in all cases. + return; + } + show(); + } + }; + private Timer secondTimer = new LoadingIndicatorTimer() { + @Override + public void run() { + if (isCancelled()) { + // IE8 does not properly cancel the timer in all cases. + return; + } + getElement().setClassName(PRIMARY_STYLE_NAME); + getElement().addClassName("second"); + // For backwards compatibility only + getElement().addClassName(PRIMARY_STYLE_NAME + "-delay"); + } + }; + private Timer thirdTimer = new LoadingIndicatorTimer() { + @Override + public void run() { + if (isCancelled()) { + // IE8 does not properly cancel the timer in all cases. + return; + } + getElement().setClassName(PRIMARY_STYLE_NAME); + getElement().addClassName("third"); + // For backwards compatibility only + getElement().addClassName(PRIMARY_STYLE_NAME + "-wait"); + } + }; + + private Element element; + + /** + * Returns the delay (in ms) which must pass before the loading indicator + * moves into the "first" state and is shown to the user + * + * @return The delay (in ms) until moving into the "first" state. Counted + * from when {@link #trigger()} is called. + */ + public int getFirstDelay() { + return firstDelay; + } + + /** + * Sets the delay (in ms) which must pass before the loading indicator moves + * into the "first" state and is shown to the user + * + * @param firstDelay + * The delay (in ms) until moving into the "first" state. Counted + * from when {@link #trigger()} is called. + */ + public void setFirstDelay(int firstDelay) { + this.firstDelay = firstDelay; + } + + /** + * Returns the delay (in ms) which must pass before the loading indicator + * moves to its "second" state. + * + * @return The delay (in ms) until the loading indicator moves into its + * "second" state. Counted from when {@link #trigger()} is called. + */ + public int getSecondDelay() { + return secondDelay; + } + + /** + * Sets the delay (in ms) which must pass before the loading indicator moves + * to its "second" state. + * + * @param secondDelay + * The delay (in ms) until the loading indicator moves into its + * "second" state. Counted from when {@link #trigger()} is + * called. + */ + public void setSecondDelay(int secondDelay) { + this.secondDelay = secondDelay; + } + + /** + * Returns the delay (in ms) which must pass before the loading indicator + * moves to its "third" state. + * + * @return The delay (in ms) until the loading indicator moves into its + * "third" state. Counted from when {@link #trigger()} is called. + */ + public int getThirdDelay() { + return thirdDelay; + } + + /** + * Sets the delay (in ms) which must pass before the loading indicator moves + * to its "third" state. + * + * @param thirdDelay + * The delay (in ms) from the event until changing the loading + * indicator into its "third" state. Counted from when + * {@link #trigger()} is called. + */ + public void setThirdDelay(int thirdDelay) { + this.thirdDelay = thirdDelay; + } + + /** + * Triggers displaying of this loading indicator. The loading indicator will + * actually be shown by {@link #show()} when the "first" delay (as specified + * by {@link #getFirstDelay()}) has passed. + * <p> + * The loading indicator will be hidden if shown when calling this method. + * </p> + */ + public void trigger() { + hide(); + firstTimer.schedule(getFirstDelay()); + } + + /** + * Shows the loading indicator in its standard state and triggers timers for + * transitioning into the "second" and "third" states. + */ + public void show() { + // Reset possible style name and display mode + getElement().setClassName(PRIMARY_STYLE_NAME); + getElement().addClassName("first"); + getElement().getStyle().setDisplay(Display.BLOCK); + + // Schedule the "second" loading indicator + int secondTimerDelay = getSecondDelay() - getFirstDelay(); + if (secondTimerDelay >= 0) { + secondTimer.schedule(secondTimerDelay); + } + + // Schedule the "third" loading indicator + int thirdTimerDelay = getThirdDelay() - getFirstDelay(); + if (thirdTimerDelay >= 0) { + thirdTimer.schedule(thirdTimerDelay); + } + } + + /** + * Returns the {@link ApplicationConnection} which uses this loading + * indicator + * + * @return The ApplicationConnection for this loading indicator + */ + public ApplicationConnection getConnection() { + return connection; + } + + /** + * Sets the {@link ApplicationConnection} which uses this loading indicator. + * Only used internally. + * + * @param connection + * The ApplicationConnection for this loading indicator + */ + void setConnection(ApplicationConnection connection) { + this.connection = connection; + } + + /** + * Hides the loading indicator (if visible). Cancels any possibly running + * timers. + */ + public void hide() { + firstTimer.cancel(); + secondTimer.cancel(); + thirdTimer.cancel(); + + getElement().getStyle().setDisplay(Display.NONE); + } + + /** + * Returns whether or not the loading indicator is showing. + * + * @return true if the loading indicator is visible, false otherwise + */ + public boolean isVisible() { + if (getElement().getStyle().getDisplay() + .equals(Display.NONE.getCssName())) { + return false; + } + + return true; + } + + /** + * Returns the root element of the loading indicator + * + * @return The loading indicator DOM element + */ + public Element getElement() { + if (element == null) { + element = DOM.createDiv(); + element.getStyle().setPosition(Position.ABSOLUTE); + getConnection().getUIConnector().getWidget().getElement() + .appendChild(element); + } + return element; + } + +} diff --git a/client/src/com/vaadin/client/VTooltip.java b/client/src/com/vaadin/client/VTooltip.java index 759b90a8cd..6191821988 100644 --- a/client/src/com/vaadin/client/VTooltip.java +++ b/client/src/com/vaadin/client/VTooltip.java @@ -15,8 +15,15 @@ */ package com.vaadin.client; +import com.google.gwt.aria.client.Id; +import com.google.gwt.aria.client.Roles; +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.DomEvent; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.MouseMoveEvent; @@ -40,11 +47,6 @@ public class VTooltip extends VOverlay { 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(); @@ -54,6 +56,16 @@ public class VTooltip extends VOverlay { // Open next tooltip faster. Disabled after 2 sec of showTooltip-silence. private boolean justClosed = false; + private String uniqueId = DOM.createUniqueId(); + private Element layoutElement; + private int maxWidth; + + // Delays for the tooltip, configurable on the server side + private int openDelay; + private int quickOpenDelay; + private int quickOpenTimeout; + private int closeTimeout; + /** * Used to show tooltips; usually used via the singleton in * {@link ApplicationConnection}. NOTE that #setOwner(Widget)} should be @@ -68,8 +80,17 @@ public class VTooltip extends VOverlay { setWidget(layout); layout.add(em); DOM.setElementProperty(description, "className", CLASSNAME + "-text"); - DOM.appendChild(layout.getElement(), description); + + layoutElement = layout.getElement(); + DOM.appendChild(layoutElement, description); setSinkShadowEvents(true); + + // Used to bind the tooltip to the owner for assistive devices + layoutElement.setId(uniqueId); + + description.setId(DOM.createUniqueId()); + Roles.getTooltipRole().set(layoutElement); + Roles.getTooltipRole().setAriaHiddenState(layoutElement, true); } /** @@ -104,8 +125,8 @@ public class VTooltip extends VOverlay { @Override public void setPosition(int offsetWidth, int offsetHeight) { - if (offsetWidth > MAX_WIDTH) { - setWidth(MAX_WIDTH + "px"); + if (offsetWidth > getMaxWidth()) { + setWidth(getMaxWidth() + "px"); // Check new height and width with reflowed content offsetWidth = getOffsetWidth(); @@ -150,7 +171,7 @@ public class VTooltip extends VOverlay { // Schedule timer for showing the tooltip according to if it was // recently closed or not. - int timeout = justClosed ? QUICK_OPEN_DELAY : OPEN_DELAY; + int timeout = justClosed ? getQuickOpenDelay() : getOpenDelay(); showTimer.schedule(timeout); opening = true; } @@ -200,19 +221,31 @@ public class VTooltip extends VOverlay { // already about to close return; } - closeTimer.schedule(CLOSE_TIMEOUT); + closeTimer.schedule(getCloseTimeout()); closing = true; justClosed = true; - justClosedTimer.schedule(QUICK_OPEN_TIMEOUT); + justClosedTimer.schedule(getQuickOpenTimeout()); + } + @Override + public void hide() { + super.hide(); + Roles.getTooltipRole().setAriaHiddenState(layoutElement, true); + Roles.getTooltipRole().removeAriaDescribedbyProperty( + tooltipEventHandler.currentElement); } private int tooltipEventMouseX; private int tooltipEventMouseY; - public void updatePosition(Event event) { - tooltipEventMouseX = DOM.eventGetClientX(event); - tooltipEventMouseY = DOM.eventGetClientY(event); + public void updatePosition(Event event, boolean isFocused) { + if (isFocused) { + tooltipEventMouseX = -1000; + tooltipEventMouseY = -1000; + } else { + tooltipEventMouseX = DOM.eventGetClientX(event); + tooltipEventMouseY = DOM.eventGetClientY(event); + } } @Override @@ -246,7 +279,7 @@ public class VTooltip extends VOverlay { } private class TooltipEventHandler implements MouseMoveHandler, - ClickHandler, KeyDownHandler { + ClickHandler, KeyDownHandler, FocusHandler, BlurHandler { /** * Current element hovered @@ -254,6 +287,11 @@ public class VTooltip extends VOverlay { private com.google.gwt.dom.client.Element currentElement = null; /** + * Current element focused + */ + private boolean currentIsFocused; + + /** * Current tooltip active */ private TooltipInfo currentTooltipInfo = null; @@ -299,6 +337,9 @@ public class VTooltip extends VOverlay { } if (connector != null && info != null) { + assert connector.hasTooltip() : "getTooltipInfo for " + + Util.getConnectorString(connector) + + " returned a tooltip even though hasTooltip claims there are no tooltips for the connector."; currentTooltipInfo = info; return true; } @@ -319,41 +360,77 @@ public class VTooltip extends VOverlay { @Override public void onMouseMove(MouseMoveEvent mme) { - Event event = Event.as(mme.getNativeEvent()); + handleShowHide(mme, false); + } + + @Override + public void onClick(ClickEvent event) { + handleHideEvent(); + } + + @Override + public void onKeyDown(KeyDownEvent event) { + handleHideEvent(); + } + + /** + * Displays Tooltip when page is navigated with the keyboard. + * + * Tooltip is not visible. This makes it possible for assistive devices + * to recognize the tooltip. + */ + @Override + public void onFocus(FocusEvent fe) { + handleShowHide(fe, true); + } + + /** + * Hides Tooltip when the page is navigated with the keyboard. + * + * Removes the Tooltip from page to make sure assistive devices don't + * recognize it by accident. + */ + @Override + public void onBlur(BlurEvent be) { + handleHideEvent(); + } + + private void handleShowHide(DomEvent domEvent, boolean isFocused) { + Event event = Event.as(domEvent.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) { + if (currentElement == element && currentIsFocused == isFocused) { return; } - currentElement = element; boolean connectorAndTooltipFound = resolveConnector((com.google.gwt.user.client.Element) element); if (!connectorAndTooltipFound) { if (isShowing()) { handleHideEvent(); + Roles.getButtonRole() + .removeAriaDescribedbyProperty(element); } else { currentTooltipInfo = null; } } else { - updatePosition(event); + updatePosition(event, isFocused); + if (isShowing()) { replaceCurrentTooltip(); + Roles.getTooltipRole().removeAriaDescribedbyProperty( + currentElement); } else { showTooltip(); } - } - } - @Override - public void onClick(ClickEvent event) { - handleHideEvent(); - } + Roles.getTooltipRole().setAriaDescribedbyProperty(element, + Id.of(uniqueId)); + } - @Override - public void onKeyDown(KeyDownEvent event) { - handleHideEvent(); + currentIsFocused = isFocused; + currentElement = element; } } @@ -370,6 +447,141 @@ public class VTooltip extends VOverlay { widget.addDomHandler(tooltipEventHandler, MouseMoveEvent.getType()); widget.addDomHandler(tooltipEventHandler, ClickEvent.getType()); widget.addDomHandler(tooltipEventHandler, KeyDownEvent.getType()); + widget.addDomHandler(tooltipEventHandler, FocusEvent.getType()); + widget.addDomHandler(tooltipEventHandler, BlurEvent.getType()); Profiler.leave("VTooltip.connectHandlersToWidget"); } + + /** + * Returns the unique id of the tooltip element. + * + * @return String containing the unique id of the tooltip, which always has + * a value + */ + public String getUniqueId() { + return uniqueId; + } + + @Override + public void setPopupPositionAndShow(PositionCallback callback) { + super.setPopupPositionAndShow(callback); + + Roles.getTooltipRole().setAriaHiddenState(layoutElement, false); + } + + /** + * Returns the time (in ms) the tooltip should be displayed after an event + * that will cause it to be closed (e.g. mouse click outside the component, + * key down). + * + * @return The close timeout (in ms) + */ + public int getCloseTimeout() { + return closeTimeout; + } + + /** + * Sets the time (in ms) the tooltip should be displayed after an event that + * will cause it to be closed (e.g. mouse click outside the component, key + * down). + * + * @param closeTimeout + * The close timeout (in ms) + */ + public void setCloseTimeout(int closeTimeout) { + this.closeTimeout = closeTimeout; + } + + /** + * Returns the time (in ms) during which {@link #getQuickOpenDelay()} should + * be used instead of {@link #getOpenDelay()}. The quick open delay is used + * when the tooltip has very recently been shown, is currently hidden but + * about to be shown again. + * + * @return The quick open timeout (in ms) + */ + public int getQuickOpenTimeout() { + return quickOpenTimeout; + } + + /** + * Sets the time (in ms) that determines when {@link #getQuickOpenDelay()} + * should be used instead of {@link #getOpenDelay()}. The quick open delay + * is used when the tooltip has very recently been shown, is currently + * hidden but about to be shown again. + * + * @param quickOpenTimeout + * The quick open timeout (in ms) + */ + public void setQuickOpenTimeout(int quickOpenTimeout) { + this.quickOpenTimeout = quickOpenTimeout; + } + + /** + * Returns the time (in ms) that should elapse before a tooltip will be + * shown, in the situation when a tooltip has very recently been shown + * (within {@link #getQuickOpenDelay()} ms). + * + * @return The quick open delay (in ms) + */ + public int getQuickOpenDelay() { + return quickOpenDelay; + } + + /** + * Sets the time (in ms) that should elapse before a tooltip will be shown, + * in the situation when a tooltip has very recently been shown (within + * {@link #getQuickOpenDelay()} ms). + * + * @param quickOpenDelay + * The quick open delay (in ms) + */ + public void setQuickOpenDelay(int quickOpenDelay) { + this.quickOpenDelay = quickOpenDelay; + } + + /** + * Returns the time (in ms) that should elapse after an event triggering + * tooltip showing has occurred (e.g. mouse over) before the tooltip is + * shown. If a tooltip has recently been shown, then + * {@link #getQuickOpenDelay()} is used instead of this. + * + * @return The open delay (in ms) + */ + public int getOpenDelay() { + return openDelay; + } + + /** + * Sets the time (in ms) that should elapse after an event triggering + * tooltip showing has occurred (e.g. mouse over) before the tooltip is + * shown. If a tooltip has recently been shown, then + * {@link #getQuickOpenDelay()} is used instead of this. + * + * @param openDelay + * The open delay (in ms) + */ + public void setOpenDelay(int openDelay) { + this.openDelay = openDelay; + } + + /** + * Sets the maximum width of the tooltip popup. + * + * @param maxWidth + * The maximum width the tooltip popup (in pixels) + */ + public void setMaxWidth(int maxWidth) { + this.maxWidth = maxWidth; + } + + /** + * Returns the maximum width of the tooltip popup. + * + * @return The maximum width the tooltip popup (in pixels) + */ + public int getMaxWidth() { + return maxWidth; + } + } diff --git a/client/src/com/vaadin/client/VUIDLBrowser.java b/client/src/com/vaadin/client/VUIDLBrowser.java index fded37ec5c..e6f38fb167 100644 --- a/client/src/com/vaadin/client/VUIDLBrowser.java +++ b/client/src/com/vaadin/client/VUIDLBrowser.java @@ -42,8 +42,12 @@ import com.vaadin.client.ui.UnknownComponentConnector; import com.vaadin.client.ui.VWindow; /** - * TODO Rename to something more Vaadin7-ish? + * @author Vaadin Ltd + * + * @deprecated as of 7.1. This class was mainly used by the old debug console + * but is retained for now for backwards compatibility. */ +@Deprecated public class VUIDLBrowser extends SimpleTree { private static final String HELP = "Shift click handle to open recursively. " + " Click components to highlight them on client side." diff --git a/client/src/com/vaadin/client/ValueMap.java b/client/src/com/vaadin/client/ValueMap.java index 5157bc91f5..4141eaa9d6 100644 --- a/client/src/com/vaadin/client/ValueMap.java +++ b/client/src/com/vaadin/client/ValueMap.java @@ -70,12 +70,12 @@ public final class ValueMap extends JavaScriptObject { return attrs; } - native JsArrayString getJSStringArray(String name) + public native JsArrayString getJSStringArray(String name) /*-{ return this[name]; }-*/; - native JsArray<ValueMap> getJSValueMapArray(String name) + public native JsArray<ValueMap> getJSValueMapArray(String name) /*-{ return this[name]; }-*/; diff --git a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java new file mode 100644 index 0000000000..bc7e0b3fd2 --- /dev/null +++ b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java @@ -0,0 +1,453 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.communication; + +import java.util.ArrayList; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.user.client.Command; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ResourceLoader; +import com.vaadin.client.ResourceLoader.ResourceLoadEvent; +import com.vaadin.client.ResourceLoader.ResourceLoadListener; +import com.vaadin.client.VConsole; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.ui.ui.UIConstants; + +/** + * The default {@link PushConnection} implementation that uses Atmosphere for + * handling the communication channel. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class AtmospherePushConnection implements PushConnection { + + protected enum State { + /** + * Opening request has been sent, but still waiting for confirmation + */ + CONNECT_PENDING, + + /** + * Connection is open and ready to use. + */ + CONNECTED, + + /** + * Connection was disconnected while the connection was pending. Wait + * for the connection to get established before closing it. No new + * messages are accepted, but pending messages will still be delivered. + */ + DISCONNECT_PENDING, + + /** + * Connection has been disconnected and should not be used any more. + */ + DISCONNECTED; + } + + /** + * Represents a message that should be sent as multiple fragments. + */ + protected static class FragmentedMessage { + + // Jetty requires length less than buffer size + private int FRAGMENT_LENGTH = ApplicationConstants.WEBSOCKET_BUFFER_SIZE - 1; + + private String message; + private int index = 0; + + public FragmentedMessage(String message) { + this.message = message; + } + + public boolean hasNextFragment() { + return index < message.length(); + } + + public String getNextFragment() { + String result; + if (index == 0) { + String header = "" + message.length() + + ApplicationConstants.WEBSOCKET_MESSAGE_DELIMITER; + int fragmentLen = FRAGMENT_LENGTH - header.length(); + result = header + getFragment(0, fragmentLen); + index += fragmentLen; + } else { + result = getFragment(index, index + FRAGMENT_LENGTH); + index += FRAGMENT_LENGTH; + } + return result; + } + + private String getFragment(int begin, int end) { + return message.substring(begin, Math.min(message.length(), end)); + } + } + + private ApplicationConnection connection; + + private JavaScriptObject socket; + + private ArrayList<String> messageQueue = new ArrayList<String>(); + + private State state = State.CONNECT_PENDING; + + private AtmosphereConfiguration config; + + private String uri; + + private String transport; + + /** + * Keeps track of the disconnect confirmation command for cases where + * pending messages should be pushed before actually disconnecting. + */ + private Command pendingDisconnectCommand; + + public AtmospherePushConnection() { + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.client.communication.PushConenction#init(com.vaadin.client + * .ApplicationConnection) + */ + @Override + public void init(final ApplicationConnection connection) { + this.connection = connection; + + runWhenAtmosphereLoaded(new Command() { + @Override + public void execute() { + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + connect(); + } + }); + } + }); + } + + private void connect() { + String baseUrl = connection + .translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX + + ApplicationConstants.PUSH_PATH + '/'); + String extraParams = UIConstants.UI_ID_PARAMETER + "=" + + connection.getConfiguration().getUIId(); + extraParams += "&" + ApplicationConstants.CSRF_TOKEN_PARAMETER + "=" + + connection.getCsrfToken(); + + // uri is needed to identify the right connection when closing + uri = ApplicationConnection.addGetParameters(baseUrl, extraParams); + + VConsole.log("Establishing push connection"); + socket = doConnect(uri, getConfig()); + } + + @Override + public boolean isActive() { + switch (state) { + case CONNECT_PENDING: + case CONNECTED: + return true; + default: + return false; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.client.communication.PushConenction#push(java.lang.String) + */ + @Override + public void push(String message) { + switch (state) { + case CONNECT_PENDING: + assert isActive(); + VConsole.log("Queuing push message: " + message); + messageQueue.add(message); + break; + case CONNECTED: + assert isActive(); + VConsole.log("Sending push message: " + message); + + if (transport.equals("websocket")) { + FragmentedMessage fragmented = new FragmentedMessage(message); + while (fragmented.hasNextFragment()) { + doPush(socket, fragmented.getNextFragment()); + } + } else { + doPush(socket, message); + } + break; + case DISCONNECT_PENDING: + case DISCONNECTED: + throw new IllegalStateException("Can not push after disconnecting"); + } + } + + protected AtmosphereConfiguration getConfig() { + if (config == null) { + config = createConfig(); + } + return config; + } + + protected void onOpen(AtmosphereResponse response) { + transport = response.getTransport(); + + VConsole.log("Push connection established using " + transport); + + switch (state) { + case CONNECT_PENDING: + state = State.CONNECTED; + for (String message : messageQueue) { + push(message); + } + messageQueue.clear(); + break; + case DISCONNECT_PENDING: + // Set state to connected to make disconnect close the connection + state = State.CONNECTED; + assert pendingDisconnectCommand != null; + disconnect(pendingDisconnectCommand); + break; + case CONNECTED: + // IE likes to open the same connection multiple times, just ignore + break; + default: + throw new IllegalStateException( + "Got onOpen event when conncetion state is " + state + + ". This should never happen."); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.communication.PushConenction#disconnect() + */ + @Override + public void disconnect(Command command) { + assert command != null; + + switch (state) { + case CONNECT_PENDING: + // Make the connection callback initiate the disconnection again + state = State.DISCONNECT_PENDING; + pendingDisconnectCommand = command; + break; + case CONNECTED: + // Normal disconnect + VConsole.log("Closing push connection"); + doDisconnect(uri); + state = State.DISCONNECTED; + command.execute(); + break; + case DISCONNECT_PENDING: + case DISCONNECTED: + throw new IllegalStateException("Can not disconnect more than once"); + } + } + + protected void onMessage(AtmosphereResponse response) { + String message = response.getResponseBody(); + if (message.startsWith("for(;;);")) { + VConsole.log("Received push message: " + message); + // "for(;;);[{json}]" -> "{json}" + message = message.substring(9, message.length() - 1); + connection.handlePushMessage(message); + } + } + + /** + * Called if the transport mechanism cannot be used and the fallback will be + * tried + */ + protected void onTransportFailure() { + VConsole.log("Push connection using primary method (" + + getConfig().getTransport() + ") failed. Trying with " + + getConfig().getFallbackTransport()); + } + + /** + * Called if the push connection fails. Atmosphere will automatically retry + * the connection until successful. + * + */ + protected void onError() { + VConsole.error("Push connection using " + getConfig().getTransport() + + " failed!"); + } + + public static abstract class AbstractJSO extends JavaScriptObject { + protected AbstractJSO() { + + } + + protected final native String getStringValue(String key) + /*-{ + return this[key]; + }-*/; + + protected final native void setStringValue(String key, String value) + /*-{ + this[key] = value; + }-*/; + + protected final native int getIntValue(String key) + /*-{ + return this[key]; + }-*/; + + protected final native void setIntValue(String key, int value) + /*-{ + this[key] = value; + }-*/; + + } + + public static class AtmosphereConfiguration extends AbstractJSO { + + protected AtmosphereConfiguration() { + super(); + } + + public final String getTransport() { + return getStringValue("transport"); + } + + public final String getFallbackTransport() { + return getStringValue("fallbackTransport"); + } + + public final void setTransport(String transport) { + setStringValue("transport", transport); + } + + public final void setFallbackTransport(String fallbackTransport) { + setStringValue("fallbackTransport", fallbackTransport); + } + } + + public static class AtmosphereResponse extends AbstractJSO { + + protected AtmosphereResponse() { + + } + + public final String getResponseBody() { + return getStringValue("responseBody"); + } + + public final String getState() { + return getStringValue("state"); + } + + public final String getError() { + return getStringValue("error"); + } + + public final String getTransport() { + return getStringValue("transport"); + } + + } + + protected native AtmosphereConfiguration createConfig() + /*-{ + return { + transport: 'websocket', + fallbackTransport: 'streaming', + contentType: 'application/json; charset=UTF-8', + reconnectInterval: '5000', + maxReconnectOnClose: 10000000, + trackMessageLength: true + }; + }-*/; + + private native JavaScriptObject doConnect(String uri, + JavaScriptObject config) + /*-{ + var self = this; + + config.url = uri; + config.onOpen = $entry(function(response) { + self.@com.vaadin.client.communication.AtmospherePushConnection::onOpen(*)(response); + }); + config.onMessage = $entry(function(response) { + self.@com.vaadin.client.communication.AtmospherePushConnection::onMessage(*)(response); + }); + config.onError = $entry(function(response) { + self.@com.vaadin.client.communication.AtmospherePushConnection::onError()(response); + }); + config.onTransportFailure = $entry(function(reason,request) { + self.@com.vaadin.client.communication.AtmospherePushConnection::onTransportFailure(*)(reason); + }); + + return $wnd.jQueryVaadin.atmosphere.subscribe(config); + }-*/; + + private native void doPush(JavaScriptObject socket, String message) + /*-{ + socket.push(message); + }-*/; + + private static native void doDisconnect(String url) + /*-{ + $wnd.jQueryVaadin.atmosphere.unsubscribeUrl(url); + }-*/; + + private static native boolean isAtmosphereLoaded() + /*-{ + return $wnd.jQueryVaadin != undefined; + }-*/; + + private void runWhenAtmosphereLoaded(final Command command) { + + if (isAtmosphereLoaded()) { + command.execute(); + } else { + VConsole.log("Loading " + ApplicationConstants.VAADIN_PUSH_JS); + ResourceLoader.get().loadScript( + connection.getConfiguration().getVaadinDirUrl() + + ApplicationConstants.VAADIN_PUSH_JS, + new ResourceLoadListener() { + @Override + public void onLoad(ResourceLoadEvent event) { + VConsole.log(ApplicationConstants.VAADIN_PUSH_JS + + " loaded"); + command.execute(); + } + + @Override + public void onError(ResourceLoadEvent event) { + VConsole.error(event.getResourceUrl() + + " could not be loaded. Push will not work."); + } + }); + } + } +} diff --git a/client/src/com/vaadin/client/communication/PushConnection.java b/client/src/com/vaadin/client/communication/PushConnection.java new file mode 100644 index 0000000000..61656242bd --- /dev/null +++ b/client/src/com/vaadin/client/communication/PushConnection.java @@ -0,0 +1,91 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.communication; + +import com.google.gwt.user.client.Command; +import com.vaadin.client.ApplicationConnection; + +/** + * Represents the client-side endpoint of a bidirectional ("push") communication + * channel. Can be used to send UIDL request messages to the server and to + * receive UIDL messages from the server (either asynchronously or as a response + * to a UIDL request.) Delegates the UIDL handling to the + * {@link ApplicationConnection}. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public interface PushConnection { + + /** + * Two-phase construction to allow using GWT.create(). + * + * @param connection + * The ApplicationConnection + */ + public void init(ApplicationConnection connection); + + /** + * Pushes a message to the server. Will throw an exception if the connection + * is not active (see {@link #isActive()}). + * <p> + * Implementation detail: The implementation is responsible for queuing + * messages that are pushed after {@link #init(ApplicationConnection)} has + * been called but before the connection has internally been set up and then + * replay those messages in the original order when the connection has been + * established. + * + * @param message + * the message to push + * @throws IllegalStateException + * if this connection is not active + * + * @see #isActive() + */ + public void push(String message); + + /** + * Checks whether this push connection is in a state where it can push + * messages to the server. A connection is active until + * {@link #disconnect(Command)} has been called. + * + * @return <code>true</code> if this connection can accept new messages; + * <code>false</code> if this connection is disconnected or + * disconnecting. + */ + public boolean isActive(); + + /** + * Closes the push connection. To ensure correct message delivery order, new + * messages should not be sent using any other channel until it has been + * confirmed that all messages pending for this connection have been + * delivered. The provided command callback is invoked when messages can be + * passed using some other communication channel. + * <p> + * After this method has been called, {@link #isActive()} returns + * <code>false</code>. Calling this method for a connection that is no + * longer active will throw an exception. + * + * @param command + * callback command invoked when the connection has been properly + * disconnected + * @throws IllegalStateException + * if this connection is not active + */ + public void disconnect(Command command); + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/debug/internal/DebugButton.java b/client/src/com/vaadin/client/debug/internal/DebugButton.java new file mode 100644 index 0000000000..a49a392fbe --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/DebugButton.java @@ -0,0 +1,109 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import com.google.gwt.user.client.ui.Button; + +/** + * Simple extension of {@link Button} that is preconfigured with for use in + * {@link VDebugWindow}. Uses icon-font for icons, and allows title to be + * specified in the constructor. + * + * @since 7.1 + * @author Vaadin Ltd + */ +public class DebugButton extends Button { + + protected boolean active = false; + + /** + * Creates a {@link Button} with the given icon-font icon. The icon id will + * be used in the <i>data-icon</i> attribute of an <i><i></i> -tag. + * + * @param icon + * Identifier for the desired icon in an icon-font + */ + public DebugButton(Icon icon) { + this(icon, null, null); + } + + /*- + public DebugButton(String caption) { + this(null, null, caption); + } + + public DebugButton(String caption, String title) { + this(null, title, caption); + } + -*/ + + /** + * Creates a {@link Button} with the given icon-font icon and title + * (tooltip). The icon id will be used in the <i>data-icon</i> attribute of + * an <i><i></i> -tag. + * + * @param icon + * Identifier for the desired icon in an icon-font + * @param title + * Button title (tooltip) + * + */ + public DebugButton(Icon icon, String title) { + this(icon, title, null); + } + + /** + * Creates a {@link Button} with the given icon-font icon, title (tooltip), + * and caption. The icon id will be used in the <i>data-icon</i> attribute + * of an <i><i></i> -tag. + * + * @param icon + * Identifier for the desired icon in an icon-font + * @param title + * Title (tooltip) + * @param caption + * Button baption + */ + public DebugButton(Icon icon, String title, String caption) { + super((icon != null ? icon : "") + (caption != null ? caption : "")); + if (title != null) { + setTitle(title); + } + + setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + } + + /** + * Adds or removes a stylename, indicating whether or not the button is in + * it's active state. + * + * @param active + */ + public void setActive(boolean active) { + this.active = active; + setStyleDependentName(VDebugWindow.STYLENAME_ACTIVE, active); + } + + /** + * Indicates wheter the Button is currently in its active state or not + * + * @return true if the Button is active, false otherwise + */ + public boolean isActive() { + return active; + } + +} diff --git a/client/src/com/vaadin/client/debug/internal/ErrorNotificationHandler.java b/client/src/com/vaadin/client/debug/internal/ErrorNotificationHandler.java new file mode 100644 index 0000000000..0e4c57494b --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/ErrorNotificationHandler.java @@ -0,0 +1,86 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.debug.internal; + +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import com.google.gwt.logging.client.TextLogFormatter; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ui.VNotification; + +/** + * Log message handler that shows big red notification for {@link Level#SEVERE} + * messages that have a throwable. + * + * @since 7.1 + * @author Vaadin Ltd + */ +public class ErrorNotificationHandler extends Handler { + public ErrorNotificationHandler() { + setLevel(Level.SEVERE); + setFormatter(new TextLogFormatter(true) { + @Override + protected String getRecordInfo(LogRecord event, String newline) { + return ""; + } + }); + } + + @Override + public void publish(LogRecord record) { + if (!isLoggable(record) || record.getThrown() == null) { + return; + } + + try { + String exceptionText = getFormatter().format(record); + + Widget owner = null; + + if (!ApplicationConfiguration.getRunningApplications().isEmpty()) { + /* + * Make a wild guess and use the first available + * ApplicationConnection. This is better than than leaving the + * exception completely unstyled... + */ + ApplicationConnection connection = ApplicationConfiguration + .getRunningApplications().get(0); + owner = connection.getUIConnector().getWidget(); + } + VNotification + .createNotification(VNotification.DELAY_FOREVER, owner) + .show("<h1>Uncaught client side exception</h1><br />" + + exceptionText, VNotification.CENTERED, "error"); + } catch (Exception e2) { + // Just swallow this exception + } + } + + @Override + public void close() { + // Nothing to do + } + + @Override + public void flush() { + // Nothing todo + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/debug/internal/HierarchySection.java b/client/src/com/vaadin/client/debug/internal/HierarchySection.java new file mode 100644 index 0000000000..e4f3e2dcb1 --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/HierarchySection.java @@ -0,0 +1,676 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gwt.core.client.JsArray; +import com.google.gwt.dom.client.Style.TextDecoration; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.DoubleClickEvent; +import com.google.gwt.event.dom.client.DoubleClickHandler; +import com.google.gwt.event.dom.client.HasDoubleClickHandlers; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.MouseOutEvent; +import com.google.gwt.event.dom.client.MouseOutHandler; +import com.google.gwt.event.dom.client.MouseOverEvent; +import com.google.gwt.event.dom.client.MouseOverHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ComputedStyle; +import com.vaadin.client.ConnectorMap; +import com.vaadin.client.JsArrayObject; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.SimpleTree; +import com.vaadin.client.Util; +import com.vaadin.client.VConsole; +import com.vaadin.client.ValueMap; +import com.vaadin.client.metadata.NoDataException; +import com.vaadin.client.metadata.Property; +import com.vaadin.client.ui.AbstractConnector; +import com.vaadin.client.ui.UnknownComponentConnector; +import com.vaadin.shared.AbstractComponentState; +import com.vaadin.shared.communication.SharedState; + +/** + * Provides functionality for examining the UI component hierarchy. + * + * @since 7.1 + * @author Vaadin Ltd + */ +public class HierarchySection implements Section { + private final DebugButton tabButton = new DebugButton(Icon.HIERARCHY, + "Examine component hierarchy"); + + private final FlowPanel content = new FlowPanel(); + private final FlowPanel controls = new FlowPanel(); + + private final Button find = new DebugButton(Icon.HIGHLIGHT, + "Select a component on the page to inspect it"); + private final Button analyze = new DebugButton(Icon.ANALYZE, + "Check layouts for potential problems"); + private final Button generateWS = new DebugButton(Icon.OPTIMIZE, + "Show used connectors and how to optimize widgetset"); + private final Button showHierarchy = new DebugButton(Icon.HIERARCHY, + "Show the connector hierarchy tree"); + + private HandlerRegistration highlightModeRegistration = null; + + public HierarchySection() { + controls.add(showHierarchy); + showHierarchy.setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + showHierarchy.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + showHierarchy(); + } + }); + + controls.add(find); + find.setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + find.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + toggleFind(); + } + }); + + controls.add(analyze); + analyze.setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + analyze.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + stopFind(); + analyzeLayouts(); + } + }); + + controls.add(generateWS); + generateWS.setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + generateWS.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + generateWidgetset(); + } + }); + + content.setStylePrimaryName(VDebugWindow.STYLENAME + "-hierarchy"); + } + + private void showHierarchy() { + Highlight.hideAll(); + content.clear(); + + // TODO Clearing and rebuilding the contents is not optimal for UX as + // any previous expansions are lost. + SimplePanel trees = new SimplePanel(); + + for (ApplicationConnection application : ApplicationConfiguration + .getRunningApplications()) { + ServerConnector uiConnector = application.getUIConnector(); + Widget connectorTree = buildConnectorTree(uiConnector); + + trees.add(connectorTree); + } + + content.add(trees); + } + + private Widget buildConnectorTree(final ServerConnector connector) { + String connectorString = Util.getConnectorString(connector); + + List<ServerConnector> children = connector.getChildren(); + + Widget widget; + if (children == null || children.isEmpty()) { + // Leaf node, just add a label + Label label = new Label(connectorString); + label.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + Highlight.showOnly(connector); + } + }); + widget = label; + } else { + SimpleTree tree = new SimpleTree(connectorString) { + @Override + protected void select(ClickEvent event) { + super.select(event); + Highlight.showOnly(connector); + } + }; + for (ServerConnector child : children) { + tree.add(buildConnectorTree(child)); + } + widget = tree; + } + + if (widget instanceof HasDoubleClickHandlers) { + HasDoubleClickHandlers has = (HasDoubleClickHandlers) widget; + has.addDoubleClickHandler(new DoubleClickHandler() { + @Override + public void onDoubleClick(DoubleClickEvent event) { + printState(connector); + } + }); + } + + return widget; + } + + @Override + public DebugButton getTabButton() { + return tabButton; + } + + @Override + public Widget getControls() { + return controls; + } + + @Override + public Widget getContent() { + return content; + } + + @Override + public void show() { + + } + + @Override + public void hide() { + stopFind(); + } + + private void generateWidgetset() { + + content.clear(); + HTML h = new HTML("Getting used connectors"); + content.add(h); + + String s = ""; + for (ApplicationConnection ac : ApplicationConfiguration + .getRunningApplications()) { + ApplicationConfiguration conf = ac.getConfiguration(); + s += "<h1>Used connectors for " + conf.getServiceUrl() + "</h1>"; + + for (String connectorName : getUsedConnectorNames(conf)) { + s += connectorName + "<br/>"; + } + + s += "<h2>To make an optimized widgetset based on these connectors, do:</h2>"; + s += "<h3>1. Add to your widgetset.gwt.xml file:</h2>"; + s += "<textarea rows=\"3\" style=\"width:90%\">"; + s += "<generate-with class=\"OptimizedConnectorBundleLoaderFactory\">\n"; + s += " <when-type-assignable class=\"com.vaadin.client.metadata.ConnectorBundleLoader\" />\n"; + s += "</generate-with>"; + s += "</textarea>"; + + s += "<h3>2. Add the following java file to your project:</h2>"; + s += "<textarea rows=\"5\" style=\"width:90%\">"; + s += generateOptimizedWidgetSet(getUsedConnectorNames(conf)); + s += "</textarea>"; + s += "<h3>3. Recompile widgetset</h2>"; + + } + + h.setHTML(s); + } + + private Set<String> getUsedConnectorNames( + ApplicationConfiguration configuration) { + int tag = 0; + Set<String> usedConnectors = new HashSet<String>(); + while (true) { + String serverSideClass = configuration + .getServerSideClassNameForTag(tag); + if (serverSideClass == null) { + break; + } + Class<? extends ServerConnector> connectorClass = configuration + .getConnectorClassByEncodedTag(tag); + if (connectorClass == null) { + break; + } + + if (connectorClass != UnknownComponentConnector.class) { + usedConnectors.add(connectorClass.getName()); + } + tag++; + if (tag > 10000) { + // Sanity check + VConsole.error("Search for used connector classes was forcefully terminated"); + break; + } + } + return usedConnectors; + } + + public String generateOptimizedWidgetSet(Set<String> usedConnectors) { + String s = "import java.util.HashSet;\n"; + s += "import java.util.Set;\n"; + + s += "import com.google.gwt.core.ext.typeinfo.JClassType;\n"; + s += "import com.vaadin.client.ui.ui.UIConnector;\n"; + s += "import com.vaadin.server.widgetsetutils.ConnectorBundleLoaderFactory;\n"; + s += "import com.vaadin.shared.ui.Connect.LoadStyle;\n\n"; + + s += "public class OptimizedConnectorBundleLoaderFactory extends\n"; + s += " ConnectorBundleLoaderFactory {\n"; + s += " private Set<String> eagerConnectors = new HashSet<String>();\n"; + s += " {\n"; + for (String c : usedConnectors) { + s += " eagerConnectors.add(" + c + + ".class.getName());\n"; + } + s += " }\n"; + s += "\n"; + s += " @Override\n"; + s += " protected LoadStyle getLoadStyle(JClassType connectorType) {\n"; + s += " if (eagerConnectors.contains(connectorType.getQualifiedBinaryName())) {\n"; + s += " return LoadStyle.EAGER;\n"; + s += " } else {\n"; + s += " // Loads all other connectors immediately after the initial view has\n"; + s += " // been rendered\n"; + s += " return LoadStyle.DEFERRED;\n"; + s += " }\n"; + s += " }\n"; + s += "}\n"; + + return s; + } + + private void analyzeLayouts() { + content.clear(); + content.add(new Label("Analyzing layouts...")); + List<ApplicationConnection> runningApplications = ApplicationConfiguration + .getRunningApplications(); + for (ApplicationConnection applicationConnection : runningApplications) { + applicationConnection.analyzeLayouts(); + } + } + + @Override + public void meta(ApplicationConnection ac, ValueMap meta) { + content.clear(); + JsArray<ValueMap> valueMapArray = meta + .getJSValueMapArray("invalidLayouts"); + int size = valueMapArray.length(); + + if (size > 0) { + SimpleTree root = new SimpleTree("Layouts analyzed, " + size + + " top level problems"); + for (int i = 0; i < size; i++) { + printLayoutError(ac, valueMapArray.get(i), root); + } + root.open(false); + content.add(root); + } else { + content.add(new Label("Layouts analyzed, no top level problems")); + } + + Set<ComponentConnector> zeroHeightComponents = new HashSet<ComponentConnector>(); + Set<ComponentConnector> zeroWidthComponents = new HashSet<ComponentConnector>(); + findZeroSizeComponents(zeroHeightComponents, zeroWidthComponents, + ac.getUIConnector()); + if (zeroHeightComponents.size() > 0 || zeroWidthComponents.size() > 0) { + content.add(new HTML("<h4> Client side notifications</h4>" + + " <em>The following relative sized components were " + + "rendered to a zero size container on the client side." + + " Note that these are not necessarily invalid " + + "states, but reported here as they might be.</em>")); + if (zeroHeightComponents.size() > 0) { + content.add(new HTML( + "<p><strong>Vertically zero size:</strong></p>")); + printClientSideDetectedIssues(zeroHeightComponents, ac); + } + if (zeroWidthComponents.size() > 0) { + content.add(new HTML( + "<p><strong>Horizontally zero size:</strong></p>")); + printClientSideDetectedIssues(zeroWidthComponents, ac); + } + } + + } + + private void printClientSideDetectedIssues( + Set<ComponentConnector> zeroSized, ApplicationConnection ac) { + + // keep track of already highlighted parents + HashSet<String> parents = new HashSet<String>(); + + for (final ComponentConnector connector : zeroSized) { + final ServerConnector parent = connector.getParent(); + final String parentId = parent.getConnectorId(); + + final Label errorDetails = new Label(Util.getSimpleName(connector) + + "[" + connector.getConnectorId() + "]" + " inside " + + Util.getSimpleName(parent)); + + if (parent instanceof ComponentConnector) { + final ComponentConnector parentConnector = (ComponentConnector) parent; + if (!parents.contains(parentId)) { + parents.add(parentId); + Highlight.show(parentConnector, "yellow"); + } + + errorDetails.addMouseOverHandler(new MouseOverHandler() { + @Override + public void onMouseOver(MouseOverEvent event) { + Highlight.hideAll(); + Highlight.show(parentConnector, "yellow"); + Highlight.show(connector); + errorDetails.getElement().getStyle() + .setTextDecoration(TextDecoration.UNDERLINE); + } + }); + errorDetails.addMouseOutHandler(new MouseOutHandler() { + @Override + public void onMouseOut(MouseOutEvent event) { + Highlight.hideAll(); + errorDetails.getElement().getStyle() + .setTextDecoration(TextDecoration.NONE); + } + }); + errorDetails.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + printState(connector); + Highlight.show(connector); + } + }); + + } + + Highlight.show(connector); + content.add(errorDetails); + + } + } + + private void printLayoutError(ApplicationConnection ac, ValueMap valueMap, + SimpleTree root) { + final String pid = valueMap.getString("id"); + + // find connector + final ComponentConnector connector = (ComponentConnector) ConnectorMap + .get(ac).getConnector(pid); + + if (connector == null) { + root.add(new SimpleTree("[" + pid + "] NOT FOUND")); + return; + } + + Highlight.show(connector); + + final SimpleTree errorNode = new SimpleTree( + Util.getSimpleName(connector) + " id: " + pid); + errorNode.addDomHandler(new MouseOverHandler() { + @Override + public void onMouseOver(MouseOverEvent event) { + Highlight.showOnly(connector); + ((Widget) event.getSource()).getElement().getStyle() + .setTextDecoration(TextDecoration.UNDERLINE); + } + }, MouseOverEvent.getType()); + errorNode.addDomHandler(new MouseOutHandler() { + @Override + public void onMouseOut(MouseOutEvent event) { + Highlight.hideAll(); + ((Widget) event.getSource()).getElement().getStyle() + .setTextDecoration(TextDecoration.NONE); + } + }, MouseOutEvent.getType()); + + errorNode.addDomHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (event.getNativeEvent().getEventTarget().cast() == errorNode + .getElement().getChild(1).cast()) { + printState(connector); + } + } + }, ClickEvent.getType()); + + VerticalPanel errorDetails = new VerticalPanel(); + + if (valueMap.containsKey("heightMsg")) { + errorDetails.add(new Label("Height problem: " + + valueMap.getString("heightMsg"))); + } + if (valueMap.containsKey("widthMsg")) { + errorDetails.add(new Label("Width problem: " + + valueMap.getString("widthMsg"))); + } + if (errorDetails.getWidgetCount() > 0) { + errorNode.add(errorDetails); + } + if (valueMap.containsKey("subErrors")) { + HTML l = new HTML( + "<em>Expand this node to show problems that may be dependent on this problem.</em>"); + errorDetails.add(l); + JsArray<ValueMap> suberrors = valueMap + .getJSValueMapArray("subErrors"); + for (int i = 0; i < suberrors.length(); i++) { + ValueMap value = suberrors.get(i); + printLayoutError(ac, value, errorNode); + } + + } + root.add(errorNode); + } + + private void findZeroSizeComponents( + Set<ComponentConnector> zeroHeightComponents, + Set<ComponentConnector> zeroWidthComponents, + ComponentConnector connector) { + Widget widget = connector.getWidget(); + ComputedStyle computedStyle = new ComputedStyle(widget.getElement()); + if (computedStyle.getIntProperty("height") == 0) { + zeroHeightComponents.add(connector); + } + if (computedStyle.getIntProperty("width") == 0) { + zeroWidthComponents.add(connector); + } + List<ServerConnector> children = connector.getChildren(); + for (ServerConnector serverConnector : children) { + if (serverConnector instanceof ComponentConnector) { + findZeroSizeComponents(zeroHeightComponents, + zeroWidthComponents, + (ComponentConnector) serverConnector); + } + } + } + + @Override + public void uidl(ApplicationConnection ac, ValueMap uidl) { + // NOP + } + + private boolean isFindMode() { + return (highlightModeRegistration != null); + } + + private void toggleFind() { + if (isFindMode()) { + stopFind(); + } else { + startFind(); + } + } + + private void startFind() { + Highlight.hideAll(); + if (!isFindMode()) { + highlightModeRegistration = Event + .addNativePreviewHandler(highlightModeHandler); + find.addStyleDependentName(VDebugWindow.STYLENAME_ACTIVE); + } + } + + private void stopFind() { + if (isFindMode()) { + highlightModeRegistration.removeHandler(); + highlightModeRegistration = null; + find.removeStyleDependentName(VDebugWindow.STYLENAME_ACTIVE); + } + } + + private void printState(ServerConnector connector) { + Highlight.showOnly(connector); + + SharedState state = connector.getState(); + + Set<String> ignoreProperties = new HashSet<String>(); + ignoreProperties.add("id"); + + String html = getRowHTML("Id", connector.getConnectorId()); + html += getRowHTML("Connector", Util.getSimpleName(connector)); + + if (connector instanceof ComponentConnector) { + ComponentConnector component = (ComponentConnector) connector; + + ignoreProperties.addAll(Arrays.asList("caption", "description", + "width", "height")); + + AbstractComponentState componentState = component.getState(); + + html += getRowHTML("Widget", + Util.getSimpleName(component.getWidget())); + html += getRowHTML("Caption", componentState.caption); + html += getRowHTML("Description", componentState.description); + html += getRowHTML("Width", componentState.width + " (actual: " + + component.getWidget().getOffsetWidth() + "px)"); + html += getRowHTML("Height", componentState.height + " (actual: " + + component.getWidget().getOffsetHeight() + "px)"); + } + + try { + JsArrayObject<Property> properties = AbstractConnector + .getStateType(connector).getPropertiesAsArray(); + for (int i = 0; i < properties.size(); i++) { + Property property = properties.get(i); + String name = property.getName(); + if (!ignoreProperties.contains(name)) { + html += getRowHTML(property.getDisplayName(), + property.getValue(state)); + } + } + } catch (NoDataException e) { + html += "<div>Could not read state, error has been logged to the console</div>"; + VConsole.error(e); + } + + content.clear(); + content.add(new HTML(html)); + } + + private String getRowHTML(String caption, Object value) { + return "<div class=\"" + VDebugWindow.STYLENAME + + "-row\"><span class=\"caption\">" + caption + + "</span><span class=\"value\">" + + Util.escapeHTML(String.valueOf(value)) + "</span></div>"; + } + + private final NativePreviewHandler highlightModeHandler = new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + + if (event.getTypeInt() == Event.ONKEYDOWN + && event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) { + stopFind(); + Highlight.hideAll(); + return; + } + if (event.getTypeInt() == Event.ONMOUSEMOVE) { + Highlight.hideAll(); + Element eventTarget = Util.getElementFromPoint(event + .getNativeEvent().getClientX(), event.getNativeEvent() + .getClientY()); + if (VDebugWindow.get().getElement().isOrHasChild(eventTarget)) { + content.clear(); + return; + } + + for (ApplicationConnection a : ApplicationConfiguration + .getRunningApplications()) { + ComponentConnector connector = Util.getConnectorForElement( + a, a.getUIConnector().getWidget(), eventTarget); + if (connector == null) { + connector = Util.getConnectorForElement(a, + RootPanel.get(), eventTarget); + } + if (connector != null) { + printState(connector); + event.cancel(); + event.consume(); + event.getNativeEvent().stopPropagation(); + return; + } + } + content.clear(); + } + if (event.getTypeInt() == Event.ONCLICK) { + Highlight.hideAll(); + event.cancel(); + event.consume(); + event.getNativeEvent().stopPropagation(); + stopFind(); + Element eventTarget = Util.getElementFromPoint(event + .getNativeEvent().getClientX(), event.getNativeEvent() + .getClientY()); + for (ApplicationConnection a : ApplicationConfiguration + .getRunningApplications()) { + ComponentConnector connector = Util.getConnectorForElement( + a, a.getUIConnector().getWidget(), eventTarget); + if (connector == null) { + connector = Util.getConnectorForElement(a, + RootPanel.get(), eventTarget); + } + + if (connector != null) { + printState(connector); + return; + } + } + } + event.cancel(); + } + + }; + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/debug/internal/Highlight.java b/client/src/com/vaadin/client/debug/internal/Highlight.java new file mode 100644 index 0000000000..f2695f58ca --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/Highlight.java @@ -0,0 +1,210 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import java.util.HashSet; + +import 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.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.BrowserInfo; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.ui.VWindow; + +/** + * Highlights a widget in the UI by overlaying a semi-transparent colored div. + * <p> + * Multiple highlights can be added, then selectively removed with + * {@link #hide(Element)} or all at once with {@link #hideAll()}. + * </p> + * <p> + * Note that highlights are intended to be short-term; highlights do not move or + * disappear with the highlighted widget, and it is also fairly likely that + * someone else calls {@link #hideAll()} eventually. + * </p> + * + * @since 7.1 + * @author Vaadin Ltd + */ +public class Highlight { + + private static final String DEFAULT_COLOR = "red"; + private static final double DEFAULT_OPACITY = 0.3; + private static final int MIN_WIDTH = 3; + private static final int MIN_HEIGHT = 3; + + static HashSet<Element> highlights; + + /** + * Highlight the {@link Widget} for the given {@link ComponentConnector}. + * <p> + * Pass the returned {@link Element} to {@link #hide(Element)} to remove + * this particular highlight. + * </p> + * + * @param connector + * ComponentConnector + * @return Highlight element + */ + static Element show(ComponentConnector connector) { + return show(connector, DEFAULT_COLOR); + } + + /** + * Highlight the {@link Widget} for the given connector if it is a + * {@link ComponentConnector}. Hide any other highlight. + * <p> + * Pass the returned {@link Element} to {@link #hide(Element)} to remove + * this particular highlight. + * </p> + * + * @since 7.1 + * + * @param connector + * the server connector to highlight + * @return Highlight element, or <code>null</code> if the connector isn't a + * component + */ + static Element showOnly(ServerConnector connector) { + hideAll(); + if (connector instanceof ComponentConnector) { + return show((ComponentConnector) connector); + } else { + return null; + } + } + + /** + * Highlights the {@link Widget} for the given {@link ComponentConnector} + * using the given color. + * <p> + * Pass the returned {@link Element} to {@link #hide(Element)} to remove + * this particular highlight. + * </p> + * + * @param connector + * ComponentConnector + * @param color + * Color of highlight + * @return Highlight element + */ + static Element show(ComponentConnector connector, String color) { + if (connector != null) { + Widget w = connector.getWidget(); + return show(w, color); + } + return null; + } + + /** + * Highlights the given {@link Widget}. + * <p> + * Pass the returned {@link Element} to {@link #hide(Element)} to remove + * this particular highlight. + * </p> + * + * @param widget + * Widget to highlight + * @return Highlight element + */ + static Element show(Widget widget) { + return show(widget, DEFAULT_COLOR); + } + + /** + * Highlight the given {@link Widget} using the given color. + * <p> + * Pass the returned {@link Element} to {@link #hide(Element)} to remove + * this particular highlight. + * </p> + * + * @param widget + * Widget to highlight + * @param color + * Color of highlight + * @return Highlight element + */ + static Element show(Widget widget, String color) { + if (widget != null) { + if (highlights == null) { + highlights = new HashSet<Element>(); + } + + Element highlight = DOM.createDiv(); + Style style = highlight.getStyle(); + style.setTop(widget.getAbsoluteTop(), Unit.PX); + style.setLeft(widget.getAbsoluteLeft(), Unit.PX); + int width = widget.getOffsetWidth(); + if (width < MIN_WIDTH) { + width = MIN_WIDTH; + } + style.setWidth(width, Unit.PX); + int height = widget.getOffsetHeight(); + if (height < MIN_HEIGHT) { + height = MIN_HEIGHT; + } + style.setHeight(height, Unit.PX); + RootPanel.getBodyElement().appendChild(highlight); + + style.setPosition(Position.ABSOLUTE); + style.setZIndex(VWindow.Z_INDEX + 1000); + style.setBackgroundColor(color); + style.setOpacity(DEFAULT_OPACITY); + if (BrowserInfo.get().isIE()) { + style.setProperty("filter", "alpha(opacity=" + + (DEFAULT_OPACITY * 100) + ")"); + } + + highlights.add(highlight); + + return highlight; + } + return null; + } + + /** + * Hides the given highlight. + * + * @param highlight + * Highlight to hide + */ + static void hide(Element highlight) { + if (highlight != null && highlight.getParentElement() != null) { + highlight.getParentElement().removeChild(highlight); + highlights.remove(highlight); + } + } + + /** + * Hides all highlights + */ + static void hideAll() { + if (highlights != null) { + for (Element highlight : highlights) { + if (highlight.getParentElement() != null) { + highlight.getParentElement().removeChild(highlight); + } + } + highlights = null; + } + } + +} diff --git a/client/src/com/vaadin/client/debug/internal/Icon.java b/client/src/com/vaadin/client/debug/internal/Icon.java new file mode 100644 index 0000000000..cc2ef3b348 --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/Icon.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.debug.internal; + +public enum Icon { + + SEARCH(""), // + OK(""), // + REMOVE(""), // + CLOSE(""), // + CLEAR(""), // + RESET_TIMER(""), // + MINIMIZE(""), // + WARNING(""), // + INFO(""), // + ERROR(""), // + HIGHLIGHT(""), // + LOG(""), // + OPTIMIZE(""), // + HIERARCHY(""), // + MENU(""), // + NETWORK(""), // + ANALYZE(""), // + SCROLL_LOCK(""), // + DEVMODE_OFF(""), // + DEVMODE_SUPER(""), // + DEVMODE_ON(""), // + // BAN_CIRCLE(""), // + MAXIMIZE(""), // + RESET(""), // + PERSIST(""); // + + private String id; + + private Icon(String id) { + this.id = id; + } + + @Override + public String toString() { + return "<i data-icon=\"" + id + "\"></i>"; + } + + public String getId() { + return id; + } + +} diff --git a/client/src/com/vaadin/client/debug/internal/LogSection.java b/client/src/com/vaadin/client/debug/internal/LogSection.java new file mode 100644 index 0000000000..74ac3641c2 --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/LogSection.java @@ -0,0 +1,355 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +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.logging.client.HtmlLogFormatter; +import com.google.gwt.storage.client.Storage; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ValueMap; + +/** + * Displays the log messages. + * <p> + * Scroll lock state is persisted. + * </p> + * + * @since 7.1 + * @author Vaadin Ltd + */ +public class LogSection implements Section { + + private final class LogSectionHandler extends Handler { + private LogSectionHandler() { + setLevel(Level.ALL); + setFormatter(new HtmlLogFormatter(true) { + @Override + protected String getHtmlPrefix(LogRecord event) { + return ""; + } + + @Override + protected String getHtmlSuffix(LogRecord event) { + return ""; + } + + @Override + protected String getRecordInfo(LogRecord event, String newline) { + return ""; + } + }); + } + + @Override + public void publish(LogRecord record) { + if (!isLoggable(record)) { + return; + } + + Formatter formatter = getFormatter(); + String msg = formatter.format(record); + + addRow(record.getLevel(), msg); + } + + @Override + public void close() { + // Nothing to do + } + + @Override + public void flush() { + // Nothing todo + } + } + + // If scroll is not locked, content will be scrolled after delay + private static final int SCROLL_DELAY = 100; + private Timer scrollTimer = null; + + // TODO should be persisted + // log content limit + private int limit = 500; + + private final DebugButton tabButton = new DebugButton(Icon.LOG, + "Debug message log"); + + private final HTML content = new HTML(); + private final Element contentElement; + private final FlowPanel controls = new FlowPanel(); + + private final Button clear = new DebugButton(Icon.CLEAR, "Clear log"); + private final Button reset = new DebugButton(Icon.RESET_TIMER, + "Reset timer"); + private final Button scroll = new DebugButton(Icon.SCROLL_LOCK, + "Scroll lock"); + + public LogSection() { + contentElement = content.getElement(); + content.setStylePrimaryName(VDebugWindow.STYLENAME + "-log"); + + // clear log button + controls.add(clear); + clear.setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + clear.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + clear(); + } + }); + + // reset timer button + controls.add(reset); + reset.setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + reset.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + resetTimer(); + } + }); + + // scroll lock toggle + controls.add(scroll); + scroll.setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + scroll.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + toggleScrollLock(); + } + }); + + // select message if row is clicked + content.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + Element el = Element + .as(event.getNativeEvent().getEventTarget()); + while (!el.getClassName().contains( + VDebugWindow.STYLENAME + "-message")) { + el = el.getParentElement(); + if (el == contentElement) { + // clicked something else + return; + } + } + selectText(el); + } + }); + + // Add handler to the root logger + Logger.getLogger("").addHandler(new LogSectionHandler()); + } + + /** + * Toggles scroll lock, writes state to persistent storage. + */ + void toggleScrollLock() { + setScrollLock(scrollTimer != null); + + Storage storage = Storage.getLocalStorageIfSupported(); + if (storage == null) { + return; + } + VDebugWindow.writeState(storage, "log-scrollLock", scrollTimer == null); + } + + /** + * Activates or deactivates scroll lock + * + * @param locked + */ + void setScrollLock(boolean locked) { + if (locked && scrollTimer != null) { + scrollTimer.cancel(); + scrollTimer = null; + + } else if (!locked && scrollTimer == null) { + scrollTimer = new Timer() { + @Override + public void run() { + Element el = (Element) contentElement.getLastChild(); + if (el != null) { + el = el.getFirstChildElement(); + if (el != null) { + el.scrollIntoView(); + } + } + } + }; + + } + scroll.setStyleDependentName(VDebugWindow.STYLENAME_ACTIVE, locked); + + } + + private native void selectText(Element el) + /*-{ + if ($doc.selection && $doc.selection.createRange) { + var r = $doc.selection.createRange(); + r.moveToElementText(el); + r.select(); + } else if ($doc.createRange && $wnd.getSelection) { + var r = $doc.createRange(); + r.selectNode(el); + var selection = $wnd.getSelection(); + selection.removeAllRanges(); + selection.addRange(r); + } + }-*/; + + private void clear() { + contentElement.setInnerText(""); + } + + private void applyLimit() { + while (contentElement.getChildCount() > limit) { + contentElement.removeChild(contentElement.getFirstChild()); + } + } + + /** + * Sets the log row limit. + * + * @param limit + */ + public void setLimit(int limit) { + this.limit = limit; + applyLimit(); + + // TODO shoud be persisted + } + + /** + * Gets the current log row limit. + * + * @return + */ + public int getLimit() { + // TODO should be read from persistent storage + return limit; + } + + @Override + public DebugButton getTabButton() { + return tabButton; + } + + @Override + public Widget getControls() { + return controls; + } + + @Override + public Widget getContent() { + return content; + } + + @Override + public void show() { + Storage storage = Storage.getLocalStorageIfSupported(); + if (storage == null) { + return; + } + setScrollLock(VDebugWindow.readState(storage, "log-scrollLock", false)); + } + + @Override + public void hide() { + // remove timer + setScrollLock(true); + } + + /** + * Schedules a scoll if scroll lock is not active. + */ + private void maybeScroll() { + if (scrollTimer != null) { + scrollTimer.cancel(); + scrollTimer.schedule(SCROLL_DELAY); + } + } + + /** + * Resets the timer and inserts a log row indicating this. + */ + private void resetTimer() { + int sinceStart = VDebugWindow.getMillisSinceStart(); + int sinceReset = VDebugWindow.resetTimer(); + Element row = DOM.createDiv(); + row.addClassName(VDebugWindow.STYLENAME + "-reset"); + row.setInnerHTML(Icon.RESET_TIMER + " Timer reset"); + row.setTitle(VDebugWindow.getTimingTooltip(sinceStart, sinceReset)); + contentElement.appendChild(row); + maybeScroll(); + } + + /** + * Adds a row to the log, applies the log row limit by removing old rows if + * needed, and scrolls new row into view if scroll lock is not active. + * + * @param level + * @param msg + * @return + */ + private Element addRow(Level level, String msg) { + int sinceReset = VDebugWindow.getMillisSinceReset(); + int sinceStart = VDebugWindow.getMillisSinceStart(); + + Element row = DOM.createDiv(); + row.addClassName(VDebugWindow.STYLENAME + "-row"); + row.addClassName(level.getName()); + + String inner = "<span class='" + VDebugWindow.STYLENAME + "-" + + "'></span><span class='" + VDebugWindow.STYLENAME + + "-time' title='" + + VDebugWindow.getTimingTooltip(sinceStart, sinceReset) + "'>" + + sinceReset + "ms</span><span class='" + + VDebugWindow.STYLENAME + "-message'>" + msg + "</span>"; + row.setInnerHTML(inner); + + contentElement.appendChild(row); + applyLimit(); + + maybeScroll(); + + return row; + } + + @Override + public void meta(ApplicationConnection ac, ValueMap meta) { + addRow(Level.FINE, "Meta: " + meta.toSource()); + } + + @Override + public void uidl(ApplicationConnection ac, ValueMap uidl) { + addRow(Level.FINE, "UIDL: " + uidl.toSource()); + } + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/debug/internal/NetworkSection.java b/client/src/com/vaadin/client/debug/internal/NetworkSection.java new file mode 100644 index 0000000000..e94791ce1f --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/NetworkSection.java @@ -0,0 +1,97 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.VUIDLBrowser; +import com.vaadin.client.ValueMap; + +/** + * Displays network activity; requests and responses. + * + * Currently only displays responses in a simple manner. + * + * @since 7.1 + * @author Vaadin Ltd + */ +public class NetworkSection implements Section { + + private final int maxSize = 10; + + private final DebugButton tabButton = new DebugButton(Icon.NETWORK, + "Communication"); + + private final HorizontalPanel controls = new HorizontalPanel(); + private final FlowPanel content = new FlowPanel(); + + @Override + public DebugButton getTabButton() { + return tabButton; + } + + @Override + public Widget getControls() { + return controls; + } + + @Override + public Widget getContent() { + return content; + } + + @Override + public void show() { + // TODO Auto-generated method stub + + } + + @Override + public void hide() { + // TODO Auto-generated method stub + + } + + @Override + public void meta(ApplicationConnection ac, ValueMap meta) { + // NOP + } + + @Override + public void uidl(ApplicationConnection ac, ValueMap uidl) { + int sinceStart = VDebugWindow.getMillisSinceStart(); + int sinceReset = VDebugWindow.getMillisSinceReset(); + VUIDLBrowser vuidlBrowser = new VUIDLBrowser(uidl, ac); + vuidlBrowser.addStyleName(VDebugWindow.STYLENAME + "-row"); + // TODO style this + /*- + vuidlBrowser.setText("<span class=\"" + VDebugWindow.STYLENAME + + "-time\">" + sinceReset + "ms</span><span class=\"" + + VDebugWindow.STYLENAME + "-message\">response</span>"); + -*/ + vuidlBrowser.setText("Response @ " + sinceReset + "ms"); + vuidlBrowser.setTitle(VDebugWindow.getTimingTooltip(sinceStart, + sinceReset)); + vuidlBrowser.close(); + content.add(vuidlBrowser); + while (content.getWidgetCount() > maxSize) { + content.remove(0); + } + } + +} diff --git a/client/src/com/vaadin/client/debug/internal/Section.java b/client/src/com/vaadin/client/debug/internal/Section.java new file mode 100644 index 0000000000..c6b8af55e8 --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/Section.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ValueMap; + +/** + * A Section is displayed as a tab in the {@link VDebugWindow}. + * + * @since 7.1 + * @author Vaadin Ltd + */ +public interface Section { + + /** + * Returns a button that will be used to activate this section, displayed as + * a tab in {@link VDebugWindow}. + * <p> + * <em>The same instance <b>must</b> be returned each time this method is called.</em> + * </p> + * <p> + * The button should preferably only have an icon (no caption), and should + * have a longer description as title (tooltip). + * </p> + * + * @return section id + */ + public DebugButton getTabButton(); + + /** + * Returns a widget that is placed on top of the Section content when the + * Section (tab) is active in the {@link VDebugWindow}. + * + * @return section controls + */ + public Widget getControls(); + + /** + * Returns a widget that is the main content of the section, displayed when + * the section is active in the {@link VDebugWindow}. + * + * @return + */ + public Widget getContent(); + + /** + * Called when the section is activated in {@link VDebugWindow}. Provides an + * opportunity to e.g start timers, add listeners etc. + */ + public void show(); + + /** + * Called when the section is deactivated in {@link VDebugWindow}. Provides + * an opportunity to e.g stop timers, remove listeners etc. + */ + public void hide(); + + public void meta(ApplicationConnection ac, ValueMap meta); + + public void uidl(ApplicationConnection ac, ValueMap uidl); +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/debug/internal/VDebugWindow.java b/client/src/com/vaadin/client/debug/internal/VDebugWindow.java new file mode 100644 index 0000000000..5aab95616a --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/VDebugWindow.java @@ -0,0 +1,1062 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.debug.internal; + +import java.util.ArrayList; +import java.util.Date; + +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.Style; +import com.google.gwt.dom.client.Style.Cursor; +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.MouseDownEvent; +import com.google.gwt.event.dom.client.MouseDownHandler; +import com.google.gwt.event.dom.client.MouseEvent; +import com.google.gwt.event.dom.client.MouseMoveEvent; +import com.google.gwt.event.dom.client.MouseMoveHandler; +import com.google.gwt.event.logical.shared.ResizeEvent; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.http.client.UrlBuilder; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.storage.client.Storage; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.Window.Location; +import com.google.gwt.user.client.ui.Button; +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.client.ApplicationConnection; +import com.vaadin.client.ValueMap; +import com.vaadin.client.ui.VOverlay; + +/** + * Debug window implementation. + * + * @since 7.1 + * @author Vaadin Ltd + */ +public final class VDebugWindow extends VOverlay { + + // CSS classes + static final String STYLENAME = "v-debugwindow"; + static final String STYLENAME_BUTTON = STYLENAME + "-button"; + static final String STYLENAME_ACTIVE = "active"; + + protected static final String STYLENAME_HEAD = STYLENAME + "-head"; + protected static final String STYLENAME_TABS = STYLENAME + "-tabs"; + protected static final String STYLENAME_TAB = STYLENAME + "-tab"; + protected static final String STYLENAME_CONTROLS = STYLENAME + "-controls"; + protected static final String STYLENAME_SECTION_HEAD = STYLENAME + + "-section-head"; + protected static final String STYLENAME_CONTENT = STYLENAME + "-content"; + protected static final String STYLENAME_SELECTED = "selected"; + + // drag this far before actually moving window + protected static final int MOVE_TRESHOLD = 5; + + // window minimum sizes + protected static final int MIN_HEIGHT = 40; + protected static final int HANDLE_SIZE = 5; + + // identifiers for localStorage + private static final String STORAGE_PREFIX = "v-debug-"; + private static final String STORAGE_FULL_X = "x"; + private static final String STORAGE_FULL_Y = "y"; + private static final String STORAGE_FULL_W = "w"; + private static final String STORAGE_FULL_H = "h"; + private static final String STORAGE_MIN_X = "mx"; + private static final String STORAGE_MIN_Y = "my"; + private static final String STORAGE_ACTIVE_SECTION = "t"; + private static final String STORAGE_IS_MINIMIZED = "m"; + private static final String STORAGE_FONT_SIZE = "s"; + + // state, these are persisted + protected Section activeSection; + protected boolean minimized = false; + protected int fullX = -10; + protected int fullY = -10; + protected int fullW = 300; + protected int fullH = 150; + protected int minX = -10; + protected int minY = 10; + protected int fontSize = 1; // 0-2 + + // Timers since application start, and last timer reset + private static final Duration start = new Duration(); + private static Duration lastReset = start; + + // outer panel + protected FlowPanel window = new FlowPanel(); + // top (tabs + controls) + protected FlowPanel head = new FlowPanel(); + protected FlowPanel tabs = new FlowPanel(); + protected FlowPanel controls = new FlowPanel(); + protected Button minimize = new DebugButton(Icon.MINIMIZE, "Minimize"); + protected Button menu = new DebugButton(Icon.MENU, "Menu"); + protected Button close = new DebugButton(Icon.CLOSE, "Close"); + + // menu + protected Menu menuPopup = new Menu(); + + // section specific area + protected FlowPanel sectionHead = new FlowPanel(); + // content wrapper + protected SimplePanel content = new SimplePanel(); + + // sections + protected ArrayList<Section> sections = new ArrayList<Section>(); + + // handles resizing (mouse) + protected ResizeHandler resizeHandler = new ResizeHandler(); + protected HandlerRegistration resizeReg = null; + protected HandlerRegistration resizeReg2 = null; + + // handles window movement (mouse) + protected MoveHandler moveHandler = new MoveHandler(); + protected HandlerRegistration moveReg = null; + + // TODO this class should really be a singleton. + static VDebugWindow instance; + + /** + * This class should only be instantiated by the framework, use + * {@link #get()} instead to get the singleton instance. + * <p> + * {@link VDebugWindow} provides windowing functionality and shows + * {@link Section}s added with {@link #addSection(Section)} as tabs. + * </p> + * <p> + * {@link Section#getTabButton()} is called to obtain a unique id for the + * Sections; the id should actually be an identifier for an icon in the + * icon-font in use. + * </p> + * <p> + * {@link Section#getControls()} and {@link Section#getContent()} is called + * when the Section is activated (displayed). Additionally + * {@link Section#show()} is called to allow the Section to initialize + * itself as needed when shown. Conversely {@link Section#hide()} is called + * when the Section is deactivated. + * </p> + * <p> + * Sections should take care to prefix CSS classnames used with + * {@link VDebugWindow}.{@link #STYLENAME} to avoid that application theme + * interferes with the debug window content. + * </p> + * <p> + * Some of the window state, such as position and size, is persisted to + * localStorage. Sections can use + * {@link #writeState(Storage, String, Object)} and + * {@link #readState(Storage, String, String)} (and relatives) to write and + * read own persisted settings, keys will automatically be prefixed with + * {@value #STORAGE_PREFIX}. + * </p> + */ + public VDebugWindow() { + super(false, false); + instance = this; + getElement().getStyle().setOverflow(Overflow.HIDDEN); + setStylePrimaryName(STYLENAME); + + setWidget(window); + window.add(head); + head.add(tabs); + head.add(controls); + head.add(sectionHead); + window.add(content); + + head.setStylePrimaryName(STYLENAME_HEAD); + tabs.setStylePrimaryName(STYLENAME_TABS); + controls.setStylePrimaryName(STYLENAME_CONTROLS); + sectionHead.setStylePrimaryName(STYLENAME_SECTION_HEAD); + content.setStylePrimaryName(STYLENAME_CONTENT); + + // add controls TODO move these + controls.add(menu); + menu.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + menuPopup.showRelativeTo(menu); + } + }); + + controls.add(minimize); + minimize.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + toggleMinimized(); + writeStoredState(); + } + }); + controls.add(close); + close.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + close(); + } + }); + + Style s = content.getElement().getStyle(); + s.setOverflow(Overflow.AUTO); + + // window can be moved by dragging header + moveReg = head.addDomHandler(moveHandler, MouseDownEvent.getType()); + // resize from all sides and corners + resizeReg = content.addDomHandler(resizeHandler, + MouseDownEvent.getType()); + // changes mouse pointer when hovering sides / corners + resizeReg2 = content.addDomHandler(resizeHandler, + MouseMoveEvent.getType()); + } + + /** + * Gets the {@link #VDebugWindow()} singleton instance. + * + * @return + */ + public static VDebugWindow get() { + if (instance == null) { + instance = new VDebugWindow(); + } + return instance; + } + + /** + * Closes the window and stops visual logging. + */ + public void close() { + // TODO disable even more + if (resizeReg != null) { + resizeReg.removeHandler(); + resizeReg2.removeHandler(); + moveReg.removeHandler(); + } + Highlight.hideAll(); + hide(); + + } + + boolean isClosed() { + return !isShowing(); + } + + /** + * Reads the stored state from localStorage. + */ + private void readStoredState() { + Storage storage = Storage.getLocalStorageIfSupported(); + if (storage == null) { + return; + } + + fullX = readState(storage, STORAGE_FULL_X, -510); + fullY = readState(storage, STORAGE_FULL_Y, -230); + fullW = readState(storage, STORAGE_FULL_W, 500); + fullH = readState(storage, STORAGE_FULL_H, 150); + minX = readState(storage, STORAGE_MIN_X, -40); + minY = readState(storage, STORAGE_MIN_Y, -70); + setFontSize(readState(storage, STORAGE_FONT_SIZE, 1)); + + activateSection(readState(storage, STORAGE_ACTIVE_SECTION, 0)); + + setMinimized(readState(storage, STORAGE_IS_MINIMIZED, false)); + + applyPositionAndSize(); + } + + /** + * Writes the persistent state to localStorage. + */ + private void writeStoredState() { + if (isClosed()) { + return; + } + Storage storage = Storage.getLocalStorageIfSupported(); + if (storage == null) { + return; + } + + writeState(storage, STORAGE_FULL_X, fullX); + writeState(storage, STORAGE_FULL_Y, fullY); + writeState(storage, STORAGE_FULL_W, fullW); + writeState(storage, STORAGE_FULL_H, fullH); + writeState(storage, STORAGE_MIN_X, minX); + writeState(storage, STORAGE_MIN_Y, minY); + writeState(storage, STORAGE_FONT_SIZE, fontSize); + + if (activeSection != null) { + writeState(storage, STORAGE_ACTIVE_SECTION, + activeSection.getTabButton()); + } + + writeState(storage, STORAGE_IS_MINIMIZED, minimized); + } + + /** + * Writes the given value to the given {@link Storage} using the given key + * (automatically prefixed with {@value #STORAGE_PREFIX}). + * + * @param storage + * @param key + * @param value + */ + static void writeState(Storage storage, String key, Object value) { + storage.setItem(STORAGE_PREFIX + key, String.valueOf(value)); + } + + /** + * Returns the item with the given key (automatically prefixed with + * {@value #STORAGE_PREFIX}) as an int from the given {@link Storage}, + * returning the given default value instead if not successful (e.g missing + * item). + * + * @param storage + * @param key + * @param def + * @return stored or default value + */ + static int readState(Storage storage, String key, int def) { + try { + return Integer.parseInt(storage.getItem(STORAGE_PREFIX + key)); + } catch (Exception e) { + return def; + } + } + + /** + * Returns the item with the given key (automatically prefixed with + * {@value #STORAGE_PREFIX}) as a boolean from the given {@link Storage}, + * returning the given default value instead if not successful (e.g missing + * item). + * + * @param storage + * @param key + * @param def + * @return stored or default value + */ + static boolean readState(Storage storage, String key, boolean def) { + try { + return Boolean.parseBoolean(storage.getItem(STORAGE_PREFIX + key)); + } catch (Exception e) { + return def; + } + } + + /** + * Returns the item with the given key (automatically prefixed with + * {@value #STORAGE_PREFIX}) as a String from the given {@link Storage}, + * returning the given default value instead if not successful (e.g missing + * item). + * + * @param storage + * @param key + * @param def + * @return stored or default value + */ + static String readState(Storage storage, String key, String def) { + String val = storage.getItem(STORAGE_PREFIX + key); + return val != null ? val : def; + } + + /** + * Resets (clears) the stored state from localStorage. + */ + private void resetStoredState() { + Storage storage = Storage.getLocalStorageIfSupported(); + if (storage == null) { + return; + } + // note: length is live + for (int i = 0; i < storage.getLength();) { + String key = storage.key(i); + if (key.startsWith(STORAGE_PREFIX)) { + removeState(storage, key.substring(STORAGE_PREFIX.length())); + } else { + i++; + } + } + } + + /** + * Removes the item with the given key (automatically prefixed with + * {@value #STORAGE_PREFIX}) from the given {@link Storage}. + * + * @param storage + * @param key + */ + private void removeState(Storage storage, String key) { + storage.removeItem(STORAGE_PREFIX + key); + } + + /** + * Applies the appropriate instance variables for width, height, x, y + * depending on if the window is minimized or not. + * + * If the value is negative, the window is positioned that amount of pixels + * from the right/bottom instead of left/top. + * + * Finally, the position is bounds-checked so that the window is not moved + * off-screen (the adjusted values are not saved). + */ + private void applyPositionAndSize() { + int x = 0; + int y = 0; + if (minimized) { + x = minX; + if (minX < 0) { + x = Window.getClientWidth() + minX; + } + y = minY; + if (minY < 0) { + y = Window.getClientHeight() + minY; + } + + } else { + x = fullX; + if (fullX < 0) { + x = Window.getClientWidth() + fullX; + } + y = fullY; + if (y < 0) { + y = Window.getClientHeight() + fullY; + } + content.setWidth(fullW + "px"); + content.setHeight(fullH + "px"); + } + + // bounds check + if (x < 0) { + x = 0; + } + if (x > Window.getClientWidth() - getOffsetWidth()) { + // not allowed off-screen to the right + x = Window.getClientWidth() - getOffsetWidth(); + } + if (y > Window.getClientHeight() - getOffsetHeight()) { + y = Window.getClientHeight() - getOffsetHeight(); + } + if (y < 0) { + y = 0; + } + + setPopupPosition(x, y); + } + + /** + * Reads position and size from the DOM to local variables (which in turn + * can be stored to localStorage) + */ + private void readPositionAndSize() { + int x = getPopupLeft(); + int fromRight = Window.getClientWidth() - x - getOffsetWidth(); + if (fromRight < x) { + x -= Window.getClientWidth(); + } + + int y = getPopupTop(); + int fromBottom = Window.getClientHeight() - y - getOffsetHeight(); + if (fromBottom < y) { + y -= Window.getClientHeight(); + } + + if (minimized) { + minY = y; + minX = x; + } else { + fullY = y; + fullX = x; + fullW = content.getOffsetWidth(); + fullH = content.getOffsetHeight(); + } + + } + + /** + * Adds the given {@link Section} as a tab in the {@link VDebugWindow} UI. + * {@link Section#getTabButton()} is called to obtain a button which is used + * tab. + * + * @param section + */ + public void addSection(final Section section) { + Button b = section.getTabButton(); + b.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + activateSection(section); + writeStoredState(); + } + }); + b.setStylePrimaryName(STYLENAME_TAB); + tabs.add(b); + sections.add(section); + + if (activeSection == null) { + activateSection(section); + } + } + + /** + * Activates the given {@link Section} + * + * @param section + */ + void activateSection(Section section) { + if (section != null && section != activeSection) { + Highlight.hideAll(); + // remove old stuff + if (activeSection != null) { + activeSection.hide(); + content.remove(activeSection.getContent()); + sectionHead.remove(activeSection.getControls()); + } + // update tab styles + for (int i = 0; i < tabs.getWidgetCount(); i++) { + Widget tab = tabs.getWidget(i); + tab.setStyleDependentName(STYLENAME_SELECTED, + tab == section.getTabButton()); + } + // add new stuff + content.add(section.getContent()); + sectionHead.add(section.getControls()); + activeSection = section; + activeSection.show(); + } + } + + void activateSection(int n) { + if (n < sections.size()) { + activateSection(sections.get(n)); + } + } + + /** + * Toggles the window between minimized and full states. + */ + private void toggleMinimized() { + setMinimized(!minimized); + writeStoredState(); + } + + /** + * Sets whether or not the window is minimized. + * + * @param minimized + */ + private void setMinimized(boolean minimized) { + this.minimized = minimized; + + tabs.setVisible(!minimized); + content.setVisible(!minimized); + sectionHead.setVisible(!minimized); + menu.setVisible(!minimized); + + applyPositionAndSize(); + } + + /** + * Sets the font size in use. + * + * @param size + */ + private void setFontSize(int size) { + removeStyleDependentName("size" + fontSize); + fontSize = size; + addStyleDependentName("size" + size); + } + + /** + * Gets the font size currently in use. + * + * @return + */ + private int getFontSize() { + return fontSize; + } + + /** + * Gets the milliseconds since application start. + * + * @return + */ + static int getMillisSinceStart() { + return start.elapsedMillis(); + } + + /** + * Gets the milliseconds since last {@link #resetTimer()} call. + * + * @return + */ + static int getMillisSinceReset() { + return lastReset.elapsedMillis(); + } + + /** + * Resets the timer. + * + * @return Milliseconds elapsed since the timer was last reset. + */ + static int resetTimer() { + int sinceLast = lastReset.elapsedMillis(); + lastReset = new Duration(); + return sinceLast; + } + + /** + * Gets a nicely formatted string with timing information suitable for + * display in tooltips. + * + * @param sinceStart + * @param sinceReset + * @return + */ + static String getTimingTooltip(int sinceStart, int sinceReset) { + String title = formatDuration(sinceStart) + " since start"; + title += ", " + formatDuration(sinceReset) + " since timer reset"; + title += " @ " + + DateTimeFormat.getFormat("HH:mm:ss.SSS").format(new Date()); + return title; + } + + /** + * Formats the given milliseconds as hours, minutes, seconds and + * milliseconds. + * + * @param ms + * @return + */ + static String formatDuration(int ms) { + NumberFormat fmt = NumberFormat.getFormat("00"); + String seconds = fmt.format((ms / 1000) % 60); + String minutes = fmt.format((ms / (1000 * 60)) % 60); + String hours = fmt.format((ms / (1000 * 60 * 60)) % 24); + + String millis = NumberFormat.getFormat("000").format(ms % 1000); + + return hours + "h " + minutes + "m " + seconds + "s " + millis + "ms"; + } + + /** + * Called when the window is initialized. + */ + public void init() { + + show(); + readStoredState(); + + Window.addResizeHandler(new com.google.gwt.event.logical.shared.ResizeHandler() { + + Timer t = new Timer() { + @Override + public void run() { + applyPositionAndSize(); + } + }; + + @Override + public void onResize(ResizeEvent event) { + t.cancel(); + // TODO less + t.schedule(1000); + } + }); + } + + /** + * Called when the result from analyzeLayouts is received. + * + * @param ac + * @param meta + */ + public void meta(ApplicationConnection ac, ValueMap meta) { + if (isClosed()) { + return; + } + for (Section s : sections) { + s.meta(ac, meta); + } + } + + /** + * Called when a response is received + * + * @param ac + * @param uidl + */ + public void uidl(ApplicationConnection ac, ValueMap uidl) { + if (isClosed()) { + return; + } + for (Section s : sections) { + s.uidl(ac, uidl); + } + } + + /* + * Inner classes + */ + + /** + * Popup menu for {@link VDebugWindow}. + * + * @since 7.1 + * @author Vaadin Ltd + */ + protected class Menu extends VOverlay { + FlowPanel content = new FlowPanel(); + + DebugButton[] sizes = new DebugButton[] { + new DebugButton(null, "Small", "A"), + new DebugButton(null, "Medium", "A"), + new DebugButton(null, "Large", "A") }; + + DebugButton[] modes = new DebugButton[] { + new DebugButton(Icon.DEVMODE_OFF, + "Debug only (causes page reload)"), + new DebugButton(Icon.DEVMODE_ON, "DevMode (causes page reload)"), + new DebugButton(Icon.DEVMODE_SUPER, + "SuperDevMode (causes page reload)") }; + + Menu() { + super(true, true); + setWidget(content); + + setStylePrimaryName(STYLENAME + "-menu"); + content.setStylePrimaryName(STYLENAME + "-menu-content"); + + FlowPanel size = new FlowPanel(); + content.add(size); + + final ClickHandler sizeHandler = new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + for (int i = 0; i < sizes.length; i++) { + Button b = sizes[i]; + if (b == event.getSource()) { + setSize(i); + } + } + hide(); + } + }; + for (int i = 0; i < sizes.length; i++) { + Button b = sizes[i]; + b.setStyleDependentName("size" + i, true); + b.addClickHandler(sizeHandler); + size.add(b); + } + + FlowPanel mode = new FlowPanel(); + content.add(mode); + final ClickHandler modeHandler = new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + for (int i = 0; i < modes.length; i++) { + Button b = modes[i]; + if (b == event.getSource()) { + setDevMode(i); + } + } + hide(); + } + }; + modes[getDevMode()].setActive(true); + for (int i = 0; i < modes.length; i++) { + Button b = modes[i]; + b.addClickHandler(modeHandler); + mode.add(b); + } + + Button reset = new DebugButton(Icon.RESET, "Restore defaults.", + " Reset"); + reset.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + resetStoredState(); + readStoredState(); + hide(); + } + }); + content.add(reset); + } + + private void setSize(int size) { + for (int i = 0; i < sizes.length; i++) { + Button b = sizes[i]; + b.setStyleDependentName(STYLENAME_ACTIVE, i == size); + } + setFontSize(size); + writeStoredState(); + } + + @Override + public void show() { + super.show(); + setSize(getFontSize()); + } + + private int getDevMode() { + if (Location.getParameter("superdevmode") != null) { + return 2; + } else if (Location.getParameter("gwt.codesvr") != null) { + return 1; + } else { + return 0; + } + } + + private void setDevMode(int mode) { + UrlBuilder u = Location.createUrlBuilder(); + switch (mode) { + case 2: + u.setParameter("superdevmode", ""); + u.removeParameter("gwt.codesvr"); + break; + case 1: + u.setParameter("gwt.codesvr", "localhost:9997"); + u.removeParameter("superdevmode"); + break; + default: + u.removeParameter("gwt.codesvr"); + u.removeParameter("superdevmode"); + } + Location.assign(u.buildString()); + } + + } + + /** + * Handler for moving window. + * + * @since 7.1 + * @author Vaadin Ltd + */ + protected class MoveHandler implements MouseDownHandler, + NativePreviewHandler { + + HandlerRegistration handler; + int startX; + int startY; + int startTop; + int startLeft; + + // moving stopped, remove handler on next event + boolean stop; + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + if (event.getTypeInt() == Event.ONMOUSEMOVE && !stop + && hasMoved(event.getNativeEvent())) { + int dx = event.getNativeEvent().getClientX() - startX; + int dy = event.getNativeEvent().getClientY() - startY; + + setPopupPosition(startLeft + dx, startTop + dy); + event.cancel(); + + } else if (event.getTypeInt() == Event.ONMOUSEUP) { + stop = true; + if (hasMoved(event.getNativeEvent())) { + event.cancel(); + } + + } else if (event.getTypeInt() == Event.ONCLICK) { + stop = true; + if (hasMoved(event.getNativeEvent())) { + event.cancel(); + } + + } else if (stop) { + stop = false; + handler.removeHandler(); + handler = null; + + readPositionAndSize(); + writeStoredState(); + } + } + + private boolean hasMoved(NativeEvent event) { + return Math.abs(startX - event.getClientX()) > MOVE_TRESHOLD + || Math.abs(startY - event.getClientY()) > MOVE_TRESHOLD; + } + + @Override + public void onMouseDown(MouseDownEvent event) { + if (handler == null) { + handler = Event.addNativePreviewHandler(MoveHandler.this); + } + startX = event.getClientX(); + startY = event.getClientY(); + startLeft = getPopupLeft(); + startTop = getPopupTop(); + stop = false; + event.preventDefault(); + } + + } + + /** + * Handler for resizing window. + * + * @since 7.1 + * @author Vaadin Ltd + */ + protected class ResizeHandler implements MouseDownHandler, + MouseMoveHandler, NativePreviewHandler { + + boolean resizeLeft; + boolean resizeRight; + boolean resizeUp; + boolean resizeDown; + + boolean sizing; + + HandlerRegistration dragHandler; + + int startX; + int startY; + int startW; + int startH; + int startTop; + int startLeft; + + @Override + public void onMouseDown(MouseDownEvent event) { + sizing = updateResizeFlags(event); + + if (sizing) { + startX = event.getClientX(); + startY = event.getClientY(); + + startW = content.getOffsetWidth(); + startH = content.getOffsetHeight(); + + startTop = getPopupTop(); + startLeft = getPopupLeft(); + + dragHandler = Event.addNativePreviewHandler(this); + + event.preventDefault(); + } + + } + + @Override + public void onMouseMove(MouseMoveEvent event) { + updateResizeFlags(event); + updateCursor(); + } + + private void updateCursor() { + Element c = content.getElement(); + if (resizeLeft) { + if (resizeUp) { + c.getStyle().setCursor(Cursor.NW_RESIZE); + } else if (resizeDown) { + c.getStyle().setCursor(Cursor.SW_RESIZE); + } else { + c.getStyle().setCursor(Cursor.W_RESIZE); + } + } else if (resizeRight) { + if (resizeUp) { + c.getStyle().setCursor(Cursor.NE_RESIZE); + } else if (resizeDown) { + c.getStyle().setCursor(Cursor.SE_RESIZE); + } else { + c.getStyle().setCursor(Cursor.E_RESIZE); + } + } else if (resizeUp) { + c.getStyle().setCursor(Cursor.N_RESIZE); + } else if (resizeDown) { + c.getStyle().setCursor(Cursor.S_RESIZE); + } else { + c.getStyle().setCursor(Cursor.AUTO); + } + } + + private boolean updateResizeFlags(MouseEvent event) { + Element c = getElement(); + int w = c.getOffsetWidth(); + int h = c.getOffsetHeight() - head.getOffsetHeight(); + int x = event.getRelativeX(c); + int y = event.getRelativeY(c) - head.getOffsetHeight(); + + resizeLeft = x < HANDLE_SIZE; + resizeRight = x > (w - HANDLE_SIZE); + resizeUp = y < HANDLE_SIZE; + resizeDown = y > (h - HANDLE_SIZE); + + return resizeLeft || resizeRight || resizeUp || resizeDown; + + } + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + if (event.getTypeInt() == Event.ONMOUSEMOVE) { + + int dx = event.getNativeEvent().getClientX() - startX; + int dy = event.getNativeEvent().getClientY() - startY; + + int minw = tabs.getOffsetWidth() + controls.getOffsetWidth(); + if (resizeLeft) { + int w = startW - dx; + if (w >= minw) { + content.setWidth(w + "px"); + setPopupPosition(startLeft + dx, getPopupTop()); + } + } else if (resizeRight) { + int w = startW + dx; + if (w >= minw) { + content.setWidth(w + "px"); + } + } + if (resizeUp) { + int h = startH - dy; + if (h >= MIN_HEIGHT) { + content.setHeight(h + "px"); + setPopupPosition(getPopupLeft(), startTop + dy); + } + } else if (resizeDown) { + int h = startH + dy; + if (h >= MIN_HEIGHT) { + content.setHeight(h + "px"); + } + } + + } else if (event.getTypeInt() == Event.ONMOUSEUP) { + dragHandler.removeHandler(); + dragHandler = null; + content.getElement().getStyle().setCursor(Cursor.AUTO); + sizing = false; + readPositionAndSize(); + writeStoredState(); + } + + event.cancel(); + } + + } + +} diff --git a/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java b/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java index ce79b4c64c..8e6ad25407 100644 --- a/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java +++ b/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java @@ -23,8 +23,8 @@ import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; import com.google.gwt.json.client.JSONArray; import com.vaadin.client.ServerConnector; -import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.communication.JavaScriptMethodInvocation; +import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.extensions.AbstractExtensionConnector; import com.vaadin.shared.extension.javascriptmanager.ExecuteJavaScriptRpc; import com.vaadin.shared.extension.javascriptmanager.JavaScriptManagerState; diff --git a/client/src/com/vaadin/client/metadata/Property.java b/client/src/com/vaadin/client/metadata/Property.java index c0c375c14c..2e0ea49c88 100644 --- a/client/src/com/vaadin/client/metadata/Property.java +++ b/client/src/com/vaadin/client/metadata/Property.java @@ -88,4 +88,29 @@ public class Property { return getSignature(); } + /** + * Gets the property name formatted for displaying in a user interface. This + * returns a string where e.g. "camelCase" has been converted to + * "Camel case". + * + * @return the name of this property, formatted for humans to read + */ + public String getDisplayName() { + String camelCase = getName(); + StringBuilder b = new StringBuilder(camelCase.length()); + for (int i = 0; i < camelCase.length(); i++) { + char charAt = camelCase.charAt(i); + if (i == 0) { + // First char always upper case + b.append(Character.toUpperCase(charAt)); + } else if (Character.isUpperCase(charAt)) { + b.append(' '); + b.append(Character.toLowerCase(charAt)); + } else { + b.append(charAt); + } + } + return b.toString(); + } + } diff --git a/client/src/com/vaadin/client/metadata/TypeDataStore.java b/client/src/com/vaadin/client/metadata/TypeDataStore.java index dff02749f8..aa37d75dc8 100644 --- a/client/src/com/vaadin/client/metadata/TypeDataStore.java +++ b/client/src/com/vaadin/client/metadata/TypeDataStore.java @@ -41,7 +41,6 @@ public class TypeDataStore { private final FastStringSet delayedMethods = FastStringSet.create(); private final FastStringSet lastOnlyMethods = FastStringSet.create(); - private final FastStringSet hasGetTooltipInfo = FastStringSet.create(); private final FastStringMap<Type> returnTypes = FastStringMap.create(); private final FastStringMap<Invoker> invokers = FastStringMap.create(); @@ -291,22 +290,4 @@ public class TypeDataStore { public static boolean hasProperties(Type type) { return get().properties.containsKey(type.getSignature()); } - - /** - * @deprecated As of 7.0.1. This is just a hack to avoid breaking backwards - * compatibility and will be removed in Vaadin 7.1 - */ - @Deprecated - public void setHasGetTooltipInfo(Class<?> clazz) { - hasGetTooltipInfo.add(getType(clazz).getSignature()); - } - - /** - * @deprecated As of 7.0.1. This is just a hack to avoid breaking backwards - * compatibility and will be removed in Vaadin 7.1 - */ - @Deprecated - public static boolean getHasGetTooltipInfo(Class clazz) { - return get().hasGetTooltipInfo.contains(getType(clazz).getSignature()); - } } diff --git a/client/src/com/vaadin/client/ui/AbstractClickEventHandler.java b/client/src/com/vaadin/client/ui/AbstractClickEventHandler.java index 2f97d30ece..e91abe9663 100644 --- a/client/src/com/vaadin/client/ui/AbstractClickEventHandler.java +++ b/client/src/com/vaadin/client/ui/AbstractClickEventHandler.java @@ -78,9 +78,8 @@ public abstract class AbstractClickEventHandler implements MouseDownHandler, && elementUnderMouse == lastMouseDownTarget) { mouseUpPreviewMatched = true; } else { - VConsole.log("Ignoring mouseup from " - + elementUnderMouse + " when mousedown was on " - + lastMouseDownTarget); + VConsole.log("Ignoring mouseup from " + elementUnderMouse + + " when mousedown was on " + lastMouseDownTarget); } } } diff --git a/client/src/com/vaadin/client/ui/AbstractComponentConnector.java b/client/src/com/vaadin/client/ui/AbstractComponentConnector.java index ecd6abae08..13d1e6d56c 100644 --- a/client/src/com/vaadin/client/ui/AbstractComponentConnector.java +++ b/client/src/com/vaadin/client/ui/AbstractComponentConnector.java @@ -35,7 +35,6 @@ import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.metadata.NoDataException; import com.vaadin.client.metadata.Type; import com.vaadin.client.metadata.TypeData; -import com.vaadin.client.metadata.TypeDataStore; import com.vaadin.client.ui.datefield.PopupDateFieldConnector; import com.vaadin.client.ui.ui.UIConnector; import com.vaadin.shared.AbstractComponentState; @@ -205,11 +204,13 @@ public abstract class AbstractComponentConnector extends AbstractConnector } } - private void updateComponentSize() { - Profiler.enter("AbstractComponentConnector.updateComponentSize"); + protected void updateComponentSize() { + updateComponentSize(getState().width == null ? "" : getState().width, + getState().height == null ? "" : getState().height); + } - String newWidth = getState().width == null ? "" : getState().width; - String newHeight = getState().height == null ? "" : getState().height; + protected void updateComponentSize(String newWidth, String newHeight) { + Profiler.enter("AbstractComponentConnector.updateComponentSize"); // Parent should be updated if either dimension changed between relative // and non-relative @@ -428,40 +429,13 @@ public abstract class AbstractComponentConnector extends AbstractConnector } } - /** - * {@inheritDoc} - * - * <p> - * When overriding this method, {@link #hasTooltip()} should also be - * overridden to return true in all situations where this method might - * return a non-empty result. - * </p> - * - * @see ComponentConnector#getTooltipInfo(Element) - */ @Override public TooltipInfo getTooltipInfo(Element element) { return new TooltipInfo(getState().description, getState().errorMessage); } - /** - * Check whether there might be a tooltip for this component. The framework - * will only add event listeners for automatically handling tooltips (using - * {@link #getTooltipInfo(Element)}) if this method returns true. - * - * @return <code>true</code> if some part of the component might have a - * tooltip, otherwise <code>false</code> - */ - private boolean hasTooltip() { - /* - * Hack to avoid breaking backwards compatibility - use a generator to - * know whether there's a custom implementation of getTooltipInfo, and - * in that case always assume that there might be tooltip. - */ - if (TypeDataStore.getHasGetTooltipInfo(getClass())) { - return true; - } - + @Override + public boolean hasTooltip() { // Normally, there is a tooltip if description or errorMessage is set AbstractComponentState state = getState(); if (state.description != null && !state.description.equals("")) { diff --git a/client/src/com/vaadin/client/ui/AbstractConnector.java b/client/src/com/vaadin/client/ui/AbstractConnector.java index 2c76aa93fe..6855c5cd2d 100644 --- a/client/src/com/vaadin/client/ui/AbstractConnector.java +++ b/client/src/com/vaadin/client/ui/AbstractConnector.java @@ -439,6 +439,7 @@ public abstract class AbstractConnector implements ServerConnector, * * @see com.vaadin.client.ServerConnector#hasEventListener(java.lang.String) */ + @Override public boolean hasEventListener(String eventIdentifier) { Set<String> reg = getState().registeredEventListeners; return (reg != null && reg.contains(eventIdentifier)); diff --git a/client/src/com/vaadin/client/ui/AbstractHasComponentsConnector.java b/client/src/com/vaadin/client/ui/AbstractHasComponentsConnector.java index 4a6aefd082..d833f076e4 100644 --- a/client/src/com/vaadin/client/ui/AbstractHasComponentsConnector.java +++ b/client/src/com/vaadin/client/ui/AbstractHasComponentsConnector.java @@ -20,9 +20,9 @@ import java.util.List; import com.google.gwt.event.shared.HandlerRegistration; import com.vaadin.client.ComponentConnector; -import com.vaadin.client.HasComponentsConnector; import com.vaadin.client.ConnectorHierarchyChangeEvent; import com.vaadin.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler; +import com.vaadin.client.HasComponentsConnector; public abstract class AbstractHasComponentsConnector extends AbstractComponentConnector implements HasComponentsConnector, diff --git a/client/src/com/vaadin/client/ui/VAbsoluteLayout.java b/client/src/com/vaadin/client/ui/VAbsoluteLayout.java index 88fbae6e88..dc080bcf7d 100644 --- a/client/src/com/vaadin/client/ui/VAbsoluteLayout.java +++ b/client/src/com/vaadin/client/ui/VAbsoluteLayout.java @@ -18,7 +18,6 @@ package com.vaadin.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.Unit; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.ui.ComplexPanel; @@ -304,108 +303,43 @@ public class VAbsoluteLayout extends ComplexPanel { * is added or removed */ public void layoutVertically() { + layout(); + } + + /** + * Performs an horizontal layout. Should be called when a widget is add or + * removed + */ + public void layoutHorizontally() { + layout(); + } + + private void layout() { for (Widget widget : getChildren()) { if (widget instanceof AbsoluteWrapper) { AbsoluteWrapper wrapper = (AbsoluteWrapper) widget; - - /* - * Cleanup old wrappers which have been left empty by other - * inner layouts moving the widget from the wrapper into their - * own hierarchy. This usually happens when a call to - * setWidget(widget) is done in an inner layout which - * automatically detaches the widget from the parent, in this - * case the wrapper, and re-attaches it somewhere else. This has - * to be done in the layout phase since the order of the - * hierarchy events are not defined. - */ - if (wrapper.getWidget() == null) { - wrapper.destroy(); - super.remove(wrapper); - continue; - } - - Style wrapperStyle = wrapper.getElement().getStyle(); - Style widgetStyle = wrapper.getWidget().getElement().getStyle(); - - // Ensure previous heights do not affect the measures - wrapperStyle.clearHeight(); - - if (widgetStyle.getHeight() != null - && widgetStyle.getHeight().endsWith("%")) { - 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 = canvas.getOffsetHeight() - - wrapper.getElement().getOffsetTop(); - } - wrapperStyle.setHeight(h, Unit.PX); - } - wrapper.updateCaptionPosition(); } } } /** - * Performs an horizontal layout. Should be called when a widget is add or - * removed + * Cleanup old wrappers which have been left empty by other inner layouts + * moving the widget from the wrapper into their own hierarchy. This usually + * happens when a call to setWidget(widget) is done in an inner layout which + * automatically detaches the widget from the parent, in this case the + * wrapper, and re-attaches it somewhere else. This has to be done in the + * layout phase since the order of the hierarchy events are not defined. */ - public void layoutHorizontally() { + public void cleanupWrappers() { for (Widget widget : getChildren()) { if (widget instanceof AbsoluteWrapper) { AbsoluteWrapper wrapper = (AbsoluteWrapper) widget; - - /* - * Cleanup old wrappers which have been left empty by other - * inner layouts moving the widget from the wrapper into their - * own hierarchy. This usually happens when a call to - * setWidget(widget) is done in an inner layout which - * automatically detaches the widget from the parent, in this - * case the wrapper, and re-attaches it somewhere else. This has - * to be done in the layout phase since the order of the - * hierarchy events are not defined. - */ if (wrapper.getWidget() == null) { wrapper.destroy(); super.remove(wrapper); continue; } - - Style wrapperStyle = wrapper.getElement().getStyle(); - Style widgetStyle = wrapper.getWidget().getElement().getStyle(); - - // Ensure previous heights do not affect the measures - wrapperStyle.clearWidth(); - - if (widgetStyle.getWidth() != null - && widgetStyle.getWidth().endsWith("%")) { - 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 = canvas.getOffsetWidth() - - wrapper.getElement().getOffsetLeft(); - } - wrapperStyle.setWidth(w, Unit.PX); - } - - wrapper.updateCaptionPosition(); } } } diff --git a/client/src/com/vaadin/client/ui/VButton.java b/client/src/com/vaadin/client/ui/VButton.java index decfb7c0cc..c67a9f8747 100644 --- a/client/src/com/vaadin/client/ui/VButton.java +++ b/client/src/com/vaadin/client/ui/VButton.java @@ -16,6 +16,7 @@ package com.vaadin.client.ui; +import com.google.gwt.aria.client.Roles; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NativeEvent; @@ -25,7 +26,6 @@ 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.client.ApplicationConnection; import com.vaadin.client.BrowserInfo; @@ -103,7 +103,7 @@ public class VButton extends FocusWidget implements ClickHandler { | Event.KEYEVENTS); // Add a11y role "button" - Accessibility.setRole(getElement(), Accessibility.ROLE_BUTTON); + Roles.getButtonRole().set(getElement()); getElement().appendChild(wrapper); wrapper.appendChild(captionElement); @@ -357,14 +357,14 @@ public class VButton extends FocusWidget implements ClickHandler { this.enabled = enabled; if (!enabled) { cleanupCaptureState(); - Accessibility.removeState(getElement(), - Accessibility.STATE_PRESSED); + Roles.getButtonRole().setAriaDisabledState(getElement(), + !enabled); super.setTabIndex(-1); } else { - Accessibility.setState(getElement(), - Accessibility.STATE_PRESSED, "false"); + Roles.getButtonRole().removeAriaDisabledState(getElement()); super.setTabIndex(tabIndex); } + } } diff --git a/client/src/com/vaadin/client/ui/VCalendar.java b/client/src/com/vaadin/client/ui/VCalendar.java new file mode 100644 index 0000000000..c5c12f2d72 --- /dev/null +++ b/client/src/com/vaadin/client/ui/VCalendar.java @@ -0,0 +1,1446 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.event.dom.client.ContextMenuHandler; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.DockPanel; +import com.google.gwt.user.client.ui.ScrollPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ui.calendar.schedule.CalendarDay; +import com.vaadin.client.ui.calendar.schedule.CalendarEvent; +import com.vaadin.client.ui.calendar.schedule.DayToolbar; +import com.vaadin.client.ui.calendar.schedule.MonthGrid; +import com.vaadin.client.ui.calendar.schedule.SimpleDayCell; +import com.vaadin.client.ui.calendar.schedule.SimpleDayToolbar; +import com.vaadin.client.ui.calendar.schedule.SimpleWeekToolbar; +import com.vaadin.client.ui.calendar.schedule.WeekGrid; +import com.vaadin.client.ui.calendar.schedule.WeeklyLongEvents; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * Client side implementation for Calendar + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class VCalendar extends Composite { + + public static final String ATTR_FIRSTDAYOFWEEK = "firstDay"; + public static final String ATTR_LASTDAYOFWEEK = "lastDay"; + public static final String ATTR_FIRSTHOUROFDAY = "firstHour"; + public static final String ATTR_LASTHOUROFDAY = "lastHour"; + + // private boolean hideWeekends; + private String[] monthNames; + private String[] dayNames; + private boolean format; + private final DockPanel outer = new DockPanel(); + private int rows; + + private boolean rangeSelectAllowed = true; + private boolean rangeMoveAllowed = true; + private boolean eventResizeAllowed = true; + private boolean eventMoveAllowed = true; + + private final SimpleDayToolbar nameToolbar = new SimpleDayToolbar(); + + private final DayToolbar dayToolbar = new DayToolbar(this); + private final SimpleWeekToolbar weekToolbar; + private WeeklyLongEvents weeklyLongEvents; + private MonthGrid monthGrid; + private WeekGrid weekGrid; + private int intWidth = 0; + private int intHeight = 0; + + protected final DateTimeFormat dateformat_datetime = DateTimeFormat + .getFormat("yyyy-MM-dd HH:mm:ss"); + protected final DateTimeFormat dateformat_date = DateTimeFormat + .getFormat("yyyy-MM-dd"); + protected final DateTimeFormat time12format_date = DateTimeFormat + .getFormat("h:mm a"); + protected final DateTimeFormat time24format_date = DateTimeFormat + .getFormat("HH:mm"); + + private boolean readOnly = false; + private boolean disabled = false; + + private boolean isHeightUndefined = false; + + private boolean isWidthUndefined = false; + private int firstDay; + private int lastDay; + private int firstHour; + private int lastHour; + + /** + * Listener interface for listening to event click events + */ + public interface DateClickListener { + /** + * Triggered when a date was clicked + * + * @param date + * The date and time that was clicked + */ + void dateClick(String date); + } + + /** + * Listener interface for listening to week number click events + */ + public interface WeekClickListener { + /** + * Called when a week number was selected. + * + * @param event + * The format of the vent string is "<year>w<week>" + */ + void weekClick(String event); + } + + /** + * Listener interface for listening to forward events + */ + public interface ForwardListener { + + /** + * Called when the calendar should move one view forward + */ + void forward(); + } + + /** + * Listener interface for listening to backward events + */ + public interface BackwardListener { + + /** + * Called when the calendar should move one view backward + */ + void backward(); + } + + /** + * Listener interface for listening to selection events + */ + public interface RangeSelectListener { + + /** + * Called when a user selected a new event by highlighting an area of + * the calendar. + * + * FIXME Fix the value nonsense. + * + * @param value + * The format of the value string is + * "<year>:<start-minutes>:<end-minutes>" if called from the + * {@link SimpleWeekToolbar} and "<yyyy-MM-dd>TO<yyyy-MM-dd>" + * if called from {@link MonthGrid} + */ + void rangeSelected(String value); + } + + /** + * Listener interface for listening to click events + */ + public interface EventClickListener { + /** + * Called when an event was clicked + * + * @param event + * The event that was clicked + */ + void eventClick(CalendarEvent event); + } + + /** + * Listener interface for listening to event moved events. Occurs when a + * user drags an event to a new position + */ + public interface EventMovedListener { + /** + * Triggered when an event was dragged to a new position and the start + * and end dates was changed + * + * @param event + * The event that was moved + */ + void eventMoved(CalendarEvent event); + } + + /** + * Listener interface for when an event gets resized (its start or end date + * changes) + */ + public interface EventResizeListener { + /** + * Triggers when the time limits for the event was changed. + * + * @param event + * The event that was changed. The new time limits have been + * updated in the event before calling this method + */ + void eventResized(CalendarEvent event); + } + + /** + * Listener interface for listening to scroll events. + */ + public interface ScrollListener { + /** + * Triggered when the calendar is scrolled + * + * @param scrollPosition + * The scroll position in pixels as returned by + * {@link ScrollPanel#getScrollPosition()} + */ + void scroll(int scrollPosition); + } + + /** + * Listener interface for listening to mouse events. + */ + public interface MouseEventListener { + /** + * Triggered when a user wants an context menu + * + * @param event + * The context menu event + * + * @param widget + * The widget that the context menu should be added to + */ + void contextMenu(ContextMenuEvent event, Widget widget); + } + + /** + * Default constructor + */ + public VCalendar() { + weekToolbar = new SimpleWeekToolbar(this); + initWidget(outer); + setStylePrimaryName("v-calendar"); + blockSelect(getElement()); + } + + /** + * Hack for IE to not select text when dragging. + * + * @param e + * The element to apply the hack on + */ + private native void blockSelect(Element e) + /*-{ + e.onselectstart = function() { + return false; + } + + e.ondragstart = function() { + return false; + } + }-*/; + + private void updateEventsToWeekGrid(CalendarEvent[] events) { + List<CalendarEvent> allDayLong = new ArrayList<CalendarEvent>(); + List<CalendarEvent> belowDayLong = new ArrayList<CalendarEvent>(); + + for (CalendarEvent e : events) { + if (e.isAllDay()) { + // Event is set on one "allDay" event or more than one. + allDayLong.add(e); + + } else { + // Event is set only on one day. + belowDayLong.add(e); + } + } + + weeklyLongEvents.addEvents(allDayLong); + + for (CalendarEvent e : belowDayLong) { + weekGrid.addEvent(e); + } + } + + /** + * Adds events to the month grid + * + * @param events + * The events to add + * @param drawImmediately + * Should the grid be rendered immediately. (currently not in + * use) + * + */ + public void updateEventsToMonthGrid(Collection<CalendarEvent> events, + boolean drawImmediately) { + for (CalendarEvent e : sortEventsByDuration(events)) { + // FIXME Why is drawImmediately not used ????? + addEventToMonthGrid(e, false); + } + } + + private void addEventToMonthGrid(CalendarEvent e, boolean renderImmediately) { + Date when = e.getStart(); + Date to = e.getEnd(); + boolean eventAdded = false; + boolean inProgress = false; // Event adding has started + boolean eventMoving = false; + List<SimpleDayCell> dayCells = new ArrayList<SimpleDayCell>(); + List<SimpleDayCell> timeCells = new ArrayList<SimpleDayCell>(); + for (int row = 0; row < monthGrid.getRowCount(); row++) { + if (eventAdded) { + break; + } + for (int cell = 0; cell < monthGrid.getCellCount(row); cell++) { + SimpleDayCell sdc = (SimpleDayCell) monthGrid.getWidget(row, + cell); + if (isEventInDay(when, to, sdc.getDate()) + && isEventInDayWithTime(when, to, sdc.getDate(), + e.getEndTime(), e.isAllDay())) { + if (!eventMoving) { + eventMoving = sdc.getMoveEvent() != null; + } + long d = e.getRangeInMilliseconds(); + if ((d > 0 && d <= DateConstants.DAYINMILLIS) + && !e.isAllDay()) { + timeCells.add(sdc); + } else { + dayCells.add(sdc); + } + inProgress = true; + continue; + } else if (inProgress) { + eventAdded = true; + inProgress = false; + break; + } + } + } + + updateEventSlotIndex(e, dayCells); + updateEventSlotIndex(e, timeCells); + + for (SimpleDayCell sdc : dayCells) { + sdc.addCalendarEvent(e); + } + for (SimpleDayCell sdc : timeCells) { + sdc.addCalendarEvent(e); + } + + if (renderImmediately) { + reDrawAllMonthEvents(!eventMoving); + } + } + + /* + * We must also handle the special case when the event lasts exactly for 24 + * hours, thus spanning two days e.g. from 1.1.2001 00:00 to 2.1.2001 00:00. + * That special case still should span one day when rendered. + */ + @SuppressWarnings("deprecation") + // Date methods are not deprecated in GWT + private boolean isEventInDayWithTime(Date from, Date to, Date date, + Date endTime, boolean isAllDay) { + return (isAllDay || !(to.getDay() == date.getDay() + && from.getDay() != to.getDay() && isMidnight(endTime))); + } + + private void updateEventSlotIndex(CalendarEvent e, List<SimpleDayCell> cells) { + if (cells.isEmpty()) { + return; + } + + if (e.getSlotIndex() == -1) { + // Update slot index + int newSlot = -1; + for (SimpleDayCell sdc : cells) { + int slot = sdc.getEventCount(); + if (slot > newSlot) { + newSlot = slot; + } + } + newSlot++; + + for (int i = 0; i < newSlot; i++) { + // check for empty slot + if (isSlotEmpty(e, i, cells)) { + newSlot = i; + break; + } + } + e.setSlotIndex(newSlot); + } + } + + private void reDrawAllMonthEvents(boolean clearCells) { + for (int row = 0; row < monthGrid.getRowCount(); row++) { + for (int cell = 0; cell < monthGrid.getCellCount(row); cell++) { + SimpleDayCell sdc = (SimpleDayCell) monthGrid.getWidget(row, + cell); + sdc.reDraw(clearCells); + } + } + } + + private boolean isSlotEmpty(CalendarEvent addedEvent, int slotIndex, + List<SimpleDayCell> cells) { + for (SimpleDayCell sdc : cells) { + CalendarEvent e = sdc.getCalendarEvent(slotIndex); + if (e != null && !e.equals(addedEvent)) { + return false; + } + } + return true; + } + + /** + * Remove a month event from the view + * + * @param target + * The event to remove + * + * @param repaintImmediately + * Should we repaint after the event was removed? + */ + public void removeMonthEvent(CalendarEvent target, + boolean repaintImmediately) { + if (target != null && target.getSlotIndex() >= 0) { + // Remove event + for (int row = 0; row < monthGrid.getRowCount(); row++) { + for (int cell = 0; cell < monthGrid.getCellCount(row); cell++) { + SimpleDayCell sdc = (SimpleDayCell) monthGrid.getWidget( + row, cell); + if (sdc == null) { + return; + } + sdc.removeEvent(target, repaintImmediately); + } + } + } + } + + /** + * Updates an event in the month grid + * + * @param changedEvent + * The event that has changed + */ + public void updateEventToMonthGrid(CalendarEvent changedEvent) { + removeMonthEvent(changedEvent, true); + changedEvent.setSlotIndex(-1); + addEventToMonthGrid(changedEvent, true); + } + + /** + * Sort the event by how long they are + * + * @param events + * The events to sort + * @return An array where the events has been sorted + */ + public CalendarEvent[] sortEventsByDuration(Collection<CalendarEvent> events) { + CalendarEvent[] sorted = events + .toArray(new CalendarEvent[events.size()]); + Arrays.sort(sorted, getEventComparator()); + return sorted; + } + + /* + * Check if the given event occurs at the given date. + */ + private boolean isEventInDay(Date eventWhen, Date eventTo, Date gridDate) { + if (eventWhen.compareTo(gridDate) <= 0 + && eventTo.compareTo(gridDate) >= 0) { + + return true; + } + + return false; + } + + /** + * Re-render the week grid + * + * @param daysCount + * The amount of days to include in the week + * @param days + * The days + * @param today + * Todays date + * @param realDayNames + * The names of the dates + */ + @SuppressWarnings("deprecation") + public void updateWeekGrid(int daysCount, List<CalendarDay> days, + Date today, String[] realDayNames) { + weekGrid.setFirstHour(getFirstHourOfTheDay()); + weekGrid.setLastHour(getLastHourOfTheDay()); + weekGrid.getTimeBar().updateTimeBar(is24HFormat()); + + dayToolbar.clear(); + dayToolbar.addBackButton(); + dayToolbar.setVerticalSized(isHeightUndefined); + dayToolbar.setHorizontalSized(isWidthUndefined); + weekGrid.clearDates(); + weekGrid.setDisabled(isDisabledOrReadOnly()); + + for (CalendarDay day : days) { + String date = day.getDate(); + String localized_date_format = day.getLocalizedDateFormat(); + Date d = dateformat_date.parse(date); + int dayOfWeek = day.getDayOfWeek(); + if (dayOfWeek < getFirstDayNumber() + || dayOfWeek > getLastDayNumber()) { + continue; + } + boolean isToday = false; + int dayOfMonth = d.getDate(); + if (today.getDate() == dayOfMonth && today.getYear() == d.getYear() + && today.getMonth() == d.getMonth()) { + isToday = true; + } + dayToolbar.add(realDayNames[dayOfWeek - 1], date, + localized_date_format, isToday ? "today" : null); + weeklyLongEvents.addDate(d); + weekGrid.addDate(d); + if (isToday) { + weekGrid.setToday(d, today); + } + } + dayToolbar.addNextButton(); + } + + /** + * Updates the events in the Month view + * + * @param daysCount + * How many days there are + * @param daysUidl + * + * @param today + * Todays date + */ + @SuppressWarnings("deprecation") + public void updateMonthGrid(int daysCount, List<CalendarDay> days, + Date today) { + int columns = getLastDayNumber() - getFirstDayNumber() + 1; + rows = (int) Math.ceil(daysCount / (double) 7); + + monthGrid = new MonthGrid(this, rows, columns); + monthGrid.setEnabled(!isDisabledOrReadOnly()); + weekToolbar.removeAllRows(); + int pos = 0; + boolean monthNameDrawn = true; + boolean firstDayFound = false; + boolean lastDayFound = false; + + for (CalendarDay day : days) { + String date = day.getDate(); + Date d = dateformat_date.parse(date); + int dayOfWeek = day.getDayOfWeek(); + int week = day.getWeek(); + + int dayOfMonth = d.getDate(); + + // reset at start of each month + if (dayOfMonth == 1) { + monthNameDrawn = false; + if (firstDayFound) { + lastDayFound = true; + } + firstDayFound = true; + } + + if (dayOfWeek < getFirstDayNumber() + || dayOfWeek > getLastDayNumber()) { + continue; + } + int y = (pos / columns); + int x = pos - (y * columns); + if (x == 0 && daysCount > 7) { + // Add week to weekToolbar for navigation + weekToolbar.addWeek(week, d.getYear()); + } + final SimpleDayCell cell = new SimpleDayCell(this, y, x); + cell.setMonthGrid(monthGrid); + cell.setDate(d); + cell.addDomHandler(new ContextMenuHandler() { + @Override + public void onContextMenu(ContextMenuEvent event) { + if (mouseEventListener != null) { + event.preventDefault(); + event.stopPropagation(); + mouseEventListener.contextMenu(event, cell); + } + } + }, ContextMenuEvent.getType()); + + if (!firstDayFound) { + cell.addStyleDependentName("prev-month"); + } else if (lastDayFound) { + cell.addStyleDependentName("next-month"); + } + + if (dayOfMonth >= 1 && !monthNameDrawn) { + cell.setMonthNameVisible(true); + monthNameDrawn = true; + } + + if (today.getDate() == dayOfMonth && today.getYear() == d.getYear() + && today.getMonth() == d.getMonth()) { + cell.setToday(true); + + } + monthGrid.setWidget(y, x, cell); + pos++; + } + } + + public void setSizeForChildren(int newWidth, int newHeight) { + intWidth = newWidth; + intHeight = newHeight; + isWidthUndefined = intWidth == -1; + dayToolbar.setVerticalSized(isHeightUndefined); + dayToolbar.setHorizontalSized(isWidthUndefined); + recalculateWidths(); + recalculateHeights(); + } + + /** + * Recalculates the heights of the sub-components in the calendar + */ + protected void recalculateHeights() { + if (monthGrid != null) { + + if (intHeight == -1) { + monthGrid.addStyleDependentName("sizedheight"); + } else { + monthGrid.removeStyleDependentName("sizedheight"); + } + + monthGrid.updateCellSizes(intWidth - weekToolbar.getOffsetWidth(), + intHeight - nameToolbar.getOffsetHeight()); + weekToolbar.setHeightPX((intHeight == -1) ? intHeight : intHeight + - nameToolbar.getOffsetHeight()); + + } else if (weekGrid != null) { + weekGrid.setHeightPX((intHeight == -1) ? intHeight : intHeight + - weeklyLongEvents.getOffsetHeight() + - dayToolbar.getOffsetHeight()); + } + } + + /** + * Recalculates the widths of the sub-components in the calendar + */ + protected void recalculateWidths() { + if (!isWidthUndefined) { + nameToolbar.setWidthPX(intWidth); + dayToolbar.setWidthPX(intWidth); + + if (monthGrid != null) { + monthGrid.updateCellSizes( + intWidth - weekToolbar.getOffsetWidth(), intHeight + - nameToolbar.getOffsetHeight()); + } else if (weekGrid != null) { + weekGrid.setWidthPX(intWidth); + weeklyLongEvents.setWidthPX(weekGrid.getInternalWidth()); + } + } else { + dayToolbar.setWidthPX(intWidth); + nameToolbar.setWidthPX(intWidth); + + if (monthGrid != null) { + if (intWidth == -1) { + monthGrid.addStyleDependentName("sizedwidth"); + + } else { + monthGrid.removeStyleDependentName("sizedwidth"); + } + } else if (weekGrid != null) { + weekGrid.setWidthPX(intWidth); + weeklyLongEvents.setWidthPX(weekGrid.getInternalWidth()); + } + } + } + + /** + * Get the date format used to format dates only (excludes time) + * + * @return + */ + public DateTimeFormat getDateFormat() { + return dateformat_date; + } + + /** + * Get the time format used to format time only (excludes date) + * + * @return + */ + public DateTimeFormat getTimeFormat() { + if (is24HFormat()) { + return time24format_date; + } + return time12format_date; + } + + /** + * Get the date and time format to format the dates (includes both date and + * time) + * + * @return + */ + public DateTimeFormat getDateTimeFormat() { + return dateformat_datetime; + } + + /** + * Is the calendar either disabled or readonly + * + * @return + */ + public boolean isDisabledOrReadOnly() { + return disabled || readOnly; + } + + /** + * Is the component disabled + */ + public boolean isDisabled() { + return disabled; + } + + /** + * Is the component disabled + * + * @param disabled + * True if disabled + */ + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + /** + * Is the component read-only + */ + public boolean isReadOnly() { + return readOnly; + } + + /** + * Is the component read-only + * + * @param readOnly + * True if component is readonly + */ + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + /** + * Get the month grid component + * + * @return + */ + public MonthGrid getMonthGrid() { + return monthGrid; + } + + /** + * Get he week grid component + * + * @return + */ + public WeekGrid getWeekGrid() { + return weekGrid; + } + + /** + * Calculates correct size for all cells (size / amount of cells ) and + * distributes any overflow over all the cells. + * + * @param totalSize + * the total amount of size reserved for all cells + * @param numberOfCells + * the number of cells + * @param sizeModifier + * a modifier which is applied to all cells before distributing + * the overflow + * @return an integer array that contains the correct size for each cell + */ + public static int[] distributeSize(int totalSize, int numberOfCells, + int sizeModifier) { + int[] cellSizes = new int[numberOfCells]; + int startingSize = totalSize / numberOfCells; + int cellSizeOverFlow = totalSize % numberOfCells; + + for (int i = 0; i < numberOfCells; i++) { + cellSizes[i] = startingSize + sizeModifier; + } + + // distribute size overflow amongst all slots + int j = 0; + while (cellSizeOverFlow > 0) { + cellSizes[j]++; + cellSizeOverFlow--; + j++; + if (j >= numberOfCells) { + j = 0; + } + } + + // cellSizes[numberOfCells - 1] += cellSizeOverFlow; + + return cellSizes; + } + + /** + * Returns a comparator which can compare calendar events. + * + * @return + */ + public static Comparator<CalendarEvent> getEventComparator() { + return new Comparator<CalendarEvent>() { + + @Override + public int compare(CalendarEvent o1, CalendarEvent o2) { + if (o1.isAllDay() != o2.isAllDay()) { + if (o2.isAllDay()) { + return 1; + } + return -1; + } + + Long d1 = o1.getRangeInMilliseconds(); + Long d2 = o2.getRangeInMilliseconds(); + int r = 0; + if (!d1.equals(0L) && !d2.equals(0L)) { + r = d2.compareTo(d1); + return (r == 0) ? ((Integer) o2.getIndex()).compareTo(o1 + .getIndex()) : r; + } + + if (d2.equals(0L) && d1.equals(0L)) { + return ((Integer) o2.getIndex()).compareTo(o1.getIndex()); + } else if (d2.equals(0L) && d1 >= DateConstants.DAYINMILLIS) { + return -1; + } else if (d2.equals(0L) && d1 < DateConstants.DAYINMILLIS) { + return 1; + } else if (d1.equals(0L) && d2 >= DateConstants.DAYINMILLIS) { + return 1; + } else if (d1.equals(0L) && d2 < DateConstants.DAYINMILLIS) { + return -1; + } + r = d2.compareTo(d1); + return (r == 0) ? ((Integer) o2.getIndex()).compareTo(o1 + .getIndex()) : r; + } + }; + } + + /** + * Is the date at midnight + * + * @param date + * The date to check + * + * @return + */ + @SuppressWarnings("deprecation") + public static boolean isMidnight(Date date) { + return (date.getHours() == 0 && date.getMinutes() == 0 && date + .getSeconds() == 0); + } + + /** + * Are the dates equal (uses second resolution) + * + * @param date1 + * The first the to compare + * @param date2 + * The second date to compare + * @return + */ + @SuppressWarnings("deprecation") + public static boolean areDatesEqualToSecond(Date date1, Date date2) { + return date1.getYear() == date2.getYear() + && date1.getMonth() == date2.getMonth() + && date1.getDay() == date2.getDay() + && date1.getHours() == date2.getHours() + && date1.getSeconds() == date2.getSeconds(); + } + + /** + * Is the calendar event zero seconds long and is occurring at midnight + * + * @param event + * The event to check + * @return + */ + public static boolean isZeroLengthMidnightEvent(CalendarEvent event) { + return areDatesEqualToSecond(event.getStartTime(), event.getEndTime()) + && isMidnight(event.getEndTime()); + } + + /** + * Should the 24h time format be used + * + * @param format + * True if the 24h format should be used else the 12h format is + * used + */ + public void set24HFormat(boolean format) { + this.format = format; + } + + /** + * Is the 24h time format used + */ + public boolean is24HFormat() { + return format; + } + + /** + * Set the names of the week days + * + * @param names + * The names of the days (Monday, Thursday,...) + */ + public void setDayNames(String[] names) { + assert (names.length == 7); + dayNames = names; + } + + /** + * Get the names of the week days + */ + public String[] getDayNames() { + return dayNames; + } + + /** + * Set the names of the months + * + * @param names + * The names of the months (January, February,...) + */ + public void setMonthNames(String[] names) { + assert (names.length == 12); + monthNames = names; + } + + /** + * Get the month names + */ + public String[] getMonthNames() { + return monthNames; + } + + /** + * Set the number when a week starts + * + * @param dayNumber + * The number of the day + */ + public void setFirstDayNumber(int dayNumber) { + assert (dayNumber >= 1 && dayNumber <= 7); + firstDay = dayNumber; + } + + /** + * Get the number when a week starts + */ + public int getFirstDayNumber() { + return firstDay; + } + + /** + * Set the number when a week ends + * + * @param dayNumber + * The number of the day + */ + public void setLastDayNumber(int dayNumber) { + assert (dayNumber >= 1 && dayNumber <= 7); + lastDay = dayNumber; + } + + /** + * Get the number when a week ends + */ + public int getLastDayNumber() { + return lastDay; + } + + /** + * Set the number when a week starts + * + * @param dayNumber + * The number of the day + */ + public void setFirstHourOfTheDay(int hour) { + assert (hour >= 0 && hour <= 23); + firstHour = hour; + } + + /** + * Get the number when a week starts + */ + public int getFirstHourOfTheDay() { + return firstHour; + } + + /** + * Set the number when a week ends + * + * @param dayNumber + * The number of the day + */ + public void setLastHourOfTheDay(int hour) { + assert (hour >= 0 && hour <= 23); + lastHour = hour; + } + + /** + * Get the number when a week ends + */ + public int getLastHourOfTheDay() { + return lastHour; + } + + /** + * Re-renders the whole week view + * + * @param scroll + * The amount of pixels to scroll the week view + * @param today + * Todays date + * @param daysInMonth + * How many days are there in the month + * @param firstDayOfWeek + * The first day of the week + * @param events + * The events to render + */ + public void updateWeekView(int scroll, Date today, int daysInMonth, + int firstDayOfWeek, Collection<CalendarEvent> events, + List<CalendarDay> days) { + + while (outer.getWidgetCount() > 0) { + outer.remove(0); + } + + monthGrid = null; + String[] realDayNames = new String[getDayNames().length]; + int j = 0; + + if (firstDayOfWeek == 2) { + for (int i = 1; i < getDayNames().length; i++) { + realDayNames[j++] = getDayNames()[i]; + } + realDayNames[j] = getDayNames()[0]; + } else { + for (int i = 0; i < getDayNames().length; i++) { + realDayNames[j++] = getDayNames()[i]; + } + + } + + weeklyLongEvents = new WeeklyLongEvents(this); + if (weekGrid == null) { + weekGrid = new WeekGrid(this, is24HFormat()); + } + updateWeekGrid(daysInMonth, days, today, realDayNames); + updateEventsToWeekGrid(sortEventsByDuration(events)); + outer.add(dayToolbar, DockPanel.NORTH); + outer.add(weeklyLongEvents, DockPanel.NORTH); + outer.add(weekGrid, DockPanel.SOUTH); + weekGrid.setVerticalScrollPosition(scroll); + } + + /** + * Re-renders the whole month view + * + * @param firstDayOfWeek + * The first day of the week + * @param today + * Todays date + * @param daysInMonth + * Amount of days in the month + * @param events + * The events to render + * @param days + * The day information + */ + public void updateMonthView(int firstDayOfWeek, Date today, + int daysInMonth, Collection<CalendarEvent> events, + List<CalendarDay> days) { + + // Remove all week numbers from bar + while (outer.getWidgetCount() > 0) { + outer.remove(0); + } + + int firstDay = getFirstDayNumber(); + int lastDay = getLastDayNumber(); + int daysPerWeek = lastDay - firstDay + 1; + int j = 0; + + String[] dayNames = getDayNames(); + String[] realDayNames = new String[daysPerWeek]; + + if (firstDayOfWeek == 2) { + for (int i = firstDay; i < lastDay + 1; i++) { + if (i == 7) { + realDayNames[j++] = dayNames[0]; + } else { + realDayNames[j++] = dayNames[i]; + } + } + } else { + for (int i = firstDay - 1; i < lastDay; i++) { + realDayNames[j++] = dayNames[i]; + } + } + + nameToolbar.setDayNames(realDayNames); + + weeklyLongEvents = null; + weekGrid = null; + + updateMonthGrid(daysInMonth, days, today); + + outer.add(nameToolbar, DockPanel.NORTH); + outer.add(weekToolbar, DockPanel.WEST); + weekToolbar.updateCellHeights(); + outer.add(monthGrid, DockPanel.CENTER); + + updateEventsToMonthGrid(events, false); + } + + private DateClickListener dateClickListener; + + /** + * Sets the listener for listening to event clicks + * + * @param listener + * The listener to use + */ + public void setListener(DateClickListener listener) { + dateClickListener = listener; + } + + /** + * Gets the listener for listening to event clicks + * + * @return + */ + public DateClickListener getDateClickListener() { + return dateClickListener; + } + + private ForwardListener forwardListener; + + /** + * Set the listener which listens to forward events from the calendar + * + * @param listener + * The listener to use + */ + public void setListener(ForwardListener listener) { + forwardListener = listener; + } + + /** + * Get the listener which listens to forward events from the calendar + * + * @return + */ + public ForwardListener getForwardListener() { + return forwardListener; + } + + private BackwardListener backwardListener; + + /** + * Set the listener which listens to backward events from the calendar + * + * @param listener + * The listener to use + */ + public void setListener(BackwardListener listener) { + backwardListener = listener; + } + + /** + * Set the listener which listens to backward events from the calendar + * + * @return + */ + public BackwardListener getBackwardListener() { + return backwardListener; + } + + private WeekClickListener weekClickListener; + + /** + * Set the listener that listens to user clicking on the week numbers + * + * @param listener + * The listener to use + */ + public void setListener(WeekClickListener listener) { + weekClickListener = listener; + } + + /** + * Get the listener that listens to user clicking on the week numbers + * + * @return + */ + public WeekClickListener getWeekClickListener() { + return weekClickListener; + } + + private RangeSelectListener rangeSelectListener; + + /** + * Set the listener that listens to the user highlighting a region in the + * calendar + * + * @param listener + * The listener to use + */ + public void setListener(RangeSelectListener listener) { + rangeSelectListener = listener; + } + + /** + * Get the listener that listens to the user highlighting a region in the + * calendar + * + * @return + */ + public RangeSelectListener getRangeSelectListener() { + return rangeSelectListener; + } + + private EventClickListener eventClickListener; + + /** + * Get the listener that listens to the user clicking on the events + */ + public EventClickListener getEventClickListener() { + return eventClickListener; + } + + /** + * Set the listener that listens to the user clicking on the events + * + * @param listener + * The listener to use + */ + public void setListener(EventClickListener listener) { + eventClickListener = listener; + } + + private EventMovedListener eventMovedListener; + + /** + * Get the listener that listens to when event is dragged to a new location + * + * @return + */ + public EventMovedListener getEventMovedListener() { + return eventMovedListener; + } + + /** + * Set the listener that listens to when event is dragged to a new location + * + * @param eventMovedListener + * The listener to use + */ + public void setListener(EventMovedListener eventMovedListener) { + this.eventMovedListener = eventMovedListener; + } + + private ScrollListener scrollListener; + + /** + * Get the listener that listens to when the calendar widget is scrolled + * + * @return + */ + public ScrollListener getScrollListener() { + return scrollListener; + } + + /** + * Set the listener that listens to when the calendar widget is scrolled + * + * @param scrollListener + * The listener to use + */ + public void setListener(ScrollListener scrollListener) { + this.scrollListener = scrollListener; + } + + private EventResizeListener eventResizeListener; + + /** + * Get the listener that listens to when an events time limits are being + * adjusted + * + * @return + */ + public EventResizeListener getEventResizeListener() { + return eventResizeListener; + } + + /** + * Set the listener that listens to when an events time limits are being + * adjusted + * + * @param eventResizeListener + * The listener to use + */ + public void setListener(EventResizeListener eventResizeListener) { + this.eventResizeListener = eventResizeListener; + } + + private MouseEventListener mouseEventListener; + private boolean forwardNavigationEnabled = true; + private boolean backwardNavigationEnabled = true; + + /** + * Get the listener that listen to mouse events + * + * @return + */ + public MouseEventListener getMouseEventListener() { + return mouseEventListener; + } + + /** + * Set the listener that listen to mouse events + * + * @param mouseEventListener + * The listener to use + */ + public void setListener(MouseEventListener mouseEventListener) { + this.mouseEventListener = mouseEventListener; + } + + /** + * Is selecting a range allowed? + */ + public boolean isRangeSelectAllowed() { + return rangeSelectAllowed; + } + + /** + * Set selecting a range allowed + * + * @param rangeSelectAllowed + * Should selecting a range be allowed + */ + public void setRangeSelectAllowed(boolean rangeSelectAllowed) { + this.rangeSelectAllowed = rangeSelectAllowed; + } + + /** + * Is moving a range allowed + * + * @return + */ + public boolean isRangeMoveAllowed() { + return rangeMoveAllowed; + } + + /** + * Is moving a range allowed + * + * @param rangeMoveAllowed + * Is it allowed + */ + public void setRangeMoveAllowed(boolean rangeMoveAllowed) { + this.rangeMoveAllowed = rangeMoveAllowed; + } + + /** + * Is resizing an event allowed + */ + public boolean isEventResizeAllowed() { + return eventResizeAllowed; + } + + /** + * Is resizing an event allowed + * + * @param eventResizeAllowed + * True if allowed false if not + */ + public void setEventResizeAllowed(boolean eventResizeAllowed) { + this.eventResizeAllowed = eventResizeAllowed; + } + + /** + * Is moving an event allowed + */ + public boolean isEventMoveAllowed() { + return eventMoveAllowed; + } + + /** + * Is moving an event allowed + * + * @param eventMoveAllowed + * True if moving is allowed, false if not + */ + public void setEventMoveAllowed(boolean eventMoveAllowed) { + this.eventMoveAllowed = eventMoveAllowed; + } + + public boolean isBackwardNavigationEnabled() { + return backwardNavigationEnabled; + } + + public void setBackwardNavigationEnabled(boolean enabled) { + backwardNavigationEnabled = enabled; + } + + public boolean isForwardNavigationEnabled() { + return forwardNavigationEnabled; + } + + public void setForwardNavigationEnabled(boolean enabled) { + forwardNavigationEnabled = enabled; + } +} diff --git a/client/src/com/vaadin/client/ui/VCalendarPanel.java b/client/src/com/vaadin/client/ui/VCalendarPanel.java index e234cc911c..311932b819 100644 --- a/client/src/com/vaadin/client/ui/VCalendarPanel.java +++ b/client/src/com/vaadin/client/ui/VCalendarPanel.java @@ -19,6 +19,8 @@ package com.vaadin.client.ui; import java.util.Date; import java.util.Iterator; +import com.google.gwt.aria.client.Roles; +import com.google.gwt.aria.client.SelectedValue; import com.google.gwt.dom.client.Node; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; @@ -40,6 +42,7 @@ 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.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Button; @@ -118,6 +121,8 @@ public class VCalendarPanel extends FocusableFlexTable implements private static final String CN_OFFMONTH = "offmonth"; + private static final String CN_OUTSIDE_RANGE = "outside-range"; + /** * Represents a click handler for when a user selects a value by using the * mouse @@ -133,6 +138,9 @@ public class VCalendarPanel extends FocusableFlexTable implements @Override public void onClick(ClickEvent event) { Date newDate = ((Day) event.getSource()).getDate(); + if (!isDateInsideRange(newDate, Resolution.DAY)) { + return; + } if (newDate.getMonth() != displayedMonth.getMonth() || newDate.getYear() != displayedMonth.getYear()) { // If an off-month date was clicked, we must change the @@ -175,9 +183,9 @@ public class VCalendarPanel extends FocusableFlexTable implements private boolean showISOWeekNumbers; - private Date displayedMonth; + private FocusedDate displayedMonth; - private Date focusedDate; + private FocusedDate focusedDate; private Day selectedDay; @@ -198,8 +206,9 @@ public class VCalendarPanel extends FocusableFlexTable implements private boolean initialRenderDone = false; public VCalendarPanel() { - + getElement().setId(DOM.createUniqueId()); setStyleName(VDateField.CLASSNAME + "-calendarpanel"); + Roles.getGridRole().set(getElement()); /* * Firefox auto-repeat works correctly only if we use a key press @@ -267,6 +276,8 @@ public class VCalendarPanel extends FocusableFlexTable implements private void selectDate(Date date) { if (selectedDay != null) { selectedDay.removeStyleDependentName(CN_SELECTED); + Roles.getGridcellRole().removeAriaSelectedState( + selectedDay.getElement()); } int rowCount = days.getRowCount(); @@ -279,6 +290,8 @@ public class VCalendarPanel extends FocusableFlexTable implements if (curday.getDate().equals(date)) { curday.addStyleDependentName(CN_SELECTED); selectedDay = curday; + Roles.getGridcellRole().setAriaSelectedState( + selectedDay.getElement(), SelectedValue.TRUE); return; } } @@ -290,7 +303,7 @@ public class VCalendarPanel extends FocusableFlexTable implements * Updates year, month, day from focusedDate to value */ private void selectFocused() { - if (focusedDate != null) { + if (focusedDate != null && isDateInsideRange(focusedDate, resolution)) { if (value == null) { // No previously selected value (set to null on server side). // Create a new date using current date and time @@ -397,10 +410,13 @@ public class VCalendarPanel extends FocusableFlexTable implements 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); setWidget(0, 3, nextMonth); @@ -414,18 +430,23 @@ public class VCalendarPanel extends FocusableFlexTable implements } 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); } + updateControlButtonRangeStyles(needsMonth); + final String monthName = needsMonth ? getDateTimeService().getMonth( displayedMonth.getMonth()) : ""; final int year = displayedMonth.getYear() + 1900; @@ -446,6 +467,48 @@ public class VCalendarPanel extends FocusableFlexTable implements + "</span>"); } + private void updateControlButtonRangeStyles(boolean needsMonth) { + + if (focusedDate == null) { + return; + } + + if (needsMonth) { + Date prevMonthDate = (Date) focusedDate.clone(); + removeOneMonth(prevMonthDate); + + if (!isDateInsideRange(prevMonthDate, Resolution.MONTH)) { + prevMonth.addStyleName(CN_OUTSIDE_RANGE); + } else { + prevMonth.removeStyleName(CN_OUTSIDE_RANGE); + } + Date nextMonthDate = (Date) focusedDate.clone(); + addOneMonth(nextMonthDate); + if (!isDateInsideRange(nextMonthDate, Resolution.MONTH)) { + nextMonth.addStyleName(CN_OUTSIDE_RANGE); + } else { + nextMonth.removeStyleName(CN_OUTSIDE_RANGE); + } + } + + Date prevYearDate = (Date) focusedDate.clone(); + prevYearDate.setYear(prevYearDate.getYear() - 1); + if (!isDateInsideRange(prevYearDate, Resolution.YEAR)) { + prevYear.addStyleName(CN_OUTSIDE_RANGE); + } else { + prevYear.removeStyleName(CN_OUTSIDE_RANGE); + } + + Date nextYearDate = (Date) focusedDate.clone(); + nextYearDate.setYear(nextYearDate.getYear() + 1); + if (!isDateInsideRange(nextYearDate, Resolution.YEAR)) { + nextYear.addStyleName(CN_OUTSIDE_RANGE); + } else { + nextYear.removeStyleName(CN_OUTSIDE_RANGE); + } + + } + private DateTimeService getDateTimeService() { return dateTimeService; } @@ -470,6 +533,107 @@ public class VCalendarPanel extends FocusableFlexTable implements } /** + * Checks inclusively whether a date is inside a range of dates or not. + * + * @param date + * @return + */ + private boolean isDateInsideRange(Date date, Resolution minResolution) { + assert (date != null); + + return isAcceptedByRangeEnd(date, minResolution) + && isAcceptedByRangeStart(date, minResolution); + } + + /** + * Accepts dates greater than or equal to rangeStart, depending on the + * resolution. If the resolution is set to DAY, the range will compare on a + * day-basis. If the resolution is set to YEAR, only years are compared. So + * even if the range is set to one millisecond in next year, also next year + * will be included. + * + * @param date + * @param minResolution + * @return + */ + private boolean isAcceptedByRangeStart(Date date, Resolution minResolution) { + assert (date != null); + + // rangeStart == null means that we accept all values below rangeEnd + if (rangeStart == null) { + return true; + } + + Date valueDuplicate = (Date) date.clone(); + Date rangeStartDuplicate = (Date) rangeStart.clone(); + + if (minResolution == Resolution.YEAR) { + return valueDuplicate.getYear() >= rangeStartDuplicate.getYear(); + } + if (minResolution == Resolution.MONTH) { + valueDuplicate = clearDateBelowMonth(valueDuplicate); + rangeStartDuplicate = clearDateBelowMonth(rangeStartDuplicate); + } else { + valueDuplicate = clearDateBelowDay(valueDuplicate); + rangeStartDuplicate = clearDateBelowDay(rangeStartDuplicate); + } + + return !rangeStartDuplicate.after(valueDuplicate); + } + + /** + * Accepts dates earlier than or equal to rangeStart, depending on the + * resolution. If the resolution is set to DAY, the range will compare on a + * day-basis. If the resolution is set to YEAR, only years are compared. So + * even if the range is set to one millisecond in next year, also next year + * will be included. + * + * @param date + * @param minResolution + * @return + */ + private boolean isAcceptedByRangeEnd(Date date, Resolution minResolution) { + assert (date != null); + + // rangeEnd == null means that we accept all values above rangeStart + if (rangeEnd == null) { + return true; + } + + Date valueDuplicate = (Date) date.clone(); + Date rangeEndDuplicate = (Date) rangeEnd.clone(); + + if (minResolution == Resolution.YEAR) { + return valueDuplicate.getYear() <= rangeEndDuplicate.getYear(); + } + if (minResolution == Resolution.MONTH) { + valueDuplicate = clearDateBelowMonth(valueDuplicate); + rangeEndDuplicate = clearDateBelowMonth(rangeEndDuplicate); + } else { + valueDuplicate = clearDateBelowDay(valueDuplicate); + rangeEndDuplicate = clearDateBelowDay(rangeEndDuplicate); + } + + return !rangeEndDuplicate.before(valueDuplicate); + + } + + private static Date clearDateBelowMonth(Date date) { + date.setDate(1); + return clearDateBelowDay(date); + } + + private static Date clearDateBelowDay(Date date) { + date.setHours(0); + date.setMinutes(0); + date.setSeconds(0); + // Clearing milliseconds + long time = date.getTime() / 1000; + date = new Date(time * 1000); + return date; + } + + /** * Builds the day and time selectors of the calendar. */ private void buildCalendarBody() { @@ -528,6 +692,10 @@ public class VCalendarPanel extends FocusableFlexTable implements } else { days.setHTML(headerRow, firstWeekdayColumn + i, ""); } + + Roles.getColumnheaderRole().set( + days.getCellFormatter().getElement(headerRow, + firstWeekdayColumn + i)); } // Zero out hours, minutes, seconds, and milliseconds to compare dates @@ -551,12 +719,20 @@ public class VCalendarPanel extends FocusableFlexTable implements for (int dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) { // Actually write the day of month - Day day = new Day((Date) curr.clone()); + Date dayDate = (Date) curr.clone(); + Day day = new Day(dayDate); + day.setStyleName(parent.getStylePrimaryName() + "-calendarpanel-day"); + if (!isDateInsideRange(dayDate, Resolution.DAY)) { + day.addStyleDependentName(CN_OUTSIDE_RANGE); + } + if (curr.equals(selectedDate)) { day.addStyleDependentName(CN_SELECTED); + Roles.getGridcellRole().setAriaSelectedState( + day.getElement(), SelectedValue.TRUE); selectedDay = day; } if (curr.equals(today)) { @@ -574,10 +750,14 @@ public class VCalendarPanel extends FocusableFlexTable implements } days.setWidget(weekOfMonth, firstWeekdayColumn + dayOfWeek, day); + Roles.getGridcellRole().set( + days.getCellFormatter().getElement(weekOfMonth, + firstWeekdayColumn + dayOfWeek)); // ISO week numbers if requested days.getCellFormatter().setVisible(weekOfMonth, weekColumn, isShowISOWeekNumbers()); + if (isShowISOWeekNumbers()) { final String baseCssClass = parent.getStylePrimaryName() + "-calendarpanel-weeknumber"; @@ -615,8 +795,9 @@ public class VCalendarPanel extends FocusableFlexTable implements 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); + focusedDate = new FocusedDate(now.getYear(), now.getMonth(), + now.getDate()); + displayedMonth = new FocusedDate(now.getYear(), now.getMonth(), 1); } if (getResolution().getCalendarField() <= Resolution.MONTH @@ -653,6 +834,17 @@ public class VCalendarPanel extends FocusableFlexTable implements * Moves the focus forward the given number of days. */ private void focusNextDay(int days) { + if (focusedDate == null) { + return; + } + + Date focusCopy = ((Date) focusedDate.clone()); + focusCopy.setDate(focusedDate.getDate() + days); + if (!isDateInsideRange(focusCopy, resolution)) { + // If not inside allowed range, then do not move anything + return; + } + int oldMonth = focusedDate.getMonth(); int oldYear = focusedDate.getYear(); focusedDate.setDate(focusedDate.getDate() + days); @@ -662,6 +854,7 @@ public class VCalendarPanel extends FocusableFlexTable implements // 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()); displayedMonth.setYear(focusedDate.getYear()); @@ -681,38 +874,83 @@ public class VCalendarPanel extends FocusableFlexTable implements */ private void focusNextMonth() { - int currentMonth = focusedDate.getMonth(); - focusedDate.setMonth(currentMonth + 1); + if (focusedDate == null) { + return; + } + // Trying to request next month + Date requestedNextMonthDate = (Date) focusedDate.clone(); + addOneMonth(requestedNextMonthDate); + + if (!isDateInsideRange(requestedNextMonthDate, Resolution.MONTH)) { + return; + } + + // Now also checking whether the day is inside the range or not. If not + // inside, + // correct it + if (!isDateInsideRange(requestedNextMonthDate, Resolution.DAY)) { + requestedNextMonthDate = adjustDateToFitInsideRange(requestedNextMonthDate); + } + focusedDate.setYear(requestedNextMonthDate.getYear()); + focusedDate.setMonth(requestedNextMonthDate.getMonth()); + focusedDate.setDate(requestedNextMonthDate.getDate()); + displayedMonth.setMonth(displayedMonth.getMonth() + 1); + + renderCalendar(); + } + + private static void addOneMonth(Date date) { + int currentMonth = date.getMonth(); int requestedMonth = (currentMonth + 1) % 12; + date.setMonth(date.getMonth() + 1); + /* * 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); + while (date.getMonth() != requestedMonth) { + date.setDate(date.getDate() - 1); } - displayedMonth.setMonth(displayedMonth.getMonth() + 1); - - renderCalendar(); } - /** - * Selects the previous month - */ - private void focusPreviousMonth() { - int currentMonth = focusedDate.getMonth(); - focusedDate.setMonth(currentMonth - 1); + private static void removeOneMonth(Date date) { + int currentMonth = date.getMonth(); + + date.setMonth(date.getMonth() - 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); + while (date.getMonth() == currentMonth) { + date.setDate(date.getDate() - 1); + } + } + + /** + * Selects the previous month + */ + private void focusPreviousMonth() { + + if (focusedDate == null) { + return; } + Date requestedPreviousMonthDate = (Date) focusedDate.clone(); + removeOneMonth(requestedPreviousMonthDate); + + if (!isDateInsideRange(requestedPreviousMonthDate, Resolution.MONTH)) { + return; + } + + if (!isDateInsideRange(requestedPreviousMonthDate, Resolution.DAY)) { + requestedPreviousMonthDate = adjustDateToFitInsideRange(requestedPreviousMonthDate); + } + focusedDate.setYear(requestedPreviousMonthDate.getYear()); + focusedDate.setMonth(requestedPreviousMonthDate.getMonth()); + focusedDate.setDate(requestedPreviousMonthDate.getDate()); displayedMonth.setMonth(displayedMonth.getMonth() - 1); renderCalendar(); @@ -722,16 +960,41 @@ public class VCalendarPanel extends FocusableFlexTable implements * 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); + + if (focusedDate == null) { + return; + } + Date previousYearDate = (Date) focusedDate.clone(); + previousYearDate.setYear(previousYearDate.getYear() - years); + // Do not focus if not inside range + if (!isDateInsideRange(previousYearDate, Resolution.YEAR)) { + return; + } + // If we remove one year, but have to roll back a bit, fit it + // into the calendar. Also the months have to be changed + if (!isDateInsideRange(previousYearDate, Resolution.DAY)) { + previousYearDate = adjustDateToFitInsideRange(previousYearDate); + + focusedDate.setYear(previousYearDate.getYear()); + focusedDate.setMonth(previousYearDate.getMonth()); + focusedDate.setDate(previousYearDate.getDate()); + displayedMonth.setYear(previousYearDate.getYear()); + displayedMonth.setMonth(previousYearDate.getMonth()); + } else { + + 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(); } @@ -739,16 +1002,41 @@ public class VCalendarPanel extends FocusableFlexTable implements * 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); + + if (focusedDate == null) { + return; } + Date nextYearDate = (Date) focusedDate.clone(); + nextYearDate.setYear(nextYearDate.getYear() + years); + // Do not focus if not inside range + if (!isDateInsideRange(nextYearDate, Resolution.YEAR)) { + return; + } + // If we add one year, but have to roll back a bit, fit it + // into the calendar. Also the months have to be changed + if (!isDateInsideRange(nextYearDate, Resolution.DAY)) { + nextYearDate = adjustDateToFitInsideRange(nextYearDate); + + focusedDate.setYear(nextYearDate.getYear()); + focusedDate.setMonth(nextYearDate.getMonth()); + focusedDate.setDate(nextYearDate.getDate()); + displayedMonth.setYear(nextYearDate.getYear()); + displayedMonth.setMonth(nextYearDate.getMonth()); + } else { + + 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(); } @@ -1062,9 +1350,10 @@ public class VCalendarPanel extends FocusableFlexTable implements */ } else if (keycode == getResetKey() && !shift) { // Restore showing value the selected value - focusedDate = new Date(value.getYear(), value.getMonth(), + focusedDate = new FocusedDate(value.getYear(), value.getMonth(), value.getDate()); - displayedMonth = new Date(value.getYear(), value.getMonth(), 1); + displayedMonth = new FocusedDate(value.getYear(), value.getMonth(), + 1); renderCalendar(); return true; } @@ -1246,6 +1535,20 @@ public class VCalendarPanel extends FocusableFlexTable implements } /** + * Adjusts a date to fit inside the range, only if outside + * + * @param date + */ + private Date adjustDateToFitInsideRange(Date date) { + if (rangeStart != null && rangeStart.after(date)) { + date = (Date) rangeStart.clone(); + } else if (rangeEnd != null && rangeEnd.before(date)) { + date = (Date) rangeEnd.clone(); + } + return date; + } + + /** * Sets the data of the Panel. * * @param currentDate @@ -1257,16 +1560,47 @@ public class VCalendarPanel extends FocusableFlexTable implements if (currentDate == value && currentDate != null) { return; } + boolean currentDateWasAdjusted = false; + // Check that selected date is inside the allowed range + if (currentDate != null && !isDateInsideRange(currentDate, resolution)) { + currentDate = adjustDateToFitInsideRange(currentDate); + currentDateWasAdjusted = true; + } Date oldDisplayedMonth = displayedMonth; value = currentDate; - if (value == null) { - focusedDate = displayedMonth = null; + // If current date was adjusted, we will not select any date, + // since that will look like a date is selected. Instead we + // only focus on the adjusted value + if (value == null || currentDateWasAdjusted) { + // If ranges enabled, we may need to focus on a different view to + // potentially not get stuck + if (rangeStart != null || rangeEnd != null) { + Date dateThatFitsInsideRange = adjustDateToFitInsideRange(new Date()); + focusedDate = new FocusedDate( + dateThatFitsInsideRange.getYear(), + dateThatFitsInsideRange.getMonth(), + dateThatFitsInsideRange.getDate()); + displayedMonth = new FocusedDate( + dateThatFitsInsideRange.getYear(), + dateThatFitsInsideRange.getMonth(), 1); + // value was adjusted. Set selected to null to not cause + // confusion, but this is only needed (and allowed) when we have + // a day + // resolution + if (getResolution().getCalendarField() >= Resolution.DAY + .getCalendarField()) { + value = null; + } + } else { + focusedDate = displayedMonth = null; + } } else { - focusedDate = new Date(value.getYear(), value.getMonth(), + focusedDate = new FocusedDate(value.getYear(), value.getMonth(), value.getDate()); - displayedMonth = new Date(value.getYear(), value.getMonth(), 1); + displayedMonth = new FocusedDate(value.getYear(), value.getMonth(), + 1); } // Re-render calendar if the displayed month is changed, @@ -1704,6 +2038,10 @@ public class VCalendarPanel extends FocusableFlexTable implements private static final String SUBPART_DAY = "day"; private static final String SUBPART_MONTH_YEAR_HEADER = "header"; + private Date rangeStart; + + private Date rangeEnd; + @Override public String getSubPartName(Element subElement) { if (contains(nextMonth, subElement)) { @@ -1821,4 +2159,75 @@ public class VCalendarPanel extends FocusableFlexTable implements mouseTimer.cancel(); } } + + /** + * Helper class to inform the screen reader that the user changed the + * selected date. It sets the value of a field that is outside the view, and + * is defined as a live area. That way the screen reader recognizes the + * change and reads it to the user. + */ + public class FocusedDate extends Date { + + public FocusedDate(int year, int month, int date) { + super(year, month, date); + } + + @Override + public void setTime(long time) { + super.setTime(time); + setLabel(); + } + + @Override + @Deprecated + public void setDate(int date) { + super.setDate(date); + setLabel(); + } + + @Override + @Deprecated + public void setMonth(int month) { + super.setMonth(month); + setLabel(); + } + + @Override + @Deprecated + public void setYear(int year) { + super.setYear(year); + setLabel(); + } + + private void setLabel() { + if (parent instanceof VPopupCalendar) { + ((VPopupCalendar) parent).setFocusedDate(this); + } + } + } + + /** + * Sets the start range for this component. The start range is inclusive, + * and it depends on the current resolution, what is considered inside the + * range. + * + * @param startDate + * - the allowed range's start date + */ + public void setRangeStart(Date rangeStart) { + this.rangeStart = rangeStart; + + } + + /** + * Sets the end range for this component. The end range is inclusive, and it + * depends on the current resolution, what is considered inside the range. + * + * @param endDate + * - the allowed range's end date + */ + public void setRangeEnd(Date rangeEnd) { + this.rangeEnd = rangeEnd; + + } } diff --git a/client/src/com/vaadin/client/ui/VCheckBox.java b/client/src/com/vaadin/client/ui/VCheckBox.java index ca1e3ebcdb..bb49dd7f0a 100644 --- a/client/src/com/vaadin/client/ui/VCheckBox.java +++ b/client/src/com/vaadin/client/ui/VCheckBox.java @@ -22,9 +22,12 @@ import com.google.gwt.user.client.Event; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.Util; import com.vaadin.client.VTooltip; +import com.vaadin.client.ui.aria.AriaHelper; +import com.vaadin.client.ui.aria.HandlesAriaInvalid; +import com.vaadin.client.ui.aria.HandlesAriaRequired; public class VCheckBox extends com.google.gwt.user.client.ui.CheckBox implements - Field { + Field, HandlesAriaInvalid, HandlesAriaRequired { public static final String CLASSNAME = "v-checkbox"; @@ -69,4 +72,23 @@ public class VCheckBox extends com.google.gwt.user.client.ui.CheckBox implements } } + /** + * Gives access to the input element. + * + * @return Element of the CheckBox itself + */ + private Element getCheckBoxElement() { + // FIXME: Would love to use a better way to access the checkbox element + return (Element) getElement().getFirstChildElement(); + } + + @Override + public void setAriaRequired(boolean required) { + AriaHelper.handleInputRequired(getCheckBoxElement(), required); + } + + @Override + public void setAriaInvalid(boolean invalid) { + AriaHelper.handleInputInvalid(getCheckBoxElement(), invalid); + } } diff --git a/client/src/com/vaadin/client/ui/VColorPickerArea.java b/client/src/com/vaadin/client/ui/VColorPickerArea.java index bdae65438f..81f2c8fcc7 100644 --- a/client/src/com/vaadin/client/ui/VColorPickerArea.java +++ b/client/src/com/vaadin/client/ui/VColorPickerArea.java @@ -67,6 +67,7 @@ public class VColorPickerArea extends Widget implements ClickHandler, HasHTML, * @param handler * @return HandlerRegistration used to remove the handler */ + @Override public HandlerRegistration addClickHandler(ClickHandler handler) { return addDomHandler(handler, ClickEvent.getType()); } diff --git a/client/src/com/vaadin/client/ui/VContextMenu.java b/client/src/com/vaadin/client/ui/VContextMenu.java index 80751652df..02ee5b07c5 100644 --- a/client/src/com/vaadin/client/ui/VContextMenu.java +++ b/client/src/com/vaadin/client/ui/VContextMenu.java @@ -29,19 +29,26 @@ 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.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.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.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.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.RootPanel; import com.google.gwt.user.client.ui.impl.FocusImpl; import com.vaadin.client.Focusable; import com.vaadin.client.Util; @@ -56,6 +63,8 @@ public class VContextMenu extends VOverlay implements SubPartAware { private int top; + private Element focusedElement; + private VLazyExecutor delayedImageLoadExecutioner = new VLazyExecutor(100, new ScheduledCommand() { @Override @@ -75,6 +84,21 @@ public class VContextMenu extends VOverlay implements SubPartAware { super(true, false, true); setWidget(menu); setStyleName("v-contextmenu"); + getElement().setId(DOM.createUniqueId()); + + addCloseHandler(new CloseHandler<PopupPanel>() { + @Override + public void onClose(CloseEvent<PopupPanel> event) { + Element currentFocus = Util.getFocusedElement(); + if (focusedElement != null + && (currentFocus == null + || menu.getElement().isOrHasChild(currentFocus) || RootPanel + .getBodyElement().equals(currentFocus))) { + focusedElement.focus(); + focusedElement = null; + } + } + }); } protected void imagesLoaded() { @@ -115,6 +139,10 @@ public class VContextMenu extends VOverlay implements SubPartAware { // Attach onload listeners to all images Util.sinkOnloadForImages(menu.getElement()); + // Store the currently focused element, which will be re-focused when + // context menu is closed + focusedElement = Util.getFocusedElement(); + setPopupPositionAndShow(new PositionCallback() { @Override public void setPosition(int offsetWidth, int offsetHeight) { @@ -168,10 +196,11 @@ public class VContextMenu extends VOverlay implements SubPartAware { */ class CMenuBar extends MenuBar implements HasFocusHandlers, HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, - Focusable, LoadHandler { + Focusable, LoadHandler, KeyUpHandler { public CMenuBar() { super(true); addDomHandler(this, LoadEvent.getType()); + addDomHandler(this, KeyUpEvent.getType()); } @Override @@ -240,6 +269,13 @@ public class VContextMenu extends VOverlay implements SubPartAware { delayedImageLoadExecutioner.trigger(); } + @Override + public void onKeyUp(KeyUpEvent event) { + // Allow to close context menu with ESC + if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) { + hide(); + } + } } @Override diff --git a/client/src/com/vaadin/client/ui/VFilterSelect.java b/client/src/com/vaadin/client/ui/VFilterSelect.java index a3156221b9..e08fbf8ab6 100644 --- a/client/src/com/vaadin/client/ui/VFilterSelect.java +++ b/client/src/com/vaadin/client/ui/VFilterSelect.java @@ -24,6 +24,7 @@ import java.util.Iterator; import java.util.List; import java.util.Set; +import com.google.gwt.aria.client.Roles; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Style; @@ -66,6 +67,10 @@ import com.vaadin.client.Focusable; import com.vaadin.client.UIDL; import com.vaadin.client.Util; import com.vaadin.client.VConsole; +import com.vaadin.client.ui.aria.AriaHelper; +import com.vaadin.client.ui.aria.HandlesAriaCaption; +import com.vaadin.client.ui.aria.HandlesAriaInvalid; +import com.vaadin.client.ui.aria.HandlesAriaRequired; import com.vaadin.client.ui.menubar.MenuBar; import com.vaadin.client.ui.menubar.MenuItem; import com.vaadin.shared.AbstractComponentState; @@ -81,7 +86,8 @@ import com.vaadin.shared.ui.combobox.FilteringMode; @SuppressWarnings("deprecation") public class VFilterSelect extends Composite implements Field, KeyDownHandler, KeyUpHandler, ClickHandler, FocusHandler, BlurHandler, Focusable, - SubPartAware { + SubPartAware, HandlesAriaCaption, HandlesAriaInvalid, + HandlesAriaRequired { /** * Represents a suggestion in the suggestion popup box @@ -241,6 +247,8 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, DOM.sinkEvents(root, Event.ONMOUSEDOWN | Event.ONMOUSEWHEEL); addCloseHandler(this); + + Roles.getListRole().set(getElement()); } /** @@ -736,6 +744,7 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, while (it.hasNext()) { final FilterSelectSuggestion s = it.next(); final MenuItem mi = new MenuItem(s.getDisplayString(), true, s); + Roles.getListitemRole().set(mi.getElement()); Util.sinkOnloadForImages(mi.getElement()); @@ -1106,9 +1115,15 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, }); popupOpener.sinkEvents(Event.ONMOUSEDOWN); + Roles.getButtonRole() + .setAriaHiddenState(popupOpener.getElement(), true); + Roles.getButtonRole().set(popupOpener.getElement()); + panel.add(tb); panel.add(popupOpener); initWidget(panel); + Roles.getComboboxRole().set(panel.getElement()); + tb.addKeyDownHandler(this); tb.addKeyUpHandler(this); @@ -1226,8 +1241,11 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, // Always update styles as they might have been overwritten if (textInputEnabled) { removeStyleDependentName(STYLE_NO_INPUT); + Roles.getTextboxRole().removeAriaReadonlyProperty(tb.getElement()); } else { addStyleDependentName(STYLE_NO_INPUT); + Roles.getTextboxRole().setAriaReadonlyProperty(tb.getElement(), + true); } if (this.textInputEnabled == textInputEnabled) { @@ -1937,4 +1955,19 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, } return null; } + + @Override + public void setAriaRequired(boolean required) { + AriaHelper.handleInputRequired(tb, required); + } + + @Override + public void setAriaInvalid(boolean invalid) { + AriaHelper.handleInputInvalid(tb, invalid); + } + + @Override + public void bindAriaCaption(Element captionElement) { + AriaHelper.bindCaption(tb, captionElement); + } } diff --git a/client/src/com/vaadin/client/ui/VFormLayout.java b/client/src/com/vaadin/client/ui/VFormLayout.java index 495e842bfd..6c2661d1d8 100644 --- a/client/src/com/vaadin/client/ui/VFormLayout.java +++ b/client/src/com/vaadin/client/ui/VFormLayout.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import com.google.gwt.aria.client.Roles; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.user.client.DOM; @@ -34,6 +35,7 @@ import com.vaadin.client.ComponentConnector; import com.vaadin.client.Focusable; import com.vaadin.client.StyleConstants; import com.vaadin.client.VTooltip; +import com.vaadin.client.ui.aria.AriaHelper; import com.vaadin.shared.AbstractComponentState; import com.vaadin.shared.ComponentConstants; import com.vaadin.shared.ui.ComponentStateUtil; @@ -93,6 +95,8 @@ public class VFormLayout extends SimplePanel { public VFormLayoutTable() { DOM.setElementProperty(getElement(), "cellPadding", "0"); DOM.setElementProperty(getElement(), "cellSpacing", "0"); + + Roles.getPresentationRole().set(getElement()); } /* @@ -276,6 +280,9 @@ public class VFormLayout extends SimplePanel { if (state.caption != null) { if (captionText == null) { captionText = DOM.createSpan(); + + AriaHelper.bindCaption(owner.getWidget(), captionText); + DOM.insertChild(getElement(), captionText, icon == null ? 0 : 1); } @@ -298,6 +305,9 @@ public class VFormLayout extends SimplePanel { boolean required = owner instanceof AbstractFieldConnector && ((AbstractFieldConnector) owner).isRequired(); + + AriaHelper.handleInputRequired(owner.getWidget(), required); + if (required) { if (requiredFieldIndicator == null) { requiredFieldIndicator = DOM.createSpan(); @@ -305,6 +315,11 @@ public class VFormLayout extends SimplePanel { DOM.setElementProperty(requiredFieldIndicator, "className", "v-required-field-indicator"); DOM.appendChild(getElement(), requiredFieldIndicator); + + // Hide the required indicator from screen reader, as this + // information is set directly at the input field + Roles.getTextboxRole().setAriaHiddenState( + requiredFieldIndicator, true); } } else { if (requiredFieldIndicator != null) { @@ -364,6 +379,8 @@ public class VFormLayout extends SimplePanel { showError = false; } + AriaHelper.handleInputInvalid(owner.getWidget(), showError); + if (showError) { if (errorIndicatorElement == null) { errorIndicatorElement = DOM.createDiv(); @@ -371,6 +388,11 @@ public class VFormLayout extends SimplePanel { DOM.setElementProperty(errorIndicatorElement, "className", "v-errorindicator"); DOM.appendChild(getElement(), errorIndicatorElement); + + // Hide the error indicator from screen reader, as this + // information is set directly at the input field + Roles.getFormRole().setAriaHiddenState( + errorIndicatorElement, true); } } else if (errorIndicatorElement != null) { diff --git a/client/src/com/vaadin/client/ui/VLabel.java b/client/src/com/vaadin/client/ui/VLabel.java index 83fc8e207e..8acd653778 100644 --- a/client/src/com/vaadin/client/ui/VLabel.java +++ b/client/src/com/vaadin/client/ui/VLabel.java @@ -16,7 +16,6 @@ package com.vaadin.client.ui; -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.client.ApplicationConnection; @@ -57,10 +56,8 @@ public class VLabel extends HTML { 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(); } } diff --git a/client/src/com/vaadin/client/ui/VNativeButton.java b/client/src/com/vaadin/client/ui/VNativeButton.java index 6e1c5bae77..71413a76e6 100644 --- a/client/src/com/vaadin/client/ui/VNativeButton.java +++ b/client/src/com/vaadin/client/ui/VNativeButton.java @@ -16,14 +16,9 @@ package com.vaadin.client.ui; -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.NativeEvent; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.MouseEvent; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.Button; @@ -31,7 +26,6 @@ import com.vaadin.client.ApplicationConnection; import com.vaadin.client.BrowserInfo; import com.vaadin.client.MouseEventDetailsBuilder; import com.vaadin.client.Util; -import com.vaadin.client.VConsole; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.ui.button.ButtonServerRpc; diff --git a/client/src/com/vaadin/client/ui/VOptionGroup.java b/client/src/com/vaadin/client/ui/VOptionGroup.java index 2ba8a9e729..eed5549e39 100644 --- a/client/src/com/vaadin/client/ui/VOptionGroup.java +++ b/client/src/com/vaadin/client/ui/VOptionGroup.java @@ -22,6 +22,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import com.google.gwt.aria.client.Roles; import com.google.gwt.core.client.Scheduler; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; @@ -99,6 +100,13 @@ public class VOptionGroup extends VOptionGroupBase implements FocusHandler, public void buildOptions(UIDL uidl) { panel.clear(); optionsEnabled.clear(); + + if (isMultiselect()) { + Roles.getGroupRole().set(getElement()); + } else { + Roles.getRadiogroupRole().set(getElement()); + } + for (final Iterator<?> it = uidl.getChildIterator(); it.hasNext();) { final UIDL opUidl = (UIDL) it.next(); CheckBox op; diff --git a/client/src/com/vaadin/client/ui/VOptionGroupBase.java b/client/src/com/vaadin/client/ui/VOptionGroupBase.java index 4d60b2eba8..cc691130ad 100644 --- a/client/src/com/vaadin/client/ui/VOptionGroupBase.java +++ b/client/src/com/vaadin/client/ui/VOptionGroupBase.java @@ -118,6 +118,7 @@ public abstract class VOptionGroupBase extends Composite implements Field, return multiselect; } + @Override public boolean isEnabled() { return enabled; } @@ -190,6 +191,7 @@ public abstract class VOptionGroupBase extends Composite implements Field, } } + @Override public void setEnabled(boolean enabled) { if (this.enabled != enabled) { this.enabled = enabled; diff --git a/client/src/com/vaadin/client/ui/VPopupCalendar.java b/client/src/com/vaadin/client/ui/VPopupCalendar.java index 2a2578aa16..e431da127d 100644 --- a/client/src/com/vaadin/client/ui/VPopupCalendar.java +++ b/client/src/com/vaadin/client/ui/VPopupCalendar.java @@ -18,6 +18,9 @@ package com.vaadin.client.ui; import java.util.Date; +import com.google.gwt.aria.client.Id; +import com.google.gwt.aria.client.LiveValue; +import com.google.gwt.aria.client.Roles; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; @@ -25,18 +28,25 @@ 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.i18n.client.DateTimeFormat; 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.FlowPanel; +import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.PopupPanel.PositionCallback; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.BrowserInfo; import com.vaadin.client.VConsole; import com.vaadin.client.ui.VCalendarPanel.FocusOutListener; import com.vaadin.client.ui.VCalendarPanel.SubmitListener; +import com.vaadin.client.ui.aria.AriaHelper; +import com.vaadin.shared.ui.datefield.PopupDateFieldState; import com.vaadin.shared.ui.datefield.Resolution; /** @@ -68,6 +78,12 @@ public class VPopupCalendar extends VTextualDate implements Field, private boolean textFieldEnabled = true; + private String captionId; + + private Label selectedDate; + + private Element descriptionForAssisitveDevicesElement; + public VPopupCalendar() { super(); @@ -75,8 +91,23 @@ public class VPopupCalendar extends VTextualDate implements Field, calendarToggle.addClickHandler(this); // -2 instead of -1 to avoid FocusWidget.onAttach to reset it calendarToggle.getElement().setTabIndex(-2); + + Roles.getButtonRole().set(calendarToggle.getElement()); + Roles.getButtonRole().setAriaHiddenState(calendarToggle.getElement(), + true); + add(calendarToggle); + // Description of the usage of the widget for assisitve device users + descriptionForAssisitveDevicesElement = DOM.createDiv(); + descriptionForAssisitveDevicesElement + .setInnerText(PopupDateFieldState.DESCRIPTION_FOR_ASSISTIVE_DEVICES); + AriaHelper.ensureHasId(descriptionForAssisitveDevicesElement); + Roles.getTextboxRole().setAriaDescribedbyProperty(text.getElement(), + Id.of(descriptionForAssisitveDevicesElement)); + AriaHelper.setVisibleForAssistiveDevicesOnly( + descriptionForAssisitveDevicesElement, true); + calendar = GWT.create(VCalendarPanel.class); calendar.setParentField(this); calendar.setFocusOutListener(new FocusOutListener() { @@ -88,6 +119,14 @@ public class VPopupCalendar extends VTextualDate implements Field, } }); + // FIXME: Problem is, that the element with the provided id does not + // exist yet in html. This is the same problem as with the context menu. + // Apply here the same fix (#11795) + Roles.getTextboxRole().setAriaControlsProperty(text.getElement(), + Id.of(calendar.getElement())); + Roles.getButtonRole().setAriaControlsProperty( + calendarToggle.getElement(), Id.of(calendar.getElement())); + calendar.setSubmitListener(new SubmitListener() { @Override public void onSubmit() { @@ -109,7 +148,20 @@ public class VPopupCalendar extends VTextualDate implements Field, popup = new VOverlay(true, true, true); popup.setOwner(this); - popup.setWidget(calendar); + FlowPanel wrapper = new FlowPanel(); + selectedDate = new Label(); + selectedDate.setStyleName(getStylePrimaryName() + "-selecteddate"); + AriaHelper.setVisibleForAssistiveDevicesOnly(selectedDate.getElement(), + true); + + Roles.getTextboxRole().setAriaLiveProperty(selectedDate.getElement(), + LiveValue.ASSERTIVE); + Roles.getTextboxRole().setAriaAtomicProperty(selectedDate.getElement(), + true); + wrapper.add(selectedDate); + wrapper.add(calendar); + + popup.setWidget(wrapper); popup.addCloseHandler(this); DOM.setElementProperty(calendar.getElement(), "id", @@ -120,6 +172,19 @@ public class VPopupCalendar extends VTextualDate implements Field, updateStyleNames(); } + @Override + protected void onAttach() { + super.onAttach(); + DOM.appendChild(RootPanel.get().getElement(), + descriptionForAssisitveDevicesElement); + } + + @Override + protected void onDetach() { + super.onDetach(); + descriptionForAssisitveDevicesElement.removeFromParent(); + } + @SuppressWarnings("deprecation") public void updateValue(Date newDate) { Date currentDate = getCurrentDate(); @@ -181,8 +246,54 @@ public class VPopupCalendar extends VTextualDate implements Field, text.setEnabled(textFieldEnabled); if (textFieldEnabled) { calendarToggle.setTabIndex(-1); + Roles.getButtonRole().setAriaHiddenState( + calendarToggle.getElement(), true); } else { calendarToggle.setTabIndex(0); + Roles.getButtonRole().setAriaHiddenState( + calendarToggle.getElement(), false); + } + + handleAriaAttributes(); + } + + @Override + public void bindAriaCaption(Element captionElement) { + if (captionElement == null) { + captionId = null; + } else { + captionId = captionElement.getId(); + } + + if (isTextFieldEnabled()) { + super.bindAriaCaption(captionElement); + } else { + AriaHelper.bindCaption(calendarToggle, captionElement); + } + + handleAriaAttributes(); + } + + private void handleAriaAttributes() { + Widget removeFromWidget; + Widget setForWidget; + + if (isTextFieldEnabled()) { + setForWidget = text; + removeFromWidget = calendarToggle; + } else { + setForWidget = calendarToggle; + removeFromWidget = text; + } + + Roles.getFormRole().removeAriaLabelledbyProperty( + removeFromWidget.getElement()); + if (captionId == null) { + Roles.getFormRole().removeAriaLabelledbyProperty( + setForWidget.getElement()); + } else { + Roles.getFormRole().setAriaLabelledbyProperty( + setForWidget.getElement(), Id.of(captionId)); } } @@ -270,10 +381,6 @@ public class VPopupCalendar extends VTextualDate implements Field, } } - // fix size - popup.setWidth(w + "px"); - popup.setHeight(h + "px"); - popup.setPopupPosition(l, t + calendarToggle.getOffsetHeight() + 2); @@ -350,6 +457,32 @@ public class VPopupCalendar extends VTextualDate implements Field, calendar.setFocus(focus); } + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + + if (enabled) { + Roles.getButtonRole().setAriaDisabledState( + calendarToggle.getElement(), true); + } else { + Roles.getButtonRole().setAriaDisabledState( + calendarToggle.getElement(), false); + } + } + + /** + * Sets the content of a special field for assistive devices, so that they + * can recognize the change and inform the user (reading out in case of + * screen reader) + * + * @param selectedDate + * Date that is currently selected + */ + public void setFocusedDate(Date selectedDate) { + this.selectedDate.setText(DateTimeFormat.getFormat("dd, MMMM, yyyy") + .format(selectedDate)); + } + /** * For internal use only. May be removed or replaced in the future. * @@ -439,4 +572,50 @@ public class VPopupCalendar extends VTextualDate implements Field, return super.getSubPartName(subElement); } + /** + * Set a description that explains the usage of the Widget for users of + * assistive devices. + * + * @param descriptionForAssistiveDevices + * String with the description + */ + public void setDescriptionForAssistiveDevices( + String descriptionForAssistiveDevices) { + descriptionForAssisitveDevicesElement + .setInnerText(descriptionForAssistiveDevices); + } + + /** + * Get the description that explains the usage of the Widget for users of + * assistive devices. + * + * @return String with the description + */ + public String getDescriptionForAssistiveDevices() { + return descriptionForAssisitveDevicesElement.getInnerText(); + } + + /** + * Sets the start range for this component. The start range is inclusive, + * and it depends on the current resolution, what is considered inside the + * range. + * + * @param startDate + * - the allowed range's start date + */ + public void setRangeStart(Date rangeStart) { + calendar.setRangeStart(rangeStart); + } + + /** + * Sets the end range for this component. The end range is inclusive, and it + * depends on the current resolution, what is considered inside the range. + * + * @param endDate + * - the allowed range's end date + */ + public void setRangeEnd(Date rangeEnd) { + calendar.setRangeEnd(rangeEnd); + } + } diff --git a/client/src/com/vaadin/client/ui/VPopupView.java b/client/src/com/vaadin/client/ui/VPopupView.java index d983da2b62..05fbd2c073 100644 --- a/client/src/com/vaadin/client/ui/VPopupView.java +++ b/client/src/com/vaadin/client/ui/VPopupView.java @@ -202,7 +202,6 @@ public class VPopupView extends HTML implements Iterable<Widget> { private boolean hasHadMouseOver = false; private boolean hideOnMouseOut = true; private final Set<Element> activeChildren = new HashSet<Element>(); - private boolean hiding = false; private ShortcutActionHandler shortcutActionHandler; @@ -264,7 +263,6 @@ public class VPopupView extends HTML implements Iterable<Widget> { @Override public void hide(boolean autoClosed) { VConsole.log("Hiding popupview"); - hiding = true; syncChildren(); if (popupComponentWidget != null && popupComponentWidget != loading) { remove(popupComponentWidget); @@ -276,8 +274,6 @@ public class VPopupView extends HTML implements Iterable<Widget> { @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. @@ -353,31 +349,6 @@ public class VPopupView extends HTML implements Iterable<Widget> { 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(); diff --git a/client/src/com/vaadin/client/ui/VTextualDate.java b/client/src/com/vaadin/client/ui/VTextualDate.java index 2f444a8587..9d7e31faab 100644 --- a/client/src/com/vaadin/client/ui/VTextualDate.java +++ b/client/src/com/vaadin/client/ui/VTextualDate.java @@ -18,6 +18,7 @@ package com.vaadin.client.ui; import java.util.Date; +import com.google.gwt.aria.client.Roles; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ChangeEvent; @@ -30,11 +31,16 @@ import com.vaadin.client.Focusable; import com.vaadin.client.LocaleNotLoadedException; import com.vaadin.client.LocaleService; import com.vaadin.client.VConsole; +import com.vaadin.client.ui.aria.AriaHelper; +import com.vaadin.client.ui.aria.HandlesAriaCaption; +import com.vaadin.client.ui.aria.HandlesAriaInvalid; +import com.vaadin.client.ui.aria.HandlesAriaRequired; import com.vaadin.shared.EventId; import com.vaadin.shared.ui.datefield.Resolution; public class VTextualDate extends VDateField implements Field, ChangeHandler, - Focusable, SubPartAware { + Focusable, SubPartAware, HandlesAriaCaption, HandlesAriaInvalid, + HandlesAriaRequired { private static final String PARSE_ERROR_CLASSNAME = "-parseerror"; @@ -76,6 +82,9 @@ public class VTextualDate extends VDateField implements Field, ChangeHandler, getClient() .updateVariable(getId(), EventId.FOCUS, "", true); } + + // Needed for tooltip event handling + VTextualDate.this.fireEvent(event); } }); text.addBlurHandler(new BlurHandler() { @@ -94,8 +103,12 @@ public class VTextualDate extends VDateField implements Field, ChangeHandler, EventId.BLUR)) { getClient().updateVariable(getId(), EventId.BLUR, "", true); } + + // Needed for tooltip event handling + VTextualDate.this.fireEvent(event); } }); + add(text); } @@ -150,6 +163,21 @@ public class VTextualDate extends VDateField implements Field, ChangeHandler, return formatStr; } + @Override + public void bindAriaCaption(Element captionElement) { + AriaHelper.bindCaption(text, captionElement); + } + + @Override + public void setAriaRequired(boolean required) { + AriaHelper.handleInputRequired(text, required); + } + + @Override + public void setAriaInvalid(boolean invalid) { + AriaHelper.handleInputInvalid(text, invalid); + } + /** * Updates the text field according to the current date (provided by * {@link #getDate()}). Takes care of updating text, enabling and disabling @@ -178,8 +206,12 @@ public class VTextualDate extends VDateField implements Field, ChangeHandler, if (readonly) { text.addStyleName("v-readonly"); + Roles.getTextboxRole().setAriaReadonlyProperty(text.getElement(), + true); } else { text.removeStyleName("v-readonly"); + Roles.getTextboxRole() + .removeAriaReadonlyProperty(text.getElement()); } } @@ -348,5 +380,4 @@ public class VTextualDate extends VDateField implements Field, ChangeHandler, return null; } - } diff --git a/client/src/com/vaadin/client/ui/VTree.java b/client/src/com/vaadin/client/ui/VTree.java index 624dce4f13..51c00ca310 100644 --- a/client/src/com/vaadin/client/ui/VTree.java +++ b/client/src/com/vaadin/client/ui/VTree.java @@ -24,6 +24,10 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; +import com.google.gwt.aria.client.ExpandedValue; +import com.google.gwt.aria.client.Id; +import com.google.gwt.aria.client.Roles; +import com.google.gwt.aria.client.SelectedValue; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; @@ -56,6 +60,8 @@ import com.vaadin.client.ConnectorMap; import com.vaadin.client.MouseEventDetailsBuilder; import com.vaadin.client.UIDL; import com.vaadin.client.Util; +import com.vaadin.client.ui.aria.AriaHelper; +import com.vaadin.client.ui.aria.HandlesAriaCaption; import com.vaadin.client.ui.dd.DDUtil; import com.vaadin.client.ui.dd.VAbstractDropHandler; import com.vaadin.client.ui.dd.VAcceptCallback; @@ -75,7 +81,8 @@ import com.vaadin.shared.ui.tree.TreeConstants; */ public class VTree extends FocusElementPanel implements VHasDropHandler, FocusHandler, BlurHandler, KeyPressHandler, KeyDownHandler, - SubPartAware, ActionOwner { + SubPartAware, ActionOwner, HandlesAriaCaption { + private String lastNodeKey = ""; public static final String CLASSNAME = "v-tree"; @@ -168,6 +175,8 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, public VTree() { super(); setStyleName(CLASSNAME); + + Roles.getTreeRole().set(body.getElement()); add(body); addFocusHandler(this); @@ -865,12 +874,24 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, } protected void constructDom() { + String labelId = DOM.createUniqueId(); + addStyleName(CLASSNAME); + String treeItemId = DOM.createUniqueId(); + getElement().setId(treeItemId); + Roles.getTreeitemRole().set(getElement()); + Roles.getTreeitemRole().setAriaSelectedState(getElement(), + SelectedValue.FALSE); + Roles.getTreeitemRole().setAriaLabelledbyProperty(getElement(), + Id.of(labelId)); nodeCaptionDiv = DOM.createDiv(); DOM.setElementProperty(nodeCaptionDiv, "className", CLASSNAME + "-caption"); Element wrapper = DOM.createDiv(); + wrapper.setId(labelId); + wrapper.setAttribute("for", treeItemId); + nodeCaptionSpan = DOM.createSpan(); DOM.appendChild(getElement(), nodeCaptionDiv); DOM.appendChild(nodeCaptionDiv, wrapper); @@ -886,6 +907,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, childNodeContainer = new FlowPanel(); childNodeContainer.setStyleName(CLASSNAME + "-children"); + Roles.getGroupRole().set(childNodeContainer.getElement()); setWidget(childNodeContainer); } @@ -914,10 +936,13 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, new String[] { key }, true); } addStyleName(CLASSNAME + "-expanded"); + Roles.getTreeitemRole().setAriaExpandedState(getElement(), + ExpandedValue.TRUE); childNodeContainer.setVisible(true); - } else { removeStyleName(CLASSNAME + "-expanded"); + Roles.getTreeitemRole().setAriaExpandedState(getElement(), + ExpandedValue.FALSE); childNodeContainer.setVisible(false); if (notifyServer) { client.updateVariable(paintableId, "collapse", @@ -1094,15 +1119,17 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, Util.scrollIntoViewVertically(nodeCaptionDiv); } - public void setIcon(String iconUrl) { + public void setIcon(String iconUrl, String altText) { if (iconUrl != null) { // Add icon if not present if (icon == null) { icon = new Icon(client); + Roles.getImgRole().set(icon.getElement()); DOM.insertBefore(DOM.getFirstChild(nodeCaptionDiv), icon.getElement(), nodeCaptionSpan); } icon.setUri(iconUrl); + icon.getElement().setAttribute("alt", altText); } else { // Remove icon if present if (icon != null) { @@ -1517,10 +1544,34 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, // Unfocus previously focused node if (focusedNode != null) { focusedNode.setFocused(false); + + Roles.getTreeRole().removeAriaActivedescendantProperty( + focusedNode.getElement()); } if (node != null) { node.setFocused(true); + Roles.getTreeitemRole().setAriaSelectedState(node.getElement(), + SelectedValue.TRUE); + + /* + * FIXME: This code needs to be changed when the keyboard navigation + * doesn't immediately trigger a selection change anymore. + * + * Right now this function is called before and after the Tree is + * rebuilt when up/down arrow keys are pressed. This leads to the + * problem, that the newly selected item is announced too often with + * a screen reader. + * + * Behaviour is different when using the Tree with and without + * screen reader. + */ + if (node.key.equals(lastNodeKey)) { + Roles.getTreeRole().setAriaActivedescendantProperty( + getFocusElement(), Id.of(node.getElement())); + } else { + lastNodeKey = node.key; + } } focusedNode = node; @@ -2161,4 +2212,8 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, keyToNode.clear(); } + @Override + public void bindAriaCaption(Element captionElement) { + AriaHelper.bindCaption(body, captionElement); + } } diff --git a/client/src/com/vaadin/client/ui/VWindow.java b/client/src/com/vaadin/client/ui/VWindow.java index 51a775cb7e..38dfdba1b8 100644 --- a/client/src/com/vaadin/client/ui/VWindow.java +++ b/client/src/com/vaadin/client/ui/VWindow.java @@ -43,12 +43,13 @@ import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.BrowserInfo; import com.vaadin.client.ConnectorMap; -import com.vaadin.client.Console; import com.vaadin.client.Focusable; import com.vaadin.client.LayoutManager; import com.vaadin.client.Util; +import com.vaadin.client.debug.internal.VDebugWindow; import com.vaadin.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; import com.vaadin.shared.EventId; +import com.vaadin.shared.ui.window.WindowMode; /** * "Sub window" component. @@ -58,18 +59,6 @@ import com.vaadin.shared.EventId; 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; @@ -114,6 +103,9 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, public Element closeBox; /** For internal use only. May be removed or replaced in the future. */ + public Element maximizeRestoreBox; + + /** For internal use only. May be removed or replaced in the future. */ public ApplicationConnection client; /** For internal use only. May be removed or replaced in the future. */ @@ -262,6 +254,9 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, resizeBox = DOM.createDiv(); DOM.setElementProperty(resizeBox, "className", CLASSNAME + "-resizebox"); closeBox = DOM.createDiv(); + maximizeRestoreBox = DOM.createDiv(); + DOM.setElementProperty(maximizeRestoreBox, "className", CLASSNAME + + "-maximizebox"); DOM.setElementProperty(closeBox, "className", CLASSNAME + "-closebox"); DOM.appendChild(footer, resizeBox); @@ -269,14 +264,15 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, DOM.setElementProperty(wrapper, "className", CLASSNAME + "-wrap"); DOM.appendChild(wrapper, header); + DOM.appendChild(wrapper, maximizeRestoreBox); 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); + sinkEvents(Event.ONDBLCLICK | Event.MOUSEEVENTS | Event.TOUCHEVENTS + | Event.ONCLICK | Event.ONLOSECAPTURE); setWidget(contentPanel); @@ -575,6 +571,31 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, } } + public void updateMaximizeRestoreClassName(boolean visible, + WindowMode windowMode) { + String className; + if (windowMode == WindowMode.MAXIMIZED) { + className = CLASSNAME + "-restorebox"; + } else { + className = CLASSNAME + "-maximizebox"; + } + if (!visible) { + className = className + " " + className + "-disabled"; + } + maximizeRestoreBox.setClassName(className); + } + + // TODO this will eventually be removed, currently used to avoid updating to + // server side. + public void setPopupPositionNoUpdate(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); + } + @Override public void setPopupPosition(int left, int top) { if (top < 0) { @@ -616,6 +637,8 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, return contents; } + private Event headerDragPending; + @Override public void onBrowserEvent(final Event event) { boolean bubble = true; @@ -632,6 +655,28 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, onCloseClick(); } bubble = false; + } else if (target == maximizeRestoreBox) { + // handled in connector + if (type != Event.ONCLICK) { + bubble = false; + } + } else if (header.isOrHasChild(target) && !dragging) { + // dblclick handled in connector + if (type != Event.ONDBLCLICK && draggable) { + if (type == Event.ONMOUSEDOWN) { + headerDragPending = event; + } else if (type == Event.ONMOUSEMOVE + && headerDragPending != null) { + // ie won't work unless this is set here + dragging = true; + onDragEvent(headerDragPending); + onDragEvent(event); + headerDragPending = null; + } else { + headerDragPending = null; + } + bubble = false; + } } else if (dragging || !contents.isOrHasChild(target)) { onDragEvent(event); bubble = false; @@ -648,7 +693,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, */ if (type == Event.ONMOUSEDOWN && !contentPanel.getElement().isOrHasChild(target) - && target != closeBox) { + && target != closeBox && target != maximizeRestoreBox) { contentPanel.focus(); } @@ -746,16 +791,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, } 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"); @@ -775,7 +811,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, } } - private void updateContentsSize() { + public void updateContentsSize() { LayoutManager layoutManager = getLayoutManager(); layoutManager.setNeedsMeasure(ConnectorMap.get(client).getConnector( this)); @@ -896,7 +932,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, // debug window Widget w = Util.findWidget(target, null); while (w != null) { - if (w instanceof Console) { + if (w instanceof VDebugWindow) { return true; // allow debug-window clicks } else if (ConnectorMap.get(client).isConnector(w)) { return false; @@ -959,10 +995,6 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, contentPanel.focus(); } - public int getMinHeight() { - return MIN_CONTENT_AREA_HEIGHT + getDecorationHeight(); - } - private int getDecorationHeight() { LayoutManager lm = getLayoutManager(); int headerHeight = lm.getOuterHeight(header); @@ -974,10 +1006,6 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, return LayoutManager.get(client); } - public int getMinWidth() { - return MIN_CONTENT_AREA_WIDTH + getDecorationWidth(); - } - private int getDecorationWidth() { LayoutManager layoutManager = getLayoutManager(); return layoutManager.getOuterWidth(getElement()) diff --git a/client/src/com/vaadin/client/ui/absolutelayout/AbsoluteLayoutConnector.java b/client/src/com/vaadin/client/ui/absolutelayout/AbsoluteLayoutConnector.java index 868c14f742..da79639dcd 100644 --- a/client/src/com/vaadin/client/ui/absolutelayout/AbsoluteLayoutConnector.java +++ b/client/src/com/vaadin/client/ui/absolutelayout/AbsoluteLayoutConnector.java @@ -100,8 +100,7 @@ public class AbsoluteLayoutConnector extends /* * (non-Javadoc) * - * @see - * com.vaadin.client.HasComponentsConnector#updateCaption(com.vaadin + * @see com.vaadin.client.HasComponentsConnector#updateCaption(com.vaadin * .client.ComponentConnector) */ @Override @@ -188,6 +187,8 @@ public class AbsoluteLayoutConnector extends oldChild.removeStateChangeHandler(childStateChangeHandler); } } + + getWidget().cleanupWrappers(); } /* diff --git a/client/src/com/vaadin/client/ui/aria/AriaHelper.java b/client/src/com/vaadin/client/ui/aria/AriaHelper.java new file mode 100644 index 0000000000..0ff58cf510 --- /dev/null +++ b/client/src/com/vaadin/client/ui/aria/AriaHelper.java @@ -0,0 +1,188 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.aria; + +import com.google.gwt.aria.client.Id; +import com.google.gwt.aria.client.InvalidValue; +import com.google.gwt.aria.client.Roles; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.Widget; + +/** + * Helper class that helps to implement the WAI-ARIA functionality. + */ +public class AriaHelper { + public static final String ASSISTIVE_DEVICE_ONLY_STYLE = "v-assistive-device-only"; + + /** + * Binds a caption (label in HTML speak) to the form element as required by + * WAI-ARIA specification. + * + * @param widget + * Widget, that should be bound to the caption + * @param captionElements + * Element with of caption to bind + */ + public static void bindCaption(Widget widget, Element captionElement) { + assert widget != null : "Valid Widget required"; + + if (widget instanceof HandlesAriaCaption) { + // Let the widget handle special cases itself + if (captionElement == null) { + ((HandlesAriaCaption) widget).bindAriaCaption(null); + } else { + ensureHasId(captionElement); + ((HandlesAriaCaption) widget).bindAriaCaption(captionElement); + } + } else if (captionElement != null) { + // Handle the default case + ensureHasId(captionElement); + String ownerId = ensureHasId(widget.getElement()); + captionElement.setAttribute("for", ownerId); + + Roles.getTextboxRole().setAriaLabelledbyProperty( + widget.getElement(), Id.of(captionElement)); + } else { + clearCaption(widget); + } + } + + /** + * Removes a binding to a caption added with bindCaption() from the provided + * Widget. + * + * @param widget + * Widget, that was bound to a caption before + */ + private static void clearCaption(Widget widget) { + Roles.getTextboxRole() + .removeAriaLabelledbyProperty(widget.getElement()); + } + + /** + * Handles the required actions depending of the input Widget being required + * or not. + * + * @param widget + * Widget, typically an input Widget like TextField + * @param required + * boolean, true when the element is required + */ + public static void handleInputRequired(Widget widget, boolean required) { + assert widget != null : "Valid Widget required"; + + if (widget instanceof HandlesAriaRequired) { + ((HandlesAriaRequired) widget).setAriaRequired(required); + } else { + handleInputRequired(widget.getElement(), required); + } + } + + /** + * Handles the required actions depending of the input element being + * required or not. + * + * @param element + * Element, typically from an input Widget like TextField + * @param required + * boolean, true when the element is required + */ + public static void handleInputRequired(Element element, boolean required) { + if (required) { + Roles.getTextboxRole().setAriaRequiredProperty(element, required); + } else { + Roles.getTextboxRole().removeAriaRequiredProperty(element); + } + } + + /** + * Handles the required actions depending of the input Widget contains + * unaccepted input. + * + * @param widget + * Widget, typically an input Widget like TextField + * @param invalid + * boolean, true when the Widget input has an error + */ + public static void handleInputInvalid(Widget widget, boolean invalid) { + assert widget != null : "Valid Widget required"; + + if (widget instanceof HandlesAriaInvalid) { + ((HandlesAriaInvalid) widget).setAriaInvalid(invalid); + } else { + handleInputInvalid(widget.getElement(), invalid); + } + } + + /** + * Handles the required actions depending of the input element contains + * unaccepted input. + * + * @param element + * Element, typically an input Widget like TextField + * @param invalid + * boolean, true when the element input has an error + */ + public static void handleInputInvalid(Element element, boolean invalid) { + if (invalid) { + Roles.getTextboxRole().setAriaInvalidState(element, + InvalidValue.TRUE); + } else { + Roles.getTextboxRole().removeAriaInvalidState(element); + } + } + + /** + * Makes sure that the provided element has an id attribute. Adds a new + * unique id if not. + * + * @param element + * Element to check + * @return String with the id of the element + */ + public static String ensureHasId(Element element) { + assert element != null : "Valid Element required"; + + String id = element.getId(); + if (null == id || id.isEmpty()) { + id = DOM.createUniqueId(); + element.setId(id); + } + return id; + } + + /** + * Allows to move an element out of the visible area of the browser window. + * + * This makes it possible to have additional information for an assistive + * device, that is not in the way for visual users. + * + * @param element + * Element to move out of sight + * @param boolean assistiveOnly true when element should only be visible for + * assistive devices, false to make the element visible for all + */ + public static void setVisibleForAssistiveDevicesOnly(Element element, + boolean assistiveOnly) { + if (assistiveOnly) { + element.addClassName(ASSISTIVE_DEVICE_ONLY_STYLE); + } else { + element.removeClassName(ASSISTIVE_DEVICE_ONLY_STYLE); + } + } +} diff --git a/client/src/com/vaadin/client/ui/aria/HandlesAriaCaption.java b/client/src/com/vaadin/client/ui/aria/HandlesAriaCaption.java new file mode 100644 index 0000000000..50f83fdede --- /dev/null +++ b/client/src/com/vaadin/client/ui/aria/HandlesAriaCaption.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.aria; + +import com.google.gwt.user.client.Element; + +/** + * Some Widgets need to handle the caption handling for WAI-ARIA themselfs, as + * for example the required ids need to be set in a specific way. In such a + * case, the Widget needs to implement this interface. + */ +public interface HandlesAriaCaption { + + /** + * Called to bind the provided caption (label in HTML speak) element to the + * main input element of the Widget. + * + * Binding should be removed from the main input field when captionElement + * is null. + * + * @param captionElement + * Element of the caption + */ + void bindAriaCaption(Element captionElement); +} diff --git a/client/src/com/vaadin/client/SynchronousXHR.java b/client/src/com/vaadin/client/ui/aria/HandlesAriaInvalid.java index a19c9bad16..05cb82b0d6 100644 --- a/client/src/com/vaadin/client/SynchronousXHR.java +++ b/client/src/com/vaadin/client/ui/aria/HandlesAriaInvalid.java @@ -13,24 +13,21 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.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 - } - }-*/; +package com.vaadin.client.ui.aria; +/** + * Some Widgets need to handle the required handling for WAI-ARIA themselfs, as + * this attribute needs to be set to the input element itself. In such a case, + * the Widget needs to implement this interface. + */ +public interface HandlesAriaInvalid { + /** + * Called to set the element, typically an input element, as invalid. + * + * @param invalid + * boolean, true when the element should be marked invalid, false + * otherwise + */ + void setAriaInvalid(boolean invalid); } diff --git a/client/src/com/vaadin/client/ui/aria/HandlesAriaRequired.java b/client/src/com/vaadin/client/ui/aria/HandlesAriaRequired.java new file mode 100644 index 0000000000..9b18bfb4de --- /dev/null +++ b/client/src/com/vaadin/client/ui/aria/HandlesAriaRequired.java @@ -0,0 +1,32 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.aria; + +/** + * Some Widgets need to handle the required handling for WAI-ARIA themselfs, as + * this attribute needs to be set to the input element itself. In such a case, + * the Widget needs to implement this interface. + */ +public interface HandlesAriaRequired { + /** + * Called to set the element, typically an input element, as required. + * + * @param required + * boolean true when the element needs to be set as required + */ + void setAriaRequired(boolean required); +} diff --git a/client/src/com/vaadin/client/ui/button/ButtonConnector.java b/client/src/com/vaadin/client/ui/button/ButtonConnector.java index 9733d206c7..fff983c168 100644 --- a/client/src/com/vaadin/client/ui/button/ButtonConnector.java +++ b/client/src/com/vaadin/client/ui/button/ButtonConnector.java @@ -24,6 +24,7 @@ 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.Element; import com.vaadin.client.EventHelper; import com.vaadin.client.MouseEventDetailsBuilder; import com.vaadin.client.communication.StateChangeEvent; @@ -83,8 +84,10 @@ public class ButtonConnector extends AbstractComponentConnector implements if (getIcon() != null) { if (getWidget().icon == null) { getWidget().icon = new Icon(getConnection()); - getWidget().wrapper.insertBefore( - getWidget().icon.getElement(), + Element iconElement = getWidget().icon.getElement(); + iconElement.setAttribute("alt", getState().iconAltText); + + getWidget().wrapper.insertBefore(iconElement, getWidget().captionElement); } getWidget().icon.setUri(getIcon()); diff --git a/client/src/com/vaadin/client/ui/calendar/CalendarConnector.java b/client/src/com/vaadin/client/ui/calendar/CalendarConnector.java new file mode 100644 index 0000000000..285d15792b --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/CalendarConnector.java @@ -0,0 +1,662 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; + +import com.google.gwt.core.shared.GWT; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.TooltipInfo; +import com.vaadin.client.UIDL; +import com.vaadin.client.Util; +import com.vaadin.client.VConsole; +import com.vaadin.client.communication.RpcProxy; +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.client.ui.Action; +import com.vaadin.client.ui.ActionOwner; +import com.vaadin.client.ui.SimpleManagedLayout; +import com.vaadin.client.ui.VCalendar; +import com.vaadin.client.ui.VCalendar.BackwardListener; +import com.vaadin.client.ui.VCalendar.DateClickListener; +import com.vaadin.client.ui.VCalendar.EventClickListener; +import com.vaadin.client.ui.VCalendar.EventMovedListener; +import com.vaadin.client.ui.VCalendar.EventResizeListener; +import com.vaadin.client.ui.VCalendar.ForwardListener; +import com.vaadin.client.ui.VCalendar.MouseEventListener; +import com.vaadin.client.ui.VCalendar.RangeSelectListener; +import com.vaadin.client.ui.VCalendar.WeekClickListener; +import com.vaadin.client.ui.calendar.schedule.CalendarDay; +import com.vaadin.client.ui.calendar.schedule.CalendarEvent; +import com.vaadin.client.ui.calendar.schedule.DateCell; +import com.vaadin.client.ui.calendar.schedule.DateCell.DateCellSlot; +import com.vaadin.client.ui.calendar.schedule.DateCellDayEvent; +import com.vaadin.client.ui.calendar.schedule.DateUtil; +import com.vaadin.client.ui.calendar.schedule.HasTooltipKey; +import com.vaadin.client.ui.calendar.schedule.SimpleDayCell; +import com.vaadin.client.ui.calendar.schedule.dd.CalendarDropHandler; +import com.vaadin.client.ui.dd.VHasDropHandler; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.calendar.CalendarClientRpc; +import com.vaadin.shared.ui.calendar.CalendarEventId; +import com.vaadin.shared.ui.calendar.CalendarServerRpc; +import com.vaadin.shared.ui.calendar.CalendarState; +import com.vaadin.shared.ui.calendar.DateConstants; +import com.vaadin.ui.Calendar; + +/** + * Handles communication between Calendar on the server side and + * {@link VCalendar} on the client side. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@Connect(value = Calendar.class, loadStyle = LoadStyle.LAZY) +public class CalendarConnector extends AbstractComponentConnector implements + VHasDropHandler, ActionOwner, SimpleManagedLayout { + + private CalendarServerRpc rpc = RpcProxy.create(CalendarServerRpc.class, + this); + + private CalendarDropHandler dropHandler; + + private final HashMap<String, String> actionMap = new HashMap<String, String>(); + private HashMap<Object, String> tooltips = new HashMap<Object, String>(); + + /** + * + */ + public CalendarConnector() { + + // Listen to events + registerListeners(); + } + + @Override + protected void init() { + super.init(); + registerRpc(CalendarClientRpc.class, new CalendarClientRpc() { + @Override + public void scroll(int scrollPosition) { + // TODO widget scroll + } + }); + getLayoutManager().registerDependency(this, getWidget().getElement()); + } + + @Override + public void onUnregister() { + super.onUnregister(); + getLayoutManager().unregisterDependency(this, getWidget().getElement()); + } + + @Override + public VCalendar getWidget() { + return (VCalendar) super.getWidget(); + } + + @Override + public CalendarState getState() { + return (CalendarState) super.getState(); + } + + /** + * Registers listeners on the calendar so server can be notified of the + * events + */ + protected void registerListeners() { + getWidget().setListener(new DateClickListener() { + @Override + public void dateClick(String date) { + if (!getWidget().isDisabledOrReadOnly() + && hasEventListener(CalendarEventId.DATECLICK)) { + rpc.dateClick(date); + } + } + }); + getWidget().setListener(new ForwardListener() { + @Override + public void forward() { + if (hasEventListener(CalendarEventId.FORWARD)) { + rpc.forward(); + } + } + }); + getWidget().setListener(new BackwardListener() { + @Override + public void backward() { + if (hasEventListener(CalendarEventId.BACKWARD)) { + rpc.backward(); + } + } + }); + getWidget().setListener(new RangeSelectListener() { + @Override + public void rangeSelected(String value) { + if (hasEventListener(CalendarEventId.RANGESELECT)) { + rpc.rangeSelect(value); + } + } + }); + getWidget().setListener(new WeekClickListener() { + @Override + public void weekClick(String event) { + if (!getWidget().isDisabledOrReadOnly() + && hasEventListener(CalendarEventId.WEEKCLICK)) { + rpc.weekClick(event); + } + } + }); + getWidget().setListener(new EventMovedListener() { + @Override + public void eventMoved(CalendarEvent event) { + if (hasEventListener(CalendarEventId.EVENTMOVE)) { + StringBuilder sb = new StringBuilder(); + sb.append(DateUtil.formatClientSideDate(event.getStart())); + sb.append("-"); + sb.append(DateUtil.formatClientSideTime(event + .getStartTime())); + rpc.eventMove(event.getIndex(), sb.toString()); + } + } + }); + getWidget().setListener(new EventResizeListener() { + @Override + public void eventResized(CalendarEvent event) { + if (hasEventListener(CalendarEventId.EVENTRESIZE)) { + StringBuilder buffer = new StringBuilder(); + + buffer.append(DateUtil.formatClientSideDate(event + .getStart())); + buffer.append("-"); + buffer.append(DateUtil.formatClientSideTime(event + .getStartTime())); + + String newStartDate = buffer.toString(); + + buffer = new StringBuilder(); + buffer.append(DateUtil.formatClientSideDate(event.getEnd())); + buffer.append("-"); + buffer.append(DateUtil.formatClientSideTime(event + .getEndTime())); + + String newEndDate = buffer.toString(); + + rpc.eventResize(event.getIndex(), newStartDate, newEndDate); + } + } + }); + getWidget().setListener(new VCalendar.ScrollListener() { + @Override + public void scroll(int scrollPosition) { + // This call is @Delayed (== non-immediate) + rpc.scroll(scrollPosition); + } + }); + getWidget().setListener(new EventClickListener() { + @Override + public void eventClick(CalendarEvent event) { + if (hasEventListener(CalendarEventId.EVENTCLICK)) { + rpc.eventClick(event.getIndex()); + } + } + }); + getWidget().setListener(new MouseEventListener() { + @Override + public void contextMenu(ContextMenuEvent event, final Widget widget) { + final NativeEvent ne = event.getNativeEvent(); + int left = ne.getClientX(); + int top = ne.getClientY(); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + getClient().getContextMenu().showAt(new ActionOwner() { + @Override + public String getPaintableId() { + return CalendarConnector.this.getPaintableId(); + } + + @Override + public ApplicationConnection getClient() { + return CalendarConnector.this.getClient(); + } + + @Override + @SuppressWarnings("deprecation") + public Action[] getActions() { + if (widget instanceof SimpleDayCell) { + /* + * Month view + */ + SimpleDayCell cell = (SimpleDayCell) widget; + Date start = new Date(cell.getDate().getYear(), + cell.getDate().getMonth(), cell.getDate() + .getDate(), 0, 0, 0); + + Date end = new Date(cell.getDate().getYear(), cell + .getDate().getMonth(), cell.getDate() + .getDate(), 23, 59, 59); + + return CalendarConnector.this.getActionsBetween( + start, end); + } else if (widget instanceof DateCell) { + /* + * Week and Day view + */ + DateCell cell = (DateCell) widget; + int slotIndex = DOM.getChildIndex( + cell.getElement(), (Element) ne + .getEventTarget().cast()); + DateCellSlot slot = cell.getSlot(slotIndex); + return CalendarConnector.this.getActionsBetween( + slot.getFrom(), slot.getTo()); + } else if (widget instanceof DateCellDayEvent) { + /* + * Context menu on event + */ + DateCellDayEvent dayEvent = (DateCellDayEvent) widget; + CalendarEvent event = dayEvent.getCalendarEvent(); + Action[] actions = CalendarConnector.this + .getActionsBetween(event.getStartTime(), + event.getEndTime()); + for (Action action : actions) { + ((VCalendarAction) action).setEvent(event); + } + return actions; + + } + return null; + } + }, left, top); + } + }); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + CalendarState state = getState(); + VCalendar widget = getWidget(); + boolean monthView = state.days.size() > 7; + + // Enable or disable the forward and backward navigation buttons + widget.setForwardNavigationEnabled(hasEventListener(CalendarEventId.FORWARD)); + widget.setBackwardNavigationEnabled(hasEventListener(CalendarEventId.BACKWARD)); + + widget.set24HFormat(state.format24H); + widget.setDayNames(state.dayNames); + widget.setMonthNames(state.monthNames); + widget.setFirstDayNumber(state.firstVisibleDayOfWeek); + widget.setLastDayNumber(state.lastVisibleDayOfWeek); + widget.setFirstHourOfTheDay(state.firstHourOfDay); + widget.setLastHourOfTheDay(state.lastHourOfDay); + widget.setReadOnly(state.readOnly); + widget.setDisabled(!state.enabled); + + widget.setRangeSelectAllowed(hasEventListener(CalendarEventId.RANGESELECT)); + widget.setRangeMoveAllowed(hasEventListener(CalendarEventId.EVENTMOVE)); + widget.setEventMoveAllowed(hasEventListener(CalendarEventId.EVENTMOVE)); + widget.setEventResizeAllowed(hasEventListener(CalendarEventId.EVENTRESIZE)); + + List<CalendarState.Day> days = state.days; + List<CalendarState.Event> events = state.events; + + if (monthView) { + updateMonthView(days, events); + } else { + updateWeekView(days, events); + } + + updateSizes(); + + registerEventToolTips(state.events); + updateActionMap(state.actions); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin.terminal + * .gwt.client.UIDL, com.vaadin.terminal.gwt.client.ApplicationConnection) + */ + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + + // check for DD -related access criteria + // Iterator<Object> childIterator = uidl.getChildIterator(); + // while (childIterator.hasNext()) { + // UIDL child = (UIDL) childIterator.next(); + // + // // Drag&drop + // if (ACCESSCRITERIA.equals(child.getTag())) { + // if (monthView + // && !(getDropHandler() instanceof CalendarMonthDropHandler)) { + // setDropHandler(new CalendarMonthDropHandler()); + // + // } else if (!monthView + // && !(getDropHandler() instanceof CalendarWeekDropHandler)) { + // setDropHandler(new CalendarWeekDropHandler()); + // } + // + // getDropHandler().setCalendarPaintable(this); + // getDropHandler().updateAcceptRules(child); + // + // } else { + // setDropHandler(null); + // } + // + // } + } + + /** + * Returns the ApplicationConnection used to connect to the server side + */ + @Override + public ApplicationConnection getClient() { + return getConnection(); + } + + /** + * Register the description of the events as tooltips. This way, any event + * displaying widget can use the event index as a key to display the + * tooltip. + */ + private void registerEventToolTips(List<CalendarState.Event> events) { + for (CalendarState.Event e : events) { + if (e.description != null && !"".equals(e.description)) { + tooltips.put(e.index, e.description); + } else { + tooltips.remove(e.index); + } + } + } + + @Override + public TooltipInfo getTooltipInfo(com.google.gwt.dom.client.Element element) { + TooltipInfo tooltipInfo = null; + Widget w = Util.findWidget((Element) element, null); + if (w instanceof HasTooltipKey) { + tooltipInfo = GWT.create(TooltipInfo.class); + String title = tooltips.get(((HasTooltipKey) w).getTooltipKey()); + tooltipInfo.setTitle(title != null ? title : ""); + } + if (tooltipInfo == null) { + tooltipInfo = super.getTooltipInfo(element); + } + return tooltipInfo; + } + + @Override + public boolean hasTooltip() { + /* + * Tooltips are not processed until updateFromUIDL, so we can't be sure + * that there are no tooltips during onStateChange when this is used. + */ + return true; + } + + private void updateMonthView(List<CalendarState.Day> days, + List<CalendarState.Event> events) { + CalendarState state = getState(); + getWidget().updateMonthView(state.firstDayOfWeek, + getWidget().getDateTimeFormat().parse(state.now), days.size(), + calendarEventListOf(events, state.format24H), + calendarDayListOf(days)); + } + + private void updateWeekView(List<CalendarState.Day> days, + List<CalendarState.Event> events) { + CalendarState state = getState(); + getWidget().updateWeekView(state.scroll, + getWidget().getDateTimeFormat().parse(state.now), days.size(), + state.firstDayOfWeek, + calendarEventListOf(events, state.format24H), + calendarDayListOf(days)); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler#getDropHandler() + */ + @Override + public CalendarDropHandler getDropHandler() { + return dropHandler; + } + + /** + * Set the drop handler + * + * @param dropHandler + * The drophandler to use + */ + public void setDropHandler(CalendarDropHandler dropHandler) { + this.dropHandler = dropHandler; + } + + private Action[] getActionsBetween(Date start, Date end) { + List<Action> actions = new ArrayList<Action>(); + for (int i = 0; i < actionKeys.size(); i++) { + final String actionKey = actionKeys.get(i); + Date actionStartDate; + Date actionEndDate; + try { + actionStartDate = getActionStartDate(actionKey); + actionEndDate = getActionEndDate(actionKey); + } catch (ParseException pe) { + VConsole.error("Failed to parse action date"); + continue; + } + + boolean startIsValid = start.compareTo(actionStartDate) >= 0; + boolean endIsValid = end.compareTo(actionEndDate) <= 0; + if (startIsValid && endIsValid) { + VCalendarAction a = new VCalendarAction(this, rpc, actionKey); + a.setCaption(getActionCaption(actionKey)); + a.setIconUrl(getActionIcon(actionKey)); + a.setActionStartDate(start); + a.setActionEndDate(end); + actions.add(a); + } + } + + return actions.toArray(new Action[actions.size()]); + } + + private List<String> actionKeys = new ArrayList<String>(); + + private void updateActionMap(List<CalendarState.Action> actions) { + actionMap.clear(); + actionKeys.clear(); + + if (actions == null) { + return; + } + + for (CalendarState.Action action : actions) { + String id = action.actionKey + "-" + action.startDate + "-" + + action.endDate; + actionMap.put(id + "_c", action.caption); + actionMap.put(id + "_s", action.startDate); + actionMap.put(id + "_e", action.endDate); + actionKeys.add(id); + if (action.iconKey != null) { + actionMap.put(id + "_i", getResourceUrl(action.iconKey)); + + } else { + actionMap.remove(id + "_i"); + } + } + } + + /** + * Get the text that is displayed for a context menu item + * + * @param actionKey + * The unique action key + * @return + */ + public String getActionCaption(String actionKey) { + return actionMap.get(actionKey + "_c"); + } + + /** + * Get the icon url for a context menu item + * + * @param actionKey + * The unique action key + * @return + */ + public String getActionIcon(String actionKey) { + return actionMap.get(actionKey + "_i"); + } + + /** + * Get the start date for an action item + * + * @param actionKey + * The unique action key + * @return + * @throws ParseException + */ + public Date getActionStartDate(String actionKey) throws ParseException { + String dateStr = actionMap.get(actionKey + "_s"); + DateTimeFormat formatter = DateTimeFormat + .getFormat(DateConstants.ACTION_DATE_FORMAT_PATTERN); + return formatter.parse(dateStr); + } + + /** + * Get the end date for an action item + * + * @param actionKey + * The unique action key + * @return + * @throws ParseException + */ + public Date getActionEndDate(String actionKey) throws ParseException { + String dateStr = actionMap.get(actionKey + "_e"); + DateTimeFormat formatter = DateTimeFormat + .getFormat(DateConstants.ACTION_DATE_FORMAT_PATTERN); + return formatter.parse(dateStr); + } + + /** + * Returns ALL currently registered events. Use {@link #getActions(Date)} to + * get the actions for a specific date + */ + @Override + public Action[] getActions() { + List<Action> actions = new ArrayList<Action>(); + for (int i = 0; i < actionKeys.size(); i++) { + final String actionKey = actionKeys.get(i); + final VCalendarAction a = new VCalendarAction(this, rpc, actionKey); + a.setCaption(getActionCaption(actionKey)); + a.setIconUrl(getActionIcon(actionKey)); + + try { + a.setActionStartDate(getActionStartDate(actionKey)); + a.setActionEndDate(getActionEndDate(actionKey)); + } catch (ParseException pe) { + VConsole.error(pe); + } + + actions.add(a); + } + return actions.toArray(new Action[actions.size()]); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ui.ActionOwner#getPaintableId() + */ + @Override + public String getPaintableId() { + return getConnectorId(); + } + + private List<CalendarEvent> calendarEventListOf( + List<CalendarState.Event> events, boolean format24h) { + List<CalendarEvent> list = new ArrayList<CalendarEvent>(events.size()); + for (CalendarState.Event event : events) { + final String dateFrom = event.dateFrom; + final String dateTo = event.dateTo; + final String timeFrom = event.timeFrom; + final String timeTo = event.timeTo; + CalendarEvent calendarEvent = new CalendarEvent(); + calendarEvent.setAllDay(event.allDay); + calendarEvent.setCaption(event.caption); + calendarEvent.setDescription(event.description); + calendarEvent.setStart(getWidget().getDateFormat().parse(dateFrom)); + calendarEvent.setEnd(getWidget().getDateFormat().parse(dateTo)); + calendarEvent.setFormat24h(format24h); + calendarEvent.setStartTime(getWidget().getDateTimeFormat().parse( + dateFrom + " " + timeFrom)); + calendarEvent.setEndTime(getWidget().getDateTimeFormat().parse( + dateTo + " " + timeTo)); + calendarEvent.setStyleName(event.styleName); + calendarEvent.setIndex(event.index); + list.add(calendarEvent); + } + return list; + } + + private List<CalendarDay> calendarDayListOf(List<CalendarState.Day> days) { + List<CalendarDay> list = new ArrayList<CalendarDay>(days.size()); + for (CalendarState.Day day : days) { + CalendarDay d = new CalendarDay(day.date, day.localizedDateFormat, + day.dayOfWeek, day.week); + + list.add(d); + } + return list; + } + + @Override + public void layout() { + updateSizes(); + } + + private void updateSizes() { + int height = getLayoutManager() + .getOuterHeight(getWidget().getElement()); + int width = getLayoutManager().getOuterWidth(getWidget().getElement()); + + if (isUndefinedWidth()) { + width = -1; + } + if (isUndefinedHeight()) { + height = -1; + } + + getWidget().setSizeForChildren(width, height); + + } +} diff --git a/client/src/com/vaadin/client/ui/calendar/VCalendarAction.java b/client/src/com/vaadin/client/ui/calendar/VCalendarAction.java new file mode 100644 index 0000000000..2a529354e5 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/VCalendarAction.java @@ -0,0 +1,138 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar; + +import java.util.Date; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.vaadin.client.ui.Action; +import com.vaadin.client.ui.calendar.schedule.CalendarEvent; +import com.vaadin.shared.ui.calendar.CalendarServerRpc; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * Action performed by the calendar + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class VCalendarAction extends Action { + + private CalendarServerRpc rpc; + + private String actionKey = ""; + + private Date actionStartDate; + + private Date actionEndDate; + + private CalendarEvent event; + + private final DateTimeFormat dateformat_datetime = DateTimeFormat + .getFormat(DateConstants.ACTION_DATE_FORMAT_PATTERN); + + /** + * + * @param owner + */ + public VCalendarAction(CalendarConnector owner) { + super(owner); + } + + /** + * Constructor + * + * @param owner + * The owner who trigger this kinds of events + * @param rpc + * The CalendarRpc which is used for executing actions + * @param key + * The unique action key which identifies this particular action + */ + public VCalendarAction(CalendarConnector owner, CalendarServerRpc rpc, + String key) { + this(owner); + this.rpc = rpc; + actionKey = key; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ui.Action#execute() + */ + @Override + public void execute() { + String startDate = dateformat_datetime.format(actionStartDate); + String endDate = dateformat_datetime.format(actionEndDate); + + if (event == null) { + rpc.actionOnEmptyCell(actionKey.split("-")[0], startDate, endDate); + } else { + rpc.actionOnEvent(actionKey.split("-")[0], startDate, endDate, + event.getIndex()); + } + + owner.getClient().getContextMenu().hide(); + } + + /** + * Get the date and time when the action starts + * + * @return + */ + public Date getActionStartDate() { + return actionStartDate; + } + + /** + * Set the date when the actions start + * + * @param actionStartDate + * The date and time when the action starts + */ + public void setActionStartDate(Date actionStartDate) { + this.actionStartDate = actionStartDate; + } + + /** + * Get the date and time when the action ends + * + * @return + */ + public Date getActionEndDate() { + return actionEndDate; + } + + /** + * Set the date and time when the action ends + * + * @param actionEndDate + * The date and time when the action ends + */ + public void setActionEndDate(Date actionEndDate) { + this.actionEndDate = actionEndDate; + } + + public CalendarEvent getEvent() { + return event; + } + + public void setEvent(CalendarEvent event) { + this.event = event; + } + +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/CalendarDay.java b/client/src/com/vaadin/client/ui/calendar/schedule/CalendarDay.java new file mode 100644 index 0000000000..ca176c08c1 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/CalendarDay.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +/** + * Utility class used to represent a day when updating views. Only used + * internally. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class CalendarDay { + private String date; + private String localizedDateFormat; + private int dayOfWeek; + private int week; + + public CalendarDay(String date, String localizedDateFormat, int dayOfWeek, + int week) { + super(); + this.date = date; + this.localizedDateFormat = localizedDateFormat; + this.dayOfWeek = dayOfWeek; + this.week = week; + } + + public String getDate() { + return date; + } + + public String getLocalizedDateFormat() { + return localizedDateFormat; + } + + public int getDayOfWeek() { + return dayOfWeek; + } + + public int getWeek() { + return week; + } +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/CalendarEvent.java b/client/src/com/vaadin/client/ui/calendar/schedule/CalendarEvent.java new file mode 100644 index 0000000000..e2c06d41ea --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/CalendarEvent.java @@ -0,0 +1,313 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Date; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * A client side implementation of a calendar event + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class CalendarEvent { + private int index; + private String caption; + private Date start, end; + private String styleName; + private Date startTime, endTime; + private String description; + private int slotIndex = -1; + private boolean format24h; + + DateTimeFormat dateformat_date = DateTimeFormat.getFormat("h:mm a"); + DateTimeFormat dateformat_date24 = DateTimeFormat.getFormat("H:mm"); + private boolean allDay; + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStyleName() + */ + public String getStyleName() { + return styleName; + } + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStart() + */ + public Date getStart() { + return start; + } + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStyleName() + * @param style + */ + public void setStyleName(String style) { + styleName = style; + } + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStart() + * @param start + */ + public void setStart(Date start) { + this.start = start; + } + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getEnd() + * @return + */ + public Date getEnd() { + return end; + } + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getEnd() + * @param end + */ + public void setEnd(Date end) { + this.end = end; + } + + /** + * Returns the start time of the event + * + * @return Time embedded in the {@link Date} object + */ + public Date getStartTime() { + return startTime; + } + + /** + * Set the start time of the event + * + * @param startTime + * The time of the event. Use the time fields in the {@link Date} + * object + */ + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + /** + * Get the end time of the event + * + * @return Time embedded in the {@link Date} object + */ + public Date getEndTime() { + return endTime; + } + + /** + * Set the end time of the event + * + * @param endTime + * Time embedded in the {@link Date} object + */ + public void setEndTime(Date endTime) { + this.endTime = endTime; + } + + /** + * Get the (server side) index of the event + * + * @return + */ + public int getIndex() { + return index; + } + + /** + * Get the index of the slot where the event in rendered + * + * @return + */ + public int getSlotIndex() { + return slotIndex; + } + + /** + * Set the index of the slot where the event in rendered + * + * @param index + * The index of the slot + */ + public void setSlotIndex(int index) { + slotIndex = index; + } + + /** + * Set the (server side) index of the event + * + * @param index + * The index + */ + public void setIndex(int index) { + this.index = index; + } + + /** + * Get the caption of the event. The caption is the text displayed in the + * calendar on the event. + * + * @return + */ + public String getCaption() { + return caption; + } + + /** + * Set the caption of the event. The caption is the text displayed in the + * calendar on the event. + * + * @param caption + * The visible caption of the event + */ + public void setCaption(String caption) { + this.caption = caption; + } + + /** + * Get the description of the event. The description is the text displayed + * when hoovering over the event with the mouse + * + * @return + */ + public String getDescription() { + return description; + } + + /** + * Set the description of the event. The description is the text displayed + * when hoovering over the event with the mouse + * + * @param description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Does the event use the 24h time format + * + * @param format24h + * True if it uses the 24h format, false if it uses the 12h time + * format + */ + public void setFormat24h(boolean format24h) { + this.format24h = format24h; + } + + /** + * Is the event an all day event. + * + * @param allDay + * True if the event should be rendered all day + */ + public void setAllDay(boolean allDay) { + this.allDay = allDay; + } + + /** + * Is the event an all day event. + * + * @return + */ + public boolean isAllDay() { + return allDay; + } + + /** + * Get the time as a formatted string + * + * @return + */ + public String getTimeAsText() { + if (format24h) { + return dateformat_date24.format(startTime); + } else { + return dateformat_date.format(startTime); + } + } + + /** + * Get the amount of milliseconds between the start and end of the event + * + * @return + */ + public long getRangeInMilliseconds() { + return getEndTime().getTime() - getStartTime().getTime(); + } + + /** + * Get the amount of minutes between the start and end of the event + * + * @return + */ + public long getRangeInMinutes() { + return (getRangeInMilliseconds() / DateConstants.MINUTEINMILLIS); + } + + /** + * Get the amount of minutes for the event on a specific day. This is useful + * if the event spans several days. + * + * @param targetDay + * The date to check + * @return + */ + public long getRangeInMinutesForDay(Date targetDay) { + if (isTimeOnDifferentDays()) { + // Time range is on different days. Calculate the second day's + // range. + long range = (getEndTime().getTime() - getEnd().getTime()) + / DateConstants.MINUTEINMILLIS; + + if (getEnd().compareTo(targetDay) != 0) { + // Calculate first day's range. + return getRangeInMinutes() - range; + } + + return range; + } else { + return getRangeInMinutes(); + } + } + + /** + * Does the event span several days + * + * @return + */ + @SuppressWarnings("deprecation") + public boolean isTimeOnDifferentDays() { + if (getEndTime().getTime() - getStart().getTime() > DateConstants.DAYINMILLIS) { + return true; + } + + if (getStart().compareTo(getEnd()) != 0) { + if (getEndTime().getHours() == 0 && getEndTime().getMinutes() == 0) { + return false; + } + return true; + } + return false; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DateCell.java b/client/src/com/vaadin/client/ui/calendar/schedule/DateCell.java new file mode 100644 index 0000000000..516447153e --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/DateCell.java @@ -0,0 +1,810 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +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.Display; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.event.dom.client.ContextMenuHandler; +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.dom.client.MouseMoveEvent; +import com.google.gwt.event.dom.client.MouseMoveHandler; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; +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.Widget; +import com.vaadin.client.Util; + +public class DateCell extends FocusableComplexPanel implements + MouseDownHandler, MouseMoveHandler, MouseUpHandler, KeyDownHandler, + ContextMenuHandler { + private static final String DRAGEMPHASISSTYLE = " dragemphasis"; + private Date date; + private int width; + private int eventRangeStart = -1; + private int eventRangeStop = -1; + final WeekGrid weekgrid; + private boolean disabled = false; + private int height; + private final Element[] slotElements; + private final List<DateCellSlot> slots = new ArrayList<DateCell.DateCellSlot>(); + private int[] slotElementHeights; + private int startingSlotHeight; + private Date today; + private Element todaybar; + private final List<HandlerRegistration> handlers; + private final int numberOfSlots; + private final int firstHour; + private final int lastHour; + + public class DateCellSlot extends Widget { + + private final DateCell cell; + + private final Date from; + + private final Date to; + + public DateCellSlot(DateCell cell, Date from, Date to) { + setElement(DOM.createDiv()); + getElement().setInnerHTML(" "); + this.cell = cell; + this.from = from; + this.to = to; + } + + public Date getFrom() { + return from; + } + + public Date getTo() { + return to; + } + + public DateCell getParentCell() { + return cell; + } + } + + public DateCell(WeekGrid parent, Date date) { + weekgrid = parent; + Element mainElement = DOM.createDiv(); + setElement(mainElement); + makeFocusable(); + setDate(date); + + addStyleName("v-calendar-day-times"); + + handlers = new LinkedList<HandlerRegistration>(); + + // 2 slots / hour + firstHour = weekgrid.getFirstHour(); + lastHour = weekgrid.getLastHour(); + numberOfSlots = (lastHour - firstHour + 1) * 2; + long slotTime = Math.round(((lastHour - firstHour + 1) * 3600000.0) + / numberOfSlots); + + slotElements = new Element[numberOfSlots]; + slotElementHeights = new int[numberOfSlots]; + + slots.clear(); + long start = getDate().getTime() + firstHour * 3600000; + long end = start + slotTime; + for (int i = 0; i < numberOfSlots; i++) { + DateCellSlot slot = new DateCellSlot(this, new Date(start), + new Date(end)); + if (i % 2 == 0) { + slot.setStyleName("v-datecellslot-even"); + } else { + slot.setStyleName("v-datecellslot"); + } + Event.sinkEvents(slot.getElement(), Event.MOUSEEVENTS); + mainElement.appendChild(slot.getElement()); + slotElements[i] = slot.getElement(); + slots.add(slot); + start = end; + end = start + slotTime; + } + + // Sink events for tooltip handling + Event.sinkEvents(mainElement, Event.MOUSEEVENTS); + } + + public int getFirstHour() { + return firstHour; + } + + public int getLastHour() { + return lastHour; + } + + @Override + protected void onAttach() { + super.onAttach(); + + handlers.add(addHandler(this, MouseDownEvent.getType())); + handlers.add(addHandler(this, MouseUpEvent.getType())); + handlers.add(addHandler(this, MouseMoveEvent.getType())); + handlers.add(addDomHandler(this, ContextMenuEvent.getType())); + handlers.add(addKeyDownHandler(this)); + } + + @Override + protected void onDetach() { + for (HandlerRegistration handler : handlers) { + handler.removeHandler(); + } + handlers.clear(); + + super.onDetach(); + } + + public int getSlotIndex(Element slotElement) { + for (int i = 0; i < slotElements.length; i++) { + if (slotElement == slotElements[i]) { + return i; + } + } + + throw new IllegalArgumentException("Element not found in this DateCell"); + } + + public DateCellSlot getSlot(int index) { + return slots.get(index); + } + + public int getNumberOfSlots() { + return numberOfSlots; + } + + public void setTimeBarWidth(int timebarWidth) { + todaybar.getStyle().setWidth(timebarWidth, Unit.PX); + } + + /** + * @param isHorizontalSized + * if true, this DateCell is sized with CSS and not via + * {@link #setWidthPX(int)} + */ + public void setHorizontalSized(boolean isHorizontalSized) { + if (isHorizontalSized) { + addStyleDependentName("Hsized"); + + width = getOffsetWidth() + - Util.measureHorizontalBorder(getElement()); + recalculateEventWidths(); + } else { + removeStyleDependentName("Hsized"); + } + } + + /** + * @param isVerticalSized + * if true, this DateCell is sized with CSS and not via + * {@link #setHeightPX(int)} + */ + public void setVerticalSized(boolean isVerticalSized) { + if (isVerticalSized) { + addStyleDependentName("Vsized"); + + // recalc heights&size for events. all other height sizes come + // from css + startingSlotHeight = slotElements[0].getOffsetHeight(); + recalculateEventPositions(); + + if (isToday()) { + recalculateTimeBarPosition(); + } + + } else { + removeStyleDependentName("Vsized"); + } + } + + public void setDate(Date date) { + this.date = date; + } + + public void setWidthPX(int cellWidth) { + width = cellWidth; + setWidth(cellWidth + "px"); + recalculateEventWidths(); + } + + public void setHeightPX(int height, int[] cellHeights) { + this.height = height; + slotElementHeights = cellHeights; + setHeight(height + "px"); + recalculateCellHeights(); + recalculateEventPositions(); + if (today != null) { + recalculateTimeBarPosition(); + } + } + + // date methods are not deprecated in GWT + @SuppressWarnings("deprecation") + private void recalculateTimeBarPosition() { + int h = today.getHours(); + int m = today.getMinutes(); + if (h >= firstHour && h <= lastHour) { + int pixelTop = weekgrid.getPixelTopFor(m + 60 * h); + todaybar.getStyle().clearDisplay(); + todaybar.getStyle().setTop(pixelTop, Unit.PX); + } else { + todaybar.getStyle().setDisplay(Display.NONE); + } + } + + private void recalculateEventPositions() { + for (int i = 0; i < getWidgetCount(); i++) { + DateCellDayEvent dayEvent = (DateCellDayEvent) getWidget(i); + updatePositionFor(dayEvent, getDate(), dayEvent.getCalendarEvent()); + } + } + + public void recalculateEventWidths() { + List<DateCellGroup> groups = new ArrayList<DateCellGroup>(); + + int count = getWidgetCount(); + + List<Integer> handled = new ArrayList<Integer>(); + + // Iterate through all events and group them. Events that overlaps + // with each other, are added to the same group. + for (int i = 0; i < count; i++) { + if (handled.contains(i)) { + continue; + } + + DateCellGroup curGroup = getOverlappingEvents(i); + handled.addAll(curGroup.getItems()); + + boolean newGroup = true; + // No need to check other groups, if size equals the count + if (curGroup.getItems().size() != count) { + // Check other groups. When the whole group overlaps with + // other group, the group is merged to the other. + for (DateCellGroup g : groups) { + + if (WeekGridMinuteTimeRange.doesOverlap( + curGroup.getDateRange(), g.getDateRange())) { + newGroup = false; + updateGroup(g, curGroup); + } + } + } else { + if (newGroup) { + groups.add(curGroup); + } + break; + } + + if (newGroup) { + groups.add(curGroup); + } + } + + drawDayEvents(groups); + } + + private void recalculateCellHeights() { + startingSlotHeight = height / numberOfSlots; + + for (int i = 0; i < slotElements.length; i++) { + slotElements[i].getStyle() + .setHeight(slotElementHeights[i], Unit.PX); + } + + Iterator<Widget> it = iterator(); + while (it.hasNext()) { + Widget child = it.next(); + if (child instanceof DateCellDayEvent) { + ((DateCellDayEvent) child).setSlotHeightInPX(getSlotHeight()); + } + + } + } + + public int getSlotHeight() { + return startingSlotHeight; + } + + public int getSlotBorder() { + return Util + .measureVerticalBorder((com.google.gwt.user.client.Element) slotElements[0]); + } + + private void drawDayEvents(List<DateCellGroup> groups) { + for (DateCellGroup g : groups) { + int col = 0; + int colCount = 0; + List<Integer> order = new ArrayList<Integer>(); + Map<Integer, Integer> columns = new HashMap<Integer, Integer>(); + for (Integer eventIndex : g.getItems()) { + DateCellDayEvent d = (DateCellDayEvent) getWidget(eventIndex); + d.setMoveWidth(width); + + int freeSpaceCol = findFreeColumnSpaceOnLeft( + new WeekGridMinuteTimeRange(d.getCalendarEvent() + .getStartTime(), d.getCalendarEvent() + .getEndTime()), order, columns); + if (freeSpaceCol >= 0) { + col = freeSpaceCol; + columns.put(eventIndex, col); + int newOrderindex = 0; + for (Integer i : order) { + if (columns.get(i) >= col) { + newOrderindex = order.indexOf(i); + break; + } + } + order.add(newOrderindex, eventIndex); + } else { + // New column + col = colCount++; + columns.put(eventIndex, col); + order.add(eventIndex); + } + } + + // Update widths and left position + int eventWidth = (width / colCount); + for (Integer index : g.getItems()) { + DateCellDayEvent d = (DateCellDayEvent) getWidget(index); + d.getElement() + .getStyle() + .setMarginLeft((eventWidth * columns.get(index)), + Unit.PX); + d.setWidth(eventWidth + "px"); + d.setSlotHeightInPX(getSlotHeight()); + } + } + } + + private int findFreeColumnSpaceOnLeft(WeekGridMinuteTimeRange dateRange, + List<Integer> order, Map<Integer, Integer> columns) { + int freeSpot = -1; + int skipIndex = -1; + for (Integer eventIndex : order) { + int col = columns.get(eventIndex); + if (col == skipIndex) { + continue; + } + + if (freeSpot != -1 && freeSpot != col) { + // Free spot found + return freeSpot; + } + + DateCellDayEvent d = (DateCellDayEvent) getWidget(eventIndex); + WeekGridMinuteTimeRange nextRange = new WeekGridMinuteTimeRange(d + .getCalendarEvent().getStartTime(), d.getCalendarEvent() + .getEndTime()); + + if (WeekGridMinuteTimeRange.doesOverlap(dateRange, nextRange)) { + skipIndex = col; + freeSpot = -1; + } else { + freeSpot = col; + } + } + + return freeSpot; + } + + /* Update top and bottom date range values. Add new index to the group. */ + private void updateGroup(DateCellGroup targetGroup, DateCellGroup byGroup) { + Date newStart = targetGroup.getStart(); + Date newEnd = targetGroup.getEnd(); + if (byGroup.getStart().before(targetGroup.getStart())) { + newStart = byGroup.getEnd(); + } + if (byGroup.getStart().after(targetGroup.getEnd())) { + newStart = byGroup.getStart(); + } + + targetGroup.setDateRange(new WeekGridMinuteTimeRange(newStart, newEnd)); + + for (Integer index : byGroup.getItems()) { + if (!targetGroup.getItems().contains(index)) { + targetGroup.add(index); + } + } + } + + /** + * Returns all overlapping DayEvent indexes in the Group. Including the + * target. + * + * @param targetIndex + * Index of DayEvent in the current DateCell widget. + * @return Group that contains all Overlapping DayEvent indexes + */ + public DateCellGroup getOverlappingEvents(int targetIndex) { + DateCellGroup g = new DateCellGroup(targetIndex); + + int count = getWidgetCount(); + DateCellDayEvent target = (DateCellDayEvent) getWidget(targetIndex); + WeekGridMinuteTimeRange targetRange = new WeekGridMinuteTimeRange( + target.getCalendarEvent().getStartTime(), target + .getCalendarEvent().getEndTime()); + Date groupStart = targetRange.getStart(); + Date groupEnd = targetRange.getEnd(); + + for (int i = 0; i < count; i++) { + if (targetIndex == i) { + continue; + } + + DateCellDayEvent d = (DateCellDayEvent) getWidget(i); + WeekGridMinuteTimeRange nextRange = new WeekGridMinuteTimeRange(d + .getCalendarEvent().getStartTime(), d.getCalendarEvent() + .getEndTime()); + if (WeekGridMinuteTimeRange.doesOverlap(targetRange, nextRange)) { + g.add(i); + + // Update top & bottom values to the greatest + if (nextRange.getStart().before(targetRange.getStart())) { + groupStart = targetRange.getStart(); + } + if (nextRange.getEnd().after(targetRange.getEnd())) { + groupEnd = targetRange.getEnd(); + } + } + } + + g.setDateRange(new WeekGridMinuteTimeRange(groupStart, groupEnd)); + return g; + } + + public Date getDate() { + return date; + } + + public void addEvent(Date targetDay, CalendarEvent calendarEvent) { + Element main = getElement(); + DateCellDayEvent dayEvent = new DateCellDayEvent(this, weekgrid, + calendarEvent); + dayEvent.setSlotHeightInPX(getSlotHeight()); + dayEvent.setDisabled(isDisabled()); + + if (startingSlotHeight > 0) { + updatePositionFor(dayEvent, targetDay, calendarEvent); + } + + add(dayEvent, (com.google.gwt.user.client.Element) main); + } + + // date methods are not deprecated in GWT + @SuppressWarnings("deprecation") + private void updatePositionFor(DateCellDayEvent dayEvent, Date targetDay, + CalendarEvent calendarEvent) { + if (canDisplay(calendarEvent)) { + + dayEvent.getElement().getStyle().clearDisplay(); + + Date fromDt = calendarEvent.getStartTime(); + int h = fromDt.getHours(); + int m = fromDt.getMinutes(); + long range = calendarEvent.getRangeInMinutesForDay(targetDay); + + boolean onDifferentDays = calendarEvent.isTimeOnDifferentDays(); + if (onDifferentDays) { + if (calendarEvent.getStart().compareTo(targetDay) != 0) { + // Current day slot is for the end date. Lets fix also + // the + // start & end times. + h = 0; + m = 0; + } + } + + int startFromMinutes = (h * 60) + m; + dayEvent.updatePosition(startFromMinutes, range); + + } else { + dayEvent.getElement().getStyle().setDisplay(Display.NONE); + } + } + + public void addEvent(DateCellDayEvent dayEvent) { + Element main = getElement(); + int index = 0; + List<CalendarEvent> events = new ArrayList<CalendarEvent>(); + + // events are the only widgets in this panel + // slots are just elements + for (; index < getWidgetCount(); index++) { + DateCellDayEvent dc = (DateCellDayEvent) getWidget(index); + dc.setDisabled(isDisabled()); + events.add(dc.getCalendarEvent()); + } + events.add(dayEvent.getCalendarEvent()); + + index = 0; + for (CalendarEvent e : weekgrid.getCalendar().sortEventsByDuration( + events)) { + if (e.equals(dayEvent.getCalendarEvent())) { + break; + } + index++; + } + this.insert(dayEvent, (com.google.gwt.user.client.Element) main, index, + true); + } + + public void removeEvent(DateCellDayEvent dayEvent) { + remove(dayEvent); + } + + /** + * + * @param event + * @return + */ + // Date methods not deprecated in GWT + @SuppressWarnings("deprecation") + private boolean canDisplay(CalendarEvent event) { + Date eventStart = event.getStartTime(); + Date eventEnd = event.getEndTime(); + + int eventStartHours = eventStart.getHours(); + int eventEndHours = eventEnd.getHours(); + + return (eventStartHours <= lastHour) && (eventEndHours >= firstHour); + } + + @Override + public void onKeyDown(KeyDownEvent event) { + int keycode = event.getNativeEvent().getKeyCode(); + if (keycode == KeyCodes.KEY_ESCAPE && eventRangeStart > -1) { + cancelRangeSelect(); + } + } + + @Override + public void onMouseDown(MouseDownEvent event) { + if (event.getNativeButton() == NativeEvent.BUTTON_LEFT) { + Element e = Element.as(event.getNativeEvent().getEventTarget()); + if (e.getClassName().contains("reserved") || isDisabled() + || !weekgrid.getParentCalendar().isRangeSelectAllowed()) { + eventRangeStart = -1; + } else { + eventRangeStart = event.getY(); + eventRangeStop = eventRangeStart; + Event.setCapture(getElement()); + setFocus(true); + } + } + } + + @Override + @SuppressWarnings("deprecation") + public void onMouseUp(MouseUpEvent event) { + if (event.getNativeButton() != NativeEvent.BUTTON_LEFT) { + return; + } + Event.releaseCapture(getElement()); + setFocus(false); + int dragDistance = Math.abs(eventRangeStart - event.getY()); + if (dragDistance > 0 && eventRangeStart >= 0) { + Element main = getElement(); + if (eventRangeStart > eventRangeStop) { + if (eventRangeStop <= -1) { + eventRangeStop = 0; + } + int temp = eventRangeStart; + eventRangeStart = eventRangeStop; + eventRangeStop = temp; + } + + NodeList<Node> nodes = main.getChildNodes(); + + int slotStart = -1; + int slotEnd = -1; + + // iterate over all child nodes, until we find first the start, + // and then the end + for (int i = 0; i < nodes.getLength(); i++) { + Element element = (Element) nodes.getItem(i); + boolean isRangeElement = element.getClassName().contains( + "v-daterange"); + + if (isRangeElement && slotStart == -1) { + slotStart = i; + slotEnd = i; // to catch one-slot selections + + } else if (isRangeElement) { + slotEnd = i; + + } else if (slotStart != -1 && slotEnd != -1) { + break; + } + } + + clearSelectionRange(); + + int startMinutes = firstHour * 60 + slotStart * 30; + int endMinutes = (firstHour * 60) + (slotEnd + 1) * 30; + Date currentDate = getDate(); + String yr = (currentDate.getYear() + 1900) + "-" + + (currentDate.getMonth() + 1) + "-" + + currentDate.getDate(); + if (weekgrid.getCalendar().getRangeSelectListener() != null) { + weekgrid.getCalendar() + .getRangeSelectListener() + .rangeSelected( + yr + ":" + startMinutes + ":" + endMinutes); + } + eventRangeStart = -1; + } else { + // Click event + eventRangeStart = -1; + cancelRangeSelect(); + + } + } + + @Override + public void onMouseMove(MouseMoveEvent event) { + if (event.getNativeButton() != NativeEvent.BUTTON_LEFT) { + return; + } + + if (eventRangeStart >= 0) { + int newY = event.getY(); + int fromY = 0; + int toY = 0; + if (newY < eventRangeStart) { + fromY = newY; + toY = eventRangeStart; + } else { + fromY = eventRangeStart; + toY = newY; + } + Element main = getElement(); + eventRangeStop = newY; + NodeList<Node> nodes = main.getChildNodes(); + for (int i = 0; i < nodes.getLength(); i++) { + Element c = (Element) nodes.getItem(i); + + if (todaybar != c) { + + int elemStart = c.getOffsetTop(); + int elemStop = elemStart + getSlotHeight(); + if (elemStart >= fromY && elemStart <= toY) { + c.addClassName("v-daterange"); + } else if (elemStop >= fromY && elemStop <= toY) { + c.addClassName("v-daterange"); + } else if (elemStop >= fromY && elemStart <= toY) { + c.addClassName("v-daterange"); + } else { + c.removeClassName("v-daterange"); + } + } + } + } + + event.preventDefault(); + } + + public void cancelRangeSelect() { + Event.releaseCapture(getElement()); + setFocus(false); + + clearSelectionRange(); + } + + private void clearSelectionRange() { + if (eventRangeStart > -1) { + // clear all "selected" class names + Element main = getElement(); + NodeList<Node> nodes = main.getChildNodes(); + + for (int i = 0; i <= 47; i++) { + Element c = (Element) nodes.getItem(i); + if (c == null) { + continue; + } + c.removeClassName("v-daterange"); + } + + eventRangeStart = -1; + } + } + + public void setToday(Date today, int width) { + this.today = today; + addStyleDependentName("today"); + Element lastChild = (Element) getElement().getLastChild(); + if (lastChild.getClassName().equals("v-calendar-current-time")) { + todaybar = lastChild; + } else { + todaybar = DOM.createDiv(); + todaybar.setClassName("v-calendar-current-time"); + getElement().appendChild(todaybar); + } + + if (width != -1) { + todaybar.getStyle().setWidth(width, Unit.PX); + } + + // position is calculated later, when we know the cell heights + } + + public Element getTodaybarElement() { + return todaybar; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public boolean isDisabled() { + return disabled; + } + + public void setDateColor(String styleName) { + this.setStyleName("v-calendar-datecell " + styleName); + } + + public boolean isToday() { + return today != null; + } + + public void addEmphasisStyle(com.google.gwt.user.client.Element elementOver) { + String originalStylename = getStyleName(elementOver); + setStyleName(elementOver, originalStylename + DRAGEMPHASISSTYLE); + } + + public void removeEmphasisStyle( + com.google.gwt.user.client.Element elementOver) { + String originalStylename = getStyleName(elementOver); + setStyleName( + elementOver, + originalStylename.substring(0, originalStylename.length() + - DRAGEMPHASISSTYLE.length())); + } + + @Override + public void onContextMenu(ContextMenuEvent event) { + if (weekgrid.getCalendar().getMouseEventListener() != null) { + event.preventDefault(); + event.stopPropagation(); + weekgrid.getCalendar().getMouseEventListener() + .contextMenu(event, DateCell.this); + } + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java new file mode 100644 index 0000000000..04e6bb7df6 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java @@ -0,0 +1,117 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Date; + +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.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.Util; +import com.vaadin.client.ui.VCalendar; + +/** + * Internally used class by the Calendar + * + * since 7.1 + */ +public class DateCellContainer extends FlowPanel implements MouseDownHandler, + MouseUpHandler { + + private Date date; + + private Widget clickTargetWidget; + + private VCalendar calendar; + + private static int borderWidth = -1; + + public DateCellContainer() { + setStylePrimaryName("v-calendar-datecell"); + } + + public static int measureBorderWidth(DateCellContainer dc) { + if (borderWidth == -1) { + borderWidth = Util.measureHorizontalBorder(dc.getElement()); + } + return borderWidth; + } + + public void setCalendar(VCalendar calendar) { + this.calendar = calendar; + } + + public void setDate(Date date) { + this.date = date; + } + + public Date getDate() { + return date; + } + + public boolean hasEvent(int slotIndex) { + return hasDateCell(slotIndex) + && ((WeeklyLongEventsDateCell) getChildren().get(slotIndex)) + .getEvent() != null; + } + + public boolean hasDateCell(int slotIndex) { + return (getChildren().size() - 1) >= slotIndex; + } + + public WeeklyLongEventsDateCell getDateCell(int slotIndex) { + if (!hasDateCell(slotIndex)) { + addEmptyEventCells(slotIndex - (getChildren().size() - 1)); + } + return (WeeklyLongEventsDateCell) getChildren().get(slotIndex); + } + + public void addEmptyEventCells(int eventCount) { + for (int i = 0; i < eventCount; i++) { + addEmptyEventCell(); + } + } + + public void addEmptyEventCell() { + WeeklyLongEventsDateCell dateCell = new WeeklyLongEventsDateCell(); + dateCell.addMouseDownHandler(this); + dateCell.addMouseUpHandler(this); + add(dateCell); + } + + @Override + public void onMouseDown(MouseDownEvent event) { + clickTargetWidget = (Widget) event.getSource(); + + event.stopPropagation(); + } + + @Override + public void onMouseUp(MouseUpEvent event) { + if (event.getSource() == clickTargetWidget + && clickTargetWidget instanceof WeeklyLongEventsDateCell + && !calendar.isDisabledOrReadOnly()) { + CalendarEvent calendarEvent = ((WeeklyLongEventsDateCell) clickTargetWidget) + .getEvent(); + if (calendar.getEventClickListener() != null) { + calendar.getEventClickListener().eventClick(calendarEvent); + } + } + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java new file mode 100644 index 0000000000..c56566bf25 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java @@ -0,0 +1,639 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import com.google.gwt.core.client.GWT; +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.Style; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.event.dom.client.ContextMenuHandler; +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.dom.client.MouseMoveEvent; +import com.google.gwt.event.dom.client.MouseMoveHandler; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; +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.HorizontalPanel; +import com.vaadin.client.Util; +import com.vaadin.client.ui.VCalendar; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * Internally used by the calendar + * + * @since 7.1 + */ +public class DateCellDayEvent extends FocusableHTML implements + MouseDownHandler, MouseUpHandler, MouseMoveHandler, KeyDownHandler, + ContextMenuHandler, HasTooltipKey { + + private final DateCell dateCell; + private Element caption = null; + private final Element eventContent; + private CalendarEvent calendarEvent = null; + private HandlerRegistration moveRegistration; + private int startY = -1; + private int startX = -1; + private String moveWidth; + public static final int halfHourInMilliSeconds = 1800 * 1000; + private Date startDatetimeFrom; + private Date startDatetimeTo; + private boolean mouseMoveStarted; + private int top; + private int startYrelative; + private int startXrelative; + private boolean disabled; + private final WeekGrid weekGrid; + private com.google.gwt.user.client.Element topResizeBar; + private com.google.gwt.user.client.Element bottomResizeBar; + private Element clickTarget; + private final Integer eventIndex; + private int slotHeight; + private final List<HandlerRegistration> handlers; + private boolean mouseMoveCanceled; + + public DateCellDayEvent(DateCell dateCell, WeekGrid parent, + CalendarEvent event) { + super(); + this.dateCell = dateCell; + + handlers = new LinkedList<HandlerRegistration>(); + + setStylePrimaryName("v-calendar-event"); + setCalendarEvent(event); + + weekGrid = parent; + + Style s = getElement().getStyle(); + if (event.getStyleName().length() > 0) { + addStyleDependentName(event.getStyleName()); + } + s.setPosition(Position.ABSOLUTE); + + caption = DOM.createDiv(); + caption.addClassName("v-calendar-event-caption"); + getElement().appendChild(caption); + + eventContent = DOM.createDiv(); + eventContent.addClassName("v-calendar-event-content"); + getElement().appendChild(eventContent); + + VCalendar calendar = weekGrid.getCalendar(); + if (weekGrid.getCalendar().isEventResizeAllowed()) { + topResizeBar = DOM.createDiv(); + bottomResizeBar = DOM.createDiv(); + + topResizeBar.addClassName("v-calendar-event-resizetop"); + bottomResizeBar.addClassName("v-calendar-event-resizebottom"); + + getElement().appendChild(topResizeBar); + getElement().appendChild(bottomResizeBar); + } + + eventIndex = event.getIndex(); + } + + @Override + protected void onAttach() { + super.onAttach(); + handlers.add(addMouseDownHandler(this)); + handlers.add(addMouseUpHandler(this)); + handlers.add(addKeyDownHandler(this)); + handlers.add(addDomHandler(this, ContextMenuEvent.getType())); + } + + @Override + protected void onDetach() { + for (HandlerRegistration handler : handlers) { + handler.removeHandler(); + } + handlers.clear(); + super.onDetach(); + } + + public void setSlotHeightInPX(int slotHeight) { + this.slotHeight = slotHeight; + } + + public void updatePosition(long startFromMinutes, long durationInMinutes) { + if (startFromMinutes < 0) { + startFromMinutes = 0; + } + top = weekGrid.getPixelTopFor((int) startFromMinutes); + + getElement().getStyle().setTop(top, Unit.PX); + if (durationInMinutes > 0) { + int heightMinutes = weekGrid.getPixelLengthFor( + (int) startFromMinutes, (int) durationInMinutes); + setHeight(heightMinutes); + } else { + setHeight(-1); + } + + boolean multiRowCaption = (durationInMinutes > 30); + updateCaptions(multiRowCaption); + } + + public int getTop() { + return top; + } + + public void setMoveWidth(int width) { + moveWidth = width + "px"; + } + + public void setHeight(int h) { + if (h == -1) { + getElement().getStyle().setProperty("height", ""); + eventContent.getStyle().setProperty("height", ""); + } else { + getElement().getStyle().setHeight(h, Unit.PX); + // FIXME measure the border height (2px) from the DOM + eventContent.getStyle().setHeight(h - 2, Unit.PX); + } + } + + /** + * @param bigMode + * If false, event is so small that caption must be in time-row + */ + private void updateCaptions(boolean bigMode) { + String separator = bigMode ? "<br />" : ": "; + caption.setInnerHTML("<span>" + calendarEvent.getTimeAsText() + + "</span>" + separator + + Util.escapeHTML(calendarEvent.getCaption())); + eventContent.setInnerHTML(""); + } + + @Override + public void onKeyDown(KeyDownEvent event) { + int keycode = event.getNativeEvent().getKeyCode(); + if (keycode == KeyCodes.KEY_ESCAPE && mouseMoveStarted) { + cancelMouseMove(); + } + } + + @Override + public void onMouseDown(MouseDownEvent event) { + startX = event.getClientX(); + startY = event.getClientY(); + if (isDisabled() || event.getNativeButton() != NativeEvent.BUTTON_LEFT) { + return; + } + + clickTarget = Element.as(event.getNativeEvent().getEventTarget()); + mouseMoveCanceled = false; + + if (weekGrid.getCalendar().isEventMoveAllowed() || clickTargetsResize()) { + moveRegistration = addMouseMoveHandler(this); + setFocus(true); + try { + startYrelative = (int) ((double) event.getRelativeY(caption) % slotHeight); + startXrelative = (event.getRelativeX(weekGrid.getElement()) - weekGrid.timebar + .getOffsetWidth()) % getDateCellWidth(); + } catch (Exception e) { + GWT.log("Exception calculating relative start position", e); + } + mouseMoveStarted = false; + Style s = getElement().getStyle(); + s.setZIndex(1000); + startDatetimeFrom = (Date) calendarEvent.getStartTime().clone(); + startDatetimeTo = (Date) calendarEvent.getEndTime().clone(); + Event.setCapture(getElement()); + } + + // make sure the right cursor is always displayed + if (clickTargetsResize()) { + addGlobalResizeStyle(); + } + + /* + * We need to stop the event propagation or else the WeekGrid range + * select will kick in + */ + event.stopPropagation(); + event.preventDefault(); + } + + @Override + public void onMouseUp(MouseUpEvent event) { + if (mouseMoveCanceled) { + return; + } + + Event.releaseCapture(getElement()); + setFocus(false); + if (moveRegistration != null) { + moveRegistration.removeHandler(); + moveRegistration = null; + } + int endX = event.getClientX(); + int endY = event.getClientY(); + int xDiff = startX - endX; + int yDiff = startY - endY; + startX = -1; + startY = -1; + mouseMoveStarted = false; + Style s = getElement().getStyle(); + s.setZIndex(1); + if (!clickTargetsResize()) { + // check if mouse has moved over threshold of 3 pixels + boolean mouseMoved = (xDiff < -3 || xDiff > 3 || yDiff < -3 || yDiff > 3); + + if (!weekGrid.getCalendar().isDisabledOrReadOnly() && mouseMoved) { + // Event Move: + // - calendar must be enabled + // - calendar must not be in read-only mode + weekGrid.eventMoved(this); + } else if (!weekGrid.getCalendar().isDisabled()) { + // Event Click: + // - calendar must be enabled (read-only is allowed) + EventTarget et = event.getNativeEvent().getEventTarget(); + Element e = Element.as(et); + if (e == caption || e == eventContent + || e.getParentElement() == caption) { + if (weekGrid.getCalendar().getEventClickListener() != null) { + weekGrid.getCalendar().getEventClickListener() + .eventClick(calendarEvent); + } + } + } + + } else { // click targeted resize bar + removeGlobalResizeStyle(); + if (weekGrid.getCalendar().getEventResizeListener() != null) { + weekGrid.getCalendar().getEventResizeListener() + .eventResized(calendarEvent); + } + } + } + + @Override + @SuppressWarnings("deprecation") + public void onMouseMove(MouseMoveEvent event) { + if (startY < 0 && startX < 0) { + return; + } + if (isDisabled()) { + Event.releaseCapture(getElement()); + mouseMoveStarted = false; + startY = -1; + startX = -1; + removeGlobalResizeStyle(); + return; + } + int currentY = event.getClientY(); + int currentX = event.getClientX(); + int moveY = (currentY - startY); + int moveX = (currentX - startX); + if ((moveY < 5 && moveY > -6) && (moveX < 5 && moveX > -6)) { + return; + } + if (!mouseMoveStarted) { + setWidth(moveWidth); + getElement().getStyle().setMarginLeft(0, Unit.PX); + mouseMoveStarted = true; + } + + HorizontalPanel parent = (HorizontalPanel) getParent().getParent(); + int relativeX = event.getRelativeX(parent.getElement()) + - weekGrid.timebar.getOffsetWidth(); + int halfHourDiff = 0; + if (moveY > 0) { + halfHourDiff = (startYrelative + moveY) / slotHeight; + } else { + halfHourDiff = (moveY - startYrelative) / slotHeight; + } + + int dateCellWidth = getDateCellWidth(); + long dayDiff = 0; + if (moveX >= 0) { + dayDiff = (startXrelative + moveX) / dateCellWidth; + } else { + dayDiff = (moveX - (dateCellWidth - startXrelative)) + / dateCellWidth; + } + + int dayOffset = relativeX / dateCellWidth; + + // sanity check for right side overflow + int dateCellCount = weekGrid.getDateCellCount(); + if (dayOffset >= dateCellCount) { + dayOffset--; + dayDiff--; + } + + int dayOffsetPx = calculateDateCellOffsetPx(dayOffset) + + weekGrid.timebar.getOffsetWidth(); + + GWT.log("DateCellWidth: " + dateCellWidth + " dayDiff: " + dayDiff + + " dayOffset: " + dayOffset + " dayOffsetPx: " + dayOffsetPx + + " startXrelative: " + startXrelative + " moveX: " + moveX); + + if (relativeX < 0 || relativeX >= getDatesWidth()) { + return; + } + + Style s = getElement().getStyle(); + + Date from = calendarEvent.getStartTime(); + Date to = calendarEvent.getEndTime(); + long duration = to.getTime() - from.getTime(); + + if (!clickTargetsResize() + && weekGrid.getCalendar().isEventMoveAllowed()) { + long daysMs = dayDiff * DateConstants.DAYINMILLIS; + from.setTime(startDatetimeFrom.getTime() + daysMs); + from.setTime(from.getTime() + + ((long) halfHourInMilliSeconds * halfHourDiff)); + to.setTime((from.getTime() + duration)); + + calendarEvent.setStartTime(from); + calendarEvent.setEndTime(to); + calendarEvent.setStart(new Date(from.getTime())); + calendarEvent.setEnd(new Date(to.getTime())); + + // Set new position for the event + long startFromMinutes = (from.getHours() * 60) + from.getMinutes(); + long range = calendarEvent.getRangeInMinutes(); + startFromMinutes = calculateStartFromMinute(startFromMinutes, from, + to, dayOffsetPx); + if (startFromMinutes < 0) { + range += startFromMinutes; + } + updatePosition(startFromMinutes, range); + + s.setLeft(dayOffsetPx, Unit.PX); + + if (weekGrid.getDateCellWidths() != null) { + s.setWidth(weekGrid.getDateCellWidths()[dayOffset], Unit.PX); + } else { + setWidth(moveWidth); + } + + } else if (clickTarget == topResizeBar) { + long oldStartTime = startDatetimeFrom.getTime(); + long newStartTime = oldStartTime + + ((long) halfHourInMilliSeconds * halfHourDiff); + + if (!isTimeRangeTooSmall(newStartTime, startDatetimeTo.getTime())) { + newStartTime = startDatetimeTo.getTime() - getMinTimeRange(); + } + + from.setTime(newStartTime); + + calendarEvent.setStartTime(from); + calendarEvent.setStart(new Date(from.getTime())); + + // Set new position for the event + long startFromMinutes = (from.getHours() * 60) + from.getMinutes(); + long range = calendarEvent.getRangeInMinutes(); + + updatePosition(startFromMinutes, range); + + } else if (clickTarget == bottomResizeBar) { + long oldEndTime = startDatetimeTo.getTime(); + long newEndTime = oldEndTime + + ((long) halfHourInMilliSeconds * halfHourDiff); + + if (!isTimeRangeTooSmall(startDatetimeFrom.getTime(), newEndTime)) { + newEndTime = startDatetimeFrom.getTime() + getMinTimeRange(); + } + + to.setTime(newEndTime); + + calendarEvent.setEndTime(to); + calendarEvent.setEnd(new Date(to.getTime())); + + // Set new position for the event + long startFromMinutes = (startDatetimeFrom.getHours() * 60) + + startDatetimeFrom.getMinutes(); + long range = calendarEvent.getRangeInMinutes(); + startFromMinutes = calculateStartFromMinute(startFromMinutes, from, + to, dayOffsetPx); + if (startFromMinutes < 0) { + range += startFromMinutes; + } + updatePosition(startFromMinutes, range); + } + } + + private void cancelMouseMove() { + mouseMoveCanceled = true; + + // reset and remove everything related to the event handling + Event.releaseCapture(getElement()); + setFocus(false); + + if (moveRegistration != null) { + moveRegistration.removeHandler(); + moveRegistration = null; + } + + mouseMoveStarted = false; + removeGlobalResizeStyle(); + + Style s = getElement().getStyle(); + s.setZIndex(1); + + // reset the position of the event + int dateCellWidth = getDateCellWidth(); + int dayOffset = startXrelative / dateCellWidth; + s.clearLeft(); + + calendarEvent.setStartTime(startDatetimeFrom); + calendarEvent.setEndTime(startDatetimeTo); + + long startFromMinutes = (startDatetimeFrom.getHours() * 60) + + startDatetimeFrom.getMinutes(); + long range = calendarEvent.getRangeInMinutes(); + + startFromMinutes = calculateStartFromMinute(startFromMinutes, + startDatetimeFrom, startDatetimeTo, dayOffset); + if (startFromMinutes < 0) { + range += startFromMinutes; + } + + updatePosition(startFromMinutes, range); + + startY = -1; + startX = -1; + + // to reset the event width + ((DateCell) getParent()).recalculateEventWidths(); + } + + // date methods are not deprecated in GWT + @SuppressWarnings("deprecation") + private long calculateStartFromMinute(long startFromMinutes, Date from, + Date to, int dayOffset) { + boolean eventStartAtDifferentDay = from.getDate() != to.getDate(); + if (eventStartAtDifferentDay) { + long minutesOnPrevDay = (getTargetDateByCurrentPosition(dayOffset) + .getTime() - from.getTime()) / DateConstants.MINUTEINMILLIS; + startFromMinutes = -1 * minutesOnPrevDay; + } + + return startFromMinutes; + } + + /** + * @param dateOffset + * @return the amount of pixels the given date is from the left side + */ + private int calculateDateCellOffsetPx(int dateOffset) { + int dateCellOffset = 0; + int[] dateWidths = weekGrid.getDateCellWidths(); + + if (dateWidths != null) { + for (int i = 0; i < dateOffset; i++) { + dateCellOffset += dateWidths[i] + 1; + } + } else { + dateCellOffset = dateOffset * weekGrid.getDateCellWidth(); + } + + return dateCellOffset; + } + + /** + * Check if the given time range is too small for events + * + * @param start + * @param end + * @return + */ + private boolean isTimeRangeTooSmall(long start, long end) { + return (end - start) >= getMinTimeRange(); + } + + /** + * @return the minimum amount of ms that an event must last when resized + */ + private long getMinTimeRange() { + return DateConstants.MINUTEINMILLIS * 30; + } + + /** + * Build the string for sending resize events to server + * + * @param event + * @return + */ + private String buildResizeString(CalendarEvent event) { + StringBuilder buffer = new StringBuilder(); + buffer.append(event.getIndex()); + buffer.append(","); + buffer.append(DateUtil.formatClientSideDate(event.getStart())); + buffer.append("-"); + buffer.append(DateUtil.formatClientSideTime(event.getStartTime())); + buffer.append(","); + buffer.append(DateUtil.formatClientSideDate(event.getEnd())); + buffer.append("-"); + buffer.append(DateUtil.formatClientSideTime(event.getEndTime())); + + return buffer.toString(); + } + + private Date getTargetDateByCurrentPosition(int left) { + DateCell newParent = (DateCell) weekGrid.content + .getWidget((left / getDateCellWidth()) + 1); + Date targetDate = newParent.getDate(); + return targetDate; + } + + private int getDateCellWidth() { + return weekGrid.getDateCellWidth(); + } + + /* Returns total width of all date cells. */ + private int getDatesWidth() { + if (weekGrid.width == -1) { + // Undefined width. Needs to be calculated by the known cell + // widths. + int count = weekGrid.content.getWidgetCount() - 1; + return count * getDateCellWidth(); + } + + return weekGrid.getInternalWidth(); + } + + /** + * @return true if the current mouse movement is resizing + */ + private boolean clickTargetsResize() { + return weekGrid.getCalendar().isEventResizeAllowed() + && (clickTarget == topResizeBar || clickTarget == bottomResizeBar); + } + + private void addGlobalResizeStyle() { + if (clickTarget == topResizeBar) { + weekGrid.getCalendar().addStyleDependentName("nresize"); + } else if (clickTarget == bottomResizeBar) { + weekGrid.getCalendar().addStyleDependentName("sresize"); + } + } + + private void removeGlobalResizeStyle() { + weekGrid.getCalendar().removeStyleDependentName("nresize"); + weekGrid.getCalendar().removeStyleDependentName("sresize"); + } + + public void setCalendarEvent(CalendarEvent calendarEvent) { + this.calendarEvent = calendarEvent; + } + + public CalendarEvent getCalendarEvent() { + return calendarEvent; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public boolean isDisabled() { + return disabled; + } + + @Override + public void onContextMenu(ContextMenuEvent event) { + if (dateCell.weekgrid.getCalendar().getMouseEventListener() != null) { + event.preventDefault(); + event.stopPropagation(); + dateCell.weekgrid.getCalendar().getMouseEventListener() + .contextMenu(event, this); + } + } + + @Override + public Object getTooltipKey() { + return eventIndex; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellGroup.java b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellGroup.java new file mode 100644 index 0000000000..79276eab7b --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellGroup.java @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Internally used by the calendar + * + * @since 7.1 + */ +public class DateCellGroup { + private WeekGridMinuteTimeRange range; + private final List<Integer> items; + + public DateCellGroup(Integer index) { + items = new ArrayList<Integer>(); + items.add(index); + } + + public WeekGridMinuteTimeRange getDateRange() { + return range; + } + + public Date getStart() { + return range.getStart(); + } + + public Date getEnd() { + return range.getEnd(); + } + + public void setDateRange(WeekGridMinuteTimeRange range) { + this.range = range; + } + + public List<Integer> getItems() { + return items; + } + + public void add(Integer index) { + items.add(index); + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DateUtil.java b/client/src/com/vaadin/client/ui/calendar/schedule/DateUtil.java new file mode 100644 index 0000000000..84726327e2 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/DateUtil.java @@ -0,0 +1,70 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Date; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * Utility class for {@link Date} operations + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class DateUtil { + + /** + * Checks if dates are same day without checking datetimes. + * + * @param date1 + * @param date2 + * @return + */ + @SuppressWarnings("deprecation") + public static boolean compareDate(Date date1, Date date2) { + if (date1.getDate() == date2.getDate() + && date1.getYear() == date2.getYear() + && date1.getMonth() == date2.getMonth()) { + return true; + } + return false; + } + + /** + * @param date + * the date to format + * + * @return given Date as String, for communicating to server-side + */ + public static String formatClientSideDate(Date date) { + DateTimeFormat dateformat_date = DateTimeFormat + .getFormat(DateConstants.CLIENT_DATE_FORMAT); + return dateformat_date.format(date); + } + + /** + * @param date + * the date to format + * @return given Date as String, for communicating to server-side + */ + public static String formatClientSideTime(Date date) { + DateTimeFormat dateformat_date = DateTimeFormat + .getFormat(DateConstants.CLIENT_TIME_FORMAT); + return dateformat_date.format(date); + } +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DayToolbar.java b/client/src/com/vaadin/client/ui/calendar/schedule/DayToolbar.java new file mode 100644 index 0000000000..6233e8111e --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/DayToolbar.java @@ -0,0 +1,181 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Iterator; + +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.ui.Button; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ui.VCalendar; + +/** + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class DayToolbar extends HorizontalPanel implements ClickHandler { + private int width = 0; + protected static final int MARGINLEFT = 50; + protected static final int MARGINRIGHT = 20; + protected Button backLabel; + protected Button nextLabel; + private boolean verticalSized; + private boolean horizontalSized; + private VCalendar calendar; + + public DayToolbar(VCalendar vcalendar) { + calendar = vcalendar; + + setStylePrimaryName("v-calendar-header-week"); + backLabel = new Button(); + backLabel.setStylePrimaryName("v-calendar-back"); + nextLabel = new Button(); + nextLabel.addClickHandler(this); + nextLabel.setStylePrimaryName("v-calendar-next"); + backLabel.addClickHandler(this); + setBorderWidth(0); + setSpacing(0); + } + + public void setWidthPX(int width) { + this.width = (width - MARGINLEFT) - MARGINRIGHT; + // super.setWidth(this.width + "px"); + if (getWidgetCount() == 0) { + return; + } + updateCellWidths(); + } + + public void updateCellWidths() { + int count = getWidgetCount(); + if (count > 0) { + setCellWidth(backLabel, MARGINLEFT + "px"); + setCellWidth(nextLabel, MARGINRIGHT + "px"); + setCellHorizontalAlignment(nextLabel, ALIGN_RIGHT); + int cellw = width / (count - 2); + int remain = width % (count - 2); + int cellw2 = cellw + 1; + if (cellw > 0) { + int[] cellWidths = VCalendar + .distributeSize(width, count - 2, 0); + for (int i = 1; i < count - 1; i++) { + Widget widget = getWidget(i); + // if (remain > 0) { + // setCellWidth(widget, cellw2 + "px"); + // remain--; + // } else { + // setCellWidth(widget, cellw + "px"); + // } + setCellWidth(widget, cellWidths[i - 1] + "px"); + widget.setWidth(cellWidths[i - 1] + "px"); + } + } + } + } + + public void add(String dayName, final String date, + String localized_date_format, String extraClass) { + Label l = new Label(dayName + " " + localized_date_format); + l.setStylePrimaryName("v-calendar-header-day"); + + if (extraClass != null) { + l.addStyleDependentName(extraClass); + } + + if (verticalSized) { + l.addStyleDependentName("Vsized"); + } + if (horizontalSized) { + l.addStyleDependentName("Hsized"); + } + + l.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (calendar.getDateClickListener() != null) { + calendar.getDateClickListener().dateClick(date); + } + } + }); + + add(l); + } + + public void addBackButton() { + if (!calendar.isBackwardNavigationEnabled()) { + nextLabel.getElement().getStyle().setHeight(0, Unit.PX); + } + add(backLabel); + } + + public void addNextButton() { + if (!calendar.isForwardNavigationEnabled()) { + backLabel.getElement().getStyle().setHeight(0, Unit.PX); + } + add(nextLabel); + } + + @Override + public void onClick(ClickEvent event) { + if (!calendar.isDisabledOrReadOnly()) { + if (event.getSource() == nextLabel) { + if (calendar.getForwardListener() != null) { + calendar.getForwardListener().forward(); + } + } else if (event.getSource() == backLabel) { + if (calendar.getBackwardListener() != null) { + calendar.getBackwardListener().backward(); + } + } + } + } + + public void setVerticalSized(boolean sized) { + verticalSized = sized; + updateDayLabelSizedStyleNames(); + } + + public void setHorizontalSized(boolean sized) { + horizontalSized = sized; + updateDayLabelSizedStyleNames(); + } + + private void updateDayLabelSizedStyleNames() { + Iterator<Widget> it = iterator(); + while (it.hasNext()) { + updateWidgetSizedStyleName(it.next()); + } + } + + private void updateWidgetSizedStyleName(Widget w) { + if (verticalSized) { + w.addStyleDependentName("Vsized"); + } else { + w.removeStyleDependentName("VSized"); + } + if (horizontalSized) { + w.addStyleDependentName("Hsized"); + } else { + w.removeStyleDependentName("HSized"); + } + } +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/FocusableComplexPanel.java b/client/src/com/vaadin/client/ui/calendar/schedule/FocusableComplexPanel.java new file mode 100644 index 0000000000..6b42caec10 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/FocusableComplexPanel.java @@ -0,0 +1,122 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +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.ComplexPanel; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.client.Focusable; + +/** + * A ComplexPanel that can be focused + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class FocusableComplexPanel extends ComplexPanel implements + HasFocusHandlers, HasBlurHandlers, HasKeyDownHandlers, + HasKeyPressHandlers, Focusable { + + protected void makeFocusable() { + // 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); + } +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/FocusableGrid.java b/client/src/com/vaadin/client/ui/calendar/schedule/FocusableGrid.java new file mode 100644 index 0000000000..b40f1c3652 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/FocusableGrid.java @@ -0,0 +1,134 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +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.Grid; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.client.Focusable; + +/** + * A Grid that can be focused + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class FocusableGrid extends Grid implements HasFocusHandlers, + HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable { + + /** + * Constructor + */ + public FocusableGrid() { + super(); + makeFocusable(); + } + + public FocusableGrid(int rows, int columns) { + super(rows, columns); + makeFocusable(); + } + + protected void makeFocusable() { + // 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); + } +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/FocusableHTML.java b/client/src/com/vaadin/client/ui/calendar/schedule/FocusableHTML.java new file mode 100644 index 0000000000..31d810608a --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/FocusableHTML.java @@ -0,0 +1,124 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +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.HTML; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.client.Focusable; + +/** + * A HTML widget that can be focused + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class FocusableHTML extends HTML implements HasFocusHandlers, + HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable { + + /** + * Constructor + */ + public FocusableHTML() { + // 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); + } +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/HasTooltipKey.java b/client/src/com/vaadin/client/ui/calendar/schedule/HasTooltipKey.java new file mode 100644 index 0000000000..5827068840 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/HasTooltipKey.java @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +/** + * For Calendar client-side internal use only. + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public interface HasTooltipKey { + /** + * Gets the key associated for the Widget implementing this interface. This + * key is used for getting a tooltip title identified by the key + * + * @return the tooltip key + */ + Object getTooltipKey(); +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java b/client/src/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java new file mode 100644 index 0000000000..b7f6ee7a3c --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java @@ -0,0 +1,142 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Date; + +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.client.ui.VCalendar; + +/** + * The label in a month cell + * + * @since 7.1 + */ +public class MonthEventLabel extends HTML implements HasTooltipKey { + + private static final String STYLENAME = "v-calendar-event"; + + private boolean timeSpecificEvent = false; + private Integer eventIndex; + private VCalendar calendar; + private String caption; + private Date time; + + /** + * Default constructor + */ + public MonthEventLabel() { + setStylePrimaryName(STYLENAME); + } + + /** + * Set the time of the event label + * + * @param date + * The date object that specifies the time + */ + public void setTime(Date date) { + time = date; + renderCaption(); + } + + /** + * Set the caption of the event label + * + * @param caption + * The caption string, can be HTML + */ + public void setCaption(String caption) { + this.caption = caption; + renderCaption(); + } + + /** + * Renders the caption in the DIV element + */ + private void renderCaption() { + StringBuilder html = new StringBuilder(); + if (caption != null && time != null) { + html.append("<span class=\"" + STYLENAME + "-time\">"); + html.append(calendar.getTimeFormat().format(time)); + html.append("</span> "); + html.append(caption); + } else if (caption != null) { + html.append(caption); + } else if (time != null) { + html.append("<span class=\"" + STYLENAME + "-time\">"); + html.append(calendar.getTimeFormat().format(time)); + html.append("</span>"); + } + super.setHTML(html.toString()); + } + + /** + * Set the (server side) index of the event + * + * @param index + * The integer index + */ + public void setEventIndex(int index) { + eventIndex = index; + } + + /** + * Set the Calendar instance this label belongs to + * + * @param calendar + * The calendar instance + */ + public void setCalendar(VCalendar calendar) { + this.calendar = calendar; + } + + /** + * Is the event bound to a specific time + * + * @return + */ + public boolean isTimeSpecificEvent() { + return timeSpecificEvent; + } + + /** + * Is the event bound to a specific time + * + * @param timeSpecificEvent + * True if the event is bound to a time, false if it is only + * bound to the day + */ + public void setTimeSpecificEvent(boolean timeSpecificEvent) { + this.timeSpecificEvent = timeSpecificEvent; + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.HTML#setHTML(java.lang.String) + */ + @Override + public void setHTML(String html) { + throw new UnsupportedOperationException( + "Use setCaption() and setTime() instead"); + } + + @Override + public Object getTooltipKey() { + return eventIndex; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/MonthGrid.java b/client/src/com/vaadin/client/ui/calendar/schedule/MonthGrid.java new file mode 100644 index 0000000000..df9bc42d2a --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/MonthGrid.java @@ -0,0 +1,216 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Date; + +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.vaadin.client.ui.VCalendar; + +/** + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class MonthGrid extends FocusableGrid implements KeyDownHandler { + + private SimpleDayCell selectionStart; + private SimpleDayCell selectionEnd; + private final VCalendar calendar; + private boolean rangeSelectDisabled; + private boolean disabled; + private boolean enabled = true; + private final HandlerRegistration keyDownHandler; + + public MonthGrid(VCalendar parent, int rows, int columns) { + super(rows, columns); + calendar = parent; + setCellSpacing(0); + setCellPadding(0); + setStylePrimaryName("v-calendar-month"); + + keyDownHandler = addKeyDownHandler(this); + } + + @Override + protected void onUnload() { + keyDownHandler.removeHandler(); + super.onUnload(); + } + + public void setSelectionEnd(SimpleDayCell simpleDayCell) { + selectionEnd = simpleDayCell; + updateSelection(); + } + + public void setSelectionStart(SimpleDayCell simpleDayCell) { + if (!rangeSelectDisabled && isEnabled()) { + selectionStart = simpleDayCell; + setFocus(true); + } + + } + + private void updateSelection() { + if (selectionStart == null) { + return; + } + if (selectionStart != null && selectionEnd != null) { + Date startDate = selectionStart.getDate(); + Date endDate = selectionEnd.getDate(); + for (int row = 0; row < getRowCount(); row++) { + for (int cell = 0; cell < getCellCount(row); cell++) { + SimpleDayCell sdc = (SimpleDayCell) getWidget(row, cell); + if (sdc == null) { + return; + } + Date d = sdc.getDate(); + if (startDate.compareTo(d) <= 0 + && endDate.compareTo(d) >= 0) { + sdc.addStyleDependentName("selected"); + } else if (startDate.compareTo(d) >= 0 + && endDate.compareTo(d) <= 0) { + sdc.addStyleDependentName("selected"); + } else { + sdc.removeStyleDependentName("selected"); + } + } + } + } + } + + public void setSelectionReady() { + if (selectionStart != null && selectionEnd != null) { + String value = ""; + Date startDate = selectionStart.getDate(); + Date endDate = selectionEnd.getDate(); + if (startDate.compareTo(endDate) > 0) { + Date temp = startDate; + startDate = endDate; + endDate = temp; + } + + if (calendar.getRangeSelectListener() != null) { + value = calendar.getDateFormat().format(startDate) + "TO" + + calendar.getDateFormat().format(endDate); + calendar.getRangeSelectListener().rangeSelected(value); + } + selectionStart = null; + selectionEnd = null; + setFocus(false); + } + } + + public void cancelRangeSelection() { + if (selectionStart != null && selectionEnd != null) { + for (int row = 0; row < getRowCount(); row++) { + for (int cell = 0; cell < getCellCount(row); cell++) { + SimpleDayCell sdc = (SimpleDayCell) getWidget(row, cell); + if (sdc == null) { + return; + } + sdc.removeStyleDependentName("selected"); + } + } + } + setFocus(false); + selectionStart = null; + } + + public void updateCellSizes(int totalWidthPX, int totalHeightPX) { + boolean setHeight = totalHeightPX > 0; + boolean setWidth = totalWidthPX > 0; + int rows = getRowCount(); + int cells = getCellCount(0); + int cellWidth = (totalWidthPX / cells) - 1; + int widthRemainder = totalWidthPX % cells; + // Division for cells might not be even. Distribute it evenly to + // will whole space. + int heightPX = totalHeightPX; + int cellHeight = heightPX / rows; + int heightRemainder = heightPX % rows; + + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cells; j++) { + SimpleDayCell sdc = (SimpleDayCell) getWidget(i, j); + + if (setWidth) { + if (widthRemainder > 0) { + sdc.setWidth(cellWidth + 1 + "px"); + widthRemainder--; + + } else { + sdc.setWidth(cellWidth + "px"); + } + } + + if (setHeight) { + if (heightRemainder > 0) { + sdc.setHeightPX(cellHeight + 1, true); + + } else { + sdc.setHeightPX(cellHeight, true); + } + } else { + sdc.setHeightPX(-1, true); + } + } + heightRemainder--; + } + } + + /** + * Disable or enable possibility to select ranges + */ + public void setRangeSelect(boolean b) { + rangeSelectDisabled = !b; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + @Override + public void onKeyDown(KeyDownEvent event) { + int keycode = event.getNativeKeyCode(); + if (KeyCodes.KEY_ESCAPE == keycode && selectionStart != null) { + cancelRangeSelection(); + } + } + + public int getDayCellIndex(SimpleDayCell dayCell) { + int rows = getRowCount(); + int cells = getCellCount(0); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cells; j++) { + SimpleDayCell sdc = (SimpleDayCell) getWidget(i, j); + if (dayCell == sdc) { + return i * cells + j; + } + } + } + + return -1; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java b/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java new file mode 100644 index 0000000000..a2bd008d01 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java @@ -0,0 +1,701 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Date; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +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.dom.client.MouseMoveEvent; +import com.google.gwt.event.dom.client.MouseMoveHandler; +import com.google.gwt.event.dom.client.MouseOverEvent; +import com.google.gwt.event.dom.client.MouseOverHandler; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; +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.HTML; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ui.FocusableFlowPanel; +import com.vaadin.client.ui.VCalendar; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * A class representing a single cell within the calendar in month-view + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class SimpleDayCell extends FocusableFlowPanel implements + MouseUpHandler, MouseDownHandler, MouseOverHandler, MouseMoveHandler { + + private static int BOTTOMSPACERHEIGHT = -1; + private static int EVENTHEIGHT = -1; + private static final int BORDERPADDINGSIZE = 1; + + private final VCalendar calendar; + private Date date; + private int intHeight; + private final HTML bottomspacer; + private final Label caption; + private final CalendarEvent[] events = new CalendarEvent[10]; + private final int cell; + private final int row; + private boolean monthNameVisible; + private HandlerRegistration mouseUpRegistration; + private HandlerRegistration mouseDownRegistration; + private HandlerRegistration mouseOverRegistration; + private boolean monthEventMouseDown; + private boolean labelMouseDown; + private int eventCount = 0; + + private int startX = -1; + private int startY = -1; + private int startYrelative; + private int startXrelative; + private Date startDateFrom; + private Date startDateTo; + private int prevDayDiff = 0; + private int prevWeekDiff = 0; + private HandlerRegistration moveRegistration; + private CalendarEvent moveEvent; + private Widget clickedWidget; + private HandlerRegistration bottomSpacerMouseDownHandler; + private boolean scrollable = false; + private boolean eventCanceled; + private MonthGrid monthGrid; + private HandlerRegistration keyDownHandler; + + public SimpleDayCell(VCalendar calendar, int row, int cell) { + this.calendar = calendar; + this.row = row; + this.cell = cell; + setStylePrimaryName("v-calendar-month-day"); + caption = new Label(); + bottomspacer = new HTML(); + bottomspacer.setStyleName("v-calendar-bottom-spacer-empty"); + bottomspacer.setWidth(3 + "em"); + caption.setStyleName("v-calendar-day-number"); + add(caption); + add(bottomspacer); + caption.addMouseDownHandler(this); + caption.addMouseUpHandler(this); + } + + @Override + public void onLoad() { + BOTTOMSPACERHEIGHT = bottomspacer.getOffsetHeight(); + EVENTHEIGHT = BOTTOMSPACERHEIGHT; + } + + public void setMonthGrid(MonthGrid monthGrid) { + this.monthGrid = monthGrid; + } + + public MonthGrid getMonthGrid() { + return monthGrid; + } + + @SuppressWarnings("deprecation") + public void setDate(Date date) { + int dateOfMonth = date.getDate(); + if (monthNameVisible) { + caption.setText(dateOfMonth + " " + + calendar.getMonthNames()[date.getMonth()]); + } else { + caption.setText("" + dateOfMonth); + } + this.date = date; + } + + public Date getDate() { + return date; + } + + public void reDraw(boolean clear) { + setHeightPX(intHeight + BORDERPADDINGSIZE, clear); + } + + /* + * Events and whole cell content are drawn by this method. By the + * clear-argument, you can choose to clear all old content. Notice that + * clearing will also remove all element's event handlers. + */ + public void setHeightPX(int px, boolean clear) { + // measure from DOM if needed + if (px < 0) { + intHeight = getOffsetHeight() - BORDERPADDINGSIZE; + } else { + intHeight = px - BORDERPADDINGSIZE; + } + + // Couldn't measure height or it ended up negative. Don't bother + // continuing + if (intHeight == -1) { + return; + } + + if (clear) { + while (getWidgetCount() > 1) { + remove(1); + } + } + + // How many events can be shown in UI + int slots = 0; + if (scrollable) { + for (int i = 0; i < events.length; i++) { + if (events[i] != null) { + slots = i + 1; + } + } + setHeight(intHeight + "px"); // Fixed height + } else { + // Dynamic height by the content + DOM.removeElementAttribute(getElement(), "height"); + slots = (intHeight - caption.getOffsetHeight() - BOTTOMSPACERHEIGHT) + / EVENTHEIGHT; + if (slots > 10) { + slots = 10; + } + } + + updateEvents(slots, clear); + + } + + public void updateEvents(int slots, boolean clear) { + int eventsAdded = 0; + + for (int i = 0; i < slots; i++) { + CalendarEvent e = events[i]; + if (e == null) { + // Empty slot + HTML slot = new HTML(); + slot.setStyleName("v-calendar-spacer"); + if (!clear) { + remove(i + 1); + insert(slot, i + 1); + } else { + add(slot); + } + } else { + // Event slot + eventsAdded++; + if (!clear) { + Widget w = getWidget(i + 1); + if (!(w instanceof MonthEventLabel)) { + remove(i + 1); + insert(createMonthEventLabel(e), i + 1); + } + } else { + add(createMonthEventLabel(e)); + } + } + } + + int remainingSpace = intHeight + - ((slots * EVENTHEIGHT) + BOTTOMSPACERHEIGHT + caption + .getOffsetHeight()); + int newHeight = remainingSpace + BOTTOMSPACERHEIGHT; + if (newHeight < 0) { + newHeight = EVENTHEIGHT; + } + bottomspacer.setHeight(newHeight + "px"); + + if (clear) { + add(bottomspacer); + } + + int more = eventCount - eventsAdded; + if (more > 0) { + if (bottomSpacerMouseDownHandler == null) { + bottomSpacerMouseDownHandler = bottomspacer + .addMouseDownHandler(this); + } + bottomspacer.setStyleName("v-calendar-bottom-spacer"); + bottomspacer.setText("+ " + more); + } else { + if (!scrollable && bottomSpacerMouseDownHandler != null) { + bottomSpacerMouseDownHandler.removeHandler(); + bottomSpacerMouseDownHandler = null; + } + + if (scrollable) { + bottomspacer.setText("[ - ]"); + } else { + bottomspacer.setStyleName("v-calendar-bottom-spacer-empty"); + bottomspacer.setText(""); + } + } + } + + private MonthEventLabel createMonthEventLabel(CalendarEvent e) { + long rangeInMillis = e.getRangeInMilliseconds(); + boolean timeEvent = rangeInMillis <= DateConstants.DAYINMILLIS + && !e.isAllDay(); + Date fromDatetime = e.getStartTime(); + + // Create a new MonthEventLabel + MonthEventLabel eventDiv = new MonthEventLabel(); + eventDiv.addStyleDependentName("month"); + eventDiv.addMouseDownHandler(this); + eventDiv.addMouseUpHandler(this); + eventDiv.setCalendar(calendar); + eventDiv.setEventIndex(e.getIndex()); + + if (timeEvent) { + eventDiv.setTimeSpecificEvent(true); + if (e.getStyleName() != null) { + eventDiv.addStyleDependentName(e.getStyleName()); + } + eventDiv.setCaption(e.getCaption()); + eventDiv.setTime(fromDatetime); + + } else { + eventDiv.setTimeSpecificEvent(false); + Date from = e.getStart(); + Date to = e.getEnd(); + if (e.getStyleName().length() > 0) { + eventDiv.addStyleName("month-event " + e.getStyleName()); + } else { + eventDiv.addStyleName("month-event"); + } + int fromCompareToDate = from.compareTo(date); + int toCompareToDate = to.compareTo(date); + eventDiv.addStyleDependentName("all-day"); + if (fromCompareToDate == 0) { + eventDiv.addStyleDependentName("start"); + eventDiv.setCaption(e.getCaption()); + + } else if (fromCompareToDate < 0 && cell == 0) { + eventDiv.addStyleDependentName("continued-from"); + eventDiv.setCaption(e.getCaption()); + } + if (toCompareToDate == 0) { + eventDiv.addStyleDependentName("end"); + } else if (toCompareToDate > 0 + && (cell + 1) == getMonthGrid().getCellCount(row)) { + eventDiv.addStyleDependentName("continued-to"); + } + if (e.getStyleName() != null) { + eventDiv.addStyleDependentName(e.getStyleName() + "-all-day"); + } + } + + return eventDiv; + } + + private void setUnlimitedCellHeight() { + scrollable = true; + addStyleDependentName("scrollable"); + } + + private void setLimitedCellHeight() { + scrollable = false; + removeStyleDependentName("scrollable"); + } + + public void addCalendarEvent(CalendarEvent e) { + eventCount++; + int slot = e.getSlotIndex(); + if (slot == -1) { + for (int i = 0; i < events.length; i++) { + if (events[i] == null) { + events[i] = e; + e.setSlotIndex(i); + break; + } + } + } else { + events[slot] = e; + } + } + + @SuppressWarnings("deprecation") + public void setMonthNameVisible(boolean b) { + monthNameVisible = b; + int dateOfMonth = date.getDate(); + caption.setText(dateOfMonth + " " + + calendar.getMonthNames()[date.getMonth()]); + } + + public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) { + return addDomHandler(handler, MouseMoveEvent.getType()); + } + + @Override + protected void onAttach() { + super.onAttach(); + mouseUpRegistration = addDomHandler(this, MouseUpEvent.getType()); + mouseDownRegistration = addDomHandler(this, MouseDownEvent.getType()); + mouseOverRegistration = addDomHandler(this, MouseOverEvent.getType()); + } + + @Override + protected void onDetach() { + mouseUpRegistration.removeHandler(); + mouseDownRegistration.removeHandler(); + mouseOverRegistration.removeHandler(); + super.onDetach(); + } + + @Override + public void onMouseUp(MouseUpEvent event) { + if (event.getNativeButton() != NativeEvent.BUTTON_LEFT) { + return; + } + + Widget w = (Widget) event.getSource(); + if (moveRegistration != null) { + Event.releaseCapture(getElement()); + moveRegistration.removeHandler(); + moveRegistration = null; + keyDownHandler.removeHandler(); + keyDownHandler = null; + } + + if (w == bottomspacer && monthEventMouseDown) { + GWT.log("Mouse up over bottomspacer"); + + } else if (clickedWidget instanceof MonthEventLabel + && monthEventMouseDown) { + MonthEventLabel mel = (MonthEventLabel) clickedWidget; + + int endX = event.getClientX(); + int endY = event.getClientY(); + int xDiff = startX - endX; + int yDiff = startY - endY; + startX = -1; + startY = -1; + prevDayDiff = 0; + prevWeekDiff = 0; + + if (!mel.isTimeSpecificEvent() + && (xDiff < -3 || xDiff > 3 || yDiff < -3 || yDiff > 3)) { + eventMoved(moveEvent); + + } else if (calendar.getEventClickListener() != null) { + CalendarEvent e = getEventByWidget(mel); + calendar.getEventClickListener().eventClick(e); + } + + moveEvent = null; + } else if (w == this) { + getMonthGrid().setSelectionReady(); + + } else if (w instanceof Label && labelMouseDown) { + String clickedDate = calendar.getDateFormat().format(date); + if (calendar.getDateClickListener() != null) { + calendar.getDateClickListener().dateClick(clickedDate); + } + } + monthEventMouseDown = false; + labelMouseDown = false; + clickedWidget = null; + } + + @Override + public void onMouseDown(MouseDownEvent event) { + if (calendar.isDisabled() + || event.getNativeButton() != NativeEvent.BUTTON_LEFT) { + return; + } + + Widget w = (Widget) event.getSource(); + clickedWidget = w; + + if (w instanceof MonthEventLabel) { + // event clicks should be allowed even when read-only + monthEventMouseDown = true; + + if (w instanceof MonthEventLabel) { + startCalendarEventDrag(event, (MonthEventLabel) w); + } + } else if (!calendar.isReadOnly()) { + // these are not allowed when in read-only + if (w == bottomspacer) { + if (scrollable) { + setLimitedCellHeight(); + } else { + setUnlimitedCellHeight(); + } + reDraw(true); + + } else if (w == this && !scrollable) { + MonthGrid grid = getMonthGrid(); + if (grid.isEnabled() && calendar.isRangeSelectAllowed()) { + grid.setSelectionStart(this); + grid.setSelectionEnd(this); + } + } else if (w instanceof Label) { + labelMouseDown = true; + } + } + + event.stopPropagation(); + event.preventDefault(); + } + + @Override + public void onMouseOver(MouseOverEvent event) { + event.preventDefault(); + getMonthGrid().setSelectionEnd(this); + } + + @Override + public void onMouseMove(MouseMoveEvent event) { + if (clickedWidget instanceof MonthEventLabel && !monthEventMouseDown + || (startY < 0 && startX < 0)) { + return; + } + + MonthEventLabel w = (MonthEventLabel) clickedWidget; + + if (calendar.isDisabledOrReadOnly()) { + Event.releaseCapture(getElement()); + monthEventMouseDown = false; + startY = -1; + startX = -1; + return; + } + + int currentY = event.getClientY(); + int currentX = event.getClientX(); + int moveY = (currentY - startY); + int moveX = (currentX - startX); + if ((moveY < 5 && moveY > -6) && (moveX < 5 && moveX > -6)) { + return; + } + + int dateCellWidth = getWidth(); + int dateCellHeigth = getHeigth(); + + Element parent = getMonthGrid().getElement(); + int relativeX = event.getRelativeX(parent); + int relativeY = event.getRelativeY(parent); + int weekDiff = 0; + if (moveY > 0) { + weekDiff = (startYrelative + moveY) / dateCellHeigth; + } else { + weekDiff = (moveY - (dateCellHeigth - startYrelative)) + / dateCellHeigth; + } + + int dayDiff = 0; + if (moveX >= 0) { + dayDiff = (startXrelative + moveX) / dateCellWidth; + } else { + dayDiff = (moveX - (dateCellWidth - startXrelative)) + / dateCellWidth; + } + // Check boundaries + if (relativeY < 0 + || relativeY >= (calendar.getMonthGrid().getRowCount() * dateCellHeigth) + || relativeX < 0 + || relativeX >= (calendar.getMonthGrid().getColumnCount() * dateCellWidth)) { + return; + } + + GWT.log("Event moving delta: " + weekDiff + " weeks " + dayDiff + + " days" + " (" + getCell() + "," + getRow() + ")"); + + CalendarEvent e = moveEvent; + if (e == null) { + e = getEventByWidget(w); + } + + Date from = e.getStart(); + Date to = e.getEnd(); + long duration = to.getTime() - from.getTime(); + + long daysMs = dayDiff * DateConstants.DAYINMILLIS; + long weeksMs = weekDiff * DateConstants.WEEKINMILLIS; + from.setTime(startDateFrom.getTime() + weeksMs + daysMs); + to.setTime((from.getTime() + duration)); + e.setStart(from); + e.setEnd(to); + e.setStartTime(new Date(from.getTime())); + e.setEndTime(new Date(to.getTime())); + + updateDragPosition(w, dayDiff, weekDiff); + } + + private void eventMoved(CalendarEvent e) { + calendar.updateEventToMonthGrid(e); + if (calendar.getEventMovedListener() != null) { + calendar.getEventMovedListener().eventMoved(e); + } + } + + public void startCalendarEventDrag(MouseDownEvent event, + final MonthEventLabel w) { + if (w.isTimeSpecificEvent()) { + return; + } + + moveRegistration = addMouseMoveHandler(this); + startX = event.getClientX(); + startY = event.getClientY(); + startYrelative = event.getRelativeY(w.getParent().getElement()) + % getHeigth(); + startXrelative = event.getRelativeX(w.getParent().getElement()) + % getWidth(); + + CalendarEvent e = getEventByWidget(w); + startDateFrom = (Date) e.getStart().clone(); + startDateTo = (Date) e.getEnd().clone(); + + Event.setCapture(getElement()); + keyDownHandler = addKeyDownHandler(new KeyDownHandler() { + + @Override + public void onKeyDown(KeyDownEvent event) { + if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) { + cancelEventDrag(w); + } + } + + }); + + focus(); + + GWT.log("Start drag"); + } + + protected void cancelEventDrag(MonthEventLabel w) { + if (moveRegistration != null) { + // reset position + if (moveEvent == null) { + moveEvent = getEventByWidget(w); + } + + moveEvent.setStart(startDateFrom); + moveEvent.setEnd(startDateTo); + calendar.updateEventToMonthGrid(moveEvent); + + // reset drag-related properties + Event.releaseCapture(getElement()); + moveRegistration.removeHandler(); + moveRegistration = null; + keyDownHandler.removeHandler(); + keyDownHandler = null; + setFocus(false); + monthEventMouseDown = false; + startY = -1; + startX = -1; + moveEvent = null; + labelMouseDown = false; + clickedWidget = null; + } + } + + public void updateDragPosition(MonthEventLabel w, int dayDiff, int weekDiff) { + // Draw event to its new position only when position has changed + if (dayDiff == prevDayDiff && weekDiff == prevWeekDiff) { + return; + } + + prevDayDiff = dayDiff; + prevWeekDiff = weekDiff; + + if (moveEvent == null) { + moveEvent = getEventByWidget(w); + } + + calendar.updateEventToMonthGrid(moveEvent); + } + + public int getRow() { + return row; + } + + public int getCell() { + return cell; + } + + public int getHeigth() { + return intHeight + BORDERPADDINGSIZE; + } + + public int getWidth() { + return getOffsetWidth() - BORDERPADDINGSIZE; + } + + public void setToday(boolean today) { + if (today) { + addStyleDependentName("today"); + } else { + removeStyleDependentName("today"); + } + } + + public boolean removeEvent(CalendarEvent targetEvent, + boolean reDrawImmediately) { + int slot = targetEvent.getSlotIndex(); + if (slot < 0) { + return false; + } + + CalendarEvent e = getCalendarEvent(slot); + if (targetEvent.equals(e)) { + events[slot] = null; + eventCount--; + if (reDrawImmediately) { + reDraw(moveEvent == null); + } + return true; + } + return false; + } + + private CalendarEvent getEventByWidget(MonthEventLabel eventWidget) { + int index = getWidgetIndex(eventWidget); + return getCalendarEvent(index - 1); + } + + public CalendarEvent getCalendarEvent(int i) { + return events[i]; + } + + public CalendarEvent[] getEvents() { + return events; + } + + public int getEventCount() { + return eventCount; + } + + public CalendarEvent getMoveEvent() { + return moveEvent; + } + + public void addEmphasisStyle() { + addStyleDependentName("dragemphasis"); + } + + public void removeEmphasisStyle() { + removeStyleDependentName("dragemphasis"); + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayToolbar.java b/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayToolbar.java new file mode 100644 index 0000000000..fc75136b93 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayToolbar.java @@ -0,0 +1,97 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; + +/** + * + * @since 7.1.0 + * @author Vaadin Ltd. + * + */ +public class SimpleDayToolbar extends HorizontalPanel { + private int width = 0; + private boolean isWidthUndefined = false; + + public SimpleDayToolbar() { + setStylePrimaryName("v-calendar-header-month"); + } + + public void setDayNames(String[] dayNames) { + clear(); + for (int i = 0; i < dayNames.length; i++) { + Label l = new Label(dayNames[i]); + l.setStylePrimaryName("v-calendar-header-day"); + add(l); + } + updateCellWidth(); + } + + public void setWidthPX(int width) { + this.width = width; + + setWidthUndefined(width == -1); + + if (!isWidthUndefined()) { + super.setWidth(this.width + "px"); + if (getWidgetCount() == 0) { + return; + } + } + updateCellWidth(); + } + + private boolean isWidthUndefined() { + return isWidthUndefined; + } + + private void setWidthUndefined(boolean isWidthUndefined) { + this.isWidthUndefined = isWidthUndefined; + + if (isWidthUndefined) { + addStyleDependentName("Hsized"); + + } else { + removeStyleDependentName("Hsized"); + } + } + + private void updateCellWidth() { + int cellw = -1; + int widgetCount = getWidgetCount(); + if (widgetCount <= 0) { + return; + } + if (isWidthUndefined()) { + Widget widget = getWidget(0); + String w = widget.getElement().getStyle().getWidth(); + if (w.length() > 2) { + cellw = Integer.parseInt(w.substring(0, w.length() - 2)); + } + } else { + cellw = width / getWidgetCount(); + } + if (cellw > 0) { + for (int i = 0; i < getWidgetCount(); i++) { + Widget widget = getWidget(i); + setCellWidth(widget, cellw + "px"); + } + } + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/SimpleWeekToolbar.java b/client/src/com/vaadin/client/ui/calendar/schedule/SimpleWeekToolbar.java new file mode 100644 index 0000000000..59902811cd --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/SimpleWeekToolbar.java @@ -0,0 +1,109 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.FlexTable; +import com.vaadin.client.ui.VCalendar; + +/** + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class SimpleWeekToolbar extends FlexTable implements ClickHandler { + private int height; + private VCalendar calendar; + private boolean isHeightUndefined; + + public SimpleWeekToolbar(VCalendar parent) { + calendar = parent; + setCellSpacing(0); + setCellPadding(0); + setStyleName("v-calendar-week-numbers"); + } + + public void addWeek(int week, int year) { + WeekLabel l = new WeekLabel(week + "", week, year); + l.addClickHandler(this); + int rowCount = getRowCount(); + insertRow(rowCount); + setWidget(rowCount, 0, l); + updateCellHeights(); + } + + public void updateCellHeights() { + if (!isHeightUndefined()) { + int rowCount = getRowCount(); + if (rowCount == 0) { + return; + } + int cellheight = (height / rowCount) - 1; + int remainder = height % rowCount; + if (cellheight < 0) { + cellheight = 0; + } + for (int i = 0; i < rowCount; i++) { + if (remainder > 0) { + getWidget(i, 0).setHeight(cellheight + 1 + "px"); + } else { + getWidget(i, 0).setHeight(cellheight + "px"); + } + getWidget(i, 0).getElement().getStyle() + .setProperty("lineHeight", cellheight + "px"); + remainder--; + } + } else { + for (int i = 0; i < getRowCount(); i++) { + getWidget(i, 0).setHeight(""); + getWidget(i, 0).getElement().getStyle() + .setProperty("lineHeight", ""); + } + } + } + + public void setHeightPX(int intHeight) { + setHeightUndefined(intHeight == -1); + height = intHeight; + updateCellHeights(); + } + + public boolean isHeightUndefined() { + return isHeightUndefined; + } + + public void setHeightUndefined(boolean isHeightUndefined) { + this.isHeightUndefined = isHeightUndefined; + + if (isHeightUndefined) { + addStyleDependentName("Vsized"); + + } else { + removeStyleDependentName("Vsized"); + } + } + + @Override + public void onClick(ClickEvent event) { + WeekLabel wl = (WeekLabel) event.getSource(); + if (calendar.getWeekClickListener() != null) { + calendar.getWeekClickListener().weekClick( + wl.getYear() + "w" + wl.getWeek()); + } + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/WeekGrid.java b/client/src/com/vaadin/client/ui/calendar/schedule/WeekGrid.java new file mode 100644 index 0000000000..450ea29549 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/WeekGrid.java @@ -0,0 +1,678 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Arrays; +import java.util.Date; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ScrollEvent; +import com.google.gwt.event.dom.client.ScrollHandler; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.ScrollPanel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.DateTimeService; +import com.vaadin.client.Util; +import com.vaadin.client.ui.VCalendar; + +/** + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class WeekGrid extends SimplePanel { + + int width = 0; + private int height = 0; + final HorizontalPanel content; + private VCalendar calendar; + private boolean disabled; + final Timebar timebar; + private Panel wrapper; + private boolean verticalScrollEnabled; + private boolean horizontalScrollEnabled; + private int[] cellHeights; + private final int slotInMinutes = 30; + private int dateCellBorder; + private DateCell dateCellOfToday; + private int[] cellWidths; + private int firstHour; + private int lastHour; + + public WeekGrid(VCalendar parent, boolean format24h) { + setCalendar(parent); + content = new HorizontalPanel(); + timebar = new Timebar(format24h); + content.add(timebar); + + wrapper = new SimplePanel(); + wrapper.setStylePrimaryName("v-calendar-week-wrapper"); + wrapper.add(content); + + setWidget(wrapper); + } + + private void setVerticalScroll(boolean isVerticalScrollEnabled) { + if (isVerticalScrollEnabled && !(isVerticalScrollable())) { + verticalScrollEnabled = true; + horizontalScrollEnabled = false; + wrapper.remove(content); + + final ScrollPanel scrollPanel = new ScrollPanel(); + scrollPanel.setStylePrimaryName("v-calendar-week-wrapper"); + scrollPanel.setWidget(content); + + scrollPanel.addScrollHandler(new ScrollHandler() { + @Override + public void onScroll(ScrollEvent event) { + if (calendar.getScrollListener() != null) { + calendar.getScrollListener().scroll( + scrollPanel.getVerticalScrollPosition()); + } + } + }); + + setWidget(scrollPanel); + wrapper = scrollPanel; + + } else if (!isVerticalScrollEnabled && (isVerticalScrollable())) { + verticalScrollEnabled = false; + horizontalScrollEnabled = false; + wrapper.remove(content); + + SimplePanel simplePanel = new SimplePanel(); + simplePanel.setStylePrimaryName("v-calendar-week-wrapper"); + simplePanel.setWidget(content); + + setWidget(simplePanel); + wrapper = simplePanel; + } + } + + public void setVerticalScrollPosition(int verticalScrollPosition) { + if (isVerticalScrollable()) { + ((ScrollPanel) wrapper) + .setVerticalScrollPosition(verticalScrollPosition); + } + } + + public int getInternalWidth() { + return width; + } + + public void addDate(Date d) { + final DateCell dc = new DateCell(this, d); + dc.setDisabled(isDisabled()); + dc.setHorizontalSized(isHorizontalScrollable() || width < 0); + dc.setVerticalSized(isVerticalScrollable()); + content.add(dc); + } + + /** + * @param dateCell + * @return get the index of the given date cell in this week, starting from + * 0 + */ + public int getDateCellIndex(DateCell dateCell) { + return content.getWidgetIndex(dateCell) - 1; + } + + /** + * @return get the slot border in pixels + */ + public int getDateSlotBorder() { + return ((DateCell) content.getWidget(1)).getSlotBorder(); + } + + private boolean isVerticalScrollable() { + return verticalScrollEnabled; + } + + private boolean isHorizontalScrollable() { + return horizontalScrollEnabled; + } + + public void setWidthPX(int width) { + if (isHorizontalScrollable()) { + updateCellWidths(); + + // Otherwise the scroll wrapper is somehow too narrow = horizontal + // scroll + wrapper.setWidth(content.getOffsetWidth() + + Util.getNativeScrollbarSize() + "px"); + + this.width = content.getOffsetWidth() - timebar.getOffsetWidth(); + + } else { + this.width = (width == -1) ? width : width + - timebar.getOffsetWidth(); + + if (isVerticalScrollable() && width != -1) { + this.width = this.width - Util.getNativeScrollbarSize(); + } + updateCellWidths(); + } + } + + public void setHeightPX(int intHeight) { + height = intHeight; + + setVerticalScroll(height <= -1); + + // if not scrollable, use any height given + if (!isVerticalScrollable() && height > 0) { + + content.setHeight(height + "px"); + setHeight(height + "px"); + wrapper.setHeight(height + "px"); + wrapper.removeStyleDependentName("Vsized"); + updateCellHeights(); + timebar.setCellHeights(cellHeights); + timebar.setHeightPX(height); + + } else if (isVerticalScrollable()) { + updateCellHeights(); + wrapper.addStyleDependentName("Vsized"); + timebar.setCellHeights(cellHeights); + timebar.setHeightPX(height); + } + } + + public void clearDates() { + while (content.getWidgetCount() > 1) { + content.remove(1); + } + + dateCellOfToday = null; + } + + /** + * @return true if this weekgrid contains a date that is today + */ + public boolean hasToday() { + return dateCellOfToday != null; + } + + public void updateCellWidths() { + if (!isHorizontalScrollable() && width != -1) { + int count = content.getWidgetCount(); + int datesWidth = width; + if (datesWidth > 0 && count > 1) { + cellWidths = VCalendar + .distributeSize(datesWidth, count - 1, -1); + + for (int i = 1; i < count; i++) { + DateCell dc = (DateCell) content.getWidget(i); + dc.setHorizontalSized(isHorizontalScrollable() || width < 0); + dc.setWidthPX(cellWidths[i - 1]); + if (dc.isToday()) { + dc.setTimeBarWidth(getOffsetWidth()); + } + } + } + + } else { + int count = content.getWidgetCount(); + if (count > 1) { + for (int i = 1; i < count; i++) { + DateCell dc = (DateCell) content.getWidget(i); + dc.setHorizontalSized(isHorizontalScrollable() || width < 0); + } + } + } + } + + /** + * @return an int-array containing the widths of the cells (days) + */ + public int[] getDateCellWidths() { + return cellWidths; + } + + public void updateCellHeights() { + if (!isVerticalScrollable()) { + int count = content.getWidgetCount(); + if (count > 1) { + DateCell first = (DateCell) content.getWidget(1); + dateCellBorder = first.getSlotBorder(); + cellHeights = VCalendar.distributeSize(height, + first.getNumberOfSlots(), -dateCellBorder); + for (int i = 1; i < count; i++) { + DateCell dc = (DateCell) content.getWidget(i); + dc.setHeightPX(height, cellHeights); + } + } + + } else { + int count = content.getWidgetCount(); + if (count > 1) { + DateCell first = (DateCell) content.getWidget(1); + dateCellBorder = first.getSlotBorder(); + int dateHeight = (first.getOffsetHeight() / first + .getNumberOfSlots()) - dateCellBorder; + cellHeights = new int[48]; + Arrays.fill(cellHeights, dateHeight); + + for (int i = 1; i < count; i++) { + DateCell dc = (DateCell) content.getWidget(i); + dc.setVerticalSized(isVerticalScrollable()); + } + } + } + } + + public void addEvent(CalendarEvent e) { + int dateCount = content.getWidgetCount(); + Date from = e.getStart(); + Date toTime = e.getEndTime(); + for (int i = 1; i < dateCount; i++) { + DateCell dc = (DateCell) content.getWidget(i); + Date dcDate = dc.getDate(); + int comp = dcDate.compareTo(from); + int comp2 = dcDate.compareTo(toTime); + if (comp >= 0 + && comp2 < 0 + || (comp == 0 && comp2 == 0 && VCalendar + .isZeroLengthMidnightEvent(e))) { + // Same event may be over two DateCells if event's date + // range floats over one day. It can't float over two days, + // because event which range is over 24 hours, will be handled + // as a "fullDay" event. + dc.addEvent(dcDate, e); + } + } + } + + public int getPixelLengthFor(int startFromMinutes, int durationInMinutes) { + int pixelLength = 0; + int currentSlot = 0; + + int firstHourInMinutes = firstHour * 60; + + if (firstHourInMinutes > startFromMinutes) { + startFromMinutes = 0; + } else { + startFromMinutes -= firstHourInMinutes; + } + + // calculate full slots to event + int slotsTillEvent = startFromMinutes / slotInMinutes; + int startOverFlowTime = slotInMinutes + - (startFromMinutes % slotInMinutes); + if (startOverFlowTime == slotInMinutes) { + startOverFlowTime = 0; + currentSlot = slotsTillEvent; + } else { + currentSlot = slotsTillEvent + 1; + } + + int durationInSlots = 0; + int endOverFlowTime = 0; + + if (startOverFlowTime > 0) { + durationInSlots = (durationInMinutes - startOverFlowTime) + / slotInMinutes; + endOverFlowTime = (durationInMinutes - startOverFlowTime) + % slotInMinutes; + + } else { + durationInSlots = durationInMinutes / slotInMinutes; + endOverFlowTime = durationInMinutes % slotInMinutes; + } + + // calculate slot overflow at start + if (startOverFlowTime > 0 && currentSlot < cellHeights.length) { + int lastSlotHeight = cellHeights[currentSlot] + dateCellBorder; + pixelLength += (int) (((double) lastSlotHeight / (double) slotInMinutes) * startOverFlowTime); + } + + // calculate length in full slots + int lastFullSlot = currentSlot + durationInSlots; + for (; currentSlot < lastFullSlot && currentSlot < cellHeights.length; currentSlot++) { + pixelLength += cellHeights[currentSlot] + dateCellBorder; + } + + // calculate overflow at end + if (endOverFlowTime > 0 && currentSlot < cellHeights.length) { + int lastSlotHeight = cellHeights[currentSlot] + dateCellBorder; + pixelLength += (int) (((double) lastSlotHeight / (double) slotInMinutes) * endOverFlowTime); + } + + // reduce possible underflow at end + if (endOverFlowTime < 0) { + int lastSlotHeight = cellHeights[currentSlot] + dateCellBorder; + pixelLength += (int) (((double) lastSlotHeight / (double) slotInMinutes) * endOverFlowTime); + } + + return pixelLength; + } + + public int getPixelTopFor(int startFromMinutes) { + int pixelsToTop = 0; + int slotIndex = 0; + + int firstHourInMinutes = firstHour * 60; + + if (firstHourInMinutes > startFromMinutes) { + startFromMinutes = 0; + } else { + startFromMinutes -= firstHourInMinutes; + } + + // calculate full slots to event + int slotsTillEvent = startFromMinutes / slotInMinutes; + int overFlowTime = startFromMinutes % slotInMinutes; + if (slotsTillEvent > 0) { + for (slotIndex = 0; slotIndex < slotsTillEvent; slotIndex++) { + pixelsToTop += cellHeights[slotIndex] + dateCellBorder; + } + } + + // calculate lengths less than one slot + if (overFlowTime > 0) { + int lastSlotHeight = cellHeights[slotIndex] + dateCellBorder; + pixelsToTop += ((double) lastSlotHeight / (double) slotInMinutes) + * overFlowTime; + } + + return pixelsToTop; + } + + public void eventMoved(DateCellDayEvent dayEvent) { + Style s = dayEvent.getElement().getStyle(); + int left = Integer.parseInt(s.getLeft().substring(0, + s.getLeft().length() - 2)); + DateCell previousParent = (DateCell) dayEvent.getParent(); + DateCell newParent = (DateCell) content + .getWidget((left / getDateCellWidth()) + 1); + CalendarEvent se = dayEvent.getCalendarEvent(); + previousParent.removeEvent(dayEvent); + newParent.addEvent(dayEvent); + if (!previousParent.equals(newParent)) { + previousParent.recalculateEventWidths(); + } + newParent.recalculateEventWidths(); + if (calendar.getEventMovedListener() != null) { + calendar.getEventMovedListener().eventMoved(se); + } + } + + public void setToday(Date todayDate, Date todayTimestamp) { + int count = content.getWidgetCount(); + if (count > 1) { + for (int i = 1; i < count; i++) { + DateCell dc = (DateCell) content.getWidget(i); + if (dc.getDate().getTime() == todayDate.getTime()) { + if (isVerticalScrollable()) { + dc.setToday(todayTimestamp, -1); + } else { + dc.setToday(todayTimestamp, getOffsetWidth()); + } + } + dateCellOfToday = dc; + } + } + } + + public DateCell getDateCellOfToday() { + return dateCellOfToday; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public boolean isDisabled() { + return disabled; + } + + public Timebar getTimeBar() { + return timebar; + } + + public void setDateColor(Date when, Date to, String styleName) { + int dateCount = content.getWidgetCount(); + for (int i = 1; i < dateCount; i++) { + DateCell dc = (DateCell) content.getWidget(i); + Date dcDate = dc.getDate(); + int comp = dcDate.compareTo(when); + int comp2 = dcDate.compareTo(to); + if (comp >= 0 && comp2 <= 0) { + dc.setDateColor(styleName); + } + } + } + + /** + * @param calendar + * the calendar to set + */ + public void setCalendar(VCalendar calendar) { + this.calendar = calendar; + } + + /** + * @return the calendar + */ + public VCalendar getCalendar() { + return calendar; + } + + /** + * Get width of the single date cell + * + * @return Date cell width + */ + public int getDateCellWidth() { + int count = content.getWidgetCount() - 1; + int cellWidth = -1; + if (count <= 0) { + return cellWidth; + } + + if (width == -1) { + Widget firstWidget = content.getWidget(1); + cellWidth = firstWidget.getElement().getOffsetWidth(); + } else { + cellWidth = getInternalWidth() / count; + } + return cellWidth; + } + + /** + * @return the number of day cells in this week + */ + public int getDateCellCount() { + return content.getWidgetCount() - 1; + } + + public void setFirstHour(int firstHour) { + this.firstHour = firstHour; + timebar.setFirstHour(firstHour); + } + + public void setLastHour(int lastHour) { + this.lastHour = lastHour; + timebar.setLastHour(lastHour); + } + + public int getFirstHour() { + return firstHour; + } + + public int getLastHour() { + return lastHour; + } + + public static class Timebar extends HTML { + + private static final int[] timesFor12h = { 12, 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11 }; + + private int height; + + private final int verticalPadding = 7; // FIXME measure this from DOM + + private int[] slotCellHeights; + + private int firstHour; + + private int lastHour; + + public Timebar(boolean format24h) { + createTimeBar(format24h); + } + + public void setLastHour(int lastHour) { + this.lastHour = lastHour; + } + + public void setFirstHour(int firstHour) { + this.firstHour = firstHour; + + } + + public void setCellHeights(int[] cellHeights) { + slotCellHeights = cellHeights; + } + + private void createTimeBar(boolean format24h) { + setStylePrimaryName("v-calendar-times"); + + // Fist "time" is empty + Element e = DOM.createDiv(); + setStyleName(e, "v-calendar-time"); + e.setInnerText(""); + getElement().appendChild(e); + + DateTimeService dts = new DateTimeService(); + + if (format24h) { + for (int i = firstHour + 1; i <= lastHour; i++) { + e = DOM.createDiv(); + setStyleName(e, "v-calendar-time"); + String delimiter = dts.getClockDelimeter(); + e.setInnerHTML("<span>" + i + "</span>" + delimiter + "00"); + getElement().appendChild(e); + } + } else { + // FIXME Use dts.getAmPmStrings(); and make sure that + // DateTimeService has a some Locale set. + String[] ampm = new String[] { "AM", "PM" }; + + int amStop = (lastHour < 11) ? lastHour : 11; + int pmStart = (firstHour > 11) ? firstHour % 11 : 0; + + if (firstHour < 12) { + for (int i = firstHour + 1; i <= amStop; i++) { + e = DOM.createDiv(); + setStyleName(e, "v-calendar-time"); + e.setInnerHTML("<span>" + timesFor12h[i] + "</span>" + + " " + ampm[0]); + getElement().appendChild(e); + } + } + + if (lastHour > 11) { + for (int i = pmStart; i < lastHour - 11; i++) { + e = DOM.createDiv(); + setStyleName(e, "v-calendar-time"); + e.setInnerHTML("<span>" + timesFor12h[i] + "</span>" + + " " + ampm[1]); + getElement().appendChild(e); + } + } + } + } + + public void updateTimeBar(boolean format24h) { + clear(); + createTimeBar(format24h); + } + + private void clear() { + while (getElement().getChildCount() > 0) { + getElement().removeChild(getElement().getChild(0)); + } + } + + public void setHeightPX(int pixelHeight) { + height = pixelHeight; + + if (pixelHeight > -1) { + // as the negative margins on children pulls the whole element + // upwards, we must compensate. otherwise the element would be + // too short + super.setHeight((height + verticalPadding) + "px"); + removeStyleDependentName("Vsized"); + updateChildHeights(); + + } else { + addStyleDependentName("Vsized"); + updateChildHeights(); + } + } + + private void updateChildHeights() { + int childCount = getElement().getChildCount(); + + if (height != -1) { + + // 23 hours + first is empty + // we try to adjust the height of time labels to the distributed + // heights of the time slots + int hoursPerDay = lastHour - firstHour + 1; + + int slotsPerHour = slotCellHeights.length / hoursPerDay; + int[] cellHeights = new int[slotCellHeights.length + / slotsPerHour]; + + int slotHeightPosition = 0; + for (int i = 0; i < cellHeights.length; i++) { + for (int j = slotHeightPosition; j < slotHeightPosition + + slotsPerHour; j++) { + cellHeights[i] += slotCellHeights[j] + 1; + // 1px more for borders + // FIXME measure from DOM + } + slotHeightPosition += slotsPerHour; + } + + for (int i = 0; i < childCount; i++) { + Element e = (Element) getElement().getChild(i); + e.getStyle().setHeight(cellHeights[i], Unit.PX); + } + + } else { + for (int i = 0; i < childCount; i++) { + Element e = (Element) getElement().getChild(i); + e.getStyle().setProperty("height", ""); + } + } + } + } + + public VCalendar getParentCalendar() { + return calendar; + } +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/WeekGridMinuteTimeRange.java b/client/src/com/vaadin/client/ui/calendar/schedule/WeekGridMinuteTimeRange.java new file mode 100644 index 0000000000..e634735be7 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/WeekGridMinuteTimeRange.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Date; + +/** + * Internally used by the calendar + * + * @since 7.1 + */ +public class WeekGridMinuteTimeRange { + private final Date start; + private final Date end; + + /** + * Creates a Date time range between start and end date. Drops seconds from + * the range. + * + * @param start + * Start time of the range + * @param end + * End time of the range + * @param clearSeconds + * Boolean Indicates, if seconds should be dropped from the range + * start and end + */ + public WeekGridMinuteTimeRange(Date start, Date end) { + this.start = new Date(start.getTime()); + this.end = new Date(end.getTime()); + this.start.setSeconds(0); + this.end.setSeconds(0); + } + + public Date getStart() { + return start; + } + + public Date getEnd() { + return end; + } + + public static boolean doesOverlap(WeekGridMinuteTimeRange a, + WeekGridMinuteTimeRange b) { + boolean overlaps = a.getStart().compareTo(b.getEnd()) < 0 + && a.getEnd().compareTo(b.getStart()) > 0; + return overlaps; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/WeekLabel.java b/client/src/com/vaadin/client/ui/calendar/schedule/WeekLabel.java new file mode 100644 index 0000000000..bde8675435 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/WeekLabel.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import com.google.gwt.user.client.ui.Label; + +/** + * A label in the {@link SimpleWeekToolbar} + * + * @since 7.1 + */ +public class WeekLabel extends Label { + private int week; + private int year; + + public WeekLabel(String string, int week2, int year2) { + super(string); + setStylePrimaryName("v-calendar-week-number"); + week = week2; + year = year2; + } + + public int getWeek() { + return week; + } + + public void setWeek(int week) { + this.week = week; + } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java b/client/src/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java new file mode 100644 index 0000000000..f7c5c0dac4 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java @@ -0,0 +1,185 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Date; +import java.util.List; + +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.vaadin.client.ui.VCalendar; + +/** + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class WeeklyLongEvents extends HorizontalPanel implements HasTooltipKey { + + public static final int EVENT_HEIGTH = 15; + + public static final int EVENT_MARGIN = 1; + + private int rowCount = 0; + + private VCalendar calendar; + + private boolean undefinedWidth; + + public WeeklyLongEvents(VCalendar calendar) { + setStylePrimaryName("v-calendar-weekly-longevents"); + this.calendar = calendar; + } + + public void addDate(Date d) { + DateCellContainer dcc = new DateCellContainer(); + dcc.setDate(d); + dcc.setCalendar(calendar); + add(dcc); + } + + public void setWidthPX(int width) { + if (getWidgetCount() == 0) { + return; + } + undefinedWidth = (width < 0); + + updateCellWidths(); + } + + public void addEvents(List<CalendarEvent> events) { + for (CalendarEvent e : events) { + addEvent(e); + } + } + + public void addEvent(CalendarEvent calendarEvent) { + updateEventSlot(calendarEvent); + + int dateCount = getWidgetCount(); + Date from = calendarEvent.getStart(); + Date to = calendarEvent.getEnd(); + boolean started = false; + for (int i = 0; i < dateCount; i++) { + DateCellContainer dc = (DateCellContainer) getWidget(i); + Date dcDate = dc.getDate(); + int comp = dcDate.compareTo(from); + int comp2 = dcDate.compareTo(to); + WeeklyLongEventsDateCell eventLabel = dc.getDateCell(calendarEvent + .getSlotIndex()); + eventLabel.setStylePrimaryName("v-calendar-event"); + if (comp >= 0 && comp2 <= 0) { + eventLabel.setEvent(calendarEvent); + eventLabel.setCalendar(calendar); + + eventLabel.addStyleDependentName("all-day"); + if (comp == 0) { + eventLabel.addStyleDependentName("start"); + } + if (comp2 == 0) { + eventLabel.addStyleDependentName("end"); + } + if (!started && comp > 0 && comp2 <= 0) { + eventLabel.addStyleDependentName("continued-from"); + } else if (i == (dateCount - 1)) { + eventLabel.addStyleDependentName("continued-to"); + } + final String extraStyle = calendarEvent.getStyleName(); + if (extraStyle != null && extraStyle.length() > 0) { + eventLabel.addStyleDependentName(extraStyle + "-all-day"); + } + if (!started) { + eventLabel.setText(calendarEvent.getCaption()); + started = true; + } + } + } + } + + private void updateEventSlot(CalendarEvent e) { + boolean foundFreeSlot = false; + int slot = 0; + while (!foundFreeSlot) { + if (isSlotFree(slot, e.getStart(), e.getEnd())) { + e.setSlotIndex(slot); + foundFreeSlot = true; + + } else { + slot++; + } + } + } + + private boolean isSlotFree(int slot, Date start, Date end) { + int dateCount = getWidgetCount(); + + // Go over all dates this week + for (int i = 0; i < dateCount; i++) { + DateCellContainer dc = (DateCellContainer) getWidget(i); + Date dcDate = dc.getDate(); + int comp = dcDate.compareTo(start); + int comp2 = dcDate.compareTo(end); + + // check if the date is in the range we need + if (comp >= 0 && comp2 <= 0) { + + // check if the slot is taken + if (dc.hasEvent(slot)) { + return false; + } + } + } + + return true; + } + + public int getRowCount() { + return rowCount; + } + + public void updateCellWidths() { + int cells = getWidgetCount(); + if (cells <= 0) { + return; + } + + int cellWidth = -1; + + // if width is undefined, use the width of the first cell + // otherwise use distributed sizes + if (undefinedWidth) { + cellWidth = calendar.getWeekGrid().getDateCellWidth() + - calendar.getWeekGrid().getDateSlotBorder(); + } + + for (int i = 0; i < cells; i++) { + DateCellContainer dc = (DateCellContainer) getWidget(i); + + if (undefinedWidth) { + dc.setWidth(cellWidth + "px"); + + } else { + dc.setWidth(calendar.getWeekGrid().getDateCellWidths()[i] + + "px"); + } + } + } + + @Override + public String getTooltipKey() { + return null; + } +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/WeeklyLongEventsDateCell.java b/client/src/com/vaadin/client/ui/calendar/schedule/WeeklyLongEventsDateCell.java new file mode 100644 index 0000000000..a97d352e81 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/WeeklyLongEventsDateCell.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule; + +import java.util.Date; + +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.client.ui.VCalendar; + +/** + * Represents a cell used in {@link WeeklyLongEvents} + * + * @since 7.1 + */ +public class WeeklyLongEventsDateCell extends HTML implements HasTooltipKey { + private Date date; + private CalendarEvent calendarEvent; + private VCalendar calendar; + + public WeeklyLongEventsDateCell() { + } + + public void setDate(Date date) { + this.date = date; + } + + public Date getDate() { + return date; + } + + public void setEvent(CalendarEvent event) { + calendarEvent = event; + } + + public CalendarEvent getEvent() { + return calendarEvent; + } + + public void setCalendar(VCalendar calendar) { + this.calendar = calendar; + } + + public VCalendar getCalendar() { + return calendar; + } + + @Override + public Object getTooltipKey() { + if (calendarEvent != null) { + return calendarEvent.getIndex(); + } + return null; + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarDropHandler.java b/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarDropHandler.java new file mode 100644 index 0000000000..aab9ca9c38 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarDropHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule.dd; + +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ui.calendar.CalendarConnector; +import com.vaadin.client.ui.dd.VAbstractDropHandler; + +/** + * Abstract base class for calendar drop handlers. + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public abstract class CalendarDropHandler extends VAbstractDropHandler { + + protected CalendarConnector calendarConnector; + + /** + * Set the calendar instance + * + * @param calendarPaintable + */ + public void setConnector(CalendarConnector calendarConnector) { + this.calendarConnector = calendarConnector; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#getConnector() + */ + @Override + public CalendarConnector getConnector() { + return calendarConnector; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VDropHandler#getApplicationConnection + * () + */ + @Override + public ApplicationConnection getApplicationConnection() { + return calendarConnector.getClient(); + } + +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java b/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java new file mode 100644 index 0000000000..913477ee14 --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java @@ -0,0 +1,167 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule.dd; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.vaadin.client.Util; +import com.vaadin.client.ui.calendar.schedule.SimpleDayCell; +import com.vaadin.client.ui.dd.VAcceptCallback; +import com.vaadin.client.ui.dd.VDragEvent; + +/** + * Handles DD when the monthly view is showing in the Calendar. In the monthly + * view, drops are only allowed in the the day cells. Only the day index is + * included in the drop details sent to the server. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class CalendarMonthDropHandler extends CalendarDropHandler { + + private Element currentTargetElement; + private SimpleDayCell currentTargetDay; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragAccepted + * (com.vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + protected void dragAccepted(VDragEvent drag) { + deEmphasis(); + currentTargetElement = drag.getElementOver(); + currentTargetDay = Util.findWidget(currentTargetElement, + SimpleDayCell.class); + emphasis(); + } + + /** + * Removed the emphasis CSS style name from the currently emphasized day + */ + private void deEmphasis() { + if (currentTargetElement != null && currentTargetDay != null) { + currentTargetDay.removeEmphasisStyle(); + currentTargetElement = null; + } + } + + /** + * Add CSS style name for the currently emphasized day + */ + private void emphasis() { + if (currentTargetElement != null && currentTargetDay != null) { + currentTargetDay.addEmphasisStyle(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragOver(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragOver(final VDragEvent drag) { + if (isLocationValid(drag.getElementOver())) { + validate(new VAcceptCallback() { + @Override + public void accepted(VDragEvent event) { + dragAccepted(drag); + } + }, drag); + } + } + + /** + * Checks if the one can perform a drop in a element + * + * @param elementOver + * The element to check + * @return + */ + private boolean isLocationValid( + com.google.gwt.user.client.Element elementOver) { + com.google.gwt.user.client.Element monthGridElement = calendarConnector + .getWidget().getMonthGrid().getElement(); + + // drops are not allowed in: + // - weekday header + // - week number bart + return DOM.isOrHasChild(monthGridElement, elementOver); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragEnter(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragEnter(VDragEvent drag) { + // NOOP, we determine drag acceptance in dragOver + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#drop(com.vaadin + * .terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public boolean drop(VDragEvent drag) { + if (isLocationValid(drag.getElementOver())) { + updateDropDetails(drag); + deEmphasis(); + return super.drop(drag); + + } else { + deEmphasis(); + return false; + } + } + + /** + * Updates the drop details sent to the server + * + * @param drag + * The drag event + */ + private void updateDropDetails(VDragEvent drag) { + int dayIndex = calendarConnector.getWidget().getMonthGrid() + .getDayCellIndex(currentTargetDay); + + drag.getDropDetails().put("dropDayIndex", dayIndex); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragLeave(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragLeave(VDragEvent drag) { + deEmphasis(); + super.dragLeave(drag); + } +} diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java b/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java new file mode 100644 index 0000000000..0ea683dc3c --- /dev/null +++ b/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java @@ -0,0 +1,182 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.calendar.schedule.dd; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.vaadin.client.Util; +import com.vaadin.client.ui.calendar.schedule.DateCell; +import com.vaadin.client.ui.calendar.schedule.DateCellDayEvent; +import com.vaadin.client.ui.dd.VAcceptCallback; +import com.vaadin.client.ui.dd.VDragEvent; + +/** + * Handles DD when the weekly view is showing in the Calendar. In the weekly + * view, drops are only allowed in the the time slots for each day. The slot + * index and the day index are included in the drop details sent to the server. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class CalendarWeekDropHandler extends CalendarDropHandler { + + private com.google.gwt.user.client.Element currentTargetElement; + private DateCell currentTargetDay; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragAccepted + * (com.vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + protected void dragAccepted(VDragEvent drag) { + deEmphasis(); + currentTargetElement = drag.getElementOver(); + currentTargetDay = Util + .findWidget(currentTargetElement, DateCell.class); + emphasis(); + } + + /** + * Removes the CSS style name from the emphasized element + */ + private void deEmphasis() { + if (currentTargetElement != null) { + currentTargetDay.removeEmphasisStyle(currentTargetElement); + currentTargetElement = null; + } + } + + /** + * Add a CSS stylen name to current target element + */ + private void emphasis() { + currentTargetDay.addEmphasisStyle(currentTargetElement); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragOver(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragOver(final VDragEvent drag) { + if (isLocationValid(drag.getElementOver())) { + validate(new VAcceptCallback() { + @Override + public void accepted(VDragEvent event) { + dragAccepted(drag); + } + }, drag); + } + } + + /** + * Checks if the location is a valid drop location + * + * @param elementOver + * The element to check + * @return + */ + private boolean isLocationValid( + com.google.gwt.user.client.Element elementOver) { + com.google.gwt.user.client.Element weekGridElement = calendarConnector + .getWidget().getWeekGrid().getElement(); + com.google.gwt.user.client.Element timeBarElement = calendarConnector + .getWidget().getWeekGrid().getTimeBar().getElement(); + + com.google.gwt.user.client.Element todayBarElement = null; + if (calendarConnector.getWidget().getWeekGrid().hasToday()) { + todayBarElement = (Element) calendarConnector.getWidget() + .getWeekGrid().getDateCellOfToday().getTodaybarElement(); + } + + // drops are not allowed in: + // - weekday header + // - allday event list + // - todaybar + // - timebar + // - events + return DOM.isOrHasChild(weekGridElement, elementOver) + && !DOM.isOrHasChild(timeBarElement, elementOver) + && todayBarElement != elementOver + && (Util.findWidget(elementOver, DateCellDayEvent.class) == null); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragEnter(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragEnter(VDragEvent drag) { + // NOOP, we determine drag acceptance in dragOver + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#drop(com.vaadin + * .terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public boolean drop(VDragEvent drag) { + if (isLocationValid(drag.getElementOver())) { + updateDropDetails(drag); + deEmphasis(); + return super.drop(drag); + + } else { + deEmphasis(); + return false; + } + } + + /** + * Update the drop details sent to the server + * + * @param drag + * The drag event + */ + private void updateDropDetails(VDragEvent drag) { + int slotIndex = currentTargetDay.getSlotIndex(currentTargetElement); + int dayIndex = calendarConnector.getWidget().getWeekGrid() + .getDateCellIndex(currentTargetDay); + + drag.getDropDetails().put("dropDayIndex", dayIndex); + drag.getDropDetails().put("dropSlotIndex", slotIndex); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragLeave(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragLeave(VDragEvent drag) { + deEmphasis(); + super.dragLeave(drag); + } +} diff --git a/client/src/com/vaadin/client/ui/checkbox/CheckBoxConnector.java b/client/src/com/vaadin/client/ui/checkbox/CheckBoxConnector.java index 772419e730..85e4e5ee8b 100644 --- a/client/src/com/vaadin/client/ui/checkbox/CheckBoxConnector.java +++ b/client/src/com/vaadin/client/ui/checkbox/CheckBoxConnector.java @@ -69,6 +69,8 @@ public class CheckBoxConnector extends AbstractFieldConnector implements blurHandlerRegistration); if (null != getState().errorMessage) { + getWidget().setAriaInvalid(true); + if (getWidget().errorIndicatorElement == null) { getWidget().errorIndicatorElement = DOM.createSpan(); getWidget().errorIndicatorElement.setInnerHTML(" "); @@ -85,8 +87,11 @@ public class CheckBoxConnector extends AbstractFieldConnector implements } else if (getWidget().errorIndicatorElement != null) { DOM.setStyleAttribute(getWidget().errorIndicatorElement, "display", "none"); + + getWidget().setAriaInvalid(false); } + getWidget().setAriaRequired(isRequired()); if (isReadOnly()) { getWidget().setEnabled(false); } diff --git a/client/src/com/vaadin/client/ui/datefield/InlineDateFieldConnector.java b/client/src/com/vaadin/client/ui/datefield/InlineDateFieldConnector.java index beff3eaa72..2fb40b3cdb 100644 --- a/client/src/com/vaadin/client/ui/datefield/InlineDateFieldConnector.java +++ b/client/src/com/vaadin/client/ui/datefield/InlineDateFieldConnector.java @@ -114,6 +114,8 @@ public class InlineDateFieldConnector extends AbstractDateFieldConnector { public void onStateChanged(StateChangeEvent stateChangeEvent) { super.onStateChanged(stateChangeEvent); getWidget().setTabIndex(getState().tabIndex); + getWidget().calendarPanel.setRangeStart(getState().rangeStart); + getWidget().calendarPanel.setRangeEnd(getState().rangeEnd); } @Override diff --git a/client/src/com/vaadin/client/ui/datefield/PopupDateFieldConnector.java b/client/src/com/vaadin/client/ui/datefield/PopupDateFieldConnector.java index 7246c27b6b..b3bb481658 100644 --- a/client/src/com/vaadin/client/ui/datefield/PopupDateFieldConnector.java +++ b/client/src/com/vaadin/client/ui/datefield/PopupDateFieldConnector.java @@ -119,6 +119,8 @@ public class PopupDateFieldConnector extends TextualDateConnector { + "-button-readonly"); } + getWidget().setDescriptionForAssistiveDevices( + getState().descriptionForAssistiveDevices); getWidget().calendarToggle.setEnabled(true); } @@ -136,6 +138,8 @@ public class PopupDateFieldConnector extends TextualDateConnector { public void onStateChanged(StateChangeEvent stateChangeEvent) { super.onStateChanged(stateChangeEvent); getWidget().setTextFieldEnabled(getState().textFieldEnabled); + getWidget().setRangeStart(getState().rangeStart); + getWidget().setRangeEnd(getState().rangeEnd); } @Override diff --git a/client/src/com/vaadin/client/ui/form/FormConnector.java b/client/src/com/vaadin/client/ui/form/FormConnector.java index 22277b6974..acd0e917fc 100644 --- a/client/src/com/vaadin/client/ui/form/FormConnector.java +++ b/client/src/com/vaadin/client/ui/form/FormConnector.java @@ -231,4 +231,9 @@ public class FormConnector extends AbstractComponentContainerConnector // as a part of the actual layout return null; } + + @Override + public boolean hasTooltip() { + return false; + } } diff --git a/client/src/com/vaadin/client/ui/formlayout/FormLayoutConnector.java b/client/src/com/vaadin/client/ui/formlayout/FormLayoutConnector.java index 1a952959f3..c65f689f7a 100644 --- a/client/src/com/vaadin/client/ui/formlayout/FormLayoutConnector.java +++ b/client/src/com/vaadin/client/ui/formlayout/FormLayoutConnector.java @@ -141,4 +141,13 @@ public class FormLayoutConnector extends AbstractLayoutConnector { return info; } + @Override + public boolean hasTooltip() { + /* + * Tooltips are fetched from child connectors -> there's no quick way of + * checking whether there might a tooltip hiding somewhere + */ + return true; + } + } diff --git a/client/src/com/vaadin/client/ui/menubar/MenuBarConnector.java b/client/src/com/vaadin/client/ui/menubar/MenuBarConnector.java index d8ca73a401..3e22ebb05b 100644 --- a/client/src/com/vaadin/client/ui/menubar/MenuBarConnector.java +++ b/client/src/com/vaadin/client/ui/menubar/MenuBarConnector.java @@ -209,4 +209,14 @@ public class MenuBarConnector extends AbstractComponentConnector implements return info; } + + @Override + public boolean hasTooltip() { + /* + * Item tooltips are not processed until updateFromUIDL, so we can't be + * sure that there are no tooltips during onStateChange when this method + * is used. + */ + return true; + } } diff --git a/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java b/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java index 50de8e0936..cb6ad25e97 100644 --- a/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java +++ b/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java @@ -31,6 +31,7 @@ import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler; import com.vaadin.client.ui.AbstractFieldConnector; import com.vaadin.client.ui.AbstractLayoutConnector; import com.vaadin.client.ui.LayoutClickEventHandler; +import com.vaadin.client.ui.aria.AriaHelper; import com.vaadin.client.ui.layout.ElementResizeEvent; import com.vaadin.client.ui.layout.ElementResizeListener; import com.vaadin.shared.AbstractFieldState; @@ -258,6 +259,10 @@ public abstract class AbstractOrderedLayoutConnector extends slot.setCaption(caption, iconUrlString, styles, error, showError, required, enabled); + AriaHelper.handleInputRequired(child.getWidget(), required); + AriaHelper.handleInputInvalid(child.getWidget(), showError); + AriaHelper.bindCaption(child.getWidget(), slot.getCaptionElement()); + if (slot.hasCaption()) { CaptionPosition pos = slot.getCaptionPosition(); getLayoutManager().addElementResizeListener( diff --git a/client/src/com/vaadin/client/ui/orderedlayout/Slot.java b/client/src/com/vaadin/client/ui/orderedlayout/Slot.java index 5fab131c65..00ff5bbc5a 100644 --- a/client/src/com/vaadin/client/ui/orderedlayout/Slot.java +++ b/client/src/com/vaadin/client/ui/orderedlayout/Slot.java @@ -18,6 +18,7 @@ package com.vaadin.client.ui.orderedlayout; import java.util.List; +import com.google.gwt.aria.client.Roles; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; @@ -507,6 +508,11 @@ public final class Slot extends SimplePanel { // character) requiredIcon.setInnerHTML("*"); requiredIcon.setClassName("v-required-field-indicator"); + + // The star should not be read by the screen reader, as it is + // purely visual. Required state is set at the element level for + // the screen reader. + Roles.getTextboxRole().setAriaHiddenState(requiredIcon, true); } caption.appendChild(requiredIcon); } else if (requiredIcon != null) { diff --git a/client/src/com/vaadin/client/ui/table/TableConnector.java b/client/src/com/vaadin/client/ui/table/TableConnector.java index 6e50bab9ab..36587ffe4d 100644 --- a/client/src/com/vaadin/client/ui/table/TableConnector.java +++ b/client/src/com/vaadin/client/ui/table/TableConnector.java @@ -395,6 +395,16 @@ public class TableConnector extends AbstractHasComponentsConnector implements } @Override + public boolean hasTooltip() { + /* + * Tooltips for individual rows and cells are not processed until + * updateFromUIDL, so we can't be sure that there are no tooltips during + * onStateChange when this method is used. + */ + return true; + } + + @Override public void onConnectorHierarchyChange( ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent) { // TODO Move code from updateFromUIDL to this method diff --git a/client/src/com/vaadin/client/ui/tabsheet/TabsheetConnector.java b/client/src/com/vaadin/client/ui/tabsheet/TabsheetConnector.java index f1ad5e792a..04a514738d 100644 --- a/client/src/com/vaadin/client/ui/tabsheet/TabsheetConnector.java +++ b/client/src/com/vaadin/client/ui/tabsheet/TabsheetConnector.java @@ -138,6 +138,16 @@ public class TabsheetConnector extends TabsheetBaseConnector implements } @Override + public boolean hasTooltip() { + /* + * Tab tooltips are not processed until updateFromUIDL, so we can't be + * sure that there are no tooltips during onStateChange when this method + * is used. + */ + return true; + } + + @Override public void onConnectorHierarchyChange( ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent) { // TODO Move code from updateFromUIDL to this method diff --git a/client/src/com/vaadin/client/ui/tree/TreeConnector.java b/client/src/com/vaadin/client/ui/tree/TreeConnector.java index d6d1cef8cc..ef016c31b7 100644 --- a/client/src/com/vaadin/client/ui/tree/TreeConnector.java +++ b/client/src/com/vaadin/client/ui/tree/TreeConnector.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import com.google.gwt.aria.client.Roles; import com.google.gwt.dom.client.Element; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.BrowserInfo; @@ -26,6 +27,7 @@ import com.vaadin.client.Paintable; import com.vaadin.client.TooltipInfo; import com.vaadin.client.UIDL; import com.vaadin.client.Util; +import com.vaadin.client.VConsole; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.AbstractComponentConnector; import com.vaadin.client.ui.VTree; @@ -93,7 +95,7 @@ public class TreeConnector extends AbstractComponentConnector implements } childTree = getWidget().new TreeNode(); getConnection().getVTooltip().connectHandlersToWidget(childTree); - updateNodeFromUIDL(childTree, childUidl); + updateNodeFromUIDL(childTree, childUidl, 1); getWidget().body.add(childTree); childTree.addStyleDependentName("root"); childTree.childNodeContainer.addStyleDependentName("root"); @@ -108,6 +110,9 @@ public class TreeConnector extends AbstractComponentConnector implements getWidget().isMultiselect = "multi".equals(selectMode); if (getWidget().isMultiselect) { + Roles.getTreeRole().setAriaMultiselectableProperty( + getWidget().getElement(), true); + if (BrowserInfo.get().isTouchDevice()) { // Always use the simple mode for touch devices that do not have // shift/ctrl keys (#8595) @@ -116,6 +121,9 @@ public class TreeConnector extends AbstractComponentConnector implements getWidget().multiSelectMode = MultiSelectMode.valueOf(uidl .getStringAttribute("multiselectmode")); } + } else { + Roles.getTreeRole().setAriaMultiselectableProperty( + getWidget().getElement(), false); } getWidget().selectedIds = uidl.getStringArrayVariableAsSet("selected"); @@ -169,7 +177,18 @@ public class TreeConnector extends AbstractComponentConnector implements // expanding node happened server side rootNode.setState(true, false); } - renderChildNodes(rootNode, (Iterator) uidl.getChildIterator()); + String levelPropertyString = Roles.getTreeitemRole() + .getAriaLevelProperty(rootNode.getElement()); + int levelProperty; + try { + levelProperty = Integer.valueOf(levelPropertyString); + } catch (NumberFormatException e) { + levelProperty = 1; + VConsole.error(e); + } + + renderChildNodes(rootNode, (Iterator) uidl.getChildIterator(), + levelProperty + 1); } } @@ -196,7 +215,10 @@ public class TreeConnector extends AbstractComponentConnector implements } - public void updateNodeFromUIDL(TreeNode treeNode, UIDL uidl) { + public void updateNodeFromUIDL(TreeNode treeNode, UIDL uidl, int level) { + Roles.getTreeitemRole().setAriaLevelProperty(treeNode.getElement(), + level); + String nodeKey = uidl.getStringAttribute("key"); treeNode.setText(uidl .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_CAPTION)); @@ -212,7 +234,8 @@ public class TreeConnector extends AbstractComponentConnector implements if (uidl.getChildCount() == 0) { treeNode.childNodeContainer.setVisible(false); } else { - renderChildNodes(treeNode, (Iterator) uidl.getChildIterator()); + renderChildNodes(treeNode, (Iterator) uidl.getChildIterator(), + level + 1); treeNode.childrenLoaded = true; } } else { @@ -239,11 +262,14 @@ public class TreeConnector extends AbstractComponentConnector implements getWidget().selectedIds.add(nodeKey); } - treeNode.setIcon(uidl - .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON)); + String iconUrl = uidl + .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON); + String iconAltText = uidl + .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON_ALT); + treeNode.setIcon(iconUrl, iconAltText); } - void renderChildNodes(TreeNode containerNode, Iterator<UIDL> i) { + void renderChildNodes(TreeNode containerNode, Iterator<UIDL> i, int level) { containerNode.childNodeContainer.clear(); containerNode.childNodeContainer.setVisible(true); while (i.hasNext()) { @@ -256,7 +282,7 @@ public class TreeConnector extends AbstractComponentConnector implements } final TreeNode childTree = getWidget().new TreeNode(); getConnection().getVTooltip().connectHandlersToWidget(childTree); - updateNodeFromUIDL(childTree, childUidl); + updateNodeFromUIDL(childTree, childUidl, level); containerNode.childNodeContainer.add(childTree); if (!i.hasNext()) { childTree @@ -306,4 +332,14 @@ public class TreeConnector extends AbstractComponentConnector implements return info; } + @Override + public boolean hasTooltip() { + /* + * Item tooltips are not processed until updateFromUIDL, so we can't be + * sure that there are no tooltips during onStateChange when this method + * is used. + */ + return true; + } + } diff --git a/client/src/com/vaadin/client/ui/ui/UIConnector.java b/client/src/com/vaadin/client/ui/ui/UIConnector.java index 593aa0d793..643d687f1d 100644 --- a/client/src/com/vaadin/client/ui/ui/UIConnector.java +++ b/client/src/com/vaadin/client/ui/ui/UIConnector.java @@ -21,9 +21,13 @@ 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.HeadElement; +import com.google.gwt.dom.client.LinkElement; 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.StyleInjector; import com.google.gwt.event.dom.client.ScrollEvent; import com.google.gwt.event.dom.client.ScrollHandler; import com.google.gwt.event.logical.shared.ResizeEvent; @@ -34,6 +38,7 @@ 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.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; @@ -45,7 +50,6 @@ import com.vaadin.client.ConnectorHierarchyChangeEvent; import com.vaadin.client.ConnectorMap; import com.vaadin.client.Focusable; import com.vaadin.client.Paintable; -import com.vaadin.client.TooltipInfo; import com.vaadin.client.UIDL; import com.vaadin.client.VConsole; import com.vaadin.client.communication.StateChangeEvent; @@ -57,12 +61,16 @@ import com.vaadin.client.ui.VNotification; import com.vaadin.client.ui.VUI; import com.vaadin.client.ui.layout.MayScrollChildren; import com.vaadin.client.ui.window.WindowConnector; +import com.vaadin.server.Page.Styles; import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.communication.MethodInvocation; import com.vaadin.shared.ui.ComponentStateUtil; import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.Connect.LoadStyle; import com.vaadin.shared.ui.ui.PageClientRpc; +import com.vaadin.shared.ui.ui.PageState; import com.vaadin.shared.ui.ui.ScrollClientRpc; +import com.vaadin.shared.ui.ui.UIClientRpc; import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.shared.ui.ui.UIServerRpc; import com.vaadin.shared.ui.ui.UIState; @@ -91,6 +99,12 @@ public class UIConnector extends AbstractSingleComponentContainerConnector public void setTitle(String title) { com.google.gwt.user.client.Window.setTitle(title); } + + @Override + public void reload() { + Window.Location.reload(); + + } }); registerRpc(ScrollClientRpc.class, new ScrollClientRpc() { @Override @@ -103,13 +117,29 @@ public class UIConnector extends AbstractSingleComponentContainerConnector getWidget().getElement().setScrollLeft(scrollLeft); } }); + registerRpc(UIClientRpc.class, new UIClientRpc() { + @Override + public void uiClosed(final boolean sessionExpired) { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + if (sessionExpired) { + getConnection().showSessionExpiredError(null); + } else { + getState().enabled = false; + updateEnabledState(getState().enabled); + } + } + }); + } + }); getWidget().addResizeHandler(new ResizeHandler() { @Override public void onResize(ResizeEvent event) { getRpcProxy(UIServerRpc.class).resize(event.getHeight(), event.getWidth(), Window.getClientWidth(), Window.getClientHeight()); - if (getState().immediate) { + if (getState().immediate || getPageState().hasResizeListeners) { getConnection().sendPendingVariableChanges(); } } @@ -257,6 +287,8 @@ public class UIConnector extends AbstractSingleComponentContainerConnector final UIDL notification = (UIDL) it.next(); VNotification.showNotification(client, notification); } + } else if (tag == "css-injections") { + injectCSS(childUidl); } } @@ -326,6 +358,47 @@ public class UIConnector extends AbstractSingleComponentContainerConnector getWidget().rendering = false; } + /** + * Reads CSS strings and resources injected by {@link Styles#inject} from + * the UIDL stream. + * + * @param uidl + * The uidl which contains "css-resource" and "css-string" tags + */ + private void injectCSS(UIDL uidl) { + + final HeadElement head = HeadElement.as(Document.get() + .getElementsByTagName(HeadElement.TAG).getItem(0)); + + /* + * Search the UIDL stream for CSS resources and strings to be injected. + */ + for (Iterator<?> it = uidl.getChildIterator(); it.hasNext();) { + UIDL cssInjectionsUidl = (UIDL) it.next(); + + // Check if we have resources to inject + if (cssInjectionsUidl.getTag().equals("css-resource")) { + String url = getWidget().connection + .translateVaadinUri(cssInjectionsUidl + .getStringAttribute("url")); + LinkElement link = LinkElement.as(DOM + .createElement(LinkElement.TAG)); + link.setRel("stylesheet"); + link.setHref(url); + link.setType("text/css"); + head.appendChild(link); + + // Check if we have CSS string to inject + } else if (cssInjectionsUidl.getTag().equals("css-string")) { + for (Iterator<?> it2 = cssInjectionsUidl.getChildIterator(); it2 + .hasNext();) { + StyleInjector.injectAtEnd((String) it2.next()); + StyleInjector.flush(); + } + } + } + } + public void init(String rootPanelId, ApplicationConnection applicationConnection) { DOM.sinkEvents(getWidget().getElement(), Event.ONKEYDOWN @@ -339,8 +412,6 @@ public class UIConnector extends AbstractSingleComponentContainerConnector String themeName = applicationConnection.getConfiguration() .getThemeName(); - // Remove chars that are not suitable for style names - themeName = themeName.replaceAll("[^a-zA-Z0-9]", ""); root.addStyleName(themeName); root.add(getWidget()); @@ -368,6 +439,8 @@ public class UIConnector extends AbstractSingleComponentContainerConnector }; + private Timer pollTimer = null; + @Override public void updateCaption(ComponentConnector component) { // NOP The main view never draws caption for its layout @@ -432,6 +505,23 @@ public class UIConnector extends AbstractSingleComponentContainerConnector return (UIState) super.getState(); } + /** + * Returns the state of the Page associated with the UI. + * <p> + * Note that state is considered an internal part of the connector. You + * should not rely on the state object outside of the connector who owns it. + * If you depend on the state of other connectors you should use their + * public API instead of their state object directly. The page state might + * not be an independent state object but can be embedded in UI state. + * </p> + * + * @since 7.1 + * @return state object of the page + */ + public PageState getPageState() { + return getState().pageState; + } + @Override public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) { ComponentConnector oldChild = null; @@ -477,13 +567,13 @@ public class UIConnector extends AbstractSingleComponentContainerConnector } @Override - public TooltipInfo getTooltipInfo(com.google.gwt.dom.client.Element element) { + public boolean hasTooltip() { /* - * Override method to make AbstractComponentConnector.hasTooltip() - * return true so there's a top level handler that takes care of hiding - * tooltips whenever the mouse is moved somewhere else. + * Always return true so there's always top level tooltip handler that + * takes care of hiding tooltips whenever the mouse is moved somewhere + * else. */ - return super.getTooltipInfo(element); + return true; } /** @@ -505,4 +595,74 @@ public class UIConnector extends AbstractSingleComponentContainerConnector } }); } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + if (stateChangeEvent.hasPropertyChanged("tooltipConfiguration")) { + getConnection().getVTooltip().setCloseTimeout( + getState().tooltipConfiguration.closeTimeout); + getConnection().getVTooltip().setOpenDelay( + getState().tooltipConfiguration.openDelay); + getConnection().getVTooltip().setQuickOpenDelay( + getState().tooltipConfiguration.quickOpenDelay); + getConnection().getVTooltip().setQuickOpenTimeout( + getState().tooltipConfiguration.quickOpenTimeout); + getConnection().getVTooltip().setMaxWidth( + getState().tooltipConfiguration.maxWidth); + } + + if (stateChangeEvent + .hasPropertyChanged("loadingIndicatorConfiguration")) { + getConnection().getLoadingIndicator().setFirstDelay( + getState().loadingIndicatorConfiguration.firstDelay); + getConnection().getLoadingIndicator().setSecondDelay( + getState().loadingIndicatorConfiguration.secondDelay); + getConnection().getLoadingIndicator().setThirdDelay( + getState().loadingIndicatorConfiguration.thirdDelay); + } + + if (stateChangeEvent.hasPropertyChanged("pollInterval")) { + configurePolling(); + } + + if (stateChangeEvent.hasPropertyChanged("pushMode")) { + getConnection().setPushEnabled(getState().pushMode.isEnabled()); + } + + if (stateChangeEvent.hasPropertyChanged("overlayContainerLabel")) { + getConnection().setOverlayContainerLabel( + getState().overlayContainerLabel); + } + } + + private void configurePolling() { + if (pollTimer != null) { + pollTimer.cancel(); + pollTimer = null; + } + if (getState().pollInterval >= 0) { + pollTimer = new Timer() { + @Override + public void run() { + /* + * Verify that polling has not recently been canceled. This + * is needed because Timer.cancel() does not always work + * properly in IE 8 until GWT issue 8101 has been fixed. + */ + if (pollTimer != null) { + getRpcProxy(UIServerRpc.class).poll(); + // Send changes even though poll is @Delayed + getConnection().sendPendingVariableChanges(); + } + } + }; + pollTimer.scheduleRepeating(getState().pollInterval); + } else { + // Ensure no more polls are sent as polling has been disabled + getConnection().removePendingInvocations( + new MethodInvocation(getConnectorId(), UIServerRpc.class + .getName(), "poll")); + } + } } diff --git a/client/src/com/vaadin/client/ui/window/WindowConnector.java b/client/src/com/vaadin/client/ui/window/WindowConnector.java index 8cfc25a9dc..90311e30ad 100644 --- a/client/src/com/vaadin/client/ui/window/WindowConnector.java +++ b/client/src/com/vaadin/client/ui/window/WindowConnector.java @@ -20,7 +20,10 @@ 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.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.DoubleClickEvent; +import com.google.gwt.event.dom.client.DoubleClickHandler; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Window; import com.vaadin.client.ApplicationConnection; @@ -31,6 +34,7 @@ import com.vaadin.client.LayoutManager; import com.vaadin.client.Paintable; import com.vaadin.client.UIDL; import com.vaadin.client.Util; +import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.AbstractSingleComponentContainerConnector; import com.vaadin.client.ui.ClickEventHandler; import com.vaadin.client.ui.PostLayoutListener; @@ -41,6 +45,7 @@ import com.vaadin.client.ui.VWindow; import com.vaadin.client.ui.layout.MayScrollChildren; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.window.WindowMode; import com.vaadin.shared.ui.window.WindowServerRpc; import com.vaadin.shared.ui.window.WindowState; @@ -57,7 +62,32 @@ public class WindowConnector extends AbstractSingleComponentContainerConnector } }; - boolean minWidthChecked = false; + abstract class WindowEventHandler implements ClickHandler, + DoubleClickHandler { + } + + private WindowEventHandler maximizeRestoreClickHandler = new WindowEventHandler() { + + @Override + public void onClick(ClickEvent event) { + final Element target = event.getNativeEvent().getEventTarget() + .cast(); + if (target == getWidget().maximizeRestoreBox) { + // Click on maximize/restore box + onMaximizeRestore(); + } + } + + @Override + public void onDoubleClick(DoubleClickEvent event) { + final Element target = event.getNativeEvent().getEventTarget() + .cast(); + if (getWidget().header.isOrHasChild(target)) { + // Double click on header + onMaximizeRestore(); + } + } + }; @Override public boolean delegateCaptionHandling() { @@ -68,12 +98,18 @@ public class WindowConnector extends AbstractSingleComponentContainerConnector protected void init() { super.init(); + VWindow window = getWidget(); + getLayoutManager().registerDependency(this, - getWidget().contentPanel.getElement()); - getLayoutManager().registerDependency(this, getWidget().header); - getLayoutManager().registerDependency(this, getWidget().footer); + window.contentPanel.getElement()); + getLayoutManager().registerDependency(this, window.header); + getLayoutManager().registerDependency(this, window.footer); - getWidget().setOwner(getConnection().getUIConnector().getWidget()); + window.addHandler(maximizeRestoreClickHandler, ClickEvent.getType()); + window.addHandler(maximizeRestoreClickHandler, + DoubleClickEvent.getType()); + + window.setOwner(getConnection().getUIConnector().getWidget()); } @Override @@ -87,109 +123,46 @@ public class WindowConnector extends AbstractSingleComponentContainerConnector @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().modal != getWidget().vaadinModality) { - getWidget().setVaadinModality(!getWidget().vaadinModality); - } - if (!getWidget().isAttached()) { - getWidget().setVisible(false); // hide until - // possible centering - getWidget().show(); - } - if (getState().resizable != getWidget().resizable) { - getWidget().setResizable(getState().resizable); - } - getWidget().resizeLazy = getState().resizeLazy; + VWindow window = getWidget(); + String connectorId = getConnectorId(); - getWidget().setDraggable(getState().draggable); + window.id = getConnectorId(); + window.client = client; - // 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 (getIcon() != null) { - iconURL = getIcon(); - } - getWidget().setCaption(getState().caption, iconURL); - } + // Workaround needed for Testing Tools (GWT generates window DOM + // slightly different in different browsers). + window.closeBox.setId(connectorId + "_window_close"); + window.maximizeRestoreBox + .setId(connectorId + "_window_maximizerestore"); - getWidget().visibilityChangesDisabled = true; + window.visibilityChangesDisabled = true; if (!isRealUpdate(uidl)) { return; } - getWidget().visibilityChangesDisabled = false; - - clickEventHandler.handleEventHandlerRegistration(); - - getWidget().immediate = getState().immediate; - - getWidget().setClosable(!isReadOnly()); - - // Initialize the position form UIDL - int positionx = getState().positionX; - int positiony = getState().positionY; - if (positionx >= 0 || positiony >= 0) { - if (positionx < 0) { - positionx = 0; - } - if (positiony < 0) { - positiony = 0; - } - getWidget().setPopupPosition(positionx, positiony); - } - - int childIndex = 0; + window.visibilityChangesDisabled = false; // 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); + if (window.shortcutHandler == null) { + window.shortcutHandler = new ShortcutActionHandler( + connectorId, client); } - getWidget().shortcutHandler.updateActionMap(childUidl); + window.shortcutHandler.updateActionMap(childUidl); } } - // setting scrollposition must happen after children is rendered - getWidget().contentPanel.setScrollPosition(getState().scrollTop); - getWidget().contentPanel - .setHorizontalScrollPosition(getState().scrollLeft); - - // 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().centered; - 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"); + window.contentPanel.focus(); + window.bringToFrontSequence = uidl.getIntAttribute("bringToFront"); VWindow.deferOrdering(); } } @@ -224,26 +197,6 @@ public class WindowConnector extends AbstractSingleComponentContainerConnector boolean hasContent = (content != null); Element contentElement = window.contentPanel.getElement(); - if (!minWidthChecked) { - boolean needsMinWidth = !isUndefinedWidth() || !hasContent - || content.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() || !hasContent - || content.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); @@ -291,9 +244,8 @@ public class WindowConnector extends AbstractSingleComponentContainerConnector @Override public void postLayout() { - minWidthChecked = false; VWindow window = getWidget(); - if (window.centered) { + if (window.centered && getState().windowMode != WindowMode.MAXIMIZED) { window.center(); } window.positionOrSizeUpdated(); @@ -304,6 +256,126 @@ public class WindowConnector extends AbstractSingleComponentContainerConnector return (WindowState) super.getState(); } + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + VWindow window = getWidget(); + WindowState state = getState(); + + if (state.modal != window.vaadinModality) { + window.setVaadinModality(!window.vaadinModality); + } + if (!window.isAttached()) { + window.setVisible(false); // hide until possible centering + window.show(); + } + boolean resizeable = state.resizable + && state.windowMode == WindowMode.NORMAL; + window.setResizable(resizeable); + + window.resizeLazy = state.resizeLazy; + + window.setDraggable(state.draggable + && state.windowMode == WindowMode.NORMAL); + + window.updateMaximizeRestoreClassName(state.resizable, state.windowMode); + + // 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 (getIcon() != null) { + iconURL = getIcon(); + } + window.setCaption(state.caption, iconURL); + + clickEventHandler.handleEventHandlerRegistration(); + + window.immediate = state.immediate; + + window.setClosable(!isReadOnly()); + // initialize position from state + updateWindowPosition(); + + // setting scrollposition must happen after children is rendered + window.contentPanel.setScrollPosition(state.scrollTop); + window.contentPanel.setHorizontalScrollPosition(state.scrollLeft); + + // 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 + window.centered = state.centered; + window.setVisible(true); + + // ensure window is not larger than browser window + if (window.getOffsetWidth() > Window.getClientWidth()) { + window.setWidth(Window.getClientWidth() + "px"); + } + if (window.getOffsetHeight() > Window.getClientHeight()) { + window.setHeight(Window.getClientHeight() + "px"); + } + } + + // Need to override default because of window mode + @Override + protected void updateComponentSize() { + if (getState().windowMode == WindowMode.NORMAL) { + super.updateComponentSize(); + } else if (getState().windowMode == WindowMode.MAXIMIZED) { + super.updateComponentSize("100%", "100%"); + } + } + + protected void updateWindowPosition() { + VWindow window = getWidget(); + WindowState state = getState(); + if (state.windowMode == WindowMode.NORMAL) { + // if centered, position handled in postLayout() + if (!state.centered + && (state.positionX >= 0 || state.positionY >= 0)) { + // If both positions are negative, then + // setWindowOrderAndPosition has already taken care of + // positioning the window so it stacks with other windows + window.setPopupPosition(state.positionX, state.positionY); + } + } else if (state.windowMode == WindowMode.MAXIMIZED) { + window.setPopupPositionNoUpdate(0, 0); + window.bringToFront(); + } + } + + protected void updateWindowMode() { + VWindow window = getWidget(); + WindowState state = getState(); + + // update draggable on widget + window.setDraggable(state.draggable + && state.windowMode == WindowMode.NORMAL); + // update resizable on widget + window.setResizable(state.resizable + && state.windowMode == WindowMode.NORMAL); + updateComponentSize(); + updateWindowPosition(); + window.updateMaximizeRestoreClassName(state.resizable, state.windowMode); + window.updateContentsSize(); + } + + protected void onMaximizeRestore() { + WindowState state = getState(); + if (state.resizable) { + if (state.windowMode == WindowMode.MAXIMIZED) { + state.windowMode = WindowMode.NORMAL; + } else { + state.windowMode = WindowMode.MAXIMIZED; + } + updateWindowMode(); + getRpcProxy(WindowServerRpc.class).windowModeChanged( + state.windowMode); + } + } + /** * Gives the WindowConnector an order number. As a side effect, moves the * window according to its order number so the windows are stacked. This |