test: improve flag files
[nikiroo-utils.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderGroup.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.Graphics;
7 import java.awt.Rectangle;
8 import java.awt.event.ActionListener;
9 import java.awt.event.ComponentAdapter;
10 import java.awt.event.ComponentEvent;
11 import java.awt.event.FocusAdapter;
12 import java.awt.event.FocusEvent;
13 import java.awt.event.KeyAdapter;
14 import java.awt.event.KeyEvent;
15 import java.util.ArrayList;
16 import java.util.List;
17
18 import javax.swing.JLabel;
19 import javax.swing.JPanel;
20
21 import be.nikiroo.fanfix.bundles.StringIdGui;
22 import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
23 import be.nikiroo.utils.ui.WrapLayout;
24
25 /**
26 * A group of {@link GuiReaderBook}s for display.
27 *
28 * @author niki
29 */
30 public class GuiReaderGroup extends JPanel {
31 private static final long serialVersionUID = 1L;
32 private BookActionListener action;
33 private Color backgroundColor;
34 private Color backgroundColorDef;
35 private Color backgroundColorDefPane;
36 private GuiReader reader;
37 private List<GuiReaderBookInfo> infos;
38 private List<GuiReaderBook> books;
39 private JPanel pane;
40 private JLabel titleLabel;
41 private boolean words; // words or authors (secondary info on books)
42 private int itemsPerLine;
43
44 /**
45 * Create a new {@link GuiReaderGroup}.
46 *
47 * @param reader
48 * the {@link GuiReaderBook} used to probe some information about
49 * the stories
50 * @param title
51 * the title of this group (can be NULL for "no title", an empty
52 * {@link String} will trigger a default title for empty groups)
53 * @param backgroundColor
54 * the background colour to use (or NULL for default)
55 */
56 public GuiReaderGroup(GuiReader reader, String title, Color backgroundColor) {
57 this.reader = reader;
58
59 this.pane = new JPanel();
60 pane.setLayout(new WrapLayout(WrapLayout.LEADING, 5, 5));
61
62 this.backgroundColorDef = getBackground();
63 this.backgroundColorDefPane = pane.getBackground();
64 setBackground(backgroundColor);
65
66 setLayout(new BorderLayout(0, 10));
67
68 // Make it focusable:
69 setFocusable(true);
70 setEnabled(true);
71 setVisible(true);
72
73 add(pane, BorderLayout.CENTER);
74
75 titleLabel = new JLabel();
76 titleLabel.setHorizontalAlignment(JLabel.CENTER);
77 add(titleLabel, BorderLayout.NORTH);
78 setTitle(title);
79
80 // Compute the number of items per line at each resize
81 addComponentListener(new ComponentAdapter() {
82 @Override
83 public void componentResized(ComponentEvent e) {
84 super.componentResized(e);
85 computeItemsPerLine();
86 }
87 });
88 computeItemsPerLine();
89
90 addKeyListener(new KeyAdapter() {
91 @Override
92 public void keyPressed(KeyEvent e) {
93 onKeyPressed(e);
94 }
95
96 @Override
97 public void keyTyped(KeyEvent e) {
98 onKeyTyped(e);
99 }
100 });
101
102 addFocusListener(new FocusAdapter() {
103 @Override
104 public void focusGained(FocusEvent e) {
105 if (getSelectedBookIndex() < 0) {
106 setSelectedBook(0, true);
107 }
108 }
109
110 @Override
111 public void focusLost(FocusEvent e) {
112 setBackground(null);
113 setSelectedBook(-1, false);
114 }
115 });
116 }
117
118 /**
119 * Note: this class supports NULL as a background colour, which will revert
120 * it to its default state.
121 * <p>
122 * Note: this class' implementation will also set the main pane background
123 * colour at the same time.
124 * <p>
125 * Sets the background colour of this component. The background colour is
126 * used only if the component is opaque, and only by subclasses of
127 * <code>JComponent</code> or <code>ComponentUI</code> implementations.
128 * Direct subclasses of <code>JComponent</code> must override
129 * <code>paintComponent</code> to honour this property.
130 * <p>
131 * It is up to the look and feel to honour this property, some may choose to
132 * ignore it.
133 *
134 * @param backgroundColor
135 * the desired background <code>Colour</code>
136 * @see java.awt.Component#getBackground
137 * @see #setOpaque
138 *
139 * @beaninfo preferred: true bound: true attribute: visualUpdate true
140 * description: The background colour of the component.
141 */
142 @Override
143 public void setBackground(Color backgroundColor) {
144 this.backgroundColor = backgroundColor;
145
146 Color cme = backgroundColor == null ? backgroundColorDef
147 : backgroundColor;
148 Color cpane = backgroundColor == null ? backgroundColorDefPane
149 : backgroundColor;
150
151 if (pane != null) { // can happen at theme setup time
152 pane.setBackground(cpane);
153 }
154 super.setBackground(cme);
155 }
156
157 /**
158 * The title of this group (can be NULL for "no title", an empty
159 * {@link String} will trigger a default title for empty groups)
160 *
161 * @param title
162 * the title or NULL
163 */
164 public void setTitle(String title) {
165 if (title != null) {
166 if (title.isEmpty()) {
167 title = GuiReader.trans(StringIdGui.MENU_AUTHORS_UNKNOWN);
168 }
169
170 titleLabel.setText(String.format("<html>"
171 + "<body style='text-align: center; color: gray;'><br><b>"
172 + "%s" + "</b></body>" + "</html>", title));
173 titleLabel.setVisible(true);
174 } else {
175 titleLabel.setVisible(false);
176 }
177 }
178
179 /**
180 * Compute how many items can fit in a line so UP and DOWN can be used to go
181 * up/down one line at a time.
182 */
183 private void computeItemsPerLine() {
184 itemsPerLine = 1;
185
186 if (books != null && books.size() > 0) {
187 // this.pane holds all the books with a hgap of 5 px
188 int wbook = books.get(0).getWidth() + 5;
189 itemsPerLine = pane.getWidth() / wbook;
190 }
191 }
192
193 /**
194 * Set the {@link ActionListener} that will be fired on each
195 * {@link GuiReaderBook} action.
196 *
197 * @param action
198 * the action
199 */
200 public void setActionListener(BookActionListener action) {
201 this.action = action;
202 refreshBooks();
203 }
204
205 /**
206 * Clear all the books in this {@link GuiReaderGroup}.
207 */
208 public void clear() {
209 refreshBooks(new ArrayList<GuiReaderBookInfo>());
210 }
211
212 /**
213 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
214 */
215 public void refreshBooks() {
216 refreshBooks(infos, words);
217 }
218
219 /**
220 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
221 *
222 * @param infos
223 * the new list of infos
224 */
225 public void refreshBooks(List<GuiReaderBookInfo> infos) {
226 refreshBooks(infos, words);
227 }
228
229 /**
230 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
231 *
232 * @param infos
233 * the new list of infos
234 * @param seeWordcount
235 * TRUE to see word counts, FALSE to see authors
236 */
237 public void refreshBooks(List<GuiReaderBookInfo> infos, boolean seeWordcount) {
238 this.infos = infos;
239 refreshBooks(seeWordcount);
240 }
241
242 /**
243 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
244 * <p>
245 * Will not change the current stories.
246 *
247 * @param seeWordcount
248 * TRUE to see word counts, FALSE to see authors
249 */
250 public void refreshBooks(boolean seeWordcount) {
251 this.words = seeWordcount;
252
253 books = new ArrayList<GuiReaderBook>();
254 invalidate();
255 pane.invalidate();
256 pane.removeAll();
257
258 if (infos != null) {
259 for (GuiReaderBookInfo info : infos) {
260 boolean isCached = false;
261 if (info.getMeta() != null && info.getMeta().getLuid() != null) {
262 isCached = reader.isCached(info.getMeta().getLuid());
263 }
264
265 GuiReaderBook book = new GuiReaderBook(reader, info, isCached,
266 words);
267 if (backgroundColor != null) {
268 book.setBackground(backgroundColor);
269 }
270
271 books.add(book);
272
273 book.addActionListener(new BookActionListener() {
274 @Override
275 public void select(GuiReaderBook book) {
276 GuiReaderGroup.this.requestFocusInWindow();
277 for (GuiReaderBook abook : books) {
278 abook.setSelected(abook == book);
279 }
280 }
281
282 @Override
283 public void popupRequested(GuiReaderBook book,
284 Component target, int x, int y) {
285 }
286
287 @Override
288 public void action(GuiReaderBook book) {
289 }
290 });
291
292 if (action != null) {
293 book.addActionListener(action);
294 }
295
296 pane.add(book);
297 }
298 }
299
300 pane.validate();
301 pane.repaint();
302 validate();
303 repaint();
304
305 computeItemsPerLine();
306 }
307
308 /**
309 * Enables or disables this component, depending on the value of the
310 * parameter <code>b</code>. An enabled component can respond to user input
311 * and generate events. Components are enabled initially by default.
312 * <p>
313 * Disabling this component will also affect its children.
314 *
315 * @param b
316 * If <code>true</code>, this component is enabled; otherwise
317 * this component is disabled
318 */
319 @Override
320 public void setEnabled(boolean b) {
321 if (books != null) {
322 for (GuiReaderBook book : books) {
323 book.setEnabled(b);
324 book.repaint();
325 }
326 }
327
328 pane.setEnabled(b);
329 super.setEnabled(b);
330 repaint();
331 }
332
333 /**
334 * The number of books in this group.
335 *
336 * @return the count
337 */
338 public int getBooksCount() {
339 return books.size();
340 }
341
342 /**
343 * Return the index of the currently selected book if any, -1 if none.
344 *
345 * @return the index or -1
346 */
347 public int getSelectedBookIndex() {
348 int index = -1;
349 for (int i = 0; i < books.size(); i++) {
350 if (books.get(i).isSelected()) {
351 index = i;
352 break;
353 }
354 }
355 return index;
356 }
357
358 /**
359 * Select the given book, or unselect all items.
360 *
361 * @param index
362 * the index of the book to select, can be outside the bounds
363 * (either all the items will be unselected or the first or last
364 * book will then be selected, see <tt>forceRange></tt>)
365 * @param forceRange
366 * TRUE to constraint the index to the first/last element, FALSE
367 * to unselect when outside the range
368 */
369 public void setSelectedBook(int index, boolean forceRange) {
370 int previousIndex = getSelectedBookIndex();
371
372 if (index >= books.size()) {
373 if (forceRange) {
374 index = books.size() - 1;
375 } else {
376 index = -1;
377 }
378 }
379
380 if (index < 0 && forceRange) {
381 index = 0;
382 }
383
384 if (previousIndex >= 0) {
385 books.get(previousIndex).setSelected(false);
386 }
387
388 if (index >= 0 && !books.isEmpty()) {
389 books.get(index).setSelected(true);
390 }
391 }
392
393 /**
394 * The action to execute when a key is typed.
395 *
396 * @param e
397 * the key event
398 */
399 private void onKeyTyped(KeyEvent e) {
400 boolean consumed = false;
401 boolean action = e.getKeyChar() == '\n';
402 boolean popup = e.getKeyChar() == ' ';
403 if (action || popup) {
404 consumed = true;
405
406 int index = getSelectedBookIndex();
407 if (index >= 0) {
408 GuiReaderBook book = books.get(index);
409 if (action) {
410 book.action();
411 } else if (popup) {
412 book.popup(book, book.getWidth() / 2, book.getHeight() / 2);
413 }
414 }
415 }
416
417 if (consumed) {
418 e.consume();
419 }
420 }
421
422 /**
423 * The action to execute when a key is pressed.
424 *
425 * @param e
426 * the key event
427 */
428 private void onKeyPressed(KeyEvent e) {
429 boolean consumed = false;
430 if (e.isActionKey()) {
431 int offset = 0;
432 switch (e.getKeyCode()) {
433 case KeyEvent.VK_LEFT:
434 offset = -1;
435 break;
436 case KeyEvent.VK_RIGHT:
437 offset = 1;
438 break;
439 case KeyEvent.VK_UP:
440 offset = -itemsPerLine;
441 break;
442 case KeyEvent.VK_DOWN:
443 offset = itemsPerLine;
444 break;
445 }
446
447 if (offset != 0) {
448 consumed = true;
449
450 int previousIndex = getSelectedBookIndex();
451 if (previousIndex >= 0) {
452 setSelectedBook(previousIndex + offset, true);
453 }
454 }
455 }
456
457 if (consumed) {
458 e.consume();
459 }
460 }
461
462 @Override
463 public void paint(Graphics g) {
464 super.paint(g);
465
466 Rectangle clip = g.getClipBounds();
467 if (clip.getWidth() <= 0 || clip.getHeight() <= 0) {
468 return;
469 }
470
471 if (!isEnabled()) {
472 g.setColor(new Color(128, 128, 128, 128));
473 g.fillRect(clip.x, clip.y, clip.width, clip.height);
474 }
475 }
476 }