Merge branch 'subtree'
[nikiroo-utils.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 118 /**
a3550a0a 119 * Note: this class supports NULL as a background colour, which will revert
c499d79f
NR
120 * it to its default state.
121 * <p>
122 * Note: this class' implementation will also set the main pane background
a3550a0a 123 * colour at the same time.
c499d79f 124 * <p>
a3550a0a
NR
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
c499d79f
NR
127 * <code>JComponent</code> or <code>ComponentUI</code> implementations.
128 * Direct subclasses of <code>JComponent</code> must override
a3550a0a 129 * <code>paintComponent</code> to honour this property.
c499d79f 130 * <p>
a3550a0a 131 * It is up to the look and feel to honour this property, some may choose to
c499d79f
NR
132 * ignore it.
133 *
a3550a0a
NR
134 * @param backgroundColor
135 * the desired background <code>Colour</code>
c499d79f
NR
136 * @see java.awt.Component#getBackground
137 * @see #setOpaque
138 *
139 * @beaninfo preferred: true bound: true attribute: visualUpdate true
a3550a0a 140 * description: The background colour of the component.
c499d79f
NR
141 */
142 @Override
143 public void setBackground(Color backgroundColor) {
a3550a0a
NR
144 this.backgroundColor = backgroundColor;
145
c499d79f
NR
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
e92e4ae3
NR
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
07e0fc1e
NR
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() {
b02c7819
NR
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 }
4310bae9
NR
191 }
192
193 /**
194 * Set the {@link ActionListener} that will be fired on each
5dd985cf 195 * {@link GuiReaderBook} action.
4310bae9
NR
196 *
197 * @param action
198 * the action
199 */
200 public void setActionListener(BookActionListener action) {
201 this.action = action;
a12b668f
NR
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.
a12b668f
NR
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) {
79a99506 226 refreshBooks(infos, words);
4310bae9
NR
227 }
228
229 /**
5dd985cf 230 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
4310bae9 231 *
c349fd48
NR
232 * @param infos
233 * the new list of infos
793f1071
NR
234 * @param seeWordcount
235 * TRUE to see word counts, FALSE to see authors
4310bae9 236 */
fb1ffdd0
NR
237 public void refreshBooks(List<GuiReaderBookInfo> infos, boolean seeWordcount) {
238 this.infos = infos;
8590da19
NR
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) {
793f1071 251 this.words = seeWordcount;
4310bae9 252
5dd985cf 253 books = new ArrayList<GuiReaderBook>();
4310bae9
NR
254 invalidate();
255 pane.invalidate();
256 pane.removeAll();
257
fb1ffdd0
NR
258 if (infos != null) {
259 for (GuiReaderBookInfo info : infos) {
79a99506 260 boolean isCached = false;
b31a0db0 261 if (info.getMeta() != null && info.getMeta().getLuid() != null) {
79a99506
NR
262 isCached = reader.isCached(info.getMeta().getLuid());
263 }
264
265 GuiReaderBook book = new GuiReaderBook(reader, info, isCached,
fb1ffdd0 266 words);
4310bae9
NR
267 if (backgroundColor != null) {
268 book.setBackground(backgroundColor);
269 }
270
271 books.add(book);
272
273 book.addActionListener(new BookActionListener() {
211f7ddb 274 @Override
5dd985cf 275 public void select(GuiReaderBook book) {
17fafa56 276 GuiReaderGroup.this.requestFocusInWindow();
5dd985cf 277 for (GuiReaderBook abook : books) {
4310bae9
NR
278 abook.setSelected(abook == book);
279 }
280 }
281
211f7ddb 282 @Override
484a31aa
NR
283 public void popupRequested(GuiReaderBook book,
284 Component target, int x, int y) {
4310bae9
NR
285 }
286
211f7ddb 287 @Override
5dd985cf 288 public void action(GuiReaderBook book) {
4310bae9
NR
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();
b02c7819
NR
304
305 computeItemsPerLine();
4310bae9
NR
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) {
5dd985cf 322 for (GuiReaderBook book : books) {
4310bae9
NR
323 book.setEnabled(b);
324 book.repaint();
325 }
326 }
327
328 pane.setEnabled(b);
329 super.setEnabled(b);
330 repaint();
331 }
07e0fc1e 332
e92e4ae3
NR
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
17fafa56
NR
342 /**
343 * Return the index of the currently selected book if any, -1 if none.
344 *
345 * @return the index or -1
346 */
e92e4ae3 347 public int getSelectedBookIndex() {
17fafa56
NR
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
dc3b0033 364 * book will then be selected, see <tt>forceRange></tt>)
17fafa56
NR
365 * @param forceRange
366 * TRUE to constraint the index to the first/last element, FALSE
367 * to unselect when outside the range
368 */
e92e4ae3 369 public void setSelectedBook(int index, boolean forceRange) {
17fafa56
NR
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
e0fa20fe 388 if (index >= 0 && !books.isEmpty()) {
17fafa56
NR
389 books.get(index).setSelected(true);
390 }
391 }
392
07e0fc1e
NR
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;
484a31aa
NR
401 boolean action = e.getKeyChar() == '\n';
402 boolean popup = e.getKeyChar() == ' ';
403 if (action || popup) {
17fafa56
NR
404 consumed = true;
405
406 int index = getSelectedBookIndex();
407 if (index >= 0) {
484a31aa
NR
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 }
17fafa56
NR
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;
07e0fc1e
NR
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:
17fafa56 440 offset = -itemsPerLine;
07e0fc1e
NR
441 break;
442 case KeyEvent.VK_DOWN:
17fafa56 443 offset = itemsPerLine;
07e0fc1e
NR
444 break;
445 }
446
447 if (offset != 0) {
448 consumed = true;
449
17fafa56
NR
450 int previousIndex = getSelectedBookIndex();
451 if (previousIndex >= 0) {
452 setSelectedBook(previousIndex + offset, true);
07e0fc1e
NR
453 }
454 }
455 }
456
457 if (consumed) {
458 e.consume();
07e0fc1e
NR
459 }
460 }
ed8cda22
NR
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 }
4310bae9 476}