new ListModel
authorNiki Roo <niki@nikiroo.be>
Sun, 19 Apr 2020 18:21:20 +0000 (20:21 +0200)
committerNiki Roo <niki@nikiroo.be>
Sun, 19 Apr 2020 18:21:20 +0000 (20:21 +0200)
src/be/nikiroo/fanfix_swing/gui/BooksPanel.java
src/be/nikiroo/fanfix_swing/gui/importer/ImporterFrame.java
src/be/nikiroo/fanfix_swing/gui/importer/ImporterItem.java
src/be/nikiroo/fanfix_swing/gui/utils/ListModel.java [new file with mode: 0644]

index 3f4094bd1803f6c3fb8714ab33956aeb4afa4b1b..580cc70c282bab1a43b3f83217394cf333ebf5d7 100644 (file)
@@ -4,7 +4,6 @@ import java.awt.BorderLayout;
 import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.Image;
-import java.awt.Point;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.MouseAdapter;
@@ -15,9 +14,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
 
-import javax.swing.DefaultListModel;
 import javax.swing.JList;
-import javax.swing.JPopupMenu;
 import javax.swing.ListCellRenderer;
 import javax.swing.ListSelectionModel;
 import javax.swing.SwingWorker;
@@ -30,36 +27,22 @@ import be.nikiroo.fanfix_swing.gui.book.BookBlock;
 import be.nikiroo.fanfix_swing.gui.book.BookInfo;
 import be.nikiroo.fanfix_swing.gui.book.BookLine;
 import be.nikiroo.fanfix_swing.gui.book.BookPopup;
+import be.nikiroo.fanfix_swing.gui.book.BookPopup.Informer;
 import be.nikiroo.fanfix_swing.gui.utils.DelayWorker;
