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;
}
};
}
}