From 6628131da5f2c899b3981d1410a2e08bd3f12795 Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Thu, 18 Apr 2019 22:21:06 +0200 Subject: [PATCH] GUI search: forgot 2 filesgit add src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchBy*! --- .../reader/ui/GuiReaderSearchByPanel.java | 230 +++++++++++ .../reader/ui/GuiReaderSearchByTagPanel.java | 356 ++++++++++++++++++ 2 files changed, 586 insertions(+) create mode 100644 src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByPanel.java create mode 100644 src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByTagPanel.java diff --git a/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByPanel.java b/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByPanel.java new file mode 100644 index 0000000..4fcda5c --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByPanel.java @@ -0,0 +1,230 @@ +package be.nikiroo.fanfix.reader.ui; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.searchable.BasicSearchable; +import be.nikiroo.fanfix.searchable.SearchableTag; +import be.nikiroo.fanfix.supported.SupportType; + +/** + * This panel represents a search panel that works for keywords and tags based + * searches. + * + * @author niki + */ +public class GuiReaderSearchByPanel extends JPanel { + private static final long serialVersionUID = 1L; + + private int actionEventId = ActionEvent.ACTION_FIRST; + + private Waitable waitable; + + private boolean searchByTags; + + private JTabbedPane searchTabs; + private GuiReaderSearchByNamePanel byName; + private GuiReaderSearchByTagPanel byTag; + + private List actions = new ArrayList(); + + public interface Waitable { + public void setWaiting(boolean waiting); + } + + // will throw illegalArgEx if bad support type, NULL allowed + public GuiReaderSearchByPanel(final SupportType supportType, + Waitable waitable) { + setLayout(new BorderLayout()); + + this.waitable = waitable; + searchByTags = false; + + Runnable fireEvent = new Runnable() { + @Override + public void run() { + fireAction(); + } + }; + + byName = new GuiReaderSearchByNamePanel(fireEvent); + byTag = new GuiReaderSearchByTagPanel(fireEvent); + + searchTabs = new JTabbedPane(); + searchTabs.addTab("By name", byName); + searchTabs.addTab("By tags", byTag); + searchTabs.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + searchByTags = (searchTabs.getSelectedComponent() == byTag); + } + }); + + add(searchTabs, BorderLayout.CENTER); + updateSearchBy(searchByTags); + setSupportType(supportType); + } + + public void setSupportType(SupportType supportType) { + BasicSearchable searchable = BasicSearchable.getSearchable(supportType); + if (searchable == null && supportType != null) { + throw new java.lang.IllegalArgumentException( + "Unupported support type: " + supportType); + } + + byName.setSearchable(searchable); + byTag.setSearchable(searchable); + } + + public int getPage() { + if (!searchByTags) { + return byName.getPage(); + } + + return byTag.getPage(); + } + + public int getMaxPage() { + if (!searchByTags) { + return byName.getMaxPage(); + } + + return byTag.getMaxPage(); + } + + // throw outOfBounds if needed + public void setPage(int page) { + if (searchByTags) { + searchTag(byTag.getCurrentTag(), page, 0); + } else { + search(byName.getCurrentKeywords(), page, 0); + } + } + + // actions will be fired in UIthread + public void addActionListener(ActionListener action) { + actions.add(action); + } + + public boolean removeActionListener(ActionListener action) { + return actions.remove(action); + } + + public List getStories() { + if (!searchByTags) { + return byName.getStories(); + } + + return byTag.getStories(); + } + + // selected item or 0 if none ! one-based ! + public int getStoryItem() { + if (!searchByTags) { + return byName.getStoryItem(); + } + + return byTag.getStoryItem(); + } + + private void fireAction() { + GuiReaderSearchFrame.inUi(new Runnable() { + @Override + public void run() { + ActionEvent ae = new ActionEvent(GuiReaderSearchByPanel.this, + actionEventId, "stories found"); + + actionEventId++; + if (actionEventId > ActionEvent.ACTION_LAST) { + actionEventId = ActionEvent.ACTION_FIRST; + } + + for (ActionListener action : actions) { + try { + action.actionPerformed(ae); + } catch (Exception e) { + GuiReaderSearchFrame.error(e); + } + } + } + }); + } + + private void updateSearchBy(final boolean byTag) { + GuiReaderSearchFrame.inUi(new Runnable() { + @Override + public void run() { + if (!byTag) { + searchTabs.setSelectedIndex(0); + } else { + searchTabs.setSelectedIndex(1); + } + } + }); + } + + // slow, start in UI mode + public void search(final String keywords, final int page, final int item) { + waitable.setWaiting(true); + updateSearchBy(false); + new Thread(new Runnable() { + @Override + public void run() { + try { + byName.search(keywords, page, item); + fireAction(); + } finally { + waitable.setWaiting(false); + } + } + }).start(); + } + + // slow, start in UI mode + // tag: null = base tags + public void searchTag(final SearchableTag tag, final int page, + final int item) { + waitable.setWaiting(true); + updateSearchBy(true); + new Thread(new Runnable() { + @Override + public void run() { + try { + + byTag.searchTag(tag, page, item); + fireAction(); + } finally { + waitable.setWaiting(false); + } + } + }).start(); + } + + /** + * Enables or disables this component, depending on the value of the + * parameter b. An enabled component can respond to user input + * and generate events. Components are enabled initially by default. + *

+ * Disabling this component will also affect its children. + * + * @param b + * If true, this component is enabled; otherwise + * this component is disabled + */ + @Override + public void setEnabled(boolean b) { + super.setEnabled(b); + searchTabs.setEnabled(b); + byName.setEnabled(b); + byTag.setEnabled(b); + } +} diff --git a/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByTagPanel.java b/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByTagPanel.java new file mode 100644 index 0000000..aa00b8b --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/ui/GuiReaderSearchByTagPanel.java @@ -0,0 +1,356 @@ +package be.nikiroo.fanfix.reader.ui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.BoxLayout; +import javax.swing.JComboBox; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.ListCellRenderer; + +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.searchable.BasicSearchable; +import be.nikiroo.fanfix.searchable.SearchableTag; + +/** + * This panel represents a search panel that works for keywords and tags based + * searches. + * + * @author niki + */ +// JCombobox not 1.6 compatible +@SuppressWarnings({ "unchecked", "rawtypes" }) +public class GuiReaderSearchByTagPanel extends JPanel { + private static final long serialVersionUID = 1L; + + private BasicSearchable searchable; + private Runnable fireEvent; + + private SearchableTag currentTag; + private JPanel tagBars; + private List combos; + + private int page; + private int maxPage; + private List stories = new ArrayList(); + private int storyItem; + + public GuiReaderSearchByTagPanel(Runnable fireEvent) { + setLayout(new BorderLayout()); + + this.fireEvent = fireEvent; + combos = new ArrayList(); + page = 1; + maxPage = -1; + + tagBars = new JPanel(); + tagBars.setLayout(new BoxLayout(tagBars, BoxLayout.Y_AXIS)); + add(tagBars, BorderLayout.NORTH); + } + + /** + * The {@link BasicSearchable} object use for the searches themselves. + *

