]> source.dussan.org Git - vaadin-framework.git/commitdiff
Allow changing theme on the fly (#2874, #14139, #14124)
authorArtur Signell <artur@vaadin.com>
Wed, 30 Jul 2014 07:53:28 +0000 (07:53 +0000)
committerHenri Sara <hesara@vaadin.com>
Mon, 4 Aug 2014 08:27:00 +0000 (08:27 +0000)
* 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

16 files changed:
WebContent/release-notes.html
client/src/com/vaadin/client/ApplicationConfiguration.java
client/src/com/vaadin/client/ApplicationConnection.java
client/src/com/vaadin/client/ResourceLoader.java
client/src/com/vaadin/client/communication/TranslatedURLReference.java [new file with mode: 0644]
client/src/com/vaadin/client/communication/URLReference_Serializer.java
client/src/com/vaadin/client/debug/internal/InfoSection.java
client/src/com/vaadin/client/ui/AbstractConnector.java
client/src/com/vaadin/client/ui/VOverlay.java
client/src/com/vaadin/client/ui/VUI.java
client/src/com/vaadin/client/ui/ui/UIConnector.java
server/src/com/vaadin/ui/UI.java
shared/src/com/vaadin/shared/ui/ui/UIState.java
uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java
uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFly.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/themes/ThemeChangeOnTheFlyTest.java [new file with mode: 0644]

index ac4e27d8a3946458582904a507027c74630c10cd..0ede61d729294b5c79f4414bc2a7d2b836fdd4b9 100644 (file)
                 the Sass CSS preprocessor heavily, 
                 providing a variety of ways to customize the look and feel of your theme. 
                 See <a href="https://vaadin.com/wiki/-/wiki/Main/Valo+theme+-+Getting+started">the Valo theme tutorial</a> or <a href="https://vaadin.com/book/-/page/themes.valo.html">the Valo theme section</a> in Book of Vaadin for information on how to get started.</li>
+            <li>Support for changing theme on the fly</li>
         </ul>
 
         <p>
index 3ccbeba6f3e336bab962e27cd5ca349baad05584..87c8ea465fe8b2491d2ab098837fbe8aa1c45a61 100644 (file)
@@ -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.
      * 
index b569c0b17c736056518f1fc86407b4674cd4eb40..6e2c6e757ca3e0ca08e377782ee65af6969cfdb6 100644 (file)
@@ -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();
     }
 
     /**
index 68a16e816299f6087521adf1c26120fa918d8a9e..ceede263fc500574e75c0e87248a9b147eb033d8 100644 (file)
@@ -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 (file)
index 0000000..b99f4c6
--- /dev/null
@@ -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());
+    }
+
+}
index 586dd626f03eea50ffbffcb6bb9de22aaf656719..4ecdc606d24d10e16910a98e75daab652f6cb2de 100644 (file)
@@ -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<URLReference> {
     @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;
     }
index 23b77a94dbc50a64ed25d11227c703e02c6fb2b5..a7a84f5f8fb3e6e18c4f53bbdd0b83ec78354097 100644 (file)
@@ -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();
index a2e0d9cd5440981267d4b66b4d9a961e11e790f6..e93ea0f50717bc881a61e0b2dfc9f216adcc770a 100644 (file)
@@ -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<String> 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;
+        }
+
+    }
 }
index c62e2c9824d9f3080324153d740100c567580890..afa13dc33764a1870acef96fd04c2757239d03f4 100644 (file)
@@ -881,7 +881,9 @@ public class VOverlay extends PopupPanel implements CloseHandler<PopupPanel> {
             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<PopupPanel> {
             }
         }
     }
-}
\ No newline at end of file
+}
index df24c3b1c719ac44af9630d664ba7ca254c114de..eae4f6319dc2dc38119512d874c94108b7097bbe 100644 (file)
@@ -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.
-     * <p>
-     * 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,
             });
         }
     }
+
 }
index 1d2a49cbd1a6d6bf7be0c6a7c7dd871e07c00f78..c88fd23ecad3985774d888a55d1d9d90d6e4506a 100644 (file)
@@ -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 <head> 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<Element> 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<Element> 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());
+    }
+
 }
index a72cbe5c3085f49d332d3c92de63e8c5758093fa..5abeea9480ea8c28d494a5818233305ced620525 100644 (file)
@@ -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
+     * <p>
+     * Calling this method will remove the old theme (CSS file) from the
+     * application and add the new theme.
+     * <p>
+     * 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;
     }
 
     /**
index 3c3785b7d559d0c94c2375493c873945a249aee4..2f51fef6ee660bcb14abbf059c88e4d094df0b8e 100644 (file)
@@ -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;
         }
     }
 
index c4573fb9b87ba1b71534328a2a6654e3107b2ccf..a4d85775f7bf5ca42f64758f600e8542e57fd754 100644 (file)
 
 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 (file)
index 0000000..ec22edd
--- /dev/null
@@ -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 (file)
index 0000000..eb010e8
--- /dev/null
@@ -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<DesiredCapabilities> getBrowsersToTest() {
+        // Seems like stylesheet onload is not fired on PhantomJS
+        // https://github.com/ariya/phantomjs/issues/12332
+        List<DesiredCapabilities> 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<Boolean>() {
+
+            @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);
+    }
+
+}