1 package be
.nikiroo
.utils
.ui
;
3 import java
.awt
.Component
;
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
;
12 import javax
.swing
.JList
;
13 import javax
.swing
.JPopupMenu
;
14 import javax
.swing
.ListCellRenderer
;
15 import javax
.swing
.SwingWorker
;
17 import be
.nikiroo
.utils
.compat
.DefaultListModel6
;
18 import be
.nikiroo
.utils
.compat
.JList6
;
19 import be
.nikiroo
.utils
.compat
.ListCellRenderer6
;
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).
26 * It also offers filter options, supports hovered changes and some more utility
32 * the type of elements and items (the same type)
34 public class ListModel
<T
> extends DefaultListModel6
<T
> {
35 private static final long serialVersionUID
= 1L;
37 /** How long to wait before displaying a tooltip, in milliseconds. */
38 private static final int DELAY_TOOLTIP_MS
= 1000;
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+).
48 * the type of elements and items (the same type)
50 public interface Predicate
<T
> {
52 * Check if an item or an element pass a filter.
57 * @return TRUE if the test passed, FALSE if not
59 public boolean test(T item
);
63 * A simple interface your elements must implement if you want to use
64 * {@link ListModel#generateRenderer(ListModel)}.
68 public interface Hoverable
{
70 * The element is currently selected.
73 * TRUE for selected, FALSE for unselected
75 public void setSelected(boolean selected
);
78 * The element is currently under the mouse cursor.
81 * TRUE if it is, FALSE if not
83 public void setHovered(boolean hovered
);
87 * An interface required to support tooltips on this {@link ListModel}.
92 * the type of elements and items (the same type)
94 public interface TooltipCreator
<T
> {
96 * Generate a tooltip {@link Window} for this element.
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).
104 * the element to generate a tooltip for
106 * TRUE for undecorated tooltip, FALSE for standalone
109 * @return the generated tooltip or NULL for none
111 public Window
generateTooltip(T t
, boolean undecorated
);
114 private int hoveredIndex
;
115 private List
<T
> items
= new ArrayList
<T
>();
116 private boolean keepSelection
= true;
118 private TooltipCreator
<T
> tooltipCreator
;
119 private Window tooltip
;
121 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
125 * Create a new {@link ListModel}.
128 * the {@link JList6} we will handle the data of (cannot be NULL)
130 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
131 public ListModel(JList6
<T
> list
) {
136 * Create a new {@link ListModel}.
139 * the {@link JList6} we will handle the data of (cannot be NULL)
141 * the popup to use and keep track of (can be NULL)
143 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
144 public ListModel(JList6
<T
> list
, JPopupMenu popup
) {
145 this((JList
) list
, popup
);
149 * Create a new {@link ListModel}.
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
157 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
158 public ListModel(JList6
<T
> list
, TooltipCreator
<T
> tooltipCreator
) {
159 this((JList
) list
, null, tooltipCreator
);
163 * Create a new {@link ListModel}.
166 * the {@link JList6} we will handle the data of (cannot be NULL)
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
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
);
180 * Create a new {@link ListModel}.
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.
187 * the {@link JList} we will handle the data of (cannot be NULL,
188 * must only contain elements of the type of this
191 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
192 public ListModel(JList list
) {
193 this(list
, null, null);
197 * Create a new {@link ListModel}.
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.
204 * the {@link JList} we will handle the data of (cannot be NULL,
205 * must only contain elements of the type of this
208 * the popup to use and keep track of (can be NULL)
210 @SuppressWarnings("rawtypes") // JList<?> not in Java 1.6
211 public ListModel(JList list
, JPopupMenu popup
) {
212 this(list
, popup
, null);
216 * Create a new {@link ListModel}.
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.
223 * the {@link JList} we will handle the data of (cannot be NULL,
224 * must only contain elements of the type of this
226 * @param tooltipCreator
227 * use this if you want the list to display tooltips on hover
230 @SuppressWarnings("rawtypes") // JList<?> not in Java 1.6
231 public ListModel(JList list
, TooltipCreator
<T
> tooltipCreator
) {
232 this(list
, null, tooltipCreator
);
236 * Create a new {@link ListModel}.
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.
243 * the {@link JList} we will handle the data of (cannot be NULL,
244 * must only contain elements of the type of this
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
252 @SuppressWarnings({ "unchecked", "rawtypes" }) // JList<?> not in Java 1.6
253 public ListModel(final JList list
, final JPopupMenu popup
,
254 TooltipCreator
<T
> tooltipCreator
) {
256 this.tooltipCreator
= tooltipCreator
;
260 final DelayWorker tooltipWatcher
= new DelayWorker(DELAY_TOOLTIP_MS
);
261 if (tooltipCreator
!= null) {
262 tooltipWatcher
.start();
265 list
.addMouseMotionListener(new MouseAdapter() {
267 public void mouseMoved(final MouseEvent me
) {
268 if (popup
!= null && popup
.isShowing())
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
);
279 synchronized (tooltipWatcher
) {
280 if (tooltip
!= null) {
281 tooltip
.setVisible(false);
286 if (ListModel
.this.tooltipCreator
!= null) {
287 tooltipWatcher
.delay("tooltip",
288 new SwingWorker
<Void
, Void
>() {
290 protected Void
doInBackground()
296 protected void done() {
297 synchronized (tooltipWatcher
) {
298 if (tooltip
!= null) {
299 tooltip
.setVisible(false);
305 || index
!= hoveredIndex
) {
310 && popup
.isShowing()) {
314 synchronized (tooltipWatcher
) {
315 if (tooltip
!= null) {
316 tooltip
.setVisible(false);
318 tooltip
= newTooltip(index
, me
);
327 list
.addMouseListener(new MouseAdapter() {
329 public void mousePressed(MouseEvent e
) {
334 public void mouseReleased(MouseEvent e
) {
339 public void mouseExited(MouseEvent e
) {
340 if (popup
!= null && popup
.isShowing())
343 if (hoveredIndex
> -1) {
344 int oldIndex
= hoveredIndex
;
346 fireElementChanged(oldIndex
);
350 private void check(MouseEvent e
) {
355 if (e
.isPopupTrigger()) {
356 if (list
.getSelectedIndices().length
<= 1) {
357 list
.setSelectedIndex(
358 list
.locationToIndex(e
.getPoint()));
361 Window oldTooltip
= tooltip
;
363 if (oldTooltip
!= null) {
364 oldTooltip
.setVisible(false);
367 popup
.show(list
, e
.getX(), e
.getY());
375 * (Try and) keep the elements that were selected when filtering.
377 * This will use toString on the elements to identify them, and can be a bit
378 * resource intensive.
380 * @return TRUE if we do
382 public boolean isKeepSelection() {
383 return keepSelection
;
387 * (Try and) keep the elements that were selected when filtering.
389 * This will use toString on the elements to identify them, and can be a bit
390 * resource intensive.
392 * @param keepSelection
393 * TRUE to try and keep them selected
395 public void setKeepSelection(boolean keepSelection
) {
396 this.keepSelection
= keepSelection
;
400 * Check if this element is currently under the mouse.
403 * the element to check
405 * @return TRUE if it is
407 public boolean isHovered(T element
) {
408 return indexOf(element
) == hoveredIndex
;
412 * Check if this element is currently under the mouse.
415 * the index of the element to check
417 * @return TRUE if it is
419 public boolean isHovered(int index
) {
420 return index
== hoveredIndex
;
424 * Add an item to the model.
427 * the new item to add
429 public void addItem(T item
) {
434 * Add items to the model.
437 * the new items to add
439 public void addAllItems(Collection
<T
> items
) {
440 this.items
.addAll(items
);
444 * Removes the first occurrence of the specified element from this list, if
445 * it is present (optional operation).
448 * the item to remove if possible (can be NULL)
450 * @return TRUE if one element was removed, FALSE if not found
452 public boolean removeItem(T item
) {
453 return items
.remove(item
);
457 * Remove the items that pass the given filter (or all items if the filter
461 * the filter (if the filter returns TRUE, the item will be
464 * @return TRUE if at least one item was removed
466 public boolean removeItemIf(Predicate
<T
> filter
) {
467 boolean changed
= false;
468 if (filter
== null) {
469 changed
= !items
.isEmpty();
472 for (int i
= 0; i
< items
.size(); i
++) {
473 if (filter
.test(items
.get(i
))) {
484 * Removes all the items from this model.
486 public void clearItems() {
491 * Filter the current elements.
493 * This method will clear all the elements then look into all the items:
494 * those that pass the given filter will be copied as elements.
497 * the filter to select which elements to keep; an item that pass
498 * the filter will be copied as an element (can be NULL, in that
499 * case all items will be copied as elements)
501 @SuppressWarnings("unchecked") // JList<?> not compatible Java 1.6
502 public void filter(Predicate
<T
> filter
) {
503 ListSnapshot snapshot
= null;
506 snapshot
= new ListSnapshot(list
);
509 for (T item
: items
) {
510 if (filter
== null || filter
.test(item
)) {
522 * Return the currently selected elements.
524 * @return the selected elements
526 public List
<T
> getSelectedElements() {
527 List
<T
> selected
= new ArrayList
<T
>();
528 for (int index
: list
.getSelectedIndices()) {
529 selected
.add(get(index
));
536 * Return the selected element if <b>one</b> and <b>only one</b> element is
537 * selected. I.E., if zero, two or more elements are selected, NULL will be
540 * @return the element if it is the only selected element, NULL otherwise
542 public T
getUniqueSelectedElement() {
543 List
<T
> selected
= getSelectedElements();
544 if (selected
.size() == 1) {
545 return selected
.get(0);
552 * Notify that this element has been changed.
555 * the index of the element
557 public void fireElementChanged(int index
) {
559 fireContentsChanged(this, index
, index
);
564 * Notify that this element has been changed.
569 public void fireElementChanged(T element
) {
570 int index
= indexOf(element
);
572 fireContentsChanged(this, index
, index
);
576 @SuppressWarnings("unchecked") // JList<?> not compatible Java 1.6
578 public T
get(int index
) {
579 return (T
) super.get(index
);
582 private Window
newTooltip(final int index
, final MouseEvent me
) {
583 final T value
= ListModel
.this.get(index
);
585 final Window newTooltip
= tooltipCreator
.generateTooltip(value
, true);
587 if (newTooltip
!= null) {
588 newTooltip
.addMouseListener(new MouseAdapter() {
590 public void mouseClicked(MouseEvent e
) {
591 Window promotedTooltip
= tooltipCreator
592 .generateTooltip(value
, false);
593 if (promotedTooltip
!= null) {
594 promotedTooltip
.setLocation(me
.getXOnScreen(),
596 promotedTooltip
.setVisible(true);
599 newTooltip
.setVisible(false);
603 newTooltip
.setLocation(me
.getXOnScreen(), me
.getYOnScreen());
604 newTooltip
.setVisible(true);
611 * Generate a {@link ListCellRenderer} that supports {@link Hoverable}
615 * the type of elements and items (the same type), which should
616 * implement {@link Hoverable} (it will not cause issues if not,
617 * but then, it will be a default renderer)
621 * @return a suitable, {@link Hoverable} compatible renderer
623 static public <T
extends Component
> ListCellRenderer6
<T
> generateRenderer(
624 final ListModel
<T
> model
) {
625 return new ListCellRenderer6
<T
>() {
627 public Component
getListCellRendererComponent(JList6
<T
> list
,
628 T item
, int index
, boolean isSelected
,
629 boolean cellHasFocus
) {
630 if (item
instanceof Hoverable
) {
631 Hoverable hoverable
= (Hoverable
) item
;
632 hoverable
.setSelected(isSelected
);
633 hoverable
.setHovered(model
.isHovered(index
));