package be.nikiroo.utils.ui; import java.awt.Component; import java.awt.Point; import java.awt.Window; 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.JList; import javax.swing.JPopupMenu; import javax.swing.ListCellRenderer; import javax.swing.SwingWorker; import be.nikiroo.utils.ui.compat.DefaultListModel6; import be.nikiroo.utils.ui.compat.JList6; import be.nikiroo.utils.ui.compat.ListCellRenderer6; /** * 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). *

* It also offers filter options, supports hovered changes and some more utility * functions. * * @author niki * * @param * the type of elements and items (the same type) */ public class ListModel extends DefaultListModel6 { private static final long serialVersionUID = 1L; /** How long to wait before displaying a tooltip, in milliseconds. */ private static final int DELAY_TOOLTIP_MS = 1000; /** * 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 * the type of elements and items (the same type) */ public interface Predicate { /** * 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); } /** * An interface required to support tooltips on this {@link ListModel}. * * @author niki * * @param * the type of elements and items (the same type) */ public interface TooltipCreator { /** * Generate a tooltip {@link Window} for this element. *

* Note that the tooltip can be of two modes: undecorated or standalone. * An undecorated tooltip will be taken care of by this * {@link ListModel}, but a standalone one is supposed to be its own * Dialog or Frame (it won't be automatically closed). * * @param t * the element to generate a tooltip for * @param undecorated * TRUE for undecorated tooltip, FALSE for standalone * tooltips * * @return the generated tooltip or NULL for none */ public Window generateTooltip(T t, boolean undecorated); } private int hoveredIndex; private List items = new ArrayList(); private boolean keepSelection = true; private DelayWorker tooltipWatcher; private JPopupMenu popup; private TooltipCreator tooltipCreator; private Window tooltip; @SuppressWarnings("rawtypes") // JList not compatible Java 1.6 private JList list; /** * Create a new {@link ListModel}. * * @param list * the {@link JList6} we will handle the data of (cannot be NULL) */ @SuppressWarnings("rawtypes") // JList not compatible Java 1.6 public ListModel(JList6 list) { this((JList) list); } /** * Create a new {@link ListModel}. *

* Note that you must take care of passing a {@link JList} that only handles * elements of the type of this {@link ListModel} -- you can also use * {@link ListModel#ListModel(JList6)} instead. * * @param list * the {@link JList} we will handle the data of (cannot be NULL, * must only contain elements of the type of this * {@link ListModel}) */ @SuppressWarnings({ "unchecked", "rawtypes" }) // JList not in Java 1.6 public ListModel(final JList list) { this.list = list; list.setModel(this); // We always have it ready tooltipWatcher = new DelayWorker(DELAY_TOOLTIP_MS); tooltipWatcher.start(); list.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(final MouseEvent me) { if (ListModel.this.popup != null && ListModel.this.popup.isShowing()) return; Point p = new Point(me.getX(), me.getY()); final int index = list.locationToIndex(p); if (index != hoveredIndex) { int oldIndex = hoveredIndex; hoveredIndex = index; fireElementChanged(oldIndex); fireElementChanged(index); if (ListModel.this.tooltipCreator != null) { showTooltip(null); tooltipWatcher.delay("tooltip", new SwingWorker() { @Override protected Void doInBackground() throws Exception { return null; } @Override protected void done() { showTooltip(null); if (index < 0 || index != hoveredIndex) { return; } if (ListModel.this.popup != null && ListModel.this.popup .isShowing()) { return; } showTooltip(newTooltip(index, me)); } }); } } } }); 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 (ListModel.this.popup != null && ListModel.this.popup.isShowing()) return; if (hoveredIndex > -1) { int oldIndex = hoveredIndex; hoveredIndex = -1; fireElementChanged(oldIndex); } } private void check(MouseEvent e) { if (ListModel.this.popup == null) { return; } if (e.isPopupTrigger()) { if (list.getSelectedIndices().length <= 1) { list.setSelectedIndex( list.locationToIndex(e.getPoint())); } showTooltip(null); ListModel.this.popup.show(list, e.getX(), e.getY()); } } }); } /** * (Try and) keep the elements that were selected when filtering. *

* This will use toString on the elements to identify them, and can be a bit * resource intensive. * * @return TRUE if we do */ public boolean isKeepSelection() { return keepSelection; } /** * (Try and) keep the elements that were selected when filtering. *

* This will use toString on the elements to identify them, and can be a bit * resource intensive. * * @param keepSelection * TRUE to try and keep them selected */ public void setKeepSelection(boolean keepSelection) { this.keepSelection = keepSelection; } /** * The popup to use and keep track of (can be NULL). * * @return the current popup */ public JPopupMenu getPopup() { return popup; } /** * The popup to use and keep track of (can be NULL). * * @param popup * the new popup */ public void setPopup(JPopupMenu popup) { this.popup = popup; } /** * You can use a {@link TooltipCreator} if you want the list to display * tooltips on mouse hover (can be NULL). * * @return the current {@link TooltipCreator} */ public TooltipCreator getTooltipCreator() { return tooltipCreator; } /** * You can use a {@link TooltipCreator} if you want the list to display * tooltips on mouse hover (can be NULL). * * @param tooltipCreator * the new {@link TooltipCreator} */ public void setTooltipCreator(TooltipCreator tooltipCreator) { this.tooltipCreator = tooltipCreator; } /** * 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 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 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. *

* 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) */ @SuppressWarnings("unchecked") // JList not compatible Java 1.6 public void filter(Predicate filter) { ListSnapshot snapshot = null; if (keepSelection) snapshot = new ListSnapshot(list); clear(); for (T item : items) { if (filter == null || filter.test(item)) { addElement(item); } } if (keepSelection) snapshot.apply(); list.repaint(); } /** * Return the currently selected elements. * * @return the selected elements */ public List getSelectedElements() { List selected = new ArrayList(); for (int index : list.getSelectedIndices()) { selected.add(get(index)); } return selected; } /** * Return the selected element if one and only one 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 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); } } @SuppressWarnings("unchecked") // JList not compatible Java 1.6 @Override public T get(int index) { return (T) super.get(index); } private Window newTooltip(final int index, final MouseEvent me) { final T value = ListModel.this.get(index); final Window newTooltip = tooltipCreator.generateTooltip(value, true); if (newTooltip != null) { newTooltip.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { Window promotedTooltip = tooltipCreator .generateTooltip(value, false); if (promotedTooltip != null) { promotedTooltip.setLocation(me.getXOnScreen(), me.getYOnScreen()); promotedTooltip.setVisible(true); } newTooltip.setVisible(false); } }); newTooltip.setLocation(me.getXOnScreen(), me.getYOnScreen()); showTooltip(newTooltip); } return newTooltip; } private void showTooltip(Window tooltip) { synchronized (tooltipWatcher) { if (this.tooltip != null) { this.tooltip.setVisible(false); this.tooltip.dispose(); } this.tooltip = tooltip; if (tooltip != null) { tooltip.setVisible(true); } } } /** * Generate a {@link ListCellRenderer} that supports {@link Hoverable} * elements. * * @param * 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 ListCellRenderer6 generateRenderer( final ListModel model) { return new ListCellRenderer6() { @Override public Component getListCellRendererComponent(JList6 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; } }; } }