Skip to main content
summaryrefslogtreecommitdiffstats
blob: 8b62ab6dd0ba6c5a873e3db6028827c65df42f30 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
/*******************************************************************************
 * Copyright (c) 2007, 2010 Oracle. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0, which accompanies this distribution
 * and is available at http://www.eclipse.org/legal/epl-v10.html.
 * 
 * Contributors:
 *     Oracle - initial API and implementation
 ******************************************************************************/
package org.eclipse.jpt.utility.internal.swing;

import java.awt.BorderLayout;
import java.awt.Font;
import javax.swing.AbstractListModel;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.eclipse.jpt.utility.internal.SimpleStringMatcher;
import org.eclipse.jpt.utility.internal.StringConverter;
import org.eclipse.jpt.utility.internal.StringMatcher;

/**
 * This panel presents an entry field and a list box of choices that
 * allows the user to filter the entries in the list box by entering
 * a pattern in the entry field.
 * 
 * By default, two wildcards are allowed in the pattern:
 * 	'*' will match any set of zero or more characters
 * 	'?' will match any single character
 * 
 * The panel consists of 4 components that can be customized:
 * 	- 1 text field
 * 	- 1 list box
 * 	- 2 labels, one for each of the above
 * 
 * Other aspects of the panel's behavior can be changed:
 * 	- the string converter determines how the objects in the
 * 		list are converted to strings and compared to the pattern
 * 		entered in the text field; by default the converter simply
 * 		uses the result of the object's #toString() method
 * 		(if you replace the string converter, you will probably
 * 		want to replace the list box's cell renderer also)
 * 	- the string matcher can also be changed if you would
 * 		like different pattern matching behavior than that
 * 		described above
 * 	- you can specify the maximum size of the list - this may
 * 		force the user to enter a pattern restrictive enough
 * 		to result in a list smaller than the maximum size; the
 * 		default is -1, which disables the restriction
 * 
 * This panel is not a typical panel, in the sense that it does not share
 * its model with clients via value models. Instead, this panel's model
 * is set and queried directly because it is designed to be used in a
 * dialog that directs the user's behavior (as opposed to a "normal"
 * window).
 */
public class FilteringListPanel<T> extends JPanel {

	/**
	 * The complete list of available choices
	 * (as opposed to the partial list held by the list box).
	 */
	private Object[] completeList;

	/**
	 * An adapter used to convert the objects in the list
	 * to strings so they can be run through the matcher
	 * and displayed in the text field.
	 */
	StringConverter<T> stringConverter;

	/** The text field. */
	private JTextField textField;
	private JLabel textFieldLabel;
	private DocumentListener textFieldListener;

	/** The list box. */
	private JList listBox;
	private JLabel listBoxLabel;

	/** The maximum number of entries displayed in the list box. */
	private int maxListSize;

	/**
	 * The matcher used to filter the list against
	 * the pattern entered in the text field. By default,
	 * this allows the two wildcard characters described in
	 * the class comment.
	 */
	private StringMatcher stringMatcher;

	/**
	 * Performance tweak: We use this buffer instead of
	 * a temporary variable during filtering so we don't have
	 * to keep re-allocating it.
	 */
	private Object[] buffer;

	private static final Border TEXT_FIELD_LABEL_BORDER = BorderFactory.createEmptyBorder(0, 0, 5, 0);
	private static final Border LIST_BOX_LABEL_BORDER = BorderFactory.createEmptyBorder(5, 0, 5, 0);


	// ********** constructors **********

	/**
	 * Construct a FilteringListPanel with the specified list of choices
	 * and initial selection. Use the default string converter to convert the
	 * choices and selection to strings (which simply calls #toString() on
	 * the objects).
	 */
	public FilteringListPanel(Object[] completeList, Object initialSelection) {
		this(completeList, initialSelection, StringConverter.Default.<T>instance());
	}

	/**
	 * Construct a FilteringListPanel with the specified list of choices
	 * and initial selection. Use the specified string converter to convert the
	 * choices and selection to strings.
	 */
	public FilteringListPanel(Object[] completeList, Object initialSelection, StringConverter<T> stringConverter) {
		super(new BorderLayout());
		this.completeList = completeList;
		this.stringConverter = stringConverter;
		this.initialize(initialSelection);
	}


	// ********** initialization **********

	private void initialize(Object initialSelection) {
		this.maxListSize = this.defaultMaxListSize();
		this.buffer = this.buildBuffer();

		this.textFieldListener = this.buildTextFieldListener();

		this.stringMatcher = this.buildStringMatcher();

		this.initializeLayout(initialSelection);
	}

