GUI search: waiting
[fanfix.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderGroup.java
CommitLineData
16a81ef7 1package be.nikiroo.fanfix.reader.ui;
4310bae9
NR
2
3import java.awt.BorderLayout;
4import java.awt.Color;
484a31aa 5import java.awt.Component;
4310bae9 6import java.awt.event.ActionListener;
07e0fc1e
NR
7import java.awt.event.ComponentAdapter;
8import java.awt.event.ComponentEvent;
17fafa56
NR
9import java.awt.event.FocusAdapter;
10import java.awt.event.FocusEvent;
07e0fc1e
NR
11import java.awt.event.KeyAdapter;
12import java.awt.event.KeyEvent;
4310bae9
NR
13import java.util.ArrayList;
14import java.util.List;
15
16import javax.swing.JLabel;
17import javax.swing.JPanel;
18
5bc9573b 19import be.nikiroo.fanfix.bundles.StringIdGui;
16a81ef7 20import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
4310bae9
NR
21import be.nikiroo.utils.ui.WrapLayout;
22
23/**
5dd985cf 24 * A group of {@link GuiReaderBook}s for display.
4310bae9
NR
25 *
26 * @author niki
27 */
5dd985cf 28public class GuiReaderGroup extends JPanel {
4310bae9
NR
29 private static final long serialVersionUID = 1L;
30 private BookActionListener action;
31 private Color backgroundColor;
c499d79f
NR
32 private Color backgroundColorDef;
33 private Color backgroundColorDefPane;
5dd985cf 34 private GuiReader reader;
79a99506 35 private List<GuiReaderBookInfo> infos;
5dd985cf 36 private List<GuiReaderBook> books;
4310bae9 37 private JPanel pane;
e92e4ae3 38 private JLabel titleLabel;
793f1071 39 private boolean words; // words or authors (secondary info on books)
07e0fc1e 40 private int itemsPerLine;
4310bae9
NR
41
42 /**
5dd985cf 43 * Create a new {@link GuiReaderGroup}.
4310bae9
NR
44 *
45 * @param reader
e42573a0
NR
46 * the {@link GuiReaderBook} used to probe some information about
47 * the stories
4310bae9 48 * @param title
e92e4ae3
NR
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)
4310bae9
NR
51 * @param backgroundColor
52 * the background colour to use (or NULL for default)
53 */
e42573a0 54 public GuiReaderGroup(GuiReader reader, String title, Color backgroundColor) {
4310bae9 55 this.reader = reader;
4310bae9
NR
56
57 this.pane = new JPanel();
4310bae9 58 pane.setLayout(new WrapLayout(WrapLayout.LEADING, 5, 5));
c499d79f
NR
59
60 this.backgroundColorDef = getBackground();
61 this.backgroundColorDefPane = pane.getBackground();
62 setBackground(backgroundColor);
4310bae9
NR
63
64 setLayout(new BorderLayout(0, 10));
17fafa56
NR
65
66 // Make it focusable:
67 setFocusable(true);
68 setEnabled(true);
69 setVisible(true);
70
4310bae9
NR
71 add(pane, BorderLayout.CENTER);
72
e92e4ae3
NR
73 titleLabel = new JLabel();
74 titleLabel.setHorizontalAlignment(JLabel.CENTER);
75 add(titleLabel, BorderLayout.NORTH);
76 setTitle(title);
07e0fc1e
NR
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() {
17fafa56
NR
89 @Override
90 public void keyPressed(KeyEvent e) {
91 onKeyPressed(e);
92 }
93
07e0fc1e
NR
94 @Override
95 public void keyTyped(KeyEvent e) {
96 onKeyTyped(e);
97 }
98 });
17fafa56
NR
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 });
07e0fc1e
NR
114 }
115
c499d79f
NR
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
e92e4ae3
NR
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
07e0fc1e
NR
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;
4310bae9
NR
182 }
183
184 /**
185 * Set the {@link ActionListener} that will be fired on each
5dd985cf 186 * {@link GuiReaderBook} action.
4310bae9
NR
187 *
188 * @param action
189 * the action
190 */
191 public void setActionListener(BookActionListener action) {
192 this.action = action;
79a99506 193 refreshBooks(infos, words);
4310bae9
NR
194 }
195
196 /**
5dd985cf 197 * Refresh the list of {@link GuiReaderBook}s displayed in the control.
4310bae9 198 *
c349fd48
NR
199 * @param infos
200 * the new list of infos
793f1071
NR
201 * @param seeWordcount
202 * TRUE to see word counts, FALSE to see authors
4310bae9 203 */
fb1ffdd0
NR
204 public void refreshBooks(List<GuiReaderBookInfo> infos, boolean seeWordcount) {
205 this.infos = infos;
8590da19
NR
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) {
793f1071 218 this.words = seeWordcount;
4310bae9 219
5dd985cf 220 books = new ArrayList<GuiReaderBook>();
4310bae9
NR
221 invalidate();
222 pane.invalidate();
223 pane.removeAll();
224
fb1ffdd0
NR
225 if (infos != null) {
226 for (GuiReaderBookInfo info : infos) {
79a99506 227 boolean isCached = false;
b31a0db0 228 if (info.getMeta() != null && info.getMeta().getLuid() != null) {
79a99506
NR
229 isCached = reader.isCached(info.getMeta().getLuid());
230 }
231
232 GuiReaderBook book = new GuiReaderBook(reader, info, isCached,
fb1ffdd0 233 words);
4310bae9
NR
234 if (backgroundColor != null) {
235 book.setBackground(backgroundColor);
236 }
237
238 books.add(book);
239
240 book.addActionListener(new BookActionListener() {
211f7ddb 241 @Override
5dd985cf 242 public void select(GuiReaderBook book) {
17fafa56 243 GuiReaderGroup.this.requestFocusInWindow();
5dd985cf 244 for (GuiReaderBook abook : books) {
4310bae9
NR
245 abook.setSelected(abook == book);
246 }
247 }
248
211f7ddb 249 @Override
484a31aa
NR
250 public void popupRequested(GuiReaderBook book,
251 Component target, int x, int y) {
4310bae9
NR
252 }
253
211f7ddb 254 @Override
5dd985cf 255 public void action(GuiReaderBook book) {
4310bae9
NR
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) {
5dd985cf 287 for (GuiReaderBook book : books) {
4310bae9
NR
288 book.setEnabled(b);
289 book.repaint();
290 }
291 }
292
293 pane.setEnabled(b);
294 super.setEnabled(b);
295 repaint();
296 }
07e0fc1e 297
e92e4ae3
NR
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
17fafa56
NR
307 /**
308 * Return the index of the currently selected book if any, -1 if none.
309 *
310 * @return the index or -1
311 */
e92e4ae3 312 public int getSelectedBookIndex() {
17fafa56
NR
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 */
e92e4ae3 334 public void setSelectedBook(int index, boolean forceRange) {
17fafa56
NR
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
e0fa20fe 353 if (index >= 0 && !books.isEmpty()) {
17fafa56
NR
354 books.get(index).setSelected(true);
355 }
356 }
357
07e0fc1e
NR
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;
484a31aa
NR
366 boolean action = e.getKeyChar() == '\n';
367 boolean popup = e.getKeyChar() == ' ';
368 if (action || popup) {
17fafa56
NR
369 consumed = true;
370
371 int index = getSelectedBookIndex();
372 if (index >= 0) {
484a31aa
NR
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 }
17fafa56
NR
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;
07e0fc1e
NR
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:
17fafa56 405 offset = -itemsPerLine;
07e0fc1e
NR
406 break;
407 case KeyEvent.VK_DOWN:
17fafa56 408 offset = itemsPerLine;
07e0fc1e
NR
409 break;
410 }
411
412 if (offset != 0) {
413 consumed = true;
414
17fafa56
NR
415 int previousIndex = getSelectedBookIndex();
416 if (previousIndex >= 0) {
417 setSelectedBook(previousIndex + offset, true);
07e0fc1e
NR
418 }
419 }
420 }
421
422 if (consumed) {
423 e.consume();
07e0fc1e
NR
424 }
425 }
4310bae9 426}