From cafa199aae3b56e71524ccccfdd0ef19c924748c Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Fri, 24 Apr 2020 20:02:21 +0200 Subject: [PATCH] re-introduce the internal viewer --- src/be/nikiroo/fanfix_swing/Actions.java | 76 +++- .../nikiroo/fanfix_swing/gui/BooksPanel.java | 2 +- .../fanfix_swing/gui/book/BookPopup.java | 2 +- .../fanfix_swing/gui/viewer/NavBar.java | 343 ++++++++++++++++++ .../fanfix_swing/gui/viewer/Viewer.java | 168 +++++++++ .../fanfix_swing/gui/viewer/ViewerPanel.java | 301 +++++++++++++++ .../gui/viewer/ViewerTextOutput.java | 128 +++++++ 7 files changed, 999 insertions(+), 21 deletions(-) create mode 100644 src/be/nikiroo/fanfix_swing/gui/viewer/NavBar.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/viewer/Viewer.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/viewer/ViewerPanel.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/viewer/ViewerTextOutput.java diff --git a/src/be/nikiroo/fanfix_swing/Actions.java b/src/be/nikiroo/fanfix_swing/Actions.java index 4c26e08b..c020097c 100644 --- a/src/be/nikiroo/fanfix_swing/Actions.java +++ b/src/be/nikiroo/fanfix_swing/Actions.java @@ -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 worker = new SwingWorker() { 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. + *

+ * 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); } } diff --git a/src/be/nikiroo/fanfix_swing/gui/BooksPanel.java b/src/be/nikiroo/fanfix_swing/gui/BooksPanel.java index 4f169544..3ccac1fe 100644 --- a/src/be/nikiroo/fanfix_swing/gui/BooksPanel.java +++ b/src/be/nikiroo/fanfix_swing/gui/BooksPanel.java @@ -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() { diff --git a/src/be/nikiroo/fanfix_swing/gui/book/BookPopup.java b/src/be/nikiroo/fanfix_swing/gui/book/BookPopup.java index 3d874cc1..c067cfb4 100644 --- a/src/be/nikiroo/fanfix_swing/gui/book/BookPopup.java +++ b/src/be/nikiroo/fanfix_swing/gui/book/BookPopup.java @@ -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 index 00000000..46735f13 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/viewer/NavBar.java @@ -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 listeners = new ArrayList(); + + /** + * Create a new navigation bar. + *

+ * The minimum must be lower or equal to the maximum. + *

+ * 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 > 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. + *

+ * May update the index if needed (if the index is < the new min). + *

+ * 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). + *

+ * May update the index if needed (if the index is > the new max). + *

+ * 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. + *

+ * Swing HTML (HTML3) is supported if surrounded by <HTML> and + * </HTML>. + *

+ * 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 = "  Page %d "; + if (max >= 0) { + base += "/ %d"; + } + base += ""; + + String ifLabel = ": %s"; + + String display = base; + String label = getExtraLabel(); + if (label != null && !label.trim().isEmpty()) { + display += ifLabel; + } + + display = "" + display + ""; + + 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 index 00000000..fc70da4d --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/viewer/Viewer.java @@ -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. + *

+ * 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 "" + chapterDisplay + ""; + } + }; + + 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. + *

+ * 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 index 00000000..3832fa23 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/viewer/ViewerPanel.java @@ -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("    <    "); + left.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setImage(--currentImage); + } + }); + main.add(left, BorderLayout.WEST); + + right = new JButton("    >    "); + 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. + *

+ * Will always be text for a non-image document. + *

+ * 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. + *

+ * Do NOT 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' enabled 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. + *

+ * Do NOT 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 index 00000000..16d863ef --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/viewer/ViewerTextOutput.java @@ -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(""); + + builder.append("

"); + builder.append("Chapter "); + builder.append(chap.getNumber()); + builder.append(": "); + builder.append(chap.getName()); + builder.append("

"); + + builder.append("
"); + } + + @Override + protected void writeChapterFooter(Chapter chap) throws IOException { + if (paraInQuote) { + builder.append("
"); + } + paraInQuote = false; + + builder.append(""); + builder.append(""); + } + + @Override + protected void writeParagraph(Paragraph para) throws IOException { + if ((para.getType() == ParagraphType.QUOTE) == !paraInQuote) { + paraInQuote = !paraInQuote; + if (paraInQuote) { + builder.append("
"); + builder.append("
"); + } else { + builder.append("
"); + builder.append("
"); + } + } + + switch (para.getType()) { + case NORMAL: + builder.append("    "); + builder.append(decorateText(para.getContent())); + builder.append("
"); + break; + case BLANK: + builder.append("

"); + break; + case BREAK: + builder.append("

"); + builder.append("* * *"); + builder.append("



"); + break; + case QUOTE: + builder.append("
"); + builder.append("    "); + builder.append("— "); + builder.append(decorateText(para.getContent())); + builder.append("
"); + + break; + case IMAGE: + } + } + + @Override + protected String enbold(String word) { + return "" + word + ""; + } + + @Override + protected String italize(String word) { + return "" + word + ""; + } + }; + } + + /** + * 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(); + } +} -- 2.27.0