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