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