From 4d2056837eaad81199dfb2fbd727e65440371889 Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Wed, 22 Feb 2017 15:23:01 +0100 Subject: [PATCH 1/1] Improve UI, implement "Save as..." menu item --- src/be/nikiroo/fanfix/Main.java | 2 +- src/be/nikiroo/fanfix/bundles/StringId.java | 15 +- .../fanfix/bundles/resources.properties | 20 +- src/be/nikiroo/fanfix/output/BasicOutput.java | 14 +- .../fanfix/reader/LocalReaderBook.java | 115 +++++++--- .../fanfix/reader/LocalReaderFrame.java | 208 ++++++++++++++---- 6 files changed, 292 insertions(+), 82 deletions(-) diff --git a/src/be/nikiroo/fanfix/Main.java b/src/be/nikiroo/fanfix/Main.java index db4316e..d8803a5 100644 --- a/src/be/nikiroo/fanfix/Main.java +++ b/src/be/nikiroo/fanfix/Main.java @@ -456,7 +456,7 @@ public class Main { for (OutputType type : OutputType.values()) { builder.append(trans(StringId.ERR_SYNTAX_TYPE, type.toString(), - type.getDesc())); + type.getDesc(true))); builder.append('\n'); } diff --git a/src/be/nikiroo/fanfix/bundles/StringId.java b/src/be/nikiroo/fanfix/bundles/StringId.java index 014d6f0..bf496fb 100644 --- a/src/be/nikiroo/fanfix/bundles/StringId.java +++ b/src/be/nikiroo/fanfix/bundles/StringId.java @@ -90,8 +90,21 @@ public enum StringId { OUTPUT_DESC_CBZ, // @Meta(what = "output format description", where = "OutputType", format = "", info = "Description of this output type") OUTPUT_DESC_LATEX, // - @Meta(what = "output format description", where = "OutputType", format = "", info = "Description of this output type") + @Meta(what = "short output format description", where = "OutputType", format = "", info = "Description of this output type") OUTPUT_DESC_SYSOUT, // + OUTPUT_DESC_SHORT, // + @Meta(what = "short output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_SHORT_EPUB, // + @Meta(what = "short output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_SHORT_TEXT, // + @Meta(what = "short output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_SHORT_INFO_TEXT, // + @Meta(what = "short output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_SHORT_CBZ, // + @Meta(what = "short output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_SHORT_LATEX, // + @Meta(what = "short output format description", where = "OutputType", format = "", info = "Description of this output type") + OUTPUT_DESC_SHORT_SYSOUT, // @Meta(what = "error message", where = "LaTeX", format = "%s = the unknown 2-code language", info = "Error message for unknown 2-letter LaTeX language code") LATEX_LANG_UNKNOWN, // @Meta(what = "'by' prefix before author name", where = "", format = "", info = "used to output the author, make sure it is covered by Config.BYS for input detection") diff --git a/src/be/nikiroo/fanfix/bundles/resources.properties b/src/be/nikiroo/fanfix/bundles/resources.properties index afe07f4..baddc94 100644 --- a/src/be/nikiroo/fanfix/bundles/resources.properties +++ b/src/be/nikiroo/fanfix/bundles/resources.properties @@ -125,9 +125,27 @@ OUTPUT_DESC_CBZ = CBZ file (basically a ZIP file containing images -- we store t # (WHAT: output format description, WHERE: OutputType) # Description of this output type OUTPUT_DESC_LATEX = A LaTeX file using the "book" template -# (WHAT: output format description, WHERE: OutputType) +# (WHAT: short output format description, WHERE: OutputType) # Description of this output type OUTPUT_DESC_SYSOUT = A simple DEBUG console output +# (WHAT: short output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_SHORT_EPUB = Electronic book (.epub) +# (WHAT: short output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_SHORT_TEXT = Plain text (.txt) +# (WHAT: short output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_SHORT_INFO_TEXT = Plain text and metadata +# (WHAT: short output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_SHORT_CBZ = Comic book (.cbz) +# (WHAT: short output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_SHORT_LATEX = LaTeX (.tex) +# (WHAT: short output format description, WHERE: OutputType) +# Description of this output type +OUTPUT_DESC_SHORT_SYSOUT = Console output # (WHAT: error message, WHERE: LaTeX, FORMAT: %s = the unknown 2-code language) # Error message for unknown 2-letter LaTeX language code LATEX_LANG_UNKNOWN = Unknown language: %s diff --git a/src/be/nikiroo/fanfix/output/BasicOutput.java b/src/be/nikiroo/fanfix/output/BasicOutput.java index a21ee97..babe753 100644 --- a/src/be/nikiroo/fanfix/output/BasicOutput.java +++ b/src/be/nikiroo/fanfix/output/BasicOutput.java @@ -52,15 +52,19 @@ public abstract class BasicOutput { /** * A description of this output type. * + * @param longDesc + * TRUE for the long description, FALSE for the short one + * * @return the description */ - public String getDesc() { - String desc = Instance.getTrans().getStringX(StringId.OUTPUT_DESC, - this.name()); + public String getDesc(boolean longDesc) { + StringId id = longDesc ? StringId.OUTPUT_DESC + : StringId.OUTPUT_DESC_SHORT; + + String desc = Instance.getTrans().getStringX(id, this.name()); if (desc == null) { - desc = Instance.getTrans() - .getString(StringId.OUTPUT_DESC, this); + desc = Instance.getTrans().getString(id, this); } return desc; diff --git a/src/be/nikiroo/fanfix/reader/LocalReaderBook.java b/src/be/nikiroo/fanfix/reader/LocalReaderBook.java index 263601c..2c6d28b 100644 --- a/src/be/nikiroo/fanfix/reader/LocalReaderBook.java +++ b/src/be/nikiroo/fanfix/reader/LocalReaderBook.java @@ -19,6 +19,7 @@ import javax.swing.JLabel; import javax.swing.JPanel; import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.data.Story; /** * A book item presented in a {@link LocalReaderFrame}. @@ -59,6 +60,9 @@ class LocalReaderBook extends JPanel { public void popupRequested(LocalReaderBook book, MouseEvent e); } + private static final long serialVersionUID = 1L; + + // TODO: export some of the configuration options? private static final int COVER_WIDTH = 100; private static final int COVER_HEIGHT = 150; private static final int SPINE_WIDTH = 5; @@ -69,43 +73,55 @@ class LocalReaderBook extends JPanel { private static final int TEXT_WIDTH = COVER_WIDTH + 40; private static final int TEXT_HEIGHT = 50; private static final String AUTHOR_COLOR = "#888888"; - private static final long serialVersionUID = 1L; + private static final Color BORDER = Color.black; + private static final long doubleClickDelay = 200; // in ms + // private JLabel icon; private JLabel title; private boolean selected; private boolean hovered; private Date lastClick; - private long doubleClickDelay = 200; // in ms + private List listeners; private String luid; private boolean cached; + /** + * Create a new {@link LocalReaderBook} item for the givn {@link Story}. + * + * @param meta + * the story {@code}link MetaData} + * @param cached + * TRUE if it is locally cached + */ public LocalReaderBook(MetaData meta, boolean cached) { this.luid = meta.getLuid(); this.cached = cached; + BufferedImage resizedImage = new BufferedImage(SPINE_WIDTH + + COVER_WIDTH, SPINE_HEIGHT + COVER_HEIGHT + HOFFSET, + BufferedImage.TYPE_4BYTE_ABGR); + Graphics2D g = resizedImage.createGraphics(); + g.setColor(Color.white); + g.fillRect(0, HOFFSET, COVER_WIDTH, COVER_HEIGHT); if (meta.getCover() != null) { - BufferedImage resizedImage = new BufferedImage(SPINE_WIDTH - + COVER_WIDTH, SPINE_HEIGHT + COVER_HEIGHT + HOFFSET, - BufferedImage.TYPE_4BYTE_ABGR); - Graphics2D g = resizedImage.createGraphics(); - g.setColor(Color.white); - g.fillRect(0, HOFFSET, COVER_WIDTH, COVER_HEIGHT); g.drawImage(meta.getCover(), 0, HOFFSET, COVER_WIDTH, COVER_HEIGHT, null); - g.dispose(); - - icon = new JLabel(new ImageIcon(resizedImage)); } else { - // TODO: a big black "X" ? - icon = new JLabel(" [ no cover ] "); + g.setColor(Color.black); + g.drawLine(0, HOFFSET, COVER_WIDTH, HOFFSET + COVER_HEIGHT); + g.drawLine(COVER_WIDTH, HOFFSET, 0, HOFFSET + COVER_HEIGHT); } + g.dispose(); + + icon = new JLabel(new ImageIcon(resizedImage)); String optAuthor = meta.getAuthor(); if (optAuthor != null && !optAuthor.isEmpty()) { optAuthor = "(" + optAuthor + ")"; } + title = new JLabel( String.format( "" @@ -126,7 +142,7 @@ class LocalReaderBook extends JPanel { /** * The book current selection state. * - * @return the selected + * @return the selection state */ public boolean isSelected() { return selected; @@ -136,18 +152,28 @@ class LocalReaderBook extends JPanel { * The book current selection state. * * @param selected - * the selected to set + * TRUE if it is selected */ public void setSelected(boolean selected) { this.selected = selected; repaint(); } + /** + * The item mouse-hover state. + * + * @param hovered + * TRUE if it is mouse-hovered + */ private void setHovered(boolean hovered) { this.hovered = hovered; repaint(); } + /** + * Setup the mouse listener that will activate {@link BookActionListener} + * events. + */ private void setupListeners() { listeners = new ArrayList(); addMouseListener(new MouseListener() { @@ -204,51 +230,83 @@ class LocalReaderBook extends JPanel { }); } + /** + * Add a new {@link BookActionListener} on this item. + * + * @param listener + * the listener + */ public void addActionListener(BookActionListener listener) { listeners.add(listener); } + /** + * The Library UID of the book represented by this item. + * + * @return the LUID + */ public String getLuid() { return luid; } /** - * This boos is cached into the {@link LocalReader} library. + * This item {@link LocalReader} library cache state. * - * @return the cached + * @return TRUE if it is present in the {@link LocalReader} cache */ public boolean isCached() { return cached; } /** - * This boos is cached into the {@link LocalReader} library. + * This item {@link LocalReader} library cache state. * * @param cached - * the cached to set + * TRUE if it is present in the {@link LocalReader} cache */ public void setCached(boolean cached) { this.cached = cached; } + /** + * Draw a "cached" icon and a partially transparent overlay if needed + * depending upon the selection and mouse-hover states on top of the normal + * component. + */ @Override public void paint(Graphics g) { super.paint(g); + Rectangle clip = g.getClipBounds(); + if (clip.getWidth() <= 0 || clip.getHeight() <= 0) { + return; + } + int h = COVER_HEIGHT; int w = COVER_WIDTH; - int xOffset = (TEXT_WIDTH - COVER_WIDTH) - 4; + int xOffset = (TEXT_WIDTH - COVER_WIDTH) - 1; + int yOffset = HOFFSET; + + if (BORDER != null) { + if (BORDER != null) { + g.setColor(BORDER); + g.drawRect(xOffset, yOffset, COVER_WIDTH, COVER_HEIGHT); + } + + xOffset++; + yOffset++; + } int[] xs = new int[] { xOffset, xOffset + SPINE_WIDTH, xOffset + w + SPINE_WIDTH, xOffset + w }; - int[] ys = new int[] { HOFFSET + h, HOFFSET + h + SPINE_HEIGHT, - HOFFSET + h + SPINE_HEIGHT, HOFFSET + h }; + int[] ys = new int[] { yOffset + h, yOffset + h + SPINE_HEIGHT, + yOffset + h + SPINE_HEIGHT, yOffset + h }; g.setColor(SPINE_COLOR_BOTTOM); g.fillPolygon(new Polygon(xs, ys, xs.length)); xs = new int[] { xOffset + w, xOffset + w + SPINE_WIDTH, xOffset + w + SPINE_WIDTH, xOffset + w }; - ys = new int[] { HOFFSET, HOFFSET + SPINE_HEIGHT, - HOFFSET + h + SPINE_HEIGHT, HOFFSET + h }; + ys = new int[] { yOffset, yOffset + SPINE_HEIGHT, + yOffset + h + SPINE_HEIGHT, yOffset + h }; g.setColor(SPINE_COLOR_RIGHT); g.fillPolygon(new Polygon(xs, ys, xs.length)); @@ -262,13 +320,12 @@ class LocalReaderBook extends JPanel { color = new Color(200, 200, 255, 100); } - Rectangle clip = g.getClipBounds(); + g.setColor(color); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + if (cached) { g.setColor(Color.green); - g.fillOval(clip.x + clip.width - 30, 10, 20, 20); + g.fillOval(COVER_WIDTH + HOFFSET + 30, 10, 20, 20); } - - g.setColor(color); - g.fillRect(clip.x, clip.y, clip.width, clip.height); } } diff --git a/src/be/nikiroo/fanfix/reader/LocalReaderFrame.java b/src/be/nikiroo/fanfix/reader/LocalReaderFrame.java index 81ebbef..985e104 100644 --- a/src/be/nikiroo/fanfix/reader/LocalReaderFrame.java +++ b/src/be/nikiroo/fanfix/reader/LocalReaderFrame.java @@ -3,16 +3,20 @@ package be.nikiroo.fanfix.reader; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Desktop; +import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; import java.awt.event.WindowEvent; import java.io.File; import java.io.IOException; +import java.net.URL; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import javax.swing.JFileChooser; import javax.swing.JFrame; @@ -24,15 +28,28 @@ import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.Library; import be.nikiroo.fanfix.bundles.UiConfig; import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.output.BasicOutput.OutputType; import be.nikiroo.fanfix.reader.LocalReaderBook.BookActionListener; import be.nikiroo.utils.Progress; import be.nikiroo.utils.ui.ProgressBar; import be.nikiroo.utils.ui.WrapLayout; +/** + * A {@link Frame} that will show a {@link LocalReaderBook} item for each + * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a + * way to copy them to the {@link LocalReader} cache ({@link LocalReader#lib}), + * read them, delete them... + * + * @author niki + */ class LocalReaderFrame extends JFrame { private static final long serialVersionUID = 1L; private LocalReader reader; @@ -45,6 +62,15 @@ class LocalReaderFrame extends JFrame { private JMenuBar bar; private LocalReaderBook selectedBook; + /** + * Create a new {@link LocalReaderFrame}. + * + * @param reader + * the associated {@link LocalReader} to forward some commands + * and access its {@link Library} + * @param type + * the type of {@link Story} to load, or NULL for all types + */ public LocalReaderFrame(LocalReader reader, String type) { super("Fanfix Library"); @@ -77,6 +103,12 @@ class LocalReaderFrame extends JFrame { setVisible(true); } + /** + * Refresh the list of {@link LocalReaderBook}s from disk. + * + * @param type + * the type of {@link Story} to load, or NULL for all types + */ private void refreshBooks(String type) { this.type = type; stories = Instance.getLibrary().getList(type); @@ -89,37 +121,8 @@ class LocalReaderFrame extends JFrame { book.setBackground(color); } - book.addMouseListener(new MouseListener() { - public void mouseReleased(MouseEvent e) { - if (e.isPopupTrigger()) - pop(e); - } - - public void mousePressed(MouseEvent e) { - if (e.isPopupTrigger()) - pop(e); - } - - public void mouseExited(MouseEvent e) { - } - - public void mouseEntered(MouseEvent e) { - } - - public void mouseClicked(MouseEvent e) { - } - - private void pop(MouseEvent e) { - JPopupMenu popup = new JPopupMenu(); - popup.add(createMenuItemExport()); - popup.add(createMenuItemRefresh()); - popup.addSeparator(); - popup.add(createMenuItemDelete()); - // popup.show(e.getComponent(), e.getX(), e.getY()); - } - }); - books.add(book); + book.addActionListener(new BookActionListener() { public void select(LocalReaderBook book) { selectedBook = book; @@ -151,6 +154,11 @@ class LocalReaderFrame extends JFrame { bookPane.repaint(); } + /** + * Create the main menu bar. + * + * @return the bar + */ private JMenuBar createMenu() { bar = new JMenuBar(); @@ -221,22 +229,86 @@ class LocalReaderFrame extends JFrame { return bar; } + /** + * Create the export menu item. + * + * @return the item + */ private JMenuItem createMenuItemExport() { - // TODO - final String notYet = "[TODO] not ready yet, but you can do it on command line, see: fanfix --help"; + final JFileChooser fc = new JFileChooser(); + fc.setAcceptAllFileFilterUsed(false); + + final Map filters = new HashMap(); + for (OutputType type : OutputType.values()) { + String ext = type.getDefaultExtension(false); + String desc = type.getDesc(false); + if (ext == null || ext.isEmpty()) { + filters.put(createAllFilter(desc), type); + } else { + filters.put(new FileNameExtensionFilter(desc, ext), type); + } + } + + // First the "ALL" filters, then, the extension filters + for (Entry entry : filters.entrySet()) { + if (!(entry.getKey() instanceof FileNameExtensionFilter)) { + fc.addChoosableFileFilter(entry.getKey()); + } + } + for (Entry entry : filters.entrySet()) { + if (entry.getKey() instanceof FileNameExtensionFilter) { + fc.addChoosableFileFilter(entry.getKey()); + } + } + // - JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_E); + JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S); export.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - JOptionPane.showMessageDialog(LocalReaderFrame.this, notYet); + if (selectedBook != null) { + fc.showDialog(LocalReaderFrame.this, "Save"); + final OutputType type = filters.get(fc.getFileFilter()); + final String path = fc.getSelectedFile().getAbsolutePath() + + type.getDefaultExtension(false); + final Progress pg = new Progress(); + outOfUi(pg, new Runnable() { + public void run() { + try { + Instance.getLibrary().export( + selectedBook.getLuid(), type, path, pg); + } catch (IOException e) { + Instance.syserr(e); + } + } + }); + } } }); return export; } + private FileFilter createAllFilter(final String desc) { + return new FileFilter() { + @Override + public String getDescription() { + return desc; + } + + @Override + public boolean accept(File f) { + return true; + } + }; + } + + /** + * Create the refresh (delete cache) menu item. + * + * @return the item + */ private JMenuItem createMenuItemRefresh() { - JMenuItem refresh = new JMenuItem("Refresh", KeyEvent.VK_R); + JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C); refresh.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (selectedBook != null) { @@ -258,6 +330,11 @@ class LocalReaderFrame extends JFrame { return refresh; } + /** + * Create the delete menu item. + * + * @return the item + */ private JMenuItem createMenuItemDelete() { JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D); delete.addActionListener(new ActionListener() { @@ -281,6 +358,11 @@ class LocalReaderFrame extends JFrame { return delete; } + /** + * Create the open menu item. + * + * @return the item + */ private JMenuItem createMenuItemOpenBook() { JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O); open.addActionListener(new ActionListener() { @@ -294,6 +376,12 @@ class LocalReaderFrame extends JFrame { return open; } + /** + * Open a {@link LocalReaderBook} item. + * + * @param book + * the {@link LocalReaderBook} to open + */ private void openBook(final LocalReaderBook book) { final Progress pg = new Progress(); outOfUi(pg, new Runnable() { @@ -335,16 +423,28 @@ class LocalReaderFrame extends JFrame { }); } + /** + * Process the given action out of the Swing UI thread and link the given + * {@link ProgressBar} to the action. + *

+ * The code will make sure that the {@link ProgressBar} (if not NULL) is set + * to done when the action is done. + * + * @param pg + * the {@link ProgressBar} or NULL + * @param run + * the action to run + */ private void outOfUi(final Progress pg, final Runnable run) { pgBar.setProgress(pg); SwingUtilities.invokeLater(new Runnable() { public void run() { - setAllEnabled(false); + setEnabled(false); pgBar.addActioListener(new ActionListener() { public void actionPerformed(ActionEvent e) { pgBar.setProgress(null); - setAllEnabled(true); + setEnabled(true); } }); } @@ -356,7 +456,7 @@ class LocalReaderFrame extends JFrame { if (pg == null) { SwingUtilities.invokeLater(new Runnable() { public void run() { - setAllEnabled(true); + setEnabled(true); } }); } else if (!pg.isDone()) { @@ -366,6 +466,12 @@ class LocalReaderFrame extends JFrame { }).start(); } + /** + * Import a {@link Story} into the main {@link Library}. + * + * @param askUrl + * TRUE for an {@link URL}, false for a {@link File} + */ private void imprt(boolean askUrl) { JFileChooser fc = new JFileChooser(); @@ -404,7 +510,7 @@ class LocalReaderFrame extends JFrame { e.getMessage(), JOptionPane.ERROR_MESSAGE); - setAllEnabled(true); + setEnabled(true); } else { refreshBooks(type); } @@ -415,19 +521,31 @@ class LocalReaderFrame extends JFrame { } } - public void setAllEnabled(boolean enabled) { + /** + * Enables or disables this component, depending on the value of the + * parameter b. An enabled component can respond to user input + * and generate events. Components are enabled initially by default. + *

+ * Disabling this component will also affect its children. + * + * @param b + * If true, this component is enabled; otherwise + * this component is disabled + */ + @Override + public void setEnabled(boolean b) { for (LocalReaderBook book : books) { - book.setEnabled(enabled); + book.setEnabled(b); book.validate(); book.repaint(); } - bar.setEnabled(enabled); - bookPane.setEnabled(enabled); + bar.setEnabled(b); + bookPane.setEnabled(b); bookPane.validate(); bookPane.repaint(); - setEnabled(enabled); + super.setEnabled(b); validate(); repaint(); } -- 2.27.0