Make StyleInjector use Scheduler API.

Patch by: bobv
Review by: bruce

http://gwt-code-reviews.appspot.com/78816

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6403 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java b/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java
index 0ff623d..f231dd5 100644
--- a/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java
+++ b/user/src/com/google/gwt/core/client/impl/SchedulerImpl.java
@@ -177,7 +177,8 @@
   }-*/;
 
   public static void scheduleIncrementalImpl(RepeatingCommand cmd) {
-    INCREMENTAL_COMMANDS.push(Task.create(cmd));
+    // Push repeating commands onto the same initial queue for relative order
+    DEFERRED_COMMANDS.push(Task.create(cmd));
     maybeSchedulePostEventPumpCommands();
   }
 
diff --git a/user/src/com/google/gwt/dom/client/StyleInjector.java b/user/src/com/google/gwt/dom/client/StyleInjector.java
index 1ec5e7c..c1dabcd 100644
--- a/user/src/com/google/gwt/dom/client/StyleInjector.java
+++ b/user/src/com/google/gwt/dom/client/StyleInjector.java
@@ -19,9 +19,15 @@
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayInteger;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 
 /**
- * Used to add stylesheets to the document.
+ * Used to add stylesheets to the document. The one-argument versions of
+ * {@link #inject}, {@link #injectAtEnd}, and {@link #injectAtStart} use
+ * {@link Scheduler#scheduleFinally} to minimize the number of individual style
+ * elements created.
  */
 public class StyleInjector {
 
@@ -120,8 +126,8 @@
          * This assertion can fail if the max number of style elements exist
          * before this module can inject any style elements, so STYLE_ELEMENTS
          * will be empty. However, the fix would degrade performance for the
-         * general case.
-         * TODO(jlabanca): Can we handle this scenario efficiently?
+         * general case. TODO(jlabanca): Can we handle this scenario
+         * efficiently?
          */
         assert shortestIdx != -1;
 
@@ -179,15 +185,106 @@
     }-*/;
   }
 
+  private static final JsArrayString toInject = JavaScriptObject.createArray().cast();
+  private static final JsArrayString toInjectAtEnd = JavaScriptObject.createArray().cast();
+  private static final JsArrayString toInjectAtStart = JavaScriptObject.createArray().cast();
+
+  private static ScheduledCommand flusher = new ScheduledCommand() {
+    public void execute() {
+      if (needsInjection) {
+        flush(null);
+      }
+    }
+  };
+
+  private static boolean needsInjection = false;
+
+  /**
+   * Add a stylesheet to the document.
+   * 
+   * @param css the CSS contents of the stylesheet
+   */
+  public static void inject(String css) {
+    inject(css, false);
+  }
+
+  /**
+   * Add a stylesheet to the document.
+   * 
+   * @param css the CSS contents of the stylesheet
+   * @param immediate if <code>true</code> the DOM will be updated immediately
+   *          instead of just before returning to the event loop. Using this
+   *          option excessively will decrease performance, especially if used
+   *          with an inject-css-on-init coding pattern
+   */
+  public static void inject(String css, boolean immediate) {
+    toInject.push(css);
+    inject(immediate);
+  }
+
+  /**
+   * Add stylesheet data to the document as though it were declared after all
+   * stylesheets previously created by {@link #inject(String)}.
+   * 
+   * @param css the CSS contents of the stylesheet
+   */
+  public static void injectAtEnd(String css) {
+    injectAtEnd(css, false);
+  }
+
+  /**
+   * Add stylesheet data to the document as though it were declared after all
+   * stylesheets previously created by {@link #inject(String)}.
+   * 
+   * @param css the CSS contents of the stylesheet
+   * @param immediate if <code>true</code> the DOM will be updated immediately
+   *          instead of just before returning to the event loop. Using this
+   *          option excessively will decrease performance, especially if used
+   *          with an inject-css-on-init coding pattern
+   */
+  public static void injectAtEnd(String css, boolean immediate) {
+    toInjectAtEnd.push(css);
+    inject(immediate);
+  }
+
+  /**
+   * Add stylesheet data to the document as though it were declared before all
+   * stylesheets previously created by {@link #inject(String)}.
+   * 
+   * @param css the CSS contents of the stylesheet
+   */
+  public static void injectAtStart(String css) {
+    injectAtStart(css, false);
+  }
+
+  /**
+   * Add stylesheet data to the document as though it were declared before all
+   * stylesheets previously created by {@link #inject(String)}.
+   * 
+   * @param css the CSS contents of the stylesheet
+   * @param immediate if <code>true</code> the DOM will be updated immediately
+   *          instead of just before returning to the event loop. Using this
+   *          option excessively will decrease performance, especially if used
+   *          with an inject-css-on-init coding pattern
+   */
+  public static void injectAtStart(String css, boolean immediate) {
+    toInjectAtStart.unshift(css);
+    inject(immediate);
+  }
+
   /**
    * Add a stylesheet to the document. The StyleElement returned by this method
    * is not guaranteed to be unique.
    * 
    * @param contents the CSS contents of the stylesheet
    * @return the StyleElement that contains the newly-injected CSS
+   * @deprecated The returned StyleElement cannot be implemented consistently
+   *             across all browsers
    */
