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