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