Improve canvas for browsers (and permutations) with partial canvas support.

Review at http://gwt-code-reviews.appspot.com/1296801


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9602 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/canvas/Canvas.gwt.xml b/user/src/com/google/gwt/canvas/Canvas.gwt.xml
index 3b1da45..ea94f47 100644
--- a/user/src/com/google/gwt/canvas/Canvas.gwt.xml
+++ b/user/src/com/google/gwt/canvas/Canvas.gwt.xml
@@ -16,5 +16,30 @@
 <module>
   <inherits name="com.google.gwt.user.User"/>
   <inherits name="com.google.gwt.canvas.dom.DOM"/>
+
+  <!-- Define the support property -->
+  <define-property name="canvasElementSupport" values="maybe,no" />
+
+  <!-- Give default support value of no -->
+  <set-property name="canvasElementSupport" value="no" />
+
+  <set-property name="canvasElementSupport" value="maybe">
+    <any>
+      <when-property-is name="user.agent" value="safari" />
+      <when-property-is name="user.agent" value="gecko1_8" />
+      <when-property-is name="user.agent" value="opera" />
+    </any>
+  </set-property>
+
+  <replace-with class="com.google.gwt.canvas.client.Canvas.CanvasElementSupportDetectedMaybe">
+    <when-type-is class="com.google.gwt.canvas.client.Canvas.CanvasElementSupportDetector" />
+    <when-property-is name="canvasElementSupport" value="maybe" />
+  </replace-with>
+
+  <replace-with class="com.google.gwt.canvas.client.Canvas.CanvasElementSupportDetectedNo">
+    <when-type-is class="com.google.gwt.canvas.client.Canvas.CanvasElementSupportDetector" />
+    <when-property-is name="canvasElementSupport" value="no" />
+  </replace-with>
+
   <source path="client"/>
 </module>
diff --git a/user/src/com/google/gwt/canvas/client/Canvas.java b/user/src/com/google/gwt/canvas/client/Canvas.java
index c6372b3..13a530b 100644
--- a/user/src/com/google/gwt/canvas/client/Canvas.java
+++ b/user/src/com/google/gwt/canvas/client/Canvas.java
@@ -17,9 +17,10 @@
 
 import com.google.gwt.canvas.dom.client.Context;
 import com.google.gwt.canvas.dom.client.Context2d;
+import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.CanvasElement;
 import com.google.gwt.dom.client.Document;
-import com.google.gwt.user.client.IsSupported;
+import com.google.gwt.dom.client.PartialSupport;
 import com.google.gwt.user.client.ui.FocusWidget;
 
 /**
@@ -31,25 +32,55 @@
  * </span>
  * </p>
  * 
- * This widget may not be supported on all browsers, see {@link IsSupported}
+ * This widget may not be supported on all browsers.
  */
