1 package be
.nikiroo
.fanfix
.reader
;
3 import java
.awt
.BorderLayout
;
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
;
14 import java
.io
.IOException
;
16 import java
.util
.ArrayList
;
17 import java
.util
.HashMap
;
18 import java
.util
.List
;
20 import java
.util
.Map
.Entry
;
22 import javax
.swing
.BoxLayout
;
23 import javax
.swing
.JFileChooser
;
24 import javax
.swing
.JFrame
;
25 import javax
.swing
.JMenu
;
26 import javax
.swing
.JMenuBar
;
27 import javax
.swing
.JMenuItem
;
28 import javax
.swing
.JOptionPane
;
29 import javax
.swing
.JPanel
;
30 import javax
.swing
.JPopupMenu
;
31 import javax
.swing
.JScrollPane
;
32 import javax
.swing
.SwingUtilities
;
33 import javax
.swing
.filechooser
.FileFilter
;
34 import javax
.swing
.filechooser
.FileNameExtensionFilter
;
36 import be
.nikiroo
.fanfix
.Instance
;
37 import be
.nikiroo
.fanfix
.bundles
.Config
;
38 import be
.nikiroo
.fanfix
.bundles
.UiConfig
;
39 import be
.nikiroo
.fanfix
.data
.MetaData
;
40 import be
.nikiroo
.fanfix
.data
.Story
;
41 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
42 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
43 import be
.nikiroo
.fanfix
.reader
.GuiReaderBook
.BookActionListener
;
44 import be
.nikiroo
.utils
.Progress
;
45 import be
.nikiroo
.utils
.Version
;
46 import be
.nikiroo
.utils
.ui
.ConfigEditor
;
47 import be
.nikiroo
.utils
.ui
.ProgressBar
;
50 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
51 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
52 * way to copy them to the {@link GuiReader} cache (
53 * {@link BasicReader#getLibrary()}), read them, delete them...
57 class GuiReaderFrame
extends JFrame
{
58 private static final long serialVersionUID
= 1L;
59 private GuiReader reader
;
60 private Map
<GuiReaderGroup
, String
> booksByType
;
61 private Map
<GuiReaderGroup
, String
> booksByAuthor
;
64 private ProgressBar pgBar
;
66 private GuiReaderBook selectedBook
;
67 private boolean words
; // words or authors (secondary info on books)
70 * Create a new {@link GuiReaderFrame}.
73 * the associated {@link GuiReader} to forward some commands and
74 * access its {@link LocalLibrary}
76 * the type of {@link Story} to load, or NULL for all types
78 public GuiReaderFrame(GuiReader reader
, String type
) {
79 super(String
.format("Fanfix %s Library", Version
.getCurrentVersion()));
83 setDefaultCloseOperation(JFrame
.EXIT_ON_CLOSE
);
85 setLayout(new BorderLayout());
88 pane
.setLayout(new BoxLayout(pane
, BoxLayout
.PAGE_AXIS
));
90 color
= Instance
.getUiConfig().getColor(UiConfig
.BACKGROUND_COLOR
);
93 pane
.setBackground(color
);
96 JScrollPane scroll
= new JScrollPane(pane
);
97 scroll
.getVerticalScrollBar().setUnitIncrement(16);
98 add(scroll
, BorderLayout
.CENTER
);
100 pgBar
= new ProgressBar();
101 add(pgBar
, BorderLayout
.SOUTH
);
103 pgBar
.addActionListener(new ActionListener() {
105 public void actionPerformed(ActionEvent e
) {
107 pgBar
.setProgress(null);
113 pgBar
.addUpdateListener(new ActionListener() {
115 public void actionPerformed(ActionEvent e
) {
122 booksByType
= new HashMap
<GuiReaderGroup
, String
>();
123 booksByAuthor
= new HashMap
<GuiReaderGroup
, String
>();
125 pane
.setVisible(false);
126 final Progress pg
= new Progress();
127 final String typeF
= type
;
128 outOfUi(pg
, new Runnable() {
131 GuiReaderFrame
.this.reader
.getLibrary().refresh(false, pg
);
133 setJMenuBar(createMenu());
134 addBookPane(typeF
, true);
137 pane
.setVisible(true);
145 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
146 * selected type or author.
149 * the author or the type, or NULL to get all the
152 * TRUE for type, FALSE for author
154 private void addBookPane(String value
, boolean type
) {
157 for (String tt
: reader
.getLibrary().getSources()) {
159 addBookPane(tt
, type
);
163 for (String tt
: reader
.getLibrary().getAuthors()) {
165 addBookPane(tt
, type
);
173 GuiReaderGroup bookPane
= new GuiReaderGroup(reader
, value
, color
);
175 booksByType
.put(bookPane
, value
);
177 booksByAuthor
.put(bookPane
, value
);
186 bookPane
.setActionListener(new BookActionListener() {
188 public void select(GuiReaderBook book
) {
193 public void popupRequested(GuiReaderBook book
, MouseEvent e
) {
194 JPopupMenu popup
= new JPopupMenu();
195 popup
.add(createMenuItemOpenBook());
196 popup
.addSeparator();
197 popup
.add(createMenuItemExport());
198 popup
.add(createMenuItemMove());
199 popup
.add(createMenuItemClearCache());
200 popup
.add(createMenuItemRedownload());
201 popup
.addSeparator();
202 popup
.add(createMenuItemDelete());
203 popup
.show(e
.getComponent(), e
.getX(), e
.getY());
207 public void action(final GuiReaderBook book
) {
213 private void removeBookPanes() {
215 booksByAuthor
.clear();
224 * Refresh the list of {@link GuiReaderBook}s from disk.
227 private void refreshBooks() {
228 for (GuiReaderGroup group
: booksByType
.keySet()) {
229 List
<MetaData
> stories
= reader
.getLibrary().getListBySource(
230 booksByType
.get(group
));
231 group
.refreshBooks(stories
, words
);
234 for (GuiReaderGroup group
: booksByAuthor
.keySet()) {
235 List
<MetaData
> stories
= reader
.getLibrary().getListByAuthor(
236 booksByAuthor
.get(group
));
237 group
.refreshBooks(stories
, words
);
245 * Create the main menu bar.
249 private JMenuBar
createMenu() {
250 bar
= new JMenuBar();
252 JMenu file
= new JMenu("File");
253 file
.setMnemonic(KeyEvent
.VK_F
);
255 JMenuItem imprt
= new JMenuItem("Import URL...", KeyEvent
.VK_U
);
256 imprt
.addActionListener(new ActionListener() {
258 public void actionPerformed(ActionEvent e
) {
262 JMenuItem imprtF
= new JMenuItem("Import File...", KeyEvent
.VK_F
);
263 imprtF
.addActionListener(new ActionListener() {
265 public void actionPerformed(ActionEvent e
) {
269 JMenuItem exit
= new JMenuItem("Exit", KeyEvent
.VK_X
);
270 exit
.addActionListener(new ActionListener() {
272 public void actionPerformed(ActionEvent e
) {
273 GuiReaderFrame
.this.dispatchEvent(new WindowEvent(
274 GuiReaderFrame
.this, WindowEvent
.WINDOW_CLOSING
));
278 file
.add(createMenuItemOpenBook());
279 file
.add(createMenuItemExport());
280 file
.add(createMenuItemMove());
289 JMenu edit
= new JMenu("Edit");
290 edit
.setMnemonic(KeyEvent
.VK_E
);
292 edit
.add(createMenuItemClearCache());
293 edit
.add(createMenuItemRedownload());
295 edit
.add(createMenuItemDelete());
299 JMenu view
= new JMenu("View");
300 view
.setMnemonic(KeyEvent
.VK_V
);
301 JMenuItem vauthors
= new JMenuItem("Author");
302 vauthors
.setMnemonic(KeyEvent
.VK_A
);
303 vauthors
.addActionListener(new ActionListener() {
305 public void actionPerformed(ActionEvent e
) {
311 JMenuItem vwords
= new JMenuItem("Word count");
312 vwords
.setMnemonic(KeyEvent
.VK_W
);
313 vwords
.addActionListener(new ActionListener() {
315 public void actionPerformed(ActionEvent e
) {
323 JMenu sources
= new JMenu("Sources");
324 sources
.setMnemonic(KeyEvent
.VK_S
);
326 List
<String
> tt
= reader
.getLibrary().getSources();
328 for (final String type
: tt
) {
329 JMenuItem item
= new JMenuItem(type
== null ?
"All" : type
);
330 item
.addActionListener(new ActionListener() {
332 public void actionPerformed(ActionEvent e
) {
334 addBookPane(type
, true);
341 sources
.addSeparator();
347 JMenu authors
= new JMenu("Authors");
348 authors
.setMnemonic(KeyEvent
.VK_A
);
350 List
<String
> aa
= reader
.getLibrary().getAuthors();
352 for (final String author
: aa
) {
353 JMenuItem item
= new JMenuItem(author
== null ?
"All"
354 : author
.isEmpty() ?
"[unknown]" : author
);
355 item
.addActionListener(new ActionListener() {
357 public void actionPerformed(ActionEvent e
) {
359 addBookPane(author
, false);
365 if (author
== null || author
.isEmpty()) {
366 authors
.addSeparator();
372 JMenu options
= new JMenu("Options");
373 options
.setMnemonic(KeyEvent
.VK_O
);
374 options
.add(createMenuItemConfig());
375 options
.add(createMenuItemUiConfig());
382 * Create the Fanfix Configuration menu item.
386 private JMenuItem
createMenuItemConfig() {
387 final String title
= "Fanfix Configuration";
388 JMenuItem item
= new JMenuItem(title
);
389 item
.setMnemonic(KeyEvent
.VK_F
);
391 item
.addActionListener(new ActionListener() {
393 public void actionPerformed(ActionEvent e
) {
394 ConfigEditor
<Config
> ed
= new ConfigEditor
<Config
>(
395 Config
.class, Instance
.getConfig(),
396 "This is where you configure the options of the program.");
397 JFrame frame
= new JFrame(title
);
399 frame
.setSize(800, 600);
400 frame
.setVisible(true);
408 * Create the UI Configuration menu item.
412 private JMenuItem
createMenuItemUiConfig() {
413 final String title
= "UI Configuration";
414 JMenuItem item
= new JMenuItem(title
);
415 item
.setMnemonic(KeyEvent
.VK_U
);
417 item
.addActionListener(new ActionListener() {
419 public void actionPerformed(ActionEvent e
) {
420 ConfigEditor
<UiConfig
> ed
= new ConfigEditor
<UiConfig
>(
421 UiConfig
.class, Instance
.getUiConfig(),
422 "This is where you configure the graphical appearence of the program.");
423 JFrame frame
= new JFrame(title
);
425 frame
.setSize(800, 600);
426 frame
.setVisible(true);
434 * Create the export menu item.
438 private JMenuItem
createMenuItemExport() {
439 final JFileChooser fc
= new JFileChooser();
440 fc
.setAcceptAllFileFilterUsed(false);
442 final Map
<FileFilter
, OutputType
> filters
= new HashMap
<FileFilter
, OutputType
>();
443 for (OutputType type
: OutputType
.values()) {
444 String ext
= type
.getDefaultExtension(false);
445 String desc
= type
.getDesc(false);
447 if (ext
== null || ext
.isEmpty()) {
448 filters
.put(createAllFilter(desc
), type
);
450 filters
.put(new FileNameExtensionFilter(desc
, ext
), type
);
454 // First the "ALL" filters, then, the extension filters
455 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
456 if (!(entry
.getKey() instanceof FileNameExtensionFilter
)) {
457 fc
.addChoosableFileFilter(entry
.getKey());
460 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
461 if (entry
.getKey() instanceof FileNameExtensionFilter
) {
462 fc
.addChoosableFileFilter(entry
.getKey());
467 JMenuItem export
= new JMenuItem("Save as...", KeyEvent
.VK_S
);
468 export
.addActionListener(new ActionListener() {
470 public void actionPerformed(ActionEvent e
) {
471 if (selectedBook
!= null) {
472 fc
.showDialog(GuiReaderFrame
.this, "Save");
473 if (fc
.getSelectedFile() != null) {
474 final OutputType type
= filters
.get(fc
.getFileFilter());
475 final String path
= fc
.getSelectedFile()
477 + type
.getDefaultExtension(false);
478 final Progress pg
= new Progress();
479 outOfUi(pg
, new Runnable() {
483 reader
.getLibrary().export(
484 selectedBook
.getMeta().getLuid(),
486 } catch (IOException e
) {
500 * Create a {@link FileFilter} that accepts all files and return the given
508 private FileFilter
createAllFilter(final String desc
) {
509 return new FileFilter() {
511 public String
getDescription() {
516 public boolean accept(File f
) {
523 * Create the refresh (delete cache) menu item.
527 private JMenuItem
createMenuItemClearCache() {
528 JMenuItem refresh
= new JMenuItem("Clear cache", KeyEvent
.VK_C
);
529 refresh
.addActionListener(new ActionListener() {
531 public void actionPerformed(ActionEvent e
) {
532 if (selectedBook
!= null) {
533 outOfUi(null, new Runnable() {
536 reader
.clearLocalReaderCache(selectedBook
.getMeta()
538 selectedBook
.setCached(false);
539 SwingUtilities
.invokeLater(new Runnable() {
542 selectedBook
.repaint();
555 * Create the delete menu item.
559 private JMenuItem
createMenuItemMove() {
560 JMenu moveTo
= new JMenu("Move to...");
561 moveTo
.setMnemonic(KeyEvent
.VK_M
);
563 List
<String
> types
= new ArrayList
<String
>();
565 types
.addAll(reader
.getLibrary().getSources());
567 for (String type
: types
) {
568 JMenuItem item
= new JMenuItem(type
== null ?
"New type..." : type
);
572 moveTo
.addSeparator();
575 final String ftype
= type
;
576 item
.addActionListener(new ActionListener() {
578 public void actionPerformed(ActionEvent e
) {
579 if (selectedBook
!= null) {
582 Object rep
= JOptionPane
.showInputDialog(
583 GuiReaderFrame
.this, "Move to:",
585 JOptionPane
.QUESTION_MESSAGE
, null, null,
586 selectedBook
.getMeta().getSource());
592 type
= rep
.toString();
595 final String ftype
= type
;
596 outOfUi(null, new Runnable() {
599 reader
.changeType(selectedBook
.getMeta()
604 SwingUtilities
.invokeLater(new Runnable() {
607 setJMenuBar(createMenu());
621 * Create the redownload (then delete original) menu item.
625 private JMenuItem
createMenuItemRedownload() {
626 JMenuItem refresh
= new JMenuItem("Redownload", KeyEvent
.VK_R
);
627 refresh
.addActionListener(new ActionListener() {
629 public void actionPerformed(ActionEvent e
) {
630 if (selectedBook
!= null) {
631 final MetaData meta
= selectedBook
.getMeta();
632 imprt(meta
.getUrl(), new Runnable() {
635 reader
.delete(meta
.getLuid());
636 GuiReaderFrame
.this.selectedBook
= null;
638 }, "Removing old copy");
647 * Create the delete menu item.
651 private JMenuItem
createMenuItemDelete() {
652 JMenuItem delete
= new JMenuItem("Delete", KeyEvent
.VK_D
);
653 delete
.addActionListener(new ActionListener() {
655 public void actionPerformed(ActionEvent e
) {
656 if (selectedBook
!= null) {
657 outOfUi(null, new Runnable() {
660 reader
.delete(selectedBook
.getMeta().getLuid());
672 * Create the open menu item.
676 private JMenuItem
createMenuItemOpenBook() {
677 JMenuItem open
= new JMenuItem("Open", KeyEvent
.VK_O
);
678 open
.addActionListener(new ActionListener() {
680 public void actionPerformed(ActionEvent e
) {
681 if (selectedBook
!= null) {
682 openBook(selectedBook
);
691 * Open a {@link GuiReaderBook} item.
694 * the {@link GuiReaderBook} to open
696 private void openBook(final GuiReaderBook book
) {
697 final Progress pg
= new Progress();
698 outOfUi(pg
, new Runnable() {
702 reader
.read(book
.getMeta().getLuid(), pg
);
703 SwingUtilities
.invokeLater(new Runnable() {
706 book
.setCached(true);
709 } catch (IOException e
) {
710 // TODO: error message?
718 * Process the given action out of the Swing UI thread and link the given
719 * {@link ProgressBar} to the action.
721 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
722 * to done when the action is done.
725 * the {@link ProgressBar} or NULL
729 private void outOfUi(Progress progress
, final Runnable run
) {
730 final Progress pg
= new Progress();
731 final Progress reload
= new Progress("Reload books");
732 if (progress
== null) {
733 progress
= new Progress();
736 pg
.addProgress(progress
, 90);
737 pg
.addProgress(reload
, 10);
740 pgBar
.setProgress(pg
);
744 new Thread(new Runnable() {
751 // will trigger pgBar ActionListener:
755 }, "outOfUi thread").start();
759 * Import a {@link Story} into the main {@link LocalLibrary}.
761 * Should be called inside the UI thread.
764 * TRUE for an {@link URL}, false for a {@link File}
766 private void imprt(boolean askUrl
) {
767 JFileChooser fc
= new JFileChooser();
771 String clipboard
= "";
773 clipboard
= ("" + Toolkit
.getDefaultToolkit()
774 .getSystemClipboard().getData(DataFlavor
.stringFlavor
))
776 } catch (Exception e
) {
777 // No data will be handled
780 if (clipboard
== null || !clipboard
.startsWith("http")) {
784 url
= JOptionPane
.showInputDialog(GuiReaderFrame
.this,
785 "url of the story to import?", "Importing from URL",
786 JOptionPane
.QUESTION_MESSAGE
, null, null, clipboard
);
787 } else if (fc
.showOpenDialog(this) != JFileChooser
.CANCEL_OPTION
) {
788 url
= fc
.getSelectedFile().getAbsolutePath();
793 if (url
!= null && !url
.toString().isEmpty()) {
794 imprt(url
.toString(), null, null);
799 * Actually import the {@link Story} into the main {@link LocalLibrary}.
801 * Should be called inside the UI thread.
804 * the {@link Story} to import by {@link URL}
806 * Action to execute on success
808 private void imprt(final String url
, final Runnable onSuccess
,
809 String onSuccessPgName
) {
810 final Progress pg
= new Progress();
811 final Progress pgImprt
= new Progress();
812 final Progress pgOnSuccess
= new Progress(onSuccessPgName
);
813 pg
.addProgress(pgImprt
, 95);
814 pg
.addProgress(pgOnSuccess
, 5);
816 outOfUi(pg
, new Runnable() {
821 reader
.getLibrary().imprt(BasicReader
.getUrl(url
), pgImprt
);
822 } catch (IOException e
) {
826 final Exception e
= ex
;
828 final boolean ok
= (e
== null);
830 pgOnSuccess
.setProgress(0);
833 SwingUtilities
.invokeLater(new Runnable() {
836 JOptionPane
.showMessageDialog(GuiReaderFrame
.this,
837 "Cannot import: " + url
, e
.getMessage(),
838 JOptionPane
.ERROR_MESSAGE
);
842 if (onSuccess
!= null) {
852 * Enables or disables this component, depending on the value of the
853 * parameter <code>b</code>. An enabled component can respond to user input
854 * and generate events. Components are enabled initially by default.
856 * Disabling this component will also affect its children.
859 * If <code>true</code>, this component is enabled; otherwise
860 * this component is disabled
863 public void setEnabled(boolean b
) {
868 for (GuiReaderGroup group
: booksByType
.keySet()) {
871 for (GuiReaderGroup group
: booksByAuthor
.keySet()) {