ListSnapshot (used on ListModel)
[fanfix.git] / 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 JList6<T> list;
84 private boolean keepSelection = true;
85
86 /**
87 * Create a new {@link ListModel}.
88 *
89 * @param list
90 * the {@link JList} we will handle the data of (cannot be NULL)
91 */
92 @SuppressWarnings({ "unchecked", "rawtypes" }) // not compatible Java 1.6
93 public ListModel(JList list) {
94 this((JList6<T>) list);
95 }
96
97 /**
98 * Create a new {@link ListModel}.
99 *
100 * @param list
101 * the {@link JList} we will handle the data of (cannot be NULL)
102 * @param popup
103 * the popup to use and keep track of (can be NULL)
104 */
105 @SuppressWarnings({ "unchecked", "rawtypes" }) // not compatible Java 1.6
106 public ListModel(final JList list, final JPopupMenu popup) {
107 this((JList6<T>) list, popup);
108 }
109
110 /**
111 * Create a new {@link ListModel}.
112 *
113 * @param list
114 * the {@link JList6} we will handle the data of (cannot be NULL)
115 */
116 public ListModel(JList6<T> list) {
117 this(list, null);
118 }
119
120 /**
121 * Create a new {@link ListModel}.
122 *
123 * @param list
124 * the {@link JList6} we will handle the data of (cannot be NULL)
125 * @param popup
126 * the popup to use and keep track of (can be NULL)
127 */
128 public ListModel(final JList6<T> list, final JPopupMenu popup) {
129 this.list = list;
130 list.setModel(this);
131
132 list.addMouseMotionListener(new MouseAdapter() {
133 @Override
134 public void mouseMoved(MouseEvent me) {
135 if (popup != null && popup.isShowing())
136 return;
137
138 Point p = new Point(me.getX(), me.getY());
139 int index = list.locationToIndex(p);
140 if (index != hoveredIndex) {
141 int oldIndex = hoveredIndex;
142 hoveredIndex = index;
143 fireElementChanged(oldIndex);
144 fireElementChanged(index);
145 }
146 }
147 });
148
149 list.addMouseListener(new MouseAdapter() {
150 @Override
151 public void mousePressed(MouseEvent e) {
152 check(e);
153 }
154
155 @Override
156 public void mouseReleased(MouseEvent e) {
157 check(e);
158 }
159
160 @Override
161 public void mouseExited(MouseEvent e) {
162 if (popup != null && popup.isShowing())
163 return;
164
165 if (hoveredIndex > -1) {
166 int oldIndex = hoveredIndex;
167 hoveredIndex = -1;
168 fireElementChanged(oldIndex);
169 }
170 }
171
172 private void check(MouseEvent e) {
173 if (popup == null) {
174 return;
175 }
176
177 if (e.isPopupTrigger()) {
178 if (list.getSelectedIndices().length <= 1) {
179 list.setSelectedIndex(
180 list.locationToIndex(e.getPoint()));
181 }
182
183 popup.show(list, e.getX(), e.getY());
184 }
185 }
186 });
187 }
188
189 /**
190 * (Try and) keep the elements that were selected when filtering.
191 * <p>
192 * This will use toString on the elements to identify them, and can be a bit
193 * resource intensive.
194 *
195 * @return TRUE if we do
196 */
197 public boolean isKeepSelection() {
198 return keepSelection;
199 }
200
201 /**
202 * (Try and) keep the elements that were selected when filtering.
203 * <p>
204 * This will use toString on the elements to identify them, and can be a bit
205 * resource intensive.
206 *
207 * @param keepSelection
208 * TRUE to try and keep them selected
209 */
210 public void setKeepSelection(boolean keepSelection) {
211 this.keepSelection = keepSelection;
212 }
213
214 /**
215 * Check if this element is currently under the mouse.
216 *
217 * @param element
218 * the element to check
219 *
220 * @return TRUE if it is
221 */
222 public boolean isHovered(T element) {
223 return indexOf(element) == hoveredIndex;
224 }
225
226 /**
227 * Check if this element is currently under the mouse.
228 *
229 * @param index
230 * the index of the element to check
231 *
232 * @return TRUE if it is
233 */
234 public boolean isHovered(int index) {
235 return index == hoveredIndex;
236 }
237
238 /**
239 * Add an item to the model.
240 *
241 * @param item
242 * the new item to add
243 */
244 public void addItem(T item) {
245 items.add(item);
246 }
247
248 /**
249 * Add items to the model.
250 *
251 * @param items
252 * the new items to add
253 */
254 public void addAllItems(Collection<T> items) {
255 this.items.addAll(items);
256 }
257
258 /**
259 * Removes the first occurrence of the specified element from this list, if
260 * it is present (optional operation).
261 *
262 * @param item
263 * the item to remove if possible (can be NULL)
264 *
265 * @return TRUE if one element was removed, FALSE if not found
266 */
267 public boolean removeItem(T item) {
268 return items.remove(item);
269 }
270
271 /**
272 * Remove the items that pass the given filter (or all items if the filter
273 * is NULL).
274 *
275 * @param filter
276 * the filter (if the filter returns TRUE, the item will be
277 * removed)
278 *
279 * @return TRUE if at least one item was removed
280 */
281 public boolean removeItemIf(Predicate<T> filter) {
282 boolean changed = false;
283 if (filter == null) {
284 changed = !items.isEmpty();
285 clearItems();
286 } else {
287 for (int i = 0; i < items.size(); i++) {
288 if (filter.test(items.get(i))) {
289 items.remove(i--);
290 changed = true;
291 }
292 }
293 }
294
295 return changed;
296 }
297
298 /**
299 * Removes all the items from this model.
300 */
301 public void clearItems() {
302 items.clear();
303 }
304
305 /**
306 * Filter the current elements.
307 * <p>
308 * This method will clear all the elements then look into all the items:
309 * those that pass the given filter will be copied as elements.
310 *
311 * @param filter
312 * the filter to select which elements to keep; an item that pass
313 * the filter will be copied as an element (can be NULL, in that
314 * case all items will be copied as elements)
315 */
316 @SuppressWarnings("unchecked") // ListModel<T> and JList<T> are not java 1.6
317 public void filter(Predicate<T> filter) {
318 ListSnapshot snapshot = null;
319
320 if (keepSelection)
321 snapshot = new ListSnapshot(list);
322
323 clear();
324 for (T item : items) {
325 if (filter == null || filter.test(item)) {
326 addElement(item);
327 }
328 }
329
330 if (keepSelection)
331 snapshot.apply();
332
333 list.repaint();
334 }
335
336 /**
337 * Return the currently selected elements.
338 *
339 * @return the selected elements
340 */
341 public List<T> getSelectedElements() {
342 List<T> selected = new ArrayList<T>();
343 for (int index : list.getSelectedIndices()) {
344 selected.add(get(index));
345 }
346
347 return selected;
348 }
349
350 /**
351 * Return the selected element if <b>one</b> and <b>only one</b> element is
352 * selected. I.E., if zero, two or more elements are selected, NULL will be
353 * returned.
354 *
355 * @return the element if it is the only selected element, NULL otherwise
356 */
357 public T getUniqueSelectedElement() {
358 List<T> selected = getSelectedElements();
359 if (selected.size() == 1) {
360 return selected.get(0);
361 }
362
363 return null;
364 }
365
366 /**
367 * Notify that this element has been changed.
368 *
369 * @param index
370 * the index of the element
371 */
372 public void fireElementChanged(int index) {
373 if (index >= 0) {
374 fireContentsChanged(this, index, index);
375 }
376 }
377
378 /**
379 * Notify that this element has been changed.
380 *
381 * @param element
382 * the element
383 */
384 public void fireElementChanged(T element) {
385 int index = indexOf(element);
386 if (index >= 0) {
387 fireContentsChanged(this, index, index);
388 }
389 }
390
391 @SuppressWarnings("unchecked") // ListModel<T> and JList<T> are not java 1.6
392 @Override
393 public T get(int index) {
394 return (T) super.get(index);
395 }
396
397 /**
398 * Generate a {@link ListCellRenderer} that supports {@link Hoverable}
399 * elements.
400 *
401 * @param <T>
402 * the type of elements and items (the same type), which should
403 * implement {@link Hoverable} (it will not cause issues if not,
404 * but then, it will be a default renderer)
405 * @param model
406 * the model to use
407 *
408 * @return a suitable, {@link Hoverable} compatible renderer
409 */
410 static public <T extends Component> ListCellRenderer6<T> generateRenderer(
411 final ListModel<T> model) {
412 return new ListCellRenderer6<T>() {
413 @Override
414 public Component getListCellRendererComponent(JList6<T> list,
415 T item, int index, boolean isSelected,
416 boolean cellHasFocus) {
417 if (item instanceof Hoverable) {
418 Hoverable hoverable = (Hoverable) item;
419 hoverable.setSelected(isSelected);
420 hoverable.setHovered(model.isHovered(index));
421 }
422
423 return item;
424 }
425 };
426 }
427 }