test: improve flag files
[nikiroo-utils.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderMainPanel.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.EventQueue;
7 import java.awt.Frame;
8 import java.awt.Toolkit;
9 import java.awt.datatransfer.DataFlavor;
10 import java.awt.event.ActionEvent;
11 import java.awt.event.ActionListener;
12 import java.awt.event.FocusAdapter;
13 import java.awt.event.FocusEvent;
14 import java.io.File;
15 import java.io.IOException;
16 import java.lang.reflect.InvocationTargetException;
17 import java.net.URL;
18 import java.net.UnknownHostException;
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.TreeMap;
23
24 import javax.swing.BoxLayout;
25 import javax.swing.JFileChooser;
26 import javax.swing.JLabel;
27 import javax.swing.JMenuBar;
28 import javax.swing.JOptionPane;
29 import javax.swing.JPanel;
30 import javax.swing.JPopupMenu;
31 import javax.swing.JScrollPane;
32 import javax.swing.SwingConstants;
33 import javax.swing.SwingUtilities;
34
35 import be.nikiroo.fanfix.Instance;
36 import be.nikiroo.fanfix.bundles.StringIdGui;
37 import be.nikiroo.fanfix.bundles.UiConfig;
38 import be.nikiroo.fanfix.data.MetaData;
39 import be.nikiroo.fanfix.data.Story;
40 import be.nikiroo.fanfix.library.BasicLibrary;
41 import be.nikiroo.fanfix.library.BasicLibrary.Status;
42 import be.nikiroo.fanfix.library.LocalLibrary;
43 import be.nikiroo.fanfix.reader.BasicReader;
44 import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
45 import be.nikiroo.utils.Progress;
46 import be.nikiroo.utils.ui.ProgressBar;
47
48 /**
49 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
50 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
51 * way to copy them to the {@link GuiReader} cache (
52 * {@link BasicReader#getLibrary()}), read them, delete them...
53 *
54 * @author niki
55 */
56 class GuiReaderMainPanel extends JPanel {
57 private static final long serialVersionUID = 1L;
58 private FrameHelper helper;
59 private Map<String, GuiReaderGroup> books;
60 private GuiReaderGroup bookPane; // for more "All"
61 private JPanel pane;
62 private Color color;
63 private ProgressBar pgBar;
64 private JMenuBar bar;
65 private GuiReaderBook selectedBook;
66 private boolean words; // words or authors (secondary info on books)
67 private boolean currentType; // type/source or author mode (All and Listing)
68
69 /**
70 * An object that offers some helper methods to access the frame that host
71 * it and the Fanfix-related functions.
72 *
73 * @author niki
74 */
75 public interface FrameHelper {
76 /**
77 * Return the reader associated to this {@link FrameHelper}.
78 *
79 * @return the reader
80 */
81 public GuiReader getReader();
82
83 /**
84 * Create the main menu bar.
85 * <p>
86 * Will invalidate the layout.
87 *
88 * @param status
89 * the library status, <b>must not</b> be NULL
90 */
91 public void createMenu(Status status);
92
93 /**
94 * Create a popup menu for a {@link GuiReaderBook} that represents a
95 * story.
96 *
97 * @return the popup menu to display
98 */
99 public JPopupMenu createBookPopup();
100
101 /**
102 * Create a popup menu for a {@link GuiReaderBook} that represents a
103 * source/type or an author.
104 *
105 * @return the popup menu to display
106 */
107 public JPopupMenu createSourceAuthorPopup();
108 }
109
110 /**
111 * A {@link Runnable} with a {@link Story} parameter.
112 *
113 * @author niki
114 */
115 public interface StoryRunnable {
116 /**
117 * Run the action.
118 *
119 * @param story
120 * the story
121 */
122 public void run(Story story);
123 }
124
125 /**
126 * Create a new {@link GuiReaderMainPanel}.
127 *
128 * @param parent
129 * the associated {@link FrameHelper} to forward some commands
130 * and access its {@link LocalLibrary}
131 * @param type
132 * the type of {@link Story} to load, or NULL for all types
133 */
134 public GuiReaderMainPanel(FrameHelper parent, String type) {
135 super(new BorderLayout(), true);
136
137 this.helper = parent;
138
139 pane = new JPanel();
140 pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
141 JScrollPane scroll = new JScrollPane(pane);
142
143 Integer icolor = Instance.getUiConfig().getColor(
144 UiConfig.BACKGROUND_COLOR);
145 if (icolor != null) {
146 color = new Color(icolor);
147 setBackground(color);
148 pane.setBackground(color);
149 scroll.setBackground(color);
150 }
151
152 scroll.getVerticalScrollBar().setUnitIncrement(16);
153 add(scroll, BorderLayout.CENTER);
154
155 String message = parent.getReader().getLibrary().getLibraryName();
156 if (!message.isEmpty()) {
157 JLabel name = new JLabel(message, SwingConstants.CENTER);
158 add(name, BorderLayout.NORTH);
159 }
160
161 pgBar = new ProgressBar();
162 add(pgBar, BorderLayout.SOUTH);
163
164 pgBar.addActionListener(new ActionListener() {
165 @Override
166 public void actionPerformed(ActionEvent e) {
167 pgBar.invalidate();
168 pgBar.setProgress(null);
169 setEnabled(true);
170 validate();
171 }
172 });
173
174 pgBar.addUpdateListener(new ActionListener() {
175 @Override
176 public void actionPerformed(ActionEvent e) {
177 pgBar.invalidate();
178 validate();
179 repaint();
180 }
181 });
182
183 books = new TreeMap<String, GuiReaderGroup>();
184
185 addFocusListener(new FocusAdapter() {
186 @Override
187 public void focusGained(FocusEvent e) {
188 focus();
189 }
190 });
191
192 pane.setVisible(false);
193 final Progress pg = new Progress();
194 final String typeF = type;
195 outOfUi(pg, true, new Runnable() {
196 @Override
197 public void run() {
198 final BasicLibrary lib = helper.getReader().getLibrary();
199 final Status status = lib.getStatus();
200
201 if (status == Status.READ_WRITE) {
202 lib.refresh(pg);
203 }
204
205 inUi(new Runnable() {
206 @Override
207 public void run() {
208 if (status.isReady()) {
209 helper.createMenu(status);
210 pane.setVisible(true);
211 if (typeF == null) {
212 try {
213 addBookPane(true, false);
214 } catch (IOException e) {
215 error(e.getLocalizedMessage(),
216 "IOException", e);
217 }
218 } else {
219 addBookPane(typeF, true);
220 }
221 } else {
222 helper.createMenu(status);
223 validate();
224
225 String desc = Instance.getTransGui().getStringX(
226 StringIdGui.ERROR_LIB_STATUS,
227 status.toString());
228 if (desc == null) {
229 desc = GuiReader
230 .trans(StringIdGui.ERROR_LIB_STATUS);
231 }
232
233 String err = lib.getLibraryName() + "\n" + desc;
234 error(err, GuiReader
235 .trans(StringIdGui.TITLE_ERROR_LIBRARY),
236 null);
237 }
238 }
239 });
240 }
241 });
242 }
243
244 public boolean getCurrentType() {
245 return currentType;
246 }
247
248 /**
249 * Add a new {@link GuiReaderGroup} on the frame to display all the
250 * sources/types or all the authors, or a listing of all the books sorted
251 * either by source or author.
252 * <p>
253 * A display of all the sources/types or all the authors will show one icon
254 * per source/type or author.
255 * <p>
256 * A listing of all the books sorted by source/type or author will display
257 * all the books.
258 *
259 * @param type
260 * TRUE for type/source, FALSE for author
261 * @param listMode
262 * TRUE to get a listing of all the sources or authors, FALSE to
263 * get one icon per source or author
264 *
265 * @throws IOException
266 * in case of I/O error
267 */
268 public void addBookPane(boolean type, boolean listMode) throws IOException {
269 this.currentType = type;
270 BasicLibrary lib = helper.getReader().getLibrary();
271 if (type) {
272 if (!listMode) {
273 addListPane(GuiReader.trans(StringIdGui.MENU_SOURCES),
274 lib.getSources(), type);
275 } else {
276 for (String tt : lib.getSources()) {
277 if (tt != null) {
278 addBookPane(tt, type);
279 }
280 }
281 }
282 } else {
283 if (!listMode) {
284 addListPane(GuiReader.trans(StringIdGui.MENU_AUTHORS),
285 lib.getAuthors(), type);
286 } else {
287 for (String tt : lib.getAuthors()) {
288 if (tt != null) {
289 addBookPane(tt, type);
290 }
291 }
292 }
293 }
294 }
295
296 /**
297 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
298 * selected type or author.
299 * <p>
300 * Will invalidate the layout.
301 *
302 * @param value
303 * the author or the type, or NULL to get all the
304 * authors-or-types
305 * @param type
306 * TRUE for type/source, FALSE for author
307 */
308 public void addBookPane(String value, boolean type) {
309 this.currentType = type;
310
311 GuiReaderGroup bookPane = new GuiReaderGroup(helper.getReader(), value,
312 color);
313
314 books.put(value, bookPane);
315
316 pane.invalidate();
317 pane.add(bookPane);
318
319 bookPane.setActionListener(new BookActionListener() {
320 @Override
321 public void select(GuiReaderBook book) {
322 selectedBook = book;
323 }
324
325 @Override
326 public void popupRequested(GuiReaderBook book, Component target,
327 int x, int y) {
328 JPopupMenu popup = helper.createBookPopup();
329 popup.show(target, x, y);
330 }
331
332 @Override
333 public void action(final GuiReaderBook book) {
334 openBook(book);
335 }
336 });
337
338 focus();
339 }
340
341 /**
342 * Clear the pane from any book that may be present, usually prior to adding
343 * new ones.
344 * <p>
345 * Will invalidate the layout.
346 */
347 public void removeBookPanes() {
348 books.clear();
349 pane.invalidate();
350 pane.removeAll();
351 }
352
353 /**
354 * Refresh the list of {@link GuiReaderBook}s from disk.
355 * <p>
356 * Will validate the layout, as it is a "refresh" operation.
357 */
358 public void refreshBooks() {
359 BasicLibrary lib = helper.getReader().getLibrary();
360 for (String value : books.keySet()) {
361 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
362
363 List<MetaData> metas;
364 try {
365 if (currentType) {
366 metas = lib.getListBySource(value);
367 } else {
368 metas = lib.getListByAuthor(value);
369 }
370 } catch (IOException e) {
371 error(e.getLocalizedMessage(), "IOException", e);
372 metas = new ArrayList<MetaData>();
373 }
374
375 for (MetaData meta : metas) {
376 infos.add(GuiReaderBookInfo.fromMeta(meta));
377 }
378
379 books.get(value).refreshBooks(infos, words);
380 }
381
382 if (bookPane != null) {
383 bookPane.refreshBooks(words);
384 }
385
386 this.validate();
387 }
388
389 /**
390 * Open a {@link GuiReaderBook} item.
391 *
392 * @param book
393 * the {@link GuiReaderBook} to open
394 */
395 public void openBook(final GuiReaderBook book) {
396 final Progress pg = new Progress();
397 outOfUi(pg, false, new Runnable() {
398 @Override
399 public void run() {
400 try {
401 helper.getReader().read(book.getInfo().getMeta().getLuid(),
402 false, pg);
403 SwingUtilities.invokeLater(new Runnable() {
404 @Override
405 public void run() {
406 book.setCached(true);
407 }
408 });
409 } catch (IOException e) {
410 Instance.getTraceHandler().error(e);
411 error(GuiReader.trans(StringIdGui.ERROR_CANNOT_OPEN),
412 GuiReader.trans(StringIdGui.TITLE_ERROR), e);
413 }
414 }
415 });
416 }
417
418 /**
419 * Process the given action out of the Swing UI thread and link the given
420 * {@link ProgressBar} to the action.
421 * <p>
422 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
423 * to done when the action is done.
424 *
425 * @param progress
426 * the {@link ProgressBar} or NULL
427 * @param refreshBooks
428 * TRUE to refresh the books after
429 * @param run
430 * the action to run
431 */
432 public void outOfUi(Progress progress, final boolean refreshBooks,
433 final Runnable run) {
434 final Progress pg = new Progress();
435 final Progress reload = new Progress(
436 GuiReader.trans(StringIdGui.PROGRESS_OUT_OF_UI_RELOAD_BOOKS));
437
438 if (progress == null) {
439 progress = new Progress();
440 }
441
442 if (refreshBooks) {
443 pg.addProgress(progress, 100);
444 } else {
445 pg.addProgress(progress, 90);
446 pg.addProgress(reload, 10);
447 }
448
449 invalidate();
450 pgBar.setProgress(pg);
451 validate();
452 setEnabled(false);
453
454 new Thread(new Runnable() {
455 @Override
456 public void run() {
457 try {
458 run.run();
459 if (refreshBooks) {
460 refreshBooks();
461 }
462 } finally {
463 reload.done();
464 if (!pg.isDone()) {
465 // will trigger pgBar ActionListener:
466 pg.done();
467 }
468 }
469 }
470 }, "outOfUi thread").start();
471 }
472
473 /**
474 * Process the given action in the main Swing UI thread.
475 * <p>
476 * The code will make sure the current thread is the main UI thread and, if
477 * not, will switch to it before executing the runnable.
478 * <p>
479 * Synchronous operation.
480 *
481 * @param run
482 * the action to run
483 */
484 public void inUi(final Runnable run) {
485 if (EventQueue.isDispatchThread()) {
486 run.run();
487 } else {
488 try {
489 EventQueue.invokeAndWait(run);
490 } catch (InterruptedException e) {
491 Instance.getTraceHandler().error(e);
492 } catch (InvocationTargetException e) {
493 Instance.getTraceHandler().error(e);
494 }
495 }
496 }
497
498 /**
499 * Import a {@link Story} into the main {@link LocalLibrary}.
500 * <p>
501 * Should be called inside the UI thread.
502 *
503 * @param askUrl
504 * TRUE for an {@link URL}, false for a {@link File}
505 */
506 public void imprt(boolean askUrl) {
507 JFileChooser fc = new JFileChooser();
508
509 Object url;
510 if (askUrl) {
511 String clipboard = "";
512 try {
513 clipboard = ("" + Toolkit.getDefaultToolkit()
514 .getSystemClipboard().getData(DataFlavor.stringFlavor))
515 .trim();
516 } catch (Exception e) {
517 // No data will be handled
518 }
519
520 if (clipboard == null || !clipboard.startsWith("http")) {
521 clipboard = "";
522 }
523
524 url = JOptionPane.showInputDialog(GuiReaderMainPanel.this,
525 GuiReader.trans(StringIdGui.SUBTITLE_IMPORT_URL),
526 GuiReader.trans(StringIdGui.TITLE_IMPORT_URL),
527 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
528 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
529 url = fc.getSelectedFile().getAbsolutePath();
530 } else {
531 url = null;
532 }
533
534 if (url != null && !url.toString().isEmpty()) {
535 imprt(url.toString(), null, null);
536 }
537 }
538
539 /**
540 * Actually import the {@link Story} into the main {@link LocalLibrary}.
541 * <p>
542 * Should be called inside the UI thread.
543 *
544 * @param url
545 * the {@link Story} to import by {@link URL}
546 * @param onSuccess
547 * Action to execute on success
548 * @param onSuccessPgName
549 * the name to use for the onSuccess progress bar
550 */
551 public void imprt(final String url, final StoryRunnable onSuccess,
552 String onSuccessPgName) {
553 final Progress pg = new Progress();
554 final Progress pgImprt = new Progress();
555 final Progress pgOnSuccess = new Progress(onSuccessPgName);
556 pg.addProgress(pgImprt, 95);
557 pg.addProgress(pgOnSuccess, 5);
558
559 outOfUi(pg, true, new Runnable() {
560 @Override
561 public void run() {
562 Exception ex = null;
563 Story story = null;
564 try {
565 story = helper.getReader().getLibrary()
566 .imprt(BasicReader.getUrl(url), pgImprt);
567 } catch (IOException e) {
568 ex = e;
569 }
570
571 final Exception e = ex;
572
573 final boolean ok = (e == null);
574
575 pgOnSuccess.setProgress(0);
576 if (!ok) {
577 if (e instanceof UnknownHostException) {
578 error(GuiReader.trans(
579 StringIdGui.ERROR_URL_NOT_SUPPORTED, url),
580 GuiReader.trans(StringIdGui.TITLE_ERROR), null);
581 } else {
582 error(GuiReader.trans(
583 StringIdGui.ERROR_URL_IMPORT_FAILED, url,
584 e.getMessage()), GuiReader
585 .trans(StringIdGui.TITLE_ERROR), e);
586 }
587 } else {
588 if (onSuccess != null) {
589 onSuccess.run(story);
590 }
591 }
592 pgOnSuccess.done();
593 }
594 });
595 }
596
597 /**
598 * Enables or disables this component, depending on the value of the
599 * parameter <code>b</code>. An enabled component can respond to user input
600 * and generate events. Components are enabled initially by default.
601 * <p>
602 * Enabling or disabling <b>this</b> component will also affect its
603 * children.
604 *
605 * @param b
606 * If <code>true</code>, this component is enabled; otherwise
607 * this component is disabled
608 */
609 @Override
610 public void setEnabled(boolean b) {
611 if (bar != null) {
612 bar.setEnabled(b);
613 }
614
615 for (GuiReaderGroup group : books.values()) {
616 group.setEnabled(b);
617 }
618 super.setEnabled(b);
619 repaint();
620 }
621
622 public void setWords(boolean words) {
623 this.words = words;
624 }
625
626 public GuiReaderBook getSelectedBook() {
627 return selectedBook;
628 }
629
630 public void unsetSelectedBook() {
631 selectedBook = null;
632 }
633
634 private void addListPane(String name, List<String> values,
635 final boolean type) {
636 GuiReader reader = helper.getReader();
637 BasicLibrary lib = reader.getLibrary();
638
639 bookPane = new GuiReaderGroup(reader, name, color);
640
641 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
642 for (String value : values) {
643 if (type) {
644 infos.add(GuiReaderBookInfo.fromSource(lib, value));
645 } else {
646 infos.add(GuiReaderBookInfo.fromAuthor(lib, value));
647 }
648 }
649
650 bookPane.refreshBooks(infos, words);
651
652 this.invalidate();
653 pane.invalidate();
654 pane.add(bookPane);
655 pane.validate();
656 this.validate();
657
658 bookPane.setActionListener(new BookActionListener() {
659 @Override
660 public void select(GuiReaderBook book) {
661 selectedBook = book;
662 }
663
664 @Override
665 public void popupRequested(GuiReaderBook book, Component target,
666 int x, int y) {
667 JPopupMenu popup = helper.createSourceAuthorPopup();
668 popup.show(target, x, y);
669 }
670
671 @Override
672 public void action(final GuiReaderBook book) {
673 removeBookPanes();
674 addBookPane(book.getInfo().getMainInfo(), type);
675 refreshBooks();
676 }
677 });
678
679 focus();
680 }
681
682 /**
683 * Focus the first {@link GuiReaderGroup} we find.
684 */
685 private void focus() {
686 GuiReaderGroup group = null;
687 Map<String, GuiReaderGroup> books = this.books;
688 if (books.size() > 0) {
689 group = books.values().iterator().next();
690 }
691
692 if (group == null) {
693 group = bookPane;
694 }
695
696 if (group != null) {
697 group.requestFocusInWindow();
698 }
699 }
700
701 /**
702 * Display an error message and log the linked {@link Exception}.
703 *
704 * @param message
705 * the message
706 * @param title
707 * the title of the error message
708 * @param e
709 * the exception to log if any
710 */
711 private void error(final String message, final String title, Exception e) {
712 Instance.getTraceHandler().error(title + ": " + message);
713 if (e != null) {
714 Instance.getTraceHandler().error(e);
715 }
716
717 SwingUtilities.invokeLater(new Runnable() {
718 @Override
719 public void run() {
720 JOptionPane.showMessageDialog(GuiReaderMainPanel.this, message,
721 title, JOptionPane.ERROR_MESSAGE);
722 }
723 });
724 }
725 }