From 02571725901ed23747ca563881d1b6608ae8db0d Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Sun, 19 Apr 2020 20:21:20 +0200 Subject: [PATCH] new ListModel --- .../nikiroo/fanfix_swing/gui/BooksPanel.java | 188 +++------ .../gui/importer/ImporterFrame.java | 107 +----- .../gui/importer/ImporterItem.java | 6 +- .../fanfix_swing/gui/utils/ListModel.java | 359 ++++++++++++++++++ 4 files changed, 444 insertions(+), 216 deletions(-) create mode 100644 src/be/nikiroo/fanfix_swing/gui/utils/ListModel.java diff --git a/src/be/nikiroo/fanfix_swing/gui/BooksPanel.java b/src/be/nikiroo/fanfix_swing/gui/BooksPanel.java index 3f4094bd..580cc70c 100644 --- a/src/be/nikiroo/fanfix_swing/gui/BooksPanel.java +++ b/src/be/nikiroo/fanfix_swing/gui/BooksPanel.java @@ -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 { - 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 bookInfos = new ArrayList(); private Map books = new HashMap(); private boolean seeWordCount; private boolean listMode; private JList list; - private int hoveredIndex = -1; - private ListModel data = new ListModel(); + private ListModel 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 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() { + @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 initList(boolean listMode) { - final JList list = new JList(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 getSelected() { - List selected = new ArrayList(); - for (int index : list.getSelectedIndices()) { - selected.add(data.get(index)); - } - - return selected; - } - - @Override - public BookInfo getUniqueSelected() { - List 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 initList() { + final JList list = new JList(); + data = new ListModel(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 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 generateRenderer() { @@ -306,7 +236,7 @@ public class BooksPanel extends ListenerPanel { } book.setSelected(isSelected); - book.setHovered(index == hoveredIndex); + book.setHovered(data.isHovered(index)); return book; } }; diff --git a/src/be/nikiroo/fanfix_swing/gui/importer/ImporterFrame.java b/src/be/nikiroo/fanfix_swing/gui/importer/ImporterFrame.java index b59215ba..c3c6059f 100644 --- a/src/be/nikiroo/fanfix_swing/gui/importer/ImporterFrame.java +++ b/src/be/nikiroo/fanfix_swing/gui/importer/ImporterFrame.java @@ -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 { - 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 list; - private ListModel data = new ListModel(); - private List items = new ArrayList(); + private ListModel data; private String filter = ""; - private int hoveredIndex = -1; public ImporterFrame() { setLayout(new BorderLayout()); - list = new JList(data); - this.add(list, BorderLayout.CENTER); + JList list = new JList(); + data = new ListModel(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() { + @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 generateRenderer() { - return new ListCellRenderer() { + data.filter(new Predicate() { @Override - public Component getListCellRendererComponent( - JList 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()); } - }; + }); } } diff --git a/src/be/nikiroo/fanfix_swing/gui/importer/ImporterItem.java b/src/be/nikiroo/fanfix_swing/gui/importer/ImporterItem.java index 11234631..62198ed9 100644 --- a/src/be/nikiroo/fanfix_swing/gui/importer/ImporterItem.java +++ b/src/be/nikiroo/fanfix_swing/gui/importer/ImporterItem.java @@ -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 index 00000000..81a8cecd --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/utils/ListModel.java @@ -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). + *

+ * 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 DefaultListModel { + 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 + * 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); + } + + private int hoveredIndex; + private List items = new ArrayList(); + private JList list; + + /** + * Create a new {@link ListModel}. + * + * @param list + * the {@link JList} we will handle the data of (cannot be NULL) + */ + public ListModel(JList 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 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 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) + */ + public void filter(Predicate 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 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); + } + } + + /** + * 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 ListCellRenderer generateRenderer( + final ListModel model) { + return new ListCellRenderer() { + @Override + public Component getListCellRendererComponent( + JList 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; + } + }; + } +} -- 2.27.0