-public class Canvas extends FocusWidget implements IsSupported {
+@PartialSupport
+public class Canvas extends FocusWidget {
+  private static CanvasElementSupportDetector detector;
+
+  /**
+   * Return a new {@link Canvas} if supported,  and null otherwise.
+   * 
+   * @return a new {@link Canvas} if supported, and null otherwise
+   */
+  public static Canvas createIfSupported() {
+    if (detector == null) {
+      detector = GWT.create(CanvasElementSupportDetector.class);
+    }
+    if (!detector.isSupportedCompileTime()) {
+      return null;
+    }
+    CanvasElement element = Document.get().createCanvasElement();
+    if (!detector.isSupportedRunTime(element)) {
+      return null;
+    }
+    return new Canvas(element);
+  }
 
   /**
    * Runtime check for whether the canvas element is supported in this browser.
-   * See {@link IsSupported}
    * 
    * @return whether the canvas element is supported
    */
-  public static final native boolean isSupported() /*-{
-    return !!$doc.createElement('canvas').getContext;
-  }-*/;
+  public static boolean isSupported() {
+    if (detector == null) {
+      detector = GWT.create(CanvasElementSupportDetector.class);
+    }
+    if (!detector.isSupportedCompileTime()) {
+      return false;
+    }
+    CanvasElement element = Document.get().createCanvasElement();
+    if (!detector.isSupportedRunTime(element)) {
+      return false;
+    }
+    return true;
+  }
 
   /**
-   * Creates a Canvas.
+   * Protected constructor. Use {@link #createIfSupported()} to create a Canvas.
    */
-  public Canvas() {
-    setElement(Document.get().createCanvasElement());
+  private Canvas(CanvasElement element) {
+    setElement(element);
   }
 
   /**
@@ -142,4 +173,67 @@
   public String toDataUrl(String type) {
     return getCanvasElement().toDataUrl(type);
   }
+
+  /**
+   * Detector for browser support of {@link CanvasElement}.
+   */
+  private static class CanvasElementSupportDetector {
+    /**
+     * Using a run-time check, return true if the {@link CanvasElement} is 
+     * supported.
+     * 
+     * @return true if supported, false otherwise.
+     */
+    static native boolean isSupportedRunTime(CanvasElement element) /*-{
+      return !!element.getContext;
+    }-*/;
+
+    /**
+     * Using a compile-time check, return true if {@link CanvasElement} might 
+     * be supported.
+     * 
+     * @return true if might be supported, false otherwise.
+     */
+    boolean isSupportedCompileTime() {
+      // will be true in CanvasElementSupportDetectedMaybe
+      // will be false in CanvasElementSupportDetectedNo
+      return false;
+    }
+  }
+
+  /**
+   * Detector for permutations that might support {@link CanvasElement}.
+   */
+  @SuppressWarnings("unused")
+  private static class CanvasElementSupportDetectedMaybe
+      extends CanvasElementSupportDetector {
+    /**
+     * Using a compile-time check, return true if {@link CanvasElement} might be
+     * supported.
+     *
+     * @return true if might be supported, false otherwise.
+     */
+    @Override
+    boolean isSupportedCompileTime() {
+      return true;
+    }
+  }
+
+  /**
+   * Detector for permutations that do not support {@link CanvasElement}.
+   */
+  @SuppressWarnings("unused")
+  private static class CanvasElementSupportDetectedNo
+      extends CanvasElementSupportDetector {
+    /**
+     * Using a compile-time check, return true if {@link CanvasElement} might be
+     * supported.
+     *
+     * @return true if might be supported, false otherwise.
+     */
+    @Override
+    boolean isSupportedCompileTime() {
+      return false;
+    } 
+  }
 }
diff --git a/user/src/com/google/gwt/dom/client/PartialSupport.java b/user/src/com/google/gwt/dom/client/PartialSupport.java
new file mode 100644
index 0000000..b8b4d20
--- /dev/null
+++ b/user/src/com/google/gwt/dom/client/PartialSupport.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dom.client;
+
+/**
+ * Annotation for classes that are only supported on a limited set of browsers.
+ * 
+ * <p>
+ * By convention, classes annotated with PartialSupport will provide two static 
+ * methods:
+ * <ol>
+ *   <li> <code>static boolean isSupported()</code> that returns whether the 
+ *   feature is supported. </li>
+ *   <li> <code>static YourType createIfSupported()</code> factory method that 
+ *   returns a new feature if supported, or null otherwise. </li>
+ * </ol>
+ * </p>
+ */
+public @interface PartialSupport {
+
+}
diff --git a/user/src/com/google/gwt/user/client/IsSupported.java b/user/src/com/google/gwt/user/client/IsSupported.java
deleted file mode 100644
index f439842..0000000
--- a/user/src/com/google/gwt/user/client/IsSupported.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2010 Google Inc.
- * 
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- * 
- * http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.gwt.user.client;
-
-/**
- * Interface for classes that are only supported on a limited set of browsers.
- * 
- * <p>
- * <span style="color:red">Experimental API: This API is still under development 
- * and is subject to change.
- * </span>
- * </p>
- * 
- * By convention, classes that implement IsSupported will provide a static
- * method <code>boolean isSupported()</code> that checks whether the feature is
- * supported at runtime.
- */
-public interface IsSupported {
-
-}
diff --git a/user/test/com/google/gwt/canvas/client/CanvasTest.java b/user/test/com/google/gwt/canvas/client/CanvasTest.java
index 147ddfe..d1b308a 100644
--- a/user/test/com/google/gwt/canvas/client/CanvasTest.java
+++ b/user/test/com/google/gwt/canvas/client/CanvasTest.java
@@ -45,10 +45,11 @@
 
   @Override
   protected void gwtSetUp() throws Exception {
-    canvas1 = new Canvas();
-    canvas2 = new Canvas();
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    canvas1 = Canvas.createIfSupported();
+    canvas2 = Canvas.createIfSupported();
+    
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     RootPanel.get().add(canvas1);
@@ -57,24 +58,29 @@
 
   @Override
   protected void gwtTearDown() throws Exception {
+    if (canvas1 == null) {
+      return; // don't continue if not supported
+    }
+    
     RootPanel.get().remove(canvas1);
     RootPanel.get().remove(canvas2);
   }
 
   /*
-   * If the canvas has no pixels (i.e. either its horizontal dimension or its vertical dimension 
-   * is zero) then the method must return the string "data:,". (This is the shortest data: URL; 
-   * it represents the empty string in a text/plain resource.)
-   * 
+   * If the canvas has no pixels (i.e. either its horizontal dimension or its
+   * vertical dimension is zero) then the method must return the string
+   * "data:,". (This is the shortest data: URL; it represents the empty string
+   * in a text/plain resource.)
+   *
    * Due to browser inconsistencies, we just check for data:something.
    */
   public void testBlankDataUrl() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
-    // Safari 3.0 does not support toDataURL(), so the following tests are disabled for
-    // Safari 3.0 and before.
+    // Safari 3.0 does not support toDataURL(), so the following tests are
+    // disabled for Safari 3.0 and before.
     if (isWebkit525OrBefore()) {
       return;
     }
@@ -87,12 +93,34 @@
     canvas1.setCoordinateSpaceWidth(0);
     
     String dataUrl = canvas1.toDataUrl();
-    assertTrue("toDataURL() should return data:something", dataUrl.startsWith("data:"));
+    assertTrue("toDataURL() should return data:something",
+        dataUrl.startsWith("data:"));
+  }
+
+  public void testDataUrlWithType() {
+    if (canvas1 == null) {
+      return; // don't continue if not supported
+    }
+
+    // Safari 3.0 does not support toDataURL(), so the following tests are
+    // disabled for Safari 3.0 and before.
+    if (isWebkit525OrBefore()) {
+      return;
+    }
+
+    canvas1.setHeight("10px");
+    canvas1.setWidth("10px");
+    canvas1.setCoordinateSpaceHeight(10);
+    canvas1.setCoordinateSpaceWidth(10);
+    
+    String dataUrl = canvas1.toDataUrl("image/png");
+    assertTrue("toDataURL(image/png) should return data:image/png[data]", 
+        dataUrl.startsWith("data:image/png"));
   }
 
   public void testHeightAndWidth() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     canvas1.setHeight("40px");
@@ -117,8 +145,8 @@
   }
 
   public void testInternalHeightAndWidth() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     canvas1.setHeight("40px");
@@ -147,24 +175,14 @@
     assertEquals(161, canvas1.getCoordinateSpaceWidth());
   }
 
