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