+  @Deprecated
   public static StyleElement injectStylesheet(String contents) {
-    return StyleInjectorImpl.IMPL.injectStyleSheet(contents);
+    toInject.push(contents);
+    return flush(toInject);
   }
 
   /**
@@ -197,9 +294,13 @@
    * 
    * @param contents the CSS contents of the stylesheet
    * @return the StyleElement that contains the newly-injected CSS
+   * @deprecated The returned StyleElement cannot be implemented consistently
+   *             across all browsers
    */
+  @Deprecated
   public static StyleElement injectStylesheetAtEnd(String contents) {
-    return StyleInjectorImpl.IMPL.injectStyleSheetAtEnd(contents);
+    toInjectAtEnd.push(contents);
+    return flush(toInjectAtEnd);
   }
 
   /**
@@ -209,9 +310,13 @@
    * 
    * @param contents the CSS contents of the stylesheet
    * @return the StyleElement that contains the newly-injected CSS
+   * @deprecated The returned StyleElement cannot be implemented consistently
+   *             across all browsers
    */
+  @Deprecated
   public static StyleElement injectStylesheetAtStart(String contents) {
-    return StyleInjectorImpl.IMPL.injectStyleSheetAtStart(contents);
+    toInjectAtStart.unshift(contents);
+    return flush(toInjectAtStart);
   }
 
   /**
@@ -224,12 +329,68 @@
    * @param style a StyleElement previously-returned from
    *          {@link #injectStylesheet(String)}.
    * @param contents the new contents of the stylesheet.
+   * @deprecated The associated StyleElement cannot be implemented consistently
+   *             across all browsers
    */