-  public void testDataUrlWithType() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+  public void testIsSupported() {
+    if (canvas1 == null) {
+      assertFalse(
+          "isSupported() should be false when createIfSupported() returns null",
+          Canvas.isSupported());
+    } else {
+      assertTrue(
+          "isSupported() should be true when createIfSupported() returns non-null", Canvas.isSupported());
     }
-
-    // Safari 3.0 does not support toDataURL(), so the following tests are disabled for
-    // Safari 3.0 and before.
-    if (isWebkit525OrBefore()) {
-      return;
-    }
-
-    canvas1.setHeight("10px");
-    canvas1.setWidth("10px");
-    canvas1.setCoordinateSpaceHeight(10);
-    canvas1.setCoordinateSpaceWidth(10);
-    
-    String dataUrl = canvas1.toDataUrl("image/png");
-    assertTrue("toDataURL(image/png) should return data:image/png[data]", 
-        dataUrl.startsWith("data:image/png"));
   }
 }
diff --git a/user/test/com/google/gwt/canvas/dom/client/Context2dTest.java b/user/test/com/google/gwt/canvas/dom/client/Context2dTest.java
index 666ff46..259cf9a 100644
--- a/user/test/com/google/gwt/canvas/dom/client/Context2dTest.java
+++ b/user/test/com/google/gwt/canvas/dom/client/Context2dTest.java
@@ -54,10 +54,11 @@
 
   @Override
   protected void gwtSetUp() throws Exception {
-    canvas1 = new Canvas();
-    canvas2 = new Canvas();
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    canvas1 = Canvas.createIfSupported();
+    canvas2 = Canvas.createIfSupported();
+
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     RootPanel.get().add(canvas1);
@@ -71,8 +72,8 @@
   }
 
   public void testArc() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     canvas1.setHeight("40px");
@@ -95,8 +96,8 @@
   }
 
   public void testFillRect() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     // Safari 3.0 does not support getImageData(), so the following tests are disabled for
@@ -150,8 +151,8 @@
   }
 
   public void testFillStyle() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     // get the 2d contexts
@@ -193,8 +194,8 @@
   }
 
   public void testFont() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     Context2d context = canvas1.getContext2d();
@@ -203,8 +204,8 @@
   }
 
   public void testGlobalAlpha() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     Context2d context = canvas1.getContext2d();
@@ -214,8 +215,8 @@
 
   // This test currently fails on IE9 and is disabled.
   public void disabled_testGlobalComposite() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     Context2d context = canvas1.getContext2d();
@@ -226,8 +227,8 @@
   }
 
   public void testGradient() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     // Safari 3.0 does not support getImageData(), so the following tests are disabled for
@@ -272,8 +273,8 @@
   }
 
   public void testImageData() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
     
     // Firefox 3.0 does not support createImageData(), so the following tests are disabled
@@ -342,8 +343,8 @@
   }
 
   public void testIsPointInPath() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     canvas1.setHeight("40px");
@@ -363,8 +364,8 @@
   }
 
   public void testLines() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     canvas1.setHeight("40px");
@@ -390,8 +391,8 @@
   }
 
   public void testMiter() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     Context2d context = canvas1.getContext2d();
@@ -400,8 +401,8 @@
   }
 
   public void testPixelManipulation() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     // Safari 3.0 does not support getImageData(), so the following tests are disabled for
@@ -431,8 +432,8 @@
   }
 
   public void testShadows() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
     
     // Firefox 3.0 returns the incorrect shadowBlur value so the following tests are disabled
@@ -459,8 +460,8 @@
   }
 
   public void testStrokeStyle() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
 
     // get the 2d contexts
@@ -502,8 +503,8 @@
   }
 
   public void testText() {
-    if (!canvas1.isSupported()) {
-      return; // disable tests if not supported
+    if (canvas1 == null) {
+      return; // don't continue if not supported
     }
     
     canvas1.setHeight("40px");