Merge branch 'subtree'
[nikiroo-utils.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderSearchByTagPanel.java
CommitLineData
6628131d
NR
1package be.nikiroo.fanfix.reader.ui;
2
3import java.awt.BorderLayout;
4import java.awt.Color;
5import java.awt.Component;
6import java.awt.event.ActionEvent;
7import java.awt.event.ActionListener;
8import java.io.IOException;
9import java.util.ArrayList;
10import java.util.List;
11
12import javax.swing.BoxLayout;
13import javax.swing.JComboBox;
14import javax.swing.JList;
15import javax.swing.JPanel;
16import javax.swing.ListCellRenderer;
17
18import be.nikiroo.fanfix.data.MetaData;
dc3b0033 19import be.nikiroo.fanfix.reader.ui.GuiReaderSearchByPanel.Waitable;
6628131d
NR
20import be.nikiroo.fanfix.searchable.BasicSearchable;
21import be.nikiroo.fanfix.searchable.SearchableTag;
dc3b0033 22import be.nikiroo.fanfix.supported.SupportType;
6628131d
NR
23
24/**
25 * This panel represents a search panel that works for keywords and tags based
26 * searches.
27 *
28 * @author niki
29 */
30// JCombobox<E> not 1.6 compatible
31@SuppressWarnings({ "unchecked", "rawtypes" })
32public class GuiReaderSearchByTagPanel extends JPanel {
33 private static final long serialVersionUID = 1L;
34
35 private BasicSearchable searchable;
dc3b0033 36 private Waitable waitable;
6628131d
NR
37
38 private SearchableTag currentTag;
39 private JPanel tagBars;
40 private List<JComboBox> combos;
41
42 private int page;
43 private int maxPage;
44 private List<MetaData> stories = new ArrayList<MetaData>();
45 private int storyItem;
46
dc3b0033 47 public GuiReaderSearchByTagPanel(Waitable waitable) {
6628131d
NR
48 setLayout(new BorderLayout());
49
dc3b0033 50 this.waitable = waitable;
6628131d 51 combos = new ArrayList<JComboBox>();
cf032e29 52 page = 0;
6628131d
NR
53 maxPage = -1;
54
55 tagBars = new JPanel();
56 tagBars.setLayout(new BoxLayout(tagBars, BoxLayout.Y_AXIS));
57 add(tagBars, BorderLayout.NORTH);
58 }
59
60 /**
61 * The {@link BasicSearchable} object use for the searches themselves.
62 * <p>
dc3b0033
NR
63 * This operation can be long and should be run outside the UI thread.
64 * <p>
6628131d
NR
65 * Can be NULL, but no searches will work.
66 *
67 * @param searchable
68 * the new searchable
69 */
70 public void setSearchable(BasicSearchable searchable) {
71 this.searchable = searchable;
cf032e29 72 page = 0;
6628131d
NR
73 maxPage = -1;
74 storyItem = 0;
75 stories = new ArrayList<MetaData>();
76 updateTags(null);
77 }
78
dc3b0033
NR
79 /**
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)}
83 * ).
84 *
85 * @return the currently displayed page of results
86 */
6628131d
NR
87 public int getPage() {
88 return page;
89 }
90
dc3b0033
NR
91 /**
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)}
95 * ).
96 * <p>
97 * For an unknown number or when not applicable, -1 is returned.
98 *
99 * @return the number of pages of results or -1
100 */
6628131d
NR
101 public int getMaxPage() {
102 return maxPage;
103 }
104
dc3b0033
NR
105 /**
106 * Return the tag used for the current search.
107 *
108 * @return the tag (which can be NULL, for "base tags")
109 */
6628131d
NR
110 public SearchableTag getCurrentTag() {
111 return currentTag;
112 }
113
dc3b0033
NR
114 /**
115 * The currently loaded stories (the result of the latest search).
116 *
117 * @return the stories
118 */
6628131d
NR
119 public List<MetaData> getStories() {
120 return stories;
121 }
122
dc3b0033
NR
123 /**
124 * Return the currently selected story (the <tt>item</tt>) if it was
125 * specified in the latest, or 0 if not.
126 * <p>
127 * Note: this is thus a 1-based index, <b>not</b> a 0-based index.
128 *
129 * @return the item
130 */
6628131d
NR
131 public int getStoryItem() {
132 return storyItem;
133 }
134
dc3b0033
NR
135 /**
136 * Update the tags displayed on screen and reset the tags bar.
137 * <p>
138 * This operation can be long and should be run outside the UI thread.
139 *
140 * @param tag
141 * the tag to use, or NULL for base tags
142 */
6628131d
NR
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) {
147 parents.add(parent);
148 parent = parent.getParent();
149 }
150
151 List<SearchableTag> rootTags = new ArrayList<SearchableTag>();
152 SearchableTag selectedRootTag = null;
153 selectedRootTag = parents.isEmpty() ? null : parents
154 .get(parents.size() - 1);
155
156 if (searchable != null) {
157 try {
158 rootTags = searchable.getTags();
159 } catch (IOException e) {
160 GuiReaderSearchFrame.error(e);
161 }
162 }
163
164 final List<SearchableTag> rootTagsF = rootTags;
165 final SearchableTag selectedRootTagF = selectedRootTag;
166
167 GuiReaderSearchFrame.inUi(new Runnable() {
168 @Override
169 public void run() {
170 tagBars.invalidate();
171 tagBars.removeAll();
172
173 addTagBar(rootTagsF, selectedRootTagF);
174
175 for (int i = parents.size() - 1; i >= 0; i--) {
176 SearchableTag selectedChild = null;
177 if (i > 0) {
178 selectedChild = parents.get(i - 1);
179 }
180
181 SearchableTag parent = parents.get(i);
182 addTagBar(parent.getChildren(), selectedChild);
183 }
184
185 tagBars.validate();
186 }
187 });
188 }
189
dc3b0033
NR
190 /**
191 * Add a tags bar (do not remove possible previous ones).
192 * <p>
193 * Will always add an "empty" (NULL) tag as first option.
194 *
195 * @param tags
196 * the tags to display
197 * @param selected
198 * the selected tag if any, or NULL for none
199 */
6628131d
NR
200 private void addTagBar(List<SearchableTag> tags,
201 final SearchableTag selected) {
202 tags.add(0, null);
203
204 final int comboIndex = combos.size();
205
206 final JComboBox combo = new JComboBox(
207 tags.toArray(new SearchableTag[] {}));
208 combo.setSelectedItem(selected);
209
210 final ListCellRenderer basic = combo.getRenderer();
211
212 combo.setRenderer(new ListCellRenderer() {
213 @Override
214 public Component getListCellRendererComponent(JList list,
215 Object value, int index, boolean isSelected,
216 boolean cellHasFocus) {
217
218 Object displayValue = value;
219 if (value instanceof SearchableTag) {
220 displayValue = ((SearchableTag) value).getName();
221 } else {
222 displayValue = "Select a tag...";
223 cellHasFocus = false;
224 isSelected = false;
225 }
226
227 Component rep = basic.getListCellRendererComponent(list,
228 displayValue, index, isSelected, cellHasFocus);
229
230 if (value == null) {
231 rep.setForeground(Color.GRAY);
232 }
233
234 return rep;
235 }
236 });
237
238 combo.addActionListener(createComboTagAction(comboIndex));
239
240 combos.add(combo);
241 tagBars.add(combo);
242 }
243
dc3b0033
NR
244 /**
245 * The action to do on {@link JComboBox} selection.
246 * <p>
247 * The content of the action is:
248 * <ul>
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>
253 * </ul>
254 *
255 * @param comboIndex
256 * the index of the related {@link JComboBox}
257 *
258 * @return the action
259 */
6628131d
NR
260 private ActionListener createComboTagAction(final int comboIndex) {
261 return new ActionListener() {
262 @Override
263 public void actionPerformed(ActionEvent ae) {
264 List<JComboBox> combos = GuiReaderSearchByTagPanel.this.combos;
265 if (combos == null || comboIndex < 0
266 || comboIndex >= combos.size()) {
267 return;
268 }
269
270 // Tag can be NULL
271 final SearchableTag tag = (SearchableTag) combos
272 .get(comboIndex).getSelectedItem();
273
274 while (comboIndex + 1 < combos.size()) {
275 JComboBox combo = combos.remove(comboIndex + 1);
276 tagBars.remove(combo);
277 }
278
279 new Thread(new Runnable() {
280 @Override
281 public void run() {
dc3b0033
NR
282 waitable.setWaiting(true);
283 try {
284 final List<SearchableTag> children = getChildrenForTag(tag);
285 if (children != null) {
286 GuiReaderSearchFrame.inUi(new Runnable() {
287 @Override
288 public void run() {
289 addTagBar(children, tag);
290 }
291 });
292 }
293
294 if (tag != null && tag.isLeaf()) {
295 storyItem = 0;
296 try {
297 searchable.fillTag(tag);
298 page = 1;
299 stories = searchable.search(tag, 1);
300 maxPage = searchable.searchPages(tag);
cf032e29 301 currentTag = tag;
dc3b0033
NR
302 } catch (IOException e) {
303 GuiReaderSearchFrame.error(e);
304 page = 0;
305 maxPage = -1;
306 stories = new ArrayList<MetaData>();
6628131d 307 }
6628131d 308
dc3b0033 309 waitable.fireEvent();
6628131d 310 }
dc3b0033
NR
311 } finally {
312 waitable.setWaiting(false);
6628131d
NR
313 }
314 }
315 }).start();
316 }
317 };
318 }
319
dc3b0033
NR
320 /**
321 * Get the children of the given tag (or the base tags if the given tag is
322 * NULL).
323 * <p>
324 * This action will "fill" ({@link BasicSearchable#fillTag(SearchableTag)})
325 * the given tag if needed first.
326 * <p>
327 * This operation can be long and should be run outside the UI thread.
328 *
329 * @param tag
330 * the tag to search into or NULL for the base tags
331 * @return the children
332 */
6628131d
NR
333 private List<SearchableTag> getChildrenForTag(final SearchableTag tag) {
334 List<SearchableTag> children = new ArrayList<SearchableTag>();
335 if (tag == null) {
336 try {
337 List<SearchableTag> baseTags = searchable.getTags();
338 children = baseTags;
339 } catch (IOException e) {
340 GuiReaderSearchFrame.error(e);
341 }
342 } else {
343 try {
344 searchable.fillTag(tag);
345 } catch (IOException e) {
346 GuiReaderSearchFrame.error(e);
347 }
348
349 if (!tag.isLeaf()) {
350 children = tag.getChildren();
351 } else {
352 children = null;
353 }
354 }
355
356 return children;
357 }
358
dc3b0033
NR
359 /**
360 * Search for the given tag on the currently selected searchable.
361 * <p>
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
364 * displayed.
365 * <p>
366 * This operation can be long and should be run outside the UI thread.
367 *
368 * @param tag
369 * the tag to search for, or NULL for base tags
370 * @param page
371 * the page of results to load
372 * @param item
373 * the item to select (or 0 for none by default)
374 *
375 * @throw IndexOutOfBoundsException if the page is out of bounds
376 */
6628131d
NR
377 public void searchTag(SearchableTag tag, int page, int item) {
378 List<MetaData> stories = new ArrayList<MetaData>();
379 int storyItem = 0;
380
381 currentTag = tag;
382 updateTags(tag);
383
384 int maxPage = -1;
385 if (tag != null) {
386 try {
387 searchable.fillTag(tag);
388
389 if (!tag.isLeaf()) {
390 List<SearchableTag> subtags = tag.getChildren();
391 if (item > 0 && item <= subtags.size()) {
392 SearchableTag subtag = subtags.get(item - 1);
393 try {
394 tag = subtag;
395 searchable.fillTag(tag);
396 } catch (IOException e) {
397 GuiReaderSearchFrame.error(e);
398 }
399 } else if (item > 0) {
400 GuiReaderSearchFrame.error(String.format(
401 "Tag item does not exist: Tag [%s], item %d",
402 tag.getFqName(), item));
403 }
404 }
405
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);
411 }
412
413 try {
414 stories = searchable.search(tag, page);
415 if (item > 0 && item <= stories.size()) {
416 storyItem = item;
417 } else if (item > 0) {
418 GuiReaderSearchFrame
419 .error(String
420 .format("Story item does not exist: Tag [%s], item %d",
421 tag.getFqName(), item));
422 }
423 } catch (IOException e) {
424 GuiReaderSearchFrame.error(e);
425 }
426 }
427 } catch (IOException e) {
428 GuiReaderSearchFrame.error(e);
429 maxPage = 0;
430 }
431 }
432
433 this.stories = stories;
434 this.storyItem = storyItem;
435 this.page = page;
436 this.maxPage = maxPage;
437 }
438
439 /**
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.
443 * <p>
444 * Disabling this component will also affect its children.
445 *
446 * @param b
447 * If <code>true</code>, this component is enabled; otherwise
448 * this component is disabled
449 */
450 @Override
451 public void setEnabled(boolean b) {
452 super.setEnabled(b);
453 tagBars.setEnabled(b);
454 for (JComboBox combo : combos) {
455 combo.setEnabled(b);
456 }
457 }
458}