search: cleanup
[nikiroo-utils.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 GuiReader reader;
33 private List<GuiReaderBookInfo> infos;
34 private List<GuiReaderBook> books;
35 private JPanel pane;
36 private JLabel titleLabel;
37 private boolean words; // words or authors (secondary info on books)
38 private int itemsPerLine;
39
40 /**
41 * Create a new {@link GuiReaderGroup}.
42 *
43 * @param reader
44 * the {@link GuiReaderBook} used to probe some information about
45 * the stories
46 * @param title
47 * the title of this group (can be NULL for "no title", an empty
48 * {@link String} will trigger a default title for empty groups)
49 * @param backgroundColor
50 * the background colour to use (or NULL for default)
51 */
52 public GuiReaderGroup(GuiReader reader, String title, Color backgroundColor) {
53 this.reader = reader;
54 this.backgroundColor = backgroundColor;
55
56 this.pane = new JPanel();
57
58 pane.setLayout(new WrapLayout(WrapLayout.LEADING, 5, 5));
59 if (backgroundColor != null) {
60 pane.setBackground(backgroundColor);
61 setBackground(backgroundColor);
62 }
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 * The title of this group (can be NULL for "no title", an empty
118 * {@link String} will trigger a default title for empty groups)
119 *
120 * @param title
121 * the title or NULL
122 */
123 public void setTitle(String title) {
124 if (title != null) {
125 if (title.isEmpty()) {
126 title = GuiReader.trans(StringIdGui.MENU_AUTHORS_UNKNOWN);
127 }
128
129 titleLabel.setText(String.format("<html>"
130 + "<body style='text-align: center; color: gray;'><br><b>"
131 + "%s" + "</b></body>" + "</html>", title));
132 titleLabel.setVisible(true);
133 } else {
134 titleLabel.setVisible(false);
135 }
136 }
137
138 /**
139 * Compute how many items can fit in a line so UP and DOWN can be used to go
140 * up/down one line at a time.
141 */
142 private void computeItemsPerLine() {
143 // TODO
144 itemsPerLine = 5;
145 }
146
147 /**
148 * Set the {@link ActionListener} that will be fired on each
149 * {@link GuiReaderBook} action.
150 *
151 * @param action
152 * the action
153 */
154 public void setActionListener(BookActionListener action) {
155 this.action = action;
156 refreshBooks(infos, words);
157 }
158
159 /**
160 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
161 *
162 * @param infos
163 * the new list of infos
164 * @param seeWordcount
165 * TRUE to see word counts, FALSE to see authors
166 */
167 public void refreshBooks(List<GuiReaderBookInfo> infos, boolean seeWordcount) {
168 this.infos = infos;
169 refreshBooks(seeWordcount);
170 }
171
172 /**
173 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
174 * <p>
175 * Will not change the current stories.
176 *
177 * @param seeWordcount
178 * TRUE to see word counts, FALSE to see authors
179 */
180 public void refreshBooks(boolean seeWordcount) {
181 this.words = seeWordcount;
182
183 books = new ArrayList<GuiReaderBook>();
184 invalidate();
185 pane.invalidate();
186 pane.removeAll();
187
188 if (infos != null) {
189 for (GuiReaderBookInfo info : infos) {
190 boolean isCached = false;
191 if (info.getMeta() != null && info.getMeta().getLuid() != null) {
192 isCached = reader.isCached(info.getMeta().getLuid());
193 }
194
195 GuiReaderBook book = new GuiReaderBook(reader, info, isCached,
196 words);
197 if (backgroundColor != null) {
198 book.setBackground(backgroundColor);
199 }
200
201 books.add(book);
202
203 book.addActionListener(new BookActionListener() {
204 @Override
205 public void select(GuiReaderBook book) {
206 GuiReaderGroup.this.requestFocusInWindow();
207 for (GuiReaderBook abook : books) {
208 abook.setSelected(abook == book);
209 }
210 }
211
212 @Override
213 public void popupRequested(GuiReaderBook book,
214 Component target, int x, int y) {
215 }
216
217 @Override
218 public void action(GuiReaderBook book) {
219 }
220 });
221
222 if (action != null) {
223 book.addActionListener(action);
224 }
225
226 pane.add(book);
227 }
228 }
229
230 pane.validate();
231 pane.repaint();
232 validate();
233 repaint();
234 }
235
236 /**
237 * Enables or disables this component, depending on the value of the
238 * parameter <code>b</code>. An enabled component can respond to user input
239 * and generate events. Components are enabled initially by default.
240 * <p>
241 * Disabling this component will also affect its children.
242 *
243 * @param b
244 * If <code>true</code>, this component is enabled; otherwise
245 * this component is disabled
246 */
247 @Override
248 public void setEnabled(boolean b) {
249 if (books != null) {
250 for (GuiReaderBook book : books) {
251 book.setEnabled(b);
252 book.repaint();
253 }
254 }
255
256 pane.setEnabled(b);
257 super.setEnabled(b);
258 repaint();
259 }
260
261 /**
262 * The number of books in this group.
263 *
264 * @return the count
265 */
266 public int getBooksCount() {
267 return books.size();
268 }
269
270 /**
271 * Return the index of the currently selected book if any, -1 if none.
272 *
273 * @return the index or -1
274 */
275 public int getSelectedBookIndex() {
276 int index = -1;
277 for (int i = 0; i < books.size(); i++) {
278 if (books.get(i).isSelected()) {
279 index = i;
280 break;
281 }
282 }
283 return index;
284 }
285
286 /**
287 * Select the given book, or unselect all items.
288 *
289 * @param index
290 * the index of the book to select, can be outside the bounds
291 * (either all the items will be unselected or the first or last
292 * book will then be selected, see <tt>forceRange>/tt>)
293 * @param forceRange
294 * TRUE to constraint the index to the first/last element, FALSE
295 * to unselect when outside the range
296 */
297 public void setSelectedBook(int index, boolean forceRange) {
298 int previousIndex = getSelectedBookIndex();
299
300 if (index >= books.size()) {
301 if (forceRange) {
302 index = books.size() - 1;
303 } else {
304 index = -1;
305 }
306 }
307
308 if (index < 0 && forceRange) {
309 index = 0;
310 }
311
312 if (previousIndex >= 0) {
313 books.get(previousIndex).setSelected(false);
314 }
315
316 if (index >= 0 && !books.isEmpty()) {
317 books.get(index).setSelected(true);
318 }
319 }
320
321 /**
322 * The action to execute when a key is typed.
323 *
324 * @param e
325 * the key event
326 */
327 private void onKeyTyped(KeyEvent e) {
328 boolean consumed = false;
329 boolean action = e.getKeyChar() == '\n';
330 boolean popup = e.getKeyChar() == ' ';
331 if (action || popup) {
332 consumed = true;
333
334 int index = getSelectedBookIndex();
335 if (index >= 0) {
336 GuiReaderBook book = books.get(index);
337 if (action) {
338 book.action();
339 } else if (popup) {
340 book.popup(book, book.getWidth() / 2, book.getHeight() / 2);
341 }
342 }
343 }
344
345 if (consumed) {
346 e.consume();
347 }
348 }
349
350 /**
351 * The action to execute when a key is pressed.
352 *
353 * @param e
354 * the key event
355 */
356 private void onKeyPressed(KeyEvent e) {
357 boolean consumed = false;
358 if (e.isActionKey()) {
359 int offset = 0;
360 switch (e.getKeyCode()) {
361 case KeyEvent.VK_LEFT:
362 offset = -1;
363 break;
364 case KeyEvent.VK_RIGHT:
365 offset = 1;
366 break;
367 case KeyEvent.VK_UP:
368 offset = -itemsPerLine;
369 break;
370 case KeyEvent.VK_DOWN:
371 offset = itemsPerLine;
372 break;
373 }
374
375 if (offset != 0) {
376 consumed = true;
377
378 int previousIndex = getSelectedBookIndex();
379 if (previousIndex >= 0) {
380 setSelectedBook(previousIndex + offset, true);
381 }
382 }
383 }
384
385 if (consumed) {
386 e.consume();
387 }
388 }
389 }