make it subtree
[fanfix.git] / reader / ui / GuiReaderMainPanel.java
diff --git a/reader/ui/GuiReaderMainPanel.java b/reader/ui/GuiReaderMainPanel.java
new file mode 100644 (file)
index 0000000..476e130
--- /dev/null
@@ -0,0 +1,792 @@
+package be.nikiroo.fanfix.reader.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.EventQueue;
+import java.awt.Frame;
+import java.awt.Toolkit;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.swing.BoxLayout;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JMenuBar;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollPane;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.bundles.StringIdGui;
+import be.nikiroo.fanfix.bundles.UiConfig;
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.data.Story;
+import be.nikiroo.fanfix.library.BasicLibrary;
+import be.nikiroo.fanfix.library.BasicLibrary.Status;
+import be.nikiroo.fanfix.library.LocalLibrary;
+import be.nikiroo.fanfix.reader.BasicReader;
+import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
+import be.nikiroo.fanfix.reader.ui.GuiReaderBookInfo.Type;
+import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.ui.ProgressBar;
+
+/**
+ * A {@link Frame} that will show a {@link GuiReaderBook} item for each
+ * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
+ * way to copy them to the {@link GuiReader} cache (
+ * {@link BasicReader#getLibrary()}), read them, delete them...
+ * 
+ * @author niki
+ */
+class GuiReaderMainPanel extends JPanel {
+       private static final long serialVersionUID = 1L;
+       private FrameHelper helper;
+       private Map<String, GuiReaderGroup> books;
+       private GuiReaderGroup bookPane; // for more "All"
+       private JPanel pane;
+       private Color color;
+       private ProgressBar pgBar;
+       private JMenuBar bar;
+       private GuiReaderBook selectedBook;
+       private boolean words; // words or authors (secondary info on books)
+       private boolean currentType; // type/source or author mode (All and Listing)
+
+       /**
+        * An object that offers some helper methods to access the frame that host
+        * it and the Fanfix-related functions.
+        * 
+        * @author niki
+        */
+       public interface FrameHelper {
+               /**
+                * Return the reader associated to this {@link FrameHelper}.
+                * 
+                * @return the reader
+                */
+               public GuiReader getReader();
+
+               /**
+                * Create the main menu bar.
+                * <p>
+                * Will invalidate the layout.
+                * 
+                * @param status
+                *            the library status, <b>must not</b> be NULL
+                */
+               public void createMenu(Status status);
+
+               /**
+                * Create a popup menu for a {@link GuiReaderBook} that represents a
+                * story.
+                * 
+                * @return the popup menu to display
+                */
+               public JPopupMenu createBookPopup();
+
+               /**
+                * Create a popup menu for a {@link GuiReaderBook} that represents a
+                * source/type or an author.
+                * 
+                * @return the popup menu to display
+                */
+               public JPopupMenu createSourceAuthorPopup();
+       }
+
+       /**
+        * A {@link Runnable} with a {@link MetaData} parameter.
+        * 
+        * @author niki
+        */
+       public interface MetaDataRunnable {
+               /**
+                * Run the action.
+                * 
+                * @param meta
+                *            the meta of the story
+                */
+               public void run(MetaData meta);
+       }
+
+       /**
+        * Create a new {@link GuiReaderMainPanel}.
+        * 
+        * @param parent
+        *            the associated {@link FrameHelper} to forward some commands
+        *            and access its {@link LocalLibrary}
+        * @param type
+        *            the type of {@link Story} to load, or NULL for all types
+        */
+       public GuiReaderMainPanel(FrameHelper parent, String type) {
+               super(new BorderLayout(), true);
+
+               this.helper = parent;
+
+               pane = new JPanel();
+               pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
+               JScrollPane scroll = new JScrollPane(pane);
+
+               Integer icolor = Instance.getUiConfig().getColor(
+                               UiConfig.BACKGROUND_COLOR);
+               if (icolor != null) {
+                       color = new Color(icolor);
+                       setBackground(color);
+                       pane.setBackground(color);
+                       scroll.setBackground(color);
+               }
+
+               scroll.getVerticalScrollBar().setUnitIncrement(16);
+               add(scroll, BorderLayout.CENTER);
+
+               String message = parent.getReader().getLibrary().getLibraryName();
+               if (!message.isEmpty()) {
+                       JLabel name = new JLabel(message, SwingConstants.CENTER);
+                       add(name, BorderLayout.NORTH);
+               }
+
+               pgBar = new ProgressBar();
+               add(pgBar, BorderLayout.SOUTH);
+
+               pgBar.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               pgBar.invalidate();
+                               pgBar.setProgress(null);
+                               setEnabled(true);
+                               validate();
+                       }
+               });
+
+               pgBar.addUpdateListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               pgBar.invalidate();
+                               validate();
+                               repaint();
+                       }
+               });
+
+               books = new TreeMap<String, GuiReaderGroup>();
+
+               addFocusListener(new FocusAdapter() {
+                       @Override
+                       public void focusGained(FocusEvent e) {
+                               focus();
+                       }
+               });
+
+               pane.setVisible(false);
+               final Progress pg = new Progress();
+               final String typeF = type;
+               outOfUi(pg, true, new Runnable() {
+                       @Override
+                       public void run() {
+                               final BasicLibrary lib = helper.getReader().getLibrary();
+                               final Status status = lib.getStatus();
+
+                               if (status == Status.READ_WRITE) {
+                                       lib.refresh(pg);
+                               }
+
+                               inUi(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               if (status.isReady()) {
+                                                       helper.createMenu(status);
+                                                       pane.setVisible(true);
+                                                       if (typeF == null) {
+                                                               try {
+                                                                       addBookPane(true, false);
+                                                               } catch (IOException e) {
+                                                                       error(e.getLocalizedMessage(),
+                                                                                       "IOException", e);
+                                                               }
+                                                       } else {
+                                                               addBookPane(typeF, true);
+                                                       }
+                                               } else {
+                                                       helper.createMenu(status);
+                                                       validate();
+
+                                                       String desc = Instance.getTransGui().getStringX(
+                                                                       StringIdGui.ERROR_LIB_STATUS,
+                                                                       status.toString());
+                                                       if (desc == null) {
+                                                               desc = GuiReader
+                                                                               .trans(StringIdGui.ERROR_LIB_STATUS);
+                                                       }
+
+                                                       String err = lib.getLibraryName() + "\n" + desc;
+                                                       error(err, GuiReader
+                                                                       .trans(StringIdGui.TITLE_ERROR_LIBRARY),
+                                                                       null);
+                                               }
+                                       }
+                               });
+                       }
+               });
+       }
+
+       public boolean getCurrentType() {
+               return currentType;
+       }
+
+       /**
+        * Add a new {@link GuiReaderGroup} on the frame to display all the
+        * sources/types or all the authors, or a listing of all the books sorted
+        * either by source or author.
+        * <p>
+        * A display of all the sources/types or all the authors will show one icon
+        * per source/type or author.
+        * <p>
+        * A listing of all the books sorted by source/type or author will display
+        * all the books.
+        * 
+        * @param type
+        *            TRUE for type/source, FALSE for author
+        * @param listMode
+        *            TRUE to get a listing of all the sources or authors, FALSE to
+        *            get one icon per source or author
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       public void addBookPane(boolean type, boolean listMode) throws IOException {
+               this.currentType = type;
+               BasicLibrary lib = helper.getReader().getLibrary();
+               if (type) {
+                       if (!listMode) {
+                               addListPane(GuiReader.trans(StringIdGui.MENU_SOURCES),
+                                               lib.getSources(), type);
+                       } else {
+                               for (String tt : lib.getSources()) {
+                                       if (tt != null) {
+                                               addBookPane(tt, type);
+                                       }
+                               }
+                       }
+               } else {
+                       if (!listMode) {
+                               addListPane(GuiReader.trans(StringIdGui.MENU_AUTHORS),
+                                               lib.getAuthors(), type);
+                       } else {
+                               for (String tt : lib.getAuthors()) {
+                                       if (tt != null) {
+                                               addBookPane(tt, type);
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Add a new {@link GuiReaderGroup} on the frame to display the books of the
+        * selected type or author.
+        * <p>
+        * Will invalidate the layout.
+        * 
+        * @param value
+        *            the author or the type, or NULL to get all the
+        *            authors-or-types
+        * @param type
+        *            TRUE for type/source, FALSE for author
+        */
+       public void addBookPane(String value, boolean type) {
+               this.currentType = type;
+
+               GuiReaderGroup bookPane = new GuiReaderGroup(helper.getReader(), value,
+                               color);
+
+               books.put(value, bookPane);
+
+               pane.invalidate();
+               pane.add(bookPane);
+
+               bookPane.setActionListener(new BookActionListener() {
+                       @Override
+                       public void select(GuiReaderBook book) {
+                               selectedBook = book;
+                       }
+
+                       @Override
+                       public void popupRequested(GuiReaderBook book, Component target,
+                                       int x, int y) {
+                               JPopupMenu popup = helper.createBookPopup();
+                               popup.show(target, x, y);
+                       }
+
+                       @Override
+                       public void action(final GuiReaderBook book) {
+                               openBook(book);
+                       }
+               });
+
+               focus();
+       }
+
+       /**
+        * Clear the pane from any book that may be present, usually prior to adding
+        * new ones.
+        * <p>
+        * Will invalidate the layout.
+        */
+       public void removeBookPanes() {
+               books.clear();
+               pane.invalidate();
+               pane.removeAll();
+       }
+
+       /**
+        * Refresh the list of {@link GuiReaderBook}s from disk.
+        * <p>
+        * Will validate the layout, as it is a "refresh" operation.
+        */
+       public void refreshBooks() {
+               BasicLibrary lib = helper.getReader().getLibrary();
+               for (String value : books.keySet()) {
+                       List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
+
+                       List<MetaData> metas;
+                       try {
+                               if (currentType) {
+                                       metas = lib.getListBySource(value);
+                               } else {
+                                       metas = lib.getListByAuthor(value);
+                               }
+                       } catch (IOException e) {
+                               error(e.getLocalizedMessage(), "IOException", e);
+                               metas = new ArrayList<MetaData>();
+                       }
+
+                       for (MetaData meta : metas) {
+                               infos.add(GuiReaderBookInfo.fromMeta(meta));
+                       }
+
+                       books.get(value).refreshBooks(infos, words);
+               }
+
+               if (bookPane != null) {
+                       bookPane.refreshBooks(words);
+               }
+
+               this.validate();
+       }
+
+       /**
+        * Open a {@link GuiReaderBook} item.
+        * 
+        * @param book
+        *            the {@link GuiReaderBook} to open
+        */
+       public void openBook(final GuiReaderBook book) {
+               final Progress pg = new Progress();
+               outOfUi(pg, false, new Runnable() {
+                       @Override
+                       public void run() {
+                               try {
+                                       helper.getReader().read(book.getInfo().getMeta().getLuid(),
+                                                       false, pg);
+                                       SwingUtilities.invokeLater(new Runnable() {
+                                               @Override
+                                               public void run() {
+                                                       book.setCached(true);
+                                               }
+                                       });
+                               } catch (IOException e) {
+                                       Instance.getTraceHandler().error(e);
+                                       error(GuiReader.trans(StringIdGui.ERROR_CANNOT_OPEN),
+                                                       GuiReader.trans(StringIdGui.TITLE_ERROR), e);
+                               }
+                       }
+               });
+       }
+       
+       /**
+        * Prefetch a {@link GuiReaderBook} item (which can be a group, in which
+        * case we prefetch all its members).
+        * 
+        * @param book
+        *            the {@link GuiReaderBook} to open
+        */
+       public void prefetchBook(final GuiReaderBook book) {
+               final List<String> luids = new LinkedList<String>();
+               try {
+                       switch (book.getInfo().getType()) {
+                       case STORY:
+                               luids.add(book.getInfo().getMeta().getLuid());
+                               break;
+                       case SOURCE:
+                               for (MetaData meta : helper.getReader().getLibrary()
+                                               .getListBySource(book.getInfo().getMainInfo())) {
+                                       luids.add(meta.getLuid());
+                               }
+                               break;
+                       case AUTHOR:
+                               for (MetaData meta : helper.getReader().getLibrary()
+                                               .getListByAuthor(book.getInfo().getMainInfo())) {
+                                       luids.add(meta.getLuid());
+                               }
+                               break;
+                       }
+               } catch (IOException e) {
+                       Instance.getTraceHandler().error(e);
+               }
+
+               final Progress pg = new Progress();
+               pg.setMax(luids.size());
+
+               outOfUi(pg, false, new Runnable() {
+                       @Override
+                       public void run() {
+                               try {
+                                       for (String luid : luids) {
+                                               Progress pgStep = new Progress();
+                                               pg.addProgress(pgStep, 1);
+
+                                               helper.getReader().prefetch(luid, pgStep);
+                                       }
+
+                                       // TODO: also set the green button on sources/authors?
+                                       // requires to do the same when all stories inside are green
+                                       if (book.getInfo().getType() == Type.STORY) {
+                                               SwingUtilities.invokeLater(new Runnable() {
+                                                       @Override
+                                                       public void run() {
+                                                               book.setCached(true);
+                                                       }
+                                               });
+                                       }
+                               } catch (IOException e) {
+                                       Instance.getTraceHandler().error(e);
+                                       error(GuiReader.trans(StringIdGui.ERROR_CANNOT_OPEN),
+                                                       GuiReader.trans(StringIdGui.TITLE_ERROR), e);
+                               }
+                       }
+               });
+       }
+
+       /**
+        * Process the given action out of the Swing UI thread and link the given
+        * {@link ProgressBar} to the action.
+        * <p>
+        * The code will make sure that the {@link ProgressBar} (if not NULL) is set
+        * to done when the action is done.
+        * 
+        * @param progress
+        *            the {@link ProgressBar} or NULL
+        * @param refreshBooks
+        *            TRUE to refresh the books after
+        * @param run
+        *            the action to run
+        */
+       public void outOfUi(Progress progress, final boolean refreshBooks,
+                       final Runnable run) {
+               final Progress pg = new Progress();
+               final Progress reload = new Progress(
+                               GuiReader.trans(StringIdGui.PROGRESS_OUT_OF_UI_RELOAD_BOOKS));
+
+               if (progress == null) {
+                       progress = new Progress();
+               }
+
+               if (refreshBooks) {
+                       pg.addProgress(progress, 100);
+               } else {
+                       pg.addProgress(progress, 90);
+                       pg.addProgress(reload, 10);
+               }
+
+               invalidate();
+               pgBar.setProgress(pg);
+               validate();
+               setEnabled(false);
+
+               new Thread(new Runnable() {
+                       @Override
+                       public void run() {
+                               try {
+                                       run.run();
+                                       if (refreshBooks) {
+                                               refreshBooks();
+                                       }
+                               } finally {
+                                       reload.done();
+                                       if (!pg.isDone()) {
+                                               // will trigger pgBar ActionListener:
+                                               pg.done();
+                                       }
+                               }
+                       }
+               }, "outOfUi thread").start();
+       }
+
+       /**
+        * Process the given action in the main Swing UI thread.
+        * <p>
+        * The code will make sure the current thread is the main UI thread and, if
+        * not, will switch to it before executing the runnable.
+        * <p>
+        * Synchronous operation.
+        * 
+        * @param run
+        *            the action to run
+        */
+       public void inUi(final Runnable run) {
+               if (EventQueue.isDispatchThread()) {
+                       run.run();
+               } else {
+                       try {
+                               EventQueue.invokeAndWait(run);
+                       } catch (InterruptedException e) {
+                               Instance.getTraceHandler().error(e);
+                       } catch (InvocationTargetException e) {
+                               Instance.getTraceHandler().error(e);
+                       }
+               }
+       }
+
+       /**
+        * Import a {@link Story} into the main {@link LocalLibrary}.
+        * <p>
+        * Should be called inside the UI thread.
+        * 
+        * @param askUrl
+        *            TRUE for an {@link URL}, false for a {@link File}
+        */
+       public void imprt(boolean askUrl) {
+               JFileChooser fc = new JFileChooser();
+
+               Object url;
+               if (askUrl) {
+                       String clipboard = "";
+                       try {
+                               clipboard = ("" + Toolkit.getDefaultToolkit()
+                                               .getSystemClipboard().getData(DataFlavor.stringFlavor))
+                                               .trim();
+                       } catch (Exception e) {
+                               // No data will be handled
+                       }
+
+                       if (clipboard == null || !(clipboard.startsWith("http://") || //
+                                       clipboard.startsWith("https://"))) {
+                               clipboard = "";
+                       }
+
+                       url = JOptionPane.showInputDialog(GuiReaderMainPanel.this,
+                                       GuiReader.trans(StringIdGui.SUBTITLE_IMPORT_URL),
+                                       GuiReader.trans(StringIdGui.TITLE_IMPORT_URL),
+                                       JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
+               } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
+                       url = fc.getSelectedFile().getAbsolutePath();
+               } else {
+                       url = null;
+               }
+
+               if (url != null && !url.toString().isEmpty()) {
+                       imprt(url.toString(), null, null);
+               }
+       }
+
+       /**
+        * Actually import the {@link Story} into the main {@link LocalLibrary}.
+        * <p>
+        * Should be called inside the UI thread.
+        * 
+        * @param url
+        *            the {@link Story} to import by {@link URL}
+        * @param onSuccess
+        *            Action to execute on success
+        * @param onSuccessPgName
+        *            the name to use for the onSuccess progress bar
+        */
+       public void imprt(final String url, final MetaDataRunnable onSuccess,
+                       String onSuccessPgName) {
+               final Progress pg = new Progress();
+               final Progress pgImprt = new Progress();
+               final Progress pgOnSuccess = new Progress(onSuccessPgName);
+               pg.addProgress(pgImprt, 95);
+               pg.addProgress(pgOnSuccess, 5);
+
+               outOfUi(pg, true, new Runnable() {
+                       @Override
+                       public void run() {
+                               Exception ex = null;
+                               MetaData meta = null;
+                               try {
+                                       meta = helper.getReader().getLibrary()
+                                                       .imprt(BasicReader.getUrl(url), pgImprt);
+                               } catch (IOException e) {
+                                       ex = e;
+                               }
+
+                               final Exception e = ex;
+
+                               final boolean ok = (e == null);
+
+                               pgOnSuccess.setProgress(0);
+                               if (!ok) {
+                                       if (e instanceof UnknownHostException) {
+                                               error(GuiReader.trans(
+                                                               StringIdGui.ERROR_URL_NOT_SUPPORTED, url),
+                                                               GuiReader.trans(StringIdGui.TITLE_ERROR), null);
+                                       } else {
+                                               error(GuiReader.trans(
+                                                               StringIdGui.ERROR_URL_IMPORT_FAILED, url,
+                                                               e.getMessage()), GuiReader
+                                                               .trans(StringIdGui.TITLE_ERROR), e);
+                                       }
+                               } else {
+                                       if (onSuccess != null) {
+                                               onSuccess.run(meta);
+                                       }
+                               }
+                               pgOnSuccess.done();
+                       }
+               });
+       }
+
+       /**
+        * Enables or disables this component, depending on the value of the
+        * parameter <code>b</code>. An enabled component can respond to user input
+        * and generate events. Components are enabled initially by default.
+        * <p>
+        * Enabling or disabling <b>this</b> component will also affect its
+        * children.
+        * 
+        * @param b
+        *            If <code>true</code>, this component is enabled; otherwise
+        *            this component is disabled
+        */
+       @Override
+       public void setEnabled(boolean b) {
+               if (bar != null) {
+                       bar.setEnabled(b);
+               }
+
+               for (GuiReaderGroup group : books.values()) {
+                       group.setEnabled(b);
+               }
+               super.setEnabled(b);
+               repaint();
+       }
+
+       public void setWords(boolean words) {
+               this.words = words;
+       }
+
+       public GuiReaderBook getSelectedBook() {
+               return selectedBook;
+       }
+
+       public void unsetSelectedBook() {
+               selectedBook = null;
+       }
+
+       private void addListPane(String name, List<String> values,
+                       final boolean type) {
+               GuiReader reader = helper.getReader();
+               BasicLibrary lib = reader.getLibrary();
+
+               bookPane = new GuiReaderGroup(reader, name, color);
+
+               List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
+               for (String value : values) {
+                       if (type) {
+                               infos.add(GuiReaderBookInfo.fromSource(lib, value));
+                       } else {
+                               infos.add(GuiReaderBookInfo.fromAuthor(lib, value));
+                       }
+               }
+
+               bookPane.refreshBooks(infos, words);
+
+               this.invalidate();
+               pane.invalidate();
+               pane.add(bookPane);
+               pane.validate();
+               this.validate();
+
+               bookPane.setActionListener(new BookActionListener() {
+                       @Override
+                       public void select(GuiReaderBook book) {
+                               selectedBook = book;
+                       }
+
+                       @Override
+                       public void popupRequested(GuiReaderBook book, Component target,
+                                       int x, int y) {
+                               JPopupMenu popup = helper.createSourceAuthorPopup();
+                               popup.show(target, x, y);
+                       }
+
+                       @Override
+                       public void action(final GuiReaderBook book) {
+                               removeBookPanes();
+                               addBookPane(book.getInfo().getMainInfo(), type);
+                               refreshBooks();
+                       }
+               });
+
+               focus();
+       }
+
+       /**
+        * Focus the first {@link GuiReaderGroup} we find.
+        */
+       private void focus() {
+               GuiReaderGroup group = null;
+               Map<String, GuiReaderGroup> books = this.books;
+               if (books.size() > 0) {
+                       group = books.values().iterator().next();
+               }
+
+               if (group == null) {
+                       group = bookPane;
+               }
+
+               if (group != null) {
+                       group.requestFocusInWindow();
+               }
+       }
+
+       /**
+        * Display an error message and log the linked {@link Exception}.
+        * 
+        * @param message
+        *            the message
+        * @param title
+        *            the title of the error message
+        * @param e
+        *            the exception to log if any
+        */
+       private void error(final String message, final String title, Exception e) {
+               Instance.getTraceHandler().error(title + ": " + message);
+               if (e != null) {
+                       Instance.getTraceHandler().error(e);
+               }
+
+               SwingUtilities.invokeLater(new Runnable() {
+                       @Override
+                       public void run() {
+                               JOptionPane.showMessageDialog(GuiReaderMainPanel.this, message,
+                                               title, JOptionPane.ERROR_MESSAGE);
+                       }
+               });
+       }
+}