001: /*******************************************************************************
002: * Copyright (c) 2000, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: * Darrell Meyer <darrell@mygwt.net> - derived implementation
011: *******************************************************************************/package net.mygwt.ui.client.widget;
012:
013: import java.util.ArrayList;
014: import java.util.Collections;
015: import java.util.Comparator;
016: import java.util.HashMap;
017: import java.util.Map;
018:
019: import net.mygwt.ui.client.Events;
020: import net.mygwt.ui.client.MyDOM;
021: import net.mygwt.ui.client.MyGWT;
022: import net.mygwt.ui.client.Style;
023: import net.mygwt.ui.client.event.BaseEvent;
024: import net.mygwt.ui.client.util.Markup;
025: import net.mygwt.ui.client.widget.menu.Menu;
026:
027: import com.google.gwt.user.client.DOM;
028: import com.google.gwt.user.client.Element;
029: import com.google.gwt.user.client.Event;
030: import com.google.gwt.user.client.ui.KeyboardListener;
031:
032: /**
033: * Displays a list of list items.
034: *
035: * <p>
036: * Important: This component MUST be sized in pixels to render properly. This
037: * can be done directly or by a layout. Percentage based sizes will not work.
038: * </p>
039: *
040: * <dl>
041: * <dt><b>Styles:</b></dt>
042: * <dd>NONE, SINGLE, MULTI, CHECK, FLAT</dd>
043: *
044: * <dt><b>Events:</b></dt>
045: *
046: * <dd><b>BeforeAdd</b> : (widget, item, index)<br>
047: * <div>Fires before an item is added or inserted. Listeners can set the
048: * <code>doit</code> field to <code>false</code> to cancel the action.</div>
049: * <ul>
050: * <li>widget : this</li>
051: * <li>item : the item being added</li>
052: * <li>index : the index at which the item will be added</li>
053: * </ul>
054: * </dd>
055: *
056: * <dd><b>BeforeRemove</b> : (widget, item)<br>
057: * <div>Fires before an item is removed. Listeners can set the <code>doit</code>
058: * field to <code>false</code> to cancel the action.</div>
059: * <ul>
060: * <li>widget : this</li>
061: * <li>item : the item being removed</li>
062: * </ul>
063: * </dd>
064: *
065: * <dd><b>Add</b> : (widget, item, index)<br>
066: * <div>Fires after an item has been added or inserted.</div>
067: * <ul>
068: * <li>widget : this</li>
069: * <li>item : the item that was added</li>
070: * <li>index : the index at which the item will be added</li>
071: * </ul>
072: * </dd>
073: *
074: * <dd><b>Remove</b> : (widget, item)<br>
075: * <div>Fires after an item has been removed.</div>
076: * <ul>
077: * <li>widget : this</li>
078: * <li>item : the item that was removed</li>
079: * </ul>
080: * </dd>
081: *
082: * <dd><b>SelectionChange</b> : (widget, item)<br>
083: * <div>Fires after the selection changes.</div>
084: * <ul>
085: * <li>widget : this</li>
086: * <li>item : the item that is selected (single select)</li>
087: * </ul>
088: * </dd>
089: *
090: * <dd><b>ContextMenu</b> : (widget)<br>
091: * <div>Fires before the list's context menu is shown. Listeners can set the
092: * <code>doit</code> field to <code>false</code> to cancel the action.</div>
093: * <ul>
094: * <li>widget : this</li>
095: * </ul>
096: * </dd>
097: *
098: * <dd><b>CheckChange</b> : (widget, item)<br>
099: * <div>Fires after an item is selected.</div>
100: * <ul>
101: * <li>widget : this</li>
102: * <li>item : the item that is selected (single select)</li>
103: * </ul>
104: * </dd>
105: *
106: * <dt><b>CSS:</b></dt>
107: * <dd>.my-list (the list itself)</dd>
108: * <dd>.my-listitem (list item)</dd>
109: * <dd>.my-listitem .my-listitem-text (list item text)</dd>
110: * </dl>
111: */
112: public class List extends ScrollContainer {
113:
114: /**
115: * maxDepth specifies the max number of parent nodes to search in
116: * {@link #findItem(Element)}. Default value is 15.
117: */
118: protected int maxDepth = 15;
119: protected ListItem lastSelected;
120: protected ListItem hoverItem;
121: boolean check;
122: private Element inner;
123: private ArrayList selected;
124: private boolean singleSelect;
125: private boolean multiSelect;
126: private int selectionMode;
127: private Map nodes = new HashMap();
128:
129: /**
130: * Creates a new single select list.
131: */
132: public List() {
133: this (Style.SINGLE);
134: }
135:
136: /**
137: * Creates a new list with the given style.
138: *
139: * @param style the style information.
140: */
141: public List(int style) {
142: setStyle(style);
143: baseStyle = "my-list";
144: attachChildren = false;
145: }
146:
147: /**
148: * Adds an item to the list.
149: *
150: * @param item the item to add
151: */
152: public void add(ListItem item) {
153: insert(item, getWidgetCount());
154: }
155:
156: /**
157: * Adds an item to the list.
158: *
159: * @param text the item's text
160: */
161: public void add(String text) {
162: ListItem item = new ListItem(text);
163: add(item);
164: }
165:
166: /**
167: * Deselects the item at the given index.
168: *
169: * @param index the index of the item to deselect
170: */
171: public void deselect(int index) {
172: selectItems(index, index, false, true);
173: }
174:
175: /**
176: * Deselects the items at the given indices.
177: *
178: * @param start the start index of the items to deselect
179: * @param end the start index of the items to deselect
180: */
181: public void deselect(int start, int end) {
182: selectItems(start, end, false, true);
183: }
184:
185: /**
186: * Deselects the item at the given index.
187: *
188: * @param item the item to deselect
189: */
190: public void deselect(ListItem item) {
191: deselect(indexOf(item));
192: }
193:
194: /**
195: * Deselects all selected items.
196: */
197: public void deselectAll() {
198: if (getWidgetCount() > 0) {
199: selectItems(0, getWidgetCount() - 1, false, false);
200: }
201: }
202:
203: /**
204: * Returns the item using the specified target.
205: *
206: * @param element the element or child element
207: * @return the item
208: */
209: public ListItem findItem(Element element) {
210: if (getItemCount() > 0) {
211: String style = getItem(0).baseStyle;
212: Element elem = MyDOM.findParent(style, element, maxDepth);
213: if (elem != null) {
214: return (ListItem) nodes.get(MyDOM.getId(elem));
215: }
216: }
217: return null;
218: }
219:
220: /**
221: * Returns an array of checked items.
222: *
223: * @return the checked items
224: */
225: public ListItem[] getChecked() {
226: java.util.List temp = new ArrayList();
227: int count = getItemCount();
228: for (int i = 0; i < count; i++) {
229: if (getItem(i).isChecked()) {
230: temp.add(getItem(i));
231: }
232: }
233: return (ListItem[]) temp.toArray(new ListItem[0]);
234: }
235:
236: public Menu getContextMenu() {
237: return super .getContextMenu();
238: }
239:
240: /**
241: * Returns the item at the specified index.
242: *
243: * @param index the index
244: * @return the item
245: */
246: public ListItem getItem(int index) {
247: if ((index < 0) || (index >= items.size()))
248: return null;
249: return (ListItem) items.get(index);
250: }
251:
252: /**
253: * Returns the number of child items.
254: *
255: * @return the item count
256: */
257: public int getItemCount() {
258: return items.size();
259: }
260:
261: /**
262: * Returns the child items.
263: *
264: * @return the child items
265: */
266: public java.util.List getItems() {
267: return items;
268: }
269:
270: public Element getLayoutTarget() {
271: return inner;
272: }
273:
274: /**
275: * Returns the selected item. If the list is multi-select, returns the first
276: * selected item.
277: *
278: * @return the item or <code>null</code> if no selections
279: */
280: public ListItem getSelectedItem() {
281: if (selected.size() > 0) {
282: return getSelection()[0];
283: }
284: return null;
285: }
286:
287: /**
288: * Returns the selected items.
289: *
290: * @return the selected items
291: */
292: public ListItem[] getSelection() {
293: return (ListItem[]) selected.toArray(new ListItem[0]);
294: }
295:
296: /**
297: * Returns the selection mode.
298: *
299: * @return the selection mode
300: */
301: public int getSelectionMode() {
302: return selectionMode;
303: }
304:
305: /**
306: * Returns the index of the item or -1 if not found.
307: *
308: * @param item the item
309: * @return the index
310: */
311: public int indexOf(ListItem item) {
312: return items.indexOf(item);
313: }
314:
315: /**
316: * Inserts an item into the list at the given index.
317: *
318: * @param item the item
319: * @param index the insert location
320: */
321: public void insert(ListItem item, int index) {
322: if (fireEvent(Events.BeforeAdd, this , item, index)) {
323: item.list = this ;
324: if (check) {
325: item.markup = Markup.ITEM_CHECK;
326: }
327: items.add(index, item);
328: if (rendered) {
329: renderItem(item, index);
330: }
331: register(item);
332: fireEvent(Events.Add, this , item);
333: }
334: }
335:
336: /**
337: * Returns <code>true</code> if the item is selected.
338: *
339: * @param item the item
340: * @return the select state
341: */
342: public boolean isSelected(ListItem item) {
343: return selected.contains(item);
344: }
345:
346: /**
347: * Moves the current selections down one level.
348: */
349: public void moveSelectedDown() {
350: int count = selected.size();
351: if (count == 0) {
352: return;
353: }
354: Collections.sort(selected, new Comparator() {
355:
356: public int compare(Object o1, Object o2) {
357: ListItem li1 = (ListItem) o1;
358: ListItem li2 = (ListItem) o2;
359: return indexOf(li1) < indexOf(li2) ? 1 : 0;
360: }
361:
362: });
363: ListItem[] items = new ListItem[count];
364: for (int i = 0; i < count; i++) {
365: items[i] = (ListItem) selected.get(i);
366: }
367: for (int j = 0; j < count; j++) {
368: int index = indexOf(items[j]);
369: if (index != (getItemCount() - 1)) {
370: remove(items[j]);
371: insert(items[j], ++index);
372: selectItems(index, index, true, true, true);
373: }
374: }
375: }
376:
377: /**
378: * Moves the current selections up one level.
379: */
380: public void moveSelectedUp() {
381: int count = selected.size();
382: if (count == 0) {
383: return;
384: }
385: Collections.sort(selected, new Comparator() {
386:
387: public int compare(Object o1, Object o2) {
388: ListItem li1 = (ListItem) o1;
389: ListItem li2 = (ListItem) o2;
390: return indexOf(li1) > indexOf(li2) ? 1 : 0;
391: }
392:
393: });
394: ListItem[] items = new ListItem[count];
395: for (int i = 0; i < count; i++) {
396: items[i] = (ListItem) selected.get(i);
397: }
398: for (int j = 0; j < count; j++) {
399: int index = indexOf(items[j]);
400: if (index > 0) {
401: remove(items[j]);
402: insert(items[j], --index);
403: selectItems(index, index, true, true, true);
404: }
405: }
406: }
407:
408: public void onBaseEvent(BaseEvent be) {
409: ListItem item = findItem(be.getTarget());
410: if (item != null) {
411: item.onBrowserEvent(be.event);
412: }
413:
414: if (item != null && be.type == Events.MouseDown
415: && !be.isRightClick()) {
416: onItemClick(be, item);
417: }
418:
419: if (lastSelected != null && be.type == Events.KeyDown) {
420: onItemKeyPress(be, lastSelected);
421: }
422: }
423:
424: /**
425: * Removes the item from the list.
426: *
427: * @param item the item to be removed
428: */
429: public void remove(ListItem item) {
430: if (fireEvent(Events.BeforeRemove, this , item)) {
431: if (lastSelected == item) {
432: lastSelected = null;
433: }
434: selected.remove(item);
435: item.list = null;
436:
437: unregister(item);
438:
439: super .remove(item);
440: fireEvent(Events.Remove, this , item);
441: }
442: }
443:
444: /**
445: * Removes all the items from the list.
446: */
447: public void removeAll() {
448: int count = getWidgetCount();
449: for (int i = 0; i < count; i++) {
450: remove(getItem(0));
451: }
452: }
453:
454: /**
455: * Scrolls the item into view.
456: *
457: * @param item the item
458: */
459: public void scrollIntoView(ListItem item) {
460: MyDOM.scrollIntoView(item.getElement(), inner, false);
461: }
462:
463: /**
464: * Selects the item at the index. If the item at the index was already
465: * selected, it remains selected.
466: *
467: * @param index the index of the item to select
468: */
469: public void select(int index) {
470: selectItems(index, index, true, multiSelect);
471: }
472:
473: /**
474: * Selects the items in the range specified by the given indices. The current
475: * selection is not cleared before the new items are selected.
476: *
477: * @param start the start of the range
478: * @param end the end of the range
479: */
480: public void select(int start, int end) {
481: selectItems(start, end, true, true);
482: }
483:
484: /**
485: * Selects the specified item.
486: *
487: * @param item the item to be selected
488: */
489: public void select(ListItem item) {
490: select(indexOf(item));
491: }
492:
493: /**
494: * Selects all of the items in the list. If the list is single-select, do
495: * nothing.
496: */
497: public void selectAll() {
498: if (multiSelect) {
499: selectItems(0, getWidgetCount() - 1, true, true);
500: }
501: }
502:
503: public void setContextMenu(Menu menu) {
504: super .setContextMenu(menu);
505: }
506:
507: /**
508: * Selects the item. The current selection is cleared.
509: *
510: * @param item the item to select
511: */
512: public void setSelection(ListItem item) {
513: int index = indexOf(item);
514: selectItems(index, index, true, false);
515: }
516:
517: /**
518: * Selects the items. The current selection is cleared.
519: *
520: * @param items the items to select
521: */
522: public void setSelection(ListItem[] items) {
523: deselectAll();
524: for (int i = 0; i < items.length; i++) {
525: int index = indexOf(items[i]);
526: selectItems(index, index, true, false);
527: }
528: }
529:
530: /**
531: * Sets the selection mode. Valids values are SINGLE and MULTI. Has no effect
532: * if called after the list is rendered.
533: *
534: * @param mode the selection mode
535: */
536: public void setSelectionMode(int mode) {
537: if (!isRendered()) {
538: this .selectionMode = mode;
539: singleSelect = mode == Style.SINGLE;
540: multiSelect = mode == Style.MULTI;
541: }
542: }
543:
544: /**
545: * Sets the list's style. Has no effect if called after the list is rendered.
546: * See the class documentation for valid styles.
547: *
548: * @param style the list style
549: */
550: public void setStyle(int style) {
551: if (!isRendered()) {
552: this .style = style | Style.FOCUSABLE;
553: attachChildren = false;
554: selected = new ArrayList();
555:
556: selectionMode = (style & Style.MULTI) != 0 ? Style.MULTI
557: : Style.SINGLE;
558: singleSelect = selectionMode == Style.SINGLE;
559: multiSelect = selectionMode == Style.MULTI;
560:
561: if ((style & Style.CHECK) != 0) {
562: check = true;
563: }
564: }
565: }
566:
567: protected void onHideContextMenu() {
568: super .onHideContextMenu();
569: clearHoverStyles();
570: }
571:
572: protected void onItemClick(BaseEvent be, ListItem item) {
573: be.stopEvent();
574: if (check) {
575: Element checkElem;
576: if (item.checkBtn == null) {
577: checkElem = item.getElement();
578: } else {
579: checkElem = item.checkBtn.getElement();
580: }
581: if (DOM.isOrHasChild(checkElem, be.getTarget())) {
582: item.setChecked(!item.isChecked());
583: fireEvent(Events.CheckChange, this , item);
584: return;
585: }
586: }
587:
588: int index = indexOf(item);
589:
590: if (DOM.eventGetButton(be.event) == Event.BUTTON_RIGHT) {
591: if (singleSelect || getSelection().length == 0) {
592: selectItems(index, index, true, false);
593: } else {
594: selectItems(index, index, true, true);
595: }
596: return;
597: }
598:
599: if (singleSelect) {
600: boolean sel = true;
601: if (isSelected(item) && be.isControlKey()) {
602: sel = false;
603: }
604: if (isSelected(item)) {
605: return;
606: }
607: selectItems(index, index, sel, false);
608: return;
609: }
610:
611: if (multiSelect) {
612: if (be.isShiftKey()) {
613: if (lastSelected != null) {
614: selectItems(indexOf(lastSelected), index, true,
615: true);
616: } else {
617: selectItems(0, index, true, false);
618: }
619: } else if (be.isControlKey()) {
620: selectItems(index, index, !isSelected(item), true);
621: } else {
622: selectItems(index, index, true, false);
623: }
624: }
625: }
626:
627: protected void onRender() {
628: inner = DOM.createDiv();
629: MyDOM.setStyleName(inner, baseStyle + "-inner");
630:
631: setElement(DOM.createDiv());
632: DOM.appendChild(getElement(), inner);
633:
634: if ((style & Style.FLAT) != 0) {
635: setStyleName(baseStyle + "-flat");
636: } else {
637: setStyleName(baseStyle);
638: }
639:
640: setScrollEnabled(true);
641:
642: disableTextSelection(true);
643:
644: sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS
645: | Event.KEYEVENTS);
646: renderAll();
647: }
648:
649: protected void onResize(int width, int height) {
650: if (height != Style.DEFAULT) {
651: height -= MyDOM.getBorderWidth(getElement(), Style.TOP
652: | Style.BOTTOM);
653: height -= 2;// inner padding
654: MyDOM.setHeight(inner, height, true);
655: }
656: if (width != Style.DEFAULT) {
657: width -= MyDOM.getBorderWidth(getElement(), Style.LEFT
658: | Style.RIGHT);
659: width -= 2;// inner padding
660: MyDOM.setWidth(inner, width, true);
661: }
662: }
663:
664: protected void onRightClick(BaseEvent be) {
665: be.cancelBubble();
666: clearHoverStyles();
667: ListItem item = findItem(be.getTarget());
668: if (item != null) {
669: select(item);
670: }
671: super .onRightClick(be);
672: }
673:
674: private void clearHoverStyles() {
675: if (hoverItem != null) {
676: hoverItem.onMouseOut(null);
677: }
678: }
679:
680: private void onItemKeyPress(BaseEvent be, ListItem item) {
681: switch (be.getKeyCode()) {
682: case KeyboardListener.KEY_UP: {
683: int index = indexOf(lastSelected) - 1;
684: if (index < 0)
685: return;
686: item = getItem(index);
687: if (item != null) {
688: selectItems(index, index, true, false);
689: MyDOM.scrollIntoView(item.getElement(), getElement(),
690: false);
691: be.preventDefault();
692: }
693: break;
694: }
695: case KeyboardListener.KEY_DOWN: {
696: int index = indexOf(lastSelected) + 1;
697: if (index > getItemCount())
698: return;
699: item = getItem(index);
700: if (item != null) {
701: selectItems(index, index, true, false);
702: MyDOM.scrollIntoView(item.getElement(), getElement(),
703: false);
704: be.preventDefault();
705: }
706: break;
707: }
708: }
709: }
710:
711: private void register(ListItem item) {
712: nodes.put(item.getId(), item);
713: }
714:
715: private void renderAll() {
716: int ct = getWidgetCount();
717: for (int i = 0; i < ct; i++) {
718: // appendChild is faster than insertChild
719: DOM.appendChild(inner, getItem(i).getElement());
720: }
721: }
722:
723: private void renderItem(ListItem item, int index) {
724: DOM.insertChild(inner, item.getElement(), index);
725: }
726:
727: private void selectItems(int startIndex, int endIndex,
728: boolean state, boolean keepSelected) {
729: selectItems(startIndex, endIndex, state, keepSelected, false);
730: }
731:
732: private void selectItems(int startIndex, int endIndex,
733: boolean state, boolean keepSelected, boolean supressEvents) {
734: if (startIndex < 0 || endIndex > getItemCount()) {
735: return;
736: }
737:
738: setSelectionStyles(false);
739:
740: if (!keepSelected) {
741: selected.clear();
742: }
743:
744: lastSelected = getItem(endIndex);
745:
746: int begin = startIndex < endIndex ? startIndex : endIndex;
747: int end = startIndex < endIndex ? endIndex : startIndex;
748:
749: for (int i = begin; i <= end; i++) {
750: ListItem item = getItem(i);
751: if (state) {
752: lastSelected = item;
753: if (!selected.contains(item)) {
754: selected.add(item);
755: }
756: if (i == begin) {
757: scrollIntoView(item);
758: }
759: if (!supressEvents) {
760: fireEvent(Events.SelectionChange, this , item);
761: }
762:
763: } else {
764: selected.remove(item);
765: if (!supressEvents) {
766: fireEvent(Events.SelectionChange);
767: }
768:
769: }
770: }
771:
772: if (MyGWT.isSafari) {
773: focus();
774: }
775: setSelectionStyles(true);
776: }
777:
778: private void setSelectionStyles(boolean select) {
779: int count = selected.size();
780: for (int i = 0; i < count; i++) {
781: ListItem item = (ListItem) selected.get(i);
782: item.setSelected(select);
783: }
784: }
785:
786: private void unregister(ListItem item) {
787: nodes.remove(item.getId());
788: }
789:
790: }
|