1 package be
.nikiroo
.fanfix
.reader
.ui
;
3 import java
.awt
.BorderLayout
;
5 import java
.awt
.event
.ActionEvent
;
6 import java
.awt
.event
.ActionListener
;
7 import java
.awt
.event
.KeyEvent
;
8 import java
.awt
.event
.WindowEvent
;
10 import java
.io
.IOException
;
11 import java
.util
.HashMap
;
12 import java
.util
.List
;
14 import java
.util
.Map
.Entry
;
16 import javax
.swing
.JFileChooser
;
17 import javax
.swing
.JFrame
;
18 import javax
.swing
.JMenu
;
19 import javax
.swing
.JMenuBar
;
20 import javax
.swing
.JMenuItem
;
21 import javax
.swing
.JOptionPane
;
22 import javax
.swing
.JPopupMenu
;
23 import javax
.swing
.SwingUtilities
;
24 import javax
.swing
.filechooser
.FileFilter
;
25 import javax
.swing
.filechooser
.FileNameExtensionFilter
;
27 import be
.nikiroo
.fanfix
.Instance
;
28 import be
.nikiroo
.fanfix
.bundles
.Config
;
29 import be
.nikiroo
.fanfix
.bundles
.UiConfig
;
30 import be
.nikiroo
.fanfix
.data
.MetaData
;
31 import be
.nikiroo
.fanfix
.data
.Story
;
32 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
33 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
34 import be
.nikiroo
.fanfix
.reader
.BasicReader
;
35 import be
.nikiroo
.fanfix
.reader
.ui
.GuiReaderMainPanel
.FrameHelper
;
36 import be
.nikiroo
.fanfix
.reader
.ui
.GuiReaderMainPanel
.StoryRunnable
;
37 import be
.nikiroo
.utils
.Progress
;
38 import be
.nikiroo
.utils
.Version
;
39 import be
.nikiroo
.utils
.ui
.ConfigEditor
;
42 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
43 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
44 * way to copy them to the {@link GuiReader} cache (
45 * {@link BasicReader#getLibrary()}), read them, delete them...
49 class GuiReaderFrame
extends JFrame
implements FrameHelper
{
50 private static final long serialVersionUID
= 1L;
51 private GuiReader reader
;
52 private GuiReaderMainPanel helpee
;
54 private enum MoveAction
{
59 * Create a new {@link GuiReaderFrame}.
62 * the associated {@link GuiReader} to forward some commands and
63 * access its {@link LocalLibrary}
65 * the type of {@link Story} to load, or NULL for all types
67 public GuiReaderFrame(GuiReader reader
, String type
) {
68 super(String
.format("Fanfix %s Library", Version
.getCurrentVersion()));
72 // TODO: should we still have that??
73 setDefaultCloseOperation(JFrame
.EXIT_ON_CLOSE
);
75 helpee
= new GuiReaderMainPanel(this, type
);
78 setLayout(new BorderLayout());
85 public JPopupMenu
createBookPopup() {
86 JPopupMenu popup
= new JPopupMenu();
87 popup
.add(createMenuItemOpenBook());
89 popup
.add(createMenuItemExport());
90 popup
.add(createMenuItemMoveTo(true));
91 popup
.add(createMenuItemSetCover());
92 popup
.add(createMenuItemClearCache());
93 popup
.add(createMenuItemRedownload());
95 popup
.add(createMenuItemRename(true));
96 popup
.add(createMenuItemSetAuthor(true));
98 popup
.add(createMenuItemDelete());
100 popup
.add(createMenuItemProperties());
105 public JPopupMenu
createSourcePopup() {
106 JPopupMenu popup
= new JPopupMenu();
107 popup
.add(createMenuItemOpenBook());
112 public void createMenu(boolean libOk
) {
113 JMenuBar bar
= new JMenuBar();
115 JMenu file
= new JMenu("File");
116 file
.setMnemonic(KeyEvent
.VK_F
);
118 JMenuItem imprt
= new JMenuItem("Import URL...", KeyEvent
.VK_U
);
119 imprt
.addActionListener(new ActionListener() {
121 public void actionPerformed(ActionEvent e
) {
125 JMenuItem imprtF
= new JMenuItem("Import File...", KeyEvent
.VK_F
);
126 imprtF
.addActionListener(new ActionListener() {
128 public void actionPerformed(ActionEvent e
) {
132 JMenuItem exit
= new JMenuItem("Exit", KeyEvent
.VK_X
);
133 exit
.addActionListener(new ActionListener() {
135 public void actionPerformed(ActionEvent e
) {
136 GuiReaderFrame
.this.dispatchEvent(new WindowEvent(
137 GuiReaderFrame
.this, WindowEvent
.WINDOW_CLOSING
));
141 file
.add(createMenuItemOpenBook());
142 file
.add(createMenuItemExport());
143 file
.add(createMenuItemMoveTo(libOk
));
148 file
.add(createMenuItemRename(libOk
));
149 file
.add(createMenuItemSetAuthor(libOk
));
155 JMenu edit
= new JMenu("Edit");
156 edit
.setMnemonic(KeyEvent
.VK_E
);
158 edit
.add(createMenuItemClearCache());
159 edit
.add(createMenuItemRedownload());
161 edit
.add(createMenuItemDelete());
165 JMenu view
= new JMenu("View");
166 view
.setMnemonic(KeyEvent
.VK_V
);
167 JMenuItem vauthors
= new JMenuItem("Author");
168 vauthors
.setMnemonic(KeyEvent
.VK_A
);
169 vauthors
.addActionListener(new ActionListener() {
171 public void actionPerformed(ActionEvent e
) {
172 helpee
.setWords(false);
173 helpee
.refreshBooks();
177 JMenuItem vwords
= new JMenuItem("Word count");
178 vwords
.setMnemonic(KeyEvent
.VK_W
);
179 vwords
.addActionListener(new ActionListener() {
181 public void actionPerformed(ActionEvent e
) {
182 helpee
.setWords(true);
183 helpee
.refreshBooks();
189 Map
<String
, List
<String
>> groupedSources
= new HashMap
<String
, List
<String
>>();
191 groupedSources
= reader
.getLibrary().getSourcesGrouped();
193 JMenu sources
= new JMenu("Sources");
194 sources
.setMnemonic(KeyEvent
.VK_S
);
195 populateMenuSA(sources
, groupedSources
, true);
198 Map
<String
, List
<String
>> goupedAuthors
= new HashMap
<String
, List
<String
>>();
200 goupedAuthors
= reader
.getLibrary().getAuthorsGrouped();
202 JMenu authors
= new JMenu("Authors");
203 authors
.setMnemonic(KeyEvent
.VK_A
);
204 populateMenuSA(authors
, goupedAuthors
, false);
207 JMenu options
= new JMenu("Options");
208 options
.setMnemonic(KeyEvent
.VK_O
);
209 options
.add(createMenuItemConfig());
210 options
.add(createMenuItemUiConfig());
217 private void populateMenuSA(JMenu menu
,
218 Map
<String
, List
<String
>> groupedValues
, boolean type
) {
220 // "All" and "Listing" special items
221 JMenuItem item
= new JMenuItem("All");
222 item
.addActionListener(getActionOpenList(type
, false));
224 item
= new JMenuItem("Listing");
225 item
.addActionListener(getActionOpenList(type
, true));
229 for (final String value
: groupedValues
.keySet()) {
230 List
<String
> list
= groupedValues
.get(value
);
231 if (type
&& list
.size() == 1 && list
.get(0).isEmpty()) {
232 // leaf item source/type
233 item
= new JMenuItem(value
.isEmpty() ?
"[unknown]" : value
);
234 item
.addActionListener(getActionOpen(value
, type
));
238 if (!type
&& groupedValues
.size() == 1) {
239 // only one group of authors
242 dir
= new JMenu(value
.isEmpty() ?
"[unknown]" : value
);
245 for (String sub
: list
) {
246 // " " instead of "" for the visual height
247 String itemName
= sub
.isEmpty() ?
" " : sub
;
248 String actualValue
= value
;
251 if (!sub
.isEmpty()) {
252 actualValue
+= "/" + sub
;
258 item
= new JMenuItem(itemName
);
259 item
.addActionListener(getActionOpen(actualValue
, type
));
271 * Return an {@link ActionListener} that will set the given source (type) as
272 * the selected/displayed one.
275 * the type (source) to select, cannot be NULL
277 * @return the {@link ActionListener}
279 private ActionListener
getActionOpen(final String source
, final boolean type
) {
280 return new ActionListener() {
282 public void actionPerformed(ActionEvent e
) {
283 helpee
.removeBookPanes();
284 helpee
.addBookPane(source
, type
);
285 helpee
.refreshBooks();
290 private ActionListener
getActionOpenList(final boolean type
,
291 final boolean listMode
) {
292 return new ActionListener() {
294 public void actionPerformed(ActionEvent e
) {
295 helpee
.removeBookPanes();
296 helpee
.addBookPane(type
, listMode
);
297 helpee
.refreshBooks();
303 * Create the Fanfix Configuration menu item.
307 private JMenuItem
createMenuItemConfig() {
308 final String title
= "Fanfix Configuration";
309 JMenuItem item
= new JMenuItem(title
);
310 item
.setMnemonic(KeyEvent
.VK_F
);
312 item
.addActionListener(new ActionListener() {
314 public void actionPerformed(ActionEvent e
) {
315 ConfigEditor
<Config
> ed
= new ConfigEditor
<Config
>(
316 Config
.class, Instance
.getConfig(),
317 "This is where you configure the options of the program.");
318 JFrame frame
= new JFrame(title
);
320 frame
.setSize(800, 600);
321 frame
.setVisible(true);
329 * Create the UI Configuration menu item.
333 private JMenuItem
createMenuItemUiConfig() {
334 final String title
= "UI Configuration";
335 JMenuItem item
= new JMenuItem(title
);
336 item
.setMnemonic(KeyEvent
.VK_U
);
338 item
.addActionListener(new ActionListener() {
340 public void actionPerformed(ActionEvent e
) {
341 ConfigEditor
<UiConfig
> ed
= new ConfigEditor
<UiConfig
>(
342 UiConfig
.class, Instance
.getUiConfig(),
343 "This is where you configure the graphical appearence of the program.");
344 JFrame frame
= new JFrame(title
);
346 frame
.setSize(800, 600);
347 frame
.setVisible(true);
355 * Create the export menu item.
359 private JMenuItem
createMenuItemExport() {
360 final JFileChooser fc
= new JFileChooser();
361 fc
.setAcceptAllFileFilterUsed(false);
363 final Map
<FileFilter
, OutputType
> filters
= new HashMap
<FileFilter
, OutputType
>();
364 for (OutputType type
: OutputType
.values()) {
365 String ext
= type
.getDefaultExtension(false);
366 String desc
= type
.getDesc(false);
368 if (ext
== null || ext
.isEmpty()) {
369 filters
.put(createAllFilter(desc
), type
);
371 filters
.put(new FileNameExtensionFilter(desc
, ext
), type
);
375 // First the "ALL" filters, then, the extension filters
376 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
377 if (!(entry
.getKey() instanceof FileNameExtensionFilter
)) {
378 fc
.addChoosableFileFilter(entry
.getKey());
381 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
382 if (entry
.getKey() instanceof FileNameExtensionFilter
) {
383 fc
.addChoosableFileFilter(entry
.getKey());
388 JMenuItem export
= new JMenuItem("Save as...", KeyEvent
.VK_S
);
389 export
.addActionListener(new ActionListener() {
391 public void actionPerformed(ActionEvent e
) {
392 final GuiReaderBook selectedBook
= helpee
.getSelectedBook();
393 if (selectedBook
!= null) {
394 fc
.showDialog(GuiReaderFrame
.this, "Save");
395 if (fc
.getSelectedFile() != null) {
396 final OutputType type
= filters
.get(fc
.getFileFilter());
397 final String path
= fc
.getSelectedFile()
399 + type
.getDefaultExtension(false);
400 final Progress pg
= new Progress();
401 helpee
.outOfUi(pg
, new Runnable() {
405 reader
.getLibrary().export(
406 selectedBook
.getMeta().getLuid(),
408 } catch (IOException e
) {
409 Instance
.getTraceHandler().error(e
);
422 * Create a {@link FileFilter} that accepts all files and return the given
430 private FileFilter
createAllFilter(final String desc
) {
431 return new FileFilter() {
433 public String
getDescription() {
438 public boolean accept(File f
) {
445 * Create the refresh (delete cache) menu item.
449 private JMenuItem
createMenuItemClearCache() {
450 JMenuItem refresh
= new JMenuItem("Clear cache", KeyEvent
.VK_C
);
451 refresh
.addActionListener(new ActionListener() {
453 public void actionPerformed(ActionEvent e
) {
454 final GuiReaderBook selectedBook
= helpee
.getSelectedBook();
455 if (selectedBook
!= null) {
456 helpee
.outOfUi(null, new Runnable() {
459 reader
.clearLocalReaderCache(selectedBook
.getMeta()
461 selectedBook
.setCached(false);
462 GuiReaderCoverImager
.clearIcon(selectedBook
464 SwingUtilities
.invokeLater(new Runnable() {
467 selectedBook
.repaint();
480 * Create the "move to" menu item.
483 * the library can be queried
487 private JMenuItem
createMenuItemMoveTo(boolean libOk
) {
488 JMenu changeTo
= new JMenu("Move to");
489 changeTo
.setMnemonic(KeyEvent
.VK_M
);
491 Map
<String
, List
<String
>> groupedSources
= new HashMap
<String
, List
<String
>>();
493 groupedSources
= reader
.getLibrary().getSourcesGrouped();
496 JMenuItem item
= new JMenuItem("New type...");
497 item
.addActionListener(createMoveAction(MoveAction
.SOURCE
, null));
499 changeTo
.addSeparator();
501 for (final String type
: groupedSources
.keySet()) {
502 List
<String
> list
= groupedSources
.get(type
);
503 if (list
.size() == 1 && list
.get(0).isEmpty()) {
504 item
= new JMenuItem(type
);
505 item
.addActionListener(createMoveAction(MoveAction
.SOURCE
, type
));
508 JMenu dir
= new JMenu(type
);
509 for (String sub
: list
) {
510 // " " instead of "" for the visual height
511 String itemName
= sub
.isEmpty() ?
" " : sub
;
512 String actualType
= type
;
513 if (!sub
.isEmpty()) {
514 actualType
+= "/" + sub
;
517 item
= new JMenuItem(itemName
);
518 item
.addActionListener(createMoveAction(MoveAction
.SOURCE
,
530 * Create the "set author" menu item.
533 * the library can be queried
537 private JMenuItem
createMenuItemSetAuthor(boolean libOk
) {
538 JMenu changeTo
= new JMenu("Set author");
539 changeTo
.setMnemonic(KeyEvent
.VK_A
);
542 JMenuItem newItem
= new JMenuItem("New author...");
543 changeTo
.add(newItem
);
544 changeTo
.addSeparator();
545 newItem
.addActionListener(createMoveAction(MoveAction
.AUTHOR
, null));
549 Map
<String
, List
<String
>> groupedAuthors
= reader
.getLibrary()
550 .getAuthorsGrouped();
552 if (groupedAuthors
.size() > 1) {
553 for (String key
: groupedAuthors
.keySet()) {
554 JMenu group
= new JMenu(key
);
555 for (String value
: groupedAuthors
.get(key
)) {
556 JMenuItem item
= new JMenuItem(value
);
557 item
.addActionListener(createMoveAction(
558 MoveAction
.AUTHOR
, value
));
563 } else if (groupedAuthors
.size() == 1) {
564 for (String value
: groupedAuthors
.values().iterator().next()) {
565 JMenuItem item
= new JMenuItem(value
);
566 item
.addActionListener(createMoveAction(MoveAction
.AUTHOR
,
577 * Create the "rename" menu item.
580 * the library can be queried
584 private JMenuItem
createMenuItemRename(
585 @SuppressWarnings("unused") boolean libOk
) {
586 JMenuItem changeTo
= new JMenuItem("Rename...");
587 changeTo
.setMnemonic(KeyEvent
.VK_R
);
588 changeTo
.addActionListener(createMoveAction(MoveAction
.TITLE
, null));
592 private ActionListener
createMoveAction(final MoveAction what
,
594 return new ActionListener() {
596 public void actionPerformed(ActionEvent e
) {
597 final GuiReaderBook selectedBook
= helpee
.getSelectedBook();
598 if (selectedBook
!= null) {
599 String changeTo
= type
;
602 if (what
== MoveAction
.SOURCE
) {
603 init
= selectedBook
.getMeta().getSource();
604 } else if (what
== MoveAction
.TITLE
) {
605 init
= selectedBook
.getMeta().getTitle();
606 } else if (what
== MoveAction
.AUTHOR
) {
607 init
= selectedBook
.getMeta().getAuthor();
610 Object rep
= JOptionPane
.showInputDialog(
611 GuiReaderFrame
.this, "Move to:",
612 "Moving story", JOptionPane
.QUESTION_MESSAGE
,
619 changeTo
= rep
.toString();
622 final String fChangeTo
= changeTo
;
623 helpee
.outOfUi(null, new Runnable() {
626 if (what
.equals("SOURCE")) {
627 reader
.changeSource(selectedBook
.getMeta()
628 .getLuid(), fChangeTo
);
629 } else if (what
.equals("TITLE")) {
630 reader
.changeTitle(selectedBook
.getMeta()
631 .getLuid(), fChangeTo
);
632 } else if (what
.equals("AUTHOR")) {
633 reader
.changeAuthor(selectedBook
.getMeta()
634 .getLuid(), fChangeTo
);
637 helpee
.unsetSelectedBook();
639 SwingUtilities
.invokeLater(new Runnable() {
653 * Create the redownload (then delete original) menu item.
657 private JMenuItem
createMenuItemRedownload() {
658 JMenuItem refresh
= new JMenuItem("Redownload", KeyEvent
.VK_R
);
659 refresh
.addActionListener(new ActionListener() {
661 public void actionPerformed(ActionEvent e
) {
662 final GuiReaderBook selectedBook
= helpee
.getSelectedBook();
663 if (selectedBook
!= null) {
664 final MetaData meta
= selectedBook
.getMeta();
665 helpee
.imprt(meta
.getUrl(), new StoryRunnable() {
667 public void run(Story story
) {
668 reader
.delete(meta
.getLuid());
669 helpee
.unsetSelectedBook();
670 MetaData newMeta
= story
.getMeta();
671 if (!newMeta
.getSource().equals(meta
.getSource())) {
672 reader
.changeSource(newMeta
.getLuid(),
676 }, "Removing old copy");
685 * Create the delete menu item.
689 private JMenuItem
createMenuItemDelete() {
690 JMenuItem delete
= new JMenuItem("Delete", KeyEvent
.VK_D
);
691 delete
.addActionListener(new ActionListener() {
693 public void actionPerformed(ActionEvent e
) {
694 final GuiReaderBook selectedBook
= helpee
.getSelectedBook();
695 if (selectedBook
!= null) {
696 helpee
.outOfUi(null, new Runnable() {
699 reader
.delete(selectedBook
.getMeta().getLuid());
700 helpee
.unsetSelectedBook();
711 * Create the properties menu item.
715 private JMenuItem
createMenuItemProperties() {
716 JMenuItem delete
= new JMenuItem("Properties", KeyEvent
.VK_P
);
717 delete
.addActionListener(new ActionListener() {
719 public void actionPerformed(ActionEvent e
) {
720 final GuiReaderBook selectedBook
= helpee
.getSelectedBook();
721 if (selectedBook
!= null) {
722 helpee
.outOfUi(null, new Runnable() {
725 new GuiReaderPropertiesFrame(reader
, selectedBook
726 .getMeta()).setVisible(true);
737 * Create the open menu item for a book or a source/type (no LUID).
741 public JMenuItem
createMenuItemOpenBook() {
742 JMenuItem open
= new JMenuItem("Open", KeyEvent
.VK_O
);
743 open
.addActionListener(new ActionListener() {
745 public void actionPerformed(ActionEvent e
) {
746 final GuiReaderBook selectedBook
= helpee
.getSelectedBook();
747 if (selectedBook
!= null) {
748 if (selectedBook
.getMeta().getLuid() == null) {
749 helpee
.removeBookPanes();
750 helpee
.addBookPane(selectedBook
.getMeta().getSource(),
752 helpee
.refreshBooks();
754 helpee
.openBook(selectedBook
);
764 * Create the SetCover menu item for a book to change the linked source
769 private JMenuItem
createMenuItemSetCover() {
770 JMenuItem open
= new JMenuItem("Set as cover for source", KeyEvent
.VK_C
);
771 open
.addActionListener(new ActionListener() {
773 public void actionPerformed(ActionEvent e
) {
774 final GuiReaderBook selectedBook
= helpee
.getSelectedBook();
775 if (selectedBook
!= null) {
776 reader
.getLibrary().setSourceCover(
777 selectedBook
.getMeta().getSource(),
778 selectedBook
.getMeta().getLuid());
779 MetaData source
= selectedBook
.getMeta().clone();
780 source
.setLuid(null);
781 GuiReaderCoverImager
.clearIcon(source
);
790 * Display an error message and log the linked {@link Exception}.
795 * the title of the error message
797 * the exception to log if any
799 public void error(final String message
, final String title
, Exception e
) {
800 Instance
.getTraceHandler().error(title
+ ": " + message
);
802 Instance
.getTraceHandler().error(e
);
805 SwingUtilities
.invokeLater(new Runnable() {
808 JOptionPane
.showMessageDialog(GuiReaderFrame
.this, message
,
809 title
, JOptionPane
.ERROR_MESSAGE
);
815 public GuiReader
getReader() {