+  @Deprecated
   public static void setContents(StyleElement style, String contents) {
     StyleInjectorImpl.IMPL.setContents(style, contents);
   }
 
   /**
+   * The <code>which</code> parameter is used to support the deprecated API.
+   */
+  private static StyleElement flush(JavaScriptObject which) {
+    StyleElement toReturn = null;
+    StyleElement maybeReturn;
+
+    if (toInjectAtStart.length() != 0) {
+      String css = toInjectAtStart.join("");
+      maybeReturn = StyleInjectorImpl.IMPL.injectStyleSheetAtStart(css);
+      if (toInjectAtStart == which) {
+        toReturn = maybeReturn;
+      }
+      toInjectAtStart.setLength(0);
+    }
+
+    if (toInject.length() != 0) {
+      String css = toInject.join("");
+      maybeReturn = StyleInjectorImpl.IMPL.injectStyleSheet(css);
+      if (toInject == which) {
+        toReturn = maybeReturn;
+      }
+      toInject.setLength(0);
+    }
+
+    if (toInjectAtEnd.length() != 0) {
+      String css = toInjectAtEnd.join("");
+      maybeReturn = StyleInjectorImpl.IMPL.injectStyleSheetAtEnd(css);
+      if (toInjectAtEnd == which) {
+        toReturn = maybeReturn;
+      }
+      toInjectAtEnd.setLength(0);
+    }
+
+    needsInjection = false;
+    return toReturn;
+  }
+
+  private static void inject(boolean immediate) {
+    if (immediate) {
+      flush(null);
+    } else {
+      schedule();
+    }
+  }
+
+  private static void schedule() {
+    if (!needsInjection) {
+      needsInjection = true;
+      Scheduler.get().scheduleFinally(flusher);
+    }
+  }
+
+  /**
    * Utility class.
    */
   private StyleInjector() {
diff --git a/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
index 0027209..adf2871 100644
--- a/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
+++ b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
@@ -902,7 +902,7 @@
     sw.indent();
     sw.println("if (!injected) {");
     sw.indentln("injected = true;");
-    sw.indentln(StyleInjector.class.getName() + ".injectStylesheet(getText());");
+    sw.indentln(StyleInjector.class.getName() + ".inject(getText());");
     sw.indentln("return true;");
     sw.println("}");
     sw.println("return false;");
diff --git a/user/test/com/google/gwt/dom/client/StyleInjectorTest.java b/user/test/com/google/gwt/dom/client/StyleInjectorTest.java
index 910d09d..0edb413 100644
--- a/user/test/com/google/gwt/dom/client/StyleInjectorTest.java
+++ b/user/test/com/google/gwt/dom/client/StyleInjectorTest.java
@@ -26,13 +26,16 @@
  */
 public class StyleInjectorTest extends GWTTestCase {
 
+  private static final int TEST_DELAY = 500;
+
   @Override
   public String getModuleName() {
     return "com.google.gwt.dom.DOMTest";
   }
 
-  @DoNotRunWith({Platform.Htmlunit})
-  public void testStyleInjector() {
+  @DoNotRunWith(value = {Platform.Htmlunit})
+  @SuppressWarnings("deprecation")
+  public void testOldMethods() {
     final DivElement elt = Document.get().createDivElement();
     elt.setId("styleInjectorTest");
     elt.setInnerHTML("Hello StyleInjector!");
@@ -43,7 +46,7 @@
     StyleInjector.injectStylesheetAtEnd("#styleInjectorTest {height: 100px;}");
 
     // We need to allow the document to be redrawn
-    delayTestFinish(500);
+    delayTestFinish(TEST_DELAY);
 
     DeferredCommand.addCommand(new Command() {
       public void execute() {
@@ -59,8 +62,9 @@
   /**
    * Ensure that the IE createStyleSheet compatibility code is exercised.
    */
-  @DoNotRunWith({Platform.Htmlunit})
-  public void testLotsOfStyles() {
+  @DoNotRunWith(value = {Platform.Htmlunit})
+  @SuppressWarnings("deprecation")
+  public void testOldMethodsWithLotsOfStyles() {
     StyleElement[] elements = new StyleElement[100];
     for (int i = 0, j = elements.length; i < j; i++) {
       elements[i] = StyleInjector.injectStylesheet("#styleInjectorTest" + i
@@ -78,7 +82,7 @@
     Document.get().getBody().appendChild(elt);
 
     // We need to allow the document to be redrawn
-    delayTestFinish(500);
+    delayTestFinish(TEST_DELAY);
 
     DeferredCommand.addCommand(new Command() {
       public void execute() {
@@ -89,4 +93,49 @@
       }
     });
   }
+
+  @DoNotRunWith(value = {Platform.Htmlunit})
+  public void testStyleInjectorBatched() {
+    testStyleInjector("testStyleInjectorBatched", false);
+  }
+
+  @DoNotRunWith(value = {Platform.Htmlunit})
+  public void testStyleInjectorImmediate() {
+    testStyleInjector("testStyleInjectorImmediate", true);
+  }
+
+  private void testStyleInjector(String testName, final boolean immediate) {
+
+    final DivElement elt = Document.get().createDivElement();
+    elt.setId(testName);
+    elt.setInnerHTML("Hello");
+    Document.get().getBody().appendChild(elt);
+
+    StyleInjector.inject("#" + testName
+        + " {position: absolute; left: 100px; width: 50px; height 50px;}",
+        immediate);
+    StyleInjector.injectAtStart("#" + testName
+        + " {left: 25px; width: 100px !important;}", immediate);
+    StyleInjector.injectAtEnd("#" + testName + " {height: 100px;}", immediate);
+
+    Command command = new Command() {
+      public void execute() {
+        assertEquals(100, elt.getOffsetLeft());
+        assertEquals(100, elt.getClientHeight());
+        assertEquals(100, elt.getClientWidth());
+
+        if (!immediate) {
+          finishTest();
+        }
+      }
+    };
+
+    if (immediate) {
+      command.execute();
+    } else {
+      DeferredCommand.addCommand(command);
+      // We need to allow the BatchedCommands to execute
+      delayTestFinish(TEST_DELAY);
+    }
+  }
 }