Remove or move java.awt dependencies
[nikiroo-utils.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderFrame.java
1 package be.nikiroo.fanfix.reader.ui;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Frame;
6 import java.awt.Toolkit;
7 import java.awt.datatransfer.DataFlavor;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.awt.event.KeyEvent;
11 import java.awt.event.MouseEvent;
12 import java.awt.event.WindowEvent;
13 import java.io.File;
14 import java.io.IOException;
15 import java.net.URL;
16 import java.net.UnknownHostException;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Map.Entry;
22
23 import javax.swing.BoxLayout;
24 import javax.swing.JFileChooser;
25 import javax.swing.JFrame;
26 import javax.swing.JLabel;
27 import javax.swing.JMenu;
28 import javax.swing.JMenuBar;
29 import javax.swing.JMenuItem;
30 import javax.swing.JOptionPane;
31 import javax.swing.JPanel;
32 import javax.swing.JPopupMenu;
33 import javax.swing.JScrollPane;
34 import javax.swing.SwingConstants;
35 import javax.swing.SwingUtilities;
36 import javax.swing.filechooser.FileFilter;
37 import javax.swing.filechooser.FileNameExtensionFilter;
38
39 import be.nikiroo.fanfix.Instance;
40 import be.nikiroo.fanfix.bundles.Config;
41 import be.nikiroo.fanfix.bundles.UiConfig;
42 import be.nikiroo.fanfix.data.MetaData;
43 import be.nikiroo.fanfix.data.Story;
44 import be.nikiroo.fanfix.library.BasicLibrary;
45 import be.nikiroo.fanfix.library.BasicLibrary.Status;
46 import be.nikiroo.fanfix.library.LocalLibrary;
47 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
48 import be.nikiroo.fanfix.reader.BasicReader;
49 import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
50 import be.nikiroo.utils.Progress;
51 import be.nikiroo.utils.Version;
52 import be.nikiroo.utils.ui.ConfigEditor;
53 import be.nikiroo.utils.ui.ProgressBar;
54
55 /**
56 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
57 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
58 * way to copy them to the {@link GuiReader} cache (
59 * {@link BasicReader#getLibrary()}), read them, delete them...
60 *
61 * @author niki
62 */
63 class GuiReaderFrame extends JFrame {
64 private static final long serialVersionUID = 1L;
65 private GuiReader reader;
66 private Map<GuiReaderGroup, String> booksByType;
67 private Map<GuiReaderGroup, String> booksByAuthor;
68 private JPanel pane;
69 private Color color;
70 private ProgressBar pgBar;
71 private JMenuBar bar;
72 private GuiReaderBook selectedBook;
73 private boolean words; // words or authors (secondary info on books)
74
75 /**
76 * A {@link Runnable} with a {@link Story} parameter.
77 *
78 * @author niki
79 */
80 private interface StoryRunnable {
81 /**
82 * Run the action.
83 *
84 * @param story
85 * the story
86 */
87 public void run(Story story);
88 }
89
90 /**
91 * Create a new {@link GuiReaderFrame}.
92 *
93 * @param reader
94 * the associated {@link GuiReader} to forward some commands and
95 * access its {@link LocalLibrary}
96 * @param type
97 * the type of {@link Story} to load, or NULL for all types
98 */
99 public GuiReaderFrame(GuiReader reader, String type) {
100 super(String.format("Fanfix %s Library", Version.getCurrentVersion()));
101
102 this.reader = reader;
103
104 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
105 setSize(800, 600);
106 setLayout(new BorderLayout());
107
108 pane = new JPanel();
109 pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
110
111 Integer icolor = Instance.getUiConfig().getColor(
112 UiConfig.BACKGROUND_COLOR);
113 if (icolor != null) {
114 color = new Color(icolor);
115 setBackground(color);
116 pane.setBackground(color);
117 }
118
119 JScrollPane scroll = new JScrollPane(pane);
120 scroll.getVerticalScrollBar().setUnitIncrement(16);
121 add(scroll, BorderLayout.CENTER);
122
123 String message = reader.getLibrary().getLibraryName();
124 if (!message.isEmpty()) {
125 JLabel name = new JLabel(message, SwingConstants.CENTER);
126 add(name, BorderLayout.NORTH);
127 }
128
129 pgBar = new ProgressBar();
130 add(pgBar, BorderLayout.SOUTH);
131
132 pgBar.addActionListener(new ActionListener() {
133 @Override
134 public void actionPerformed(ActionEvent e) {
135 invalidate();
136 pgBar.setProgress(null);
137 validate();
138 setEnabled(true);
139 }
140 });
141
142 pgBar.addUpdateListener(new ActionListener() {
143 @Override
144 public void actionPerformed(ActionEvent e) {
145 invalidate();
146 validate();
147 repaint();
148 }
149 });
150
151 booksByType = new HashMap<GuiReaderGroup, String>();
152 booksByAuthor = new HashMap<GuiReaderGroup, String>();
153
154 pane.setVisible(false);
155 final Progress pg = new Progress();
156 final String typeF = type;
157 outOfUi(pg, new Runnable() {
158 @Override
159 public void run() {
160 BasicLibrary lib = GuiReaderFrame.this.reader.getLibrary();
161 Status status = lib.getStatus();
162
163 if (status == Status.READY) {
164 lib.refresh(pg);
165 invalidate();
166 setJMenuBar(createMenu(true));
167 addBookPane(typeF, true);
168 refreshBooks();
169 validate();
170 pane.setVisible(true);
171 } else {
172 invalidate();
173 setJMenuBar(createMenu(false));
174 validate();
175
176 String err = lib.getLibraryName() + "\n";
177 switch (status) {
178 case INVALID:
179 err += "Library not valid";
180 break;
181
182 case UNAUTORIZED:
183 err += "You are not allowed to access this library";
184 break;
185
186 case UNAVAILABLE:
187 err += "Library currently unavilable";
188 break;
189
190 default:
191 err += "An error occured when contacting the library";
192 break;
193 }
194
195 error(err, "Library error", null);
196 }
197 }
198 });
199
200 setVisible(true);
201 }
202
203 private void addSourcePanes() {
204 // Sources -> i18n
205 GuiReaderGroup bookPane = new GuiReaderGroup(reader, "Sources", color);
206
207 List<MetaData> sources = new ArrayList<MetaData>();
208 for (String source : reader.getLibrary().getSources()) {
209 MetaData mSource = new MetaData();
210 mSource.setLuid(null);
211 mSource.setTitle(source);
212 mSource.setSource(source);
213 sources.add(mSource);
214 }
215
216 bookPane.refreshBooks(sources, false);
217
218 this.invalidate();
219 pane.invalidate();
220 pane.add(bookPane);
221 pane.validate();
222 this.validate();
223
224 bookPane.setActionListener(new BookActionListener() {
225 @Override
226 public void select(GuiReaderBook book) {
227 selectedBook = book;
228 }
229
230 @Override
231 public void popupRequested(GuiReaderBook book, MouseEvent e) {
232 JPopupMenu popup = new JPopupMenu();
233 popup.add(createMenuItemOpenBook());
234 popup.show(e.getComponent(), e.getX(), e.getY());
235 }
236
237 @Override
238 public void action(final GuiReaderBook book) {
239 removeBookPanes();
240 addBookPane(book.getMeta().getSource(), true);
241 refreshBooks();
242 }
243 });
244 }
245
246 /**
247 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
248 * selected type or author.
249 *
250 * @param value
251 * the author or the type, or NULL to get all the
252 * authors-or-types
253 * @param type
254 * TRUE for type, FALSE for author
255 */
256 private void addBookPane(String value, boolean type) {
257 if (value == null) {
258 if (type) {
259 if (Instance.getUiConfig().getBoolean(UiConfig.SOURCE_PAGE,
260 false)) {
261 addSourcePanes();
262 } else {
263 for (String tt : reader.getLibrary().getSources()) {
264 if (tt != null) {
265 addBookPane(tt, type);
266 }
267 }
268 }
269 } else {
270 for (String tt : reader.getLibrary().getAuthors()) {
271 if (tt != null) {
272 addBookPane(tt, type);
273 }
274 }
275 }
276
277 return;
278 }
279
280 GuiReaderGroup bookPane = new GuiReaderGroup(reader, value, color);
281 if (type) {
282 booksByType.put(bookPane, value);
283 } else {
284 booksByAuthor.put(bookPane, value);
285 }
286
287 this.invalidate();
288 pane.invalidate();
289 pane.add(bookPane);
290 pane.validate();
291 this.validate();
292
293 bookPane.setActionListener(new BookActionListener() {
294 @Override
295 public void select(GuiReaderBook book) {
296 selectedBook = book;
297 }
298
299 @Override
300 public void popupRequested(GuiReaderBook book, MouseEvent e) {
301 JPopupMenu popup = new JPopupMenu();
302 popup.add(createMenuItemOpenBook());
303 popup.addSeparator();
304 popup.add(createMenuItemExport());
305 popup.add(createMenuItemMove(true));
306 popup.add(createMenuItemSetCover());
307 popup.add(createMenuItemClearCache());
308 popup.add(createMenuItemRedownload());
309 popup.addSeparator();
310 popup.add(createMenuItemDelete());
311 popup.show(e.getComponent(), e.getX(), e.getY());
312 }
313
314 @Override
315 public void action(final GuiReaderBook book) {
316 openBook(book);
317 }
318 });
319 }
320
321 private void removeBookPanes() {
322 booksByType.clear();
323 booksByAuthor.clear();
324 pane.invalidate();
325 this.invalidate();
326 pane.removeAll();
327 pane.validate();
328 this.validate();
329 }
330
331 /**
332 * Refresh the list of {@link GuiReaderBook}s from disk.
333 *
334 */
335 private void refreshBooks() {
336 for (GuiReaderGroup group : booksByType.keySet()) {
337 List<MetaData> stories = reader.getLibrary().getListBySource(
338 booksByType.get(group));
339 group.refreshBooks(stories, words);
340 }
341
342 for (GuiReaderGroup group : booksByAuthor.keySet()) {
343 List<MetaData> stories = reader.getLibrary().getListByAuthor(
344 booksByAuthor.get(group));
345 group.refreshBooks(stories, words);
346 }
347
348 pane.repaint();
349 this.repaint();
350 }
351
352 /**
353 * Create the main menu bar.
354 *
355 * @param libOk
356 * the library can be queried
357 *
358 * @return the bar
359 */
360 private JMenuBar createMenu(boolean libOk) {
361 bar = new JMenuBar();
362
363 JMenu file = new JMenu("File");
364 file.setMnemonic(KeyEvent.VK_F);
365
366 JMenuItem imprt = new JMenuItem("Import URL...", KeyEvent.VK_U);
367 imprt.addActionListener(new ActionListener() {
368 @Override
369 public void actionPerformed(ActionEvent e) {
370 imprt(true);
371 }
372 });
373 JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
374 imprtF.addActionListener(new ActionListener() {
375 @Override
376 public void actionPerformed(ActionEvent e) {
377 imprt(false);
378 }
379 });
380 JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X);
381 exit.addActionListener(new ActionListener() {
382 @Override
383 public void actionPerformed(ActionEvent e) {
384 GuiReaderFrame.this.dispatchEvent(new WindowEvent(
385 GuiReaderFrame.this, WindowEvent.WINDOW_CLOSING));
386 }
387 });
388
389 file.add(createMenuItemOpenBook());
390 file.add(createMenuItemExport());
391 file.add(createMenuItemMove(libOk));
392 file.addSeparator();
393 file.add(imprt);
394 file.add(imprtF);
395 file.addSeparator();
396 file.add(exit);
397
398 bar.add(file);
399
400 JMenu edit = new JMenu("Edit");
401 edit.setMnemonic(KeyEvent.VK_E);
402
403 edit.add(createMenuItemClearCache());
404 edit.add(createMenuItemRedownload());
405 edit.addSeparator();
406 edit.add(createMenuItemDelete());
407
408 bar.add(edit);
409
410 JMenu view = new JMenu("View");
411 view.setMnemonic(KeyEvent.VK_V);
412 JMenuItem vauthors = new JMenuItem("Author");
413 vauthors.setMnemonic(KeyEvent.VK_A);
414 vauthors.addActionListener(new ActionListener() {
415 @Override
416 public void actionPerformed(ActionEvent e) {
417 words = false;
418 refreshBooks();
419 }
420 });
421 view.add(vauthors);
422 JMenuItem vwords = new JMenuItem("Word count");
423 vwords.setMnemonic(KeyEvent.VK_W);
424 vwords.addActionListener(new ActionListener() {
425 @Override
426 public void actionPerformed(ActionEvent e) {
427 words = true;
428 refreshBooks();
429 }
430 });
431 view.add(vwords);
432 bar.add(view);
433
434 JMenu sources = new JMenu("Sources");
435 sources.setMnemonic(KeyEvent.VK_S);
436
437 List<String> tt = new ArrayList<String>();
438 if (libOk) {
439 tt.addAll(reader.getLibrary().getSources());
440 }
441 tt.add(0, null);
442
443 for (final String type : tt) {
444 JMenuItem item = new JMenuItem(type == null ? "All" : type);
445 item.addActionListener(new ActionListener() {
446 @Override
447 public void actionPerformed(ActionEvent e) {
448 removeBookPanes();
449 addBookPane(type, true);
450 refreshBooks();
451 }
452 });
453 sources.add(item);
454
455 if (type == null) {
456 sources.addSeparator();
457 }
458 }
459
460 bar.add(sources);
461
462 JMenu authors = new JMenu("Authors");
463 authors.setMnemonic(KeyEvent.VK_A);
464
465 List<String> aa = new ArrayList<String>();
466 if (libOk) {
467 aa.addAll(reader.getLibrary().getAuthors());
468 }
469 aa.add(0, null);
470 for (final String author : aa) {
471 JMenuItem item = new JMenuItem(author == null ? "All"
472 : author.isEmpty() ? "[unknown]" : author);
473 item.addActionListener(new ActionListener() {
474 @Override
475 public void actionPerformed(ActionEvent e) {
476 removeBookPanes();
477 addBookPane(author, false);
478 refreshBooks();
479 }
480 });
481 authors.add(item);
482
483 if (author == null || author.isEmpty()) {
484 authors.addSeparator();
485 }
486 }
487
488 bar.add(authors);
489
490 JMenu options = new JMenu("Options");
491 options.setMnemonic(KeyEvent.VK_O);
492 options.add(createMenuItemConfig());
493 options.add(createMenuItemUiConfig());
494 bar.add(options);
495
496 return bar;
497 }
498
499 /**
500 * Create the Fanfix Configuration menu item.
501 *
502 * @return the item
503 */
504 private JMenuItem createMenuItemConfig() {
505 final String title = "Fanfix Configuration";
506 JMenuItem item = new JMenuItem(title);
507 item.setMnemonic(KeyEvent.VK_F);
508
509 item.addActionListener(new ActionListener() {
510 @Override
511 public void actionPerformed(ActionEvent e) {
512 ConfigEditor<Config> ed = new ConfigEditor<Config>(
513 Config.class, Instance.getConfig(),
514 "This is where you configure the options of the program.");
515 JFrame frame = new JFrame(title);
516 frame.add(ed);
517 frame.setSize(800, 600);
518 frame.setVisible(true);
519 }
520 });
521
522 return item;
523 }
524
525 /**
526 * Create the UI Configuration menu item.
527 *
528 * @return the item
529 */
530 private JMenuItem createMenuItemUiConfig() {
531 final String title = "UI Configuration";
532 JMenuItem item = new JMenuItem(title);
533 item.setMnemonic(KeyEvent.VK_U);
534
535 item.addActionListener(new ActionListener() {
536 @Override
537 public void actionPerformed(ActionEvent e) {
538 ConfigEditor<UiConfig> ed = new ConfigEditor<UiConfig>(
539 UiConfig.class, Instance.getUiConfig(),
540 "This is where you configure the graphical appearence of the program.");
541 JFrame frame = new JFrame(title);
542 frame.add(ed);
543 frame.setSize(800, 600);
544 frame.setVisible(true);
545 }
546 });
547
548 return item;
549 }
550
551 /**
552 * Create the export menu item.
553 *
554 * @return the item
555 */
556 private JMenuItem createMenuItemExport() {
557 final JFileChooser fc = new JFileChooser();
558 fc.setAcceptAllFileFilterUsed(false);
559
560 final Map<FileFilter, OutputType> filters = new HashMap<FileFilter, OutputType>();
561 for (OutputType type : OutputType.values()) {
562 String ext = type.getDefaultExtension(false);
563 String desc = type.getDesc(false);
564
565 if (ext == null || ext.isEmpty()) {
566 filters.put(createAllFilter(desc), type);
567 } else {
568 filters.put(new FileNameExtensionFilter(desc, ext), type);
569 }
570 }
571
572 // First the "ALL" filters, then, the extension filters
573 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
574 if (!(entry.getKey() instanceof FileNameExtensionFilter)) {
575 fc.addChoosableFileFilter(entry.getKey());
576 }
577 }
578 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
579 if (entry.getKey() instanceof FileNameExtensionFilter) {
580 fc.addChoosableFileFilter(entry.getKey());
581 }
582 }
583 //
584
585 JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S);
586 export.addActionListener(new ActionListener() {
587 @Override
588 public void actionPerformed(ActionEvent e) {
589 if (selectedBook != null) {
590 fc.showDialog(GuiReaderFrame.this, "Save");
591 if (fc.getSelectedFile() != null) {
592 final OutputType type = filters.get(fc.getFileFilter());
593 final String path = fc.getSelectedFile()
594 .getAbsolutePath()
595 + type.getDefaultExtension(false);
596 final Progress pg = new Progress();
597 outOfUi(pg, new Runnable() {
598 @Override
599 public void run() {
600 try {
601 reader.getLibrary().export(
602 selectedBook.getMeta().getLuid(),
603 type, path, pg);
604 } catch (IOException e) {
605 Instance.getTraceHandler().error(e);
606 }
607 }
608 });
609 }
610 }
611 }
612 });
613
614 return export;
615 }
616
617 /**
618 * Create a {@link FileFilter} that accepts all files and return the given
619 * description.
620 *
621 * @param desc
622 * the description
623 *
624 * @return the filter
625 */
626 private FileFilter createAllFilter(final String desc) {
627 return new FileFilter() {
628 @Override
629 public String getDescription() {
630 return desc;
631 }
632
633 @Override
634 public boolean accept(File f) {
635 return true;
636 }
637 };
638 }
639
640 /**
641 * Create the refresh (delete cache) menu item.
642 *
643 * @return the item
644 */
645 private JMenuItem createMenuItemClearCache() {
646 JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
647 refresh.addActionListener(new ActionListener() {
648 @Override
649 public void actionPerformed(ActionEvent e) {
650 if (selectedBook != null) {
651 outOfUi(null, new Runnable() {
652 @Override
653 public void run() {
654 reader.clearLocalReaderCache(selectedBook.getMeta()
655 .getLuid());
656 selectedBook.setCached(false);
657 GuiReaderBook.clearIcon(selectedBook.getMeta());
658 SwingUtilities.invokeLater(new Runnable() {
659 @Override
660 public void run() {
661 selectedBook.repaint();
662 }
663 });
664 }
665 });
666 }
667 }
668 });
669
670 return refresh;
671 }
672
673 /**
674 * Create the delete menu item.
675 *
676 * @param libOk
677 * the library can be queried
678 *
679 * @return the item
680 */
681 private JMenuItem createMenuItemMove(boolean libOk) {
682 JMenu moveTo = new JMenu("Move to...");
683 moveTo.setMnemonic(KeyEvent.VK_M);
684
685 List<String> types = new ArrayList<String>();
686 types.add(null);
687 if (libOk) {
688 types.addAll(reader.getLibrary().getSources());
689 }
690
691 for (String type : types) {
692 JMenuItem item = new JMenuItem(type == null ? "New type..." : type);
693
694 moveTo.add(item);
695 if (type == null) {
696 moveTo.addSeparator();
697 }
698
699 final String ftype = type;
700 item.addActionListener(new ActionListener() {
701 @Override
702 public void actionPerformed(ActionEvent e) {
703 if (selectedBook != null) {
704 String type = ftype;
705 if (type == null) {
706 Object rep = JOptionPane.showInputDialog(
707 GuiReaderFrame.this, "Move to:",
708 "Moving story",
709 JOptionPane.QUESTION_MESSAGE, null, null,
710 selectedBook.getMeta().getSource());
711
712 if (rep == null) {
713 return;
714 }
715
716 type = rep.toString();
717 }
718
719 final String ftype = type;
720 outOfUi(null, new Runnable() {
721 @Override
722 public void run() {
723 reader.changeType(selectedBook.getMeta()
724 .getLuid(), ftype);
725
726 selectedBook = null;
727
728 SwingUtilities.invokeLater(new Runnable() {
729 @Override
730 public void run() {
731 setJMenuBar(createMenu(true));
732 }
733 });
734 }
735 });
736 }
737 }
738 });
739 }
740
741 return moveTo;
742 }
743
744 /**
745 * Create the redownload (then delete original) menu item.
746 *
747 * @return the item
748 */
749 private JMenuItem createMenuItemRedownload() {
750 JMenuItem refresh = new JMenuItem("Redownload", KeyEvent.VK_R);
751 refresh.addActionListener(new ActionListener() {
752 @Override
753 public void actionPerformed(ActionEvent e) {
754 if (selectedBook != null) {
755 final MetaData meta = selectedBook.getMeta();
756 imprt(meta.getUrl(), new StoryRunnable() {
757 @Override
758 public void run(Story story) {
759 reader.delete(meta.getLuid());
760 GuiReaderFrame.this.selectedBook = null;
761 MetaData newMeta = story.getMeta();
762 if (!newMeta.getSource().equals(meta.getSource())) {
763 reader.changeType(newMeta.getLuid(),
764 meta.getSource());
765 }
766 }
767 }, "Removing old copy");
768 }
769 }
770 });
771
772 return refresh;
773 }
774
775 /**
776 * Create the delete menu item.
777 *
778 * @return the item
779 */
780 private JMenuItem createMenuItemDelete() {
781 JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D);
782 delete.addActionListener(new ActionListener() {
783 @Override
784 public void actionPerformed(ActionEvent e) {
785 if (selectedBook != null) {
786 outOfUi(null, new Runnable() {
787 @Override
788 public void run() {
789 reader.delete(selectedBook.getMeta().getLuid());
790 selectedBook = null;
791 }
792 });
793 }
794 }
795 });
796
797 return delete;
798 }
799
800 /**
801 * Create the open menu item for a book or a source (no LUID).
802 *
803 * @return the item
804 */
805 private JMenuItem createMenuItemOpenBook() {
806 JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
807 open.addActionListener(new ActionListener() {
808 @Override
809 public void actionPerformed(ActionEvent e) {
810 if (selectedBook != null) {
811 if (selectedBook.getMeta().getLuid() == null) {
812 removeBookPanes();
813 addBookPane(selectedBook.getMeta().getSource(), true);
814 refreshBooks();
815 } else {
816 openBook(selectedBook);
817 }
818 }
819 }
820 });
821
822 return open;
823 }
824
825 /**
826 * Create the SetCover menu item for a book to change the linked source
827 * cover.
828 *
829 * @return the item
830 */
831 private JMenuItem createMenuItemSetCover() {
832 JMenuItem open = new JMenuItem("Set as cover for source", KeyEvent.VK_C);
833 open.addActionListener(new ActionListener() {
834 @Override
835 public void actionPerformed(ActionEvent e) {
836 if (selectedBook != null) {
837 reader.getLibrary().setSourceCover(
838 selectedBook.getMeta().getSource(),
839 selectedBook.getMeta().getLuid());
840 MetaData source = selectedBook.getMeta().clone();
841 source.setLuid(null);
842 GuiReaderBook.clearIcon(source);
843 }
844 }
845 });
846
847 return open;
848 }
849
850 /**
851 * Open a {@link GuiReaderBook} item.
852 *
853 * @param book
854 * the {@link GuiReaderBook} to open
855 */
856 private void openBook(final GuiReaderBook book) {
857 final Progress pg = new Progress();
858 outOfUi(pg, new Runnable() {
859 @Override
860 public void run() {
861 try {
862 reader.read(book.getMeta().getLuid(), pg);
863 SwingUtilities.invokeLater(new Runnable() {
864 @Override
865 public void run() {
866 book.setCached(true);
867 }
868 });
869 } catch (IOException e) {
870 // TODO: error message?
871 Instance.getTraceHandler().error(e);
872 }
873 }
874 });
875 }
876
877 /**
878 * Process the given action out of the Swing UI thread and link the given
879 * {@link ProgressBar} to the action.
880 * <p>
881 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
882 * to done when the action is done.
883 *
884 * @param progress
885 * the {@link ProgressBar} or NULL
886 * @param run
887 * the action to run
888 */
889 private void outOfUi(Progress progress, final Runnable run) {
890 final Progress pg = new Progress();
891 final Progress reload = new Progress("Reload books");
892 if (progress == null) {
893 progress = new Progress();
894 }
895
896 pg.addProgress(progress, 90);
897 pg.addProgress(reload, 10);
898
899 invalidate();
900 pgBar.setProgress(pg);
901 validate();
902 setEnabled(false);
903
904 new Thread(new Runnable() {
905 @Override
906 public void run() {
907 try {
908 run.run();
909 refreshBooks();
910 } finally {
911 reload.done();
912 if (!pg.isDone()) {
913 // will trigger pgBar ActionListener:
914 pg.done();
915 }
916 }
917 }
918 }, "outOfUi thread").start();
919 }
920
921 /**
922 * Import a {@link Story} into the main {@link LocalLibrary}.
923 * <p>
924 * Should be called inside the UI thread.
925 *
926 * @param askUrl
927 * TRUE for an {@link URL}, false for a {@link File}
928 */
929 private void imprt(boolean askUrl) {
930 JFileChooser fc = new JFileChooser();
931
932 Object url;
933 if (askUrl) {
934 String clipboard = "";
935 try {
936 clipboard = ("" + Toolkit.getDefaultToolkit()
937 .getSystemClipboard().getData(DataFlavor.stringFlavor))
938 .trim();
939 } catch (Exception e) {
940 // No data will be handled
941 }
942
943 if (clipboard == null || !clipboard.startsWith("http")) {
944 clipboard = "";
945 }
946
947 url = JOptionPane.showInputDialog(GuiReaderFrame.this,
948 "url of the story to import?", "Importing from URL",
949 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
950 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
951 url = fc.getSelectedFile().getAbsolutePath();
952 } else {
953 url = null;
954 }
955
956 if (url != null && !url.toString().isEmpty()) {
957 imprt(url.toString(), null, null);
958 }
959 }
960
961 /**
962 * Actually import the {@link Story} into the main {@link LocalLibrary}.
963 * <p>
964 * Should be called inside the UI thread.
965 *
966 * @param url
967 * the {@link Story} to import by {@link URL}
968 * @param onSuccess
969 * Action to execute on success
970 */
971 private void imprt(final String url, final StoryRunnable onSuccess,
972 String onSuccessPgName) {
973 final Progress pg = new Progress();
974 final Progress pgImprt = new Progress();
975 final Progress pgOnSuccess = new Progress(onSuccessPgName);
976 pg.addProgress(pgImprt, 95);
977 pg.addProgress(pgOnSuccess, 5);
978
979 outOfUi(pg, new Runnable() {
980 @Override
981 public void run() {
982 Exception ex = null;
983 Story story = null;
984 try {
985 story = reader.getLibrary().imprt(BasicReader.getUrl(url),
986 pgImprt);
987 } catch (IOException e) {
988 ex = e;
989 }
990
991 final Exception e = ex;
992
993 final boolean ok = (e == null);
994
995 pgOnSuccess.setProgress(0);
996 if (!ok) {
997 if (e instanceof UnknownHostException) {
998 error("URL not supported: " + url, "Cannot import URL",
999 null);
1000 } else {
1001 error("Failed to import " + url + ": \n"
1002 + e.getMessage(), "Cannot import URL", e);
1003 }
1004 } else {
1005 if (onSuccess != null) {
1006 onSuccess.run(story);
1007 }
1008 }
1009 pgOnSuccess.done();
1010 }
1011 });
1012 }
1013
1014 /**
1015 * Enables or disables this component, depending on the value of the
1016 * parameter <code>b</code>. An enabled component can respond to user input
1017 * and generate events. Components are enabled initially by default.
1018 * <p>
1019 * Disabling this component will also affect its children.
1020 *
1021 * @param b
1022 * If <code>true</code>, this component is enabled; otherwise
1023 * this component is disabled
1024 */
1025 @Override
1026 public void setEnabled(boolean b) {
1027 if (bar != null) {
1028 bar.setEnabled(b);
1029 }
1030
1031 for (GuiReaderGroup group : booksByType.keySet()) {
1032 group.setEnabled(b);
1033 }
1034 for (GuiReaderGroup group : booksByAuthor.keySet()) {
1035 group.setEnabled(b);
1036 }
1037 super.setEnabled(b);
1038 repaint();
1039 }
1040
1041 /**
1042 * Display an error message and log the linked {@link Exception}.
1043 *
1044 * @param message
1045 * the message
1046 * @param title
1047 * the title of the error message
1048 * @param e
1049 * the exception to log if any
1050 */
1051 private void error(final String message, final String title, Exception e) {
1052 Instance.getTraceHandler().error(title + ": " + message);
1053 if (e != null) {
1054 Instance.getTraceHandler().error(e);
1055 }
1056
1057 SwingUtilities.invokeLater(new Runnable() {
1058 @Override
1059 public void run() {
1060 JOptionPane.showMessageDialog(GuiReaderFrame.this, message,
1061 title, JOptionPane.ERROR_MESSAGE);
1062 }
1063 });
1064 }
1065 }