001: /*******************************************************************************
002: * Copyright (c) 2000, 2006 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: * Sebastian Davids <sdavids@gmx.de> - Fix for bug 19346 - Dialog font should be activated and used by other components.
011: *******************************************************************************/package org.eclipse.ui.dialogs;
012:
013: import java.util.Comparator;
014: import java.util.HashSet;
015: import java.util.Set;
016: import java.util.Vector;
017:
018: import org.eclipse.core.runtime.Assert;
019: import org.eclipse.core.runtime.IProgressMonitor;
020: import org.eclipse.core.runtime.IStatus;
021: import org.eclipse.core.runtime.Status;
022: import org.eclipse.jface.viewers.ILabelProvider;
023: import org.eclipse.swt.SWT;
024: import org.eclipse.swt.SWTException;
025: import org.eclipse.swt.accessibility.Accessible;
026: import org.eclipse.swt.events.DisposeEvent;
027: import org.eclipse.swt.events.DisposeListener;
028: import org.eclipse.swt.events.SelectionListener;
029: import org.eclipse.swt.graphics.Image;
030: import org.eclipse.swt.layout.GridData;
031: import org.eclipse.swt.layout.GridLayout;
032: import org.eclipse.swt.widgets.Composite;
033: import org.eclipse.swt.widgets.Event;
034: import org.eclipse.swt.widgets.Table;
035: import org.eclipse.swt.widgets.TableItem;
036: import org.eclipse.ui.internal.WorkbenchMessages;
037: import org.eclipse.ui.internal.misc.StringMatcher;
038: import org.eclipse.ui.internal.util.Util;
039: import org.eclipse.ui.progress.WorkbenchJob;
040:
041: /**
042: * A composite widget which holds a list of elements for user selection. The
043: * elements are sorted alphabetically. Optionally, the elements can be filtered
044: * and duplicate entries can be hidden (folding).
045: *
046: * @since 2.0
047: */
048: public class FilteredList extends Composite {
049: /**
050: * The FilterMatcher is the interface used to check filtering criterea.
051: */
052: public interface FilterMatcher {
053: /**
054: * Sets the filter.
055: *
056: * @param pattern
057: * the filter pattern.
058: * @param ignoreCase
059: * a flag indicating whether pattern matching is case
060: * insensitive or not.
061: * @param ignoreWildCards
062: * a flag indicating whether wildcard characters are
063: * interpreted or not.
064: */
065: void setFilter(String pattern, boolean ignoreCase,
066: boolean ignoreWildCards);
067:
068: /**
069: * @param element
070: * The element to test against.
071: * @return <code>true</code> if the object matches the pattern,
072: * <code>false</code> otherwise. <code>setFilter()</code>
073: * must have been called at least once prior to a call to this
074: * method.
075: */
076: boolean match(Object element);
077: }
078:
079: private class DefaultFilterMatcher implements FilterMatcher {
080: private StringMatcher fMatcher;
081:
082: public void setFilter(String pattern, boolean ignoreCase,
083: boolean ignoreWildCards) {
084: fMatcher = new StringMatcher(pattern + '*', ignoreCase,
085: ignoreWildCards);
086: }
087:
088: public boolean match(Object element) {
089: return fMatcher.match(fLabelProvider.getText(element));
090: }
091: }
092:
093: private Table fList;
094:
095: ILabelProvider fLabelProvider;
096:
097: private boolean fMatchEmptyString = true;
098:
099: private boolean fIgnoreCase;
100:
101: private boolean fAllowDuplicates;
102:
103: private String fFilter = ""; //$NON-NLS-1$
104:
105: private TwoArrayQuickSorter fSorter;
106:
107: Object[] fElements = new Object[0];
108:
109: Label[] fLabels;
110:
111: Vector fImages = new Vector();
112:
113: int[] fFoldedIndices;
114:
115: int fFoldedCount;
116:
117: int[] fFilteredIndices;
118:
119: int fFilteredCount;
120:
121: private FilterMatcher fFilterMatcher = new DefaultFilterMatcher();
122:
123: Comparator fComparator;
124:
125: TableUpdateJob fUpdateJob;
126:
127: /**
128: * Label is a private class used for comparing list objects
129: */
130: private static class Label {
131: /**
132: * The string portion of the label.
133: */
134: public final String string;
135:
136: /**
137: * The image portion of the label.
138: */
139: public final Image image;
140:
141: /**
142: * Create a new instance of label.
143: *
144: * @param newString
145: * @param image
146: */
147: public Label(String newString, Image image) {
148: if (newString == null) {
149: this .string = Util.ZERO_LENGTH_STRING;
150: } else {
151: this .string = newString;
152: }
153: this .image = image;
154: }
155:
156: /**
157: * Return whether or not the receiver is the same as label.
158: *
159: * @param label
160: * @return boolean
161: */
162: public boolean equals(Label label) {
163: if (label == null) {
164: return false;
165: }
166: // If the string portions match (whether null or not), fall
167: // through and check the image portion.
168: if (string == null && label.string != null) {
169: return false;
170: }
171: if ((string != null) && (!string.equals(label.string))) {
172: return false;
173: }
174: if (image == null) {
175: return label.image == null;
176: }
177: return image.equals(label.image);
178: }
179: }
180:
181: private final class LabelComparator implements Comparator {
182: private boolean labelIgnoreCase;
183:
184: LabelComparator(boolean ignoreCase) {
185: labelIgnoreCase = ignoreCase;
186: }
187:
188: public int compare(Object left, Object right) {
189: Label leftLabel = (Label) left;
190: Label rightLabel = (Label) right;
191: int value;
192: if (fComparator == null) {
193: value = labelIgnoreCase ? leftLabel.string
194: .compareToIgnoreCase(rightLabel.string)
195: : leftLabel.string.compareTo(rightLabel.string);
196: } else {
197: value = fComparator.compare(leftLabel.string,
198: rightLabel.string);
199: }
200: if (value != 0) {
201: return value;
202: }
203: // images are allowed to be null
204: if (leftLabel.image == null) {
205: return (rightLabel.image == null) ? 0 : -1;
206: } else if (rightLabel.image == null) {
207: return +1;
208: } else {
209: return fImages.indexOf(leftLabel.image)
210: - fImages.indexOf(rightLabel.image);
211: }
212: }
213: }
214:
215: /**
216: * Constructs a new filtered list.
217: *
218: * @param parent
219: * the parent composite
220: * @param style
221: * the widget style
222: * @param labelProvider
223: * the label renderer
224: * @param ignoreCase
225: * specifies whether sorting and folding is case sensitive
226: * @param allowDuplicates
227: * specifies whether folding of duplicates is desired
228: * @param matchEmptyString
229: * specifies whether empty filter strings should filter
230: * everything or nothing
231: */
232: public FilteredList(Composite parent, int style,
233: ILabelProvider labelProvider, boolean ignoreCase,
234: boolean allowDuplicates, boolean matchEmptyString) {
235: super (parent, SWT.NONE);
236: GridLayout layout = new GridLayout();
237: layout.marginHeight = 0;
238: layout.marginWidth = 0;
239: setLayout(layout);
240: fList = new Table(this , style);
241: fList.setLayoutData(new GridData(GridData.FILL_BOTH));
242: fList.setFont(parent.getFont());
243: fList.addDisposeListener(new DisposeListener() {
244: public void widgetDisposed(DisposeEvent e) {
245: fLabelProvider.dispose();
246: if (fUpdateJob != null) {
247: fUpdateJob.cancel();
248: }
249: }
250: });
251: fLabelProvider = labelProvider;
252: fIgnoreCase = ignoreCase;
253: fSorter = new TwoArrayQuickSorter(new LabelComparator(
254: ignoreCase));
255: fAllowDuplicates = allowDuplicates;
256: fMatchEmptyString = matchEmptyString;
257: }
258:
259: /**
260: * Sets the list of elements.
261: *
262: * @param elements
263: * the elements to be shown in the list.
264: */
265: public void setElements(Object[] elements) {
266: if (elements == null) {
267: fElements = new Object[0];
268: } else {
269: // copy list for sorting
270: fElements = new Object[elements.length];
271: System
272: .arraycopy(elements, 0, fElements, 0,
273: elements.length);
274: }
275: int length = fElements.length;
276: // fill labels
277: fLabels = new Label[length];
278: Set imageSet = new HashSet();
279: for (int i = 0; i != length; i++) {
280: String text = fLabelProvider.getText(fElements[i]);
281: Image image = fLabelProvider.getImage(fElements[i]);
282: fLabels[i] = new Label(text, image);
283: imageSet.add(image);
284: }
285: fImages.clear();
286: fImages.addAll(imageSet);
287: fSorter.sort(fLabels, fElements);
288: fFilteredIndices = new int[length];
289: fFoldedIndices = new int[length];
290: updateList();
291: }
292:
293: /**
294: * Tests if the list (before folding and filtering) is empty.
295: *
296: * @return returns <code>true</code> if the list is empty,
297: * <code>false</code> otherwise.
298: */
299: public boolean isEmpty() {
300: return (fElements == null) || (fElements.length == 0);
301: }
302:
303: /**
304: * Sets the filter matcher.
305: *
306: * @param filterMatcher
307: */
308: public void setFilterMatcher(FilterMatcher filterMatcher) {
309: Assert.isNotNull(filterMatcher);
310: fFilterMatcher = filterMatcher;
311: }
312:
313: /**
314: * Sets a custom comparator for sorting the list.
315: *
316: * @param comparator
317: */
318: public void setComparator(Comparator comparator) {
319: Assert.isNotNull(comparator);
320: fComparator = comparator;
321: }
322:
323: /**
324: * Adds a selection listener to the list.
325: *
326: * @param listener
327: * the selection listener to be added.
328: */
329: public void addSelectionListener(SelectionListener listener) {
330: fList.addSelectionListener(listener);
331: }
332:
333: /**
334: * Removes a selection listener from the list.
335: *
336: * @param listener
337: * the selection listener to be removed.
338: */
339: public void removeSelectionListener(SelectionListener listener) {
340: fList.removeSelectionListener(listener);
341: }
342:
343: /**
344: * Sets the selection of the list. Empty or null array removes selection.
345: *
346: * @param selection
347: * an array of indices specifying the selection.
348: */
349: public void setSelection(int[] selection) {
350: if (selection == null || selection.length == 0) {
351: fList.deselectAll();
352: } else {
353: // If there is no working update job, or the update job is ready to
354: // accept selections, set the selection immediately.
355: if (fUpdateJob == null) {
356: fList.setSelection(selection);
357: fList.notifyListeners(SWT.Selection, new Event());
358: } else {
359: // There is an update job doing the population of the list, so
360: // it should update the selection.
361: fUpdateJob.updateSelection(selection);
362: }
363: }
364: }
365:
366: /**
367: * Returns the selection of the list.
368: *
369: * @return returns an array of indices specifying the current selection.
370: */
371: public int[] getSelectionIndices() {
372: return fList.getSelectionIndices();
373: }
374:
375: /**
376: * Returns the selection of the list. This is a convenience function for
377: * <code>getSelectionIndices()</code>.
378: *
379: * @return returns the index of the selection, -1 for no selection.
380: */
381: public int getSelectionIndex() {
382: return fList.getSelectionIndex();
383: }
384:
385: /**
386: * Sets the selection of the list. Empty or null array removes selection.
387: *
388: * @param elements
389: * the array of elements to be selected.
390: */
391: public void setSelection(Object[] elements) {
392: if (elements == null || elements.length == 0) {
393: fList.deselectAll();
394: return;
395: }
396: if (fElements == null) {
397: return;
398: }
399: // fill indices
400: int[] indices = new int[elements.length];
401: for (int i = 0; i != elements.length; i++) {
402: int j;
403: for (j = 0; j != fFoldedCount; j++) {
404: int max = (j == fFoldedCount - 1) ? fFilteredCount
405: : fFoldedIndices[j + 1];
406: int l;
407: for (l = fFoldedIndices[j]; l != max; l++) {
408: // found matching element?
409: if (fElements[fFilteredIndices[l]]
410: .equals(elements[i])) {
411: indices[i] = j;
412: break;
413: }
414: }
415: if (l != max) {
416: break;
417: }
418: }
419: // not found
420: if (j == fFoldedCount) {
421: indices[i] = 0;
422: }
423: }
424: setSelection(indices);
425: }
426:
427: /**
428: * Returns an array of the selected elements. The type of the elements
429: * returned in the list are the same as the ones passed with
430: * <code>setElements</code>. The array does not contain the rendered
431: * strings.
432: *
433: * @return returns the array of selected elements.
434: */
435: public Object[] getSelection() {
436: if (fList.isDisposed() || (fList.getSelectionCount() == 0)) {
437: return new Object[0];
438: }
439: int[] indices = fList.getSelectionIndices();
440: Object[] elements = new Object[indices.length];
441: for (int i = 0; i != indices.length; i++) {
442: elements[i] = fElements[fFilteredIndices[fFoldedIndices[indices[i]]]];
443: }
444: return elements;
445: }
446:
447: /**
448: * Sets the filter pattern. Current only prefix filter patterns are
449: * supported.
450: *
451: * @param filter
452: * the filter pattern.
453: */
454: public void setFilter(String filter) {
455: fFilter = (filter == null) ? "" : filter; //$NON-NLS-1$
456: updateList();
457: }
458:
459: private void updateList() {
460: fFilteredCount = filter();
461: fFoldedCount = fold();
462: if (fUpdateJob != null) {
463: fUpdateJob.cancel();
464: }
465: fUpdateJob = new TableUpdateJob(fList, fFoldedCount);
466: fUpdateJob.schedule();
467: }
468:
469: /**
470: * Returns the filter pattern.
471: *
472: * @return returns the filter pattern.
473: */
474: public String getFilter() {
475: return fFilter;
476: }
477:
478: /**
479: * Returns all elements which are folded together to one entry in the list.
480: *
481: * @param index
482: * the index selecting the entry in the list.
483: * @return returns an array of elements folded together, <code>null</code>
484: * if index is out of range.
485: */
486: public Object[] getFoldedElements(int index) {
487: if ((index < 0) || (index >= fFoldedCount)) {
488: return null;
489: }
490: int start = fFoldedIndices[index];
491: int count = (index == fFoldedCount - 1) ? fFilteredCount
492: - start : fFoldedIndices[index + 1] - start;
493: Object[] elements = new Object[count];
494: for (int i = 0; i != count; i++) {
495: elements[i] = fElements[fFilteredIndices[start + i]];
496: }
497: return elements;
498: }
499:
500: /*
501: * Folds duplicate entries. Two elements are considered as a pair of
502: * duplicates if they coiincide in the rendered string and image. @return
503: * returns the number of elements after folding.
504: */
505: private int fold() {
506: if (fAllowDuplicates) {
507: for (int i = 0; i != fFilteredCount; i++) {
508: fFoldedIndices[i] = i; // identity mapping
509: }
510: return fFilteredCount;
511: }
512: int k = 0;
513: Label last = null;
514: for (int i = 0; i != fFilteredCount; i++) {
515: int j = fFilteredIndices[i];
516: Label current = fLabels[j];
517: if (!current.equals(last)) {
518: fFoldedIndices[k] = i;
519: k++;
520: last = current;
521: }
522: }
523: return k;
524: }
525:
526: /*
527: * Filters the list with the filter pattern. @return returns the number of
528: * elements after filtering.
529: */
530: private int filter() {
531: if (((fFilter == null) || (fFilter.length() == 0))
532: && !fMatchEmptyString) {
533: return 0;
534: }
535: fFilterMatcher.setFilter(fFilter.trim(), fIgnoreCase, false);
536: int k = 0;
537: for (int i = 0; i != fElements.length; i++) {
538: if (fFilterMatcher.match(fElements[i])) {
539: fFilteredIndices[k++] = i;
540: }
541: }
542: return k;
543: }
544:
545: private class TableUpdateJob extends WorkbenchJob {
546: final Table fTable;
547:
548: final int fCount;
549:
550: private int currentIndex = 0;
551:
552: /*
553: * Programmatic selections requested while this job was running.
554: */
555: int[] indicesToSelect;
556:
557: private boolean readyForSelection = false;
558:
559: /**
560: * Create a new instance of a job used to update the table.
561: *
562: * @param table
563: * @param count
564: * The number of items to update per running.
565: */
566: public TableUpdateJob(Table table, int count) {
567: super (WorkbenchMessages.FilteredList_UpdateJobName);
568: setSystem(true);
569: fTable = table;
570: fCount = count;
571: }
572:
573: /*
574: * (non-Javadoc)
575: *
576: * @see org.eclipse.ui.progress.UIJob#runInUIThread(org.eclipse.core.runtime.IProgressMonitor)
577: */
578: public IStatus runInUIThread(IProgressMonitor monitor) {
579: if (fTable.isDisposed()) {
580: return Status.CANCEL_STATUS;
581: }
582: int itemCount = fTable.getItemCount();
583:
584: // Remove excess items
585: if (fCount < itemCount) {
586: fTable.setRedraw(false);
587: fTable.remove(fCount, itemCount - 1);
588: fTable.setRedraw(true);
589: itemCount = fTable.getItemCount();
590: }
591: // table empty -> no selection
592: if (fCount == 0) {
593: fTable.notifyListeners(SWT.Selection, new Event());
594: return Status.OK_STATUS;
595: }
596: // How many we are going to do this time.
597: int iterations = Math.min(10, fCount - currentIndex);
598: for (int i = 0; i < iterations; i++) {
599: if (monitor.isCanceled()) {
600: return Status.CANCEL_STATUS;
601: }
602: final TableItem item = (currentIndex < itemCount) ? fTable
603: .getItem(currentIndex)
604: : new TableItem(fTable, SWT.NONE);
605: final Label label = fLabels[fFilteredIndices[fFoldedIndices[currentIndex]]];
606: item.setText(label.string);
607: item.setImage(label.image);
608: currentIndex++;
609: }
610: if (monitor.isCanceled()) {
611: return Status.CANCEL_STATUS;
612: }
613: if (currentIndex < fCount) {
614: schedule(100);
615: } else {
616: if (indicesToSelect == null) {
617: // Make a default selection in the table if there is none.
618: // If a selection has already been made, honor it.
619: // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=112146
620: if (fCount > 0) {
621: if (fTable.getSelectionIndices().length == 0) {
622: defaultSelect();
623: } else {
624: // There is a selection, but it likely hasn't changed since the
625: // job started. Force a selection notification, since the
626: // items represented by the selection have changed.
627: // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=119456
628: fTable.notifyListeners(SWT.Selection,
629: new Event());
630: }
631: }
632: } else {
633: // Set the selection as indicated.
634: selectAndNotify(indicesToSelect);
635: }
636: // This flag signifies that the selection can now be directly
637: // updated in the widget.
638: readyForSelection = true;
639: }
640: return Status.OK_STATUS;
641: }
642:
643: /**
644: * Update the selection for the supplied indices.
645: *
646: * @param indices
647: */
648: void updateSelection(final int[] indices) {
649: indicesToSelect = indices;
650: if (readyForSelection) {
651: selectAndNotify(indices);
652: }
653: }
654:
655: /**
656: * Select the first element if there is no selection
657: */
658: private void defaultSelect() {
659: /**
660: * Reset to the first selection if no index has been queued.
661: */
662: selectAndNotify(new int[] { 0 });
663: }
664:
665: /**
666: * Select the supplied indices and notify any listeners
667: *
668: * @param indices
669: */
670: private void selectAndNotify(final int[] indices) {
671: // It is possible that the table was disposed
672: // before the update finished. If so then leave
673: if (fTable.isDisposed()) {
674: return;
675: }
676: fTable.setSelection(indices);
677: fTable.notifyListeners(SWT.Selection, new Event());
678: }
679: }
680:
681: /**
682: * Returns whether or not duplicates are allowed.
683: *
684: * @return <code>true</code> indicates duplicates are allowed
685: */
686: public boolean getAllowDuplicates() {
687: return fAllowDuplicates;
688: }
689:
690: /**
691: * Sets whether or not duplicates are allowed. If this value is set the
692: * items should be set again for this value to take effect.
693: *
694: * @param allowDuplicates
695: * <code>true</code> indicates duplicates are allowed
696: */
697: public void setAllowDuplicates(boolean allowDuplicates) {
698: this .fAllowDuplicates = allowDuplicates;
699: }
700:
701: /**
702: * Returns whether or not case should be ignored.
703: *
704: * @return <code>true</code> if case should be ignored
705: */
706: public boolean getIgnoreCase() {
707: return fIgnoreCase;
708: }
709:
710: /**
711: * Sets whether or not case should be ignored If this value is set the items
712: * should be set again for this value to take effect.
713: *
714: * @param ignoreCase
715: * <code>true</code> if case should be ignored
716: */
717: public void setIgnoreCase(boolean ignoreCase) {
718: this .fIgnoreCase = ignoreCase;
719: }
720:
721: /**
722: * Returns whether empty filter strings should filter everything or nothing.
723: *
724: * @return <code>true</code> for the empty string to match all items,
725: * <code>false</code> to match none
726: */
727: public boolean getMatchEmptyString() {
728: return fMatchEmptyString;
729: }
730:
731: /**
732: * Sets whether empty filter strings should filter everything or nothing. If
733: * this value is set the items should be set again for this value to take
734: * effect.
735: *
736: * @param matchEmptyString
737: * <code>true</code> for the empty string to match all items,
738: * <code>false</code> to match none
739: */
740: public void setMatchEmptyString(boolean matchEmptyString) {
741: this .fMatchEmptyString = matchEmptyString;
742: }
743:
744: /**
745: * Returns the label provider for the items.
746: *
747: * @return the label provider
748: */
749: public ILabelProvider getLabelProvider() {
750: return fLabelProvider;
751: }
752:
753: /**
754: * Sets the label provider. If this value is set the items should be set
755: * again for this value to take effect.
756: *
757: * @param labelProvider
758: * the label provider
759: */
760: public void setLabelProvider(ILabelProvider labelProvider) {
761: this .fLabelProvider = labelProvider;
762: }
763:
764: /**
765: * Returns the accessible object for the receiver.
766: * If this is the first time this object is requested,
767: * then the object is created and returned.
768: *
769: * @return the accessible object
770: *
771: * @exception SWTException <ul>
772: * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
773: * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
774: * </ul>
775: *
776: * @see Accessible#addAccessibleListener
777: * @see Accessible#addAccessibleControlListener
778: *
779: * @since 3.3
780: */
781: public Accessible getAccessible() {
782: return fList.getAccessible();
783: }
784: }
|