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.event.MouseAdapter;
6 import java.awt.event.MouseEvent;
7 import java.util.ArrayList;
8 import java.util.Collection;
9 import java.util.List;
10
11 import javax.swing.JList;
12 import javax.swing.JPopupMenu;
13 import javax.swing.ListCellRenderer;
14
15 import be.nikiroo.utils.compat.DefaultListModel6;
16 import be.nikiroo.utils.compat.JList6;
17 import be.nikiroo.utils.compat.ListCellRenderer6;
18
19 /**
20 * A {@link javax.swing.ListModel} that can maintain 2 lists; one with the
21 * actual data (the elements), and a second one with the items that are
22 * currently displayed (the items).
23 * <p>
24 * It also offers filter options, supports hovered changes and some more utility
25 * functions.
26 *
27 * @author niki
28 *
29 * @param <T>
30 * the type of elements and items (the same type)
31 */
32 public class ListModel<T> extends DefaultListModel6<T> {
33 private static final long serialVersionUID = 1L;
34
35 /**
36 * A filter interface, to check for a condition (note that a Predicate class
37 * already exists in Java 1.8+, and is compatible with this one if you
38 * change the signatures -- but I support java 1.6+).
39 *
40 * @author niki
41 *
42 * @param <T>
43 * the type of elements and items (the same type)
44 */
45 public interface Predicate<T> {
46 /**
47 * Check if an item or an element pass a filter.
48 *
49 * @param item
50 * the item to test
51 *
52 * @return TRUE if the test passed, FALSE if not
53 */
54 public boolean test(T item);
55 }
56
57 /**
58 * A simple interface your elements must implement if you want to use
59 * {@link ListModel#generateRenderer(ListModel)}.
60 *
61 * @author niki
62 */
63 public interface Hoverable {
64 /**
65 * The element is currently selected.
66 *
67 * @param selected
68 * TRUE for selected, FALSE for unselected
69 */
70 public void setSelected(boolean selected);
71
72 /**
73 * The element is currently under the mouse cursor.
74 *
75 * @param hovered
76 * TRUE if it is, FALSE if not
77 */
78 public void setHovered(boolean hovered);
79 }
80
81 private int hoveredIndex;
82 private List<T> items = new ArrayList<T>();
83 private boolean keepSelection = true;
84
85 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
86 private JList list;
87
88 /**
89 * Create a new {@link ListModel}.
90 *
91 * @param list
92 * the {@link JList6} we will handle the data of (cannot be NULL)
93 */
94 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
95 public ListModel(JList6<T> list) {
96 this((JList) list);
97 }
98
99 /**
100 * Create a new {@link ListModel}.
101 *
102 * @param list
103 * the {@link JList6} we will handle the data of (cannot be NULL)
104 * @param popup
105 * the popup to use and keep track of (can be NULL)
106 */
107 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
108 public ListModel(final JList6<T> list, final JPopupMenu popup) {
109 this((JList) list, popup);
110 }
111
112 /**
113 * Create a new {@link ListModel}.
114 * <p>
115 * Note that you must take care of passing a {@link JList} that only handles
116 * elements of the type of this {@link ListModel} -- you can also use
117 * {@link ListModel#ListModel(JList6)} instead.
118 *
119 * @param list
120 * the {@link JList} we will handle the data of (cannot be NULL,
121 * must only contain elements of the type of this
122 * {@link ListModel})
123 */
124 @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
125 public ListModel(JList list) {
126 this(list, null);
127 }
128
129 /**
130 * Create a new {@link ListModel}.
131 * <p>
132 * Note that you must take care of passing a {@link JList} that only handles
133 * elements of the type of this {@link ListModel} -- you can also use
134 * {@link ListModel#ListModel(JList6, JPopupMenu)} instead.
135 *
136 * @param list
137 * the {@link JList} we will handle the data of (cannot be NULL,
138 * must only contain elements of the type of this
139 * {@link ListModel})
140 * @param popup
141 * the popup to use and keep track of (can be NULL)
142 */
143 @SuppressWarnings({ "unchecked", "rawtypes" }) // JList<?> not in Java 1.6
144 public ListModel(final JList list, final JPopupMenu popup) {
145 this.list = list;
146 list.setModel(this);
147
148 list.addMouseMotionListener(new MouseAdapter() {
149 @Override
150 public void mouseMoved(MouseEvent me) {
151 if (popup != null && popup.isShowing())
152 return;
153
154 Point p = new Point(me.getX(), me.getY());
155 int index = list.locationToIndex(p);
156 if (index != hoveredIndex) {
157 int oldIndex = hoveredIndex;
158 hoveredIndex = index;
159 fireElementChanged(oldIndex);
160 fireElementChanged(index);
161 }
162 }
163 });
164
165 list.addMouseListener(new MouseAdapter() {
166 @Override
167 public void mousePressed(MouseEvent e) {
168 check(e);
169 }
170
171 @Override
172 public void mouseReleased(MouseEvent e) {
173 check(e);
174 }
175
176 @Override
177 public void mouseExited(MouseEvent e) {
178 if (popup != null && popup.isShowing())
179 return;
180
181 if (hoveredIndex > -1) {
182 int oldIndex = hoveredIndex;
183 hoveredIndex = -1;
184 fireElementChanged(oldIndex);
185 }
186 }
187
188 private void check(MouseEvent e) {
189 if (popup == null) {
190 return;
191 }
192
193 if (e.isPopupTrigger()) {
194 if (list.getSelectedIndices().length <= 1) {
195 list.setSelectedIndex(
196 list.locationToIndex(e.getPoint()));
197 }
198
199 popup.show(list, e.getX(), e.getY());
200 }
201 }
202 });
203 }
204
205 /**
206 * (Try and) keep the elements that were selected when filtering.
207 * <p>
208 * This will use toString on the elements to identify them, and can be a bit
209 * resource intensive.
210 *
211 * @return TRUE if we do
212 */
213 public boolean isKeepSelection() {
214 return keepSelection;
215 }
216
217 /**
218 * (Try and) keep the elements that were selected when filtering.
219 * <p>
220 * This will use toString on the elements to identify them, and can be a bit
221 * resource intensive.
222 *
223 * @param keepSelection
224 * TRUE to try and keep them selected
225 */
226 public void setKeepSelection(boolean keepSelection) {
227 this.keepSelection = keepSelection;
228 }
229
230 /**
231 * Check if this element is currently under the mouse.
232 *
233 * @param element
234 * the element to check
235 *
236 * @return TRUE if it is
237 */
238 public boolean isHovered(T element) {
239 return indexOf(element) == hoveredIndex;
240 }
241
242 /**
243 * Check if this element is currently under the mouse.
244 *
245 * @param index
246 * the index of the element to check
247 *
248 * @return TRUE if it is
249 */
250 public boolean isHovered(int index) {
251 return index == hoveredIndex;
252 }
253
254 /**
255 * Add an item to the model.
256 *
257 * @param item
258 * the new item to add
259 */
260 public void addItem(T item) {
261 items.add(item);
262 }
263
264 /**
265 * Add items to the model.
266 *
267 * @param items
268 * the new items to add
269 */
270 public void addAllItems(Collection<T> items) {
271 this.items.addAll(items);
272 }
273
274 /**
275 * Removes the first occurrence of the specified element from this list, if
276 * it is present (optional operation).
277 *
278 * @param item
279 * the item to remove if possible (can be NULL)
280 *
281 * @return TRUE if one element was removed, FALSE if not found
282 */
283 public boolean removeItem(T item) {
284 return items.remove(item);
285 }
286
287 /**
288 * Remove the items that pass the given filter (or all items if the filter
289 * is NULL).
290 *
291 * @param filter
292 * the filter (if the filter returns TRUE, the item will be
293 * removed)
294 *
295 * @return TRUE if at least one item was removed
296 */
297 public boolean removeItemIf(Predicate<T> filter) {
298 boolean changed = false;
299 if (filter == null) {
300 changed = !items.isEmpty();
301 clearItems();
302 } else {
303 for (int i = 0; i < items.size(); i++) {
304 if (filter.test(items.get(i))) {
305 items.remove(i--);
306 changed = true;
307 }
308 }
309 }
310
311 return changed;
312 }
313
314 /**
315 * Removes all the items from this model.
316 */
317 public void clearItems() {
318 items.clear();
319 }
320
321 /**
322 * Filter the current elements.
323 * <p>
324 * This method will clear all the elements then look into all the items:
325 * those that pass the given filter will be copied as elements.
326 *
327 * @param filter
328 * the filter to select which elements to keep; an item that pass
329 * the filter will be copied as an element (can be NULL, in that
330 * case all items will be copied as elements)
331 */
332 @SuppressWarnings("unchecked") // JList<?> not compatible Java 1.6
333 public void filter(Predicate<T> filter) {
334 ListSnapshot snapshot = null;
335
336 if (keepSelection)
337 snapshot = new ListSnapshot(list);
338
339 clear();
340 for (T item : items) {
341 if (filter == null || filter.test(item)) {
342 addElement(item);
343 }
344 }
345
346 if (keepSelection)
347 snapshot.apply();
348
349 list.repaint();
350 }
351
352 /**
353 * Return the currently selected elements.
354 *
355 * @return the selected elements
356 */
357 public List<T> getSelectedElements() {
358 List<T> selected = new ArrayList<T>();
359 for (int index : list.getSelectedIndices()) {
360 selected.add(get(index));
361 }
362
363 return selected;
364 }
365
366 /**
367 * Return the selected element if <b>one</b> and <b>only one</b> element is
368 * selected. I.E., if zero, two or more elements are selected, NULL will be
369 * returned.
370 *
371 * @return the element if it is the only selected element, NULL otherwise
372 */
373 public T getUniqueSelectedElement() {
374 List<T> selected = getSelectedElements();
375 if (selected.size() == 1) {
376 return selected.get(0);
377 }
378
379 return null;
380 }
381
382 /**
383 * Notify that this element has been changed.
384 *
385 * @param index
386 * the index of the element
387 */
388 public void fireElementChanged(int index) {
389 if (index >= 0) {
390 fireContentsChanged(this, index, index);
391 }
392 }
393
394 /**
395 * Notify that this element has been changed.
396 *
397 * @param element
398 * the element
399 */
400 public void fireElementChanged(T element) {
401 int index = indexOf(element);
402 if (index >= 0) {
403 fireContentsChanged(this, index, index);
404 }
405 }
406
407 @SuppressWarnings("unchecked") // JList<?> not compatible Java 1.6
408 @Override
409 public T get(int index) {
410 return (T) super.get(index);
411 }
412
413 /**
414 * Generate a {@link ListCellRenderer} that supports {@link Hoverable}
415 * elements.
416 *
417 * @param <T>
418 * the type of elements and items (the same type), which should
419 * implement {@link Hoverable} (it will not cause issues if not,
420 * but then, it will be a default renderer)
421 * @param model
422 * the model to use
423 *
424 * @return a suitable, {@link Hoverable} compatible renderer
425 */
426 static public <T extends Component> ListCellRenderer6<T> generateRenderer(
427 final ListModel<T> model) {
428 return new ListCellRenderer6<T>() {
429 @Override
430 public Component getListCellRendererComponent(JList6<T> list,
431 T item, int index, boolean isSelected,
432 boolean cellHasFocus) {
433 if (item instanceof Hoverable) {
434 Hoverable hoverable = (Hoverable) item;
435 hoverable.setSelected(isSelected);
436 hoverable.setHovered(model.isHovered(index));
437 }
438
439 return item;
440 }
441 };
442 }
443 }