001: /* Combobox.java
002:
003: {{IS_NOTE
004: Purpose:
005:
006: Description:
007:
008: History:
009: Thu Dec 15 17:33:01 2005, Created by tomyeh
010: }}IS_NOTE
011:
012: Copyright (C) 2005 Potix Corporation. All Rights Reserved.
013:
014: {{IS_RIGHT
015: This program is distributed under GPL Version 2.0 in the hope that
016: it will be useful, but WITHOUT ANY WARRANTY.
017: }}IS_RIGHT
018: */
019: package org.zkoss.zul;
020:
021: import java.util.Iterator;
022: import java.util.List;
023: import java.util.Set;
024:
025: import org.zkoss.lang.Classes;
026: import org.zkoss.lang.Exceptions;
027: import org.zkoss.lang.Objects;
028: import org.zkoss.util.logging.Log;
029: import org.zkoss.xml.HTMLs;
030:
031: import org.zkoss.zk.ui.Component;
032: import org.zkoss.zk.ui.UiException;
033: import org.zkoss.zk.ui.WrongValueException;
034: import org.zkoss.zk.ui.event.Event;
035: import org.zkoss.zk.ui.event.EventListener;
036: import org.zkoss.zk.ui.event.Events;
037: import org.zkoss.zk.ui.event.InputEvent;
038: import org.zkoss.zk.ui.ext.client.Selectable;
039: import org.zkoss.zk.ui.ext.render.ChildChangedAware;
040: import org.zkoss.zk.au.out.AuInvoke;
041: import org.zkoss.zul.event.ListDataEvent;
042: import org.zkoss.zul.event.ListDataListener;
043:
044: /**
045: * A combo box.
046: *
047: * <p>Non-XUL extension. It is used to replace XUL menulist. This class
048: * is more flexible than menulist, such as {@link #setAutocomplete}
049: * {@link #setAutodrop}.
050: *
051: * <p>Default {@link #getSclass}: combobox.
052: *
053: * <p>Events: onOpen, onSelect<br/>
054: * Developers can listen to the onOpen event and initializes it
055: * when {@link org.zkoss.zk.ui.event.OpenEvent#isOpen} is true, and/or
056: * clean up if false.
057: *
058: * * <p>Besides assign a list model, you could assign a renderer
059: * (a {@link ComboitemRenderer} instance) to a combobox, such that
060: * the combobox will use this renderer to render the data returned by
061: * {@link ListModel#getElementAt}.
062: * If not assigned, the default renderer, which assumes a label per
063: * combo item, is used.
064: * In other words, the default renderer adds a label to
065: * a row by calling toString against the object returned
066: * by {@link ListModel#getElementAt}. (since 3.0.2)
067: *
068: * <p>Note: to have better performance, onOpen is sent only if
069: * a non-deferrable event listener is registered
070: * (see {@link org.zkoss.zk.ui.event.Deferrable}).
071: *
072: * @author tomyeh
073: * @see Comboitem
074: */
075: public class Combobox extends Textbox {
076: private static final Log log = Log.lookup(Combobox.class);
077: private static final String DEFAULT_IMAGE = "~./zul/img/combobtn.gif";
078: private String _img;
079: private boolean _autodrop, _autocomplete, _btnVisible = true;
080: private transient Comboitem _selItem;
081: private ListModel _model;
082: private ComboitemRenderer _renderer;
083: private transient ListDataListener _dataListener;
084: private transient EventListener _eventListener;
085:
086: public Combobox() {
087: setSclass("combobox");
088: }
089:
090: public Combobox(String value) throws WrongValueException {
091: this ();
092: setValue(value);
093: }
094:
095: protected String coerceToString(Object value) {
096: final Constraint constr = getConstraint();
097: final String val = super .coerceToString(value);
098: if (val.length() > 0
099: && constr != null
100: && constr instanceof SimpleConstraint
101: && (((SimpleConstraint) constr).getFlags() & SimpleConstraint.STRICT) != 0) {
102: for (Iterator it = getItems().iterator(); it.hasNext();) {
103: final String label = ((Comboitem) it.next()).getLabel();
104: if (val.equalsIgnoreCase(label))
105: return label;
106: }
107: }
108: return val;
109: }
110:
111: //-- ListModel dependent codes --//
112: /** Returns the list model associated with this combobox, or null
113: * if this combobox is not associated with any list data model.
114: * <p> Note: for implementation of auto-complete, the result of {@link #getItemCount()} is a subset of model.
115: * So, if the model implemented {@link ListSubModel} interface, you can't use the index of model to find the comboitem by {@link #getItemAtIndex(int)}.
116: * @since 3.0.2
117: * @see ListSubModel#getSubModel(Object, int)
118: */
119: public ListModel getModel() {
120: return _model;
121: }
122:
123: /** Sets the list model associated with this combobox.
124: * If a non-null model is assigned, no matter whether it is the same as
125: * the previous, it will always cause re-render.
126: *
127: * @param model the list model to associate, or null to dis-associate
128: * any previous model.
129: * @exception UiException if failed to initialize with the model
130: * @since 3.0.2
131: */
132: public void setModel(ListModel model) {
133: if (model != null) {
134: if (_model != model) {
135: if (_model != null) {
136: _model.removeListDataListener(_dataListener);
137: } else if (!getItems().isEmpty())
138: getItems().clear();
139:
140: initDataListener();
141: _model = model;
142: _model.addListDataListener(_dataListener);
143: if (model instanceof ListSubModel)
144: addEventListener(Events.ON_CHANGING, _eventListener);
145: }
146:
147: postOnInitRender(null);
148: //Since user might setModel and setRender separately or repeatedly,
149: //we don't handle it right now until the event processing phase
150: //such that we won't render the same set of data twice
151: //--
152: //For better performance, we shall load the first few row now
153: //(to save a roundtrip)
154: } else if (_model != null) {
155: _model.removeListDataListener(_dataListener);
156: if (_model instanceof ListSubModel)
157: removeEventListener(Events.ON_CHANGING, _eventListener);
158: _model = null;
159: if (!getItems().isEmpty())
160: getItems().clear();
161: }
162: }
163:
164: private void initDataListener() {
165: if (_dataListener == null)
166: _dataListener = new ListDataListener() {
167: public void onChange(ListDataEvent event) {
168: postOnInitRender(null);
169: }
170: };
171: if (_eventListener == null)
172: _eventListener = new EventListener() {
173: public void onEvent(Event event) throws Exception {
174: if (getModel() instanceof ListSubModel) {
175: final InputEvent ie = (InputEvent) event;
176: if (!ie.isChangingBySelectBack())
177: postOnInitRender(ie.getValue());
178: }
179: }
180: };
181: }
182:
183: /** Returns the renderer to render each row, or null if the default
184: * renderer is used.
185: * @since 3.0.2
186: */
187: public ComboitemRenderer getItemRenderer() {
188: return _renderer;
189: }
190:
191: /** Sets the renderer which is used to render each row
192: * if {@link #getModel} is not null.
193: *
194: * <p>Note: changing a render will not cause the combobox to re-render.
195: * If you want it to re-render, you could assign the same model again
196: * (i.e., setModel(getModel())), or fire an {@link ListDataEvent} event.
197: *
198: * @param renderer the renderer, or null to use the default.
199: * @exception UiException if failed to initialize with the model
200: * @since 3.0.2
201: */
202: public void setItemRenderer(ComboitemRenderer renderer) {
203: _renderer = renderer;
204: }
205:
206: /** Sets the renderer by use of a class name.
207: * It creates an instance automatically.
208: *@since 3.0.2
209: */
210: public void setItemRenderer(String clsnm)
211: throws ClassNotFoundException, NoSuchMethodException,
212: IllegalAccessException, InstantiationException,
213: java.lang.reflect.InvocationTargetException {
214: if (clsnm != null)
215: setItemRenderer((ComboitemRenderer) Classes
216: .newInstanceByThread(clsnm));
217: }
218:
219: /** Synchronizes the combobox to be consistent with the specified model.
220: */
221: private ListModel syncModel(String index) {
222: ComboitemRenderer renderer = null;
223: final ListModel subset = _model instanceof ListSubModel ? ((ListSubModel) _model)
224: .getSubModel(index, -1)
225: : _model;
226: final int newsz = subset.getSize();
227:
228: if (!getItems().isEmpty())
229: getItems().clear();
230:
231: for (int j = 0; j < newsz; ++j) {
232: if (renderer == null)
233: renderer = getRealRenderer();
234: newUnloadedItem(renderer).setParent(this );
235: }
236: return subset;
237: }
238:
239: /** Creates an new and unloaded Comboitem. */
240: private final Comboitem newUnloadedItem(ComboitemRenderer renderer) {
241: Comboitem item = null;
242: if (renderer instanceof ComboitemRendererExt)
243: item = ((ComboitemRendererExt) renderer).newComboitem(this );
244:
245: if (item == null) {
246: item = new Comboitem();
247: item.applyProperties();
248: }
249: return item;
250: }
251:
252: /** Handles a private event, onInitRender. It is used only for
253: * implementation, and you rarely need to invoke it explicitly.
254: * @since 3.0.2
255: */
256: public void onInitRender(Event data) {
257: final Renderer renderer = new Renderer();
258: final ListModel subset = syncModel(data.getData() != null ? (String) data
259: .getData()
260: : getValue());
261: try {
262: int pgsz = subset.getSize(), ofs = 0, j = 0;
263: for (Iterator it = getItems().listIterator(ofs); j < pgsz
264: && it.hasNext(); ++j)
265: renderer.render(subset, (Comboitem) it.next());
266: } catch (Throwable ex) {
267: renderer.doCatch(ex);
268: } finally {
269: renderer.doFinally();
270: }
271: Events.postEvent("onInitRenderLater", this , null);// notify databinding load-when.
272: }
273:
274: private void postOnInitRender(String idx) {
275: Events.postEvent("onInitRender", this , idx);
276: }
277:
278: private static final ComboitemRenderer getDefaultItemRenderer() {
279: return _defRend;
280: }
281:
282: private static final ComboitemRenderer _defRend = new ComboitemRenderer() {
283: public void render(Comboitem item, Object data) {
284: item.setLabel(Objects.toString(data));
285: item.setValue(data);
286: }
287: };
288:
289: /** Returns the renderer used to render rows.
290: */
291: private ComboitemRenderer getRealRenderer() {
292: return _renderer != null ? _renderer : getDefaultItemRenderer();
293: }
294:
295: /** Used to render comboitem if _model is specified. */
296: private class Renderer implements java.io.Serializable {
297: private final ComboitemRenderer _renderer;
298: private boolean _rendered, _ctrled;
299:
300: private Renderer() {
301: _renderer = getRealRenderer();
302: }
303:
304: private void render(ListModel subset, Comboitem item)
305: throws Throwable {
306:
307: if (!_rendered && (_renderer instanceof RendererCtrl)) {
308: ((RendererCtrl) _renderer).doTry();
309: _ctrled = true;
310: }
311:
312: try {
313: _renderer.render(item, subset.getElementAt(getItems()
314: .indexOf(item)));
315: } catch (Throwable ex) {
316: try {
317: item.setLabel(Exceptions.getMessage(ex));
318: } catch (Throwable t) {
319: log.error(t);
320: }
321: throw ex;
322: }
323: _rendered = true;
324: }
325:
326: private void doCatch(Throwable ex) {
327: if (_ctrled) {
328: try {
329: ((RendererCtrl) _renderer).doCatch(ex);
330: } catch (Throwable t) {
331: throw UiException.Aide.wrap(t);
332: }
333: } else {
334: throw UiException.Aide.wrap(ex);
335: }
336: }
337:
338: private void doFinally() {
339: if (_ctrled)
340: ((RendererCtrl) _renderer).doFinally();
341: }
342: }
343:
344: /** Returns whether to automatically drop the list if users is changing
345: * this text box.
346: * <p>Default: false.
347: */
348: public boolean isAutodrop() {
349: return _autodrop;
350: }
351:
352: /** Sets whether to automatically drop the list if users is changing
353: * this text box.
354: */
355: public void setAutodrop(boolean autodrop) {
356: if (_autodrop != autodrop) {
357: _autodrop = autodrop;
358: smartUpdate("z.adr", autodrop);
359: }
360: }
361:
362: /** Returns whether to automatically complete this text box
363: * by matching the nearest item ({@link Comboitem}.
364: *
365: * <p>Default: false.
366: *
367: * <p>If true, the nearest item will be searched and the text box is
368: * updated automatically.
369: * If false, user has to click the item or use the DOWN or UP keys to
370: * select it back.
371: *
372: * <p>Note: this feature is reserved and not yet implemented.
373: * Don't confuse it with the auto-completion feature mentioned by
374: * other framework. Such kind of auto-completion is supported well
375: * by listening to the onChanging event.
376: */
377: public boolean isAutocomplete() {
378: return _autocomplete;
379: }
380:
381: /** Sets whether to automatically complete this text box
382: * by matching the nearest item ({@link Comboitem}.
383: */
384: public void setAutocomplete(boolean autocomplete) {
385: if (_autocomplete != autocomplete) {
386: _autocomplete = autocomplete;
387: smartUpdate("z.aco", autocomplete);
388: }
389: }
390:
391: /** Drops down or closes the list of combo items ({@link Comboitem}.
392: *
393: * @since 3.0.1
394: * @see #open
395: * @see #close
396: */
397: public void setOpen(boolean open) {
398: if (open)
399: open();
400: else
401: close();
402: }
403:
404: /** Drops down the list of combo items ({@link Comboitem}.
405: * It is the same as setOpen(true).
406: *
407: * @since 3.0.1
408: */
409: public void open() {
410: response("dropdn", new AuInvoke(this , "dropdn", true));
411: }
412:
413: /** Closes the list of combo items ({@link Comboitem} if it was
414: * dropped down.
415: * It is the same as setOpen(false).
416: *
417: * @since 3.0.1
418: */
419: public void close() {
420: response("dropdn", new AuInvoke(this , "dropdn", false));
421: }
422:
423: /** Returns whether the button (on the right of the textbox) is visible.
424: * <p>Default: true.
425: */
426: public boolean isButtonVisible() {
427: return _btnVisible;
428: }
429:
430: /** Sets whether the button (on the right of the textbox) is visible.
431: */
432: public void setButtonVisible(boolean visible) {
433: if (_btnVisible != visible) {
434: _btnVisible = visible;
435: smartUpdate("z.btnVisi", visible);
436: }
437: }
438:
439: /** Returns the URI of the button image.
440: * @since 2.4.1
441: */
442: public String getImage() {
443: return _img != null ? _img : DEFAULT_IMAGE;
444: }
445:
446: /** Sets the URI of the button image.
447: *
448: * @param img the URI of the button image. If null or empty, the default
449: * URI is used.
450: * @since 2.4.1
451: */
452: public void setImage(String img) {
453: if (img != null
454: && (img.length() == 0 || DEFAULT_IMAGE.equals(img)))
455: img = null;
456: if (!Objects.equals(_img, img)) {
457: _img = img;
458: invalidate();
459: }
460: }
461:
462: /** Returns a 'live' list of all {@link Comboitem}.
463: * By live we mean you can add or remove them directly with
464: * the List interface.
465: *
466: * <p>Currently, it is the same as {@link #getChildren}. However,
467: * we might add other kind of children in the future.
468: */
469: public List getItems() {
470: return getChildren();
471: }
472:
473: /** Returns the number of items.
474: */
475: public int getItemCount() {
476: return getItems().size();
477: }
478:
479: /** Returns the item at the specified index.
480: */
481: public Comboitem getItemAtIndex(int index) {
482: return (Comboitem) getItems().get(index);
483: }
484:
485: /** Appends an item.
486: */
487: public Comboitem appendItem(String label) {
488: final Comboitem item = new Comboitem(label);
489: item.setParent(this );
490: return item;
491: }
492:
493: /** Removes the child item in the list box at the given index.
494: * @return the removed item.
495: */
496: public Comboitem removeItemAt(int index) {
497: final Comboitem item = getItemAtIndex(index);
498: removeChild(item);
499: return item;
500: }
501:
502: /** Returns the selected item.
503: * @since 2.4.0
504: */
505: public Comboitem getSelectedItem() {
506: return _selItem;
507: }
508:
509: /** Deselects the currently selected items and selects the given item.
510: * <p>Note: if the label of comboitem has the same more than one, the first
511: * comboitem will be selected at client side, it is a limitation of {@link Combobox}
512: * and it is different from {@link Listbox}.</p>
513: * @since 3.0.2
514: */
515: public void setSelectedItem(Comboitem item) {
516: setSelectedIndex(getItems().indexOf(item));
517: }
518:
519: /** Deselects the currently selected items and selects
520: * the item with the given index.
521: * <p>Note: if the label of comboitem has the same more than one, the first
522: * comboitem will be selected at client side, it is a limitation of {@link Combobox}
523: * and it is different from {@link Listbox}.</p>
524: * @since 3.0.2
525: */
526: public void setSelectedIndex(int jsel) {
527: if (jsel >= getItemCount())
528: throw new UiException("Out of bound: " + jsel
529: + " while size=" + getItemCount());
530: if (jsel < -1)
531: jsel = -1;
532: if (jsel < 0) {
533: _selItem = null;
534: setValue("");
535: } else {
536: _selItem = getItemAtIndex(jsel);
537: setValue(_selItem.getLabel());
538: }
539: }
540:
541: /** Returns the index of the selected item, or -1 if not selected.
542: * @since 3.0.1
543: */
544: public int getSelectedIndex() {
545: return _selItem != null ? getItems().indexOf(_selItem) : -1;
546: }
547:
548: //-- super --//
549: public void setMultiline(boolean multiline) {
550: if (multiline)
551: throw new UnsupportedOperationException(
552: "Combobox doesn't support multiline");
553: }
554:
555: public void setRows(int rows) {
556: if (rows != 1)
557: throw new UnsupportedOperationException(
558: "Combobox doesn't support multiple rows, " + rows);
559: }
560:
561: public String getOuterAttrs() {
562: final String attrs = super .getOuterAttrs();
563: final boolean aco = isAutocomplete(), adr = isAutodrop();
564: if (!isAsapRequired(Events.ON_OPEN)
565: && !isAsapRequired(Events.ON_SELECT) && !aco && !adr)
566: return attrs;
567:
568: final StringBuffer sb = new StringBuffer(64).append(attrs);
569: appendAsapAttr(sb, Events.ON_OPEN);
570: appendAsapAttr(sb, Events.ON_SELECT);
571: if (aco)
572: HTMLs.appendAttribute(sb, "z.aco", "true");
573: if (adr)
574: HTMLs.appendAttribute(sb, "z.adr", "true");
575: return sb.toString();
576: }
577:
578: public String getInnerAttrs() {
579: final String attrs = super .getInnerAttrs();
580: final String style = getInnerStyle();
581: return style.length() > 0 ? attrs + " style=\"" + style + '"'
582: : attrs;
583: }
584:
585: private String getInnerStyle() {
586: final StringBuffer sb = new StringBuffer(32).append(HTMLs
587: .getTextRelevantStyle(getRealStyle()));
588: HTMLs.appendStyle(sb, "width", getWidth());
589: HTMLs.appendStyle(sb, "height", getHeight());
590: return sb.toString();
591: }
592:
593: /** Returns RS_NO_WIDTH|RS_NO_HEIGHT.
594: */
595: protected int getRealStyleFlags() {
596: return super .getRealStyleFlags() | RS_NO_WIDTH | RS_NO_HEIGHT;
597: }
598:
599: //-- Component --//
600: public boolean insertBefore(Component newChild, Component refChild) {
601: if (!(newChild instanceof Comboitem))
602: throw new UiException("Unsupported child for Combobox: "
603: + newChild);
604: return super .insertBefore(newChild, refChild);
605: }
606:
607: /** Childable. */
608: public boolean isChildable() {
609: return true;
610: }
611:
612: public void onChildAdded(Component child) {
613: super .onChildAdded(child);
614: smartUpdate("repos", "true");
615: }
616:
617: public void onChildRemoved(Component child) {
618: super .onChildRemoved(child);
619: smartUpdate("repos", "true");
620: }
621:
622: /*package*/final void reIndex() {
623: final String value = getValue();
624: if (_selItem == null
625: || !Objects.equals(value, _selItem.getLabel())) {
626: _selItem = null;
627: for (Iterator it = getItems().iterator(); it.hasNext();) {
628: final Comboitem item = (Comboitem) it.next();
629: if (Objects.equals(value, item.getLabel())) {
630: _selItem = item;
631: break;
632: }
633: }
634: }
635: }
636:
637: //Cloneable//
638: public Object clone() {
639: final int idx = getSelectedIndex();
640: final Combobox clone = (Combobox) super .clone();
641: clone._selItem = idx > -1 && clone.getItemCount() > idx ? clone
642: .getItemAtIndex(idx) : null;
643: return clone;
644: }
645:
646: // Serializable//
647: //NOTE: they must be declared as private
648: private synchronized void writeObject(java.io.ObjectOutputStream s)
649: throws java.io.IOException {
650: s.defaultWriteObject();
651:
652: s.writeInt(getSelectedIndex());
653: }
654:
655: private synchronized void readObject(java.io.ObjectInputStream s)
656: throws java.io.IOException, ClassNotFoundException {
657: s.defaultReadObject();
658:
659: final int idx = s.readInt();
660: if (idx > -1 && getItemCount() > idx)
661: _selItem = getItemAtIndex(idx);
662: }
663:
664: //-- ComponentCtrl --//
665: protected Object newExtraCtrl() {
666: return new ExtraCtrl();
667: }
668:
669: /** A utility class to implement {@link #getExtraCtrl}.
670: * It is used only by component developers.
671: */
672: protected class ExtraCtrl extends Textbox.ExtraCtrl implements
673: ChildChangedAware, Selectable {
674: //ChildChangedAware//
675: public boolean isChildChangedAware() {
676: return true;
677: }
678:
679: public void selectItemsByClient(Set selItems) {
680: _selItem = selItems != null && !selItems.isEmpty() ? (Comboitem) selItems
681: .iterator().next()
682: : null;
683: }
684: }
685: }
|