+import be.nikiroo.fanfix_swing.gui.utils.ListModel;
+import be.nikiroo.fanfix_swing.gui.utils.ListModel.Predicate;
 import be.nikiroo.fanfix_swing.gui.utils.ListenerPanel;
 import be.nikiroo.fanfix_swing.gui.utils.UiHelper;
 
 public class BooksPanel extends ListenerPanel {
-       private class ListModel extends DefaultListModel<BookInfo> {
-               public void fireElementChanged(int index) {
-                       if (index >= 0) {
-                               fireContentsChanged(this, index, index);
-                       }
-               }
-
-               public void fireElementChanged(BookInfo element) {
-                       int index = indexOf(element);
-                       if (index >= 0) {
-                               fireContentsChanged(this, index, index);
-                       }
-               }
-       }
-
        static public final String INVALIDATE_CACHE = "invalidate_cache";
 
-       private List<BookInfo> bookInfos = new ArrayList<BookInfo>();
        private Map<BookInfo, BookLine> books = new HashMap<BookInfo, BookLine>();
        private boolean seeWordCount;
        private boolean listMode;
 
        private JList<BookInfo> list;
-       private int hoveredIndex = -1;
-       private ListModel data = new ListModel();
+       private ListModel<BookInfo> data;
        private DelayWorker bookCoverUpdater;
        private String filter = "";
 
@@ -79,7 +62,10 @@ public class BooksPanel extends ListenerPanel {
 
                bookCoverUpdater = new DelayWorker(20);
                bookCoverUpdater.start();
-               add(UiHelper.scroll(initList(listMode)), BorderLayout.CENTER);
+
+               list = initList();
+               setListMode(listMode);
+               add(UiHelper.scroll(list), BorderLayout.CENTER);
        }
 
        // null or empty -> all sources
@@ -115,23 +101,22 @@ public class BooksPanel extends ListenerPanel {
        }
 
        public void load(List<BookInfo> bookInfos) {
-               this.bookInfos.clear();
-               this.bookInfos.addAll(bookInfos);
+               data.clearItems();
+               data.addAllItems(bookInfos);
                bookCoverUpdater.clear();
 
                filter();
        }
 
        private void filter() {
-               data.clear();
-               for (BookInfo bookInfo : bookInfos) {
-                       if (bookInfo.getMainInfo() == null || filter.isEmpty()
-                                       || bookInfo.getMainInfo().toLowerCase()
-                                                       .contains(filter.toLowerCase())) {
-                               data.addElement(bookInfo);
+               data.filter(new Predicate<BookInfo>() {
+                       @Override
+                       public boolean test(BookInfo item) {
+                               return item.getMainInfo() == null || filter.isEmpty()
+                                               || item.getMainInfo().toLowerCase()
+                                                               .contains(filter.toLowerCase());
                        }
-               }
-               list.repaint();
+               });
        }
 
        /**
@@ -161,90 +146,12 @@ public class BooksPanel extends ListenerPanel {
                }
        }
 
-       private JList<BookInfo> initList(boolean listMode) {
-               final JList<BookInfo> list = new JList<BookInfo>(data);
-
-               final JPopupMenu popup = new BookPopup(
-                               Instance.getInstance().getLibrary(), new BookPopup.Informer() {
-                                       @Override
-                                       public void setCached(BookInfo book, boolean cached) {
-                                               book.setCached(cached);
-                                               fireElementChanged(book);
-                                       }
-                                       
-                                       @Override
-                                       public void fireElementChanged(BookInfo book) {
-                                               data.fireElementChanged(book);
-                                       }
-                                       
-                                       @Override
-                                       public void removeElement(BookInfo book) {
-                                               data.removeElement(book);
-                                       }
-
-                                       @Override
-                                       public List<BookInfo> getSelected() {
-                                               List<BookInfo> selected = new ArrayList<BookInfo>();
-                                               for (int index : list.getSelectedIndices()) {
-                                                       selected.add(data.get(index));
-                                               }
-
-                                               return selected;
-                                       }
-
-                                       @Override
-                                       public BookInfo getUniqueSelected() {
-                                               List<BookInfo> selected = getSelected();
-                                               if (selected.size() == 1) {
-                                                       return selected.get(0);
-                                               }
-                                               return null;
-                                       }
-
-                                       @Override
-                                       public void invalidateCache() {
-                                               // TODO: also reset the popup menu for sources/author
-                                               fireActionPerformed(INVALIDATE_CACHE);
-                                       }
-                               });
+       private JList<BookInfo> initList() {
+               final JList<BookInfo> list = new JList<BookInfo>();
+               data = new ListModel<BookInfo>(list, new BookPopup(
+                               Instance.getInstance().getLibrary(), initInformer()));
 
-               list.addMouseMotionListener(new MouseAdapter() {
-                       @Override
-                       public void mouseMoved(MouseEvent me) {
-                               if (popup.isShowing())
-                                       return;
-
-                               Point p = new Point(me.getX(), me.getY());
-                               int index = list.locationToIndex(p);
-                               if (index != hoveredIndex) {
-                                       hoveredIndex = index;
-                                       list.repaint();
-                               }
-                       }
-               });
                list.addMouseListener(new MouseAdapter() {
-                       @Override
-                       public void mousePressed(MouseEvent e) {
-                               check(e);
-                       }
-
-                       @Override
-                       public void mouseReleased(MouseEvent e) {
-                               check(e);
-                       }
-
-                       @Override
-                       public void mouseExited(MouseEvent e) {
-                               if (popup.isShowing())
-                                       return;
-
-                               if (hoveredIndex > -1) {
-                                       int index = hoveredIndex;
-                                       hoveredIndex = -1;
-                                       data.fireElementChanged(index);
-                               }
-                       }
-
                        @Override
                        public void mouseClicked(MouseEvent e) {
                                super.mouseClicked(e);
@@ -265,17 +172,6 @@ public class BooksPanel extends ListenerPanel {
                                                        });
                                }
                        }
-
-                       private void check(MouseEvent e) {
-                               if (e.isPopupTrigger()) {
-                                       if (list.getSelectedIndices().length <= 1) {
-                                               list.setSelectedIndex(
-                                                               list.locationToIndex(e.getPoint()));
-                                       }
-
-                                       popup.show(list, e.getX(), e.getY());
-                               }
-                       }
                });
 
                list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
@@ -283,9 +179,43 @@ public class BooksPanel extends ListenerPanel {
                list.setCellRenderer(generateRenderer());
                list.setVisibleRowCount(0);
 
-               this.list = list;
-               setListMode(listMode);
-               return this.list;
+               return list;
+       }
+
+       private Informer initInformer() {
+               return new BookPopup.Informer() {
+                       @Override
+                       public void setCached(BookInfo book, boolean cached) {
+                               book.setCached(cached);
+                               fireElementChanged(book);
+                       }
+
+                       @Override
+                       public void fireElementChanged(BookInfo book) {
+                               data.fireElementChanged(book);
+                       }
+
+                       @Override
+                       public void removeElement(BookInfo book) {
+                               data.removeElement(book);
+                       }
+
+                       @Override
+                       public List<BookInfo> getSelected() {
+                               return data.getSelectedElements();
+                       }
+
+                       @Override
+                       public BookInfo getUniqueSelected() {
+                               return data.getUniqueSelectedElement();
+                       }
+
+                       @Override
+                       public void invalidateCache() {
+                               // TODO: also reset the popup menu for sources/author
+                               fireActionPerformed(INVALIDATE_CACHE);
+                       }
+               };
        }
 
        private ListCellRenderer<BookInfo> generateRenderer() {
@@ -306,7 +236,7 @@ public class BooksPanel extends ListenerPanel {
                                }
 
                                book.setSelected(isSelected);
-                               book.setHovered(index == hoveredIndex);
+                               book.setHovered(data.isHovered(index));
                                return book;
                        }
                };
index b59215badb3feab06707d8e18896337b8c71816d..c3c6059f14c96511f268a927c77466879336f7aa 100644 (file)
@@ -1,29 +1,20 @@
 package be.nikiroo.fanfix_swing.gui.importer;
 
 import java.awt.BorderLayout;
-import java.awt.Component;
 import java.awt.Container;
-import java.awt.Point;
 import java.awt.Toolkit;
 import java.awt.datatransfer.DataFlavor;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
 import java.io.File;
 import java.net.URL;
-import java.util.ArrayList;
-import java.util.List;
 
-import javax.swing.DefaultListCellRenderer;
-import javax.swing.DefaultListModel;
 import javax.swing.JButton;
 import javax.swing.JFileChooser;
 import javax.swing.JFrame;
 import javax.swing.JList;
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
-import javax.swing.ListCellRenderer;
 import javax.swing.ListSelectionModel;
 
 import be.nikiroo.fanfix.Instance;
@@ -33,68 +24,26 @@ import be.nikiroo.fanfix.reader.BasicReader;
 import be.nikiroo.fanfix.supported.BasicSupport;
 import be.nikiroo.fanfix_swing.Actions;
 import be.nikiroo.fanfix_swing.gui.SearchBar;
-import be.nikiroo.fanfix_swing.gui.book.BookBlock;
-import be.nikiroo.fanfix_swing.gui.book.BookInfo;
-import be.nikiroo.fanfix_swing.gui.book.BookLine;
+import be.nikiroo.fanfix_swing.gui.utils.ListModel;
+import be.nikiroo.fanfix_swing.gui.utils.ListModel.Predicate;
 import be.nikiroo.utils.Progress;
-import be.nikiroo.utils.Progress.ProgressListener;
 
 public class ImporterFrame extends JFrame {
-       private class ListModel extends DefaultListModel<ImporterItem> {
-               public void fireElementChanged(int index) {
-                       if (index >= 0) {
-                               fireContentsChanged(this, index, index);
-                       }
-               }
-
-               public void fireElementChanged(ImporterItem element) {
-                       int index = indexOf(element);
-                       if (index >= 0) {
-                               fireContentsChanged(this, index, index);
-                       }
-               }
-       }
-
-       private JList<ImporterItem> list;
-       private ListModel data = new ListModel();
-       private List<ImporterItem> items = new ArrayList<ImporterItem>();
+       private ListModel<ImporterItem> data;
        private String filter = "";
-       private int hoveredIndex = -1;
 
        public ImporterFrame() {
                setLayout(new BorderLayout());
 
-               list = new JList<ImporterItem>(data);
-               this.add(list, BorderLayout.CENTER);
+               JList<ImporterItem> list = new JList<ImporterItem>();
+               data = new ListModel<ImporterItem>(list);
 
+               list.setCellRenderer(ListModel.generateRenderer(data));
                list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
                list.setSelectedIndex(0);
-               list.setCellRenderer(generateRenderer());
                list.setVisibleRowCount(5);
 
-               list.addMouseMotionListener(new MouseAdapter() {
-                       @Override
-                       public void mouseMoved(MouseEvent me) {
-                               Point p = new Point(me.getX(), me.getY());
-                               int index = list.locationToIndex(p);
-                               if (index != hoveredIndex) {
-                                       int oldIndex = hoveredIndex;
-                                       hoveredIndex = index;
-                                       data.fireElementChanged(oldIndex);
-                                       data.fireElementChanged(index);
-                               }
-                       }
-               });
-               list.addMouseListener(new MouseAdapter() {
-                       @Override
-                       public void mouseExited(MouseEvent e) {
-                               if (hoveredIndex > -1) {
-                                       int oldIndex = hoveredIndex;
-                                       hoveredIndex = -1;
-                                       data.fireElementChanged(oldIndex);
-                               }
-                       }
-               });
+               this.add(list, BorderLayout.CENTER);
 
                JPanel top = new JPanel();
                top.setLayout(new BorderLayout());
@@ -115,13 +64,13 @@ public class ImporterFrame extends JFrame {
                clear.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               boolean changed = false;
-                               for (int i = 0; i < items.size(); i++) {
-                                       if (items.get(i).isDone()) {
-                                               items.remove(i--);
-                                               changed = true;
-                                       }
-                               }
+                               boolean changed = data
+                                               .removeItemIf(new Predicate<ImporterItem>() {
+                                                       @Override
+                                                       public boolean test(ImporterItem item) {
+                                                               return item.isDone();
+                                                       }
+                                               });
 
                                if (changed) {
                                        filter();
@@ -222,32 +171,18 @@ public class ImporterFrame extends JFrame {
                        }
                });
 
-               items.add(item);
+               data.addItem(item);
                filter();
        }
 
        private void filter() {
-               data.clear();
-               for (ImporterItem item : items) {
-                       String text = item.getStoryName() + " " + item.getAction();
-                       if (filter.isEmpty() || text.isEmpty()
-                                       || text.toLowerCase().contains(filter.toLowerCase())) {
-                               data.addElement(item);
-                       }
-               }
-               list.repaint();
-       }
-
-       private ListCellRenderer<ImporterItem> generateRenderer() {
-               return new ListCellRenderer<ImporterItem>() {
+               data.filter(new Predicate<ImporterItem>() {
                        @Override
-                       public Component getListCellRendererComponent(
-                                       JList<? extends ImporterItem> list, ImporterItem item,
-                                       int index, boolean isSelected, boolean cellHasFocus) {
-                               item.setSelected(isSelected);
-                               item.setHovered(index == hoveredIndex);
-                               return item;
+                       public boolean test(ImporterItem item) {
+                               String text = item.getStoryName() + " " + item.getAction();
+                               return filter.isEmpty() || text.isEmpty()
+                                               || text.toLowerCase().contains(filter.toLowerCase());
                        }
-               };
+               });
        }
 }
index 1123463119aa810c4309b705cb910939f570b6a0..62198ed918db3061b3aabea78b95def852316344 100644 (file)
@@ -2,14 +2,18 @@ package be.nikiroo.fanfix_swing.gui.importer;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
+import java.awt.Component;
 import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.Rectangle;
 
 import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
 import javax.swing.SwingUtilities;
 
 import be.nikiroo.fanfix_swing.gui.utils.CoverImager;
+import be.nikiroo.fanfix_swing.gui.utils.ListModel;
 import be.nikiroo.fanfix_swing.gui.utils.ListenerPanel;
 import be.nikiroo.utils.Progress;
 import be.nikiroo.utils.Progress.ProgressListener;
@@ -131,7 +135,7 @@ public class ImporterItem extends ListenerPanel {
                                        progress = pg.getRelativeProgress();
                                        action = currentAction;
 
-                                       // The rest must be done in the UI thead
+                                       // The rest must be done in the UI thread
                                        SwingUtilities.invokeLater(new Runnable() {
                                                @Override
                                                public void run() {
diff --git a/src/be/nikiroo/fanfix_swing/gui/utils/ListModel.java b/src/be/nikiroo/fanfix_swing/gui/utils/ListModel.java
new file mode 100644 (file)
index 0000000..81a8cec
--- /dev/null
@@ -0,0 +1,359 @@
+package be.nikiroo.fanfix_swing.gui.utils;
+
+import java.awt.Component;
+import java.awt.Point;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.swing.DefaultListModel;
+import javax.swing.JList;
+import javax.swing.JPopupMenu;
+import javax.swing.ListCellRenderer;
+
+/**
+ * A {@link javax.swing.ListModel} that can maintain 2 lists; one with the
+ * actual data (the elements), and a second one with the items that are
+ * currently displayed (the items).
+ * <p>
+ * It also offers filter options, supports hovered changes and some more utility
+ * functions.
+ * 
+ * @author niki
+ *
+ * @param <T>
+ *            the type of elements and items (the same type)
+ */
+public class ListModel<T> extends DefaultListModel<T> {
+       private static final long serialVersionUID = 1L;
+
+       /**
+        * A filter interface, to check for a condition (note that a Predicate class
+        * already exists in Java 1.8+, and is compatible with this one if you
+        * change the signatures -- but I support java 1.6+).
+        * 
+        * @author niki
+        *
+        * @param <T>
+        *            the type of elements and items (the same type)
+        */
+       public interface Predicate<T> {
+               /**
+                * Check if an item or an element pass a filter.
+                * 
+                * @param item
+                *            the item to test
+                * 
+                * @return TRUE if the test passed, FALSE if not
+                */
+               public boolean test(T item);
+       }
+
+       /**
+        * A simple interface your elements must implement if you want to use
+        * {@link ListModel#generateRenderer(ListModel)}.
+        * 
+        * @author niki
+        */
+       public interface Hoverable {
+               /**
+                * The element is currently selected.
+                * 
+                * @param selected
+                *            TRUE for selected, FALSE for unselected
+                */
+               public void setSelected(boolean selected);
+
+               /**
+                * The element is currently under the mouse cursor.
+                * 
+                * @param hovered
+                *            TRUE if it is, FALSE if not
+                */
+               public void setHovered(boolean hovered);
+       }
+
+       private int hoveredIndex;
+       private List<T> items = new ArrayList<T>();
+       private JList<T> list;
+
+       /**
+        * Create a new {@link ListModel}.
+        * 
+        * @param list
+        *            the {@link JList} we will handle the data of (cannot be NULL)
+        */
+       public ListModel(JList<T> list) {
+               this(list, null);
+       }
+
+       /**
+        * Create a new {@link ListModel}.
+        * 
+        * @param list
+        *            the {@link JList} we will handle the data of (cannot be NULL)
+        * @param popup
+        *            the popup to use and keep track of (can be NULL)
+        */
+       public ListModel(final JList<T> list, final JPopupMenu popup) {
+               this.list = list;
+               list.setModel(this);
+
+               list.addMouseMotionListener(new MouseAdapter() {
+                       @Override
+                       public void mouseMoved(MouseEvent me) {
+                               if (popup != null && popup.isShowing())
+                                       return;
+
+                               Point p = new Point(me.getX(), me.getY());
+                               int index = list.locationToIndex(p);
+                               if (index != hoveredIndex) {
+                                       int oldIndex = hoveredIndex;
+                                       hoveredIndex = index;
+                                       fireElementChanged(oldIndex);
+                                       fireElementChanged(index);
+                               }
+                       }
+               });
+
+               list.addMouseListener(new MouseAdapter() {
+                       @Override
+                       public void mousePressed(MouseEvent e) {
+                               check(e);
+                       }
+
+                       @Override
+                       public void mouseReleased(MouseEvent e) {
+                               check(e);
+                       }
+
+                       @Override
+                       public void mouseExited(MouseEvent e) {
+                               if (popup != null && popup.isShowing())
+                                       return;
+
+                               if (hoveredIndex > -1) {
+                                       int oldIndex = hoveredIndex;
+                                       hoveredIndex = -1;
+                                       fireElementChanged(oldIndex);
+                               }
+                       }
+
+                       private void check(MouseEvent e) {
+                               if (popup == null) {
+                                       return;
+                               }
+
+                               if (e.isPopupTrigger()) {
+                                       if (list.getSelectedIndices().length <= 1) {
+                                               list.setSelectedIndex(
+                                                               list.locationToIndex(e.getPoint()));
+                                       }
+
+                                       popup.show(list, e.getX(), e.getY());
+                               }
+                       }
+               });
+       }
+
+       /**
+        * Check if this element is currently under the mouse.
+        * 
+        * @param element
+        *            the element to check
+        * 
+        * @return TRUE if it is
+        */
+       public boolean isHovered(T element) {
+               return indexOf(element) == hoveredIndex;
+       }
+
+       /**
+        * Check if this element is currently under the mouse.
+        * 
+        * @param index
+        *            the index of the element to check
+        * 
+        * @return TRUE if it is
+        */
+       public boolean isHovered(int index) {
+               return index == hoveredIndex;
+       }
+
+       /**
+        * Add an item to the model.
+        * 
+        * @param item
+        *            the new item to add
+        */
+       public void addItem(T item) {
+               items.add(item);
+       }
+
+       /**
+        * Add items to the model.
+        * 
+        * @param items
+        *            the new items to add
+        */
+       public void addAllItems(Collection<T> items) {
+               this.items.addAll(items);
+       }
+
+       /**
+        * Removes the first occurrence of the specified element from this list, if
+        * it is present (optional operation).
+        * 
+        * @param item
+        *            the item to remove if possible (can be NULL)
+        * 
+        * @return TRUE if one element was removed, FALSE if not found
+        */
+       public boolean removeItem(T item) {
+               return items.remove(item);
+       }
+
+       /**
+        * Remove the items that pass the given filter (or all items if the filter
+        * is NULL).
+        * 
+        * @param filter
+        *            the filter (if the filter returns TRUE, the item will be
+        *            removed)
+        * 
+        * @return TRUE if at least one item was removed
+        */
+       public boolean removeItemIf(Predicate<T> filter) {
+               boolean changed = false;
+               if (filter == null) {
+                       changed = !items.isEmpty();
+                       clearItems();
+               } else {
+                       for (int i = 0; i < items.size(); i++) {
+                               if (filter.test(items.get(i))) {
+                                       items.remove(i--);
+                                       changed = true;
+                               }
+                       }
+               }
+
+               return changed;
+       }
+
+       /**
+        * Removes all the items from this model.
+        */
+       public void clearItems() {
+               items.clear();
+       }
+
+       /**
+        * Filter the current elements.
+        * <p>
+        * This method will clear all the elements then look into all the items:
+        * those that pass the given filter will be copied as elements.
+        * 
+        * @param filter
+        *            the filter to select which elements to keep; an item that pass
+        *            the filter will be copied as an element (can be NULL, in that
+        *            case all items will be copied as elements)
+        */
+       public void filter(Predicate<T> filter) {
+               clear();
+               for (T item : items) {
+                       if (filter == null || filter.test(item)) {
+                               addElement(item);
+                       }
+               }
+
+               list.repaint();
+       }
+
+       /**
+        * Return the currently selected elements.
+        * 
+        * @return the selected elements
+        */
+       public List<T> getSelectedElements() {
+               List<T> selected = new ArrayList<T>();
+               for (int index : list.getSelectedIndices()) {
+                       selected.add(get(index));
+               }
+
+               return selected;
+       }
+
+       /**
+        * Return the selected element if <b>one</b> and <b>only one</b> element is
+        * selected. I.E., if zero, two or more elements are selected, NULL will be
+        * returned.
+        * 
+        * @return the element if it is the only selected element, NULL otherwise
+        */
+       public T getUniqueSelectedElement() {
+               List<T> selected = getSelectedElements();
+               if (selected.size() == 1) {
+                       return selected.get(0);
+               }
+
+               return null;
+       }
+
+       /**
+        * Notify that this element has been changed.
+        * 
+        * @param index
+        *            the index of the element
+        */
+       public void fireElementChanged(int index) {
+               if (index >= 0) {
+                       fireContentsChanged(this, index, index);
+               }
+       }
+
+       /**
+        * Notify that this element has been changed.
+        * 
+        * @param element
+        *            the element
+        */
+       public void fireElementChanged(T element) {
+               int index = indexOf(element);
+               if (index >= 0) {
+                       fireContentsChanged(this, index, index);
+               }
+       }
+
+       /**
+        * Generate a {@link ListCellRenderer} that supports {@link Hoverable}
+        * elements.
+        * 
+        * @param <T>
+        *            the type of elements and items (the same type), which should
+        *            implement {@link Hoverable} (it will not cause issues if not,
+        *            but then, it will be a default renderer)
+        * @param model
+        *            the model to use
+        * 
+        * @return a suitable, {@link Hoverable} compatible renderer
+        */
+       static public <T extends Component> ListCellRenderer<T> generateRenderer(
+                       final ListModel<T> model) {
+               return new ListCellRenderer<T>() {
+                       @Override
+                       public Component getListCellRendererComponent(
+                                       JList<? extends T> list, T item, int index,
+                                       boolean isSelected, boolean cellHasFocus) {
+                               if (item instanceof Hoverable) {
+                                       Hoverable hoverable = (Hoverable) item;
+                                       hoverable.setSelected(isSelected);
+                                       hoverable.setHovered(model.isHovered(index));
+                               }
+
+                               return item;
+                       }
+               };
+       }
+}