	private Object[] buildBuffer() {
		return new Object[this.max()];
	}

	/**
	 * Return the current max number of entries allowed in the list box.
	 */
	private int max() {
		if (this.maxListSize == -1) {
			return this.completeList.length;
		}
		return Math.min(this.maxListSize, this.completeList.length);
	}

	/**
	 * Build a listener that will listen to changes in the text field
	 * and filter the list appropriately.
	 */
	private DocumentListener buildTextFieldListener() {
		return new DocumentListener() {
			public void insertUpdate(DocumentEvent e) {
				FilteringListPanel.this.filterList();
			}
			public void changedUpdate(DocumentEvent e) {
				FilteringListPanel.this.filterList();
			}
			public void removeUpdate(DocumentEvent e) {
				FilteringListPanel.this.filterList();
			}
			@Override
			public String toString() {
				return "text field listener"; //$NON-NLS-1$
			}
		};
	}

	private int defaultMaxListSize() {
		return -1;
	}

	private StringMatcher buildStringMatcher() {
		return new SimpleStringMatcher<T>();
	}

	private void initializeLayout(Object initialSelection) {
		// text field
		JPanel textFieldPanel = new JPanel(new BorderLayout());
		this.textFieldLabel = new JLabel();
		this.textFieldLabel.setBorder(TEXT_FIELD_LABEL_BORDER);
		textFieldPanel.add(this.textFieldLabel, BorderLayout.NORTH);

		this.textField = new JTextField();
		this.textField.getDocument().addDocumentListener(this.textFieldListener);
		this.textFieldLabel.setLabelFor(this.textField);
		textFieldPanel.add(this.textField, BorderLayout.CENTER);

		this.add(textFieldPanel, BorderLayout.NORTH);

		// list box
		JPanel listBoxPanel = new JPanel(new BorderLayout());
		this.listBoxLabel = new JLabel();
		this.listBoxLabel.setBorder(LIST_BOX_LABEL_BORDER);
		listBoxPanel.add(this.listBoxLabel, BorderLayout.NORTH);

		this.listBox = new JList();
		this.listBox.setDoubleBuffered(true);
		this.listBox.setModel(this.buildPartialArrayListModel(this.completeList, this.max()));
		this.listBox.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		// performance tweak(?)
		this.listBox.setPrototypeCellValue(this.prototypeCellValue());
		this.listBox.setPrototypeCellValue(null);
		this.listBox.setCellRenderer(this.buildDefaultCellRenderer());
		this.listBoxLabel.setLabelFor(this.listBox);
		// bug 2777802 - scroll bars shouldn't be on the tab sequence
		JScrollPane listBoxScrollPane = new JScrollPane(this.listBox);
		listBoxScrollPane.getHorizontalScrollBar().setFocusable(false);
		listBoxScrollPane.getVerticalScrollBar().setFocusable(false);
		listBoxPanel.add(listBoxScrollPane, BorderLayout.CENTER);

		// initialize the widgets
		this.listBox.setSelectedValue(initialSelection, true);
		this.textField.select(0, this.textField.getText().length());

		this.add(listBoxPanel, BorderLayout.CENTER);
	}


	// ********** public API **********

	public Object selection() {
		return this.listBox.getSelectedValue();
	}

	public void setSelection(Object selection) {
		this.listBox.setSelectedValue(selection, true);
	}

	public Object[] completeList() {
		return this.completeList;
	}

	/**
	 * rebuild the filtering buffer and re-apply the filter
	 * to the new list
	 */
	public void setCompleteList(Object[] completeList) {
		this.completeList = completeList;
		if (this.buffer.length < this.max()) {
			// the buffer will never shrink - might want to re-consider...  ~bjv
			this.buffer = this.buildBuffer();
		}
		this.filterList();
	}

	public int maxListSize() {
		return this.maxListSize;
	}

	public void setMaxListSize(int maxListSize) {
		this.maxListSize = maxListSize;
		if (this.buffer.length < this.max()) {
			// the buffer will never shrink - might want to re-consider...  ~bjv
			this.buffer = this.buildBuffer();
		}
		this.filterList();
	}

	public StringConverter<T> stringConverter() {
		return this.stringConverter;
	}

	/**
	 * apply the new filter to the list
	 */
	public void setStringConverter(StringConverter<T> stringConverter) {
		this.stringConverter = stringConverter;
		this.filterList();
	}

