3029f61a04c43411da2400f5cf6fb75374dc1a81
[fanfix.git] / ui / ListModel.java
1 package be.nikiroo.utils.ui;
2
3 import java.awt.Component;
4 import java.awt.Point;
5 import java.awt.Window;
6 import java.awt.event.MouseAdapter;
7 import java.awt.event.MouseEvent;
8 import java.util.ArrayList;
9 import java.util.Collection;
10 import java.util.List;
11
12 import javax.swing.JList;
13 import javax.swing.JPopupMenu;
14 import javax.swing.ListCellRenderer;
15 import javax.swing.SwingWorker;
16
17 import be.nikiroo.utils.compat.DefaultListModel6;
18 import be.nikiroo.utils.compat.JList6;
19 import be.nikiroo.utils.compat.ListCellRenderer6;
20
21 /**
22 * A {@link javax.swing.ListModel} that can maintain 2 lists; one with the
23 * actual data (the elements), and a second one with the items that are
24 * currently displayed (the items).
25 * <p>
26 * It also offers filter options, supports hovered changes and some more utility
27 * functions.
28 *
29 * @author niki
30 *
31 * @param <T>
32 * the type of elements and items (the same type)
33 */
34 public class ListModel<T> extends DefaultListModel6<T> {
35 private static final long serialVersionUID = 1L;
36
37 /** How long to wait before displaying a tooltip, in milliseconds. */
38 private static final int DELAY_TOOLTIP_MS = 1000;
39
40 /**
41 * A filter interface, to check for a condition (note that a Predicate class
42 * already exists in Java 1.8+, and is compatible with this one if you
43 * change the signatures -- but I support java 1.6+).
44 *
45 * @author niki
46 *
47 * @param <T>
48 * the type of elements and items (the same type)
49 */
50 public interface Predicate<T> {
51 /**
52 * Check if an item or an element pass a filter.
53 *
54 * @param item
55 * the item to test
56 *
57 * @return TRUE if the test passed, FALSE if not
58 */
59 public boolean test(T item);
60 }
61
62 /**
63 * A simple interface your elements must implement if you want to use
64 * {@link ListModel#generateRenderer(ListModel)}.
65 *
66 * @author niki
67 */
68 public interface Hoverable {
69 /**
70 * The element is currently selected.
71 *
72 * @param selected
73 * TRUE for selected, FALSE for unselected
74 */
75 public void setSelected(boolean selected);
76
77 /**
78 * The element is currently under the mouse cursor.
79 *
80 * @param hovered
81 * TRUE if it is, FALSE if not
82 */
83 public void setHovered(boolean hovered);
84 }
85
86 /**
87 * An interface required to support tooltips on this {@link ListModel}.
88 *
89 * @author niki
90 *
91 * @param <T>
92 * the type of elements and items (the same type)
93 */
94 public interface TooltipCreator<T> {
95 /**
96 * Generate a tooltip {@link Window} for this element.
97 * <p>
98 * Note that the tooltip can be of two modes: undecorated or standalone.
99 * An undecorated tooltip will be taken care of by this
100 * {@link ListModel}, but a standalone one is supposed to be its own
101 * Dialog or Frame (it won't be automatically closed).
102 *
103 * @param t
104 * the element to generate a tooltip for
105 * @param undecorated
106 * TRUE for undecorated tooltip, FALSE for standalone
107 * tooltips
108 *
109 * @return the generated tooltip or NULL for none
110 */
111 public Window generateTooltip(T t, boolean undecorated);
112 }
113
114 private int hoveredIndex;
115 private List<T> items = new ArrayList<T>();
116 private boolean keepSelection = true;
117
118 private TooltipCreator<T> tooltipCreator;
119 private Window tooltip;
120
121 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
122 private JList list;
123
124 /**
125 * Create a new {@link ListModel}.
126 *
127 * @param list
128 * the {@link JList6} we will handle the data of (cannot be NULL)
129 */
130 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
131 public ListModel(JList6<T> list) {
132 this((JList) list);
133 }
134
135 /**
136 * Create a new {@link ListModel}.
137 *
138 * @param list
139 * the {@link JList6} we will handle the data of (cannot be NULL)
140 * @param popup
141 * the popup to use and keep track of (can be NULL)
142 */
143 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
144 public ListModel(JList6<T> list, JPopupMenu popup) {
145 this((JList) list, popup);
146 }
147
148 /**
149 * Create a new {@link ListModel}.
150 *
151 * @param list
152 * the {@link JList6} we will handle the data of (cannot be NULL)
153 * @param tooltipCreator
154 * use this if you want the list to display tooltips on hover
155 * (can be NULL)
156 */
157 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
158 public ListModel(JList6<T> list, TooltipCreator<T> tooltipCreator) {
159 this((JList) list, null, tooltipCreator);
160 }
161
162 /**
163 * Create a new {@link ListModel}.
164 *
165 * @param list
166 * the {@link JList6} we will handle the data of (cannot be NULL)
167 * @param popup
168 * the popup to use and keep track of (can be NULL)
169 * @param tooltipCreator
170 * use this if you want the list to display tooltips on hover
171 * (can be NULL)
172 */
173 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
174 public ListModel(JList6<T> list, JPopupMenu popup,
175 TooltipCreator<T> tooltipCreator) {
176 this((JList) list, popup, tooltipCreator);
177 }
178
179 /**
180 * Create a new {@link ListModel}.
181 * <p>
182 * Note that you must take care of passing a {@link JList} that only handles
183 * elements of the type of this {@link ListModel} -- you can also use
184 * {@link ListModel#ListModel(JList6)} instead.
185 *
186 * @param list
187 * the {@link JList} we will handle the data of (cannot be NULL,
188 * must only contain elements of the type of this
189 * {@link ListModel})
190 */
191 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
192 public ListModel(JList list) {
193 this(list, null, null);
194 }
195
196 /**
197 * Create a new {@link ListModel}.
198 * <p>
199 * Note that you must take care of passing a {@link JList} that only handles
200 * elements of the type of this {@link ListModel} -- you can also use
201 * {@link ListModel#ListModel(JList6, JPopupMenu)} instead.
202 *
203 * @param list
204 * the {@link JList} we will handle the data of (cannot be NULL,
205 * must only contain elements of the type of this
206 * {@link ListModel})
207 * @param popup
208 * the popup to use and keep track of (can be NULL)
209 */
210 @SuppressWarnings("rawtypes") // JList<?> not in Java 1.6
211 public ListModel(JList list, JPopupMenu popup) {
212 this(list, popup, null);
213 }
214
215 /**
216 * Create a new {@link ListModel}.
217 * <p>
218 * Note that you must take care of passing a {@link JList} that only handles
219 * elements of the type of this {@link ListModel} -- you can also use
220 * {@link ListModel#ListModel(JList6, JPopupMenu)} instead.
221 *
222 * @param list
223 * the {@link JList} we will handle the data of (cannot be NULL,
224 * must only contain elements of the type of this
225 * {@link ListModel})
226 * @param tooltipCreator
227 * use this if you want the list to display tooltips on hover
228 * (can be NULL)
229 */
230 @SuppressWarnings("rawtypes") // JList<?> not in Java 1.6
231 public ListModel(JList list, TooltipCreator<T> tooltipCreator) {
232 this(list, null, tooltipCreator);
233 }
234
235 /**
236 * Create a new {@link ListModel}.
237 * <p>
238 * Note that you must take care of passing a {@link JList} that only handles
239 * elements of the type of this {@link ListModel} -- you can also use
240 * {@link ListModel#ListModel(JList6, JPopupMenu)} instead.
241 *
242 * @param list
243 * the {@link JList} we will handle the data of (cannot be NULL,
244 * must only contain elements of the type of this
245 * {@link ListModel})
246 * @param popup
247 * the popup to use and keep track of (can be NULL)
248 * @param tooltipCreator
249 * use this if you want the list to display tooltips on hover
250 * (can be NULL)
251 */
252 @SuppressWarnings({ "unchecked", "rawtypes" }) // JList<?> not in Java 1.6
253 public ListModel(final JList list, final JPopupMenu popup,
254 TooltipCreator<T> tooltipCreator) {
255 this.list = list;
256 this.tooltipCreator = tooltipCreator;
257
258 list.setModel(this);
259
260 final DelayWorker tooltipWatcher = new DelayWorker(DELAY_TOOLTIP_MS);
261 if (tooltipCreator != null) {
262 tooltipWatcher.start();
263 }
264
265 list.addMouseMotionListener(new MouseAdapter() {
266 @Override
267 public void mouseMoved(final MouseEvent me) {
268 if (popup != null && popup.isShowing())
269 return;
270
271 Point p = new Point(me.getX(), me.getY());
272 final int index = list.locationToIndex(p);
273 if (index != hoveredIndex) {
274 int oldIndex = hoveredIndex;
275 hoveredIndex = index;
276 fireElementChanged(oldIndex);
277 fireElementChanged(index);
278
279 Window oldTooltip = tooltip;
280 tooltip = null;
281 if (oldTooltip != null) {
282 oldTooltip.setVisible(false);
283 }
284
285 if (ListModel.this.tooltipCreator != null) {
286 tooltipWatcher.delay("tooltip",
287 new SwingWorker<Void, Void>() {
288 @Override
289 protected Void doInBackground()
290 throws Exception {
291 return null;
292 }
293
294 @Override
295 protected void done() {
296 if (index < 0
297 || index != hoveredIndex) {
298 return;
299 }
300
301 tooltip = newTooltip(index, me);
302 }
303 });
304 }
305 }
306 }
307 });
308
309 list.addMouseListener(new MouseAdapter() {
310 @Override
311 public void mousePressed(MouseEvent e) {
312 check(e);
313 }
314
315 @Override
316 public void mouseReleased(MouseEvent e) {
317 check(e);
318 }
319
320 @Override
321 public void mouseExited(MouseEvent e) {
322 if (popup != null && popup.isShowing())
323 return;
324
325 if (hoveredIndex > -1) {
326 int oldIndex = hoveredIndex;
327 hoveredIndex = -1;
328 fireElementChanged(oldIndex);
329 }
330 }
331
332 private void check(MouseEvent e) {
333 if (popup == null) {
334 return;
335 }
336
337 if (e.isPopupTrigger()) {
338 if (list.getSelectedIndices().length <= 1) {
339 list.setSelectedIndex(
340 list.locationToIndex(e.getPoint()));
341 }
342
343 popup.show(list, e.getX(), e.getY());
344 }
345 }
346
347 });
348 }
349
350 /**
351 * (Try and) keep the elements that were selected when filtering.
352 * <p>
353 * This will use toString on the elements to identify them, and can be a bit
354 * resource intensive.
355 *
356 * @return TRUE if we do
357 */
358 public boolean isKeepSelection() {
359 return keepSelection;
360 }
361
362 /**
363 * (Try and) keep the elements that were selected when filtering.
364 * <p>
365 * This will use toString on the elements to identify them, and can be a bit
366 * resource intensive.
367 *
368 * @param keepSelection
369 * TRUE to try and keep them selected
370 */
371 public void setKeepSelection(boolean keepSelection) {
372 this.keepSelection = keepSelection;
373 }
374
375 /**
376 * Check if this element is currently under the mouse.
377 *
378 * @param element
379 * the element to check
380 *
381 * @return TRUE if it is
382 */
383 public boolean isHovered(T element) {
384 return indexOf(element) == hoveredIndex;
385 }
386
387 /**
388 * Check if this element is currently under the mouse.
389 *
390 * @param index
391 * the index of the element to check
392 *
393 * @return TRUE if it is
394 */
395 public boolean isHovered(int index) {
396 return index == hoveredIndex;
397 }
398
399 /**
400 * Add an item to the model.
401 *
402 * @param item
403 * the new item to add
404 */
405 public void addItem(T item) {
406 items.add(item);
407 }
408
409 /**
410 * Add items to the model.
411 *
412 * @param items
413 * the new items to add
414 */
415 public void addAllItems(Collection<T> items) {
416 this.items.addAll(items);
417 }
418
419 /**
420 * Removes the first occurrence of the specified element from this list, if
421 * it is present (optional operation).
422 *
423 * @param item
424 * the item to remove if possible (can be NULL)
425 *
426 * @return TRUE if one element was removed, FALSE if not found
427 */
428 public boolean removeItem(T item) {
429 return items.remove(item);
430 }
431
432 /**
433 * Remove the items that pass the given filter (or all items if the filter
434 * is NULL).
435 *
436 * @param filter
437 * the filter (if the filter returns TRUE, the item will be
438 * removed)
439 *
440 * @return TRUE if at least one item was removed
441 */
442 public boolean removeItemIf(Predicate<T> filter) {
443 boolean changed = false;
444 if (filter == null) {
445 changed = !items.isEmpty();
446 clearItems();
447 } else {
448 for (int i = 0; i < items.size(); i++) {
449 if (filter.test(items.get(i))) {
450 items.remove(i--);
451 changed = true;
452 }
453 }
454 }
455
456 return changed;
457 }
458
459 /**
460 * Removes all the items from this model.
461 */
462 public void clearItems() {
463 items.clear();
464 }
465
466 /**
467 * Filter the current elements.
468 * <p>
469 * This method will clear all the elements then look into all the items:
470 * those that pass the given filter will be copied as elements.
471 *
472 * @param filter
473 * the filter to select which elements to keep; an item that pass
474 * the filter will be copied as an element (can be NULL, in that
475 * case all items will be copied as elements)
476 */
477 @SuppressWarnings("unchecked") // JList<?> not compatible Java 1.6
478 public void filter(Predicate<T> filter) {
479 ListSnapshot snapshot = null;
480
481 if (keepSelection)
482 snapshot = new ListSnapshot(list);
483
484 clear();
485 for (T item : items) {
486 if (filter == null || filter.test(item)) {
487 addElement(item);
488 }
489 }
490
491 if (keepSelection)
492 snapshot.apply();
493
494 list.repaint();
495 }
496
497 /**
498 * Return the currently selected elements.
499 *
500 * @return the selected elements
501 */
502 public List<T> getSelectedElements() {
503 List<T> selected = new ArrayList<T>();
504 for (int index : list.getSelectedIndices()) {
505 selected.add(get(index));
506 }
507
508 return selected;
509 }
510
511 /**
512 * Return the selected element if <b>one</b> and <b>only one</b> element is
513 * selected. I.E., if zero, two or more elements are selected, NULL will be
514 * returned.
515 *
516 * @return the element if it is the only selected element, NULL otherwise
517 */
518 public T getUniqueSelectedElement() {
519 List<T> selected = getSelectedElements();
520 if (selected.size() == 1) {
521 return selected.get(0);
522 }
523
524 return null;
525 }
526
527 /**
528 * Notify that this element has been changed.
529 *
530 * @param index
531 * the index of the element
532 */
533 public void fireElementChanged(int index) {
534 if (index >= 0) {
535 fireContentsChanged(this, index, index);
536 }
537 }
538
539 /**
540 * Notify that this element has been changed.
541 *
542 * @param element
543 * the element
544 */
545 public void fireElementChanged(T element) {
546 int index = indexOf(element);
547 if (index >= 0) {
548 fireContentsChanged(this, index, index);
549 }
550 }
551
552 @SuppressWarnings("unchecked") // JList<?> not compatible Java 1.6
553 @Override
554 public T get(int index) {
555 return (T) super.get(index);
556 }
557
558 private Window newTooltip(final int index, final MouseEvent me) {
559 final T value = ListModel.this.get(index);
560
561 final Window newTooltip = tooltipCreator.generateTooltip(value, true);
562
563 if (newTooltip != null) {
564 newTooltip.addMouseListener(new MouseAdapter() {
565 @Override
566 public void mouseClicked(MouseEvent e) {
567
568 Window promotedTooltip = tooltipCreator
569 .generateTooltip(value, false);
570 promotedTooltip.setLocation(newTooltip.getLocation());
571 newTooltip.setVisible(false);
572 promotedTooltip.setVisible(true);
573 }
574 });
575 newTooltip.setLocation(me.getXOnScreen(), me.getYOnScreen());
576
577 newTooltip.setVisible(true);
578 }
579
580 return newTooltip;
581 }
582
583 /**
584 * Generate a {@link ListCellRenderer} that supports {@link Hoverable}
585 * elements.
586 *
587 * @param <T>
588 * the type of elements and items (the same type), which should
589 * implement {@link Hoverable} (it will not cause issues if not,
590 * but then, it will be a default renderer)
591 * @param model
592 * the model to use
593 *
594 * @return a suitable, {@link Hoverable} compatible renderer
595 */
596 static public <T extends Component> ListCellRenderer6<T> generateRenderer(
597 final ListModel<T> model) {
598 return new ListCellRenderer6<T>() {
599 @Override
600 public Component getListCellRendererComponent(JList6<T> list,
601 T item, int index, boolean isSelected,
602 boolean cellHasFocus) {
603 if (item instanceof Hoverable) {
604 Hoverable hoverable = (Hoverable) item;
605 hoverable.setSelected(isSelected);
606 hoverable.setHovered(model.isHovered(index));
607 }
608
609 return item;
610 }
611 };
612 }
613 }