625dc93e2d4c9fd84d364994a9093f1f57b315c1
[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
212 : parents.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(createComboTagAction(comboIndex));
286
287 combos.add(combo);
288 tagBars.add(combo);
289 }
290
291 private ActionListener createComboTagAction(final int comboIndex) {
292 return new ActionListener() {
293 @Override
294 public void actionPerformed(ActionEvent ae) {
295 List<JComboBox> combos = GuiReaderSearchByNamePanel.this.combos;
296 if (combos == null || comboIndex < 0 || comboIndex >= combos.size()) {
297 return;
298 }
299
300 // Tag can be NULL
301 final SearchableTag tag = (SearchableTag) combos.get(comboIndex)
302 .getSelectedItem();
303
304 while (comboIndex + 1 < combos.size()) {
305 JComboBox combo = combos.remove(comboIndex + 1);
306 tagBars.remove(combo);
307 }
308
309 new Thread(new Runnable() {
310 @Override
311 public void run() {
312 final List<SearchableTag> children = getChildrenForTag(tag);
313 if (children != null) {
314 GuiReaderSearchFrame.inUi(new Runnable() {
315 @Override
316 public void run() {
317 addTagBar(children, tag);
318 }
319 });
320 }
321
322 if (tag != null && tag.isLeaf()) {
323 try {
324 GuiReaderSearchByNamePanel.this.page = 1;
325 stories = searchable.search(tag, 1);
326 } catch (IOException e) {
327 GuiReaderSearchFrame.error(e);
328 GuiReaderSearchByNamePanel.this.page = 0;
329 stories = new ArrayList<MetaData>();
330 }
331
332 fireAction();
333 }
334 }
335 }).start();
336 }
337 };
338 }
339
340 // sync, add children of tag, NULL = base tags
341 // return children of the tag or base tags or NULL
342 private List<SearchableTag> getChildrenForTag(final SearchableTag tag) {
343 List<SearchableTag> children = new ArrayList<SearchableTag>();
344 if (tag == null) {
345 try {
346 List<SearchableTag> baseTags = searchable.getTags();
347 children = baseTags;
348 } catch (IOException e) {
349 GuiReaderSearchFrame.error(e);
350 }
351 } else {
352 try {
353 searchable.fillTag(tag);
354 } catch (IOException e) {
355 GuiReaderSearchFrame.error(e);
356 }
357
358 if (!tag.isLeaf()) {
359 children = tag.getChildren();
360 } else {
361 children = null;
362 }
363 }
364
365 return children;
366 }
367
368 // item 0 = no selection, else = default selection
369 // return: maxpage
370 public int search(String keywords, int item) {
371 List<MetaData> stories = new ArrayList<MetaData>();
372 int storyItem = 0;
373
374 updateSearchBy(false);
375 updateKeywords(keywords);
376
377 int maxPage = -1;
378 try {
379 maxPage = searchable.searchPages(keywords);
380 } catch (IOException e) {
381 GuiReaderSearchFrame.error(e);
382 }
383
384 if (page > 0) {
385 try {
386 stories = searchable.search(keywords, page);
387 } catch (IOException e) {
388 GuiReaderSearchFrame.error(e);
389 stories = new ArrayList<MetaData>();
390 }
391
392 if (item > 0 && item <= stories.size()) {
393 storyItem = item;
394 } else if (item > 0) {
395 GuiReaderSearchFrame.error(String.format(
396 "Story item does not exist: Search [%s], item %d",
397 keywords, item));
398 }
399 }
400
401 this.stories = stories;
402 this.storyItem = storyItem;
403 fireAction();
404
405 return maxPage;
406 }
407
408 // tag: null = base tags
409 // return: max pages
410 public int searchTag(SearchableTag tag, int item) {
411 List<MetaData> stories = new ArrayList<MetaData>();
412 int storyItem = 0;
413
414 updateSearchBy(true);
415 updateTags(tag);
416
417 int maxPage = 1;
418 if (tag != null) {
419 try {
420 searchable.fillTag(tag);
421
422 if (!tag.isLeaf()) {
423 List<SearchableTag> subtags = tag.getChildren();
424 if (item > 0 && item <= subtags.size()) {
425 SearchableTag subtag = subtags.get(item - 1);
426 try {
427 tag = subtag;
428 searchable.fillTag(tag);
429 } catch (IOException e) {
430 GuiReaderSearchFrame.error(e);
431 }
432 } else if (item > 0) {
433 GuiReaderSearchFrame.error(String.format(
434 "Tag item does not exist: Tag [%s], item %d",
435 tag.getFqName(), item));
436 }
437 }
438
439 maxPage = searchable.searchPages(tag);
440 if (page > 0) {
441 if (tag.isLeaf()) {
442 try {
443 stories = searchable.search(tag, page);
444 if (item > 0 && item <= stories.size()) {
445 storyItem = item;
446 } else if (item > 0) {
447 GuiReaderSearchFrame.error(String.format(
448 "Story item does not exist: Tag [%s], item %d",
449 tag.getFqName(), item));
450 }
451 } catch (IOException e) {
452 GuiReaderSearchFrame.error(e);
453 }
454 }
455 }
456 } catch (IOException e) {
457 GuiReaderSearchFrame.error(e);
458 maxPage = 0;
459 }
460 }
461
462 this.stories = stories;
463 this.storyItem = storyItem;
464 fireAction();
465
466 return maxPage;
467 }
468
469 /**
470 * Enables or disables this component, depending on the value of the
471 * parameter <code>b</code>. An enabled component can respond to user input
472 * and generate events. Components are enabled initially by default.
473 * <p>
474 * Disabling this component will also affect its children.
475 *
476 * @param b
477 * If <code>true</code>, this component is enabled; otherwise
478 * this component is disabled
479 */
480 @Override
481 public void setEnabled(final boolean waiting) {
482 GuiReaderSearchFrame.inUi(new Runnable() {
483 @Override
484 public void run() {
485 GuiReaderSearchByNamePanel.super.setEnabled(!waiting);
486 keywordsField.setEnabled(!waiting);
487 submitKeywords.setEnabled(!waiting);
488 // TODO
489 }
490 });
491 }
492 }