Fanfiction step2 + SearchableTags
[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 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,
198 Component target, int x, int y) {
199 }
200
201 @Override
202 public void action(GuiReaderBook book) {
203 }
204 });
205
206 if (action != null) {
207 book.addActionListener(action);
208 }
209
210 pane.add(book);
211 }
212 }
213
214 pane.validate();
215 pane.repaint();
216 validate();
217 repaint();
218 }
219
220 /**
221 * Enables or disables this component, depending on the value of the
222 * parameter <code>b</code>. An enabled component can respond to user input
223 * and generate events. Components are enabled initially by default.
224 * <p>
225 * Disabling this component will also affect its children.
226 *
227 * @param b
228 * If <code>true</code>, this component is enabled; otherwise
229 * this component is disabled
230 */
231 @Override
232 public void setEnabled(boolean b) {
233 if (books != null) {
234 for (GuiReaderBook book : books) {
235 book.setEnabled(b);
236 book.repaint();
237 }
238 }
239
240 pane.setEnabled(b);
241 super.setEnabled(b);
242 repaint();
243 }
244
245 /**
246 * Return the index of the currently selected book if any, -1 if none.
247 *
248 * @return the index or -1
249 */
250 private int getSelectedBookIndex() {
251 int index = -1;
252 for (int i = 0; i < books.size(); i++) {
253 if (books.get(i).isSelected()) {
254 index = i;
255 break;
256 }
257 }
258 return index;
259 }
260
261 /**
262 * Select the given book, or unselect all items.
263 *
264 * @param index
265 * the index of the book to select, can be outside the bounds
266 * (either all the items will be unselected or the first or last
267 * book will then be selected, see <tt>forceRange>/tt>)
268 * @param forceRange
269 * TRUE to constraint the index to the first/last element, FALSE
270 * to unselect when outside the range
271 */
272 private void setSelectedBook(int index, boolean forceRange) {
273 int previousIndex = getSelectedBookIndex();
274
275 if (index >= books.size()) {
276 if (forceRange) {
277 index = books.size() - 1;
278 } else {
279 index = -1;
280 }
281 }
282
283 if (index < 0 && forceRange) {
284 index = 0;
285 }
286
287 if (previousIndex >= 0) {
288 books.get(previousIndex).setSelected(false);
289 }
290
291 if (index >= 0) {
292 books.get(index).setSelected(true);
293 }
294 }
295
296 /**
297 * The action to execute when a key is typed.
298 *
299 * @param e
300 * the key event
301 */
302 private void onKeyTyped(KeyEvent e) {
303 boolean consumed = false;
304 boolean action = e.getKeyChar() == '\n';
305 boolean popup = e.getKeyChar() == ' ';
306 if (action || popup) {
307 consumed = true;
308
309 int index = getSelectedBookIndex();
310 if (index >= 0) {
311 GuiReaderBook book = books.get(index);
312 if (action) {
313 book.action();
314 } else if (popup) {
315 book.popup(book, book.getWidth() / 2, book.getHeight() / 2);
316 }
317 }
318 }
319
320 if (consumed) {
321 e.consume();
322 }
323 }
324
325 /**
326 * The action to execute when a key is pressed.
327 *
328 * @param e
329 * the key event
330 */
331 private void onKeyPressed(KeyEvent e) {
332 boolean consumed = false;
333 if (e.isActionKey()) {
334 int offset = 0;
335 switch (e.getKeyCode()) {
336 case KeyEvent.VK_LEFT:
337 offset = -1;
338 break;
339 case KeyEvent.VK_RIGHT:
340 offset = 1;
341 break;
342 case KeyEvent.VK_UP:
343 offset = -itemsPerLine;
344 break;
345 case KeyEvent.VK_DOWN:
346 offset = itemsPerLine;
347 break;
348 }
349
350 if (offset != 0) {
351 consumed = true;
352
353 int previousIndex = getSelectedBookIndex();
354 if (previousIndex >= 0) {
355 setSelectedBook(previousIndex + offset, true);
356 }
357 }
358 }
359
360 if (consumed) {
361 e.consume();
362 }
363 }
364 }