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