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