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