17bf43cbb455a66bb357e68ddb53f0d97d426ebb
[fanfix.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 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
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
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() {
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 }
189 }
190
191 /**
192 * Set the {@link ActionListener} that will be fired on each
193 * {@link GuiReaderBook} action.
194 *
195 * @param action
196 * the action
197 */
198 public void setActionListener(BookActionListener action) {
199 this.action = action;
200 refreshBooks(infos, words);
201 }
202
203 /**
204 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
205 *
206 * @param infos
207 * the new list of infos
208 * @param seeWordcount
209 * TRUE to see word counts, FALSE to see authors
210 */
211 public void refreshBooks(List<GuiReaderBookInfo> infos, boolean seeWordcount) {
212 this.infos = infos;
213 refreshBooks(seeWordcount);
214 }
215
216 /**
217 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
218 * <p>
219 * Will not change the current stories.
220 *
221 * @param seeWordcount
222 * TRUE to see word counts, FALSE to see authors
223 */
224 public void refreshBooks(boolean seeWordcount) {
225 this.words = seeWordcount;
226
227 books = new ArrayList<GuiReaderBook>();
228 invalidate();
229 pane.invalidate();
230 pane.removeAll();
231
232 if (infos != null) {
233 for (GuiReaderBookInfo info : infos) {
234 boolean isCached = false;
235 if (info.getMeta() != null && info.getMeta().getLuid() != null) {
236 isCached = reader.isCached(info.getMeta().getLuid());
237 }
238
239 GuiReaderBook book = new GuiReaderBook(reader, info, isCached,
240 words);
241 if (backgroundColor != null) {
242 book.setBackground(backgroundColor);
243 }
244
245 books.add(book);
246
247 book.addActionListener(new BookActionListener() {
248 @Override
249 public void select(GuiReaderBook book) {
250 GuiReaderGroup.this.requestFocusInWindow();
251 for (GuiReaderBook abook : books) {
252 abook.setSelected(abook == book);
253 }
254 }
255
256 @Override
257 public void popupRequested(GuiReaderBook book,
258 Component target, int x, int y) {
259 }
260
261 @Override
262 public void action(GuiReaderBook book) {
263 }
264 });
265
266 if (action != null) {
267 book.addActionListener(action);
268 }
269
270 pane.add(book);
271 }
272 }
273
274 pane.validate();
275 pane.repaint();
276 validate();
277 repaint();
278
279 computeItemsPerLine();
280 }
281
282 /**
283 * Enables or disables this component, depending on the value of the
284 * parameter <code>b</code>. An enabled component can respond to user input
285 * and generate events. Components are enabled initially by default.
286 * <p>
287 * Disabling this component will also affect its children.
288 *
289 * @param b
290 * If <code>true</code>, this component is enabled; otherwise
291 * this component is disabled
292 */
293 @Override
294 public void setEnabled(boolean b) {
295 if (books != null) {
296 for (GuiReaderBook book : books) {
297 book.setEnabled(b);
298 book.repaint();
299 }
300 }
301
302 pane.setEnabled(b);
303 super.setEnabled(b);
304 repaint();
305 }
306
307 /**
308 * The number of books in this group.
309 *
310 * @return the count
311 */
312 public int getBooksCount() {
313 return books.size();
314 }
315
316 /**
317 * Return the index of the currently selected book if any, -1 if none.
318 *
319 * @return the index or -1
320 */
321 public int getSelectedBookIndex() {
322 int index = -1;
323 for (int i = 0; i < books.size(); i++) {
324 if (books.get(i).isSelected()) {
325 index = i;
326 break;
327 }
328 }
329 return index;
330 }
331
332 /**
333 * Select the given book, or unselect all items.
334 *
335 * @param index
336 * the index of the book to select, can be outside the bounds
337 * (either all the items will be unselected or the first or last
338 * book will then be selected, see <tt>forceRange>/tt>)
339 * @param forceRange
340 * TRUE to constraint the index to the first/last element, FALSE
341 * to unselect when outside the range
342 */
343 public void setSelectedBook(int index, boolean forceRange) {
344 int previousIndex = getSelectedBookIndex();
345
346 if (index >= books.size()) {
347 if (forceRange) {
348 index = books.size() - 1;
349 } else {
350 index = -1;
351 }
352 }
353
354 if (index < 0 && forceRange) {
355 index = 0;
356 }
357
358 if (previousIndex >= 0) {
359 books.get(previousIndex).setSelected(false);
360 }
361
362 if (index >= 0 && !books.isEmpty()) {
363 books.get(index).setSelected(true);
364 }
365 }
366
367 /**
368 * The action to execute when a key is typed.
369 *
370 * @param e
371 * the key event
372 */
373 private void onKeyTyped(KeyEvent e) {
374 boolean consumed = false;
375 boolean action = e.getKeyChar() == '\n';
376 boolean popup = e.getKeyChar() == ' ';
377 if (action || popup) {
378 consumed = true;
379
380 int index = getSelectedBookIndex();
381 if (index >= 0) {
382 GuiReaderBook book = books.get(index);
383 if (action) {
384 book.action();
385 } else if (popup) {
386 book.popup(book, book.getWidth() / 2, book.getHeight() / 2);
387 }
388 }
389 }
390
391 if (consumed) {
392 e.consume();
393 }
394 }
395
396 /**
397 * The action to execute when a key is pressed.
398 *
399 * @param e
400 * the key event
401 */
402 private void onKeyPressed(KeyEvent e) {
403 boolean consumed = false;
404 if (e.isActionKey()) {
405 int offset = 0;
406 switch (e.getKeyCode()) {
407 case KeyEvent.VK_LEFT:
408 offset = -1;
409 break;
410 case KeyEvent.VK_RIGHT:
411 offset = 1;
412 break;
413 case KeyEvent.VK_UP:
414 offset = -itemsPerLine;
415 break;
416 case KeyEvent.VK_DOWN:
417 offset = itemsPerLine;
418 break;
419 }
420
421 if (offset != 0) {
422 consumed = true;
423
424 int previousIndex = getSelectedBookIndex();
425 if (previousIndex >= 0) {
426 setSelectedBook(previousIndex + offset, true);
427 }
428 }
429 }
430
431 if (consumed) {
432 e.consume();
433 }
434 }
435
436 @Override
437 public void paint(Graphics g) {
438 super.paint(g);
439
440 Rectangle clip = g.getClipBounds();
441 if (clip.getWidth() <= 0 || clip.getHeight() <= 0) {
442 return;
443 }
444
445 if (!isEnabled()) {
446 g.setColor(new Color(128, 128, 128, 128));
447 g.fillRect(clip.x, clip.y, clip.width, clip.height);
448 }
449 }
450 }