re-introduce the internal viewer
authorNiki Roo <niki@nikiroo.be>
Fri, 24 Apr 2020 18:02:21 +0000 (20:02 +0200)
committerNiki Roo <niki@nikiroo.be>
Fri, 24 Apr 2020 18:02:21 +0000 (20:02 +0200)
src/be/nikiroo/fanfix_swing/Actions.java
src/be/nikiroo/fanfix_swing/gui/BooksPanel.java
src/be/nikiroo/fanfix_swing/gui/book/BookPopup.java
src/be/nikiroo/fanfix_swing/gui/viewer/NavBar.java [new file with mode: 0644]
src/be/nikiroo/fanfix_swing/gui/viewer/Viewer.java [new file with mode: 0644]
src/be/nikiroo/fanfix_swing/gui/viewer/ViewerPanel.java [new file with mode: 0644]
src/be/nikiroo/fanfix_swing/gui/viewer/ViewerTextOutput.java [new file with mode: 0644]

index 4c26e08b37aeb80accf4196bfeed468ffb8e5985..c020097c9f5d6f3ba96f63b7e34b7dd644cdfd01 100644 (file)
@@ -21,10 +21,11 @@ import be.nikiroo.fanfix.library.BasicLibrary;
 import be.nikiroo.fanfix.library.LocalLibrary;
 import be.nikiroo.fanfix.reader.BasicReader;
 import be.nikiroo.fanfix_swing.gui.utils.UiHelper;
