db21c49c8c0d9414c4be0d6e3b1d4f41c043ed52
[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.event.ActionListener;
6 import java.awt.event.ComponentAdapter;
7 import java.awt.event.ComponentEvent;
8 import java.awt.event.FocusAdapter;
9 import java.awt.event.FocusEvent;
10 import java.awt.event.KeyAdapter;
11 import java.awt.event.KeyEvent;
12 import java.awt.event.MouseEvent;
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 boolean words; // words or authors (secondary info on books)
37 private int itemsPerLine;
38
39 /**
40 * Create a new {@link GuiReaderGroup}.
41 *
42 * @param reader
43 * the {@link GuiReaderBook} used to probe some information about
44 * the stories
45 * @param title
46 * the title of this group
47 * @param backgroundColor
48 * the background colour to use (or NULL for default)
49 */
50 public GuiReaderGroup(GuiReader reader, String title, Color backgroundColor) {
51 this.reader = reader;
52 this.backgroundColor = backgroundColor;
53
54 this.pane = new JPanel();
55
56 pane.setLayout(new WrapLayout(WrapLayout.LEADING, 5, 5));
57 if (backgroundColor != null) {
58 pane.setBackground(backgroundColor);
59 setBackground(backgroundColor);
60 }
61
62 setLayout(new BorderLayout(0, 10));
63
64 // Make it focusable:
65 setFocusable(true);
66 setEnabled(true);
67 setVisible(true);
68
69 add(pane, BorderLayout.CENTER);
70
71 if (title != null) {
72 if (title.isEmpty()) {
73 title = GuiReader.trans(StringIdGui.MENU_AUTHORS_UNKNOWN);
74 }
75
76 JLabel label = new JLabel();
77 label.setText(String.format("<html>"
78 + "<body style='text-align: center; color: gray;'><br><b>"
79 + "%s" + "</b></body>" + "</html>", title));
80 label.setHorizontalAlignment(JLabel.CENTER);
81 add(label, BorderLayout.NORTH);
82 }
83
84 // Compute the number of items per line at each resize
85 addComponentListener(new ComponentAdapter() {
86 @Override
87 public void componentResized(ComponentEvent e) {
88 super.componentResized(e);
89 computeItemsPerLine();
90 }
91 });
92 computeItemsPerLine();
93
94 addKeyListener(new KeyAdapter() {
95 @Override
96 public void keyPressed(KeyEvent e) {
97 onKeyPressed(e);
98 }
99
100 @Override
101 public void keyTyped(KeyEvent e) {
102 onKeyTyped(e);
103 }
104 });
105
106 addFocusListener(new FocusAdapter() {
107 @Override
108 public void focusGained(FocusEvent e) {
109 if (getSelectedBookIndex() < 0) {
110 setSelectedBook(0, true);
111 }
112 }
113
114 @Override
115 public void focusLost(FocusEvent e) {
116 setBackground(null);
117 setSelectedBook(-1, false);
118 }
119 });
120 }
121
122 /**
123 * Compute how many items can fit in a line so UP and DOWN can be used to go
124 * up/down one line at a time.
125 */
126 private void computeItemsPerLine() {
127 // TODO
128 itemsPerLine = 5;
129 }
130
131 /**
132 * Set the {@link ActionListener} that will be fired on each
133 * {@link GuiReaderBook} action.
134 *
135 * @param action
136 * the action
137 */
138 public void setActionListener(BookActionListener action) {
139 this.action = action;
140 refreshBooks(infos, words);
141 }
142
143 /**
144 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
145 *
146 * @param infos
147 * the new list of infos
148 * @param seeWordcount
149 * TRUE to see word counts, FALSE to see authors
150 */
151 public void refreshBooks(List<GuiReaderBookInfo> infos, boolean seeWordcount) {
152 this.infos = infos;
153 refreshBooks(seeWordcount);
154 }
155
156 /**
157 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
158 * <p>
159 * Will not change the current stories.
160 *
161 * @param seeWordcount
162 * TRUE to see word counts, FALSE to see authors
163 */
164 public void refreshBooks(boolean seeWordcount) {
165 this.words = seeWordcount;
166
167 books = new ArrayList<GuiReaderBook>();
168 invalidate();
169 pane.invalidate();
170 pane.removeAll();
171
172 if (infos != null) {
173 for (GuiReaderBookInfo info : infos) {
174 boolean isCached = false;
175 if (info.getMeta() != null) {
176 isCached = reader.isCached(info.getMeta().getLuid());
177 }
178
179 GuiReaderBook book = new GuiReaderBook(reader, info, isCached,
180 words);
181 if (backgroundColor != null) {
182 book.setBackground(backgroundColor);
183 }
184
185 books.add(book);
186
187 book.addActionListener(new BookActionListener() {
188 @Override
189 public void select(GuiReaderBook book) {
190 GuiReaderGroup.this.requestFocusInWindow();
191 for (GuiReaderBook abook : books) {
192 abook.setSelected(abook == book);
193 }
194 }
195
196 @Override
197 public void popupRequested(GuiReaderBook book, MouseEvent e) {
198 }
199
200 @Override
201 public void action(GuiReaderBook book) {
202 }
203 });
204
205 if (action != null) {
206 book.addActionListener(action);
207 }
208
209 pane.add(book);
210 }
211 }
212
213 pane.validate();
214 pane.repaint();
215 validate();
216 repaint();
217 }
218
219 /**
220 * Enables or disables this component, depending on the value of the
221 * parameter <code>b</code>. An enabled component can respond to user input
222 * and generate events. Components are enabled initially by default.
223 * <p>
224 * Disabling this component will also affect its children.
225 *
226 * @param b
227 * If <code>true</code>, this component is enabled; otherwise
228 * this component is disabled
229 */
230 @Override
231 public void setEnabled(boolean b) {
232 if (books != null) {
233 for (GuiReaderBook book : books) {
234 book.setEnabled(b);
235 book.repaint();
236 }
237 }
238
239 pane.setEnabled(b);
240 super.setEnabled(b);
241 repaint();
242 }
243
244 /**
245 * Return the index of the currently selected book if any, -1 if none.
246 *
247 * @return the index or -1
248 */
249 private int getSelectedBookIndex() {
250 int index = -1;
251 for (int i = 0; i < books.size(); i++) {
252 if (books.get(i).isSelected()) {
253 index = i;
254 break;
255 }
256 }
257 return index;
258 }
259
260 /**
261 * Select the given book, or unselect all items.
262 *
263 * @param index
264 * the index of the book to select, can be outside the bounds
265 * (either all the items will be unselected or the first or last
266 * book will then be selected, see <tt>forceRange>/tt>)
267 * @param forceRange
268 * TRUE to constraint the index to the first/last element, FALSE
269 * to unselect when outside the range
270 */
271 private void setSelectedBook(int index, boolean forceRange) {
272 int previousIndex = getSelectedBookIndex();
273
274 if (index >= books.size()) {
275 if (forceRange) {
276 index = books.size() - 1;
277 } else {
278 index = -1;
279 }
280 }
281
282 if (index < 0 && forceRange) {
283 index = 0;
284 }
285
286 if (previousIndex >= 0) {
287 books.get(previousIndex).setSelected(false);
288 }
289
290 if (index >= 0) {
291 books.get(index).setSelected(true);
292 }
293 }
294
295 /**
296 * The action to execute when a key is typed.
297 *
298 * @param e
299 * the key event
300 */
301 private void onKeyTyped(KeyEvent e) {
302 boolean consumed = false;
303 if (e.getKeyChar() == '\n') {
304 consumed = true;
305
306 int index = getSelectedBookIndex();
307 if (index >= 0) {
308 books.get(index).action();
309 }
310 }
311
312 if (consumed) {
313 e.consume();
314 }
315 }
316
317 /**
318 * The action to execute when a key is pressed.
319 *
320 * @param e
321 * the key event
322 */
323 private void onKeyPressed(KeyEvent e) {
324 boolean consumed = false;
325 if (e.isActionKey()) {
326 int offset = 0;
327 switch (e.getKeyCode()) {
328 case KeyEvent.VK_LEFT:
329 offset = -1;
330 break;
331 case KeyEvent.VK_RIGHT:
332 offset = 1;
333 break;
334 case KeyEvent.VK_UP:
335 offset = -itemsPerLine;
336 break;
337 case KeyEvent.VK_DOWN:
338 offset = itemsPerLine;
339 break;
340 }
341
342 if (offset != 0) {
343 consumed = true;
344
345 int previousIndex = getSelectedBookIndex();
346 if (previousIndex >= 0) {
347 setSelectedBook(previousIndex + offset, true);
348 }
349 }
350 }
351
352 if (consumed) {
353 e.consume();
354 }
355 }
356 }