a627d3a9e0b65c70c1abccad31bbc5b28c341e82
[fanfix.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderSearchByNamePanel.java
1 package be.nikiroo.fanfix.reader.ui;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
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;
11
12 import javax.swing.BoxLayout;
13 import javax.swing.JButton;
14 import javax.swing.JComboBox;
15 import javax.swing.JList;
16 import javax.swing.JPanel;
17 import javax.swing.JTabbedPane;
18 import javax.swing.JTextField;
19 import javax.swing.ListCellRenderer;
20
21 import be.nikiroo.fanfix.data.MetaData;
22 import be.nikiroo.fanfix.searchable.BasicSearchable;
23 import be.nikiroo.fanfix.searchable.SearchableTag;
24 import be.nikiroo.fanfix.supported.SupportType;
25
26 /**
27 * This panel represents a search panel that works for keywords and tags based
28 * searches.
29 *
30 * @author niki
31 */
32 // JCombobox<E> not 1.6 compatible
33 @SuppressWarnings({ "unchecked", "rawtypes" })
34 public class GuiReaderSearchByNamePanel extends JPanel {
35 private static final long serialVersionUID = 1L;
36
37 private int actionEventId = ActionEvent.ACTION_FIRST;
38
39 private SupportType supportType;
40 private BasicSearchable searchable;
41 private int page;
42 private boolean searchByTags;
43
44 private String keywords;
45 private JTabbedPane searchTabs;
46 private JTextField keywordsField;
47 private JButton submitKeywords;
48
49 private JPanel tagBars;
50 private List<JComboBox> combos;
51 private JComboBox comboSupportTypes;
52
53 private List<ActionListener> actions = new ArrayList<ActionListener>();
54 private List<MetaData> stories = new ArrayList<MetaData>();
55 private int storyItem;
56
57 // will throw illegalArgEx if bad support type, NULL allowed
58 public GuiReaderSearchByNamePanel(SupportType supportType) {
59 setLayout(new BorderLayout());
60
61 // TODO: check if null really is OK for supportType (must be)
62
63 setSupportType(supportType);
64 page = 1;
65 searchByTags = false;
66
67 searchTabs = new JTabbedPane();
68 searchTabs.addTab("By name", createByNameSearchPanel());
69 searchTabs.addTab("By tags", createByTagSearchPanel());
70
71 add(searchTabs, BorderLayout.CENTER);
72 }
73
74 private JPanel createByNameSearchPanel() {
75 JPanel byName = new JPanel(new BorderLayout());
76
77 keywordsField = new JTextField();
78 byName.add(keywordsField, BorderLayout.CENTER);
79
80 submitKeywords = new JButton("Search");
81 byName.add(submitKeywords, BorderLayout.EAST);
82
83 // TODO: ENTER -> search
84
85 submitKeywords.addActionListener(new ActionListener() {
86 @Override
87 public void actionPerformed(ActionEvent e) {
88 search(keywordsField.getText(), 0, null);
89 }
90 });
91
92 return byName;
93 }
94
95 private JPanel createByTagSearchPanel() {
96 combos = new ArrayList<JComboBox>();
97
98 JPanel byTag = new JPanel();
99 tagBars = new JPanel();
100 tagBars.setLayout(new BoxLayout(tagBars, BoxLayout.Y_AXIS));
101 byTag.add(tagBars, BorderLayout.NORTH);
102
103 return byTag;
104 }
105
106 public SupportType getSupportType() {
107 return supportType;
108 }
109
110 public void setSupportType(SupportType supportType) {
111 BasicSearchable searchable = BasicSearchable.getSearchable(supportType);
112 if (searchable == null && supportType != null) {
113 throw new java.lang.IllegalArgumentException(
114 "Unupported support type: " + supportType);
115 }
116
117 // TODO: if <>, reset all
118 // if new, set the base tags
119
120 this.supportType = supportType;
121 this.searchable = searchable;
122 }
123
124 public int getPage() {
125 return page;
126 }
127
128 public void setPage(int page) {
129 // TODO: set against maxPage
130 // TODO: update last search?
131 this.page = page;
132 }
133
134 // actions will be fired in UIthread
135 public void addActionListener(ActionListener action) {
136 actions.add(action);
137 }
138
139 public boolean removeActionListener(ActionListener action) {
140 return actions.remove(action);
141 }
142
143 public List<MetaData> getStories() {
144 return stories;
145 }
146
147 // selected item or 0 if none ! one-based !
148 public int getStoryItem() {
149 return storyItem;
150 }
151
152 private void fireAction(final Runnable inUi) {
153 GuiReaderSearchFrame.inUi(new Runnable() {
154 @Override
155 public void run() {
156 ActionEvent ae = new ActionEvent(
157 GuiReaderSearchByNamePanel.this, actionEventId,
158 "stories found");
159
160 actionEventId++;
161 if (actionEventId > ActionEvent.ACTION_LAST) {
162 actionEventId = ActionEvent.ACTION_FIRST;
163 }
164
165 for (ActionListener action : actions) {
166 try {
167 action.actionPerformed(ae);
168 } catch (Exception e) {
169 GuiReaderSearchFrame.error(e);
170 }
171 }
172
173 if (inUi != null) {
174 inUi.run();
175 }
176 }
177 });
178 }
179
180 private void updateSearchBy(final boolean byTag) {
181 if (byTag != this.searchByTags) {
182 GuiReaderSearchFrame.inUi(new Runnable() {
183 @Override
184 public void run() {
185 if (!byTag) {
186 searchTabs.setSelectedIndex(0);
187 } else {
188 searchTabs.setSelectedIndex(1);
189 }
190 }
191 });
192 }
193 }
194
195 // cannot be NULL
196 private void updateKeywords(final String keywords) {
197 if (!keywords.equals(this.keywords)) {
198 GuiReaderSearchFrame.inUi(new Runnable() {
199 @Override
200 public void run() {
201 GuiReaderSearchByNamePanel.this.keywords = keywords;
202 keywordsField.setText(keywords);
203 }
204 });
205 }
206 }
207
208 // update and reset the tagsbar
209 // can be NULL, for base tags
210 private void updateTags(final SearchableTag tag) {
211 final List<SearchableTag> parents = new ArrayList<SearchableTag>();
212 SearchableTag parent = (tag == null) ? null : tag;
213 while (parent != null) {
214 parents.add(parent);
215 parent = parent.getParent();
216 }
217
218 List<SearchableTag> rootTags = null;
219 SearchableTag selectedRootTag = null;
220 selectedRootTag = parents.isEmpty() ? null
221 : parents.get(parents.size() - 1);
222
223 try {
224 rootTags = searchable.getTags();
225 } catch (IOException e) {
226 GuiReaderSearchFrame.error(e);
227 }
228
229 final List<SearchableTag> rootTagsF = rootTags;
230 final SearchableTag selectedRootTagF = selectedRootTag;
231
232 GuiReaderSearchFrame.inUi(new Runnable() {
233 @Override
234 public void run() {
235 tagBars.invalidate();
236 tagBars.removeAll();
237
238 addTagBar(rootTagsF, selectedRootTagF);
239
240 for (int i = parents.size() - 1; i >= 0; i--) {
241 SearchableTag selectedChild = null;
242 if (i > 0) {
243 selectedChild = parents.get(i - 1);
244 }
245
246 SearchableTag parent = parents.get(i);
247 addTagBar(parent.getChildren(), selectedChild);
248 }
249
250 tagBars.validate();
251 }
252 });
253 }
254
255 // must be quick and no thread change
256 private void addTagBar(List<SearchableTag> tags,
257 final SearchableTag selected) {
258 tags.add(0, null);
259
260 final int comboIndex = combos.size();
261
262 final JComboBox combo = new JComboBox(
263 tags.toArray(new SearchableTag[] {}));
264 combo.setSelectedItem(selected);
265
266 final ListCellRenderer basic = combo.getRenderer();
267
268 combo.setRenderer(new ListCellRenderer() {
269 @Override
270 public Component getListCellRendererComponent(JList list,
271 Object value, int index, boolean isSelected,
272 boolean cellHasFocus) {
273
274 Object displayValue = value;
275 if (value instanceof SearchableTag) {
276 displayValue = ((SearchableTag) value).getName();
277 } else {
278 displayValue = "Select a tag...";
279 cellHasFocus = false;
280 isSelected = false;
281 }
282
283 Component rep = basic.getListCellRendererComponent(list,
284 displayValue, index, isSelected, cellHasFocus);
285
286 if (value == null) {
287 rep.setForeground(Color.GRAY);
288 }
289
290 return rep;
291 }
292 });
293
294 combo.addActionListener(createComboTagAction(comboIndex));
295
296 combos.add(combo);
297 tagBars.add(combo);
298 }
299
300 private ActionListener createComboTagAction(final int comboIndex) {
301 return new ActionListener() {
302 @Override
303 public void actionPerformed(ActionEvent ae) {
304 List<JComboBox> combos = GuiReaderSearchByNamePanel.this.combos;
305 if (combos == null || comboIndex < 0 || comboIndex >= combos.size()) {
306 return;
307 }
308
309 // Tag can be NULL
310 final SearchableTag tag = (SearchableTag) combos.get(comboIndex)
311 .getSelectedItem();
312
313 while (comboIndex + 1 < combos.size()) {
314 JComboBox combo = combos.remove(comboIndex + 1);
315 tagBars.remove(combo);
316 }
317
318 new Thread(new Runnable() {
319 @Override
320 public void run() {
321 final List<SearchableTag> children = getChildrenForTag(tag);
322 if (children != null) {
323 GuiReaderSearchFrame.inUi(new Runnable() {
324 @Override
325 public void run() {
326 addTagBar(children, tag);
327 }
328 });
329 }
330
331 if (tag != null && tag.isLeaf()) {
332 try {
333 GuiReaderSearchByNamePanel.this.page = 1;
334 stories = searchable.search(tag, 1);
335 } catch (IOException e) {
336 GuiReaderSearchFrame.error(e);
337 GuiReaderSearchByNamePanel.this.page = 0;
338 stories = new ArrayList<MetaData>();
339 }
340
341 fireAction(null);
342 }
343 }
344 }).start();
345 }
346 };
347 }
348
349 // sync, add children of tag, NULL = base tags
350 // return children of the tag or base tags or NULL
351 private List<SearchableTag> getChildrenForTag(final SearchableTag tag) {
352 List<SearchableTag> children = new ArrayList<SearchableTag>();
353 if (tag == null) {
354 try {
355 List<SearchableTag> baseTags = searchable.getTags();
356 children = baseTags;
357 } catch (IOException e) {
358 GuiReaderSearchFrame.error(e);
359 }
360 } else {
361 try {
362 searchable.fillTag(tag);
363 } catch (IOException e) {
364 GuiReaderSearchFrame.error(e);
365 }
366
367 if (!tag.isLeaf()) {
368 children = tag.getChildren();
369 } else {
370 children = null;
371 }
372 }
373
374 return children;
375 }
376
377 // item 0 = no selection, else = default selection
378 // return: maxpage
379 public int search(String keywords, int item, Runnable inUi) {
380 List<MetaData> stories = new ArrayList<MetaData>();
381 int storyItem = 0;
382
383 updateSearchBy(false);
384 updateKeywords(keywords);
385
386 int maxPage = -1;
387 try {
388 maxPage = searchable.searchPages(keywords);
389 } catch (IOException e) {
390 GuiReaderSearchFrame.error(e);
391 }
392
393 if (page > 0) {
394 try {
395 stories = searchable.search(keywords, page);
396 } catch (IOException e) {
397 GuiReaderSearchFrame.error(e);
398 stories = new ArrayList<MetaData>();
399 }
400
401 if (item > 0 && item <= stories.size()) {
402 storyItem = item;
403 } else if (item > 0) {
404 GuiReaderSearchFrame.error(String.format(
405 "Story item does not exist: Search [%s], item %d",
406 keywords, item));
407 }
408 }
409
410 this.stories = stories;
411 this.storyItem = storyItem;
412 fireAction(inUi);
413
414 return maxPage;
415 }
416
417 // tag: null = base tags
418 // return: max pages
419 public int searchTag(SearchableTag tag, int item, Runnable inUi) {
420 List<MetaData> stories = new ArrayList<MetaData>();
421 int storyItem = 0;
422
423 updateSearchBy(true);
424 updateTags(tag);
425
426 int maxPage = 1;
427 if (tag != null) {
428 try {
429 searchable.fillTag(tag);
430
431 if (!tag.isLeaf()) {
432 List<SearchableTag> subtags = tag.getChildren();
433 if (item > 0 && item <= subtags.size()) {
434 SearchableTag subtag = subtags.get(item - 1);
435 try {
436 tag = subtag;
437 searchable.fillTag(tag);
438 } catch (IOException e) {
439 GuiReaderSearchFrame.error(e);
440 }
441 } else if (item > 0) {
442 GuiReaderSearchFrame.error(String.format(
443 "Tag item does not exist: Tag [%s], item %d",
444 tag.getFqName(), item));
445 }
446 }
447
448 maxPage = searchable.searchPages(tag);
449 if (page > 0) {
450 if (tag.isLeaf()) {
451 try {
452 stories = searchable.search(tag, page);
453 if (item > 0 && item <= stories.size()) {
454 storyItem = item;
455 } else if (item > 0) {
456 GuiReaderSearchFrame.error(String.format(
457 "Story item does not exist: Tag [%s], item %d",
458 tag.getFqName(), item));
459 }
460 } catch (IOException e) {
461 GuiReaderSearchFrame.error(e);
462 }
463 }
464 }
465 } catch (IOException e) {
466 GuiReaderSearchFrame.error(e);
467 maxPage = 0;
468 }
469 }
470
471 this.stories = stories;
472 this.storyItem = storyItem;
473 fireAction(inUi);
474
475 return maxPage;
476 }
477
478 /**
479 * Enables or disables this component, depending on the value of the
480 * parameter <code>b</code>. An enabled component can respond to user input
481 * and generate events. Components are enabled initially by default.
482 * <p>
483 * Disabling this component will also affect its children.
484 *
485 * @param b
486 * If <code>true</code>, this component is enabled; otherwise
487 * this component is disabled
488 */
489 @Override
490 public void setEnabled(final boolean waiting) {
491 GuiReaderSearchFrame.inUi(new Runnable() {
492 @Override
493 public void run() {
494 GuiReaderSearchByNamePanel.super.setEnabled(!waiting);
495 keywordsField.setEnabled(!waiting);
496 submitKeywords.setEnabled(!waiting);
497 // TODO
498 }
499 });
500 }
501 }