+import be.nikiroo.fanfix_swing.gui.viewer.Viewer;
 import be.nikiroo.utils.Progress;
 
 public class Actions {
-       static public void openExternal(final BasicLibrary lib, MetaData meta,
+       static public void openBook(final BasicLibrary lib, MetaData meta,
                        final Container parent, final Runnable onDone) {
                Container parentWindow = parent;
                while (!(parentWindow instanceof Window) && parentWindow != null) {
@@ -64,10 +65,12 @@ public class Actions {
 
                final SwingWorker<File, Void> worker = new SwingWorker<File, Void>() {
                        private File target;
+                       private Story story;
 
                        @Override
                        protected File doInBackground() throws Exception {
                                target = lib.getFile(luid, null);
+                               story = lib.getStory(luid, null);
                                return null;
                        }
 
@@ -75,7 +78,25 @@ public class Actions {
                        protected void done() {
                                try {
                                        get();
-                                       openExternal(target, isImageDocument);
+                                       boolean internalImg = Instance
+                                                       .getInstance()
+                                                       .getUiConfig()
+                                                       .getBoolean(
+                                                                       UiConfig.IMAGES_DOCUMENT_USE_INTERNAL_READER,
+                                                                       true);
+                                       boolean internalNonImg = Instance
+                                                       .getInstance()
+                                                       .getUiConfig()
+                                                       .getBoolean(
+                                                                       UiConfig.NON_IMAGES_DOCUMENT_USE_INTERNAL_READER,
+                                                                       true);
+
+                                       if (isImageDocument && internalImg || !isImageDocument
+                                                       && internalNonImg) {
+                                               openInternal(story);
+                                       } else {
+                                               openExternal(target, isImageDocument);
+                                       }
                                } catch (Exception e) {
                                        // TODO: i18n
                                        UiHelper.error(parent, e.getLocalizedMessage(),
@@ -98,6 +119,19 @@ public class Actions {
                worker.execute();
        }
 
+       /**
+        * Open the {@link Story} with an internal reader.
+        * <p>
+        * Asynchronous.
+        * 
+        * @param story
+        *            the story to open
+        */
+       static private void openInternal(Story story) {
+               Viewer viewer = new Viewer(Instance.getInstance().getLibrary(), story);
+               viewer.setVisible(true);
+       }
+
        /**
         * Open the {@link Story} with an external reader (the program will be
         * passed the given target file).
@@ -110,7 +144,7 @@ public class Actions {
         * @throws IOException
         *             in case of I/O error
         */
-       static public void openExternal(File target, boolean isImageDocument)
+       static private void openExternal(File target, boolean isImageDocument)
                        throws IOException {
                String program = null;
                if (isImageDocument) {
@@ -161,14 +195,13 @@ public class Actions {
                                }
                        }
                        if (!ok) {
-                               throw new IOException(
-                                               "Cannot find a program to start the file");
+                               throw new IOException("Cannot find a program to start the file");
                        }
                } else {
                        Instance.getInstance().getTraceHandler()
                                        .trace("starting external program: " + program);
-                       proc = Runtime.getRuntime()
-                                       .exec(new String[] { program, target.getAbsolutePath() });
+                       proc = Runtime.getRuntime().exec(
+                                       new String[] { program, target.getAbsolutePath() });
                }
 
                if (proc != null && sync) {
@@ -215,21 +248,26 @@ public class Actions {
                                } catch (IOException e) {
                                        pg.done();
                                        if (e instanceof UnknownHostException) {
-                                               UiHelper.error(parent,
-                                                               Instance.getInstance().getTransGui().getString(
-                                                                               StringIdGui.ERROR_URL_NOT_SUPPORTED,
-                                                                               url),
-                                                               Instance.getInstance().getTransGui().getString(
-                                                                               StringIdGui.TITLE_ERROR),
+                                               UiHelper.error(
+                                                               parent,
+                                                               Instance.getInstance()
+                                                                               .getTransGui()
+                                                                               .getString(
+                                                                                               StringIdGui.ERROR_URL_NOT_SUPPORTED,
+                                                                                               url),
+                                                               Instance.getInstance().getTransGui()
+                                                                               .getString(StringIdGui.TITLE_ERROR),
                                                                null);
                                        } else {
-                                               UiHelper.error(parent,
-                                                               Instance.getInstance().getTransGui().getString(
-                                                                               StringIdGui.ERROR_URL_IMPORT_FAILED,
-                                                                               url, e.getMessage()),
+                                               UiHelper.error(
+                                                               parent,
+                                                               Instance.getInstance()
+                                                                               .getTransGui()
+                                                                               .getString(
+                                                                                               StringIdGui.ERROR_URL_IMPORT_FAILED,
+                                                                                               url, e.getMessage()),
                                                                Instance.getInstance().getTransGui()
-                                                                               .getString(StringIdGui.TITLE_ERROR),
-                                                               e);
+                                                                               .getString(StringIdGui.TITLE_ERROR), e);
                                        }
                                }
 
index 4f169544d7e32283d0b020c9383d6709ada658a9..3ccac1fea6ad8a27e9c1a1afb44caa0dec6c5a4d 100644 (file)
@@ -210,7 +210,7 @@ public class BooksPanel extends ListenerPanel {
                                        final BookInfo book = data.get(index);
                                        BasicLibrary lib = Instance.getInstance().getLibrary();
 
-                                       Actions.openExternal(lib, book.getMeta(), BooksPanel.this,
+                                       Actions.openBook(lib, book.getMeta(), BooksPanel.this,
                                                        new Runnable() {
                                                                @Override
                                                                public void run() {
index 3d874cc1b99f394835882fafe14bf037a95bb36e..c067cfb446e7b1420d01c95bc80febdb020d215b 100644 (file)
@@ -711,7 +711,7 @@ public class BookPopup extends JPopupMenu {
                        public void actionPerformed(ActionEvent e) {
                                final BookInfo book = informer.getUniqueSelected();
                                if (book != null) {
-                                       Actions.openExternal(lib, book.getMeta(),
+                                       Actions.openBook(lib, book.getMeta(),
                                                        BookPopup.this.getParent(), new Runnable() {
                                                                @Override
                                                                public void run() {
diff --git a/src/be/nikiroo/fanfix_swing/gui/viewer/NavBar.java b/src/be/nikiroo/fanfix_swing/gui/viewer/NavBar.java
new file mode 100644 (file)
index 0000000..46735f1
--- /dev/null
@@ -0,0 +1,343 @@
+package be.nikiroo.fanfix_swing.gui.viewer;
+
+import java.awt.Color;
+import java.awt.LayoutManager;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import be.nikiroo.fanfix.Instance;
+
+/**
+ * A Swing-based navigation bar, that displays first/previous/next/last page
+ * buttons.
+ * 
+ * @author niki
+ */
+public class NavBar extends JPanel {
+       private static final long serialVersionUID = 1L;
+
+       private JLabel label;
+       private int index = 0;
+       private int min = 0;
+       private int max = 0;
+       private JButton[] navButtons;
+       String extraLabel = null;
+
+       private List<ActionListener> listeners = new ArrayList<ActionListener>();
+
+       /**
+        * Create a new navigation bar.
+        * <p>
+        * The minimum must be lower or equal to the maximum.
+        * <p>
+        * Note than a max of "-1" means "infinite".
+        * 
+        * @param min
+        *            the minimum page number (cannot be negative)
+        * @param max
+        *            the maximum page number (cannot be lower than min, except if
+        *            -1 (infinite))
+        * 
+        * @throws IndexOutOfBoundsException
+        *             if min &gt; max and max is not "-1"
+        */
+       public NavBar(int min, int max) {
+               if (min > max && max != -1) {
+                       throw new IndexOutOfBoundsException(String.format(
+                                       "min (%d) > max (%d)", min, max));
+               }
+
+               LayoutManager layout = new BoxLayout(this, BoxLayout.X_AXIS);
+               setLayout(layout);
+
+               // TODO:
+               // JButton up = new BasicArrowButton(BasicArrowButton.NORTH);
+               // JButton down = new BasicArrowButton(BasicArrowButton.SOUTH);
+
+               navButtons = new JButton[4];
+
+               navButtons[0] = createNavButton("<<", new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setIndex(NavBar.this.min);
+                               fireEvent();
+                       }
+               });
+               navButtons[1] = createNavButton(" < ", new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setIndex(index - 1);
+                               fireEvent();
+                       }
+               });
+               navButtons[2] = createNavButton(" > ", new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setIndex(index + 1);
+                               fireEvent();
+                       }
+               });
+               navButtons[3] = createNavButton(">>", new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setIndex(NavBar.this.max);
+                               fireEvent();
+                       }
+               });
+
+               for (JButton navButton : navButtons) {
+                       add(navButton);
+               }
+
+               label = new JLabel("");
+               add(label);
+
+               this.min = min;
+               this.max = max;
+               this.index = min;
+
+               updateEnabled();
+               updateLabel();
+               fireEvent();
+       }
+
+       /**
+        * The current index, must be between {@link NavBar#min} and
+        * {@link NavBar#max}, both inclusive.
+        * 
+        * @return the index
+        */
+       public int getIndex() {
+               return index;
+       }
+
+       /**
+        * The current index, must be between {@link NavBar#min} and
+        * {@link NavBar#max}, both inclusive.
+        * 
+        * @param index
+        *            the new index
+        */
+       public void setIndex(int index) {
+               if (index != this.index) {
+                       if (index < min || (index > max && max != -1)) {
+                               throw new IndexOutOfBoundsException(String.format(
+                                               "Index %d but min/max is [%d/%d]", index, min, max));
+                       }
+
+                       this.index = index;
+                       updateLabel();
+               }
+
+               updateEnabled();
+       }
+
+       /**
+        * The minimun page number. Cannot be negative.
+        * 
+        * @return the min
+        */
+       public int getMin() {
+               return min;
+       }
+
+       /**
+        * The minimum page number. Cannot be negative.
+        * <p>
+        * May update the index if needed (if the index is &lt; the new min).
+        * <p>
+        * Will also (always) update the label and enable/disable the required
+        * buttons.
+        * 
+        * @param min
+        *            the new min
+        */
+       public void setMin(int min) {
+               this.min = min;
+               if (index < min) {
+                       index = min;
+               }
+               updateEnabled();
+               updateLabel();
+
+       }
+
+       /**
+        * The maximum page number. Cannot be lower than min, except if -1
+        * (infinite).
+        * 
+        * @return the max
+        */
+       public int getMax() {
+               return max;
+       }
+
+       /**
+        * The maximum page number. Cannot be lower than min, except if -1
+        * (infinite).
+        * <p>
+        * May update the index if needed (if the index is &gt; the new max).
+        * <p>
+        * Will also (always) update the label and enable/disable the required
+        * buttons.
+        * 
+        * @param max
+        *            the new max
+        */
+       public void setMax(int max) {
+               this.max = max;
+               if (index > max && max != -1) {
+                       index = max;
+               }
+               updateEnabled();
+               updateLabel();
+       }
+
+       /**
+        * The current extra label to display with the default
+        * {@link NavBar#computeLabel(int, int, int)} implementation.
+        * 
+        * @return the current label
+        */
+       public String getExtraLabel() {
+               return extraLabel;
+       }
+
+       /**
+        * The current extra label to display with the default
+        * {@link NavBar#computeLabel(int, int, int)} implementation.
+        * 
+        * @param currentLabel
+        *            the new current label
+        */
+       public void setExtraLabel(String currentLabel) {
+               this.extraLabel = currentLabel;
+               updateLabel();
+       }
+
+       /**
+        * Add a listener that will be called on each page change.
+        * 
+        * @param listener
+        *            the new listener
+        */
+       public void addActionListener(ActionListener listener) {
+               listeners.add(listener);
+       }
+
+       /**
+        * Remove the given listener if possible.
+        * 
+        * @param listener
+        *            the listener to remove
+        * @return TRUE if it was removed, FALSE if it was not found
+        */
+       public boolean removeActionListener(ActionListener listener) {
+               return listeners.remove(listener);
+       }
+
+       /**
+        * Remove all the listeners.
+        */
+       public void clearActionsListeners() {
+               listeners.clear();
+       }
+
+       /**
+        * Notify a change of page.
+        */
+       public void fireEvent() {
+               for (ActionListener listener : listeners) {
+                       try {
+                               listener.actionPerformed(new ActionEvent(this,
+                                               ActionEvent.ACTION_FIRST, "page changed"));
+                       } catch (Exception e) {
+                               Instance.getInstance().getTraceHandler().error(e);
+                       }
+               }
+       }
+
+       /**
+        * Create a single navigation button.
+        * 
+        * @param text
+        *            the text to display
+        * @param action
+        *            the action to take on click
+        * @return the button
+        */
+       private JButton createNavButton(String text, ActionListener action) {
+               JButton navButton = new JButton(text);
+               navButton.addActionListener(action);
+               navButton.setForeground(Color.BLUE);
+               return navButton;
+       }
+
+       /**
+        * Update the label displayed in the UI.
+        */
+       private void updateLabel() {
+               label.setText(computeLabel(index, min, max));
+       }
+
+       /**
+        * Update the navigation buttons "enabled" state according to the current
+        * index value.
+        */
+       private void updateEnabled() {
+               navButtons[0].setEnabled(index > min);
+               navButtons[1].setEnabled(index > min);
+               navButtons[2].setEnabled(index < max || max == -1);
+               navButtons[3].setEnabled(index < max || max == -1);
+       }
+
+       /**
+        * Return the label to display for the given index.
+        * <p>
+        * Swing HTML (HTML3) is supported if surrounded by &lt;HTML&gt; and
+        * &lt;/HTML&gt;.
+        * <p>
+        * By default, return "Page 1/5: current_label" (with the current index and
+        * {@link NavBar#getCurrentLabel()}).
+        * 
+        * @param index
+        *            the new index number
+        * @param mix
+        *            the minimum index (inclusive)
+        * @param max
+        *            the maximum index (inclusive)
+        * @return the label
+        */
+       protected String computeLabel(int index,
+                       @SuppressWarnings("unused") int min, int max) {
+
+               String base = "&nbsp;&nbsp;<B>Page <SPAN COLOR='#444466'>%d</SPAN>&nbsp;";
+               if (max >= 0) {
+                       base += "/&nbsp;%d";
+               }
+               base += "</B>";
+
+               String ifLabel = ": %s";
+
+               String display = base;
+               String label = getExtraLabel();
+               if (label != null && !label.trim().isEmpty()) {
+                       display += ifLabel;
+               }
+
+               display = "<HTML>" + display + "</HTML>";
+
+               if (max >= 0) {
+                       return String.format(display, index, max, label);
+               }
+
+               return String.format(display, index, label);
+       }
+}
diff --git a/src/be/nikiroo/fanfix_swing/gui/viewer/Viewer.java b/src/be/nikiroo/fanfix_swing/gui/viewer/Viewer.java
new file mode 100644 (file)
index 0000000..fc70da4
--- /dev/null
@@ -0,0 +1,168 @@
+package be.nikiroo.fanfix_swing.gui.viewer;
+
+import java.awt.BorderLayout;
+import java.awt.Font;
+import java.awt.LayoutManager;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.SwingConstants;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.bundles.StringIdGui;
+import be.nikiroo.fanfix.data.Chapter;
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.data.Story;
+import be.nikiroo.fanfix.library.BasicLibrary;
+import be.nikiroo.fanfix_swing.gui.PropertiesPanel;
+
+/**
+ * An internal, Swing-based {@link Story} viewer.
+ * <p>
+ * Works on both text and image document (see {@link MetaData#isImageDocument()}
+ * ).
+ * 
+ * @author niki
+ */
+public class Viewer extends JFrame {
+       private static final long serialVersionUID = 1L;
+
+       private Story story;
+       private MetaData meta;
+       private JLabel title;
+       private PropertiesPanel descPane;
+       private ViewerPanel mainPanel;
+       private NavBar navbar;
+
+       /**
+        * Create a new {@link Story} viewer.
+        * 
+        * @param lib
+        *            the {@link BasicLibrary} to load the cover from
+        * @param story
+        *            the {@link Story} to display
+        */
+       public Viewer(BasicLibrary lib, Story story) {
+               setTitle(Instance
+                               .getInstance()
+                               .getTransGui()
+                               .getString(StringIdGui.TITLE_STORY, story.getMeta().getLuid(),
+                                               story.getMeta().getTitle()));
+
+               setSize(800, 600);
+
+               this.story = story;
+               this.meta = story.getMeta();
+
+               initGuiBase(lib);
+               initGuiNavButtons();
+
+               setChapter(-1);
+       }
+
+       /**
+        * Initialise the base panel with everything but the navigation buttons.
+        * 
+        * @param lib
+        *            the {@link BasicLibrary} to use to retrieve the cover image in
+        *            the description panel
+        */
+       private void initGuiBase(BasicLibrary lib) {
+               setLayout(new BorderLayout());
+
+               title = new JLabel();
+               title.setFont(new Font(Font.SERIF, Font.BOLD,
+                               title.getFont().getSize() * 3));
+               title.setText(meta.getTitle());
+               title.setHorizontalAlignment(SwingConstants.CENTER);
+               title.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+               add(title, BorderLayout.NORTH);
+
+               JPanel contentPane = new JPanel(new BorderLayout());
+               add(contentPane, BorderLayout.CENTER);
+
+               descPane = new PropertiesPanel(lib, meta);
+               contentPane.add(descPane, BorderLayout.NORTH);
+
+               mainPanel = new ViewerPanel(story);
+               contentPane.add(mainPanel, BorderLayout.CENTER);
+       }
+
+       /**
+        * Create the 4 navigation buttons in {@link Viewer#navButtons} and
+        * initialise them.
+        */
+       private void initGuiNavButtons() {
+               navbar = new NavBar(-1, story.getChapters().size() - 1) {
+                       private static final long serialVersionUID = 1L;
+
+                       @Override
+                       protected String computeLabel(int index, int min, int max) {
+                               int chapter = index;
+                               Chapter chap;
+                               if (chapter < 0) {
+                                       chap = meta.getResume();
+                                       descPane.setVisible(true);
+                               } else {
+                                       chap = story.getChapters().get(chapter);
+                                       descPane.setVisible(false);
+                               }
+
+                               String chapterDisplay = Instance
+                                               .getInstance()
+                                               .getTransGui()
+                                               .getString(StringIdGui.CHAPTER_HTML_UNNAMED,
+                                                               chap.getNumber(), story.getChapters().size());
+                               if (chap.getName() != null && !chap.getName().trim().isEmpty()) {
+                                       chapterDisplay = Instance
+                                                       .getInstance()
+                                                       .getTransGui()
+                                                       .getString(StringIdGui.CHAPTER_HTML_NAMED,
+                                                                       chap.getNumber(),
+                                                                       story.getChapters().size(), chap.getName());
+                               }
+
+                               return "<HTML>" + chapterDisplay + "</HTML>";
+                       }
+               };
+
+               navbar.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setChapter(navbar.getIndex());
+                       }
+               });
+
+               JPanel navButtonsPane = new JPanel();
+               LayoutManager layout = new BoxLayout(navButtonsPane, BoxLayout.X_AXIS);
+               navButtonsPane.setLayout(layout);
+
+               add(navbar, BorderLayout.SOUTH);
+       }
+
+       /**
+        * Set the current chapter, 0-based.
+        * <p>
+        * Chapter -1 is reserved for the description page.
+        * 
+        * @param chapter
+        *            the chapter number to set
+        */
+       private void setChapter(int chapter) {
+               Chapter chap;
+               if (chapter < 0) {
+                       chap = meta.getResume();
+                       descPane.setVisible(true);
+               } else {
+                       chap = story.getChapters().get(chapter);
+                       descPane.setVisible(false);
+               }
+
+               mainPanel.setChapter(chap);
+       }
+}
diff --git a/src/be/nikiroo/fanfix_swing/gui/viewer/ViewerPanel.java b/src/be/nikiroo/fanfix_swing/gui/viewer/ViewerPanel.java
new file mode 100644 (file)
index 0000000..3832fa2
--- /dev/null
@@ -0,0 +1,301 @@
+package be.nikiroo.fanfix_swing.gui.viewer;
+
+import java.awt.BorderLayout;
+import java.awt.EventQueue;
+import java.awt.Graphics2D;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.image.BufferedImage;
+
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JEditorPane;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.JScrollPane;
+import javax.swing.SwingConstants;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.bundles.StringIdGui;
+import be.nikiroo.fanfix.data.Chapter;
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.data.Story;
+import be.nikiroo.utils.Image;
+import be.nikiroo.utils.ui.ImageUtilsAwt;
+
+/**
+ * A {@link JPanel} that will show a {@link Story} chapter on screen.
+ * 
+ * @author niki
+ */
+public class ViewerPanel extends JPanel {
+       private static final long serialVersionUID = 1L;
+
+       private boolean imageDocument;
+       private Chapter chap;
+       private JScrollPane scroll;
+       private ViewerTextOutput htmlOutput;
+
+       // text only:
+       private JEditorPane text;
+
+       // image only:
+       private JLabel image;
+       private JProgressBar imageProgress;
+       private int currentImage;
+       private JButton left;
+       private JButton right;
+
+       /**
+        * Create a new viewer.
+        * 
+        * @param story
+        *            the {@link Story} to work on
+        */
+       public ViewerPanel(Story story) {
+               this(story.getMeta(), story.getMeta().isImageDocument());
+       }
+
+       /**
+        * Create a new viewer.
+        * 
+        * @param meta
+        *            the {@link MetaData} of the story to show
+        * @param isImageDocument
+        *            TRUE if it is an image document, FALSE if not
+        */
+       public ViewerPanel(MetaData meta, boolean isImageDocument) {
+               super(new BorderLayout());
+
+               this.imageDocument = isImageDocument;
+
+               this.text = new JEditorPane("text/html", "");
+               text.setEditable(false);
+               text.setAlignmentY(TOP_ALIGNMENT);
+               htmlOutput = new ViewerTextOutput();
+
+               image = new JLabel();
+               image.setHorizontalAlignment(SwingConstants.CENTER);
+
+               scroll = new JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
+                               JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+               scroll.getVerticalScrollBar().setUnitIncrement(16);
+
+               // TODO:
+               // JButton up = new BasicArrowButton(BasicArrowButton.NORTH);
+               // JButton down = new BasicArrowButton(BasicArrowButton.SOUTH);
+
+               if (!imageDocument) {
+                       add(scroll, BorderLayout.CENTER);
+               } else {
+                       imageProgress = new JProgressBar();
+                       imageProgress.setStringPainted(true);
+                       add(imageProgress, BorderLayout.SOUTH);
+
+                       JPanel main = new JPanel(new BorderLayout());
+                       main.add(scroll, BorderLayout.CENTER);
+
+                       left = new JButton("<HTML>&nbsp; &nbsp; &lt; &nbsp; &nbsp;</HTML>");
+                       left.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       setImage(--currentImage);
+                               }
+                       });
+                       main.add(left, BorderLayout.WEST);
+
+                       right = new JButton("<HTML>&nbsp; &nbsp; &gt; &nbsp; &nbsp;</HTML>");
+                       right.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       setImage(++currentImage);
+                               }
+                       });
+                       main.add(right, BorderLayout.EAST);
+
+                       add(main, BorderLayout.CENTER);
+                       main.invalidate();
+               }
+
+               setChapter(meta.getResume());
+       }
+
+       /**
+        * Load the given chapter.
+        * <p>
+        * Will always be text for a non-image document.
+        * <p>
+        * Will be an image and left/right controls for an image-document, except
+        * for chapter 0 which will be text (chapter 0 = resume).
+        * 
+        * @param chap
+        *            the chapter to load
+        */
+       public void setChapter(Chapter chap) {
+               this.chap = chap;
+
+               if (!imageDocument) {
+                       setText(chap);
+               } else {
+                       left.setVisible(chap.getNumber() > 0);
+                       right.setVisible(chap.getNumber() > 0);
+                       imageProgress.setVisible(chap.getNumber() > 0);
+
+                       imageProgress.setMinimum(0);
+                       imageProgress.setMaximum(chap.getParagraphs().size() - 1);
+
+                       if (chap.getNumber() == 0) {
+                               setText(chap);
+                       } else {
+                               setImage(0);
+                       }
+               }
+       }
+
+       /**
+        * Will set and display the current chapter text.
+        * 
+        * @param chap
+        *            the chapter to display
+        */
+       private void setText(final Chapter chap) {
+               new Thread(new Runnable() {
+                       @Override
+                       public void run() {
+                               final String content = htmlOutput.convert(chap);
+                               // Wait until size computations are correct
+                               while (!scroll.isValid()) {
+                                       try {
+                                               Thread.sleep(1);
+                                       } catch (InterruptedException e) {
+                                       }
+                               }
+
+                               setText(content);
+                       }
+               }).start();
+       }
+
+       /**
+        * Actually set the text in the UI.
+        * <p>
+        * Do <b>NOT</b> use this method from the UI thread.
+        * 
+        * @param content
+        *            the text
+        */
+       private void setText(final String content) {
+               EventQueue.invokeLater(new Runnable() {
+                       @Override
+                       public void run() {
+                               text.setText(content);
+                               text.setCaretPosition(0);
+                               scroll.setViewportView(text);
+                       }
+               });
+       }
+
+       /**
+        * Will set and display the current image, take care about the progression
+        * and update the left and right cursors' <tt>enabled</tt> property.
+        * 
+        * @param i
+        *            the image index to load
+        */
+       private void setImage(int i) {
+               left.setEnabled(i > 0);
+               right.setEnabled(i + 1 < chap.getParagraphs().size());
+
+               if (i < 0 || i >= chap.getParagraphs().size()) {
+                       return;
+               }
+
+               imageProgress.setValue(i);
+               imageProgress.setString(Instance
+                               .getInstance()
+                               .getTransGui()
+                               .getString(StringIdGui.IMAGE_PROGRESSION, i + 1,
+                                               chap.getParagraphs().size()));
+
+               currentImage = i;
+
+               final Image img = chap.getParagraphs().get(i).getContentImage();
+
+               // prepare the viewport to get the right sizes later on
+               image.setIcon(null);
+               scroll.setViewportView(image);
+
+               new Thread(new Runnable() {
+                       @Override
+                       public void run() {
+                               // Wait until size computations are correct
+                               while (!scroll.isValid()) {
+                                       try {
+                                               Thread.sleep(1);
+                                       } catch (InterruptedException e) {
+                                       }
+                               }
+
+                               if (img == null) {
+                                       setText("Error: cannot render image.");
+                               } else {
+                                       setImage(img);
+                               }
+                       }
+               }).start();
+       }
+
+       /**
+        * Actually set the image in the UI.
+        * <p>
+        * Do <b>NOT</b> use this method from the UI thread.
+        * 
+        * @param img
+        *            the image to set
+        */
+       private void setImage(Image img) {
+               try {
+                       int scrollWidth = scroll.getWidth()
+                                       - scroll.getVerticalScrollBar().getWidth();
+
+                       BufferedImage buffImg = ImageUtilsAwt.fromImage(img);
+
+                       int iw = buffImg.getWidth();
+                       int ih = buffImg.getHeight();
+                       double ratio = ((double) ih) / iw;
+
+                       int w = scrollWidth;
+                       int h = (int) (ratio * scrollWidth);
+
+                       BufferedImage resizedImage = new BufferedImage(w, h,
+                                       BufferedImage.TYPE_4BYTE_ABGR);
+
+                       Graphics2D g = resizedImage.createGraphics();
+                       try {
+                               g.drawImage(buffImg, 0, 0, w, h, null);
+                       } finally {
+                               g.dispose();
+                       }
+
+                       final Icon icon = new ImageIcon(resizedImage);
+                       EventQueue.invokeLater(new Runnable() {
+                               @Override
+                               public void run() {
+                                       image.setIcon(icon);
+                                       scroll.setViewportView(image);
+                               }
+                       });
+               } catch (Exception e) {
+                       Instance.getInstance().getTraceHandler()
+                                       .error(new Exception("Failed to load image into label", e));
+                       EventQueue.invokeLater(new Runnable() {
+                               @Override
+                               public void run() {
+                                       text.setText("Error: cannot load image.");
+                               }
+                       });
+               }
+       }
+}
diff --git a/src/be/nikiroo/fanfix_swing/gui/viewer/ViewerTextOutput.java b/src/be/nikiroo/fanfix_swing/gui/viewer/ViewerTextOutput.java
new file mode 100644 (file)
index 0000000..16d863e
--- /dev/null
@@ -0,0 +1,128 @@
+package be.nikiroo.fanfix_swing.gui.viewer;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.data.Chapter;
+import be.nikiroo.fanfix.data.Paragraph;
+import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
+import be.nikiroo.fanfix.data.Story;
+import be.nikiroo.fanfix.output.BasicOutput;
+
+/**
+ * This class can export a chapter into HTML3 code ready for Java Swing support.
+ * 
+ * @author niki
+ */
+public class ViewerTextOutput {
+       private StringBuilder builder;
+       private BasicOutput output;
+       private Story fakeStory;
+
+       /**
+        * Create a new {@link ViewerTextOutput} that will convert a
+        * {@link Chapter} into HTML3 suited for Java Swing.
+        */
+       public ViewerTextOutput() {
+               builder = new StringBuilder();
+               fakeStory = new Story();
+
+               output = new BasicOutput() {
+                       private boolean paraInQuote;
+
+                       @Override
+                       protected void writeChapterHeader(Chapter chap) throws IOException {
+                               builder.append("<HTML>");
+
+                               builder.append("<H1>");
+                               builder.append("Chapter ");
+                               builder.append(chap.getNumber());
+                               builder.append(": ");
+                               builder.append(chap.getName());
+                               builder.append("</H1>");
+
+                               builder.append("<DIV align='justify'>");
+                       }
+
+                       @Override
+                       protected void writeChapterFooter(Chapter chap) throws IOException {
+                               if (paraInQuote) {
+                                       builder.append("</DIV>");
+                               }
+                               paraInQuote = false;
+
+                               builder.append("</DIV>");
+                               builder.append("</HTML>");
+                       }
+
+                       @Override
+                       protected void writeParagraph(Paragraph para) throws IOException {
+                               if ((para.getType() == ParagraphType.QUOTE) == !paraInQuote) {
+                                       paraInQuote = !paraInQuote;
+                                       if (paraInQuote) {
+                                               builder.append("<BR>");
+                                               builder.append("<DIV>");
+                                       } else {
+                                               builder.append("</DIV>");
+                                               builder.append("<BR>");
+                                       }
+                               }
+
+                               switch (para.getType()) {
+                               case NORMAL:
+                                       builder.append("&nbsp;&nbsp;&nbsp;&nbsp;");
+                                       builder.append(decorateText(para.getContent()));
+                                       builder.append("<BR>");
+                                       break;
+                               case BLANK:
+                                       builder.append("<BR><BR>");
+                                       break;
+                               case BREAK:
+                                       builder.append("<BR><P COLOR='#7777DD' ALIGN='CENTER'><B>");
+                                       builder.append("* * *");
+                                       builder.append("</B></P><BR><BR>");
+                                       break;
+                               case QUOTE:
+                                       builder.append("<DIV>");
+                                       builder.append("&nbsp;&nbsp;&nbsp;&nbsp;");
+                                       builder.append("&mdash;&nbsp;");
+                                       builder.append(decorateText(para.getContent()));
+                                       builder.append("</DIV>");
+
+                                       break;
+                               case IMAGE:
+                               }
+                       }
+
+                       @Override
+                       protected String enbold(String word) {
+                               return "<B COLOR='#7777DD'>" + word + "</B>";
+                       }
+
+                       @Override
+                       protected String italize(String word) {
+                               return "<I COLOR='GRAY'>" + word + "</I>";
+                       }
+               };
+       }
+
+       /**
+        * Convert the chapter into HTML3 code.
+        * 
+        * @param chap
+        *            the {@link Chapter} to convert.
+        * 
+        * @return HTML3 code tested with Java Swing
+        */
+       public String convert(Chapter chap) {
+               builder.setLength(0);
+               try {
+                       fakeStory.setChapters(Arrays.asList(chap));
+                       output.process(fakeStory, null, null);
+               } catch (IOException e) {
+                       Instance.getInstance().getTraceHandler().error(e);
+               }
+               return builder.toString();
+       }
+}