bae2742733b474c1f81e3c734a3cebb1e503d84d
[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 MetaData} parameter.
112 *
113 * @author niki
114 */
115 public interface MetaDataRunnable {
116 /**
117 * Run the action.
118 *
119 * @param meta
120 * the meta of the story
121 */
122 public void run(MetaData meta);
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.startsWith("https://"))) {
522 clipboard = "";
523 }
524
525 url = JOptionPane.showInputDialog(GuiReaderMainPanel.this,
526 GuiReader.trans(StringIdGui.SUBTITLE_IMPORT_URL),
527 GuiReader.trans(StringIdGui.TITLE_IMPORT_URL),
528 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
529 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
530 url = fc.getSelectedFile().getAbsolutePath();
531 } else {
532 url = null;
533 }
534
535 if (url != null && !url.toString().isEmpty()) {
536 imprt(url.toString(), null, null);
537 }
538 }
539
540 /**
541 * Actually import the {@link Story} into the main {@link LocalLibrary}.
542 * <p>
543 * Should be called inside the UI thread.
544 *
545 * @param url
546 * the {@link Story} to import by {@link URL}
547 * @param onSuccess
548 * Action to execute on success
549 * @param onSuccessPgName
550 * the name to use for the onSuccess progress bar
551 */
552 public void imprt(final String url, final MetaDataRunnable onSuccess,
553 String onSuccessPgName) {
554 final Progress pg = new Progress();
555 final Progress pgImprt = new Progress();
556 final Progress pgOnSuccess = new Progress(onSuccessPgName);
557 pg.addProgress(pgImprt, 95);
558 pg.addProgress(pgOnSuccess, 5);
559
560 outOfUi(pg, true, new Runnable() {
561 @Override
562 public void run() {
563 Exception ex = null;
564 MetaData meta = null;
565 try {
566 meta = helper.getReader().getLibrary()
567 .imprt(BasicReader.getUrl(url), pgImprt);
568 } catch (IOException e) {
569 ex = e;
570 }
571
572 final Exception e = ex;
573
574 final boolean ok = (e == null);
575
576 pgOnSuccess.setProgress(0);
577 if (!ok) {
578 if (e instanceof UnknownHostException) {
579 error(GuiReader.trans(
580 StringIdGui.ERROR_URL_NOT_SUPPORTED, url),
581 GuiReader.trans(StringIdGui.TITLE_ERROR), null);
582 } else {
583 error(GuiReader.trans(
584 StringIdGui.ERROR_URL_IMPORT_FAILED, url,
585 e.getMessage()), GuiReader
586 .trans(StringIdGui.TITLE_ERROR), e);
587 }
588 } else {
589 if (onSuccess != null) {
590 onSuccess.run(meta);
591 }
592 }
593 pgOnSuccess.done();
594 }
595 });
596 }
597
598 /**
599 * Enables or disables this component, depending on the value of the
600 * parameter <code>b</code>. An enabled component can respond to user input
601 * and generate events. Components are enabled initially by default.
602 * <p>
603 * Enabling or disabling <b>this</b> component will also affect its
604 * children.
605 *
606 * @param b
607 * If <code>true</code>, this component is enabled; otherwise
608 * this component is disabled
609 */
610 @Override
611 public void setEnabled(boolean b) {
612 if (bar != null) {
613 bar.setEnabled(b);
614 }
615
616 for (GuiReaderGroup group : books.values()) {
617 group.setEnabled(b);
618 }
619 super.setEnabled(b);
620 repaint();
621 }
622
623 public void setWords(boolean words) {
624 this.words = words;
625 }
626
627 public GuiReaderBook getSelectedBook() {
628 return selectedBook;
629 }
630
631 public void unsetSelectedBook() {
632 selectedBook = null;
633 }
634
635 private void addListPane(String name, List<String> values,
636 final boolean type) {
637 GuiReader reader = helper.getReader();
638 BasicLibrary lib = reader.getLibrary();
639
640 bookPane = new GuiReaderGroup(reader, name, color);
641
642 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
643 for (String value : values) {
644 if (type) {
645 infos.add(GuiReaderBookInfo.fromSource(lib, value));
646 } else {
647 infos.add(GuiReaderBookInfo.fromAuthor(lib, value));
648 }
649 }
650
651 bookPane.refreshBooks(infos, words);
652
653 this.invalidate();
654 pane.invalidate();
655 pane.add(bookPane);
656 pane.validate();
657 this.validate();
658
659 bookPane.setActionListener(new BookActionListener() {
660 @Override
661 public void select(GuiReaderBook book) {
662 selectedBook = book;
663 }
664
665 @Override
666 public void popupRequested(GuiReaderBook book, Component target,
667 int x, int y) {
668 JPopupMenu popup = helper.createSourceAuthorPopup();
669 popup.show(target, x, y);
670 }
671
672 @Override
673 public void action(final GuiReaderBook book) {
674 removeBookPanes();
675 addBookPane(book.getInfo().getMainInfo(), type);
676 refreshBooks();
677 }
678 });
679
680 focus();
681 }
682
683 /**
684 * Focus the first {@link GuiReaderGroup} we find.
685 */
686 private void focus() {
687 GuiReaderGroup group = null;
688 Map<String, GuiReaderGroup> books = this.books;
689 if (books.size() > 0) {
690 group = books.values().iterator().next();
691 }
692
693 if (group == null) {
694 group = bookPane;
695 }
696
697 if (group != null) {
698 group.requestFocusInWindow();
699 }
700 }
701
702 /**
703 * Display an error message and log the linked {@link Exception}.
704 *
705 * @param message
706 * the message
707 * @param title
708 * the title of the error message
709 * @param e
710 * the exception to log if any
711 */
712 private void error(final String message, final String title, Exception e) {
713 Instance.getTraceHandler().error(title + ": " + message);
714 if (e != null) {
715 Instance.getTraceHandler().error(e);
716 }
717
718 SwingUtilities.invokeLater(new Runnable() {
719 @Override
720 public void run() {
721 JOptionPane.showMessageDialog(GuiReaderMainPanel.this, message,
722 title, JOptionPane.ERROR_MESSAGE);
723 }
724 });
725 }
726 }