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
.List
;
22 import java
.util
.TreeMap
;
24 import javax
.swing
.BoxLayout
;
25 import javax
.swing
.JFileChooser
;
26 import javax
.swing
.JLabel
;
27 import javax
.swing
.JMenuBar
;
28 import javax
.swing
.JOptionPane
;
29 import javax
.swing
.JPanel
;
30 import javax
.swing
.JPopupMenu
;
31 import javax
.swing
.JScrollPane
;
32 import javax
.swing
.SwingConstants
;
33 import javax
.swing
.SwingUtilities
;
35 import be
.nikiroo
.fanfix
.Instance
;
36 import be
.nikiroo
.fanfix
.bundles
.StringIdGui
;
37 import be
.nikiroo
.fanfix
.bundles
.UiConfig
;
38 import be
.nikiroo
.fanfix
.data
.MetaData
;
39 import be
.nikiroo
.fanfix
.data
.Story
;
40 import be
.nikiroo
.fanfix
.library
.BasicLibrary
;
41 import be
.nikiroo
.fanfix
.library
.BasicLibrary
.Status
;
42 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
43 import be
.nikiroo
.fanfix
.reader
.BasicReader
;
44 import be
.nikiroo
.fanfix
.reader
.ui
.GuiReaderBook
.BookActionListener
;
45 import be
.nikiroo
.utils
.Progress
;
46 import be
.nikiroo
.utils
.ui
.ProgressBar
;
49 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
50 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
51 * way to copy them to the {@link GuiReader} cache (
52 * {@link BasicReader#getLibrary()}), read them, delete them...
56 class GuiReaderMainPanel
extends JPanel
{
57 private static final long serialVersionUID
= 1L;
58 private FrameHelper helper
;
59 private Map
<String
, GuiReaderGroup
> books
;
60 private GuiReaderGroup bookPane
; // for more "All"
63 private ProgressBar pgBar
;
65 private GuiReaderBook selectedBook
;
66 private boolean words
; // words or authors (secondary info on books)
67 private boolean currentType
; // type/source or author mode (All and Listing)
70 * An object that offers some helper methods to access the frame that host
71 * it and the Fanfix-related functions.
75 public interface FrameHelper
{
77 * Return the reader associated to this {@link FrameHelper}.
81 public GuiReader
getReader();
84 * Create the main menu bar.
86 * Will invalidate the layout.
89 * the library status, <b>must not</b> be NULL
91 public void createMenu(Status status
);
94 * Create a popup menu for a {@link GuiReaderBook} that represents a
97 * @return the popup menu to display
99 public JPopupMenu
createBookPopup();
102 * Create a popup menu for a {@link GuiReaderBook} that represents a
103 * source/type or an author.
105 * @return the popup menu to display
107 public JPopupMenu
createSourceAuthorPopup();
111 * A {@link Runnable} with a {@link Story} parameter.
115 public interface StoryRunnable
{
122 public void run(Story story
);
126 * Create a new {@link GuiReaderMainPanel}.
129 * the associated {@link FrameHelper} to forward some commands
130 * and access its {@link LocalLibrary}
132 * the type of {@link Story} to load, or NULL for all types
134 public GuiReaderMainPanel(FrameHelper parent
, String type
) {
135 super(new BorderLayout(), true);
137 this.helper
= parent
;
140 pane
.setLayout(new BoxLayout(pane
, BoxLayout
.PAGE_AXIS
));
142 Integer icolor
= Instance
.getUiConfig().getColor(
143 UiConfig
.BACKGROUND_COLOR
);
144 if (icolor
!= null) {
145 color
= new Color(icolor
);
146 setBackground(color
);
147 pane
.setBackground(color
);
150 JScrollPane scroll
= new JScrollPane(pane
);
151 scroll
.getVerticalScrollBar().setUnitIncrement(16);
152 add(scroll
, BorderLayout
.CENTER
);
154 String message
= parent
.getReader().getLibrary().getLibraryName();
155 if (!message
.isEmpty()) {
156 JLabel name
= new JLabel(message
, SwingConstants
.CENTER
);
157 add(name
, BorderLayout
.NORTH
);
160 pgBar
= new ProgressBar();
161 add(pgBar
, BorderLayout
.SOUTH
);
163 pgBar
.addActionListener(new ActionListener() {
165 public void actionPerformed(ActionEvent e
) {
167 pgBar
.setProgress(null);
173 pgBar
.addUpdateListener(new ActionListener() {
175 public void actionPerformed(ActionEvent e
) {
182 books
= new TreeMap
<String
, GuiReaderGroup
>();
184 addFocusListener(new FocusAdapter() {
186 public void focusGained(FocusEvent e
) {
191 pane
.setVisible(false);
192 final Progress pg
= new Progress();
193 final String typeF
= type
;
194 outOfUi(pg
, true, new Runnable() {
197 final BasicLibrary lib
= helper
.getReader().getLibrary();
198 final Status status
= lib
.getStatus();
200 if (status
== Status
.READ_WRITE
) {
204 inUi(new Runnable() {
207 if (status
.isReady()) {
208 helper
.createMenu(status
);
209 pane
.setVisible(true);
212 addBookPane(true, false);
213 } catch (IOException e
) {
214 error(e
.getLocalizedMessage(),
218 addBookPane(typeF
, true);
221 helper
.createMenu(status
);
224 String desc
= Instance
.getTransGui().getStringX(
225 StringIdGui
.ERROR_LIB_STATUS
,
229 .trans(StringIdGui
.ERROR_LIB_STATUS
);
232 String err
= lib
.getLibraryName() + "\n" + desc
;
234 .trans(StringIdGui
.TITLE_ERROR_LIBRARY
),
243 public boolean getCurrentType() {
248 * Add a new {@link GuiReaderGroup} on the frame to display all the
249 * sources/types or all the authors, or a listing of all the books sorted
250 * either by source or author.
252 * A display of all the sources/types or all the authors will show one icon
253 * per source/type or author.
255 * A listing of all the books sorted by source/type or author will display
259 * TRUE for type/source, FALSE for author
261 * TRUE to get a listing of all the sources or authors, FALSE to
262 * get one icon per source or author
264 * @throws IOException
265 * in case of I/O error
267 public void addBookPane(boolean type
, boolean listMode
) throws IOException
{
268 this.currentType
= type
;
269 BasicLibrary lib
= helper
.getReader().getLibrary();
272 addListPane(GuiReader
.trans(StringIdGui
.MENU_SOURCES
),
273 lib
.getSources(), type
);
275 for (String tt
: lib
.getSources()) {
277 addBookPane(tt
, type
);
283 addListPane(GuiReader
.trans(StringIdGui
.MENU_AUTHORS
),
284 lib
.getAuthors(), type
);
286 for (String tt
: lib
.getAuthors()) {
288 addBookPane(tt
, type
);
296 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
297 * selected type or author.
299 * Will invalidate the layout.
302 * the author or the type, or NULL to get all the
305 * TRUE for type/source, FALSE for author
307 public void addBookPane(String value
, boolean type
) {
308 this.currentType
= type
;
310 GuiReaderGroup bookPane
= new GuiReaderGroup(helper
.getReader(), value
,
313 books
.put(value
, bookPane
);
318 bookPane
.setActionListener(new BookActionListener() {
320 public void select(GuiReaderBook book
) {
325 public void popupRequested(GuiReaderBook book
, Component target
,
327 JPopupMenu popup
= helper
.createBookPopup();
328 popup
.show(target
, x
, y
);
332 public void action(final GuiReaderBook book
) {
341 * Clear the pane from any book that may be present, usually prior to adding
344 * Will invalidate the layout.
346 public void removeBookPanes() {
353 * Refresh the list of {@link GuiReaderBook}s from disk.
355 * Will validate the layout, as it is a "refresh" operation.
357 public void refreshBooks() {
358 BasicLibrary lib
= helper
.getReader().getLibrary();
359 for (String value
: books
.keySet()) {
360 List
<GuiReaderBookInfo
> infos
= new ArrayList
<GuiReaderBookInfo
>();
362 List
<MetaData
> metas
;
365 metas
= lib
.getListBySource(value
);
367 metas
= lib
.getListByAuthor(value
);
369 } catch (IOException e
) {
370 error(e
.getLocalizedMessage(), "IOException", e
);
371 metas
= new ArrayList
<MetaData
>();
374 for (MetaData meta
: metas
) {
375 infos
.add(GuiReaderBookInfo
.fromMeta(meta
));
378 books
.get(value
).refreshBooks(infos
, words
);
381 if (bookPane
!= null) {
382 bookPane
.refreshBooks(words
);
389 * Open a {@link GuiReaderBook} item.
392 * the {@link GuiReaderBook} to open
394 public void openBook(final GuiReaderBook book
) {
395 final Progress pg
= new Progress();
396 outOfUi(pg
, false, new Runnable() {
400 helper
.getReader().read(book
.getInfo().getMeta().getLuid(),
402 SwingUtilities
.invokeLater(new Runnable() {
405 book
.setCached(true);
408 } catch (IOException e
) {
409 Instance
.getTraceHandler().error(e
);
410 error(GuiReader
.trans(StringIdGui
.ERROR_CANNOT_OPEN
),
411 GuiReader
.trans(StringIdGui
.TITLE_ERROR
), e
);
418 * Process the given action out of the Swing UI thread and link the given
419 * {@link ProgressBar} to the action.
421 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
422 * to done when the action is done.
425 * the {@link ProgressBar} or NULL
426 * @param refreshBooks
427 * TRUE to refresh the books after
431 public void outOfUi(Progress progress
, final boolean refreshBooks
,
432 final Runnable run
) {
433 final Progress pg
= new Progress();
434 final Progress reload
= new Progress(
435 GuiReader
.trans(StringIdGui
.PROGRESS_OUT_OF_UI_RELOAD_BOOKS
));
437 if (progress
== null) {
438 progress
= new Progress();
442 pg
.addProgress(progress
, 100);
444 pg
.addProgress(progress
, 90);
445 pg
.addProgress(reload
, 10);
449 pgBar
.setProgress(pg
);
453 new Thread(new Runnable() {
464 // will trigger pgBar ActionListener:
469 }, "outOfUi thread").start();
473 * Process the given action in the main Swing UI thread.
475 * The code will make sure the current thread is the main UI thread and, if
476 * not, will switch to it before executing the runnable.
478 * Synchronous operation.
483 public void inUi(final Runnable run
) {
484 if (EventQueue
.isDispatchThread()) {
488 EventQueue
.invokeAndWait(run
);
489 } catch (InterruptedException e
) {
490 Instance
.getTraceHandler().error(e
);
491 } catch (InvocationTargetException e
) {
492 Instance
.getTraceHandler().error(e
);
498 * Import a {@link Story} into the main {@link LocalLibrary}.
500 * Should be called inside the UI thread.
503 * TRUE for an {@link URL}, false for a {@link File}
505 public void imprt(boolean askUrl
) {
506 JFileChooser fc
= new JFileChooser();
510 String clipboard
= "";
512 clipboard
= ("" + Toolkit
.getDefaultToolkit()
513 .getSystemClipboard().getData(DataFlavor
.stringFlavor
))
515 } catch (Exception e
) {
516 // No data will be handled
519 if (clipboard
== null || !clipboard
.startsWith("http")) {
523 url
= JOptionPane
.showInputDialog(GuiReaderMainPanel
.this,
524 GuiReader
.trans(StringIdGui
.SUBTITLE_IMPORT_URL
),
525 GuiReader
.trans(StringIdGui
.TITLE_IMPORT_URL
),
526 JOptionPane
.QUESTION_MESSAGE
, null, null, clipboard
);
527 } else if (fc
.showOpenDialog(this) != JFileChooser
.CANCEL_OPTION
) {
528 url
= fc
.getSelectedFile().getAbsolutePath();
533 if (url
!= null && !url
.toString().isEmpty()) {
534 imprt(url
.toString(), null, null);
539 * Actually import the {@link Story} into the main {@link LocalLibrary}.
541 * Should be called inside the UI thread.
544 * the {@link Story} to import by {@link URL}
546 * Action to execute on success
547 * @param onSuccessPgName
548 * the name to use for the onSuccess progress bar
550 public void imprt(final String url
, final StoryRunnable onSuccess
,
551 String onSuccessPgName
) {
552 final Progress pg
= new Progress();
553 final Progress pgImprt
= new Progress();
554 final Progress pgOnSuccess
= new Progress(onSuccessPgName
);
555 pg
.addProgress(pgImprt
, 95);
556 pg
.addProgress(pgOnSuccess
, 5);
558 outOfUi(pg
, true, new Runnable() {
564 story
= helper
.getReader().getLibrary()
565 .imprt(BasicReader
.getUrl(url
), pgImprt
);
566 } catch (IOException e
) {
570 final Exception e
= ex
;
572 final boolean ok
= (e
== null);
574 pgOnSuccess
.setProgress(0);
576 if (e
instanceof UnknownHostException
) {
577 error(GuiReader
.trans(
578 StringIdGui
.ERROR_URL_NOT_SUPPORTED
, url
),
579 GuiReader
.trans(StringIdGui
.TITLE_ERROR
), null);
581 error(GuiReader
.trans(
582 StringIdGui
.ERROR_URL_IMPORT_FAILED
, url
,
583 e
.getMessage()), GuiReader
584 .trans(StringIdGui
.TITLE_ERROR
), e
);
587 if (onSuccess
!= null) {
588 onSuccess
.run(story
);
597 * Enables or disables this component, depending on the value of the
598 * parameter <code>b</code>. An enabled component can respond to user input
599 * and generate events. Components are enabled initially by default.
601 * Enabling or disabling <b>this</b> component will also affect its
605 * If <code>true</code>, this component is enabled; otherwise
606 * this component is disabled
609 public void setEnabled(boolean b
) {
614 for (GuiReaderGroup group
: books
.values()) {
621 public void setWords(boolean words
) {
625 public GuiReaderBook
getSelectedBook() {
629 public void unsetSelectedBook() {
633 private void addListPane(String name
, List
<String
> values
,
634 final boolean type
) {
635 GuiReader reader
= helper
.getReader();
636 BasicLibrary lib
= reader
.getLibrary();
638 bookPane
= new GuiReaderGroup(reader
, name
, color
);
640 List
<GuiReaderBookInfo
> infos
= new ArrayList
<GuiReaderBookInfo
>();
641 for (String value
: values
) {
643 infos
.add(GuiReaderBookInfo
.fromSource(lib
, value
));
645 infos
.add(GuiReaderBookInfo
.fromAuthor(lib
, value
));
649 bookPane
.refreshBooks(infos
, words
);
657 bookPane
.setActionListener(new BookActionListener() {
659 public void select(GuiReaderBook book
) {
664 public void popupRequested(GuiReaderBook book
, Component target
,
666 JPopupMenu popup
= helper
.createSourceAuthorPopup();
667 popup
.show(target
, x
, y
);
671 public void action(final GuiReaderBook book
) {
673 addBookPane(book
.getInfo().getMainInfo(), type
);
682 * Focus the first {@link GuiReaderGroup} we find.
684 private void focus() {
685 GuiReaderGroup group
= null;
686 Map
<String
, GuiReaderGroup
> books
= this.books
;
687 if (books
.size() > 0) {
688 group
= books
.values().iterator().next();
696 group
.requestFocusInWindow();
701 * Display an error message and log the linked {@link Exception}.
706 * the title of the error message
708 * the exception to log if any
710 private void error(final String message
, final String title
, Exception e
) {
711 Instance
.getTraceHandler().error(title
+ ": " + message
);
713 Instance
.getTraceHandler().error(e
);
716 SwingUtilities
.invokeLater(new Runnable() {
719 JOptionPane
.showMessageDialog(GuiReaderMainPanel
.this, message
,
720 title
, JOptionPane
.ERROR_MESSAGE
);