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 mainPanel
;
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 mainPanel
= new GuiReaderMainPanel(this, type
);
75 setLayout(new BorderLayout());
80 public JPopupMenu
createBookPopup() {
81 JPopupMenu popup
= new JPopupMenu();
82 popup
.add(createMenuItemOpenBook());
84 popup
.add(createMenuItemExport());
85 popup
.add(createMenuItemMoveTo(true));
86 popup
.add(createMenuItemSetCover());
87 popup
.add(createMenuItemClearCache());
88 popup
.add(createMenuItemRedownload());
90 popup
.add(createMenuItemRename(true));
91 popup
.add(createMenuItemSetAuthor(true));
93 popup
.add(createMenuItemDelete());
95 popup
.add(createMenuItemProperties());
100 public JPopupMenu
createSourcePopup() {
101 JPopupMenu popup
= new JPopupMenu();
102 popup
.add(createMenuItemOpenBook());
107 public void createMenu(boolean libOk
) {
108 JMenuBar bar
= new JMenuBar();
110 JMenu file
= new JMenu("File");
111 file
.setMnemonic(KeyEvent
.VK_F
);
113 JMenuItem imprt
= new JMenuItem("Import URL...", KeyEvent
.VK_U
);
114 imprt
.addActionListener(new ActionListener() {
116 public void actionPerformed(ActionEvent e
) {
117 mainPanel
.imprt(true);
120 JMenuItem imprtF
= new JMenuItem("Import File...", KeyEvent
.VK_F
);
121 imprtF
.addActionListener(new ActionListener() {
123 public void actionPerformed(ActionEvent e
) {
124 mainPanel
.imprt(false);
127 JMenuItem exit
= new JMenuItem("Exit", KeyEvent
.VK_X
);
128 exit
.addActionListener(new ActionListener() {
130 public void actionPerformed(ActionEvent e
) {
131 GuiReaderFrame
.this.dispatchEvent(new WindowEvent(
132 GuiReaderFrame
.this, WindowEvent
.WINDOW_CLOSING
));
136 file
.add(createMenuItemOpenBook());
137 file
.add(createMenuItemExport());
138 file
.add(createMenuItemMoveTo(libOk
));
143 file
.add(createMenuItemRename(libOk
));
144 file
.add(createMenuItemSetAuthor(libOk
));
150 JMenu edit
= new JMenu("Edit");
151 edit
.setMnemonic(KeyEvent
.VK_E
);
153 edit
.add(createMenuItemClearCache());
154 edit
.add(createMenuItemRedownload());
156 edit
.add(createMenuItemDelete());
160 JMenu view
= new JMenu("View");
161 view
.setMnemonic(KeyEvent
.VK_V
);
162 JMenuItem vauthors
= new JMenuItem("Author");
163 vauthors
.setMnemonic(KeyEvent
.VK_A
);
164 vauthors
.addActionListener(new ActionListener() {
166 public void actionPerformed(ActionEvent e
) {
167 mainPanel
.setWords(false);
168 mainPanel
.refreshBooks();
172 JMenuItem vwords
= new JMenuItem("Word count");
173 vwords
.setMnemonic(KeyEvent
.VK_W
);
174 vwords
.addActionListener(new ActionListener() {
176 public void actionPerformed(ActionEvent e
) {
177 mainPanel
.setWords(true);
178 mainPanel
.refreshBooks();
184 Map
<String
, List
<String
>> groupedSources
= new HashMap
<String
, List
<String
>>();
186 groupedSources
= reader
.getLibrary().getSourcesGrouped();
188 JMenu sources
= new JMenu("Sources");
189 sources
.setMnemonic(KeyEvent
.VK_S
);
190 populateMenuSA(sources
, groupedSources
, true);
193 Map
<String
, List
<String
>> goupedAuthors
= new HashMap
<String
, List
<String
>>();
195 goupedAuthors
= reader
.getLibrary().getAuthorsGrouped();
197 JMenu authors
= new JMenu("Authors");
198 authors
.setMnemonic(KeyEvent
.VK_A
);
199 populateMenuSA(authors
, goupedAuthors
, false);
202 JMenu options
= new JMenu("Options");
203 options
.setMnemonic(KeyEvent
.VK_O
);
204 options
.add(createMenuItemConfig());
205 options
.add(createMenuItemUiConfig());
212 private void populateMenuSA(JMenu menu
,
213 Map
<String
, List
<String
>> groupedValues
, boolean type
) {
215 // "All" and "Listing" special items
216 JMenuItem item
= new JMenuItem("All");
217 item
.addActionListener(getActionOpenList(type
, false));
219 item
= new JMenuItem("Listing");
220 item
.addActionListener(getActionOpenList(type
, true));
224 for (final String value
: groupedValues
.keySet()) {
225 List
<String
> list
= groupedValues
.get(value
);
226 if (type
&& list
.size() == 1 && list
.get(0).isEmpty()) {
227 // leaf item source/type
228 item
= new JMenuItem(value
.isEmpty() ?
"[unknown]" : value
);
229 item
.addActionListener(getActionOpen(value
, type
));
233 if (!type
&& groupedValues
.size() == 1) {
234 // only one group of authors
237 dir
= new JMenu(value
.isEmpty() ?
"[unknown]" : value
);
240 for (String sub
: list
) {
241 // " " instead of "" for the visual height
242 String itemName
= sub
.isEmpty() ?
" " : sub
;
243 String actualValue
= value
;
246 if (!sub
.isEmpty()) {
247 actualValue
+= "/" + sub
;
253 item
= new JMenuItem(itemName
);
254 item
.addActionListener(getActionOpen(actualValue
, type
));
266 * Return an {@link ActionListener} that will set the given source (type) as
267 * the selected/displayed one.
270 * the type (source) to select, cannot be NULL
272 * @return the {@link ActionListener}
274 private ActionListener
getActionOpen(final String source
, final boolean type
) {
275 return new ActionListener() {
277 public void actionPerformed(ActionEvent e
) {
278 mainPanel
.removeBookPanes();
279 mainPanel
.addBookPane(source
, type
);
280 mainPanel
.refreshBooks();
285 private ActionListener
getActionOpenList(final boolean type
,
286 final boolean listMode
) {
287 return new ActionListener() {
289 public void actionPerformed(ActionEvent e
) {
290 mainPanel
.removeBookPanes();
291 mainPanel
.addBookPane(type
, listMode
);
292 mainPanel
.refreshBooks();
298 * Create the Fanfix Configuration menu item.
302 private JMenuItem
createMenuItemConfig() {
303 final String title
= "Fanfix Configuration";
304 JMenuItem item
= new JMenuItem(title
);
305 item
.setMnemonic(KeyEvent
.VK_F
);
307 item
.addActionListener(new ActionListener() {
309 public void actionPerformed(ActionEvent e
) {
310 ConfigEditor
<Config
> ed
= new ConfigEditor
<Config
>(
311 Config
.class, Instance
.getConfig(),
312 "This is where you configure the options of the program.");
313 JFrame frame
= new JFrame(title
);
315 frame
.setSize(800, 600);
316 frame
.setVisible(true);
324 * Create the UI Configuration menu item.
328 private JMenuItem
createMenuItemUiConfig() {
329 final String title
= "UI Configuration";
330 JMenuItem item
= new JMenuItem(title
);
331 item
.setMnemonic(KeyEvent
.VK_U
);
333 item
.addActionListener(new ActionListener() {
335 public void actionPerformed(ActionEvent e
) {
336 ConfigEditor
<UiConfig
> ed
= new ConfigEditor
<UiConfig
>(
337 UiConfig
.class, Instance
.getUiConfig(),
338 "This is where you configure the graphical appearence of the program.");
339 JFrame frame
= new JFrame(title
);
341 frame
.setSize(800, 600);
342 frame
.setVisible(true);
350 * Create the export menu item.
354 private JMenuItem
createMenuItemExport() {
355 final JFileChooser fc
= new JFileChooser();
356 fc
.setAcceptAllFileFilterUsed(false);
358 final Map
<FileFilter
, OutputType
> filters
= new HashMap
<FileFilter
, OutputType
>();
359 for (OutputType type
: OutputType
.values()) {
360 String ext
= type
.getDefaultExtension(false);
361 String desc
= type
.getDesc(false);
363 if (ext
== null || ext
.isEmpty()) {
364 filters
.put(createAllFilter(desc
), type
);
366 filters
.put(new FileNameExtensionFilter(desc
, ext
), type
);
370 // First the "ALL" filters, then, the extension filters
371 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
372 if (!(entry
.getKey() instanceof FileNameExtensionFilter
)) {
373 fc
.addChoosableFileFilter(entry
.getKey());
376 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
377 if (entry
.getKey() instanceof FileNameExtensionFilter
) {
378 fc
.addChoosableFileFilter(entry
.getKey());
383 JMenuItem export
= new JMenuItem("Save as...", KeyEvent
.VK_S
);
384 export
.addActionListener(new ActionListener() {
386 public void actionPerformed(ActionEvent e
) {
387 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
388 if (selectedBook
!= null) {
389 fc
.showDialog(GuiReaderFrame
.this, "Save");
390 if (fc
.getSelectedFile() != null) {
391 final OutputType type
= filters
.get(fc
.getFileFilter());
392 final String path
= fc
.getSelectedFile()
394 + type
.getDefaultExtension(false);
395 final Progress pg
= new Progress();
396 mainPanel
.outOfUi(pg
, new Runnable() {
400 reader
.getLibrary().export(
401 selectedBook
.getMeta().getLuid(),
403 } catch (IOException e
) {
404 Instance
.getTraceHandler().error(e
);
417 * Create a {@link FileFilter} that accepts all files and return the given
425 private FileFilter
createAllFilter(final String desc
) {
426 return new FileFilter() {
428 public String
getDescription() {
433 public boolean accept(File f
) {
440 * Create the refresh (delete cache) menu item.
444 private JMenuItem
createMenuItemClearCache() {
445 JMenuItem refresh
= new JMenuItem("Clear cache", KeyEvent
.VK_C
);
446 refresh
.addActionListener(new ActionListener() {
448 public void actionPerformed(ActionEvent e
) {
449 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
450 if (selectedBook
!= null) {
451 mainPanel
.outOfUi(null, new Runnable() {
454 reader
.clearLocalReaderCache(selectedBook
.getMeta()
456 selectedBook
.setCached(false);
457 GuiReaderCoverImager
.clearIcon(selectedBook
459 SwingUtilities
.invokeLater(new Runnable() {
462 selectedBook
.repaint();
475 * Create the "move to" menu item.
478 * the library can be queried
482 private JMenuItem
createMenuItemMoveTo(boolean libOk
) {
483 JMenu changeTo
= new JMenu("Move to");
484 changeTo
.setMnemonic(KeyEvent
.VK_M
);
486 Map
<String
, List
<String
>> groupedSources
= new HashMap
<String
, List
<String
>>();
488 groupedSources
= reader
.getLibrary().getSourcesGrouped();
491 JMenuItem item
= new JMenuItem("New type...");
492 item
.addActionListener(createMoveAction(MoveAction
.SOURCE
, null));
494 changeTo
.addSeparator();
496 for (final String type
: groupedSources
.keySet()) {
497 List
<String
> list
= groupedSources
.get(type
);
498 if (list
.size() == 1 && list
.get(0).isEmpty()) {
499 item
= new JMenuItem(type
);
500 item
.addActionListener(createMoveAction(MoveAction
.SOURCE
, type
));
503 JMenu dir
= new JMenu(type
);
504 for (String sub
: list
) {
505 // " " instead of "" for the visual height
506 String itemName
= sub
.isEmpty() ?
" " : sub
;
507 String actualType
= type
;
508 if (!sub
.isEmpty()) {
509 actualType
+= "/" + sub
;
512 item
= new JMenuItem(itemName
);
513 item
.addActionListener(createMoveAction(MoveAction
.SOURCE
,
525 * Create the "set author" menu item.
528 * the library can be queried
532 private JMenuItem
createMenuItemSetAuthor(boolean libOk
) {
533 JMenu changeTo
= new JMenu("Set author");
534 changeTo
.setMnemonic(KeyEvent
.VK_A
);
537 JMenuItem newItem
= new JMenuItem("New author...");
538 changeTo
.add(newItem
);
539 changeTo
.addSeparator();
540 newItem
.addActionListener(createMoveAction(MoveAction
.AUTHOR
, null));
544 Map
<String
, List
<String
>> groupedAuthors
= reader
.getLibrary()
545 .getAuthorsGrouped();
547 if (groupedAuthors
.size() > 1) {
548 for (String key
: groupedAuthors
.keySet()) {
549 JMenu group
= new JMenu(key
);
550 for (String value
: groupedAuthors
.get(key
)) {
551 JMenuItem item
= new JMenuItem(value
);
552 item
.addActionListener(createMoveAction(
553 MoveAction
.AUTHOR
, value
));
558 } else if (groupedAuthors
.size() == 1) {
559 for (String value
: groupedAuthors
.values().iterator().next()) {
560 JMenuItem item
= new JMenuItem(value
);
561 item
.addActionListener(createMoveAction(MoveAction
.AUTHOR
,
572 * Create the "rename" menu item.
575 * the library can be queried
579 private JMenuItem
createMenuItemRename(
580 @SuppressWarnings("unused") boolean libOk
) {
581 JMenuItem changeTo
= new JMenuItem("Rename...");
582 changeTo
.setMnemonic(KeyEvent
.VK_R
);
583 changeTo
.addActionListener(createMoveAction(MoveAction
.TITLE
, null));
587 private ActionListener
createMoveAction(final MoveAction what
,
589 return new ActionListener() {
591 public void actionPerformed(ActionEvent e
) {
592 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
593 if (selectedBook
!= null) {
594 String changeTo
= type
;
597 if (what
== MoveAction
.SOURCE
) {
598 init
= selectedBook
.getMeta().getSource();
599 } else if (what
== MoveAction
.TITLE
) {
600 init
= selectedBook
.getMeta().getTitle();
601 } else if (what
== MoveAction
.AUTHOR
) {
602 init
= selectedBook
.getMeta().getAuthor();
605 Object rep
= JOptionPane
.showInputDialog(
606 GuiReaderFrame
.this, "Move to:",
607 "Moving story", JOptionPane
.QUESTION_MESSAGE
,
614 changeTo
= rep
.toString();
617 final String fChangeTo
= changeTo
;
618 mainPanel
.outOfUi(null, new Runnable() {
621 if (what
.equals("SOURCE")) {
622 reader
.changeSource(selectedBook
.getMeta()
623 .getLuid(), fChangeTo
);
624 } else if (what
.equals("TITLE")) {
625 reader
.changeTitle(selectedBook
.getMeta()
626 .getLuid(), fChangeTo
);
627 } else if (what
.equals("AUTHOR")) {
628 reader
.changeAuthor(selectedBook
.getMeta()
629 .getLuid(), fChangeTo
);
632 mainPanel
.unsetSelectedBook();
634 SwingUtilities
.invokeLater(new Runnable() {
648 * Create the redownload (then delete original) menu item.
652 private JMenuItem
createMenuItemRedownload() {
653 JMenuItem refresh
= new JMenuItem("Redownload", KeyEvent
.VK_R
);
654 refresh
.addActionListener(new ActionListener() {
656 public void actionPerformed(ActionEvent e
) {
657 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
658 if (selectedBook
!= null) {
659 final MetaData meta
= selectedBook
.getMeta();
660 mainPanel
.imprt(meta
.getUrl(), new StoryRunnable() {
662 public void run(Story story
) {
663 reader
.delete(meta
.getLuid());
664 mainPanel
.unsetSelectedBook();
665 MetaData newMeta
= story
.getMeta();
666 if (!newMeta
.getSource().equals(meta
.getSource())) {
667 reader
.changeSource(newMeta
.getLuid(),
671 }, "Removing old copy");
680 * Create the delete menu item.
684 private JMenuItem
createMenuItemDelete() {
685 JMenuItem delete
= new JMenuItem("Delete", KeyEvent
.VK_D
);
686 delete
.addActionListener(new ActionListener() {
688 public void actionPerformed(ActionEvent e
) {
689 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
690 if (selectedBook
!= null) {
691 mainPanel
.outOfUi(null, new Runnable() {
694 reader
.delete(selectedBook
.getMeta().getLuid());
695 mainPanel
.unsetSelectedBook();
706 * Create the properties menu item.
710 private JMenuItem
createMenuItemProperties() {
711 JMenuItem delete
= new JMenuItem("Properties", KeyEvent
.VK_P
);
712 delete
.addActionListener(new ActionListener() {
714 public void actionPerformed(ActionEvent e
) {
715 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
716 if (selectedBook
!= null) {
717 mainPanel
.outOfUi(null, new Runnable() {
720 new GuiReaderPropertiesFrame(reader
, selectedBook
721 .getMeta()).setVisible(true);
732 * Create the open menu item for a book or a source/type (no LUID).
736 public JMenuItem
createMenuItemOpenBook() {
737 JMenuItem open
= new JMenuItem("Open", KeyEvent
.VK_O
);
738 open
.addActionListener(new ActionListener() {
740 public void actionPerformed(ActionEvent e
) {
741 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
742 if (selectedBook
!= null) {
743 if (selectedBook
.getMeta().getLuid() == null) {
744 mainPanel
.removeBookPanes();
745 mainPanel
.addBookPane(selectedBook
.getMeta()
747 mainPanel
.refreshBooks();
749 mainPanel
.openBook(selectedBook
);
759 * Create the SetCover menu item for a book to change the linked source
764 private JMenuItem
createMenuItemSetCover() {
765 JMenuItem open
= new JMenuItem("Set as cover for source", KeyEvent
.VK_C
);
766 open
.addActionListener(new ActionListener() {
768 public void actionPerformed(ActionEvent e
) {
769 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
770 if (selectedBook
!= null) {
771 reader
.getLibrary().setSourceCover(
772 selectedBook
.getMeta().getSource(),
773 selectedBook
.getMeta().getLuid());
774 MetaData source
= selectedBook
.getMeta().clone();
775 source
.setLuid(null);
776 GuiReaderCoverImager
.clearIcon(source
);
785 * Display an error message and log the linked {@link Exception}.
790 * the title of the error message
792 * the exception to log if any
794 public void error(final String message
, final String title
, Exception e
) {
795 Instance
.getTraceHandler().error(title
+ ": " + message
);
797 Instance
.getTraceHandler().error(e
);
800 SwingUtilities
.invokeLater(new Runnable() {
803 JOptionPane
.showMessageDialog(GuiReaderFrame
.this, message
,
804 title
, JOptionPane
.ERROR_MESSAGE
);
810 public GuiReader
getReader() {