001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.user.client.ui;
017:
018: import com.google.gwt.user.client.DOM;
019: import com.google.gwt.user.client.Element;
020:
021: /**
022: * The superclass for all user-interface objects. It simply wraps a DOM element,
023: * and cannot receive events. Most interesting user-interface classes derive
024: * from {@link com.google.gwt.user.client.ui.Widget}.
025: *
026: * <h3>Styling With CSS</h3>
027: * <p>
028: * All <code>UIObject</code> objects can be styled using CSS. Style names that
029: * are specified programmatically in Java source are implicitly associated with
030: * CSS style rules. In terms of HTML and CSS, a GWT style name is the element's
031: * CSS "class". By convention, GWT style names are of the form
032: * <code>[project]-[widget]</code>.
033: * </p>
034: *
035: * <p>
036: * For example, the {@link Button} widget has the style name
037: * <code>gwt-Button</code>, meaning that within the <code>Button</code>
038: * constructor, the following call occurs:
039: *
040: * <pre class="code">
041: * setStyleName("gwt-Button");</pre>
042: *
043: * A corresponding CSS style rule can then be written as follows:
044: *
045: * <pre class="code">
046: * // Example of how you might choose to style a Button widget
047: * .gwt-Button {
048: * background-color: yellow;
049: * color: black;
050: * font-size: 24pt;
051: * }</pre>
052: *
053: * Note the dot prefix in the CSS style rule. This syntax is called a <a
054: * href="http://www.w3.org/TR/REC-CSS2/selector.html#class-html">CSS class
055: * selector</a>.
056: * </p>
057: *
058: * <h3>Style Name Specifics</h3>
059: * <p>
060: * Every <code>UIObject</code> has a <i>primary style name</i> that
061: * identifies the key CSS style rule that should always be applied to it. Use
062: * {@link #setStylePrimaryName(String)} to specify an object's primary style
063: * name. In most cases, the primary style name is set in a widget's constructor
064: * and never changes again during execution. In the case that no primary style
065: * name is specified, it defaults to the first style name that is added.
066: * </p>
067: *
068: * <p>
069: * More complex styling behavior can be achieved by manipulating an object's
070: * <i>secondary style names</i>. Secondary style names can be added and removed
071: * using {@link #addStyleName(String)} and {@link #removeStyleName(String)}.
072: * The purpose of secondary style names is to associate a variety of CSS style
073: * rules over time as an object progresses through different visual states.
074: * </p>
075: *
076: * <p>
077: * There is an important special formulation of secondary style names called
078: * <i>dependent style names</i>. A dependent style name is a secondary style
079: * name prefixed with the primary style name of the widget itself. See
080: * {@link #addStyleName(String)} for details.
081: * </p>
082: */
083: public abstract class UIObject {
084:
085: private static final String EMPTY_STYLENAME_MSG = "Style names cannot be empty";
086:
087: private static final String NULL_HANDLE_MSG = "Null widget handle. If you "
088: + "are creating a composite, ensure that initWidget() has been called.";
089:
090: public static native boolean isVisible(Element elem) /*-{
091: return (elem.style.display != 'none');
092: }-*/;
093:
094: public static native void setVisible(Element elem, boolean visible) /*-{
095: elem.style.display = visible ? '' : 'none';
096: }-*/;
097:
098: /**
099: * Gets all of the element's style names, as a space-separated list.
100: *
101: * @param elem the element whose style is to be retrieved
102: * @return the objects's space-separated style names
103: */
104: protected static String getStyleName(Element elem) {
105: return DOM.getElementProperty(elem, "className");
106: }
107:
108: /**
109: * Gets the element's primary style name.
110: *
111: * @param elem the element whose primary style name is to be retrieved
112: * @return the element's primary style name
113: */
114: protected static String getStylePrimaryName(Element elem) {
115: String fullClassName = getStyleName(elem);
116:
117: // The primary style name is always the first token of the full CSS class
118: // name. There can be no leading whitespace in the class name, so it's not
119: // necessary to trim() it.
120: int spaceIdx = fullClassName.indexOf(' ');
121: if (spaceIdx >= 0) {
122: return fullClassName.substring(0, spaceIdx);
123: }
124: return fullClassName;
125: }
126:
127: /**
128: * Clears all of the element's style names and sets it to the given style.
129: *
130: * @param elem the element whose style is to be modified
131: * @param styleName the new style name
132: */
133: protected static void setStyleName(Element elem, String styleName) {
134: DOM.setElementProperty(elem, "className", styleName);
135: }
136:
137: /**
138: * This convenience method adds or removes a style name for a given element.
139: * This method is typically used to add and remove secondary style names, but
140: * it can be used to remove primary stylenames as well, but that is not
141: * recommended. See {@link #setStyleName(String)} for a description of how
142: * primary and secondary style names are used.
143: *
144: * @param elem the element whose style is to be modified
145: * @param style the secondary style name to be added or removed
146: * @param add <code>true</code> to add the given style, <code>false</code>
147: * to remove it
148: */
149: protected static void setStyleName(Element elem, String style,
150: boolean add) {
151: if (elem == null) {
152: throw new RuntimeException(NULL_HANDLE_MSG);
153: }
154:
155: style = style.trim();
156: if (style.length() == 0) {
157: throw new IllegalArgumentException(EMPTY_STYLENAME_MSG);
158: }
159:
160: // Get the current style string.
161: String oldStyle = getStyleName(elem);
162: int idx = oldStyle.indexOf(style);
163:
164: // Calculate matching index.
165: while (idx != -1) {
166: if (idx == 0 || oldStyle.charAt(idx - 1) == ' ') {
167: int last = idx + style.length();
168: int lastPos = oldStyle.length();
169: if ((last == lastPos)
170: || ((last < lastPos) && (oldStyle.charAt(last) == ' '))) {
171: break;
172: }
173: }
174: idx = oldStyle.indexOf(style, idx + 1);
175: }
176:
177: if (add) {
178: // Only add the style if it's not already present.
179: if (idx == -1) {
180: if (oldStyle.length() > 0) {
181: oldStyle += " ";
182: }
183: DOM.setElementProperty(elem, "className", oldStyle
184: + style);
185: }
186: } else {
187: // Don't try to remove the style if it's not there.
188: if (idx != -1) {
189: // Get the leading and trailing parts, without the removed name.
190: String begin = oldStyle.substring(0, idx).trim();
191: String end = oldStyle.substring(idx + style.length())
192: .trim();
193:
194: // Some contortions to make sure we don't leave extra spaces.
195: String newClassName;
196: if (begin.length() == 0) {
197: newClassName = end;
198: } else if (end.length() == 0) {
199: newClassName = begin;
200: } else {
201: newClassName = begin + " " + end;
202: }
203:
204: DOM.setElementProperty(elem, "className", newClassName);
205: }
206: }
207: }
208:
209: /**
210: * Sets the element's primary style name and updates all dependent style
211: * names.
212: *
213: * @param elem the element whose style is to be reset
214: * @param style the new primary style name
215: * @see #setStyleName(Element, String, boolean)
216: */
217: protected static void setStylePrimaryName(Element elem, String style) {
218: if (elem == null) {
219: throw new RuntimeException(NULL_HANDLE_MSG);
220: }
221:
222: // Style names cannot contain leading or trailing whitespace, and cannot
223: // legally be empty.
224: style = style.trim();
225: if (style.length() == 0) {
226: throw new IllegalArgumentException(EMPTY_STYLENAME_MSG);
227: }
228:
229: updatePrimaryAndDependentStyleNames(elem, style);
230: }
231:
232: /**
233: * Replaces all instances of the primary style name with newPrimaryStyleName.
234: */
235: private static native void updatePrimaryAndDependentStyleNames(
236: Element elem, String newPrimaryStyle) /*-{
237: var classes = elem.className.split(/\s+/);
238: if (!classes) {
239: return;
240: }
241:
242: var oldPrimaryStyle = classes[0];
243: var oldPrimaryStyleLen = oldPrimaryStyle.length;
244:
245: classes[0] = newPrimaryStyle;
246: for (var i = 1, n = classes.length; i < n; i++) {
247: var name = classes[i];
248: if (name.length > oldPrimaryStyleLen
249: && name.charAt(oldPrimaryStyleLen) == '-'
250: && name.indexOf(oldPrimaryStyle) == 0) {
251: classes[i] = newPrimaryStyle + name.substring(oldPrimaryStyleLen);
252: }
253: }
254: elem.className = classes.join(" ");
255: }-*/;
256:
257: private Element element;
258:
259: /**
260: * Adds a dependent style name by specifying the style name's suffix. The
261: * actual form of the style name that is added is:
262: *
263: * <pre class="code">
264: * getStylePrimaryName() + '-' + styleSuffix
265: * </pre>
266: *
267: * @param styleSuffix the suffix of the dependent style to be added.
268: * @see #setStylePrimaryName(String)
269: * @see #removeStyleDependentName(String)
270: * @see #addStyleName(String)
271: */
272: public void addStyleDependentName(String styleSuffix) {
273: addStyleName(getStylePrimaryName() + '-' + styleSuffix);
274: }
275:
276: /**
277: * Adds a secondary or dependent style name to this object. A secondary style
278: * name is an additional style name that is, in HTML/CSS terms, included as a
279: * space-separated token in the value of the CSS <code>class</code>
280: * attribute for this object's root element.
281: *
282: * <p>
283: * The most important use for this method is to add a special kind of
284: * secondary style name called a <i>dependent style name</i>. To add a
285: * dependent style name, use {@link #addStyleDependentName(String)}, which
286: * will prefix the 'style' argument with the result of
287: * {@link #getStylePrimaryName()} (followed by a '-'). For example, suppose
288: * the primary style name is <code>gwt-TextBox</code>. If the following
289: * method is called as <code>obj.setReadOnly(true)</code>:
290: * </p>
291: *
292: * <pre class="code">
293: * public void setReadOnly(boolean readOnly) {
294: * isReadOnlyMode = readOnly;
295: *
296: * // Create a dependent style name.
297: * String readOnlyStyle = "readonly";
298: *
299: * if (readOnly) {
300: * addStyleDependentName(readOnlyStyle);
301: * } else {
302: * removeStyleDependentName(readOnlyStyle);
303: * }
304: * }</pre>
305: *
306: * <p>
307: * then both of the CSS style rules below will be applied:
308: * </p>
309: *
310: * <pre class="code">
311: *
312: * // This rule is based on the primary style name and is always active.
313: * .gwt-TextBox {
314: * font-size: 12pt;
315: * }
316: *
317: * // This rule is based on a dependent style name that is only active
318: * // when the widget has called addStyleName(getStylePrimaryName() +
319: * // "-readonly").
320: * .gwt-TextBox-readonly {
321: * background-color: lightgrey;
322: * border: none;
323: * }</pre>
324: *
325: * <p>
326: * Dependent style names are powerful because they are automatically updated
327: * whenever the primary style name changes. Continuing with the example above,
328: * if the primary style name changed due to the following call:
329: * </p>
330: *
331: * <pre class="code">setStylePrimaryName("my-TextThingy");</pre>
332: *
333: * <p>
334: * then the object would be re-associated with following style rules, removing
335: * those that were shown above.
336: * </p>
337: *
338: * <pre class="code">
339: * .my-TextThingy {
340: * font-size: 20pt;
341: * }
342: *
343: * .my-TextThingy-readonly {
344: * background-color: red;
345: * border: 2px solid yellow;
346: * }</pre>
347: *
348: * <p>
349: * Secondary style names that are not dependent style names are not
350: * automatically updated when the primary style name changes.
351: * </p>
352: *
353: * @param style the secondary style name to be added
354: * @see UIObject
355: * @see #removeStyleName(String)
356: */
357: public void addStyleName(String style) {
358: setStyleName(getStyleElement(), style, true);
359: }
360:
361: /**
362: * Gets the object's absolute left position in pixels, as measured from the
363: * browser window's client area.
364: *
365: * @return the object's absolute left position
366: */
367: public int getAbsoluteLeft() {
368: return DOM.getAbsoluteLeft(getElement());
369: }
370:
371: /**
372: * Gets the object's absolute top position in pixels, as measured from the
373: * browser window's client area.
374: *
375: * @return the object's absolute top position
376: */
377: public int getAbsoluteTop() {
378: return DOM.getAbsoluteTop(getElement());
379: }
380:
381: /**
382: * Gets a handle to the object's underlying DOM element.
383: *
384: * @return the object's browser element
385: */
386: public Element getElement() {
387: return element;
388: }
389:
390: /**
391: * Gets the object's offset height in pixels. This is the total height of the
392: * object, including decorations such as border, margin, and padding.
393: *
394: * @return the object's offset height
395: */
396: public int getOffsetHeight() {
397: return DOM.getElementPropertyInt(element, "offsetHeight");
398: }
399:
400: /**
401: * Gets the object's offset width in pixels. This is the total width of the
402: * object, including decorations such as border, margin, and padding.
403: *
404: * @return the object's offset width
405: */
406: public int getOffsetWidth() {
407: return DOM.getElementPropertyInt(element, "offsetWidth");
408: }
409:
410: /**
411: * Gets all of the object's style names, as a space-separated list. If you
412: * wish to retrieve only the primary style name, call
413: * {@link #getStylePrimaryName()}.
414: *
415: * @return the objects's space-separated style names
416: * @see #getStylePrimaryName()
417: */
418: public String getStyleName() {
419: return getStyleName(getStyleElement());
420: }
421:
422: /**
423: * Gets the primary style name associated with the object.
424: *
425: * @return the object's primary style name
426: * @see #setStyleName(String)
427: * @see #addStyleName(String)
428: * @see #removeStyleName(String)
429: */
430: public String getStylePrimaryName() {
431: return getStylePrimaryName(getStyleElement());
432: }
433:
434: /**
435: * Gets the title associated with this object. The title is the 'tool-tip'
436: * displayed to users when they hover over the object.
437: *
438: * @return the object's title
439: */
440: public String getTitle() {
441: return DOM.getElementProperty(element, "title");
442: }
443:
444: /**
445: * Determines whether or not this object is visible.
446: *
447: * @return <code>true</code> if the object is visible
448: */
449: public boolean isVisible() {
450: return isVisible(element);
451: }
452:
453: /**
454: * Removes a dependent style name by specifying the style name's suffix.
455: *
456: * @param styleSuffix the suffix of the dependent style to be removed
457: * @see #setStylePrimaryName(Element, String)
458: * @see #addStyleDependentName(String)
459: * @see #addStyleName(String)
460: */
461: public void removeStyleDependentName(String styleSuffix) {
462: removeStyleName(getStylePrimaryName() + '-' + styleSuffix);
463: }
464:
465: /**
466: * Removes a style name. This method is typically used to remove secondary
467: * style names, but it can be used to remove primary stylenames as well. That
468: * use is not recommended.
469: *
470: * @param style the secondary style name to be removed
471: * @see #addStyleName(String)
472: */
473: public void removeStyleName(String style) {
474: setStyleName(getStyleElement(), style, false);
475: }
476:
477: /**
478: * Sets the object's height. This height does not include decorations such as
479: * border, margin, and padding.
480: *
481: * @param height the object's new height, in CSS units (e.g. "10px", "1em")
482: */
483: public void setHeight(String height) {
484: // This exists to deal with an inconsistency in IE's implementation where
485: // it won't accept negative numbers in length measurements
486: assert extractLengthValue(height.trim().toLowerCase()) >= 0 : "CSS heights should not be negative";
487: DOM.setStyleAttribute(element, "height", height);
488: }
489:
490: /**
491: * Sets the object's size, in pixels, not including decorations such as
492: * border, margin, and padding.
493: *
494: * @param width the object's new width, in pixels
495: * @param height the object's new height, in pixels
496: */
497: public void setPixelSize(int width, int height) {
498: if (width >= 0) {
499: setWidth(width + "px");
500: }
501: if (height >= 0) {
502: setHeight(height + "px");
503: }
504: }
505:
506: /**
507: * Sets the object's size. This size does not include decorations such as
508: * border, margin, and padding.
509: *
510: * @param width the object's new width, in CSS units (e.g. "10px", "1em")
511: * @param height the object's new height, in CSS units (e.g. "10px", "1em")
512: */
513: public void setSize(String width, String height) {
514: setWidth(width);
515: setHeight(height);
516: }
517:
518: /**
519: * Clears all of the object's style names and sets it to the given style. You
520: * should normally use {@link #setStylePrimaryName(String)} unless you wish to
521: * explicitly remove all existing styles.
522: *
523: * @param style the new style name
524: * @see #setStylePrimaryName(String)
525: */
526: public void setStyleName(String style) {
527: setStyleName(getStyleElement(), style);
528: }
529:
530: /**
531: * Sets the object's primary style name and updates all dependent style names.
532: *
533: * @param style the new primary style name
534: * @see #addStyleName(String)
535: * @see #removeStyleName(String)
536: */
537: public void setStylePrimaryName(String style) {
538: setStylePrimaryName(getStyleElement(), style);
539: }
540:
541: /**
542: * Sets the title associated with this object. The title is the 'tool-tip'
543: * displayed to users when they hover over the object.
544: *
545: * @param title the object's new title
546: */
547: public void setTitle(String title) {
548: if (title == null || title.length() == 0) {
549: DOM.removeElementAttribute(element, "title");
550: } else {
551: DOM.setElementAttribute(element, "title", title);
552: }
553: }
554:
555: /**
556: * Sets whether this object is visible.
557: *
558: * @param visible <code>true</code> to show the object, <code>false</code>
559: * to hide it
560: */
561: public void setVisible(boolean visible) {
562: setVisible(element, visible);
563: }
564:
565: /**
566: * Sets the object's width. This width does not include decorations such as
567: * border, margin, and padding.
568: *
569: * @param width the object's new width, in CSS units (e.g. "10px", "1em")
570: */
571: public void setWidth(String width) {
572: // This exists to deal with an inconsistency in IE's implementation where
573: // it won't accept negative numbers in length measurements
574: assert extractLengthValue(width.trim().toLowerCase()) >= 0 : "CSS widths should not be negative";
575: DOM.setStyleAttribute(element, "width", width);
576: }
577:
578: /**
579: * Adds a set of events to be sunk by this object. Note that only
580: * {@link Widget widgets} may actually receive events, but can receive events
581: * from all objects contained within them.
582: *
583: * @param eventBitsToAdd a bitfield representing the set of events to be added
584: * to this element's event set
585: * @see com.google.gwt.user.client.Event
586: */
587: public void sinkEvents(int eventBitsToAdd) {
588: DOM.sinkEvents(getElement(), eventBitsToAdd
589: | DOM.getEventsSunk(getElement()));
590: }
591:
592: /**
593: * This method is overridden so that any object can be viewed in the debugger
594: * as an HTML snippet.
595: *
596: * @return a string representation of the object
597: */
598: @Override
599: public String toString() {
600: if (element == null) {
601: return "(null handle)";
602: }
603: return DOM.toString(element);
604: }
605:
606: /**
607: * Removes a set of events from this object's event list.
608: *
609: * @param eventBitsToRemove a bitfield representing the set of events to be
610: * removed from this element's event set
611: * @see #sinkEvents
612: * @see com.google.gwt.user.client.Event
613: */
614: public void unsinkEvents(int eventBitsToRemove) {
615: DOM.sinkEvents(getElement(), DOM.getEventsSunk(getElement())
616: & (~eventBitsToRemove));
617: }
618:
619: /**
620: * Template method that returns the element to which style names will be
621: * applied. By default it returns the root element, but this method may be
622: * overridden to apply styles to a child element.
623: *
624: * @return the element to which style names will be applied
625: */
626: protected Element getStyleElement() {
627: return element;
628: }
629:
630: /**
631: * Sets this object's browser element. UIObject subclasses must call this
632: * method before attempting to call any other methods.
633: *
634: * If the browser element has already been set, then the current element's
635: * position is located in the DOM and removed. The new element is added into
636: * the previous element's position.
637: *
638: * @param elem the object's new element
639: */
640: protected void setElement(Element elem) {
641: if (this .element != null) {
642: // replace this.element in its parent with elem.
643: replaceNode(this .element, elem);
644: }
645:
646: this .element = elem;
647: }
648:
649: /**
650: * Intended to be used to pull the value out of a CSS length. We rely on the
651: * behavior of parseFloat to ignore non-numeric chars in its input. If the
652: * value is "auto" or "inherit", 0 will be returned.
653: *
654: * @param s The CSS length string to extract
655: * @return The leading numeric portion of <code>s</code>, or 0 if "auto" or
656: * "inherit" are passed in.
657: */
658: private native double extractLengthValue(String s) /*-{
659: if (s == "auto" || s == "inherit" || s == "") {
660: return 0;
661: } else {
662: return parseFloat(s);
663: }
664: }-*/;
665:
666: private native void replaceNode(Element node, Element newNode) /*-{
667: var p = node.parentNode;
668: if (!p) {
669: return;
670: }
671: p.insertBefore(newNode, node);
672: p.removeChild(node);
673: }-*/;
674: }
|