Introduces inline styles to ui.xml files, completing
http://code.google.com/p/google-web-toolkit/issues/detail?id=3984 to a
first approximation.
Only CssResource works so far, via <ui:style>. @Sprite won't work
until <ui:image> is implemented, and <ui:data> is still needed as well.
Reviewed by: spoon
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6114 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
index a6fcec7..f26d9ca 100644
--- a/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
+++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderParser.java
@@ -20,6 +20,7 @@
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.parsers.NullInterpreter;
import com.google.gwt.uibinder.rebind.messages.MessagesWriter;
import com.google.gwt.uibinder.rebind.model.ImplicitClientBundle;
import com.google.gwt.uibinder.rebind.model.ImplicitCssResource;
@@ -39,6 +40,7 @@
private final MessagesWriter messagesWriter;
private final FieldManager fieldManager;
private final ImplicitClientBundle bundleClass;
+ private final JClassType cssResourceType;
public UiBinderParser(UiBinderWriter writer, MessagesWriter messagesWriter,
FieldManager fieldManager, TypeOracle oracle,
@@ -48,7 +50,8 @@
this.messagesWriter = messagesWriter;
this.fieldManager = fieldManager;
this.bundleClass = bundleClass;
- }
+ this.cssResourceType = oracle.findType(CssResource.class.getCanonicalName());
+ }
/**
* Parses the root UiBinder element, and kicks off the parsing of the rest of
@@ -56,7 +59,7 @@
*/
public String parse(XMLElement elem) throws UnableToCompleteException {
// TODO(rjrjr) Clearly need to break these find* methods out into their own
- // parsers, need registration scheme for ui binder's own parsers
+ // parsers, an so need a registration scheme for uibinder-specific parsers
findStyles(elem);
findResources(elem);
messagesWriter.findMessagesConfig(elem);
@@ -66,8 +69,16 @@
private JClassType consumeCssResourceType(XMLElement elem)
throws UnableToCompleteException {
- JClassType publicType = consumeTypeAttribute(elem);
- JClassType cssResourceType = oracle.findType(CssResource.class.getCanonicalName());
+ String typeName = elem.consumeAttribute("type", null);
+ if (typeName == null) {
+ return cssResourceType;
+ }
+
+ JClassType publicType = oracle.findType(typeName);
+ if (publicType == null) {
+ writer.die("In %s, no such type %s", elem, typeName);
+ }
+
if (!publicType.isAssignableTo(cssResourceType)) {
writer.die("In %s, type %s does not extend %s", elem,
publicType.getQualifiedSourceName(),
@@ -131,13 +142,18 @@
}
private void createStyle(XMLElement elem) throws UnableToCompleteException {
- // Won't be required for long
- String source = elem.consumeRequiredAttribute("source");
+ String body = elem.consumeInnerText(new NullInterpreter<String>());
+ if (body.length() > 0 && elem.hasAttribute("source")) {
+ writer.die("In %s, cannot use both a source attribute and inline css text.", elem);
+ }
+
+ String source = elem.consumeAttribute("source");
String name = elem.consumeAttribute("field", "style");
JClassType publicType = consumeCssResourceType(elem);
ImplicitCssResource cssMethod = bundleClass.createCssResource(name, source,
- publicType);
+ publicType, body);
+
FieldWriter field = fieldManager.registerFieldOfGeneratedType(
cssMethod.getPackageName(), cssMethod.getClassName(),
cssMethod.getName());
diff --git a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
index 2f6d347..0ff9000 100644
--- a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
+++ b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
@@ -1,12 +1,12 @@
/*
* Copyright 2008 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
@@ -47,7 +47,7 @@
public interface Interpreter<T> {
/**
* Given an XMLElement, return its filtered value.
- *
+ *
* @throws UnableToCompleteException on error
*/
T interpretElement(XMLElement elem) throws UnableToCompleteException;
@@ -115,7 +115,7 @@
/**
* Consumes the given attribute and returns its trimmed value, or null if it
* was unset. The returned string is not escaped.
- *
+ *
* @param name the attribute's full name (including prefix)
* @return the attribute's value, or null
*/
@@ -128,7 +128,7 @@
/**
* Consumes the given attribute and returns its trimmed value, or the given
* default value if it was unset. The returned string is not escaped.
- *
+ *
* @param name the attribute's full name (including prefix)
* @param defaultValue the value to return if the attribute was unset
* @return the attribute's value, or defaultValue
@@ -143,7 +143,7 @@
/**
* Consumes the given attribute as a boolean value.
- *
+ *
* @throws UnableToCompleteException
*/
public boolean consumeBooleanAttribute(String attr)
@@ -177,9 +177,9 @@
* Consumes and returns all child elements selected by the interpreter. Note
* that text nodes are not elements, and so are not presented for
* interpretation, and are not consumed.
- *
+ *
* @param interpreter Should return true for any child that should be consumed
- * and returned.
+ * and returned by the consumeChildElements call
* @throws UnableToCompleteException
*/
public Collection<XMLElement> consumeChildElements(
@@ -217,7 +217,7 @@
* The odds are you want to use
* {@link com.google.gwt.templates.parsers.HtmlInterpreter} for an HTML value,
* or {@link com.google.gwt.templates.parsers.TextInterpreter} for text.
- *
+ *
* @param interpreter Called for each element, expected to return a string
* replacement for it, or null if it should be left as is
*/
@@ -250,7 +250,7 @@
* This call requires an interpreter to make sense of any special children.
* The odds are you want to use
* {@link com.google.gwt.templates.parsers.TextInterpreter}
- *
+ *
* @throws UnableToCompleteException If any elements present are not consumed
* by the interpreter
*/
@@ -292,7 +292,7 @@
*/
public String consumeOpeningTag() {
String rtn = getOpeningTag();
-
+
for (int i = getAttributeCount() - 1; i >= 0; i--) {
getAttribute(i).consumeValue();
}
@@ -306,7 +306,7 @@
throws UnableToCompleteException {
String value = consumeAttribute(name);
if ("".equals(value)) {
- writer.die("In %s, missing required attribute name\"%s\"", this);
+ writer.die("In %s, missing required attribute name\"%s\"", this, name);
}
return value;
}
@@ -319,8 +319,9 @@
XMLElement ret = null;
for (XMLElement child : consumeChildElements()) {
if (ret != null) {
- throw new RuntimeException(getLocalName()
- + " may only contain a single child element.");
+ throw new RuntimeException(String.format(
+ "%s may only contain a single child element, but found"
+ + "%s and %s.", getLocalName(), ret, child));
}
ret = child;
diff --git a/user/src/com/google/gwt/uibinder/rebind/model/ImplicitClientBundle.java b/user/src/com/google/gwt/uibinder/rebind/model/ImplicitClientBundle.java
index 3c1457e..3f17973 100644
--- a/user/src/com/google/gwt/uibinder/rebind/model/ImplicitClientBundle.java
+++ b/user/src/com/google/gwt/uibinder/rebind/model/ImplicitClientBundle.java
@@ -44,8 +44,8 @@
public ImplicitClientBundle(String packageName, String uiBinderImplClassName,
String fieldName, MortalLogger logger) {
this.packageName = packageName;
- this.className = uiBinderImplClassName + "GenBundle";
- this.cssBaseName = uiBinderImplClassName + "GenCss";
+ this.className = uiBinderImplClassName + "_GenBundle";
+ this.cssBaseName = uiBinderImplClassName + "_GenCss";
this.fieldName = fieldName;
this.logger = logger;
}
@@ -57,12 +57,13 @@
* @param source path to the .css file resource
* @param extendedInterface the public interface implemented by this
* CssResource, or null
+ * @param body the inline css text
* @return
*/
public ImplicitCssResource createCssResource(String name, String source,
- JClassType extendedInterface) {
+ JClassType extendedInterface, String body) {
ImplicitCssResource css = new ImplicitCssResource(packageName, cssBaseName
- + name, name, source, extendedInterface, logger);
+ + name, name, source, extendedInterface, body, logger);
cssMethods.add(css);
return css;
}
diff --git a/user/src/com/google/gwt/uibinder/rebind/model/ImplicitCssResource.java b/user/src/com/google/gwt/uibinder/rebind/model/ImplicitCssResource.java
index 76284da..5742cec 100644
--- a/user/src/com/google/gwt/uibinder/rebind/model/ImplicitCssResource.java
+++ b/user/src/com/google/gwt/uibinder/rebind/model/ImplicitCssResource.java
@@ -20,8 +20,14 @@
import com.google.gwt.resources.css.ExtractClassNamesVisitor;
import com.google.gwt.resources.css.GenerateCssAst;
import com.google.gwt.resources.css.ast.CssStylesheet;
+import com.google.gwt.resources.ext.ResourceGeneratorUtil;
import com.google.gwt.uibinder.rebind.MortalLogger;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.MalformedURLException;
import java.net.URL;
import java.util.Set;
@@ -36,16 +42,33 @@
private final String name;
private final String source;
private final JClassType extendedInterface;
+ private final String body;
private final MortalLogger logger;
+ private File generatedFile;
public ImplicitCssResource(String packageName, String className, String name,
- String source, JClassType extendedInterface, MortalLogger logger) {
+ String source, JClassType extendedInterface, String body,
+ MortalLogger logger) {
this.packageName = packageName;
this.className = className;
this.name = name;
- this.source = source;
this.extendedInterface = extendedInterface;
+ this.body = body;
this.logger = logger;
+
+ if (body.length() > 0) {
+ assert "".equals(source); // Enforced for real by the parser
+
+ /*
+ * We're going to write the inline body to a temporary File and register
+ * it with the CssResource world under the name in this.source, via
+ * ResourceGeneratorUtil.addNamedFile(). When CssResourceGenerator sees
+ * this name in an @Source annotation it will know to use the registered
+ * file rather than load a resource.
+ */
+ source = String.format("uibinder:%s.%s.css", packageName, className);
+ }
+ this.source = source;
}
/**
@@ -58,29 +81,20 @@
/**
* @return the set of CSS classnames in the underlying .css files
*
- * @throws UnableToCompleteException if the user has called for a .css file we can't find.
+ * @throws UnableToCompleteException if the user has called for a .css file we
+ * can't find.
*/
public Set<String> getCssClassNames() throws UnableToCompleteException {
- /*
- * TODO(rjrjr,bobv) refactor ResourceGeneratorUtil.findResources so we can
- * find them the same way ClientBundle does. For now, just look relative to
- * this package
- */
+ URL[] urls;
- ClassLoader classLoader = ImplicitCssResource.class.getClassLoader();
- String path = packageName.replace(".", "/");
-
- String[] sources = source.split(" ");
- URL[] urls = new URL[sources.length];
- int i = 0;
-
- for (String s : sources) {
- String resourcePath = path + '/' + s;
- URL found = classLoader.getResource(resourcePath);
- if (null == found) {
- logger.die("Unable to find resource: " + resourcePath);
+ if (body.length() == 0) {
+ urls = getExternalCss();
+ } else {
+ try {
+ urls = new URL[] {getGeneratedFile().toURL()};
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
}
- urls[i++] = found;
}
CssStylesheet sheet = GenerateCssAst.exec(logger.getTreeLogger(), urls);
@@ -111,9 +125,52 @@
}
/**
- * @return the user declared names of the associated .css files
+ * @return the name of the .css file(s), separate by white space
*/
public String getSource() {
return source;
}
+
+ private URL[] getExternalCss() throws UnableToCompleteException {
+ /*
+ * TODO(rjrjr,bobv) refactor ResourceGeneratorUtil.findResources so we can
+ * find them the same way ClientBundle does. For now, just look relative to
+ * this package
+ */
+
+ ClassLoader classLoader = ImplicitCssResource.class.getClassLoader();
+ String path = packageName.replace(".", "/");
+
+ String[] sources = source.split(" ");
+ URL[] urls = new URL[sources.length];
+ int i = 0;
+
+ for (String s : sources) {
+ String resourcePath = path + '/' + s;
+ URL found = classLoader.getResource(resourcePath);
+ if (null == found) {
+ logger.die("Unable to find resource: " + resourcePath);
+ }
+ urls[i++] = found;
+ }
+ return urls;
+ }
+
+ private File getGeneratedFile() {
+ if (generatedFile == null) {
+ try {
+ File f = File.createTempFile(String.format("uiBinder_%s_%s",
+ packageName, className), ".css");
+
+ BufferedWriter out = new BufferedWriter(new FileWriter(f));
+ out.write(body);
+ out.close();
+ generatedFile = f;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ ResourceGeneratorUtil.addNamedFile(getSource(), generatedFile);
+ }
+ return generatedFile;
+ }
}
diff --git a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.css b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.css
index 3d0f78d..c712178 100644
--- a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.css
+++ b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.css
@@ -7,6 +7,6 @@
* Demonstrates that the ui.xml has access to styles that
* do not back any declared CssResource api
*/
-.privateStyle {
+.privateColor {
color: SteelBlue;
}
diff --git a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java
index bd47e88..edb568f 100644
--- a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java
+++ b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.java
@@ -111,6 +111,8 @@
@UiField DListElement widgetCrazyDefinitionList;
@UiField HTMLPanel customTagHtmlPanel;
@UiField ParagraphElement privateStyleParagraph;
+ @UiField ParagraphElement reallyPrivateStyleParagraph;
+ @UiField SpanElement totallyPrivateStyleSpan;
public WidgetBasedUi() {
this.bundledLabel = new Label();
diff --git a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
index 288690d..33ba429 100644
--- a/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
+++ b/user/src/com/google/gwt/uibinder/sample/client/WidgetBasedUi.ui.xml
@@ -69,7 +69,30 @@
<!--
Tests creating a CssResource from an external file.
-->
-<ui:style field='myStyle' source='WidgetBasedUi.css' type='com.google.gwt.uibinder.sample.client.WidgetBasedUi.Style'/>
+<ui:style field='myStyle' source='WidgetBasedUi.css'
+ type='com.google.gwt.uibinder.sample.client.WidgetBasedUi.Style'/>
+
+<ui:style field='myOtherStyle' type='com.google.gwt.uibinder.sample.client.WidgetBasedUi.Style'>
+ /* because we extend WidgetBasedUi.Style and it's tagged @Shared, we can refine the menubar
+ style defined in other files */
+
+ .menuBar.psychedelic {
+ background-color: Yellow;
+ }
+
+ /* Visible only to this template */
+ .privateColor {
+ color: Purple;
+ }
+</ui:style>
+
+<ui:style field='myTotallyPrivateStyle'>
+ /* An inline style tied to no public type */
+
+ .superPrivateColor {
+ background-color: Gold;
+ }
+</ui:style>
<gwt:DockPanel ui:field="root" width="100%">
<gwt:Dock direction='NORTH'>
@@ -149,7 +172,7 @@
Piggledy
</ui:msg>
</div>
- <gwt:MenuBar vertical="true" styleName="{myStyle.menuBar}">
+ <gwt:MenuBar vertical="true" styleName="{myStyle.menuBar} {myOtherStyle.psychedelic}">
<gwt:MenuItem>delta</gwt:MenuItem>
<gwt:MenuItem>echo</gwt:MenuItem>
<gwt:MenuItem>foxtrot</gwt:MenuItem>
@@ -264,8 +287,14 @@
This stylish paragraph also gets its good looks from the
external ClientBundle.
</p>
- <p ui:field='privateStyleParagraph' class='{myStyle.privateStyle}'>
- This one is has a private style, visible only inside the ui.xml file.
+ <p ui:field='privateStyleParagraph' class='{myStyle.privateColor}'>
+ This one is has a private style, defined out in WidgetBaseUi.css and used only by this ui.xml file.
+ </p>
+ <p ui:field='reallyPrivateStyleParagraph' class='{myOtherStyle.privateColor}'>
+ And this style is defined right here inside the ui.xml file.
+ <span ui:field='totallyPrivateStyleSpan' class='{myTotallyPrivateStyle.superPrivateColor}'>
+ (This one too, but even more so.)
+ </span>
</p>
<h2>Evolving</h2>
<p>
diff --git a/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java b/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java
index 5172b0c..e2ba40c 100644
--- a/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java
+++ b/user/test/com/google/gwt/uibinder/sample/client/UiBinderTest.java
@@ -265,11 +265,22 @@
assertTrue(((HTML) north).getHTML().contains("Title area"));
}
- public void testPrivateStyle() {
+ public void testPrivateStyleFromExternalCss() {
ParagraphElement p = widgetUi.privateStyleParagraph;
assertTrue("Some kind of class should be set", p.getClassName().length() > 0);
}
+ public void testPrivateStylesFromInlineCss() {
+ ParagraphElement p = widgetUi.reallyPrivateStyleParagraph;
+ assertTrue("Some kind of class should be set",
+ p.getClassName().length() > 0);
+ assertFalse("Should be a different style than privateStyleParagraph's",
+ widgetUi.privateStyleParagraph.getClassName().equals(p.getClassName()));
+
+ assertTrue("Some kind of class should be set",
+ widgetUi.totallyPrivateStyleSpan.getClassName().length() > 0);
+ }
+
@DoNotRunWith(Platform.Htmlunit)
public void testRadioButton() {
RadioButton able = widgetUi.myRadioAble;