1 package be
.nikiroo
.fanfix
.reader
.ui
;
3 import java
.awt
.BorderLayout
;
5 import java
.awt
.Component
;
6 import java
.awt
.EventQueue
;
8 import java
.awt
.Toolkit
;
9 import java
.awt
.datatransfer
.DataFlavor
;
10 import java
.awt
.event
.ActionEvent
;
11 import java
.awt
.event
.ActionListener
;
12 import java
.awt
.event
.FocusAdapter
;
13 import java
.awt
.event
.FocusEvent
;
15 import java
.io
.IOException
;
16 import java
.lang
.reflect
.InvocationTargetException
;
18 import java
.net
.UnknownHostException
;
19 import java
.util
.ArrayList
;
20 import java
.util
.LinkedList
;
21 import java
.util
.List
;
23 import java
.util
.TreeMap
;
25 import javax
.swing
.BoxLayout
;
26 import javax
.swing
.JFileChooser
;
27 import javax
.swing
.JLabel
;
28 import javax
.swing
.JMenuBar
;
29 import javax
.swing
.JOptionPane
;
30 import javax
.swing
.JPanel
;
31 import javax
.swing
.JPopupMenu
;
32 import javax
.swing
.JScrollPane
;
33 import javax
.swing
.SwingConstants
;
34 import javax
.swing
.SwingUtilities
;
36 import be
.nikiroo
.fanfix
.Instance
;
37 import be
.nikiroo
.fanfix
.bundles
.StringIdGui
;
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
.BasicLibrary
;
42 import be
.nikiroo
.fanfix
.library
.BasicLibrary
.Status
;
43 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
44 import be
.nikiroo
.fanfix
.reader
.BasicReader
;
45 import be
.nikiroo
.fanfix
.reader
.ui
.GuiReaderBook
.BookActionListener
;
46 import be
.nikiroo
.fanfix
.reader
.ui
.GuiReaderBookInfo
.Type
;
47 import be
.nikiroo
.utils
.Progress
;
48 import be
.nikiroo
.utils
.ui
.ProgressBar
;
51 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
52 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
53 * way to copy them to the {@link GuiReader} cache (
54 * {@link BasicReader#getLibrary()}), read them, delete them...
58 class GuiReaderMainPanel
extends JPanel
{
59 private static final long serialVersionUID
= 1L;
60 private FrameHelper helper
;
61 private Map
<String
, GuiReaderGroup
> books
;
62 private GuiReaderGroup bookPane
; // for more "All"
65 private ProgressBar pgBar
;
67 private GuiReaderBook selectedBook
;
68 private boolean words
; // words or authors (secondary info on books)
69 private boolean currentType
; // type/source or author mode (All and Listing)
72 * An object that offers some helper methods to access the frame that host
73 * it and the Fanfix-related functions.
77 public interface FrameHelper
{
79 * Return the reader associated to this {@link FrameHelper}.
83 public GuiReader
getReader();
86 * Create the main menu bar.
88 * Will invalidate the layout.
91 * the library status, <b>must not</b> be NULL
93 public void createMenu(Status status
);
96 * Create a popup menu for a {@link GuiReaderBook} that represents a
99 * @return the popup menu to display
101 public JPopupMenu
createBookPopup();
104 * Create a popup menu for a {@link GuiReaderBook} that represents a
105 * source/type or an author.
107 * @return the popup menu to display
109 public JPopupMenu
createSourceAuthorPopup();
113 * A {@link Runnable} with a {@link MetaData} parameter.
117 public interface MetaDataRunnable
{
122 * the meta of the story
124 public void run(MetaData meta
);
128 * Create a new {@link GuiReaderMainPanel}.
131 * the associated {@link FrameHelper} to forward some commands
132 * and access its {@link LocalLibrary}
134 * the type of {@link Story} to load, or NULL for all types
136 public GuiReaderMainPanel(FrameHelper parent
, String type
) {
137 super(new BorderLayout(), true);
139 this.helper
= parent
;
142 pane
.setLayout(new BoxLayout(pane
, BoxLayout
.PAGE_AXIS
));
143 JScrollPane scroll
= new JScrollPane(pane
);
145 Integer icolor
= Instance
.getInstance().getUiConfig().getColor(UiConfig
.BACKGROUND_COLOR
);
146 if (icolor
!= null) {
147 color
= new Color(icolor
);
148 setBackground(color
);
149 pane
.setBackground(color
);
150 scroll
.setBackground(color
);
153 scroll
.getVerticalScrollBar().setUnitIncrement(16);
154 add(scroll
, BorderLayout
.CENTER
);
156 String message
= parent
.getReader().getLibrary().getLibraryName();
157 if (!message
.isEmpty()) {
158 JLabel name
= new JLabel(message
, SwingConstants
.CENTER
);
159 add(name
, BorderLayout
.NORTH
);
162 pgBar
= new ProgressBar();
163 add(pgBar
, BorderLayout
.SOUTH
);
165 pgBar
.addActionListener(new ActionListener() {
167 public void actionPerformed(ActionEvent e
) {
169 pgBar
.setProgress(null);
175 pgBar
.addUpdateListener(new ActionListener() {
177 public void actionPerformed(ActionEvent e
) {
184 books
= new TreeMap
<String
, GuiReaderGroup
>();
186 addFocusListener(new FocusAdapter() {
188 public void focusGained(FocusEvent e
) {
193 pane
.setVisible(false);
194 final Progress pg
= new Progress();
195 final String typeF
= type
;
196 outOfUi(pg
, true, new Runnable() {
199 final BasicLibrary lib
= helper
.getReader().getLibrary();
200 final Status status
= lib
.getStatus();
202 if (status
== Status
.READ_WRITE
) {
206 inUi(new Runnable() {
209 if (status
.isReady()) {
210 helper
.createMenu(status
);
211 pane
.setVisible(true);
214 addBookPane(true, false);
215 } catch (IOException e
) {
216 error(e
.getLocalizedMessage(),
220 addBookPane(typeF
, true);
223 helper
.createMenu(status
);
226 String desc
= Instance
.getInstance().getTransGui().getStringX(StringIdGui
.ERROR_LIB_STATUS
,
230 .trans(StringIdGui
.ERROR_LIB_STATUS
);
233 String err
= lib
.getLibraryName() + "\n" + desc
;
235 .trans(StringIdGui
.TITLE_ERROR_LIBRARY
),
244 public boolean getCurrentType() {
249 * Add a new {@link GuiReaderGroup} on the frame to display all the
250 * sources/types or all the authors, or a listing of all the books sorted
251 * either by source or author.
253 * A display of all the sources/types or all the authors will show one icon
254 * per source/type or author.
256 * A listing of all the books sorted by source/type or author will display
260 * TRUE for type/source, FALSE for author
262 * TRUE to get a listing of all the sources or authors, FALSE to
263 * get one icon per source or author
265 * @throws IOException
266 * in case of I/O error
268 public void addBookPane(boolean type
, boolean listMode
) throws IOException
{
269 this.currentType
= type
;
270 BasicLibrary lib
= helper
.getReader().getLibrary();
273 addListPane(GuiReader
.trans(StringIdGui
.MENU_SOURCES
),
274 lib
.getSources(), type
);
276 for (String tt
: lib
.getSources()) {
278 addBookPane(tt
, type
);
284 addListPane(GuiReader
.trans(StringIdGui
.MENU_AUTHORS
),
285 lib
.getAuthors(), type
);
287 for (String tt
: lib
.getAuthors()) {
289 addBookPane(tt
, type
);
297 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
298 * selected type or author.
300 * Will invalidate the layout.
303 * the author or the type, or NULL to get all the
306 * TRUE for type/source, FALSE for author
308 public void addBookPane(String value
, boolean type
) {
309 this.currentType
= type
;
311 GuiReaderGroup bookPane
= new GuiReaderGroup(helper
.getReader(), value
,
314 books
.put(value
, bookPane
);
319 bookPane
.setActionListener(new BookActionListener() {
321 public void select(GuiReaderBook book
) {
326 public void popupRequested(GuiReaderBook book
, Component target
,
328 JPopupMenu popup
= helper
.createBookPopup();
329 popup
.show(target
, x
, y
);
333 public void action(final GuiReaderBook book
) {
342 * Clear the pane from any book that may be present, usually prior to adding
345 * Will invalidate the layout.
347 public void removeBookPanes() {
354 * Refresh the list of {@link GuiReaderBook}s from disk.
356 * Will validate the layout, as it is a "refresh" operation.
358 public void refreshBooks() {
359 BasicLibrary lib
= helper
.getReader().getLibrary();
360 for (String value
: books
.keySet()) {
361 List
<GuiReaderBookInfo
> infos
= new ArrayList
<GuiReaderBookInfo
>();
363 List
<MetaData
> metas
;
366 metas
= lib
.getList().filter(value
, null, null);
368 metas
= lib
.getList().filter(null, value
, null);
370 } catch (IOException e
) {
371 error(e
.getLocalizedMessage(), "IOException", e
);
372 metas
= new ArrayList
<MetaData
>();
375 for (MetaData meta
: metas
) {
376 infos
.add(GuiReaderBookInfo
.fromMeta(meta
));
379 books
.get(value
).refreshBooks(infos
, words
);
382 if (bookPane
!= null) {
383 bookPane
.refreshBooks(words
);
390 * Open a {@link GuiReaderBook} item.
393 * the {@link GuiReaderBook} to open
395 public void openBook(final GuiReaderBook book
) {
396 final Progress pg
= new Progress();
397 outOfUi(pg
, false, new Runnable() {
401 helper
.getReader().read(book
.getInfo().getMeta().getLuid(),
403 SwingUtilities
.invokeLater(new Runnable() {
406 book
.setCached(true);
409 } catch (IOException e
) {
410 Instance
.getInstance().getTraceHandler().error(e
);
411 error(GuiReader
.trans(StringIdGui
.ERROR_CANNOT_OPEN
),
412 GuiReader
.trans(StringIdGui
.TITLE_ERROR
), e
);
419 * Prefetch a {@link GuiReaderBook} item (which can be a group, in which
420 * case we prefetch all its members).
423 * the {@link GuiReaderBook} to open
425 public void prefetchBook(final GuiReaderBook book
) {
426 final List
<String
> luids
= new LinkedList
<String
>();
428 switch (book
.getInfo().getType()) {
430 luids
.add(book
.getInfo().getMeta().getLuid());
433 for (MetaData meta
: helper
.getReader().getLibrary()
434 .getList().filter(book
.getInfo().getMainInfo(), null, null)) {
435 luids
.add(meta
.getLuid());
439 for (MetaData meta
: helper
.getReader().getLibrary()
440 .getList().filter(null, book
.getInfo().getMainInfo(), null)) {
441 luids
.add(meta
.getLuid());
445 } catch (IOException e
) {
446 Instance
.getInstance().getTraceHandler().error(e
);
449 final Progress pg
= new Progress();
450 pg
.setMax(luids
.size());
452 outOfUi(pg
, false, new Runnable() {
456 for (String luid
: luids
) {
457 Progress pgStep
= new Progress();
458 pg
.addProgress(pgStep
, 1);
460 helper
.getReader().prefetch(luid
, pgStep
);
463 // TODO: also set the green button on sources/authors?
464 // requires to do the same when all stories inside are green
465 if (book
.getInfo().getType() == Type
.STORY
) {
466 SwingUtilities
.invokeLater(new Runnable() {
469 book
.setCached(true);
473 } catch (IOException e
) {
474 Instance
.getInstance().getTraceHandler().error(e
);
475 error(GuiReader
.trans(StringIdGui
.ERROR_CANNOT_OPEN
),
476 GuiReader
.trans(StringIdGui
.TITLE_ERROR
), e
);
483 * Process the given action out of the Swing UI thread and link the given
484 * {@link ProgressBar} to the action.
486 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
487 * to done when the action is done.
490 * the {@link ProgressBar} or NULL
491 * @param refreshBooks
492 * TRUE to refresh the books after
496 public void outOfUi(Progress progress
, final boolean refreshBooks
,
497 final Runnable run
) {
498 final Progress pg
= new Progress();
499 final Progress reload
= new Progress(
500 GuiReader
.trans(StringIdGui
.PROGRESS_OUT_OF_UI_RELOAD_BOOKS
));
502 if (progress
== null) {
503 progress
= new Progress();
507 pg
.addProgress(progress
, 100);
509 pg
.addProgress(progress
, 90);
510 pg
.addProgress(reload
, 10);
514 pgBar
.setProgress(pg
);
518 new Thread(new Runnable() {
529 // will trigger pgBar ActionListener:
534 }, "outOfUi thread").start();
538 * Process the given action in the main Swing UI thread.
540 * The code will make sure the current thread is the main UI thread and, if
541 * not, will switch to it before executing the runnable.
543 * Synchronous operation.
548 public void inUi(final Runnable run
) {
549 if (EventQueue
.isDispatchThread()) {
553 EventQueue
.invokeAndWait(run
);
554 } catch (InterruptedException e
) {
555 Instance
.getInstance().getTraceHandler().error(e
);
556 } catch (InvocationTargetException e
) {
557 Instance
.getInstance().getTraceHandler().error(e
);
563 * Import a {@link Story} into the main {@link LocalLibrary}.
565 * Should be called inside the UI thread.
568 * TRUE for an {@link URL}, false for a {@link File}
570 public void imprt(boolean askUrl
) {
571 JFileChooser fc
= new JFileChooser();
575 String clipboard
= "";
577 clipboard
= ("" + Toolkit
.getDefaultToolkit()
578 .getSystemClipboard().getData(DataFlavor
.stringFlavor
))
580 } catch (Exception e
) {
581 // No data will be handled
584 if (clipboard
== null || !(clipboard
.startsWith("http://") || //
585 clipboard
.startsWith("https://"))) {
589 url
= JOptionPane
.showInputDialog(GuiReaderMainPanel
.this,
590 GuiReader
.trans(StringIdGui
.SUBTITLE_IMPORT_URL
),
591 GuiReader
.trans(StringIdGui
.TITLE_IMPORT_URL
),
592 JOptionPane
.QUESTION_MESSAGE
, null, null, clipboard
);
593 } else if (fc
.showOpenDialog(this) != JFileChooser
.CANCEL_OPTION
) {
594 url
= fc
.getSelectedFile().getAbsolutePath();
599 if (url
!= null && !url
.toString().isEmpty()) {
600 imprt(url
.toString(), null, null);
605 * Actually import the {@link Story} into the main {@link LocalLibrary}.
607 * Should be called inside the UI thread.
610 * the {@link Story} to import by {@link URL}
612 * Action to execute on success
613 * @param onSuccessPgName
614 * the name to use for the onSuccess progress bar
616 public void imprt(final String url
, final MetaDataRunnable onSuccess
,
617 String onSuccessPgName
) {
618 final Progress pg
= new Progress();
619 final Progress pgImprt
= new Progress();
620 final Progress pgOnSuccess
= new Progress(onSuccessPgName
);
621 pg
.addProgress(pgImprt
, 95);
622 pg
.addProgress(pgOnSuccess
, 5);
624 outOfUi(pg
, true, new Runnable() {
628 MetaData meta
= null;
630 meta
= helper
.getReader().getLibrary()
631 .imprt(BasicReader
.getUrl(url
), pgImprt
);
632 } catch (IOException e
) {
636 final Exception e
= ex
;
638 final boolean ok
= (e
== null);
640 pgOnSuccess
.setProgress(0);
642 if (e
instanceof UnknownHostException
) {
643 error(GuiReader
.trans(
644 StringIdGui
.ERROR_URL_NOT_SUPPORTED
, url
),
645 GuiReader
.trans(StringIdGui
.TITLE_ERROR
), null);
647 error(GuiReader
.trans(
648 StringIdGui
.ERROR_URL_IMPORT_FAILED
, url
,
649 e
.getMessage()), GuiReader
650 .trans(StringIdGui
.TITLE_ERROR
), e
);
653 if (onSuccess
!= null) {
663 * Enables or disables this component, depending on the value of the
664 * parameter <code>b</code>. An enabled component can respond to user input
665 * and generate events. Components are enabled initially by default.
667 * Enabling or disabling <b>this</b> component will also affect its
671 * If <code>true</code>, this component is enabled; otherwise
672 * this component is disabled
675 public void setEnabled(boolean b
) {
680 for (GuiReaderGroup group
: books
.values()) {
687 public void setWords(boolean words
) {
691 public GuiReaderBook
getSelectedBook() {
695 public void unsetSelectedBook() {
699 private void addListPane(String name
, List
<String
> values
,
700 final boolean type
) {
701 GuiReader reader
= helper
.getReader();
702 BasicLibrary lib
= reader
.getLibrary();
704 bookPane
= new GuiReaderGroup(reader
, name
, color
);
706 List
<GuiReaderBookInfo
> infos
= new ArrayList
<GuiReaderBookInfo
>();
707 for (String value
: values
) {
709 infos
.add(GuiReaderBookInfo
.fromSource(lib
, value
));
711 infos
.add(GuiReaderBookInfo
.fromAuthor(lib
, value
));
715 bookPane
.refreshBooks(infos
, words
);
723 bookPane
.setActionListener(new BookActionListener() {
725 public void select(GuiReaderBook book
) {
730 public void popupRequested(GuiReaderBook book
, Component target
,
732 JPopupMenu popup
= helper
.createSourceAuthorPopup();
733 popup
.show(target
, x
, y
);
737 public void action(final GuiReaderBook book
) {
739 addBookPane(book
.getInfo().getMainInfo(), type
);
748 * Focus the first {@link GuiReaderGroup} we find.
750 private void focus() {
751 GuiReaderGroup group
= null;
752 Map
<String
, GuiReaderGroup
> books
= this.books
;
753 if (books
.size() > 0) {
754 group
= books
.values().iterator().next();
762 group
.requestFocusInWindow();
767 * Display an error message and log the linked {@link Exception}.
772 * the title of the error message
774 * the exception to log if any
776 private void error(final String message
, final String title
, Exception e
) {
777 Instance
.getInstance().getTraceHandler().error(title
+ ": " + message
);
779 Instance
.getInstance().getTraceHandler().error(e
);
782 SwingUtilities
.invokeLater(new Runnable() {
785 JOptionPane
.showMessageDialog(GuiReaderMainPanel
.this, message
,
786 title
, JOptionPane
.ERROR_MESSAGE
);