From 422c08c6145b8c8eaff4f453b5c0276beef27290 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 30 Jul 2014 07:53:28 +0000 Subject: [PATCH] Allow changing theme on the fly (#2874, #14139, #14124) * Updates UI and overlay container class names when the theme changes * Initially verifies that the theme has actually been loaded (for the embed case) and class names have been properly set * Forces a state change to all components to re-translate theme:// URLs * Runs a full layout after the new theme has been loaded and activated Change-Id: I5a7391abe1bb467130bbb4660e4829b43f3e4255 --- WebContent/release-notes.html | 1 + .../client/ApplicationConfiguration.java | 17 +- .../vaadin/client/ApplicationConnection.java | 5 +- .../src/com/vaadin/client/ResourceLoader.java | 23 +- .../communication/TranslatedURLReference.java | 45 +++ .../URLReference_Serializer.java | 12 +- .../client/debug/internal/InfoSection.java | 2 +- .../vaadin/client/ui/AbstractConnector.java | 24 ++ client/src/com/vaadin/client/ui/VOverlay.java | 6 +- client/src/com/vaadin/client/ui/VUI.java | 25 +- .../com/vaadin/client/ui/ui/UIConnector.java | 307 ++++++++++++++++-- server/src/com/vaadin/ui/UI.java | 27 +- .../src/com/vaadin/shared/ui/ui/UIState.java | 8 +- .../com/vaadin/tests/tb3/AbstractTB3Test.java | 48 ++- .../tests/themes/ThemeChangeOnTheFly.java | 109 +++++++ .../tests/themes/ThemeChangeOnTheFlyTest.java | 121 +++++++ 16 files changed, 701 insertions(+), 79 deletions(-) create mode 100644 client/src/com/vaadin/client/communication/TranslatedURLReference.java create mode 100644 uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFly.java create mode 100644 uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFlyTest.java diff --git a/WebContent/release-notes.html b/WebContent/release-notes.html index ac4e27d8a3..0ede61d729 100644 --- a/WebContent/release-notes.html +++ b/WebContent/release-notes.html @@ -101,6 +101,7 @@ the Sass CSS preprocessor heavily, providing a variety of ways to customize the look and feel of your theme. See the Valo theme tutorial or the Valo theme section in Book of Vaadin for information on how to get started. +
  • Support for changing theme on the fly
  • diff --git a/client/src/com/vaadin/client/ApplicationConfiguration.java b/client/src/com/vaadin/client/ApplicationConfiguration.java index 3ccbeba6f3..87c8ea465f 100644 --- a/client/src/com/vaadin/client/ApplicationConfiguration.java +++ b/client/src/com/vaadin/client/ApplicationConfiguration.java @@ -51,6 +51,7 @@ import com.vaadin.client.metadata.ConnectorBundleLoader; import com.vaadin.client.metadata.NoDataException; import com.vaadin.client.metadata.TypeData; import com.vaadin.client.ui.UnknownComponentConnector; +import com.vaadin.client.ui.ui.UIConnector; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.ui.ui.UIConstants; @@ -84,7 +85,7 @@ public class ApplicationConfiguration implements EntryPoint { return null; } else { return value +""; - } + } }-*/; /** @@ -105,7 +106,7 @@ public class ApplicationConfiguration implements EntryPoint { } else { // $entry not needed as function is not exported return @java.lang.Boolean::valueOf(Z)(value); - } + } }-*/; /** @@ -126,7 +127,7 @@ public class ApplicationConfiguration implements EntryPoint { } else { // $entry not needed as function is not exported return @java.lang.Integer::valueOf(I)(value); - } + } }-*/; /** @@ -285,14 +286,16 @@ public class ApplicationConfiguration implements EntryPoint { return serviceUrl; } + /** + * @return the theme name used when initializing the application + * @deprecated as of 7.3. Use {@link UIConnector#getActiveTheme()} to get + * the theme currently in use + */ + @Deprecated public String getThemeName() { return getJsoConfiguration(id).getConfigString("theme"); } - public String getThemeUri() { - return getVaadinDirUrl() + "themes/" + getThemeName(); - } - /** * Gets the URL of the VAADIN directory on the server. * diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java index b569c0b17c..6e2c6e757c 100644 --- a/client/src/com/vaadin/client/ApplicationConnection.java +++ b/client/src/com/vaadin/client/ApplicationConnection.java @@ -3110,7 +3110,7 @@ public class ApplicationConnection implements HasHandlers { return null; } if (uidlUri.startsWith("theme://")) { - final String themeUri = configuration.getThemeUri(); + final String themeUri = getThemeUri(); if (themeUri == null) { VConsole.error("Theme not set: ThemeResource will not be found. (" + uidlUri + ")"); @@ -3176,7 +3176,8 @@ public class ApplicationConnection implements HasHandlers { * @return URI to the current theme */ public String getThemeUri() { - return configuration.getThemeUri(); + return configuration.getVaadinDirUrl() + "themes/" + + getUIConnector().getActiveTheme(); } /** diff --git a/client/src/com/vaadin/client/ResourceLoader.java b/client/src/com/vaadin/client/ResourceLoader.java index 68a16e8162..ceede263fc 100644 --- a/client/src/com/vaadin/client/ResourceLoader.java +++ b/client/src/com/vaadin/client/ResourceLoader.java @@ -375,7 +375,20 @@ public class ResourceLoader { } } - private native void addOnloadHandler(Element element, + /** + * Adds an onload listener to the given element, which should be a link or a + * script tag. The listener is called whenever loading is complete or an + * error occurred. + * + * @since 7.3 + * @param element + * the element to attach a listener to + * @param listener + * the listener to call + * @param event + * the event passed to the listener + */ + public static native void addOnloadHandler(Element element, ResourceLoadListener listener, ResourceLoadEvent event) /*-{ element.onload = $entry(function() { @@ -390,11 +403,11 @@ public class ResourceLoader { element.onreadystatechange = null; listener.@com.vaadin.client.ResourceLoader.ResourceLoadListener::onError(Lcom/vaadin/client/ResourceLoader$ResourceLoadEvent;)(event); }); - element.onreadystatechange = function() { + element.onreadystatechange = function() { if ("loaded" === element.readyState || "complete" === element.readyState ) { element.onload(arguments[0]); } - }; + }; }-*/; /** @@ -520,12 +533,12 @@ public class ResourceLoader { if (rules === undefined) { rules = sheet.rules; } - + if (rules === null) { // Style sheet loaded, but can't access length because of XSS -> assume there's something there return 1; } - + // Return length so we can distinguish 0 (probably 404 error) from normal case. return rules.length; } catch (err) { diff --git a/client/src/com/vaadin/client/communication/TranslatedURLReference.java b/client/src/com/vaadin/client/communication/TranslatedURLReference.java new file mode 100644 index 0000000000..b99f4c6e32 --- /dev/null +++ b/client/src/com/vaadin/client/communication/TranslatedURLReference.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS 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.vaadin.client.ApplicationConnection; +import com.vaadin.shared.communication.URLReference; + +/** + * A URLReference implementation which does late URL translation to be able to + * re-translate URLs if e.g. the theme changes + * + * @since 7.3 + * @author Vaadin Ltd + */ +public class TranslatedURLReference extends URLReference { + + private ApplicationConnection connection; + + /** + * @param connection + * the connection to set + */ + public void setConnection(ApplicationConnection connection) { + this.connection = connection; + } + + @Override + public String getURL() { + return connection.translateVaadinUri(super.getURL()); + } + +} diff --git a/client/src/com/vaadin/client/communication/URLReference_Serializer.java b/client/src/com/vaadin/client/communication/URLReference_Serializer.java index 586dd626f0..4ecdc606d2 100644 --- a/client/src/com/vaadin/client/communication/URLReference_Serializer.java +++ b/client/src/com/vaadin/client/communication/URLReference_Serializer.java @@ -1,12 +1,12 @@ /* * Copyright 2000-2014 Vaadin Ltd. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -30,14 +30,16 @@ public class URLReference_Serializer implements JSONSerializer { @Override public URLReference deserialize(Type type, JSONValue jsonValue, ApplicationConnection connection) { - URLReference reference = GWT.create(URLReference.class); + TranslatedURLReference reference = GWT + .create(TranslatedURLReference.class); + reference.setConnection(connection); JSONObject json = (JSONObject) jsonValue; if (json.containsKey(URL_FIELD)) { JSONValue jsonURL = json.get(URL_FIELD); String URL = (String) JsonDecoder.decodeValue( new Type(String.class.getName(), null), jsonURL, null, connection); - reference.setURL(connection.translateVaadinUri(URL)); + reference.setURL(URL); } return reference; } diff --git a/client/src/com/vaadin/client/debug/internal/InfoSection.java b/client/src/com/vaadin/client/debug/internal/InfoSection.java index 23b77a94db..a7a84f5f8f 100644 --- a/client/src/com/vaadin/client/debug/internal/InfoSection.java +++ b/client/src/com/vaadin/client/debug/internal/InfoSection.java @@ -163,7 +163,7 @@ public class InfoSection implements Section { addVersionInfo(configuration); addRow("Widget set", GWT.getModuleName()); - addRow("Theme", connection.getConfiguration().getThemeName()); + addRow("Theme", connection.getUIConnector().getActiveTheme()); String communicationMethodInfo = connection .getCommunicationMethodName(); diff --git a/client/src/com/vaadin/client/ui/AbstractConnector.java b/client/src/com/vaadin/client/ui/AbstractConnector.java index a2e0d9cd54..e93ea0f507 100644 --- a/client/src/com/vaadin/client/ui/AbstractConnector.java +++ b/client/src/com/vaadin/client/ui/AbstractConnector.java @@ -28,6 +28,7 @@ import com.google.gwt.event.shared.HandlerManager; import com.google.web.bindery.event.shared.HandlerRegistration; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.FastStringMap; +import com.vaadin.client.FastStringSet; import com.vaadin.client.JsArrayObject; import com.vaadin.client.Profiler; import com.vaadin.client.ServerConnector; @@ -479,4 +480,27 @@ public abstract class AbstractConnector implements ServerConnector, Set reg = getState().registeredEventListeners; return (reg != null && reg.contains(eventIdentifier)); } + + /** + * Force the connector to recheck its state variables as the variables or + * their meaning might have changed. + * + * @since 7.3 + */ + public void forceStateChange() { + StateChangeEvent event = new FullStateChangeEvent(this); + fireEvent(event); + } + + private static class FullStateChangeEvent extends StateChangeEvent { + public FullStateChangeEvent(ServerConnector connector) { + super(connector, FastStringSet.create()); + } + + @Override + public boolean hasPropertyChanged(String property) { + return true; + } + + } } diff --git a/client/src/com/vaadin/client/ui/VOverlay.java b/client/src/com/vaadin/client/ui/VOverlay.java index c62e2c9824..afa13dc337 100644 --- a/client/src/com/vaadin/client/ui/VOverlay.java +++ b/client/src/com/vaadin/client/ui/VOverlay.java @@ -881,7 +881,9 @@ public class VOverlay extends PopupPanel implements CloseHandler { container.setId(id); String styles = ac.getUIConnector().getWidget().getParent() .getStyleName(); - container.addClassName(styles); + if (styles != null && !styles.equals("")) { + container.addClassName(styles); + } container.addClassName(CLASSNAME_CONTAINER); RootPanel.get().getElement().appendChild(container); } @@ -1059,4 +1061,4 @@ public class VOverlay extends PopupPanel implements CloseHandler { } } } -} \ No newline at end of file +} diff --git a/client/src/com/vaadin/client/ui/VUI.java b/client/src/com/vaadin/client/ui/VUI.java index df24c3b1c7..eae4f6319d 100644 --- a/client/src/com/vaadin/client/ui/VUI.java +++ b/client/src/com/vaadin/client/ui/VUI.java @@ -48,11 +48,12 @@ import com.vaadin.client.Util; import com.vaadin.client.VConsole; import com.vaadin.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; import com.vaadin.client.ui.TouchScrollDelegate.TouchScrollHandler; +import com.vaadin.client.ui.ui.UIConnector; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.ui.ui.UIConstants; /** - * + * */ public class VUI extends SimplePanel implements ResizeHandler, Window.ClosingHandler, ShortcutActionHandlerOwner, Focusable, @@ -61,9 +62,6 @@ public class VUI extends SimplePanel implements ResizeHandler, private static int MONITOR_PARENT_TIMER_INTERVAL = 1000; - /** For internal use only. May be removed or replaced in the future. */ - public String theme; - /** For internal use only. May be removed or replaced in the future. */ public String id; @@ -319,19 +317,15 @@ public class VUI extends SimplePanel implements ResizeHandler, } } - public String getTheme() { - return theme; - } - /** - * Used to reload host page on theme changes. - *

    - * For internal use only. May be removed or replaced in the future. + * @return the name of the theme in use by this UI. + * @deprecated as of 7.3. Use {@link UIConnector#getActiveTheme()} instead. */ - public static native void reloadHostPage() - /*-{ - $wnd.location.reload(); - }-*/; + @Deprecated + public String getTheme() { + return ((UIConnector) ConnectorMap.get(connection).getConnector(this)) + .getActiveTheme(); + } /** * Returns true if the body is NOT generated, i.e if someone else has made @@ -530,4 +524,5 @@ public class VUI extends SimplePanel implements ResizeHandler, }); } } + } diff --git a/client/src/com/vaadin/client/ui/ui/UIConnector.java b/client/src/com/vaadin/client/ui/ui/UIConnector.java index 1d2a49cbd1..c88fd23eca 100644 --- a/client/src/com/vaadin/client/ui/ui/UIConnector.java +++ b/client/src/com/vaadin/client/ui/ui/UIConnector.java @@ -1,12 +1,12 @@ /* * Copyright 2000-2014 Vaadin Ltd. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -18,6 +18,7 @@ package com.vaadin.client.ui.ui; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.logging.Logger; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; @@ -26,8 +27,10 @@ import com.google.gwt.dom.client.Element; 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.NodeList; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.StyleElement; import com.google.gwt.dom.client.StyleInjector; import com.google.gwt.event.dom.client.ScrollEvent; import com.google.gwt.event.dom.client.ScrollHandler; @@ -51,12 +54,17 @@ import com.vaadin.client.ComponentConnector; import com.vaadin.client.ConnectorHierarchyChangeEvent; import com.vaadin.client.Focusable; import com.vaadin.client.Paintable; +import com.vaadin.client.ResourceLoader; +import com.vaadin.client.ResourceLoader.ResourceLoadEvent; +import com.vaadin.client.ResourceLoader.ResourceLoadListener; import com.vaadin.client.ServerConnector; import com.vaadin.client.UIDL; import com.vaadin.client.VConsole; import com.vaadin.client.ValueMap; +import com.vaadin.client.annotations.OnStateChange; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler; +import com.vaadin.client.ui.AbstractConnector; import com.vaadin.client.ui.AbstractSingleComponentContainerConnector; import com.vaadin.client.ui.ClickEventHandler; import com.vaadin.client.ui.ShortcutActionHandler; @@ -66,6 +74,7 @@ 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.ApplicationConstants; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.communication.MethodInvocation; import com.vaadin.shared.ui.ComponentStateUtil; @@ -80,6 +89,7 @@ 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; +import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.UI; @Connect(value = UI.class, loadStyle = LoadStyle.EAGER) @@ -88,6 +98,8 @@ public class UIConnector extends AbstractSingleComponentContainerConnector private HandlerRegistration childStateChangeHandlerRegistration; + private String activeTheme = null; + private final StateChangeHandler childStateChangeHandler = new StateChangeHandler() { @Override public void onStateChanged(StateChangeEvent stateChangeEvent) { @@ -197,14 +209,6 @@ public class UIConnector extends AbstractSingleComponentContainerConnector getWidget().immediate = getState().immediate; getWidget().resizeLazy = uidl.hasAttribute(UIConstants.RESIZE_LAZY); - String newTheme = uidl.getStringAttribute("theme"); - if (getWidget().theme != null && !newTheme.equals(getWidget().theme)) { - // Complete page refresh is needed due css can affect layout - // calculations etc - getWidget().reloadHostPage(); - } else { - getWidget().theme = newTheme; - } // this also implicitly removes old styles String styles = ""; styles += getWidget().getStylePrimaryName() + " "; @@ -405,9 +409,6 @@ public class UIConnector extends AbstractSingleComponentContainerConnector */ 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. */ @@ -424,8 +425,7 @@ public class UIConnector extends AbstractSingleComponentContainerConnector link.setRel("stylesheet"); link.setHref(url); link.setType("text/css"); - head.appendChild(link); - + getHead().appendChild(link); // Check if we have CSS string to inject } else if (cssInjectionsUidl.getTag().equals("css-string")) { for (Iterator it2 = cssInjectionsUidl.getChildIterator(); it2 @@ -437,8 +437,54 @@ public class UIConnector extends AbstractSingleComponentContainerConnector } } + /** + * Internal helper to get the tag of the page + * + * @since 7.3 + * @return the head element + */ + private HeadElement getHead() { + return HeadElement.as(Document.get() + .getElementsByTagName(HeadElement.TAG).getItem(0)); + } + + /** + * Internal helper for removing any stylesheet with the given URL + * + * @since 7.3 + * @param url + * the url to match with existing stylesheets + */ + private void removeStylesheet(String url) { + NodeList linkTags = getHead().getElementsByTagName( + LinkElement.TAG); + for (int i = 0; i < linkTags.getLength(); i++) { + LinkElement link = LinkElement.as(linkTags.getItem(i)); + if (!"stylesheet".equals(link.getRel())) { + continue; + } + if (!"text/css".equals(link.getType())) { + continue; + } + if (url.equals(link.getHref())) { + getHead().removeChild(link); + } + } + } + public void init(String rootPanelId, ApplicationConnection applicationConnection) { + // Create a style tag for style injections so they don't end up in + // the theme tag in IE8-IE10 (we don't want to wipe them out if we + // change theme). + // StyleInjectorImplIE always injects to the last style tag on the page. + if (BrowserInfo.get().isIE() + && BrowserInfo.get().getBrowserMajorVersion() < 11) { + StyleElement style = Document.get().createStyleElement(); + style.setType("text/css"); + getHead().appendChild(style); + } + DOM.sinkEvents(getWidget().getElement(), Event.ONKEYDOWN | Event.ONSCROLL); @@ -448,9 +494,7 @@ public class UIConnector extends AbstractSingleComponentContainerConnector // the user root.getElement().setInnerHTML(""); - String themeName = applicationConnection.getConfiguration() - .getThemeName(); - root.addStyleName(themeName); + activeTheme = applicationConnection.getConfiguration().getThemeName(); root.add(getWidget()); @@ -760,4 +804,229 @@ public class UIConnector extends AbstractSingleComponentContainerConnector getRpcProxy(DebugWindowServerRpc.class).showServerDebugInfo( serverConnector); } + + @OnStateChange("theme") + void onThemeChange() { + final String oldTheme = activeTheme; + final String newTheme = getState().theme; + final String oldThemeUrl = getThemeUrl(oldTheme); + final String newThemeUrl = getThemeUrl(newTheme); + + if (SharedUtil.equals(oldTheme, newTheme)) { + // This should only happen on the initial load when activeTheme has + // been updated in init. + + if (newTheme == null) { + return; + } + + // For the embedded case we cannot be 100% sure that the theme has + // been loaded and that the style names have been set. + + if (findStylesheetTag(oldThemeUrl) == null) { + // If there is no style tag, load it the normal way (the class + // name will be added when theme has been loaded) + replaceTheme(null, newTheme, null, newThemeUrl); + } else if (!getWidget().getParent().getElement() + .hasClassName(newTheme)) { + // If only the class name is missing, add that + activateTheme(newTheme); + } + return; + } + + getLogger().info("Changing theme from " + oldTheme + " to " + newTheme); + replaceTheme(oldTheme, newTheme, oldThemeUrl, newThemeUrl); + } + + /** + * Loads the new theme and removes references to the old theme + * + * @param oldTheme + * The name of the old theme + * @param newTheme + * The name of the new theme + * @param oldThemeUrl + * The url of the old theme + * @param newThemeUrl + * The url of the new theme + */ + private void replaceTheme(final String oldTheme, final String newTheme, + String oldThemeUrl, final String newThemeUrl) { + + LinkElement tagToReplace = null; + + if (oldTheme != null) { + tagToReplace = findStylesheetTag(oldThemeUrl); + + if (tagToReplace == null) { + getLogger() + .warning( + "Did not find the link tag for the old theme (" + + oldThemeUrl + + "), adding a new stylesheet for the new theme (" + + newThemeUrl + ")"); + } + } + + if (newTheme != null) { + loadTheme(newTheme, newThemeUrl, tagToReplace); + } else { + if (tagToReplace != null) { + tagToReplace.getParentElement().removeChild(tagToReplace); + } + + activateTheme(null); + } + + } + + /** + * Finds a link tag for a style sheet with the given URL + * + * @since 7.3 + * @param url + * the URL of the style sheet + * @return the link tag or null if no matching link tag was found + */ + private LinkElement findStylesheetTag(String url) { + NodeList linkTags = getHead().getElementsByTagName( + LinkElement.TAG); + for (int i = 0; i < linkTags.getLength(); i++) { + final LinkElement link = LinkElement.as(linkTags.getItem(i)); + if ("stylesheet".equals(link.getRel()) + && "text/css".equals(link.getType()) + && url.equals(link.getHref())) { + return link; + } + } + return null; + } + + /** + * Loads the given theme and replaces the given link element with the new + * theme link element. + * + * @param newTheme + * The name of the new theme + * @param newThemeUrl + * The url of the new theme + * @param tagToReplace + * The link element to replace. If null, then the new link + * element is added at the end. + */ + private void loadTheme(final String newTheme, final String newThemeUrl, + final LinkElement tagToReplace) { + LinkElement newThemeLinkElement = Document.get().createLinkElement(); + newThemeLinkElement.setRel("stylesheet"); + newThemeLinkElement.setType("text/css"); + newThemeLinkElement.setHref(newThemeUrl); + ResourceLoader.addOnloadHandler(newThemeLinkElement, + new ResourceLoadListener() { + + @Override + public void onLoad(ResourceLoadEvent event) { + getLogger().info( + "Loading of " + newTheme + " from " + + newThemeUrl + " completed"); + + if (tagToReplace != null) { + tagToReplace.getParentElement().removeChild( + tagToReplace); + } + activateTheme(newTheme); + } + + @Override + public void onError(ResourceLoadEvent event) { + getLogger().warning( + "Could not load theme from " + + getThemeUrl(newTheme)); + } + }, null); + + if (tagToReplace != null) { + getHead().insertBefore(newThemeLinkElement, tagToReplace); + } else { + getHead().appendChild(newThemeLinkElement); + } + } + + /** + * Activates the new theme. Assumes the theme has been loaded and taken into + * use in the browser. + * + * @since 7.3 + * @param newTheme + */ + private void activateTheme(String newTheme) { + if (activeTheme != null) { + getWidget().getParent().removeStyleName(activeTheme); + VOverlay.getOverlayContainer(getConnection()).removeClassName( + activeTheme); + } + + activeTheme = newTheme; + + if (newTheme != null) { + getWidget().getParent().addStyleName(newTheme); + VOverlay.getOverlayContainer(getConnection()).addClassName( + activeTheme); + } + + forceStateChangeRecursively(UIConnector.this); + getLayoutManager().forceLayout(); + } + + /** + * Force a full recursive recheck of every connector's state variables. + * + * @see #forceStateChange() + * + * @since 7.3 + */ + protected static void forceStateChangeRecursively( + AbstractConnector connector) { + connector.forceStateChange(); + + for (ServerConnector child : connector.getChildren()) { + if (child instanceof AbstractConnector) { + forceStateChangeRecursively((AbstractConnector) child); + } else { + getLogger().warning( + "Could not force state change for unknown connector type: " + + child.getClass().getName()); + } + } + + } + + /** + * Internal helper to get the theme URL for a given theme + * + * @since 7.3 + * @param theme + * the name of the theme + * @return The URL the theme can be loaded from + */ + private String getThemeUrl(String theme) { + return getConnection().translateVaadinUri( + ApplicationConstants.VAADIN_PROTOCOL_PREFIX + "themes/" + theme + + "/styles" + ".css"); + } + + /** + * Returns the name of the theme currently in used by the UI + * + * @since 7.3 + * @return the theme name used by this UI + */ + public String getActiveTheme() { + return activeTheme; + } + + private static Logger getLogger() { + return Logger.getLogger(UIConnector.class.getName()); + } + } diff --git a/server/src/com/vaadin/ui/UI.java b/server/src/com/vaadin/ui/UI.java index a72cbe5c30..5abeea9480 100644 --- a/server/src/com/vaadin/ui/UI.java +++ b/server/src/com/vaadin/ui/UI.java @@ -549,8 +549,6 @@ public abstract class UI extends AbstractSingleComponentContainer implements private boolean resizeLazy = false; - private String theme; - private Navigator navigator; private PushConnection pushConnection = null; @@ -633,7 +631,7 @@ public abstract class UI extends AbstractSingleComponentContainer implements this.embedId = embedId; // Actual theme - used for finding CustomLayout templates - theme = request.getParameter("theme"); + getState().theme = request.getParameter("theme"); getPage().init(request); @@ -1135,12 +1133,31 @@ public abstract class UI extends AbstractSingleComponentContainer implements } /** - * Gets the theme that was used when the UI was initialized. + * Gets the theme currently in use by this UI * * @return the theme name */ public String getTheme() { - return theme; + return getState(false).theme; + } + + /** + * Sets the theme currently in use by this UI + *

    + * Calling this method will remove the old theme (CSS file) from the + * application and add the new theme. + *

    + * Note that this method is NOT SAFE to call in a portal environment or + * other environment where there are multiple UIs on the same page. The old + * CSS file will be removed even if there are other UIs on the page which + * are still using it. + * + * @since 7.3 + * @param theme + * The new theme name + */ + public void setTheme(String theme) { + getState().theme = theme; } /** diff --git a/shared/src/com/vaadin/shared/ui/ui/UIState.java b/shared/src/com/vaadin/shared/ui/ui/UIState.java index 3c3785b7d5..2f51fef6ee 100644 --- a/shared/src/com/vaadin/shared/ui/ui/UIState.java +++ b/shared/src/com/vaadin/shared/ui/ui/UIState.java @@ -62,6 +62,12 @@ public class UIState extends TabIndexState { * Configuration for the push channel */ public PushConfigurationState pushConfiguration = new PushConfigurationState(); + /** + * Currently used theme. + * + * @since 7.3 + */ + public String theme; { primaryStyleName = "v-ui"; // Default is 1 for legacy reasons @@ -95,7 +101,7 @@ public class UIState extends TabIndexState { NotificationRole role) { this.prefix = prefix; this.postfix = postfix; - this.notificationRole = role; + notificationRole = role; } } diff --git a/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java b/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java index c4573fb9b8..a4d85775f7 100644 --- a/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java +++ b/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java @@ -16,15 +16,15 @@ package com.vaadin.tests.tb3; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.net.URL; -import java.util.Collections; -import java.util.List; - +import com.thoughtworks.selenium.webdriven.WebDriverBackedSelenium; +import com.vaadin.server.LegacyApplication; +import com.vaadin.server.UIProvider; +import com.vaadin.testbench.TestBench; import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.TestBenchTestCase; +import com.vaadin.tests.components.AbstractTestUIWithLog; +import com.vaadin.tests.tb3.MultiBrowserTest.Browser; +import com.vaadin.ui.UI; import org.junit.After; import org.junit.Before; import org.junit.runner.RunWith; @@ -41,14 +41,13 @@ import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; -import com.thoughtworks.selenium.webdriven.WebDriverBackedSelenium; -import com.vaadin.server.LegacyApplication; -import com.vaadin.server.UIProvider; -import com.vaadin.testbench.TestBench; -import com.vaadin.testbench.TestBenchTestCase; -import com.vaadin.tests.components.AbstractTestUIWithLog; -import com.vaadin.tests.tb3.MultiBrowserTest.Browser; -import com.vaadin.ui.UI; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.net.URL; +import java.util.Collections; +import java.util.List; import static com.vaadin.tests.tb3.TB3Runner.localWebDriverIsUsed; @@ -224,7 +223,22 @@ public abstract class AbstractTB3Test extends TestBenchTestCase { * {@link #isPush()}. */ protected void openTestURL() { - driver.get(getTestUrl()); + openTestURL(""); + } + + /** + * Opens the given test (defined by {@link #getTestUrl()}, optionally with + * debug window and/or push (depending on {@link #isDebug()} and + * {@link #isPush()}. + */ + protected void openTestURL(String extraParameters) { + String url = getTestUrl(); + if (url.contains("?")) { + url = url + "&" + extraParameters; + } else { + url = url + "?" + extraParameters; + } + driver.get(url); } /** diff --git a/uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFly.java b/uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFly.java new file mode 100644 index 0000000000..ec22edd205 --- /dev/null +++ b/uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFly.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.tests.themes; + +import com.vaadin.annotations.Theme; +import com.vaadin.server.ThemeResource; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUIWithLog; +import com.vaadin.tests.util.PersonContainer; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Image; +import com.vaadin.ui.Label; +import com.vaadin.ui.Table; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; + +@Theme("reindeer") +public class ThemeChangeOnTheFly extends AbstractTestUIWithLog { + + @Override + protected void setup(VaadinRequest request) { + Button inject = new Button("Inject blue background"); + inject.addClickListener(new ClickListener() { + + @Override + public void buttonClick(ClickEvent event) { + getPage().getStyles().add( + ".v-app { background: blue !important;}"); + + } + }); + addComponent(inject); + + GridLayout gl = new GridLayout(2, 4); + gl.setCaption("Change theme by clicking a button"); + for (final String theme : new String[] { "reindeer", "runo", + "chameleon", "base", null }) { + Button b = new Button(theme); + b.setId(theme + ""); + b.addClickListener(new ClickListener() { + + @Override + public void buttonClick(ClickEvent event) { + getUI().setTheme(theme); + } + }); + gl.addComponent(b); + } + + Table t = new Table(); + PersonContainer pc = PersonContainer.createWithTestData(); + pc.addNestedContainerBean("address"); + t.setContainerDataSource(pc); + gl.addComponent(t, 0, 3, 1, 3); + gl.setRowExpandRatio(3, 1); + + gl.setWidth("500px"); + gl.setHeight("800px"); + + HorizontalLayout images = new HorizontalLayout(); + images.setSpacing(true); + + Label l = new Label("Chameleon theme image in caption"); + l.setIcon(new ThemeResource("img/magnifier.png")); + images.addComponent(l); + Image image = new Image("Runo theme image", new ThemeResource( + "icons/64/ok.png")); + images.addComponent(image); + image = new Image("Reindeer theme image", new ThemeResource( + "button/img/left-focus.png")); + images.addComponent(image); + addComponent(images); + addComponent(gl); + + getLayout().setSpacing(true); + + Window w = new Window(); + w.setContent(new VerticalLayout(new Button("Button in window"))); + addWindow(w); + } + + @Override + protected String getTestDescription() { + return "Test that you can change theme on the fly"; + } + + @Override + protected Integer getTicketNumber() { + return 2874; + } + +} diff --git a/uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFlyTest.java b/uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFlyTest.java new file mode 100644 index 0000000000..eb010e82ee --- /dev/null +++ b/uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFlyTest.java @@ -0,0 +1,121 @@ +/* + * 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.tests.themes; + +import java.io.IOException; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.support.ui.ExpectedCondition; + +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.tests.tb3.MultiBrowserTest; + +public class ThemeChangeOnTheFlyTest extends MultiBrowserTest { + + @Override + public List getBrowsersToTest() { + // Seems like stylesheet onload is not fired on PhantomJS + // https://github.com/ariya/phantomjs/issues/12332 + List l = super.getBrowsersToTest(); + l.remove(Browser.PHANTOMJS.getDesiredCapabilities()); + return l; + } + + @Test + public void injectedStyleAndThemeChange() throws IOException { + openTestURL(); + $(ButtonElement.class).caption("Inject blue background").first() + .click(); + changeTheme("runo"); + compareScreen("runo-blue-background"); + } + + @Test + public void reindeerToOthers() throws IOException { + openTestURL(); + compareScreen("reindeer"); + + changeThemeAndCompare("runo"); + changeThemeAndCompare("chameleon"); + changeThemeAndCompare("base"); + + } + + @Test + public void runoToReindeer() throws IOException { + openTestURL("theme=runo"); + compareScreen("runo"); + changeThemeAndCompare("reindeer"); + } + + @Test + public void reindeerToNullToReindeer() throws IOException { + openTestURL(); + + changeThemeAndCompare("null"); + changeThemeAndCompare("reindeer"); + } + + private void changeThemeAndCompare(String theme) throws IOException { + changeTheme(theme); + compareScreen(theme); + } + + private void changeTheme(String theme) { + $(ButtonElement.class).id(theme).click(); + if (theme.equals("null")) { + waitForThemeToChange(""); + assertOverlayTheme(""); + } else { + waitForThemeToChange(theme); + assertOverlayTheme(theme); + } + } + + private void waitForThemeToChange(final String theme) { + + final WebElement rootDiv = findElement(By + .xpath("//div[contains(@class,'v-app')]")); + waitUntil(new ExpectedCondition() { + + @Override + public Boolean apply(WebDriver input) { + String rootClass = rootDiv.getAttribute("class").trim(); + String expected = "v-app " + theme; + expected = expected.trim(); + return rootClass.equals(expected); + } + }, 30); + } + + private void assertOverlayTheme(String theme) { + final WebElement overlayContainerDiv = findElement(By + .xpath("//div[contains(@class,'v-overlay-container')]")); + String expected = "v-app v-overlay-container " + theme; + expected = expected.trim(); + + String overlayClass = overlayContainerDiv.getAttribute("class").trim(); + + Assert.assertEquals(expected, overlayClass); + } + +} -- 2.39.5