/* * Copyright 2007 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.ui; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; /** * The superclass for all user-interface objects. It simply wraps a DOM element, * and cannot receive events. Most interesting user-interface classes derive * from {@link com.google.gwt.user.client.ui.Widget}. * *

Styling With CSS

*

* All UIObject objects can be styled using CSS. Style names that * are specified programmatically in Java source are implicitly associated with * CSS style rules. In terms of HTML and CSS, a GWT style name is the element's * CSS "class". By convention, GWT style names are of the form * [project]-[widget]. *

* *

* For example, the {@link Button} widget has the style name * gwt-Button, meaning that within the Button * constructor, the following call occurs: * *

 * setStyleName("gwt-Button");
* * A corresponding CSS style rule can then be written as follows: * *
 * // Example of how you might choose to style a Button widget 
 * .gwt-Button {
 *   background-color: yellow;
 *   color: black;
 *   font-size: 24pt;
 * }
* * Note the dot prefix in the CSS style rule. This syntax is called a CSS class * selector. *

* *

Style Name Specifics

*

* Every UIObject has a primary style name that * identifies the key CSS style rule that should always be applied to it. Use * {@link #setStyleName(String)} to specify an object's primary style name. In * most cases, the primary style name is set in a widget's constructor and never * changes again during execution. In the case that no primary style name is * specified, it defaults to gwt-nostyle. *

* *

* More complex styling behavior can be achieved by manipulating an object's * secondary style names. Secondary style names can be added and removed * using {@link #addStyleName(String)} and {@link #removeStyleName(String)}. * The purpose of secondary style names is to associate a variety of CSS style * rules over time as an object progresses through different visual states. *

* *

* There is an important special formulation of secondary style names called * dependent style names. A dependent style name is a secondary style * name prefixed with the primary style name of the widget itself. See * {@link #addStyleName(String)} for details. *

*/ public abstract class UIObject { private static final String EMPTY_STYLENAME_MSG = "Style names cannot be empty"; private static final String NULL_HANDLE_MSG = "Null widget handle. If you " + "are creating a composite, ensure that initWidget() has been called."; private static final String STYLE_EMPTY = "gwt-nostyle"; public static native boolean isVisible(Element elem) /*-{ return (elem.style.display != 'none'); }-*/; public static native void setVisible(Element elem, boolean visible) /*-{ elem.style.display = visible ? '' : 'none'; }-*/; /** * Sets the object's primary style name and updates all dependent style names. * * @param elem the element whose style is to be reset * @param style the new primary style name * @see #setStyleName(Element, String, boolean) */ protected static void resetStyleName(Element elem, String style) { if (elem == null) { throw new RuntimeException(NULL_HANDLE_MSG); } // Style names cannot contain leading or trailing whitespace, and cannot // legally be empty. style = style.trim(); if (style.length() == 0) { throw new IllegalArgumentException(EMPTY_STYLENAME_MSG); } ensurePrimaryStyleName(elem); updatePrimaryAndDependentStyleNames(elem, style); } /** * This convenience method adds or removes a secondary style name to the * primary style name for a given element. Set {@link #setStyleName(String)} * for a description of how primary and secondary style names are used. * * @param elem the element whose style is to be modified * @param style the secondary style name to be added or removed * @param add true to add the given style, false * to remove it */ protected static void setStyleName(Element elem, String style, boolean add) { if (elem == null) { throw new RuntimeException(NULL_HANDLE_MSG); } style = style.trim(); if (style.length() == 0) { throw new IllegalArgumentException(EMPTY_STYLENAME_MSG); } // Get the current style string. String oldStyle = ensurePrimaryStyleName(elem); int idx; if (oldStyle == null) { idx = -1; oldStyle = ""; } else { idx = oldStyle.indexOf(style); } // Calculate matching index. while (idx != -1) { if (idx == 0 || oldStyle.charAt(idx - 1) == ' ') { int last = idx + style.length(); int lastPos = oldStyle.length(); if ((last == lastPos) || ((last < lastPos) && (oldStyle.charAt(last) == ' '))) { break; } } idx = oldStyle.indexOf(style, idx + 1); } if (add) { // Only add the style if it's not already present. if (idx == -1) { if (oldStyle.length() > 0) { oldStyle += " "; } DOM.setElementProperty(elem, "className", oldStyle + style); } } else { // Don't try to remove the style if it's not there. if (idx != -1) { if (idx == 0) { // You can't remove the base (i.e. the first) style name. throw new IllegalArgumentException("Cannot remove base style name"); } String begin = oldStyle.substring(0, idx); String end = oldStyle.substring(idx + style.length()); DOM.setElementProperty(elem, "className", begin + end); } } } /** * Ensure that the root element has a primary style name. If one is not * already present, then it is assigned the default style name. * * @return the primary style name */ private static String ensurePrimaryStyleName(Element elem) { String className = DOM.getElementProperty(elem, "className").trim(); if ("".equals(className)) { className = STYLE_EMPTY; DOM.setElementProperty(elem, "className", className); } return className; } /** * Replaces all instances of the primary style name with newPrimaryStyleName. */ private static native void updatePrimaryAndDependentStyleNames(Element elem, String newStyle) /*-{ var className = elem.className; var spaceIdx = className.indexOf(' '); if (spaceIdx >= 0) { // Get the old base style name from the beginning of the className. var oldStyle = className.substring(0, spaceIdx); // Replace oldStyle with newStyle. We have to do this by hand because // there is no String.replaceAll() and String.replace() takes a regex, // which we can't guarantee is safe on arbitrary class names. var newClassName = '', curIdx = 0; while (true) { var idx = className.indexOf(oldStyle, curIdx); if (idx == -1) { newClassName += className.substring(curIdx); break; } newClassName += className.substring(curIdx, idx); newClassName += newStyle; curIdx = idx + oldStyle.length; } elem.className = newClassName; } else { // There was no space, and therefore only one class name, which we can // simply clobber. elem.className = newStyle; } }-*/; private Element element; /** * Adds a secondary or dependent style name to this object. A secondary style * name is an additional style name that is, in HTML/CSS terms, included as a * space-separated token in the value of the CSS class * attribute for this object's root element. * *

* The most important use for this method is to add a special kind of * secondary style name called a dependent style name. To add a * dependent style name, prefix the 'style' argument with the result of * {@link #getStyleName()}. For example, suppose the primary style name is * gwt-TextBox. If the following method is called as * obj.setReadOnly(true): *

* *
   * public void setReadOnly(boolean readOnly) {
   *   isReadOnlyMode = readOnly;
   *   
   *   // Create a dependent style name.
   *   String readOnlyStyle = getStyleName() + "-readonly";
   *    
   *   if (readOnly) {
   *     addStyleName(readOnlyStyle);
   *   } else {
   *     removeStyleName(readOnlyStyle);
   *   }
   * }
* *

* then both of the CSS style rules below will be applied: *

* *
   *
   * // This rule is based on the primary style name and is always active.
   * .gwt-TextBox {
   *   font-size: 12pt;
   * }
   * 
   * // This rule is based on a dependent style name that is only active
   * // when the widget has called addStyleName(getStyleName() + "-readonly"). 
   * .gwt-TextBox-readonly {
   *   background-color: lightgrey;
   *   border: none;
   * }
* *

* Dependent style names are powerful because they are automatically updated * whenever the primary style name changes. Continuing with the example above, * if the primary style name changed due to the following call: *

* *
setStyleName("my-TextThingy");
* *

* then the object would be re-associated with style rules below rather than * those above: *

* *
   * .my-TextThingy {
   *   font-size: 12pt;
   * }
   * 
   * .my-TextThingy-readonly {
   *   background-color: lightgrey;
   *   border: none;
   * }
* *

* Secondary style names that are not dependent style names are not * automatically updated when the primary style name changes. *

* * @param style the secondary style name to be added * @see UIObject * @see #removeStyleName(String) */ public void addStyleName(String style) { setStyleName(getStyleElement(), style, true); } /** * Gets the object's absolute left position in pixels, as measured from the * browser window's client area. * * @return the object's absolute left position */ public int getAbsoluteLeft() { return DOM.getAbsoluteLeft(getElement()); } /** * Gets the object's absolute top position in pixels, as measured from the * browser window's client area. * * @return the object's absolute top position */ public int getAbsoluteTop() { return DOM.getAbsoluteTop(getElement()); } /** * Gets a handle to the object's underlying DOM element. * * @return the object's browser element */ public Element getElement() { return element; } /** * Gets the object's offset height in pixels. This is the total height of the * object, including decorations such as border, margin, and padding. * * @return the object's offset height */ public int getOffsetHeight() { return DOM.getElementPropertyInt(element, "offsetHeight"); } /** * Gets the object's offset width in pixels. This is the total width of the * object, including decorations such as border, margin, and padding. * * @return the object's offset width */ public int getOffsetWidth() { return DOM.getElementPropertyInt(element, "offsetWidth"); } /** * Gets the primary style name associated with the object. * * @return the object's primary style name * @see #setStyleName(String) * @see #addStyleName(String) * @see #removeStyleName(String) */ public String getStyleName() { String fullClassName = ensurePrimaryStyleName(getStyleElement()); // The base style name is always the first token of the full CSS class // name. There can be no leading whitespace in the class name, so it's not // necessary to trim() it. int spaceIdx = fullClassName.indexOf(' '); if (spaceIdx >= 0) { return fullClassName.substring(0, spaceIdx); } return fullClassName; } /** * Gets the title associated with this object. The title is the 'tool-tip' * displayed to users when they hover over the object. * * @return the object's title */ public String getTitle() { return DOM.getElementProperty(element, "title"); } /** * Determines whether or not this object is visible. * * @return true if the object is visible */ public boolean isVisible() { return isVisible(element); } /** * Removes a secondary style name. * * @param style the secondary style name to be removed * @see #addStyleName(String) */ public void removeStyleName(String style) { setStyleName(getStyleElement(), style, false); } /** * Sets the object's height. This height does not include decorations such as * border, margin, and padding. * * @param height the object's new height, in CSS units (e.g. "10px", "1em") */ public void setHeight(String height) { // This exists to deal with an inconsistency in IE's implementation where // it won't accept negative numbers in length measurements assert extractLengthValue(height.trim().toLowerCase()) >= 0 : "CSS heights should not be negative"; DOM.setStyleAttribute(element, "height", height); } /** * Sets the object's size, in pixels, not including decorations such as * border, margin, and padding. * * @param width the object's new width, in pixels * @param height the object's new height, in pixels */ public void setPixelSize(int width, int height) { if (width >= 0) { setWidth(width + "px"); } if (height >= 0) { setHeight(height + "px"); } } /** * Sets the object's size. This size does not include decorations such as * border, margin, and padding. * * @param width the object's new width, in CSS units (e.g. "10px", "1em") * @param height the object's new height, in CSS units (e.g. "10px", "1em") */ public void setSize(String width, String height) { setWidth(width); setHeight(height); } /** * Sets the object's primary style name and updates all dependent style names. * * @param style the new primary style name * @see #addStyleName(String) * @see #removeStyleName(String) */ public void setStyleName(String style) { resetStyleName(getStyleElement(), style); } /** * Sets the title associated with this object. The title is the 'tool-tip' * displayed to users when they hover over the object. * * @param title the object's new title */ public void setTitle(String title) { if (title == null || title.length() == 0) { DOM.removeElementAttribute(element, "title"); } else { DOM.setElementAttribute(element, "title", title); } } /** * Sets whether this object is visible. * * @param visible true to show the object, false * to hide it */ public void setVisible(boolean visible) { setVisible(element, visible); } /** * Sets the object's width. This width does not include decorations such as * border, margin, and padding. * * @param width the object's new width, in CSS units (e.g. "10px", "1em") */ public void setWidth(String width) { // This exists to deal with an inconsistency in IE's implementation where // it won't accept negative numbers in length measurements assert extractLengthValue(width.trim().toLowerCase()) >= 0 : "CSS widths should not be negative"; DOM.setStyleAttribute(element, "width", width); } /** * Adds a set of events to be sunk by this object. Note that only * {@link Widget widgets} may actually receive events, but can receive events * from all objects contained within them. * * @param eventBitsToAdd a bitfield representing the set of events to be added * to this element's event set * @see com.google.gwt.user.client.Event */ public void sinkEvents(int eventBitsToAdd) { DOM.sinkEvents(getElement(), eventBitsToAdd | DOM.getEventsSunk(getElement())); } /** * This method is overridden so that any object can be viewed in the debugger * as an HTML snippet. * * @return a string representation of the object */ public String toString() { if (element == null) { return "(null handle)"; } return DOM.toString(element); } /** * Removes a set of events from this object's event list. * * @param eventBitsToRemove a bitfield representing the set of events to be * removed from this element's event set * @see #sinkEvents * @see com.google.gwt.user.client.Event */ public void unsinkEvents(int eventBitsToRemove) { DOM.sinkEvents(getElement(), DOM.getEventsSunk(getElement()) & (~eventBitsToRemove)); } /** * Template method that returns the element to which style names will be * applied. By default it returns the root element, but this method may be * overridden to apply styles to a child element. * * @return the element to which style names will be applied */ protected Element getStyleElement() { return element; } /** * Sets this object's browser element. UIObject subclasses must call this * method before attempting to call any other methods. * * If the browser element has already been set, then the current element's * position is located in the DOM and removed. The new element is added into * the previous element's position. * * @param elem the object's new element */ protected void setElement(Element elem) { if (this.element != null) { // replace this.element in its parent with elem. replaceNode(this.element, elem); } this.element = elem; // We do not actually force the creation of a primary style name here. // Instead, we do it lazily -- when it is aboslutely required -- // in getStyleName(), addStyleName(), and removeStyleName(). } /** * Intended to be used to pull the value out of a CSS length. We rely on the * behavior of parseFloat to ignore non-numeric chars in its input. If the * value is "auto" or "inherit", 0 will be returned. * * @param s The CSS length string to extract * @return The leading numeric portion of s, or 0 if "auto" or * "inherit" are passed in. */ private native double extractLengthValue(String s) /*-{ if (s == "auto" || s == "inherit" || s == "") { return 0; } else { return parseFloat(s); } }-*/; private native void replaceNode(Element node, Element newNode) /*-{ var p = node.parentNode; if (!p) { return; } p.insertBefore(newNode, node); p.removeChild(node); }-*/; }