GUI: search: step x + 1
[fanfix.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderSearch.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.EventQueue;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.io.IOException;
10 import java.lang.reflect.InvocationTargetException;
11 import java.util.ArrayList;
12 import java.util.List;
13
14 import javax.swing.BoxLayout;
15 import javax.swing.JButton;
16 import javax.swing.JComboBox;
17 import javax.swing.JFrame;
18 import javax.swing.JLabel;
19 import javax.swing.JList;
20 import javax.swing.JPanel;
21 import javax.swing.JScrollPane;
22 import javax.swing.JTabbedPane;
23 import javax.swing.JTextField;
24 import javax.swing.ListCellRenderer;
25
26 import be.nikiroo.fanfix.Instance;
27 import be.nikiroo.fanfix.data.MetaData;
28 import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
29 import be.nikiroo.fanfix.searchable.BasicSearchable;
30 import be.nikiroo.fanfix.searchable.SearchableTag;
31 import be.nikiroo.fanfix.supported.SupportType;
32
33 /**
34 * This frame will allow you to search through the supported websites for new
35 * stories/comics.
36 *
37 * @author niki
38 */
39 public class GuiReaderSearch extends JFrame {
40 private static final long serialVersionUID = 1L;
41
42 private List<SupportType> supportTypes;
43 private SupportType supportType;
44 private boolean searchByTags;
45 private List<SearchableTag> tags;
46 private String keywords;
47 private int page;
48 private int maxPage;
49
50 private JPanel tagBars;
51
52 private JComboBox<SupportType> comboSupportTypes;
53 private JTabbedPane searchTabs;
54 private JTextField keywordsField;
55 private JButton submitKeywords;
56
57 private boolean seeWordcount;
58 private GuiReaderGroup books;
59
60 public GuiReaderSearch(final GuiReader reader) {
61 super("Browse stories");
62 setLayout(new BorderLayout());
63 setSize(800, 600);
64
65 tags = new ArrayList<SearchableTag>();
66 page = 1; // TODO
67 maxPage = -1;
68 searchByTags = false;
69
70 supportTypes = new ArrayList<SupportType>();
71 for (SupportType type : SupportType.values()) {
72 if (BasicSearchable.getSearchable(type) != null) {
73 supportTypes.add(type);
74 }
75 }
76 supportType = supportTypes.isEmpty() ? null : supportTypes.get(0);
77
78 comboSupportTypes = new JComboBox<SupportType>(
79 supportTypes.toArray(new SupportType[] {}));
80 comboSupportTypes.addActionListener(new ActionListener() {
81 @Override
82 public void actionPerformed(ActionEvent e) {
83 updateSupportType((SupportType) comboSupportTypes
84 .getSelectedItem());
85 }
86 });
87 JPanel searchSites = new JPanel(new BorderLayout());
88 searchSites.add(comboSupportTypes, BorderLayout.CENTER);
89 searchSites.add(new JLabel(" " + "Website : "), BorderLayout.WEST);
90
91 searchTabs = new JTabbedPane();
92 searchTabs.addTab("By name", createByNameSearchPanel());
93 searchTabs.addTab("By tags", createByTagSearchPanel());
94
95 JPanel top = new JPanel(new BorderLayout());
96 top.add(searchSites, BorderLayout.NORTH);
97 top.add(searchTabs, BorderLayout.CENTER);
98
99 add(top, BorderLayout.NORTH);
100
101 books = new GuiReaderGroup(reader, null, null);
102 books.setActionListener(new BookActionListener() {
103 @Override
104 public void select(GuiReaderBook book) {
105 }
106
107 @Override
108 public void popupRequested(GuiReaderBook book, Component target,
109 int x, int y) {
110 }
111
112 @Override
113 public void action(GuiReaderBook book) {
114 new GuiReaderSearchAction(reader.getLibrary(), book.getInfo())
115 .setVisible(true);
116 }
117 });
118 JScrollPane scroll = new JScrollPane(books);
119 scroll.getVerticalScrollBar().setUnitIncrement(16);
120 add(scroll, BorderLayout.CENTER);
121
122 updateTags(null);
123 }
124
125 private JPanel createByNameSearchPanel() {
126 JPanel byName = new JPanel(new BorderLayout());
127
128 keywordsField = new JTextField();
129 byName.add(keywordsField, BorderLayout.CENTER);
130
131 submitKeywords = new JButton("Search");
132 byName.add(submitKeywords, BorderLayout.EAST);
133
134 // TODO: ENTER -> search
135
136 submitKeywords.addActionListener(new ActionListener() {
137 @Override
138 public void actionPerformed(ActionEvent e) {
139 search(supportType, keywordsField.getText(), page, 0);
140 }
141 });
142
143 return byName;
144 }
145
146 private JPanel createByTagSearchPanel() {
147 JPanel byTag = new JPanel();
148 tagBars = new JPanel();
149 tagBars.setLayout(new BoxLayout(tagBars, BoxLayout.Y_AXIS));
150 byTag.add(tagBars, BorderLayout.NORTH);
151
152 return byTag;
153 }
154
155 private void updateSupportType(SupportType supportType) {
156 if (supportType != this.supportType) {
157 this.supportType = supportType;
158 comboSupportTypes.setSelectedItem(supportType);
159 books.clear();
160 updateTags(null);
161 }
162 }
163
164 private void updateSearchBy(final boolean byTag) {
165 if (byTag != this.searchByTags) {
166 inUi(new Runnable() {
167 @Override
168 public void run() {
169 if (!byTag) {
170 searchTabs.setSelectedIndex(0);
171 } else {
172 searchTabs.setSelectedIndex(1);
173 }
174 }
175 });
176 }
177 }
178
179 private void updatePages(final int page, final Integer maxPage) {
180 inUi(new Runnable() {
181 @Override
182 public void run() {
183 GuiReaderSearch.this.page = page;
184 GuiReaderSearch.this.maxPage = maxPage;
185 // TODO: gui
186 System.out.println("page: " + page);
187 System.out.println("max page: " + maxPage);
188 }
189 });
190 }
191
192 // cannot be NULL
193 private void updateKeywords(final String keywords) {
194 if (!keywords.equals(this.keywords)) {
195 inUi(new Runnable() {
196 @Override
197 public void run() {
198 GuiReaderSearch.this.keywords = keywords;
199 keywordsField.setText(keywords);
200 }
201 });
202 }
203 }
204
205 // can be NULL, for base tags
206 private void updateTags(final SearchableTag tag) {
207 final List<SearchableTag> parents = new ArrayList<SearchableTag>();
208 SearchableTag parent = (tag == null) ? null : tag;
209 while (parent != null) {
210 parents.add(parent);
211 parent = parent.getParent();
212 }
213
214 inUi(new Runnable() {
215 @Override
216 public void run() {
217 tagBars.invalidate();
218 tagBars.removeAll();
219
220 // TODO: Slow UI
221 // TODO: select the right one
222 try {
223 addTagBar(BasicSearchable.getSearchable(supportType)
224 .getTags(), tag);
225 } catch (IOException e) {
226 error(e);
227 }
228
229 for (int i = parents.size() - 1; i >= 0; i--) {
230 SearchableTag parent = parents.get(i);
231 addTagBar(parent.getChildren(), parent);
232 }
233
234 tagBars.validate();
235 }
236 });
237 }
238
239 private void updateBooks(final List<GuiReaderBookInfo> infos) {
240 setWaitingScreen(true);
241 inUi(new Runnable() {
242 @Override
243 public void run() {
244 books.refreshBooks(infos, seeWordcount);
245 setWaitingScreen(false);
246 }
247 });
248 }
249
250 private void addTagBar(List<SearchableTag> tags,
251 final SearchableTag selected) {
252 tags.add(0, null);
253
254 final JComboBox<SearchableTag> combo = new JComboBox<SearchableTag>(
255 tags.toArray(new SearchableTag[] {}));
256 combo.setSelectedItem(selected);
257
258 // We want to pass it a String
259 @SuppressWarnings({ "rawtypes" })
260 final ListCellRenderer basic = combo.getRenderer();
261
262 combo.setRenderer(new ListCellRenderer<SearchableTag>() {
263 @Override
264 public Component getListCellRendererComponent(
265 JList<? extends SearchableTag> list, SearchableTag value,
266 int index, boolean isSelected, boolean cellHasFocus) {
267
268 Object displayValue = value;
269 if (value == null) {
270 displayValue = "Select a tag...";
271 cellHasFocus = false;
272 isSelected = false;
273 } else {
274 displayValue = value.getName();
275 }
276
277 // We willingly pass a String here
278 @SuppressWarnings("unchecked")
279 Component rep = basic.getListCellRendererComponent(list,
280 displayValue, index, isSelected, cellHasFocus);
281
282 if (value == null) {
283 rep.setForeground(Color.GRAY);
284 }
285
286 return rep;
287 }
288 });
289
290 combo.addActionListener(new ActionListener() {
291 @Override
292 public void actionPerformed(ActionEvent e) {
293 final SearchableTag tag = (SearchableTag) combo
294 .getSelectedItem();
295 if (tag != null) {
296 addTagBar(tag, new Runnable() {
297 @Override
298 public void run() {
299 // TODO: stories if needed
300 setWaitingScreen(false);
301 }
302 });
303 }
304 }
305 });
306
307 tagBars.add(combo);
308 }
309
310 // async, add children of tag, NULL = base tags
311 private void addTagBar(final SearchableTag tag, final Runnable inUi) {
312 new Thread(new Runnable() {
313 @Override
314 public void run() {
315 BasicSearchable searchable = BasicSearchable
316 .getSearchable(supportType);
317
318 List<SearchableTag> children = new ArrayList<SearchableTag>();
319 if (tag == null) {
320 try {
321 List<SearchableTag> baseTags = searchable.getTags();
322 children = baseTags;
323 } catch (IOException e) {
324 error(e);
325 }
326 } else {
327 try {
328 searchable.fillTag(tag);
329 } catch (IOException e) {
330 error(e);
331 }
332
333 if (!tag.isLeaf()) {
334 children = tag.getChildren();
335 } else {
336 children = null;
337 // TODO: stories
338 }
339 }
340
341 final List<SearchableTag> fchildren = children;
342 inUi(new Runnable() {
343 @Override
344 public void run() {
345 if (fchildren != null) {
346 addTagBar(fchildren, tag);
347 }
348
349 if (inUi != null) {
350 inUi.run();
351 }
352 }
353 });
354 }
355 }).start();
356 }
357
358 // item 0 = no selection, else = default selection
359 public void search(final SupportType searchOn, final String keywords,
360 final int page, final int item) {
361
362 setWaitingScreen(true);
363
364 updateSupportType(searchOn);
365 updateSearchBy(false);
366 updateKeywords(keywords);
367 updatePages(page, maxPage);
368
369 new Thread(new Runnable() {
370 @Override
371 public void run() {
372 BasicSearchable search = BasicSearchable
373 .getSearchable(searchOn);
374
375 int maxPage = -1;
376 try {
377 maxPage = search.searchPages(keywords);
378 } catch (IOException e) {
379 error(e);
380 }
381
382 if (page <= 0) {
383 updateBooks(new ArrayList<GuiReaderBookInfo>());
384 updatePages(0, maxPage);
385 } else {
386 List<MetaData> results;
387 try {
388 results = search.search(keywords, page);
389 } catch (IOException e) {
390 error(e);
391 results = new ArrayList<MetaData>();
392 }
393
394 search(results, page, maxPage, item);
395
396 // ! 1-based index !
397 if (item > 0 && item <= books.getBooksCount()) {
398 // TODO: "click" on item ITEM
399 }
400 }
401
402 setWaitingScreen(false);
403 }
404 }).start();
405 }
406
407 // tag: must be filled (or NULL for base tags)
408 public void searchTag(final SupportType searchOn, final int page,
409 final int item, final SearchableTag tag) {
410
411 setWaitingScreen(true);
412
413 updateSupportType(searchOn);
414 updateSearchBy(true);
415 updateTags(tag);
416 updatePages(page, maxPage);
417
418 new Thread(new Runnable() {
419 @Override
420 public void run() {
421 BasicSearchable search = BasicSearchable
422 .getSearchable(searchOn);
423
424 if (tag != null) {
425 int maxPage = 0;
426 try {
427 maxPage = search.searchPages(tag);
428 } catch (IOException e) {
429 error(e);
430 }
431
432 updatePages(page, maxPage);
433
434 if (page > 0) {
435 List<MetaData> metas = new ArrayList<MetaData>();
436
437 if (tag.isLeaf()) {
438 try {
439 metas = search.search(tag, page);
440 } catch (IOException e) {
441 error(e);
442 }
443 } else {
444 List<SearchableTag> subtags = tag.getChildren();
445 if (item > 0 && item <= subtags.size()) {
446 SearchableTag subtag = subtags.get(item - 1);
447 try {
448 metas = search.search(subtag, page);
449 maxPage = subtag.getPages();
450 } catch (IOException e) {
451 error(e);
452 }
453 }
454 }
455
456 updatePages(page, maxPage);
457 search(metas, page, maxPage, item);
458 }
459 }
460
461 setWaitingScreen(false);
462 }
463 }).start();
464 }
465
466 // item 0 = no selection, else = default selection
467 public void search(final List<MetaData> results, final int page,
468 final int maxPage, final int item) {
469
470 updatePages(page, maxPage);
471
472 if (page <= 0) {
473 updateBooks(new ArrayList<GuiReaderBookInfo>());
474 updatePages(0, maxPage);
475 } else {
476 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
477 for (MetaData meta : results) {
478 infos.add(GuiReaderBookInfo.fromMeta(meta));
479 }
480
481 updateBooks(infos);
482
483 // ! 1-based index !
484 if (item > 0 && item <= books.getBooksCount()) {
485 // TODO: "click" on item ITEM
486 }
487 }
488 }
489
490 /**
491 * Process the given action in the main Swing UI thread.
492 * <p>
493 * The code will make sure the current thread is the main UI thread and, if
494 * not, will switch to it before executing the runnable.
495 * <p>
496 * Synchronous operation.
497 *
498 * @param run
499 * the action to run
500 */
501 private void inUi(final Runnable run) {
502 if (EventQueue.isDispatchThread()) {
503 run.run();
504 } else {
505 try {
506 EventQueue.invokeAndWait(run);
507 } catch (InterruptedException e) {
508 error(e);
509 } catch (InvocationTargetException e) {
510 error(e);
511 }
512 }
513 }
514
515 private void error(Exception e) {
516 Instance.getTraceHandler().error(e);
517 }
518
519 private void setWaitingScreen(final boolean waiting) {
520 inUi(new Runnable() {
521 @Override
522 public void run() {
523 GuiReaderSearch.this.setEnabled(!waiting);
524 books.setEnabled(!waiting);
525 submitKeywords.setEnabled(!waiting);
526 }
527 });
528 }
529 }