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