GUI search: code cleanup + jDoc
[fanfix.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
NR
51 combos = new ArrayList<JComboBox>();
52 page = 1;
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;
72 page = 1;
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);
301 } catch (IOException e) {
302 GuiReaderSearchFrame.error(e);
303 page = 0;
304 maxPage = -1;
305 stories = new ArrayList<MetaData>();
6628131d 306 }
6628131d 307
dc3b0033 308 waitable.fireEvent();
6628131d 309 }
dc3b0033
NR
310 } finally {
311 waitable.setWaiting(false);
6628131d
NR
312 }
313 }
314 }).start();
315 }
316 };
317 }
318
dc3b0033
NR
319 /**
320 * Get the children of the given tag (or the base tags if the given tag is
321 * NULL).
322 * <p>
323 * This action will "fill" ({@link BasicSearchable#fillTag(SearchableTag)})
324 * the given tag if needed first.
325 * <p>
326 * This operation can be long and should be run outside the UI thread.
327 *
328 * @param tag
329 * the tag to search into or NULL for the base tags
330 * @return the children
331 */
6628131d
NR
332 private List<SearchableTag> getChildrenForTag(final SearchableTag tag) {
333 List<SearchableTag> children = new ArrayList<SearchableTag>();
334 if (tag == null) {
335 try {
336 List<SearchableTag> baseTags = searchable.getTags();
337 children = baseTags;
338 } catch (IOException e) {
339 GuiReaderSearchFrame.error(e);
340 }
341 } else {
342 try {
343 searchable.fillTag(tag);
344 } catch (IOException e) {
345 GuiReaderSearchFrame.error(e);
346 }
347
348 if (!tag.isLeaf()) {
349 children = tag.getChildren();
350 } else {
351 children = null;
352 }
353 }
354
355 return children;
356 }
357
dc3b0033
NR
358 /**
359 * Search for the given tag on the currently selected searchable.
360 * <p>
361 * If the tag contains children tags, those will be displayed so you can
362 * select them; if the tag is a leaf tag, the linked stories will be
363 * displayed.
364 * <p>
365 * This operation can be long and should be run outside the UI thread.
366 *
367 * @param tag
368 * the tag to search for, or NULL for base tags
369 * @param page
370 * the page of results to load
371 * @param item
372 * the item to select (or 0 for none by default)
373 *
374 * @throw IndexOutOfBoundsException if the page is out of bounds
375 */
6628131d
NR
376 public void searchTag(SearchableTag tag, int page, int item) {
377 List<MetaData> stories = new ArrayList<MetaData>();
378 int storyItem = 0;
379
380 currentTag = tag;
381 updateTags(tag);
382
383 int maxPage = -1;
384 if (tag != null) {
385 try {
386 searchable.fillTag(tag);
387
388 if (!tag.isLeaf()) {
389 List<SearchableTag> subtags = tag.getChildren();
390 if (item > 0 && item <= subtags.size()) {
391 SearchableTag subtag = subtags.get(item - 1);
392 try {
393 tag = subtag;
394 searchable.fillTag(tag);
395 } catch (IOException e) {
396 GuiReaderSearchFrame.error(e);
397 }
398 } else if (item > 0) {
399 GuiReaderSearchFrame.error(String.format(
400 "Tag item does not exist: Tag [%s], item %d",
401 tag.getFqName(), item));
402 }
403 }
404
405 maxPage = searchable.searchPages(tag);
406 if (page > 0 && tag.isLeaf()) {
407 if (maxPage >= 0 && (page <= 0 || page > maxPage)) {
408 throw new IndexOutOfBoundsException("Page " + page
409 + " out of " + maxPage);
410 }
411
412 try {
413 stories = searchable.search(tag, page);
414 if (item > 0 && item <= stories.size()) {
415 storyItem = item;
416 } else if (item > 0) {
417 GuiReaderSearchFrame
418 .error(String
419 .format("Story item does not exist: Tag [%s], item %d",
420 tag.getFqName(), item));
421 }
422 } catch (IOException e) {
423 GuiReaderSearchFrame.error(e);
424 }
425 }
426 } catch (IOException e) {
427 GuiReaderSearchFrame.error(e);
428 maxPage = 0;
429 }
430 }
431
432 this.stories = stories;
433 this.storyItem = storyItem;
434 this.page = page;
435 this.maxPage = maxPage;
436 }
437
438 /**
439 * Enables or disables this component, depending on the value of the
440 * parameter <code>b</code>. An enabled component can respond to user input
441 * and generate events. Components are enabled initially by default.
442 * <p>
443 * Disabling this component will also affect its children.
444 *
445 * @param b
446 * If <code>true</code>, this component is enabled; otherwise
447 * this component is disabled
448 */
449 @Override
450 public void setEnabled(boolean b) {
451 super.setEnabled(b);
452 tagBars.setEnabled(b);
453 for (JComboBox combo : combos) {
454 combo.setEnabled(b);
455 }
456 }
457}