Commit | Line | Data |
---|---|---|
6628131d NR |
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.JComboBox; | |
14 | import javax.swing.JList; | |
15 | import javax.swing.JPanel; | |
16 | import javax.swing.ListCellRenderer; | |
17 | ||
18 | import be.nikiroo.fanfix.data.MetaData; | |
19 | import be.nikiroo.fanfix.searchable.BasicSearchable; | |
20 | import be.nikiroo.fanfix.searchable.SearchableTag; | |
21 | ||
22 | /** | |
23 | * This panel represents a search panel that works for keywords and tags based | |
24 | * searches. | |
25 | * | |
26 | * @author niki | |
27 | */ | |
28 | // JCombobox<E> not 1.6 compatible | |
29 | @SuppressWarnings({ "unchecked", "rawtypes" }) | |
30 | public class GuiReaderSearchByTagPanel extends JPanel { | |
31 | private static final long serialVersionUID = 1L; | |
32 | ||
33 | private BasicSearchable searchable; | |
34 | private Runnable fireEvent; | |
35 | ||
36 | private SearchableTag currentTag; | |
37 | private JPanel tagBars; | |
38 | private List<JComboBox> combos; | |
39 | ||
40 | private int page; | |
41 | private int maxPage; | |
42 | private List<MetaData> stories = new ArrayList<MetaData>(); | |
43 | private int storyItem; | |
44 | ||
45 | public GuiReaderSearchByTagPanel(Runnable fireEvent) { | |
46 | setLayout(new BorderLayout()); | |
47 | ||
48 | this.fireEvent = fireEvent; | |
49 | combos = new ArrayList<JComboBox>(); | |
50 | page = 1; | |
51 | maxPage = -1; | |
52 | ||
53 | tagBars = new JPanel(); | |
54 | tagBars.setLayout(new BoxLayout(tagBars, BoxLayout.Y_AXIS)); | |
55 | add(tagBars, BorderLayout.NORTH); | |
56 | } | |
57 | ||
58 | /** | |
59 | * The {@link BasicSearchable} object use for the searches themselves. | |
60 | * <p> | |
61 | * Can be NULL, but no searches will work. | |
62 | * | |
63 | * @param searchable | |
64 | * the new searchable | |
65 | */ | |
66 | public void setSearchable(BasicSearchable searchable) { | |
67 | this.searchable = searchable; | |
68 | page = 1; | |
69 | maxPage = -1; | |
70 | storyItem = 0; | |
71 | stories = new ArrayList<MetaData>(); | |
72 | updateTags(null); | |
73 | } | |
74 | ||
75 | public int getPage() { | |
76 | return page; | |
77 | } | |
78 | ||
79 | public int getMaxPage() { | |
80 | return maxPage; | |
81 | } | |
82 | ||
83 | public SearchableTag getCurrentTag() { | |
84 | return currentTag; | |
85 | } | |
86 | ||
87 | public List<MetaData> getStories() { | |
88 | return stories; | |
89 | } | |
90 | ||
91 | // selected item or 0 if none ! one-based ! | |
92 | public int getStoryItem() { | |
93 | return storyItem; | |
94 | } | |
95 | ||
96 | // update and reset the tagsbar | |
97 | // can be NULL, for base tags | |
98 | private void updateTags(final SearchableTag tag) { | |
99 | final List<SearchableTag> parents = new ArrayList<SearchableTag>(); | |
100 | SearchableTag parent = (tag == null) ? null : tag; | |
101 | while (parent != null) { | |
102 | parents.add(parent); | |
103 | parent = parent.getParent(); | |
104 | } | |
105 | ||
106 | List<SearchableTag> rootTags = new ArrayList<SearchableTag>(); | |
107 | SearchableTag selectedRootTag = null; | |
108 | selectedRootTag = parents.isEmpty() ? null : parents | |
109 | .get(parents.size() - 1); | |
110 | ||
111 | if (searchable != null) { | |
112 | try { | |
113 | rootTags = searchable.getTags(); | |
114 | } catch (IOException e) { | |
115 | GuiReaderSearchFrame.error(e); | |
116 | } | |
117 | } | |
118 | ||
119 | final List<SearchableTag> rootTagsF = rootTags; | |
120 | final SearchableTag selectedRootTagF = selectedRootTag; | |
121 | ||
122 | GuiReaderSearchFrame.inUi(new Runnable() { | |
123 | @Override | |
124 | public void run() { | |
125 | tagBars.invalidate(); | |
126 | tagBars.removeAll(); | |
127 | ||
128 | addTagBar(rootTagsF, selectedRootTagF); | |
129 | ||
130 | for (int i = parents.size() - 1; i >= 0; i--) { | |
131 | SearchableTag selectedChild = null; | |
132 | if (i > 0) { | |
133 | selectedChild = parents.get(i - 1); | |
134 | } | |
135 | ||
136 | SearchableTag parent = parents.get(i); | |
137 | addTagBar(parent.getChildren(), selectedChild); | |
138 | } | |
139 | ||
140 | tagBars.validate(); | |
141 | } | |
142 | }); | |
143 | } | |
144 | ||
145 | // must be quick and no thread change | |
146 | private void addTagBar(List<SearchableTag> tags, | |
147 | final SearchableTag selected) { | |
148 | tags.add(0, null); | |
149 | ||
150 | final int comboIndex = combos.size(); | |
151 | ||
152 | final JComboBox combo = new JComboBox( | |
153 | tags.toArray(new SearchableTag[] {})); | |
154 | combo.setSelectedItem(selected); | |
155 | ||
156 | final ListCellRenderer basic = combo.getRenderer(); | |
157 | ||
158 | combo.setRenderer(new ListCellRenderer() { | |
159 | @Override | |
160 | public Component getListCellRendererComponent(JList list, | |
161 | Object value, int index, boolean isSelected, | |
162 | boolean cellHasFocus) { | |
163 | ||
164 | Object displayValue = value; | |
165 | if (value instanceof SearchableTag) { | |
166 | displayValue = ((SearchableTag) value).getName(); | |
167 | } else { | |
168 | displayValue = "Select a tag..."; | |
169 | cellHasFocus = false; | |
170 | isSelected = false; | |
171 | } | |
172 | ||
173 | Component rep = basic.getListCellRendererComponent(list, | |
174 | displayValue, index, isSelected, cellHasFocus); | |
175 | ||
176 | if (value == null) { | |
177 | rep.setForeground(Color.GRAY); | |
178 | } | |
179 | ||
180 | return rep; | |
181 | } | |
182 | }); | |
183 | ||
184 | combo.addActionListener(createComboTagAction(comboIndex)); | |
185 | ||
186 | combos.add(combo); | |
187 | tagBars.add(combo); | |
188 | } | |
189 | ||
190 | private ActionListener createComboTagAction(final int comboIndex) { | |
191 | return new ActionListener() { | |
192 | @Override | |
193 | public void actionPerformed(ActionEvent ae) { | |
194 | List<JComboBox> combos = GuiReaderSearchByTagPanel.this.combos; | |
195 | if (combos == null || comboIndex < 0 | |
196 | || comboIndex >= combos.size()) { | |
197 | return; | |
198 | } | |
199 | ||
200 | // Tag can be NULL | |
201 | final SearchableTag tag = (SearchableTag) combos | |
202 | .get(comboIndex).getSelectedItem(); | |
203 | ||
204 | while (comboIndex + 1 < combos.size()) { | |
205 | JComboBox combo = combos.remove(comboIndex + 1); | |
206 | tagBars.remove(combo); | |
207 | } | |
208 | ||
209 | new Thread(new Runnable() { | |
210 | @Override | |
211 | public void run() { | |
212 | final List<SearchableTag> children = getChildrenForTag(tag); | |
213 | if (children != null) { | |
214 | GuiReaderSearchFrame.inUi(new Runnable() { | |
215 | @Override | |
216 | public void run() { | |
217 | addTagBar(children, tag); | |
218 | } | |
219 | }); | |
220 | } | |
221 | ||
222 | if (tag != null && tag.isLeaf()) { | |
223 | storyItem = 0; | |
224 | try { | |
225 | searchable.fillTag(tag); | |
226 | page = 1; | |
227 | stories = searchable.search(tag, 1); | |
228 | maxPage = searchable.searchPages(tag); | |
229 | } catch (IOException e) { | |
230 | GuiReaderSearchFrame.error(e); | |
231 | page = 0; | |
232 | maxPage = -1; | |
233 | stories = new ArrayList<MetaData>(); | |
234 | } | |
235 | ||
236 | fireEvent.run(); | |
237 | } | |
238 | } | |
239 | }).start(); | |
240 | } | |
241 | }; | |
242 | } | |
243 | ||
244 | // sync, add children of tag, NULL = base tags | |
245 | // return children of the tag or base tags or NULL | |
246 | private List<SearchableTag> getChildrenForTag(final SearchableTag tag) { | |
247 | List<SearchableTag> children = new ArrayList<SearchableTag>(); | |
248 | if (tag == null) { | |
249 | try { | |
250 | List<SearchableTag> baseTags = searchable.getTags(); | |
251 | children = baseTags; | |
252 | } catch (IOException e) { | |
253 | GuiReaderSearchFrame.error(e); | |
254 | } | |
255 | } else { | |
256 | try { | |
257 | searchable.fillTag(tag); | |
258 | } catch (IOException e) { | |
259 | GuiReaderSearchFrame.error(e); | |
260 | } | |
261 | ||
262 | if (!tag.isLeaf()) { | |
263 | children = tag.getChildren(); | |
264 | } else { | |
265 | children = null; | |
266 | } | |
267 | } | |
268 | ||
269 | return children; | |
270 | } | |
271 | ||
272 | // slow | |
273 | // tag: null = base tags | |
274 | // throw if page > max, but only if stories | |
275 | public void searchTag(SearchableTag tag, int page, int item) { | |
276 | List<MetaData> stories = new ArrayList<MetaData>(); | |
277 | int storyItem = 0; | |
278 | ||
279 | currentTag = tag; | |
280 | updateTags(tag); | |
281 | ||
282 | int maxPage = -1; | |
283 | if (tag != null) { | |
284 | try { | |
285 | searchable.fillTag(tag); | |
286 | ||
287 | if (!tag.isLeaf()) { | |
288 | List<SearchableTag> subtags = tag.getChildren(); | |
289 | if (item > 0 && item <= subtags.size()) { | |
290 | SearchableTag subtag = subtags.get(item - 1); | |
291 | try { | |
292 | tag = subtag; | |
293 | searchable.fillTag(tag); | |
294 | } catch (IOException e) { | |
295 | GuiReaderSearchFrame.error(e); | |
296 | } | |
297 | } else if (item > 0) { | |
298 | GuiReaderSearchFrame.error(String.format( | |
299 | "Tag item does not exist: Tag [%s], item %d", | |
300 | tag.getFqName(), item)); | |
301 | } | |
302 | } | |
303 | ||
304 | maxPage = searchable.searchPages(tag); | |
305 | if (page > 0 && tag.isLeaf()) { | |
306 | if (maxPage >= 0 && (page <= 0 || page > maxPage)) { | |
307 | throw new IndexOutOfBoundsException("Page " + page | |
308 | + " out of " + maxPage); | |
309 | } | |
310 | ||
311 | try { | |
312 | stories = searchable.search(tag, page); | |
313 | if (item > 0 && item <= stories.size()) { | |
314 | storyItem = item; | |
315 | } else if (item > 0) { | |
316 | GuiReaderSearchFrame | |
317 | .error(String | |
318 | .format("Story item does not exist: Tag [%s], item %d", | |
319 | tag.getFqName(), item)); | |
320 | } | |
321 | } catch (IOException e) { | |
322 | GuiReaderSearchFrame.error(e); | |
323 | } | |
324 | } | |
325 | } catch (IOException e) { | |
326 | GuiReaderSearchFrame.error(e); | |
327 | maxPage = 0; | |
328 | } | |
329 | } | |
330 | ||
331 | this.stories = stories; | |
332 | this.storyItem = storyItem; | |
333 | this.page = page; | |
334 | this.maxPage = maxPage; | |
335 | } | |
336 | ||
337 | /** | |
338 | * Enables or disables this component, depending on the value of the | |
339 | * parameter <code>b</code>. An enabled component can respond to user input | |
340 | * and generate events. Components are enabled initially by default. | |
341 | * <p> | |
342 | * Disabling this component will also affect its children. | |
343 | * | |
344 | * @param b | |
345 | * If <code>true</code>, this component is enabled; otherwise | |
346 | * this component is disabled | |
347 | */ | |
348 | @Override | |
349 | public void setEnabled(boolean b) { | |
350 | super.setEnabled(b); | |
351 | tagBars.setEnabled(b); | |
352 | for (JComboBox combo : combos) { | |
353 | combo.setEnabled(b); | |
354 | } | |
355 | } | |
356 | } |