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;
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+).
45 * the type of elements and items (the same type)
47 public interface Predicate
<T
> {
49 * Check if an item or an element pass a filter.
54 * @return TRUE if the test passed, FALSE if not
56 public boolean test(T item
);
60 * A simple interface your elements must implement if you want to use
61 * {@link ListModel#generateRenderer(ListModel)}.
65 public interface Hoverable
{
67 * The element is currently selected.
70 * TRUE for selected, FALSE for unselected
72 public void setSelected(boolean selected
);
75 * The element is currently under the mouse cursor.
78 * TRUE if it is, FALSE if not
80 public void setHovered(boolean hovered
);
84 * An interface required to support tooltips on this {@link ListModel}.
89 * the type of elements and items (the same type)
91 public interface TooltipCreator
<T
> {
93 * Generate a tooltip {@link Window} for this element.
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).
101 * the element to generate a tooltip for
103 * TRUE for undecorated tooltip, FALSE for standalone
106 * @return the generated tooltip or NULL for none
108 public Window
generateTooltip(T t
, boolean undecorated
);
111 private int hoveredIndex
;
112 private List
<T
> items
= new ArrayList
<T
>();
113 private boolean keepSelection
= true;
115 private TooltipCreator
<T
> tooltipCreator
;
116 private Window tooltip
;
118 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
122 * Create a new {@link ListModel}.
125 * the {@link JList6} we will handle the data of (cannot be NULL)
127 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
128 public ListModel(JList6
<T
> list
) {
133 * Create a new {@link ListModel}.
136 * the {@link JList6} we will handle the data of (cannot be NULL)
138 * the popup to use and keep track of (can be NULL)
140 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
141 public ListModel(JList6
<T
> list
, JPopupMenu popup
) {
142 this((JList
) list
, popup
);
146 * Create a new {@link ListModel}.
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
154 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
155 public ListModel(JList6
<T
> list
, TooltipCreator
<T
> tooltipCreator
) {
156 this((JList
) list
, null, tooltipCreator
);
160 * Create a new {@link ListModel}.
163 * the {@link JList6} we will handle the data of (cannot be NULL)
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
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
);
177 * Create a new {@link ListModel}.
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.
184 * the {@link JList} we will handle the data of (cannot be NULL,
185 * must only contain elements of the type of this
188 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
189 public ListModel(JList list
) {
190 this(list
, null, null);
194 * Create a new {@link ListModel}.
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.
201 * the {@link JList} we will handle the data of (cannot be NULL,
202 * must only contain elements of the type of this
205 * the popup to use and keep track of (can be NULL)
207 @SuppressWarnings("rawtypes") // JList<?> not in Java 1.6
208 public ListModel(JList list
, JPopupMenu popup
) {
209 this(list
, popup
, null);
213 * Create a new {@link ListModel}.
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.
220 * the {@link JList} we will handle the data of (cannot be NULL,
221 * must only contain elements of the type of this
223 * @param tooltipCreator
224 * use this if you want the list to display tooltips on hover
227 @SuppressWarnings("rawtypes") // JList<?> not in Java 1.6
228 public ListModel(JList list
, TooltipCreator
<T
> tooltipCreator
) {
229 this(list
, null, tooltipCreator
);
233 * Create a new {@link ListModel}.
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.
240 * the {@link JList} we will handle the data of (cannot be NULL,
241 * must only contain elements of the type of this
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
249 @SuppressWarnings({ "unchecked", "rawtypes" }) // JList<?> not in Java 1.6
250 public ListModel(final JList list
, final JPopupMenu popup
,
251 TooltipCreator
<T
> tooltipCreator
) {
253 this.tooltipCreator
= tooltipCreator
;
257 final DelayWorker tooltipWatcher
= new DelayWorker(500);
258 if (tooltipCreator
!= null) {
259 tooltipWatcher
.start();
262 list
.addMouseMotionListener(new MouseAdapter() {
264 public void mouseMoved(final MouseEvent me
) {
265 if (popup
!= null && popup
.isShowing())
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
);
276 if (ListModel
.this.tooltipCreator
!= null) {
277 tooltipWatcher
.delay("tooltip",
278 new SwingWorker
<Void
, Void
>() {
280 protected Void
doInBackground()
286 protected void done() {
287 Window oldTooltip
= tooltip
;
289 if (oldTooltip
!= null) {
290 oldTooltip
.setVisible(false);
294 || index
!= hoveredIndex
) {
298 tooltip
= newTooltip(index
, me
);
306 list
.addMouseListener(new MouseAdapter() {
308 public void mousePressed(MouseEvent e
) {
313 public void mouseReleased(MouseEvent e
) {
318 public void mouseExited(MouseEvent e
) {
319 if (popup
!= null && popup
.isShowing())
322 if (hoveredIndex
> -1) {
323 int oldIndex
= hoveredIndex
;
325 fireElementChanged(oldIndex
);
329 private void check(MouseEvent e
) {
334 if (e
.isPopupTrigger()) {
335 if (list
.getSelectedIndices().length
<= 1) {
336 list
.setSelectedIndex(
337 list
.locationToIndex(e
.getPoint()));
340 popup
.show(list
, e
.getX(), e
.getY());
348 * (Try and) keep the elements that were selected when filtering.
350 * This will use toString on the elements to identify them, and can be a bit
351 * resource intensive.
353 * @return TRUE if we do
355 public boolean isKeepSelection() {
356 return keepSelection
;
360 * (Try and) keep the elements that were selected when filtering.
362 * This will use toString on the elements to identify them, and can be a bit
363 * resource intensive.
365 * @param keepSelection
366 * TRUE to try and keep them selected
368 public void setKeepSelection(boolean keepSelection
) {
369 this.keepSelection
= keepSelection
;
373 * Check if this element is currently under the mouse.
376 * the element to check
378 * @return TRUE if it is
380 public boolean isHovered(T element
) {
381 return indexOf(element
) == hoveredIndex
;
385 * Check if this element is currently under the mouse.
388 * the index of the element to check
390 * @return TRUE if it is
392 public boolean isHovered(int index
) {
393 return index
== hoveredIndex
;
397 * Add an item to the model.
400 * the new item to add
402 public void addItem(T item
) {
407 * Add items to the model.
410 * the new items to add
412 public void addAllItems(Collection
<T
> items
) {
413 this.items
.addAll(items
);
417 * Removes the first occurrence of the specified element from this list, if
418 * it is present (optional operation).
421 * the item to remove if possible (can be NULL)
423 * @return TRUE if one element was removed, FALSE if not found
425 public boolean removeItem(T item
) {
426 return items
.remove(item
);
430 * Remove the items that pass the given filter (or all items if the filter
434 * the filter (if the filter returns TRUE, the item will be
437 * @return TRUE if at least one item was removed
439 public boolean removeItemIf(Predicate
<T
> filter
) {
440 boolean changed
= false;
441 if (filter
== null) {
442 changed
= !items
.isEmpty();
445 for (int i
= 0; i
< items
.size(); i
++) {
446 if (filter
.test(items
.get(i
))) {
457 * Removes all the items from this model.
459 public void clearItems() {
464 * Filter the current elements.
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.
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)
474 @SuppressWarnings("unchecked") // JList<?> not compatible Java 1.6
475 public void filter(Predicate
<T
> filter
) {
476 ListSnapshot snapshot
= null;
479 snapshot
= new ListSnapshot(list
);
482 for (T item
: items
) {
483 if (filter
== null || filter
.test(item
)) {
495 * Return the currently selected elements.
497 * @return the selected elements
499 public List
<T
> getSelectedElements() {
500 List
<T
> selected
= new ArrayList
<T
>();
501 for (int index
: list
.getSelectedIndices()) {
502 selected
.add(get(index
));
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
513 * @return the element if it is the only selected element, NULL otherwise
515 public T
getUniqueSelectedElement() {
516 List
<T
> selected
= getSelectedElements();
517 if (selected
.size() == 1) {
518 return selected
.get(0);
525 * Notify that this element has been changed.
528 * the index of the element
530 public void fireElementChanged(int index
) {
532 fireContentsChanged(this, index
, index
);
537 * Notify that this element has been changed.
542 public void fireElementChanged(T element
) {
543 int index
= indexOf(element
);
545 fireContentsChanged(this, index
, index
);
549 @SuppressWarnings("unchecked") // JList<?> not compatible Java 1.6
551 public T
get(int index
) {
552 return (T
) super.get(index
);
555 private Window
newTooltip(final int index
, final MouseEvent me
) {
556 final T value
= ListModel
.this.get(index
);
558 final Window newTooltip
= tooltipCreator
.generateTooltip(value
, true);
560 if (newTooltip
!= null) {
561 newTooltip
.addMouseListener(new MouseAdapter() {
563 public void mouseClicked(MouseEvent e
) {
565 Window promotedTooltip
= tooltipCreator
566 .generateTooltip(value
, false);
567 promotedTooltip
.setLocation(newTooltip
.getLocation());
568 newTooltip
.setVisible(false);
569 promotedTooltip
.setVisible(true);
572 newTooltip
.setLocation(me
.getXOnScreen(), me
.getYOnScreen());
574 newTooltip
.setVisible(true);
581 * Generate a {@link ListCellRenderer} that supports {@link Hoverable}
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)
591 * @return a suitable, {@link Hoverable} compatible renderer
593 static public <T
extends Component
> ListCellRenderer6
<T
> generateRenderer(
594 final ListModel
<T
> model
) {
595 return new ListCellRenderer6
<T
>() {
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
));