1 package be
.nikiroo
.fanfix
.reader
.ui
;
3 import java
.awt
.BorderLayout
;
5 import java
.awt
.Component
;
6 import java
.awt
.event
.ActionEvent
;
7 import java
.awt
.event
.ActionListener
;
8 import java
.io
.IOException
;
9 import java
.util
.ArrayList
;
10 import java
.util
.List
;
12 import javax
.swing
.BoxLayout
;
13 import javax
.swing
.JComboBox
;
14 import javax
.swing
.JList
;
15 import javax
.swing
.JPanel
;
16 import javax
.swing
.ListCellRenderer
;
18 import be
.nikiroo
.fanfix
.data
.MetaData
;
19 import be
.nikiroo
.fanfix
.reader
.ui
.GuiReaderSearchByPanel
.Waitable
;
20 import be
.nikiroo
.fanfix
.searchable
.BasicSearchable
;
21 import be
.nikiroo
.fanfix
.searchable
.SearchableTag
;
22 import be
.nikiroo
.fanfix
.supported
.SupportType
;
25 * This panel represents a search panel that works for keywords and tags based
30 // JCombobox<E> not 1.6 compatible
31 @SuppressWarnings({ "unchecked", "rawtypes" })
32 public class GuiReaderSearchByTagPanel
extends JPanel
{
33 private static final long serialVersionUID
= 1L;
35 private BasicSearchable searchable
;
36 private Waitable waitable
;
38 private SearchableTag currentTag
;
39 private JPanel tagBars
;
40 private List
<JComboBox
> combos
;
44 private List
<MetaData
> stories
= new ArrayList
<MetaData
>();
45 private int storyItem
;
47 public GuiReaderSearchByTagPanel(Waitable waitable
) {
48 setLayout(new BorderLayout());
50 this.waitable
= waitable
;
51 combos
= new ArrayList
<JComboBox
>();
55 tagBars
= new JPanel();
56 tagBars
.setLayout(new BoxLayout(tagBars
, BoxLayout
.Y_AXIS
));
57 add(tagBars
, BorderLayout
.NORTH
);
61 * The {@link BasicSearchable} object use for the searches themselves.
63 * This operation can be long and should be run outside the UI thread.
65 * Can be NULL, but no searches will work.
70 public void setSearchable(BasicSearchable searchable
) {
71 this.searchable
= searchable
;
75 stories
= new ArrayList
<MetaData
>();
80 * The currently displayed page of result for the current search (see the
81 * <tt>page</tt> parameter of
82 * {@link GuiReaderSearchByTagPanel#searchTag(SupportType, int, int, SearchableTag)}
85 * @return the currently displayed page of results
87 public int getPage() {
92 * The number of pages of result for the current search (see the
93 * <tt>page</tt> parameter of
94 * {@link GuiReaderSearchByPanel#searchTag(SupportType, int, int, SearchableTag)}
97 * For an unknown number or when not applicable, -1 is returned.
99 * @return the number of pages of results or -1
101 public int getMaxPage() {
106 * Return the tag used for the current search.
108 * @return the tag (which can be NULL, for "base tags")
110 public SearchableTag
getCurrentTag() {
115 * The currently loaded stories (the result of the latest search).
117 * @return the stories
119 public List
<MetaData
> getStories() {
124 * Return the currently selected story (the <tt>item</tt>) if it was
125 * specified in the latest, or 0 if not.
127 * Note: this is thus a 1-based index, <b>not</b> a 0-based index.
131 public int getStoryItem() {
136 * Update the tags displayed on screen and reset the tags bar.
138 * This operation can be long and should be run outside the UI thread.
141 * the tag to use, or NULL for base tags
143 private void updateTags(final SearchableTag tag
) {
144 final List
<SearchableTag
> parents
= new ArrayList
<SearchableTag
>();
145 SearchableTag parent
= (tag
== null) ?
null : tag
;
146 while (parent
!= null) {
148 parent
= parent
.getParent();
151 List
<SearchableTag
> rootTags
= new ArrayList
<SearchableTag
>();
152 SearchableTag selectedRootTag
= null;
153 selectedRootTag
= parents
.isEmpty() ?
null : parents
154 .get(parents
.size() - 1);
156 if (searchable
!= null) {
158 rootTags
= searchable
.getTags();
159 } catch (IOException e
) {
160 GuiReaderSearchFrame
.error(e
);
164 final List
<SearchableTag
> rootTagsF
= rootTags
;
165 final SearchableTag selectedRootTagF
= selectedRootTag
;
167 GuiReaderSearchFrame
.inUi(new Runnable() {
170 tagBars
.invalidate();
173 addTagBar(rootTagsF
, selectedRootTagF
);
175 for (int i
= parents
.size() - 1; i
>= 0; i
--) {
176 SearchableTag selectedChild
= null;
178 selectedChild
= parents
.get(i
- 1);
181 SearchableTag parent
= parents
.get(i
);
182 addTagBar(parent
.getChildren(), selectedChild
);
191 * Add a tags bar (do not remove possible previous ones).
193 * Will always add an "empty" (NULL) tag as first option.
196 * the tags to display
198 * the selected tag if any, or NULL for none
200 private void addTagBar(List
<SearchableTag
> tags
,
201 final SearchableTag selected
) {
204 final int comboIndex
= combos
.size();
206 final JComboBox combo
= new JComboBox(
207 tags
.toArray(new SearchableTag
[] {}));
208 combo
.setSelectedItem(selected
);
210 final ListCellRenderer basic
= combo
.getRenderer();
212 combo
.setRenderer(new ListCellRenderer() {
214 public Component
getListCellRendererComponent(JList list
,
215 Object value
, int index
, boolean isSelected
,
216 boolean cellHasFocus
) {
218 Object displayValue
= value
;
219 if (value
instanceof SearchableTag
) {
220 displayValue
= ((SearchableTag
) value
).getName();
222 displayValue
= "Select a tag...";
223 cellHasFocus
= false;
227 Component rep
= basic
.getListCellRendererComponent(list
,
228 displayValue
, index
, isSelected
, cellHasFocus
);
231 rep
.setForeground(Color
.GRAY
);
238 combo
.addActionListener(createComboTagAction(comboIndex
));
245 * The action to do on {@link JComboBox} selection.
247 * The content of the action is:
249 * <li>Remove all tags bar below this one</li>
250 * <li>Load the subtags if any in anew tags bar</li>
251 * <li>Load the related stories if the tag was a leaf tag and notify the
252 * {@link Waitable} (via {@link Waitable#fireEvent()})</li>
256 * the index of the related {@link JComboBox}
260 private ActionListener
createComboTagAction(final int comboIndex
) {
261 return new ActionListener() {
263 public void actionPerformed(ActionEvent ae
) {
264 List
<JComboBox
> combos
= GuiReaderSearchByTagPanel
.this.combos
;
265 if (combos
== null || comboIndex
< 0
266 || comboIndex
>= combos
.size()) {
271 final SearchableTag tag
= (SearchableTag
) combos
272 .get(comboIndex
).getSelectedItem();
274 while (comboIndex
+ 1 < combos
.size()) {
275 JComboBox combo
= combos
.remove(comboIndex
+ 1);
276 tagBars
.remove(combo
);
279 new Thread(new Runnable() {
282 waitable
.setWaiting(true);
284 final List
<SearchableTag
> children
= getChildrenForTag(tag
);
285 if (children
!= null) {
286 GuiReaderSearchFrame
.inUi(new Runnable() {
289 addTagBar(children
, tag
);
294 if (tag
!= null && tag
.isLeaf()) {
297 searchable
.fillTag(tag
);
299 stories
= searchable
.search(tag
, 1);
300 maxPage
= searchable
.searchPages(tag
);
302 } catch (IOException e
) {
303 GuiReaderSearchFrame
.error(e
);
306 stories
= new ArrayList
<MetaData
>();
309 waitable
.fireEvent();
312 waitable
.setWaiting(false);
321 * Get the children of the given tag (or the base tags if the given tag is
324 * This action will "fill" ({@link BasicSearchable#fillTag(SearchableTag)})
325 * the given tag if needed first.
327 * This operation can be long and should be run outside the UI thread.
330 * the tag to search into or NULL for the base tags
331 * @return the children
333 private List
<SearchableTag
> getChildrenForTag(final SearchableTag tag
) {
334 List
<SearchableTag
> children
= new ArrayList
<SearchableTag
>();
337 List
<SearchableTag
> baseTags
= searchable
.getTags();
339 } catch (IOException e
) {
340 GuiReaderSearchFrame
.error(e
);
344 searchable
.fillTag(tag
);
345 } catch (IOException e
) {
346 GuiReaderSearchFrame
.error(e
);
350 children
= tag
.getChildren();
360 * Search for the given tag on the currently selected searchable.
362 * If the tag contains children tags, those will be displayed so you can
363 * select them; if the tag is a leaf tag, the linked stories will be
366 * This operation can be long and should be run outside the UI thread.
369 * the tag to search for, or NULL for base tags
371 * the page of results to load
373 * the item to select (or 0 for none by default)
375 * @throw IndexOutOfBoundsException if the page is out of bounds
377 public void searchTag(SearchableTag tag
, int page
, int item
) {
378 List
<MetaData
> stories
= new ArrayList
<MetaData
>();
387 searchable
.fillTag(tag
);
390 List
<SearchableTag
> subtags
= tag
.getChildren();
391 if (item
> 0 && item
<= subtags
.size()) {
392 SearchableTag subtag
= subtags
.get(item
- 1);
395 searchable
.fillTag(tag
);
396 } catch (IOException e
) {
397 GuiReaderSearchFrame
.error(e
);
399 } else if (item
> 0) {
400 GuiReaderSearchFrame
.error(String
.format(
401 "Tag item does not exist: Tag [%s], item %d",
402 tag
.getFqName(), item
));
406 maxPage
= searchable
.searchPages(tag
);
407 if (page
> 0 && tag
.isLeaf()) {
408 if (maxPage
>= 0 && (page
<= 0 || page
> maxPage
)) {
409 throw new IndexOutOfBoundsException("Page " + page
410 + " out of " + maxPage
);
414 stories
= searchable
.search(tag
, page
);
415 if (item
> 0 && item
<= stories
.size()) {
417 } else if (item
> 0) {
420 .format("Story item does not exist: Tag [%s], item %d",
421 tag
.getFqName(), item
));
423 } catch (IOException e
) {
424 GuiReaderSearchFrame
.error(e
);
427 } catch (IOException e
) {
428 GuiReaderSearchFrame
.error(e
);
433 this.stories
= stories
;
434 this.storyItem
= storyItem
;
436 this.maxPage
= maxPage
;
440 * Enables or disables this component, depending on the value of the
441 * parameter <code>b</code>. An enabled component can respond to user input
442 * and generate events. Components are enabled initially by default.
444 * Disabling this component will also affect its children.
447 * If <code>true</code>, this component is enabled; otherwise
448 * this component is disabled
451 public void setEnabled(boolean b
) {
453 tagBars
.setEnabled(b
);
454 for (JComboBox combo
: combos
) {