From: Niki Roo Date: Fri, 1 May 2020 09:10:54 +0000 (+0200) Subject: Merge commit '8b2627ce767579eb616e262b3f45f810a88ec200' X-Git-Url: http://git.nikiroo.be/?a=commitdiff_plain;h=63d02b928bb47510e2c8525631c52f3face1c36f;hp=f77ae043232d01b794a8857d5cad50ae35208477;p=fanfix.git Merge commit '8b2627ce767579eb616e262b3f45f810a88ec200' --- diff --git a/Progress.java b/Progress.java index dea6be3..748d4a6 100644 --- a/Progress.java +++ b/Progress.java @@ -9,6 +9,16 @@ import java.util.Map.Entry; /** * Progress reporting system, possibly nested. + *

+ * A {@link Progress} can have a name, and that name will be reported through + * the event system (it will report the first non-null name in the stack from + * the {@link Progress} from which the event originated to the parent the event + * is listened on). + *

+ * The {@link Progress} also has a table of keys/values shared amongst all the + * hierarchy (note that when adding a {@link Progress} to others, its values + * will be prioritized if some with the same keys were already present in the + * hierarchy). * * @author niki */ @@ -35,6 +45,7 @@ public class Progress { public void progress(Progress progress, String name); } + private Map map = new HashMap(); private Progress parent = null; private Object lock = new Object(); private String name; @@ -425,9 +436,60 @@ public class Progress { }; synchronized (lock) { + // Should not happen but just in case + if (this.map != progress.map) { + this.map.putAll(progress.map); + } + progress.map = this.map; progress.parent = this; this.children.put(progress, weight); progress.addProgressListener(progressListener); } } + + /** + * Set the given value for the given key on this {@link Progress} and it's + * children. + * + * @param key + * the key + * @param value + * the value + */ + public void put(Object key, Object value) { + map.put(key, value); + } + + /** + * Return the value associated with this key as a {@link String} if any, + * NULL if not. + *

+ * If the value is not NULL but not a {@link String}, it will be converted + * via {@link Object#toString()}. + * + * @param key + * the key to check + * + * @return the value or NULL + */ + public String getString(Object key) { + Object value = map.get(key); + if (value == null) { + return null; + } + + return value.toString(); + } + + /** + * Return the value associated with this key if any, NULL if not. + * + * @param key + * the key to check + * + * @return the value or NULL + */ + public Object get(Object key) { + return map.get(key); + } } diff --git a/ui/BreadCrumbsBar.java b/ui/BreadCrumbsBar.java index 8b993e6..a0e205c 100644 --- a/ui/BreadCrumbsBar.java +++ b/ui/BreadCrumbsBar.java @@ -3,7 +3,6 @@ package be.nikiroo.utils.ui; import java.awt.BorderLayout; import java.awt.Dimension; -import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; diff --git a/ui/DataNode.java b/ui/DataNode.java index 2d3ac26..b4dbe7b 100644 --- a/ui/DataNode.java +++ b/ui/DataNode.java @@ -78,8 +78,14 @@ public class DataNode { return userData; } - protected int count() { - int s = 0; + /** + * The total number of nodes present in this {@link DataNode} (including + * itself and descendants). + * + * @return the number + */ + public int count() { + int s = 1; for (DataNode child : children) { s += child.count(); } diff --git a/ui/DataTree.java b/ui/DataTree.java index e941a71..6b3657d 100644 --- a/ui/DataTree.java +++ b/ui/DataTree.java @@ -10,7 +10,7 @@ import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.MutableTreeNode; public abstract class DataTree { - private DataNode data; + protected DataNode data; public DataNode loadData() throws IOException { return this.data = extractData(); @@ -65,24 +65,4 @@ public abstract class DataTree { return new DataNode(children, source.getUserData()); } - - // TODO: not in this class: - - public void loadInto(DefaultMutableTreeNode root, String filter) { - DataNode filtered = getRoot(filter); - for (DataNode child : filtered.getChildren()) { - root.add(nodeToNode(child)); - } - } - - private MutableTreeNode nodeToNode(DataNode node) { - // TODO: node.toString - DefaultMutableTreeNode otherNode = new DefaultMutableTreeNode( - node.toString()); - for (DataNode child : node.getChildren()) { - otherNode.add(nodeToNode(child)); - } - - return otherNode; - } } diff --git a/ui/DelayWorker.java b/ui/DelayWorker.java index 3618b89..2a16c98 100644 --- a/ui/DelayWorker.java +++ b/ui/DelayWorker.java @@ -201,7 +201,7 @@ public class DelayWorker { * @param worker * the process to delay */ - public void delay(final String id, final SwingWorker worker) { + public void delay(String id, SwingWorker worker) { synchronized (lazyEnCoursLock) { lazyEnCours.put(id, worker); } diff --git a/ui/ListModel.java b/ui/ListModel.java index 06e7914..cf16d5f 100644 --- a/ui/ListModel.java +++ b/ui/ListModel.java @@ -2,6 +2,7 @@ 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; @@ -11,6 +12,7 @@ import java.util.List; import javax.swing.JList; import javax.swing.JPopupMenu; import javax.swing.ListCellRenderer; +import javax.swing.SwingWorker; import be.nikiroo.utils.compat.DefaultListModel6; import be.nikiroo.utils.compat.JList6; @@ -78,32 +80,66 @@ public class ListModel extends DefaultListModel6 { 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 JList6 list; + private boolean keepSelection = true; + + 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 JList} we will handle the data of (cannot be NULL) + * the {@link JList6} we will handle the data of (cannot be NULL) */ - @SuppressWarnings({ "unchecked", "rawtypes" }) // not compatible Java 1.6 - public ListModel(JList list) { - this((JList6) list); + @SuppressWarnings("rawtypes") // JList not compatible Java 1.6 + public ListModel(JList6 list) { + this((JList) list); } /** * Create a new {@link ListModel}. * * @param list - * the {@link JList} we will handle the data of (cannot be NULL) + * the {@link JList6} we will handle the data of (cannot be NULL) * @param popup * the popup to use and keep track of (can be NULL) */ - @SuppressWarnings({ "unchecked", "rawtypes" }) // not compatible Java 1.6 - public ListModel(final JList list, final JPopupMenu popup) { - this((JList6) list, popup); + @SuppressWarnings("rawtypes") // JList not compatible Java 1.6 + public ListModel(JList6 list, JPopupMenu popup) { + this((JList) list, popup); } /** @@ -111,9 +147,13 @@ public class ListModel extends DefaultListModel6 { * * @param list * the {@link JList6} we will handle the data of (cannot be NULL) + * @param tooltipCreator + * use this if you want the list to display tooltips on hover + * (can be NULL) */ - public ListModel(JList6 list) { - this(list, null); + @SuppressWarnings("rawtypes") // JList not compatible Java 1.6 + public ListModel(JList6 list, TooltipCreator tooltipCreator) { + this((JList) list, null, tooltipCreator); } /** @@ -123,24 +163,142 @@ public class ListModel extends DefaultListModel6 { * the {@link JList6} we will handle the data of (cannot be NULL) * @param popup * the popup to use and keep track of (can be NULL) + * @param tooltipCreator + * use this if you want the list to display tooltips on hover + * (can be NULL) + */ + @SuppressWarnings("rawtypes") // JList not compatible Java 1.6 + public ListModel(JList6 list, JPopupMenu popup, + TooltipCreator tooltipCreator) { + this((JList) list, popup, tooltipCreator); + } + + /** + * 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("rawtypes") // JList not compatible Java 1.6 + public ListModel(JList list) { + this(list, null, null); + } + + /** + * 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, JPopupMenu)} 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}) + * @param popup + * the popup to use and keep track of (can be NULL) */ - public ListModel(final JList6 list, final JPopupMenu popup) { + @SuppressWarnings("rawtypes") // JList not in Java 1.6 + public ListModel(JList list, JPopupMenu popup) { + this(list, popup, null); + } + + /** + * 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, JPopupMenu)} 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}) + * @param tooltipCreator + * use this if you want the list to display tooltips on hover + * (can be NULL) + */ + @SuppressWarnings("rawtypes") // JList not in Java 1.6 + public ListModel(JList list, TooltipCreator tooltipCreator) { + this(list, null, tooltipCreator); + } + + /** + * 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, JPopupMenu)} 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}) + * @param popup + * the popup to use and keep track of (can be NULL) + * @param tooltipCreator + * use this if you want the list to display tooltips on hover + * (can be NULL) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) // JList not in Java 1.6 + public ListModel(final JList list, final JPopupMenu popup, + TooltipCreator tooltipCreator) { this.list = list; + this.tooltipCreator = tooltipCreator; + list.setModel(this); + final DelayWorker tooltipWatcher = new DelayWorker(500); + if (tooltipCreator != null) { + tooltipWatcher.start(); + } + list.addMouseMotionListener(new MouseAdapter() { @Override - public void mouseMoved(MouseEvent me) { + public void mouseMoved(final MouseEvent me) { if (popup != null && popup.isShowing()) return; Point p = new Point(me.getX(), me.getY()); - int index = list.locationToIndex(p); + final int index = list.locationToIndex(p); if (index != hoveredIndex) { int oldIndex = hoveredIndex; hoveredIndex = index; fireElementChanged(oldIndex); fireElementChanged(index); + + if (ListModel.this.tooltipCreator != null) { + tooltipWatcher.delay("tooltip", + new SwingWorker() { + @Override + protected Void doInBackground() + throws Exception { + return null; + } + + @Override + protected void done() { + Window oldTooltip = tooltip; + tooltip = null; + if (oldTooltip != null) { + oldTooltip.setVisible(false); + } + + if (index < 0 + || index != hoveredIndex) { + return; + } + + tooltip = newTooltip(index, me); + } + }); + } } } }); @@ -182,9 +340,35 @@ public class ListModel extends DefaultListModel6 { 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; + } + /** * Check if this element is currently under the mouse. * @@ -287,8 +471,13 @@ public class ListModel extends DefaultListModel6 { * the filter will be copied as an element (can be NULL, in that * case all items will be copied as elements) */ - @SuppressWarnings("unchecked") // ListModel and JList are not java 1.6 + @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)) { @@ -296,6 +485,9 @@ public class ListModel extends DefaultListModel6 { } } + if (keepSelection) + snapshot.apply(); + list.repaint(); } @@ -354,12 +546,37 @@ public class ListModel extends DefaultListModel6 { } } - @SuppressWarnings("unchecked") // ListModel and JList are not java 1.6 + @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); + promotedTooltip.setLocation(newTooltip.getLocation()); + newTooltip.setVisible(false); + promotedTooltip.setVisible(true); + } + }); + newTooltip.setLocation(me.getXOnScreen(), me.getYOnScreen()); + + newTooltip.setVisible(true); + } + + return newTooltip; + } + /** * Generate a {@link ListCellRenderer} that supports {@link Hoverable} * elements. diff --git a/ui/ListSnapshot.java b/ui/ListSnapshot.java new file mode 100644 index 0000000..d2e89c8 --- /dev/null +++ b/ui/ListSnapshot.java @@ -0,0 +1,62 @@ +package be.nikiroo.utils.ui; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JList; + +public class ListSnapshot { + private JList list; + private List elements = new ArrayList(); + + public ListSnapshot(JList list) { + this.list = list; + + for (int index : list.getSelectedIndices()) { + elements.add(list.getModel().getElementAt(index)); + } + } + + public void apply() { + applyTo(list); + } + + public void applyTo(JList list) { + List indices = new ArrayList(); + for (int i = 0; i < list.getModel().getSize(); i++) { + Object newObject = list.getModel().getElementAt(i); + for (Object oldObject : elements) { + if (isSameElement(oldObject, newObject)) { + indices.add(i); + break; + } + } + } + + int a[] = new int[indices.size()]; + for (int i = 0; i < indices.size(); i++) { + a[i] = indices.get(i); + } + list.setSelectedIndices(a); + } + + // You can override this + protected boolean isSameElement(Object oldElement, Object newElement) { + if (oldElement == null || newElement == null) + return oldElement == null && newElement == null; + + return oldElement.toString().equals(newElement.toString()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("List Snapshot of: ").append(list).append("\n"); + builder.append("Selected elements:\n"); + for (Object element : elements) { + builder.append("\t").append(element).append("\n"); + } + + return builder.toString(); + } +} diff --git a/ui/ListenerItem.java b/ui/ListenerItem.java new file mode 100644 index 0000000..3fa41c8 --- /dev/null +++ b/ui/ListenerItem.java @@ -0,0 +1,53 @@ +package be.nikiroo.utils.ui; + +import java.awt.event.ActionListener; + +/** + * The default {@link ActionListener} add/remove/fire methods. + * + * @author niki + */ +public interface ListenerItem { + /** + * Check that this {@link ListenerItem} currently has + * {@link ActionListener}s that listen on it. + * + * @return TRUE if it has + */ + public boolean hasListeners(); + + /** + * Check how many events are currently waiting for an + * {@link ActionListener}. + * + * @return the number of waiting events (can be 0) + */ + public int getWaitingEventCount(); + + /** + * Adds the specified action listener to receive action events from this + * {@link ListenerItem}. + * + * @param listener + * the action listener to be added + */ + public void addActionListener(ActionListener listener); + + /** + * Removes the specified action listener so that it no longer receives + * action events from this {@link ListenerItem}. + * + * @param listener + * the action listener to be removed + */ + public void removeActionListener(ActionListener listener); + + /** + * Notify the listeners of an action. + * + * @param listenerCommand + * A string that may specify a command (possibly one of several) + * associated with the event + */ + public void fireActionPerformed(String listenerCommand); +} diff --git a/ui/ListenerPanel.java b/ui/ListenerPanel.java index 144cdd2..ada0796 100644 --- a/ui/ListenerPanel.java +++ b/ui/ListenerPanel.java @@ -17,7 +17,7 @@ import javax.swing.JPanel; * * @author niki */ -public class ListenerPanel extends JPanel { +public class ListenerPanel extends JPanel implements ListenerItem { private static final long serialVersionUID = 1L; /** Waiting queue until at least one listener is here to get the events. */ @@ -30,33 +30,17 @@ public class ListenerPanel extends JPanel { waitingQueue = new LinkedList(); } - /** - * Check that this {@link ListenerPanel} currently has - * {@link ActionListener}s that listen on it. - * - * @return TRUE if it has - */ + @Override public synchronized boolean hasListeners() { return listenerList.getListenerList().length > 1; } - /** - * Check how many events are currently waiting for an - * {@link ActionListener}. - * - * @return the number of waiting events (can be 0) - */ + @Override public synchronized int getWaitingEventCount() { return waitingQueue.size(); } - /** - * Adds the specified action listener to receive action events from this - * {@link ListenerPanel}. - * - * @param listener - * the action listener to be added - */ + @Override public synchronized void addActionListener(ActionListener listener) { if (!hasListeners()) { while (!waitingQueue.isEmpty()) { @@ -67,25 +51,13 @@ public class ListenerPanel extends JPanel { listenerList.add(ActionListener.class, listener); } - /** - * Removes the specified action listener so that it no longer receives - * action events from this {@link ListenerPanel}. - * - * @param listener - * the action listener to be removed - */ + @Override public synchronized void removeActionListener(ActionListener listener) { listenerList.remove(ActionListener.class, listener); } - /** - * Notify the listeners of an action. - * - * @param listenerCommand - * A string that may specify a command (possibly one of several) - * associated with the event - */ - protected synchronized void fireActionPerformed(String listenerCommand) { + @Override + public synchronized void fireActionPerformed(String listenerCommand) { ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, listenerCommand); diff --git a/ui/UIUtils.java b/ui/UIUtils.java index 9f16aab..5861d00 100644 --- a/ui/UIUtils.java +++ b/ui/UIUtils.java @@ -178,12 +178,39 @@ public class UIUtils { * @return the {@link JScrollPane} */ static public JScrollPane scroll(JComponent pane, boolean allowHorizontal) { + return scroll(pane, allowHorizontal, true); + } + + /** + * Add a {@link JScrollPane} around the given panel and use a sensible (for + * me) increment for the mouse wheel. + * + * @param pane + * the panel to wrap in a {@link JScrollPane} + * @param allowHorizontal + * allow horizontal scrolling (not always desired) + * @param allowVertical + * allow vertical scrolling (usually yes, but sometimes you only + * want horizontal) + * + * @return the {@link JScrollPane} + */ + static public JScrollPane scroll(JComponent pane, boolean allowHorizontal, + boolean allowVertical) { JScrollPane scroll = new JScrollPane(pane); + scroll.getVerticalScrollBar().setUnitIncrement(16); + scroll.getHorizontalScrollBar().setUnitIncrement(16); + if (!allowHorizontal) { scroll.setHorizontalScrollBarPolicy( JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); } + if (!allowVertical) { + scroll.setVerticalScrollBarPolicy( + JScrollPane.VERTICAL_SCROLLBAR_NEVER); + } + return scroll; } }