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.reader.ui.GuiReaderSearchByPanel.Waitable; 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 */ // JCombobox not 1.6 compatible @SuppressWarnings({ "unchecked", "rawtypes" }) public class GuiReaderSearchByTagPanel extends JPanel { private static final long serialVersionUID = 1L; private BasicSearchable searchable; private Waitable waitable; 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(Waitable waitable) { setLayout(new BorderLayout()); this.waitable = waitable; combos = new ArrayList(); page = 0; 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. *

* This operation can be long and should be run outside the UI thread. *

* Can be NULL, but no searches will work. * * @param searchable * the new searchable */ public void setSearchable(BasicSearchable searchable) { this.searchable = searchable; page = 0; maxPage = -1; storyItem = 0; stories = new ArrayList(); updateTags(null); } /** * The currently displayed page of result for the current search (see the * page parameter of * {@link GuiReaderSearchByTagPanel#searchTag(SupportType, int, int, SearchableTag)} * ). * * @return the currently displayed page of results */ public int getPage() { return page; } /** * The number of pages of result for the current search (see the * page parameter of * {@link GuiReaderSearchByPanel#searchTag(SupportType, int, int, SearchableTag)} * ). *

* For an unknown number or when not applicable, -1 is returned. * * @return the number of pages of results or -1 */ public int getMaxPage() { return maxPage; } /** * Return the tag used for the current search. * * @return the tag (which can be NULL, for "base tags") */ public SearchableTag getCurrentTag() { return currentTag; } /** * The currently loaded stories (the result of the latest search). * * @return the stories */ public List getStories() { return stories; } /** * Return the currently selected story (the item) if it was * specified in the latest, or 0 if not. *

* Note: this is thus a 1-based index, not a 0-based index. * * @return the item */ public int getStoryItem() { return storyItem; } /** * Update the tags displayed on screen and reset the tags bar. *

* This operation can be long and should be run outside the UI thread. * * @param tag * the tag to use, or 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(); } }); } /** * Add a tags bar (do not remove possible previous ones). *

* Will always add an "empty" (NULL) tag as first option. * * @param tags * the tags to display * @param selected * the selected tag if any, or NULL for none */ 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); } /** * The action to do on {@link JComboBox} selection. *

* The content of the action is: *

    *
  • Remove all tags bar below this one
  • *
  • Load the subtags if any in anew tags bar
  • *
  • Load the related stories if the tag was a leaf tag and notify the * {@link Waitable} (via {@link Waitable#fireEvent()})
  • *
* * @param comboIndex * the index of the related {@link JComboBox} * * @return the action */ 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() { waitable.setWaiting(true); try { 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); currentTag = tag; } catch (IOException e) { GuiReaderSearchFrame.error(e); page = 0; maxPage = -1; stories = new ArrayList(); } waitable.fireEvent(); } } finally { waitable.setWaiting(false); } } }).start(); } }; } /** * Get the children of the given tag (or the base tags if the given tag is * NULL). *

* This action will "fill" ({@link BasicSearchable#fillTag(SearchableTag)}) * the given tag if needed first. *

* This operation can be long and should be run outside the UI thread. * * @param tag * the tag to search into or NULL for the base tags * @return the children */ 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; } /** * Search for the given tag on the currently selected searchable. *

* If the tag contains children tags, those will be displayed so you can * select them; if the tag is a leaf tag, the linked stories will be * displayed. *

* This operation can be long and should be run outside the UI thread. * * @param tag * the tag to search for, or NULL for base tags * @param page * the page of results to load * @param item * the item to select (or 0 for none by default) * * @throw IndexOutOfBoundsException if the page is out of bounds */ 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); } } }