+ * Can be NULL, but no searches will work. + * + * @param searchable + * the new searchable + */ + public void setSearchable(BasicSearchable searchable) { + this.searchable = searchable; + page = 1; + maxPage = -1; + storyItem = 0; + stories = new ArrayList(); + updateTags(null); + } + + public int getPage() { + return page; + } + + public int getMaxPage() { + return maxPage; + } + + public SearchableTag getCurrentTag() { + return currentTag; + } + + public List getStories() { + return stories; + } + + // selected item or 0 if none ! one-based ! + public int getStoryItem() { + return storyItem; + } + + // update and reset the tagsbar + // can be NULL, for base tags + private void updateTags(final SearchableTag tag) { + final List parents = new ArrayList(); + SearchableTag parent = (tag == null) ? null : tag; + while (parent != null) { + parents.add(parent); + parent = parent.getParent(); + } + + List rootTags = new ArrayList(); + SearchableTag selectedRootTag = null; + selectedRootTag = parents.isEmpty() ? null : parents + .get(parents.size() - 1); + + if (searchable != null) { + try { + rootTags = searchable.getTags(); + } catch (IOException e) { + GuiReaderSearchFrame.error(e); + } + } + + final List rootTagsF = rootTags; + final SearchableTag selectedRootTagF = selectedRootTag; + + GuiReaderSearchFrame.inUi(new Runnable() { + @Override + public void run() { + tagBars.invalidate(); + tagBars.removeAll(); + + addTagBar(rootTagsF, selectedRootTagF); + + for (int i = parents.size() - 1; i >= 0; i--) { + SearchableTag selectedChild = null; + if (i > 0) { + selectedChild = parents.get(i - 1); + } + + SearchableTag parent = parents.get(i); + addTagBar(parent.getChildren(), selectedChild); + } + + tagBars.validate(); + } + }); + } + + // must be quick and no thread change + private void addTagBar(List tags, + final SearchableTag selected) { + tags.add(0, null); + + final int comboIndex = combos.size(); + + final JComboBox combo = new JComboBox( + tags.toArray(new SearchableTag[] {})); + combo.setSelectedItem(selected); + + final ListCellRenderer basic = combo.getRenderer(); + + combo.setRenderer(new ListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, + Object value, int index, boolean isSelected, + boolean cellHasFocus) { + + Object displayValue = value; + if (value instanceof SearchableTag) { + displayValue = ((SearchableTag) value).getName(); + } else { + displayValue = "Select a tag..."; + cellHasFocus = false; + isSelected = false; + } + + Component rep = basic.getListCellRendererComponent(list, + displayValue, index, isSelected, cellHasFocus); + + if (value == null) { + rep.setForeground(Color.GRAY); + } + + return rep; + } + }); + + combo.addActionListener(createComboTagAction(comboIndex)); + + combos.add(combo); + tagBars.add(combo); + } + + private ActionListener createComboTagAction(final int comboIndex) { + return new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + List combos = GuiReaderSearchByTagPanel.this.combos; + if (combos == null || comboIndex < 0 + || comboIndex >= combos.size()) { + return; + } + + // Tag can be NULL + final SearchableTag tag = (SearchableTag) combos + .get(comboIndex).getSelectedItem(); + + while (comboIndex + 1 < combos.size()) { + JComboBox combo = combos.remove(comboIndex + 1); + tagBars.remove(combo); + } + + new Thread(new Runnable() { + @Override + public void run() { + final List children = getChildrenForTag(tag); + if (children != null) { + GuiReaderSearchFrame.inUi(new Runnable() { + @Override + public void run() { + addTagBar(children, tag); + } + }); + } + + if (tag != null && tag.isLeaf()) { + storyItem = 0; + try { + searchable.fillTag(tag); + page = 1; + stories = searchable.search(tag, 1); + maxPage = searchable.searchPages(tag); + } catch (IOException e) { + GuiReaderSearchFrame.error(e); + page = 0; + maxPage = -1; + stories = new ArrayList(); + } + + fireEvent.run(); + } + } + }).start(); + } + }; + } + + // sync, add children of tag, NULL = base tags + // return children of the tag or base tags or NULL + private List getChildrenForTag(final SearchableTag tag) { + List children = new ArrayList(); + if (tag == null) { + try { + List baseTags = searchable.getTags(); + children = baseTags; + } catch (IOException e) { + GuiReaderSearchFrame.error(e); + } + } else { + try { + searchable.fillTag(tag); + } catch (IOException e) { + GuiReaderSearchFrame.error(e); + } + + if (!tag.isLeaf()) { + children = tag.getChildren(); + } else { + children = null; + } + } + + return children; + } + + // slow + // tag: null = base tags + // throw if page > max, but only if stories + public void searchTag(SearchableTag tag, int page, int item) { + List stories = new ArrayList(); + int storyItem = 0; + + currentTag = tag; + updateTags(tag); + + int maxPage = -1; + if (tag != null) { + try { + searchable.fillTag(tag); + + if (!tag.isLeaf()) { + List subtags = tag.getChildren(); + if (item > 0 && item <= subtags.size()) { + SearchableTag subtag = subtags.get(item - 1); + try { + tag = subtag; + searchable.fillTag(tag); + } catch (IOException e) { + GuiReaderSearchFrame.error(e); + } + } else if (item > 0) { + GuiReaderSearchFrame.error(String.format( + "Tag item does not exist: Tag [%s], item %d", + tag.getFqName(), item)); + } + } + + maxPage = searchable.searchPages(tag); + if (page > 0 && tag.isLeaf()) { + if (maxPage >= 0 && (page <= 0 || page > maxPage)) { + throw new IndexOutOfBoundsException("Page " + page + + " out of " + maxPage); + } + + try { + stories = searchable.search(tag, page); + if (item > 0 && item <= stories.size()) { + storyItem = item; + } else if (item > 0) { + GuiReaderSearchFrame + .error(String + .format("Story item does not exist: Tag [%s], item %d", + tag.getFqName(), item)); + } + } catch (IOException e) { + GuiReaderSearchFrame.error(e); + } + } + } catch (IOException e) { + GuiReaderSearchFrame.error(e); + maxPage = 0; + } + } + + this.stories = stories; + this.storyItem = storyItem; + this.page = page; + this.maxPage = maxPage; + } + + /** + * Enables or disables this component, depending on the value of the + * parameter b. An enabled component can respond to user input + * and generate events. Components are enabled initially by default. + *

+ * Disabling this component will also affect its children. + * + * @param b + * If true, this component is enabled; otherwise + * this component is disabled + */ + @Override + public void setEnabled(boolean b) { + super.setEnabled(b); + tagBars.setEnabled(b); + for (JComboBox combo : combos) { + combo.setEnabled(b); + } + } +} -- 2.27.0