/* * 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}. * *
* 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. * * *
* 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 addtrue
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. * * @returntrue
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);
}-*/;
}