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.core.client.GWT;
019: import com.google.gwt.user.client.DOM;
020: import com.google.gwt.user.client.Element;
021: import com.google.gwt.user.client.Event;
022:
023: import java.util.ArrayList;
024: import java.util.Iterator;
025:
026: /**
027: * A widget that consists of a header and a content panel that discloses the
028: * content when a user clicks on the header.
029: *
030: * <h3>CSS Style Rules</h3>
031: * <ul class="css">
032: * <li>.gwt-DisclosurePanel { the panel's primary style }</li>
033: * <li>.gwt-DisclosurePanel-open { dependent style set when panel is open }</li>
034: * <li>.gwt-DisclosurePanel-closed { dependent style set when panel is closed }</li>
035: * <li>.header { the header section }</li>
036: * <li>.content { the content section }</li>
037: * </ul>
038: *
039: * <p>
040: * <img class='gallery' src='DisclosurePanel.png'/>
041: * </p>
042: *
043: * <p>
044: * The header and content sections can be easily selected using css with a child
045: * selector:<br/> .gwt-DisclosurePanel-open .header { ... }
046: * </p>
047: */
048: public final class DisclosurePanel extends Composite implements
049: FiresDisclosureEvents, HasWidgets {
050:
051: /**
052: * Used to wrap widgets in the header to provide click support. Effectively
053: * wraps the widget in an <code>anchor</code> to get automatic keyboard
054: * access.
055: */
056: private final class ClickableHeader extends SimplePanel {
057:
058: private ClickableHeader() {
059: // Anchor is used to allow keyboard access.
060: super (DOM.createAnchor());
061: Element elem = getElement();
062: DOM.setElementProperty(elem, "href", "javascript:void(0);");
063: // Avoids layout problems from having blocks in inlines.
064: DOM.setStyleAttribute(elem, "display", "block");
065: sinkEvents(Event.ONCLICK);
066: setStyleName(STYLENAME_HEADER);
067: }
068:
069: @Override
070: public final void onBrowserEvent(Event event) {
071: // no need to call super.
072: switch (DOM.eventGetType(event)) {
073: case Event.ONCLICK:
074: // Prevent link default action.
075: DOM.eventPreventDefault(event);
076: setOpen(!isOpen);
077: }
078: }
079: }
080:
081: /**
082: * The default header widget used within a {@link DisclosurePanel}.
083: */
084: private class DefaultHeader extends Widget implements HasText,
085: DisclosureHandler {
086:
087: /**
088: * imageTD holds the image for the icon, not null. labelTD holds the text
089: * for the label.
090: */
091: private final Element labelTD;
092:
093: private final Image iconImage;
094: private final DisclosurePanelImages images;
095:
096: private DefaultHeader(DisclosurePanelImages images, String text) {
097: this .images = images;
098:
099: iconImage = isOpen ? images.disclosurePanelOpen()
100: .createImage() : images.disclosurePanelClosed()
101: .createImage();
102:
103: // I do not need any Widgets here, just a DOM structure.
104: Element root = DOM.createTable();
105: Element tbody = DOM.createTBody();
106: Element tr = DOM.createTR();
107: final Element imageTD = DOM.createTD();
108: labelTD = DOM.createTD();
109:
110: setElement(root);
111:
112: DOM.appendChild(root, tbody);
113: DOM.appendChild(tbody, tr);
114: DOM.appendChild(tr, imageTD);
115: DOM.appendChild(tr, labelTD);
116:
117: // set image TD to be same width as image.
118: DOM.setElementProperty(imageTD, "align", "center");
119: DOM.setElementProperty(imageTD, "valign", "middle");
120: DOM.setStyleAttribute(imageTD, "width", iconImage
121: .getWidth()
122: + "px");
123:
124: DOM.appendChild(imageTD, iconImage.getElement());
125:
126: setText(text);
127:
128: addEventHandler(this );
129: setStyle();
130: }
131:
132: public final String getText() {
133: return DOM.getInnerText(labelTD);
134: }
135:
136: public final void onClose(DisclosureEvent event) {
137: setStyle();
138: }
139:
140: public final void onOpen(DisclosureEvent event) {
141: setStyle();
142: }
143:
144: public final void setText(String text) {
145: DOM.setInnerText(labelTD, text);
146: }
147:
148: private void setStyle() {
149: if (isOpen) {
150: images.disclosurePanelOpen().applyTo(iconImage);
151: } else {
152: images.disclosurePanelClosed().applyTo(iconImage);
153: }
154: }
155: }
156:
157: // Stylename constants.
158: private static final String STYLENAME_DEFAULT = "gwt-DisclosurePanel";
159:
160: private static final String STYLENAME_SUFFIX_OPEN = "open";
161:
162: private static final String STYLENAME_SUFFIX_CLOSED = "closed";
163:
164: private static final String STYLENAME_HEADER = "header";
165:
166: private static final String STYLENAME_CONTENT = "content";
167:
168: private static DisclosurePanelImages createDefaultImages() {
169: return GWT.create(DisclosurePanelImages.class);
170: }
171:
172: /**
173: * top level widget. The first child will be a reference to {@link #header}.
174: * The second child will either not exist or be a non-null to reference to
175: * {@link #content}.
176: */
177: private final VerticalPanel mainPanel = new VerticalPanel();
178:
179: /**
180: * holds the header widget.
181: */
182: private final ClickableHeader header = new ClickableHeader();
183:
184: /**
185: * the content widget, this can be null.
186: */
187: private Widget content;
188:
189: private boolean isOpen = false;
190:
191: /**
192: * null until #{@link #addEventHandler(DisclosureHandler)} is called (lazily
193: * initialized).
194: */
195: private ArrayList<DisclosureHandler> handlers;
196:
197: /**
198: * Creates an empty DisclosurePanel that is initially closed.
199: */
200: public DisclosurePanel() {
201: init(false);
202: }
203:
204: /**
205: * Creates a DisclosurePanel with the specified header text, an initial
206: * open/close state and a bundle of images to be used in the default header
207: * widget.
208: *
209: * @param images a bundle that provides disclosure panel specific images
210: * @param headerText the text to be displayed in the header
211: * @param isOpen the initial open/close state of the content panel
212: */
213: public DisclosurePanel(DisclosurePanelImages images,
214: String headerText, boolean isOpen) {
215: init(isOpen);
216: setHeader(new DefaultHeader(images, headerText));
217: }
218:
219: /**
220: * Creates a DisclosurePanel that will be initially closed using the specified
221: * text in the header.
222: *
223: * @param headerText the text to be displayed in the header.
224: */
225: public DisclosurePanel(String headerText) {
226: this (createDefaultImages(), headerText, false);
227: }
228:
229: /**
230: * Creates a DisclosurePanel with the specified header text and an initial
231: * open/close state.
232: *
233: * @param headerText the text to be displayed in the header
234: * @param isOpen the initial open/close state of the content panel
235: */
236: public DisclosurePanel(String headerText, boolean isOpen) {
237: this (createDefaultImages(), headerText, isOpen);
238: }
239:
240: /**
241: * Creates a DisclosurePanel that will be initially closed using a widget as
242: * the header.
243: *
244: * @param header the widget to be used as a header
245: */
246: public DisclosurePanel(Widget header) {
247: this (header, false);
248: }
249:
250: /**
251: * Creates a DisclosurePanel using a widget as the header and an initial
252: * open/close state.
253: *
254: * @param header the widget to be used as a header
255: * @param isOpen the initial open/close state of the content panel
256: */
257: public DisclosurePanel(Widget header, boolean isOpen) {
258: init(isOpen);
259: setHeader(header);
260: }
261:
262: public void add(Widget w) {
263: if (this .getContent() == null) {
264: setContent(w);
265: } else {
266: throw new IllegalStateException(
267: "A DisclosurePanel can only contain two Widgets.");
268: }
269: }
270:
271: /**
272: * Attaches an event handler to the panel to receive {@link DisclosureEvent}
273: * notification.
274: *
275: * @param handler the handler to be added (should not be null)
276: */
277: public final void addEventHandler(DisclosureHandler handler) {
278: if (handlers == null) {
279: handlers = new ArrayList<DisclosureHandler>();
280: }
281: handlers.add(handler);
282: }
283:
284: public void clear() {
285: setContent(null);
286: }
287:
288: /**
289: * Gets the widget that was previously set in {@link #setContent(Widget)}.
290: *
291: * @return the panel's current content widget
292: */
293: public final Widget getContent() {
294: return content;
295: }
296:
297: /**
298: * Gets the widget that is currently being used as a header.
299: *
300: * @return the widget currently being used as a header
301: */
302: public final Widget getHeader() {
303: return header.getWidget();
304: }
305:
306: /**
307: * Gets a {@link HasText} instance to provide access to the headers's text, if
308: * the header widget does provide such access.
309: *
310: * @return a reference to the header widget if it implements {@link HasText},
311: * <code>null</code> otherwise
312: */
313: public final HasText getHeaderTextAccessor() {
314: Widget widget = header.getWidget();
315: return (widget instanceof HasText) ? (HasText) widget : null;
316: }
317:
318: /**
319: * Determines whether the panel is open.
320: *
321: * @return <code>true</code> if panel is in open state
322: */
323: public final boolean isOpen() {
324: return isOpen;
325: }
326:
327: public Iterator<Widget> iterator() {
328: return WidgetIterators.createWidgetIterator(this ,
329: new Widget[] { getContent() });
330: }
331:
332: public boolean remove(Widget w) {
333: if (w == getContent()) {
334: setContent(null);
335: return true;
336: }
337: return false;
338: }
339:
340: /**
341: * Removes an event handler from the panel.
342: *
343: * @param handler the handler to be removed
344: */
345: public final void removeEventHandler(DisclosureHandler handler) {
346: if (handlers == null) {
347: return;
348: }
349: handlers.remove(handler);
350: }
351:
352: /**
353: * Sets the content widget which can be opened and closed by this panel. If
354: * there is a preexisting content widget, it will be detached.
355: *
356: * @param content the widget to be used as the content panel
357: */
358: public final void setContent(Widget content) {
359: final Widget currentContent = this .content;
360:
361: // Remove existing content widget.
362: if (currentContent != null) {
363: mainPanel.remove(currentContent);
364: currentContent.removeStyleName(STYLENAME_CONTENT);
365: }
366:
367: // Add new content widget if != null.
368: this .content = content;
369: if (content != null) {
370: mainPanel.add(content);
371: content.addStyleName(STYLENAME_CONTENT);
372: setContentDisplay();
373: }
374: }
375:
376: /**
377: * Sets the widget used as the header for the panel.
378: *
379: * @param headerWidget the widget to be used as the header
380: */
381: public final void setHeader(Widget headerWidget) {
382: header.setWidget(headerWidget);
383: }
384:
385: /**
386: * Changes the visible state of this <code>DisclosurePanel</code>.
387: *
388: * @param isOpen <code>true</code> to open the panel, <code>false</code>
389: * to close
390: */
391: public final void setOpen(boolean isOpen) {
392: if (this .isOpen != isOpen) {
393: this .isOpen = isOpen;
394: setContentDisplay();
395: fireEvent();
396: }
397: }
398:
399: private void fireEvent() {
400: if (handlers == null) {
401: return;
402: }
403:
404: DisclosureEvent event = new DisclosureEvent(this );
405: for (DisclosureHandler handler : handlers) {
406: if (isOpen) {
407: handler.onOpen(event);
408: } else {
409: handler.onClose(event);
410: }
411: }
412: }
413:
414: private void init(boolean isOpen) {
415: initWidget(mainPanel);
416: mainPanel.add(header);
417: this .isOpen = isOpen;
418: setStyleName(STYLENAME_DEFAULT);
419: setContentDisplay();
420: }
421:
422: private void setContentDisplay() {
423: if (isOpen) {
424: removeStyleDependentName(STYLENAME_SUFFIX_CLOSED);
425: addStyleDependentName(STYLENAME_SUFFIX_OPEN);
426: } else {
427: removeStyleDependentName(STYLENAME_SUFFIX_OPEN);
428: addStyleDependentName(STYLENAME_SUFFIX_CLOSED);
429: }
430:
431: if (content != null) {
432: content.setVisible(isOpen);
433: }
434: }
435: }
|