Merge branch 'subtree'
[fanfix.git] / src / be / nikiroo / utils / 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 final 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 if (tooltipCreator != null) {
280 showTooltip(null);
281
282 tooltipWatcher.delay("tooltip",
283 new SwingWorker<Void, Void>() {
284 @Override
285 protected Void doInBackground()
286 throws Exception {
287 return null;
288 }
289
290 @Override
291 protected void done() {
292 showTooltip(null);
293
294 if (index < 0
295 || index != hoveredIndex) {
296 return;
297 }
298
299 if (popup != null
300 && popup.isShowing()) {
301 return;
302 }
303
304 showTooltip(newTooltip(index, me));
305 }
306 });
307 }
308 }
309 }
310 });
311
312 list.addMouseListener(new MouseAdapter() {
313 @Override
314 public void mousePressed(MouseEvent e) {
315 check(e);
316 }
317
318 @Override
319 public void mouseReleased(MouseEvent e) {
320 check(e);
321 }
322
323 @Override
324 public void mouseExited(MouseEvent e) {
325 if (popup != null && popup.isShowing())
326 return;
327
328 if (hoveredIndex > -1) {
329 int oldIndex = hoveredIndex;
330 hoveredIndex = -1;
331 fireElementChanged(oldIndex);
332 }
333 }
334
335 private void check(MouseEvent e) {
336 if (popup == null) {
337 return;
338 }
339
340 if (e.isPopupTrigger()) {
341 if (list.getSelectedIndices().length <= 1) {
342 list.setSelectedIndex(
343 list.locationToIndex(e.getPoint()));
344 }
345
346 showTooltip(null);
347 popup.show(list, e.getX(), e.getY());
348 }
349 }
350
351 });
352 }
353
354 /**
355 * (Try and) keep the elements that were selected when filtering.
356 * <p>
357 * This will use toString on the elements to identify them, and can be a bit
358 * resource intensive.
359 *
360 * @return TRUE if we do
361 */
362 public boolean isKeepSelection() {
363 return keepSelection;
364 }
365
366 /**
367 * (Try and) keep the elements that were selected when filtering.
368 * <p>
369 * This will use toString on the elements to identify them, and can be a bit
370 * resource intensive.
371 *
372 * @param keepSelection
373 * TRUE to try and keep them selected
374 */
375 public void setKeepSelection(boolean keepSelection) {
376 this.keepSelection = keepSelection;
377 }
378
379 /**
380 * Check if this element is currently under the mouse.
381 *
382 * @param element
383 * the element to check
384 *
385 * @return TRUE if it is
386 */
387 public boolean isHovered(T element) {
388 return indexOf(element) == hoveredIndex;
389 }
390
391 /**
392 * Check if this element is currently under the mouse.
393 *
394 * @param index
395 * the index of the element to check
396 *
397 * @return TRUE if it is
398 */
399 public boolean isHovered(int index) {
400 return index == hoveredIndex;
401 }
402
403 /**
404 * Add an item to the model.
405 *
406 * @param item
407 * the new item to add
408 */
409 public void addItem(T item) {
410 items.add(item);
411 }
412
413 /**
414 * Add items to the model.
415 *
416 * @param items
417 * the new items to add
418 */
419 public void addAllItems(Collection<T> items) {
420 this.items.addAll(items);
421 }
422
423 /**
424 * Removes the first occurrence of the specified element from this list, if
425 * it is present (optional operation).
426 *
427 * @param item
428 * the item to remove if possible (can be NULL)
429 *
430 * @return TRUE if one element was removed, FALSE if not found
431 */
432 public boolean removeItem(T item) {
433 return items.remove(item);
434 }
435
436 /**
437 * Remove the items that pass the given filter (or all items if the filter
438 * is NULL).
439 *
440 * @param filter
441 * the filter (if the filter returns TRUE, the item will be
442 * removed)
443 *
444 * @return TRUE if at least one item was removed
445 */
446 public boolean removeItemIf(Predicate<T> filter) {
447 boolean changed = false;
448 if (filter == null) {
449 changed = !items.isEmpty();
450 clearItems();
451 } else {
452 for (int i = 0; i < items.size(); i++) {
453 if (filter.test(items.get(i))) {
454 items.remove(i--);
455 changed = true;
456 }
457 }
458 }
459
460 return changed;
461 }
462
463 /**
464 * Removes all the items from this model.
465 */
466 public void clearItems() {
467 items.clear();
468 }
469
470 /**
471 * Filter the current elements.
472 * <p>
473 * This method will clear all the elements then look into all the items:
474 * those that pass the given filter will be copied as elements.
475 *
476 * @param filter
477 * the filter to select which elements to keep; an item that pass
478 * the filter will be copied as an element (can be NULL, in that
479 * case all items will be copied as elements)
480 */
481 @SuppressWarnings("unchecked") // JList<?> not compatible Java 1.6
482 public void filter(Predicate<T> filter) {
483 ListSnapshot snapshot = null;
484
485 if (keepSelection)
486 snapshot = new ListSnapshot(list);
487
488 clear();
489 for (T item : items) {
490 if (filter == null || filter.test(item)) {
491 addElement(item);
492 }
493 }
494
495 if (keepSelection)
496 snapshot.apply();
497
498 list.repaint();
499 }
500
501 /**
502 * Return the currently selected elements.
503 *
504 * @return the selected elements
505 */
506 public List<T> getSelectedElements() {
507 List<T> selected = new ArrayList<T>();
508 for (int index : list.getSelectedIndices()) {
509 selected.add(get(index));
510 }
511
512 return selected;
513 }
514
515 /**
516 * Return the selected element if <b>one</b> and <b>only one</b> element is
517 * selected. I.E., if zero, two or more elements are selected, NULL will be
518 * returned.
519 *
520 * @return the element if it is the only selected element, NULL otherwise
521 */
522 public T getUniqueSelectedElement() {
523 List<T> selected = getSelectedElements();
524 if (selected.size() == 1) {
525 return selected.get(0);
526 }
527
528 return null;
529 }
530
531 /**
532 * Notify that this element has been changed.
533 *
534 * @param index
535 * the index of the element
536 */
537 public void fireElementChanged(int index) {
538 if (index >= 0) {
539 fireContentsChanged(this, index, index);
540 }
541 }
542
543 /**
544 * Notify that this element has been changed.
545 *
546 * @param element
547 * the element
548 */
549 public void fireElementChanged(T element) {
550 int index = indexOf(element);
551 if (index >= 0) {
552 fireContentsChanged(this, index, index);
553 }
554 }
555
556 @SuppressWarnings("unchecked") // JList<?> not compatible Java 1.6
557 @Override
558 public T get(int index) {
559 return (T) super.get(index);
560 }
561
562 private Window newTooltip(final int index, final MouseEvent me) {
563 final T value = ListModel.this.get(index);
564 final Window newTooltip = tooltipCreator.generateTooltip(value, true);
565 if (newTooltip != null) {
566 newTooltip.addMouseListener(new MouseAdapter() {
567 @Override
568 public void mouseClicked(MouseEvent e) {
569 Window promotedTooltip = tooltipCreator
570 .generateTooltip(value, false);
571 if (promotedTooltip != null) {
572 promotedTooltip.setLocation(me.getXOnScreen(),
573 me.getYOnScreen());
574 promotedTooltip.setVisible(true);
575 }
576
577 newTooltip.setVisible(false);
578 }
579 });
580
581 newTooltip.setLocation(me.getXOnScreen(), me.getYOnScreen());
582 showTooltip(newTooltip);
583 }
584
585 return newTooltip;
586 }
587
588 private void showTooltip(Window tooltip) {
589 synchronized (tooltipCreator) {
590 if (this.tooltip != null) {
591 this.tooltip.setVisible(false);
592 this.tooltip.dispose();
593 }
594
595 this.tooltip = tooltip;
596
597 if (tooltip != null) {
598 tooltip.setVisible(true);
599 }
600 }
601 }
602
603 /**
604 * Generate a {@link ListCellRenderer} that supports {@link Hoverable}
605 * elements.
606 *
607 * @param <T>
608 * the type of elements and items (the same type), which should
609 * implement {@link Hoverable} (it will not cause issues if not,
610 * but then, it will be a default renderer)
611 * @param model
612 * the model to use
613 *
614 * @return a suitable, {@link Hoverable} compatible renderer
615 */
616 static public <T extends Component> ListCellRenderer6<T> generateRenderer(
617 final ListModel<T> model) {
618 return new ListCellRenderer6<T>() {
619 @Override
620 public Component getListCellRendererComponent(JList6<T> list,
621 T item, int index, boolean isSelected,
622 boolean cellHasFocus) {
623 if (item instanceof Hoverable) {
624 Hoverable hoverable = (Hoverable) item;
625 hoverable.setSelected(isSelected);
626 hoverable.setHovered(model.isHovered(index));
627 }
628
629 return item;
630 }
631 };
632 }
633 }