	/**
	 * allow client code to access the text field
	 * (so we can set the focus)
	 */
	public JTextField textField() {
		return this.textField;
	}

	/**
	 * allow client code to access the text field label
	 */
	public JLabel textFieldLabel() {
		return this.textFieldLabel;
	}

	/**
	 * convenience method
	 */
	public void setTextFieldLabelText(String text) {
		this.textFieldLabel.setText(text);
	}

	/**
	 * allow client code to access the list box
	 * (so we can add mouse listeners for double-clicking)
	 */
	public JList listBox() {
		return this.listBox;
	}

	/**
	 * convenience method
	 */
	public void setListBoxCellRenderer(ListCellRenderer renderer) {
		this.listBox.setCellRenderer(renderer);
	}

	/**
	 * allow client code to access the list box label
	 */
	public JLabel listBoxLabel() {
		return this.listBoxLabel;
	}

	/**
	 * convenience method
	 */
	public void setListBoxLabelText(String text) {
		this.listBoxLabel.setText(text);
	}

	/**
	 * convenience method
	 */
	public void setComponentsFont(Font font) {
		this.textFieldLabel.setFont(font);
		this.textField.setFont(font);
		this.listBoxLabel.setFont(font);
		this.listBox.setFont(font);
	}

	public StringMatcher stringMatcher() {
		return this.stringMatcher;
	}

	/**
	 * re-apply the filter to the list
	 */
	public void setStringMatcher(StringMatcher stringMatcher) {
		this.stringMatcher = stringMatcher;
		this.filterList();
	}


	// ********** internal methods **********

	/**
	 * Allow subclasses to disable performance tweak
	 * by returning null here.
	 */
	protected String prototypeCellValue() {
		return "==========> A_STRING_THAT_IS_DEFINITELY_LONGER_THAN_EVERY_STRING_IN_THE_LIST <=========="; //$NON-NLS-1$
	}

	/**
	 * By default, use the string converter to build the text
	 * used by the list box's cell renderer.
	 */
	protected ListCellRenderer buildDefaultCellRenderer() {
		return new SimpleListCellRenderer() {
			@Override
			@SuppressWarnings("unchecked")
			protected String buildText(Object value) {
				return FilteringListPanel.this.stringConverter.convertToString((T) value);
			}
		};
	}

	/**
	 * Something has changed that requires us to filter the list.
	 * 
	 * This method is synchronized because a fast typist can
	 * generate events quicker than we can filter the list. (?  ~bjv)
	 */
	synchronized void filterList() {
		// temporarily stop listening to the list box selection, since we will
		// be changing the selection during the filtering and don't want
		// that to affect the text field
		this.filterList(this.textField.getText());
	}

	/**
	 * Filter the contents of the list box to match the
	 * specified pattern.
	 */
	private void filterList(String pattern) {
		if (pattern.length() == 0) {
			this.listBox.setModel(this.buildPartialArrayListModel(this.completeList, this.max()));
		} else {
			this.stringMatcher.setPatternString(pattern);
			int j = 0;
			int len = this.completeList.length;
			int max = this.max();
			for (int i = 0; i < len; i++) {
				if (this.stringMatcher.matches(this.stringConverter.convertToString(this.entry(i)))) {
					this.buffer[j++] = this.completeList[i];
				}
				if (j == max) {
					break;
				}
			}
			this.listBox.setModel(this.buildPartialArrayListModel(this.buffer, j));
		}

		// after filtering the list, determine the appropriate selection
		if (this.listBox.getModel().getSize() == 0) {
			this.listBox.getSelectionModel().clearSelection();
		} else {
			this.listBox.getSelectionModel().setAnchorSelectionIndex(0);
			this.listBox.getSelectionModel().setLeadSelectionIndex(0);
			this.listBox.ensureIndexIsVisible(0);
		}
	}

	/**
	 * minimize scope of suppressed warnings
	 */
	@SuppressWarnings("unchecked")
	private T entry(int index) {
		return (T) this.completeList[index];
	}

	/**
	 * Build a list model that wraps only a portion of the specified array.
	 * The model will include the array entries from 0 to (size - 1).
	 */
	private ListModel buildPartialArrayListModel(final Object[] array, final int size) {
		return new AbstractListModel() {
			public int getSize() {
				return size;
			}
			public Object getElementAt(int index) {
				return array[index];
			}
		};
	}
}

Back to the top