Merge branch 'subtree'
authorNiki Roo <niki@nikiroo.be>
Thu, 7 May 2020 20:28:23 +0000 (22:28 +0200)
committerNiki Roo <niki@nikiroo.be>
Thu, 7 May 2020 20:28:23 +0000 (22:28 +0200)
src/be/nikiroo/utils/Downloader.java
src/be/nikiroo/utils/ImageUtils.java
src/be/nikiroo/utils/VersionCheck.java [new file with mode: 0644]
src/be/nikiroo/utils/ui/BreadCrumbsBar.java
src/be/nikiroo/utils/ui/ImageUtilsAwt.java
src/be/nikiroo/utils/ui/ListModel.java
src/be/nikiroo/utils/ui/NavBar.java
src/be/nikiroo/utils/ui/UIUtils.java
src/be/nikiroo/utils/ui/ZoomBox.java [new file with mode: 0644]

index 0487933295ec8c4902665d2bffcd81e57debda2c..4191d0aea0da85511e2741c50c2a6c83806f612f 100644 (file)
@@ -39,7 +39,7 @@ public class Downloader {
         *            the User-Agent to use to download the resources -- note that
         *            some websites require one, some actively blacklist real UAs
         *            like the one from wget, some whitelist a couple of browsers
-        *            only (!)
+        *            only (!) -- can be NULL
         */
        public Downloader(String UA) {
                this(UA, null);
@@ -52,7 +52,7 @@ public class Downloader {
         *            the User-Agent to use to download the resources -- note that
         *            some websites require one, some actively blacklist real UAs
         *            like the one from wget, some whitelist a couple of browsers
-        *            only (!)
+        *            only (!) -- can be NULL
         * @param cache
         *            the {@link Cache} to use for all access (can be NULL)
         */
@@ -433,7 +433,9 @@ public class Downloader {
                        conn.setRequestProperty("Cookie", cookies);
                }
 
-               conn.setRequestProperty("User-Agent", UA);
+               if (UA != null) {
+                       conn.setRequestProperty("User-Agent", UA);
+               }
                conn.setRequestProperty("Accept-Encoding", "gzip");
                conn.setRequestProperty("Accept", "*/*");
                conn.setRequestProperty("Charset", "utf-8");
index 2b8ff8fa9ec722056904d310343761f979285924..877c8fa8c26b6b8a1a585505cf70f4a37d3d4fc7 100644 (file)
@@ -44,40 +44,39 @@ public abstract class ImageUtils {
        /**
         * Scale a dimension.
         * 
-        * @param areaWidth
-        *            the base width of the target dimension for snap sizes
-        * @param areaHeight
-        *            the base height of the target dimension for snap sizes
+        * 
         * @param imageWidth
         *            the actual image width
         * @param imageHeight
         *            the actual image height
+        * @param areaWidth
+        *            the base width of the target dimension for snap sizes
+        * @param areaHeight
+        *            the base height of the target dimension for snap sizes
         * @param zoom
-        *            the zoom factor, or -1 for snap size
-        * @param zoomSnapWidth
-        *            if snap size, TRUE to snap to width (and FALSE, snap to
-        *            height)
+        *            the zoom factor (ignored on snap mode)
+        * @param snapMode
+        *            NULL for no snap mode, TRUE to snap to width and FALSE for
+        *            snap to height)
         * 
         * @return the scaled size, width is [0] and height is [1] (minimum is 1x1)
         */
-       protected static Integer[] scaleSize(int areaWidth, int areaHeight,
-                       int imageWidth, int imageHeight, double zoom, boolean zoomSnapWidth) {
+       protected static Integer[] scaleSize(int imageWidth, int imageHeight,
+                       int areaWidth, int areaHeight, double zoom, Boolean snapMode) {
                int width;
                int height;
-               if (zoom > 0) {
+               if (snapMode == null) {
                        width = (int) Math.round(imageWidth * zoom);
                        height = (int) Math.round(imageHeight * zoom);
+               } else if (snapMode) {
+                       width = areaWidth;
+                       height = (int) Math
+                                       .round((((double) areaWidth) / imageWidth) * imageHeight);
                } else {
-                       if (zoomSnapWidth) {
-                               width = areaWidth;
-                               height = (int) Math.round(
-                                               (((double) areaWidth) / imageWidth) * imageHeight);
-                       } else {
-                               height = areaHeight;
-                               width = (int) Math.round(
-                                               (((double) areaHeight) / imageHeight) * imageWidth);
+                       height = areaHeight;
+                       width = (int) Math
+                                       .round((((double) areaHeight) / imageHeight) * imageWidth);
 
-                       }
                }
 
                if (width < 1)
diff --git a/src/be/nikiroo/utils/VersionCheck.java b/src/be/nikiroo/utils/VersionCheck.java
new file mode 100644 (file)
index 0000000..0e16c2b
--- /dev/null
@@ -0,0 +1,172 @@
+package be.nikiroo.utils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Version checker: can check the current version of the program against a
+ * remote changelog, and list the missed updates and their description.
+ * 
+ * @author niki
+ */
+public class VersionCheck {
+       private static final String base = "https://github.com/${PROJECT}/raw/master/changelog${LANG}.md";
+       private static Downloader downloader = new Downloader(null);
+
+       private Version current;
+       private List<Version> newer;
+       private Map<Version, List<String>> changes;
+
+       /**
+        * Create a new {@link VersionCheck}.
+        * 
+        * @param current
+        *            the current version of the program
+        * @param newer
+        *            the list of available {@link Version}s newer the current one
+        * @param changes
+        *            the list of changes
+        */
+       private VersionCheck(Version current, List<Version> newer,
+                       Map<Version, List<String>> changes) {
+               this.current = current;
+               this.newer = newer;
+               this.changes = changes;
+       }
+
+       /**
+        * Check if there are more recent {@link Version}s of this program
+        * available.
+        * 
+        * @return TRUE if there is at least one
+        */
+       public boolean isNewVersionAvailable() {
+               return !newer.isEmpty();
+       }
+
+       /**
+        * The current {@link Version} of the program.
+        * 
+        * @return the current {@link Version}
+        */
+       public Version getCurrentVersion() {
+               return current;
+       }
+
+       /**
+        * The list of available {@link Version}s newer than the current one.
+        * 
+        * @return the newer {@link Version}s
+        */
+       public List<Version> getNewer() {
+               return newer;
+       }
+
+       /**
+        * The list of changes for each available {@link Version} newer than the
+        * current one.
+        * 
+        * @return the list of changes
+        */
+       public Map<Version, List<String>> getChanges() {
+               return changes;
+       }
+
+       /**
+        * Check if there are available {@link Version}s of this program more recent
+        * than the current one.
+        * 
+        * @param githubProject
+        *            the GitHub project to check on, for instance "nikiroo/fanfix"
+        * @param lang
+        *            the current locale, so we can try to get the changelog in the
+        *            correct language (can be NULL, will fetch the default
+        *            changelog)
+        * 
+        * @return a {@link VersionCheck}
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       public static VersionCheck check(String githubProject, Locale lang)
+                       throws IOException {
+               Version current = Version.getCurrentVersion();
+               List<Version> newer = new ArrayList<Version>();
+               Map<Version, List<String>> changes = new HashMap<Version, List<String>>();
+
+               // Use the right project:
+               String base = VersionCheck.base.replace("${PROJECT}", githubProject);
+
+               // Prepare the URLs according to the user's language (we take here
+               // "-fr_BE" as an example):
+               String fr = lang == null ? "" : "-" + lang.getLanguage();
+               String BE = lang == null ? ""
+                               : "_" + lang.getCountry().replace(".UTF8", "");
+               String urlFrBE = base.replace("${LANG}", fr + BE);
+               String urlFr = base.replace("${LANG}", "-" + fr);
+               String urlDefault = base.replace("${LANG}", "");
+
+               InputStream in = null;
+               for (String url : new String[] { urlFrBE, urlFr, urlDefault }) {
+                       try {
+                               in = downloader.open(new URL(url), false);
+                               break;
+                       } catch (IOException e) {
+                       }
+               }
+
+               if (in == null) {
+                       throw new IOException("No changelog found");
+               }
+
+               BufferedReader reader = new BufferedReader(
+                               new InputStreamReader(in, "UTF-8"));
+               try {
+                       Version version = new Version();
+                       for (String line = reader.readLine(); line != null; line = reader
+                                       .readLine()) {
+                               if (line.startsWith("## Version ")) {
+                                       version = new Version(
+                                                       line.substring("## Version ".length()));
+                                       if (version.isNewerThan(current)) {
+                                               newer.add(version);
+                                               changes.put(version, new ArrayList<String>());
+                                       } else {
+                                               version = new Version();
+                                       }
+                               } else if (!version.isEmpty() && !newer.isEmpty()
+                                               && !line.isEmpty()) {
+                                       List<String> ch = changes.get(newer.get(newer.size() - 1));
+                                       if (!ch.isEmpty() && !line.startsWith("- ")) {
+                                               int i = ch.size() - 1;
+                                               ch.set(i, ch.get(i) + " " + line.trim());
+                                       } else {
+                                               ch.add(line.substring("- ".length()).trim());
+                                       }
+                               }
+                       }
+               } finally {
+                       reader.close();
+               }
+
+               return new VersionCheck(current, newer, changes);
+       }
+       
+       @Override
+       public String toString() {
+               return String.format(
+                               "Version checker: version [%s], %d releases behind latest version [%s]", //
+                               current, //
+                               newer.size(), //
+                               newer.isEmpty() ? current : newer.get(newer.size() - 1)//
+               );
+       }
+}
index da382f9ceaeb9ef8c5638dca51c09576db75a79d..ed7e0bb0f9012d710ea113c9064117ec09c0759a 100644 (file)
@@ -39,11 +39,12 @@ public class BreadCrumbsBar<T> extends ListenerPanel {
                                                }
                                        }
                                });
-                               
+
                                this.add(button, BorderLayout.CENTER);
                        }
 
-                       if (!node.getChildren().isEmpty()) {
+                       if ((node.isRoot() && node.getChildren().isEmpty())
+                                       || !node.getChildren().isEmpty()) {
                                // TODO allow an image or ">", viewer
                                down = new JToggleButton(">");
                                final JPopupMenu popup = new JPopupMenu();
@@ -112,6 +113,8 @@ public class BreadCrumbsBar<T> extends ListenerPanel {
                        }
                });
 
+               setSelectedNode(new DataNode<T>(null, null));
+
                new SwingWorker<DataNode<T>, Void>() {
                        @Override
                        protected DataNode<T> doInBackground() throws Exception {
@@ -122,7 +125,10 @@ public class BreadCrumbsBar<T> extends ListenerPanel {
                        @Override
                        protected void done() {
                                try {
-                                       node = get();
+                                       DataNode<T> node = get();
+
+                                       setSelectedNode(null);
+                                       BreadCrumbsBar.this.node = node;
                                        addCrumb(node);
 
                                        // TODO: option?
index 19c16a01749eff75fc430d68222fb7e243b2f5b4..c273e0d45879628543dcab75f2c7c88f133224d9 100644 (file)
@@ -262,32 +262,69 @@ public class ImageUtilsAwt extends ImageUtils {
                return image;
        }
 
+       /**
+        * Scale a dimension.
+        * 
+        * @param imageSize
+        *            the actual image size
+        * @param areaSize
+        *            the base size of the target to get snap sizes for
+        * @param zoom
+        *            the zoom factor (ignored on snap mode)
+        * @param snapMode
+        *            NULL for no snap mode, TRUE to snap to width and FALSE for
+        *            snap to height)
+        * 
+        * @return the scaled (minimum is 1x1)
+        */
+       public static Dimension scaleSize(Dimension imageSize, Dimension areaSize,
+                       double zoom, Boolean snapMode) {
+               Integer[] sz = scaleSize(imageSize.width, imageSize.height,
+                               areaSize.width, areaSize.height, zoom, snapMode);
+               return new Dimension(sz[0], sz[1]);
+       }
+
        /**
         * Resize the given image.
         * 
+        * @param image
+        *            the image to resize
         * @param areaSize
         *            the base size of the target dimension for snap sizes
+        * @param zoom
+        *            the zoom factor (ignored on snap mode)
+        * @param snapMode
+        *            NULL for no snap mode, TRUE to snap to width and FALSE for
+        *            snap to height)
+        * 
+        * @return a new, resized image
+        */
+       public static BufferedImage scaleImage(BufferedImage image,
+                       Dimension areaSize, double zoom, Boolean snapMode) {
+               Dimension scaledSize = scaleSize(
+                               new Dimension(image.getWidth(), image.getHeight()), areaSize,
+                               zoom, snapMode);
+
+               return scaleImage(image, scaledSize);
+       }
+
+       /**
+        * Resize the given image.
+        * 
         * @param image
         *            the image to resize
-        * @param zoom
-        *            the zoom factor or -1 for snap size
-        * @param zoomSnapWidth
-        *            if snap size, TRUE to snap to width (and FALSE, snap to
-        *            height)
+        * @param targetSize
+        *            the target size
         * 
         * @return a new, resized image
         */
-       public static BufferedImage scaleImage(Dimension areaSize,
-                       BufferedImage image, double zoom, boolean zoomSnapWidth) {
-               Integer scaledSize[] = scaleSize(areaSize.width, areaSize.height,
-                               image.getWidth(), image.getHeight(), zoom, zoomSnapWidth);
-               int width = scaledSize[0];
-               int height = scaledSize[1];
-               BufferedImage resizedImage = new BufferedImage(width, height,
-                               BufferedImage.TYPE_4BYTE_ABGR);
+       public static BufferedImage scaleImage(BufferedImage image,
+                       Dimension targetSize) {
+               BufferedImage resizedImage = new BufferedImage(targetSize.width,
+                               targetSize.height, BufferedImage.TYPE_4BYTE_ABGR);
                Graphics2D g = resizedImage.createGraphics();
                try {
-                       g.drawImage(image, 0, 0, width, height, null);
+                       g.drawImage(image, 0, 0, targetSize.width, targetSize.height, null);
                } finally {
                        g.dispose();
                }
index d76c5dc2c169a595e6a90b48d41b110bac7d6a46..7cc23b8c34b78262b33b5dc065e525d6e2a1ce90 100644 (file)
@@ -115,6 +115,8 @@ public class ListModel<T> extends DefaultListModel6<T> {
        private List<T> items = new ArrayList<T>();
        private boolean keepSelection = true;
 
+       private DelayWorker tooltipWatcher;
+       private JPopupMenu popup;
        private TooltipCreator<T> tooltipCreator;
        private Window tooltip;
 
@@ -132,50 +134,6 @@ public class ListModel<T> extends DefaultListModel6<T> {
                this((JList) list);
        }
 
-       /**
-        * Create a new {@link ListModel}.
-        * 
-        * @param list
-        *            the {@link JList6} we will handle the data of (cannot be NULL)
-        * @param popup
-        *            the popup to use and keep track of (can be NULL)
-        */
-       @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
-       public ListModel(JList6<T> list, JPopupMenu popup) {
-               this((JList) list, popup);
-       }
-
-       /**
-        * Create a new {@link ListModel}.
-        * 
-        * @param list
-        *            the {@link JList6} we will handle the data of (cannot be NULL)
-        * @param tooltipCreator
-        *            use this if you want the list to display tooltips on hover
-        *            (can be NULL)
-        */
-       @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
-       public ListModel(JList6<T> list, TooltipCreator<T> tooltipCreator) {
-               this((JList) list, null, tooltipCreator);
-       }
-
-       /**
-        * Create a new {@link ListModel}.
-        * 
-        * @param list
-        *            the {@link JList6} we will handle the data of (cannot be NULL)
-        * @param popup
-        *            the popup to use and keep track of (can be NULL)
-        * @param tooltipCreator
-        *            use this if you want the list to display tooltips on hover
-        *            (can be NULL)
-        */
-       @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
-       public ListModel(JList6<T> list, JPopupMenu popup,
-                       TooltipCreator<T> tooltipCreator) {
-               this((JList) list, popup, tooltipCreator);
-       }
-
        /**
         * Create a new {@link ListModel}.
         * <p>
@@ -188,84 +146,21 @@ public class ListModel<T> extends DefaultListModel6<T> {
         *            must only contain elements of the type of this
         *            {@link ListModel})
         */
-       @SuppressWarnings("rawtypes") // JList<?> not compatible Java 1.6
-       public ListModel(JList list) {
-               this(list, null, null);
-       }
-
-       /**
-        * Create a new {@link ListModel}.
-        * <p>
-        * Note that you must take care of passing a {@link JList} that only handles
-        * elements of the type of this {@link ListModel} -- you can also use
-        * {@link ListModel#ListModel(JList6, JPopupMenu)} instead.
-        * 
-        * @param list
-        *            the {@link JList} we will handle the data of (cannot be NULL,
-        *            must only contain elements of the type of this
-        *            {@link ListModel})
-        * @param popup
-        *            the popup to use and keep track of (can be NULL)
-        */
-       @SuppressWarnings("rawtypes") // JList<?> not in Java 1.6
-       public ListModel(JList list, JPopupMenu popup) {
-               this(list, popup, null);
-       }
-
-       /**
-        * Create a new {@link ListModel}.
-        * <p>
-        * Note that you must take care of passing a {@link JList} that only handles
-        * elements of the type of this {@link ListModel} -- you can also use
-        * {@link ListModel#ListModel(JList6, JPopupMenu)} instead.
-        * 
-        * @param list
-        *            the {@link JList} we will handle the data of (cannot be NULL,
-        *            must only contain elements of the type of this
-        *            {@link ListModel})
-        * @param tooltipCreator
-        *            use this if you want the list to display tooltips on hover
-        *            (can be NULL)
-        */
-       @SuppressWarnings("rawtypes") // JList<?> not in Java 1.6
-       public ListModel(JList list, TooltipCreator<T> tooltipCreator) {
-               this(list, null, tooltipCreator);
-       }
-
-       /**
-        * Create a new {@link ListModel}.
-        * <p>
-        * Note that you must take care of passing a {@link JList} that only handles
-        * elements of the type of this {@link ListModel} -- you can also use
-        * {@link ListModel#ListModel(JList6, JPopupMenu)} instead.
-        * 
-        * @param list
-        *            the {@link JList} we will handle the data of (cannot be NULL,
-        *            must only contain elements of the type of this
-        *            {@link ListModel})
-        * @param popup
-        *            the popup to use and keep track of (can be NULL)
-        * @param tooltipCreator
-        *            use this if you want the list to display tooltips on hover
-        *            (can be NULL)
-        */
        @SuppressWarnings({ "unchecked", "rawtypes" }) // JList<?> not in Java 1.6
-       public ListModel(final JList list, final JPopupMenu popup,
-                       final TooltipCreator<T> tooltipCreator) {
+       public ListModel(final JList list) {
                this.list = list;
-               this.tooltipCreator = tooltipCreator;
 
                list.setModel(this);
 
-               final DelayWorker tooltipWatcher = new DelayWorker(DELAY_TOOLTIP_MS);
-               if (tooltipCreator != null) {
-                       tooltipWatcher.start();
-               }
+               // We always have it ready
+               tooltipWatcher = new DelayWorker(DELAY_TOOLTIP_MS);
+               tooltipWatcher.start();
 
                list.addMouseMotionListener(new MouseAdapter() {
                        @Override
                        public void mouseMoved(final MouseEvent me) {
-                               if (popup != null && popup.isShowing())
+                               if (ListModel.this.popup != null
+                                               && ListModel.this.popup.isShowing())
                                        return;
 
                                Point p = new Point(me.getX(), me.getY());
@@ -276,7 +171,7 @@ public class ListModel<T> extends DefaultListModel6<T> {
                                        fireElementChanged(oldIndex);
                                        fireElementChanged(index);
 
-                                       if (tooltipCreator != null) {
+                                       if (ListModel.this.tooltipCreator != null) {
                                                showTooltip(null);
 
                                                tooltipWatcher.delay("tooltip",
@@ -296,8 +191,9 @@ public class ListModel<T> extends DefaultListModel6<T> {
                                                                                        return;
                                                                                }
 
-                                                                               if (popup != null
-                                                                                               && popup.isShowing()) {
+                                                                               if (ListModel.this.popup != null
+                                                                                               && ListModel.this.popup
+                                                                                                               .isShowing()) {
                                                                                        return;
                                                                                }
 
@@ -322,7 +218,8 @@ public class ListModel<T> extends DefaultListModel6<T> {
 
                        @Override
                        public void mouseExited(MouseEvent e) {
-                               if (popup != null && popup.isShowing())
+                               if (ListModel.this.popup != null
+                                               && ListModel.this.popup.isShowing())
                                        return;
 
                                if (hoveredIndex > -1) {
@@ -333,7 +230,7 @@ public class ListModel<T> extends DefaultListModel6<T> {
                        }
 
                        private void check(MouseEvent e) {
-                               if (popup == null) {
+                               if (ListModel.this.popup == null) {
                                        return;
                                }
 
@@ -344,7 +241,7 @@ public class ListModel<T> extends DefaultListModel6<T> {
                                        }
 
                                        showTooltip(null);
-                                       popup.show(list, e.getX(), e.getY());
+                                       ListModel.this.popup.show(list, e.getX(), e.getY());
                                }
                        }
 
@@ -376,6 +273,46 @@ public class ListModel<T> extends DefaultListModel6<T> {
                this.keepSelection = keepSelection;
        }
 
+       /**
+        * The popup to use and keep track of (can be NULL).
+        * 
+        * @return the current popup
+        */
+       public JPopupMenu getPopup() {
+               return popup;
+       }
+
+       /**
+        * The popup to use and keep track of (can be NULL).
+        * 
+        * @param popup
+        *            the new popup
+        */
+       public void setPopup(JPopupMenu popup) {
+               this.popup = popup;
+       }
+
+       /**
+        * You can use a {@link TooltipCreator} if you want the list to display
+        * tooltips on mouse hover (can be NULL).
+        * 
+        * @return the current {@link TooltipCreator}
+        */
+       public TooltipCreator<T> getTooltipCreator() {
+               return tooltipCreator;
+       }
+
+       /**
+        * You can use a {@link TooltipCreator} if you want the list to display
+        * tooltips on mouse hover (can be NULL).
+        * 
+        * @param tooltipCreator
+        *            the new {@link TooltipCreator}
+        */
+       public void setTooltipCreator(TooltipCreator<T> tooltipCreator) {
+               this.tooltipCreator = tooltipCreator;
+       }
+
        /**
         * Check if this element is currently under the mouse.
         * 
@@ -586,7 +523,7 @@ public class ListModel<T> extends DefaultListModel6<T> {
        }
 
        private void showTooltip(Window tooltip) {
-               synchronized (tooltipCreator) {
+               synchronized (tooltipWatcher) {
                        if (this.tooltip != null) {
                                this.tooltip.setVisible(false);
                                this.tooltip.dispose();
index e99115c329ede86d6cd7919618d6b0346134d39e..607b2cff2c3918e9d27b14648ca570fb82c4b25c 100644 (file)
@@ -1,10 +1,10 @@
 package be.nikiroo.utils.ui;
 
 import java.awt.Dimension;
-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.Icon;
 import javax.swing.JButton;
@@ -24,6 +24,7 @@ public class NavBar extends ListenerPanel {
        public static final String PAGE_CHANGED = "page changed";
 
        private JTextField page;
+       private JLabel pageLabel;
        private JLabel maxPage;
        private JLabel label;
 
@@ -32,6 +33,8 @@ public class NavBar extends ListenerPanel {
        private int max = 0;
        private String extraLabel = null;
 
+       private boolean vertical;
+
        private JButton first;
        private JButton previous;
        private JButton next;
@@ -40,9 +43,10 @@ public class NavBar extends ListenerPanel {
        /**
         * Create a new navigation bar.
         * <p>
-        * The minimum must be lower or equal to the maximum.
+        * The minimum must be lower or equal to the maximum, but a max of "-1"
+        * means "infinite".
         * <p>
-        * Note than a max of "-1" means "infinite".
+        * A {@link NavBar#PAGE_CHANGED} event will be fired on startup.
         * 
         * @param min
         *            the minimum page number (cannot be negative)
@@ -59,9 +63,6 @@ public class NavBar extends ListenerPanel {
                                        String.format("min (%d) > max (%d)", min, max));
                }
 
-               LayoutManager layout = new BoxLayout(this, BoxLayout.X_AXIS);
-               setLayout(layout);
-
                // Page navigation
                first = new JButton();
                first.addActionListener(new ActionListener() {
@@ -79,12 +80,11 @@ public class NavBar extends ListenerPanel {
                        }
                });
 
+               final int defaultHeight = new JButton("dummy")
+                               .getPreferredSize().height;
+               final int width4 = new JButton("1234").getPreferredSize().width;
                page = new JTextField(Integer.toString(min));
-               page.setPreferredSize(
-                               new Dimension(new JButton("1234").getPreferredSize().width,
-                                               new JButton("dummy").getPreferredSize().height));
-               page.setMaximumSize(new Dimension(Integer.MAX_VALUE,
-                               page.getPreferredSize().height));
+               page.setPreferredSize(new Dimension(width4, defaultHeight));
                page.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
@@ -102,7 +102,11 @@ public class NavBar extends ListenerPanel {
                        }
                });
 
+               pageLabel = new JLabel(Integer.toString(min));
+               pageLabel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
+
                maxPage = new JLabel("of " + max);
+               maxPage.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
 
                next = new JButton();
                next.addActionListener(new ActionListener() {
@@ -120,27 +124,19 @@ public class NavBar extends ListenerPanel {
                        }
                });
 
+               label = new JLabel("");
+
                // Set the << < > >> "icons"
                setIcons(null, null, null, null);
 
-               this.add(first);
-               this.add(previous);
-               this.add(new JLabel(" "));
-               this.add(page);
-               this.add(new JLabel(" "));
-               this.add(maxPage);
-               this.add(new JLabel(" "));
-               this.add(next);
-               this.add(last);
-
-               this.add(label = new JLabel(""));
-
                this.min = min;
                this.max = max;
                this.index = min;
 
                updateEnabled();
                updateLabel();
+               setOrientation(vertical);
+
                fireActionPerformed(PAGE_CHANGED);
        }
 
@@ -345,11 +341,63 @@ public class NavBar extends ListenerPanel {
                this.last.setText(last == null ? ">>" : "");
        }
 
+       /**
+        * The general orientation of the component.
+        * 
+        * @return TRUE for vertical orientation, FALSE for horisontal orientation
+        */
+       public boolean getOrientation() {
+               return vertical;
+       }
+
+       /**
+        * Update the general orientation of the component.
+        * 
+        * @param vertical
+        *            TRUE for vertical orientation, FALSE for horisontal
+        *            orientation
+        * 
+        * @return TRUE if it changed something
+        */
+       public boolean setOrientation(boolean vertical) {
+               if (getWidth() == 0 || this.vertical != vertical) {
+                       this.vertical = vertical;
+
+                       BoxLayout layout = new BoxLayout(this,
+                                       vertical ? BoxLayout.Y_AXIS : BoxLayout.X_AXIS);
+                       this.removeAll();
+                       setLayout(layout);
+
+                       this.add(first);
+                       this.add(previous);
+                       if (vertical) {
+                               this.add(pageLabel);
+                       } else {
+                               this.add(page);
+                       }
+                       this.add(maxPage);
+                       this.add(next);
+                       this.add(last);
+
+                       if (!vertical) {
+                               this.add(label);
+                       }
+
+                       this.revalidate();
+                       this.repaint();
+
+                       return true;
+               }
+
+               return false;
+       }
+
        /**
         * Update the label displayed in the UI.
         */
        private void updateLabel() {
                label.setText(getExtraLabel());
+               pageLabel.setText(Integer.toString(index));
                page.setText(Integer.toString(index));
        }
 
index 7d78d1e8c38669b118dab12828da99c351091b8c..6c4038977d9ed001934944128c8632dbb6dfe301 100644 (file)
@@ -1,17 +1,30 @@
 package be.nikiroo.utils.ui;
 
 import java.awt.Color;
+import java.awt.Component;
+import java.awt.Desktop;
 import java.awt.GradientPaint;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.awt.Paint;
 import java.awt.RadialGradientPaint;
 import java.awt.RenderingHints;
+import java.io.IOException;
+import java.net.URISyntaxException;
 
 import javax.swing.JComponent;
+import javax.swing.JEditorPane;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
 import javax.swing.JScrollPane;
 import javax.swing.UIManager;
 import javax.swing.UnsupportedLookAndFeelException;
+import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.HyperlinkListener;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.utils.Version;
+import be.nikiroo.utils.VersionCheck;
 
 /**
  * Some Java Swing utilities.
@@ -94,9 +107,9 @@ public class UIUtils {
         * @param color
         *            the base colour
         * @param x
-        *            the X coordinate
+        *            the X coordinate of the upper left corner
         * @param y
-        *            the Y coordinate
+        *            the Y coordinate of the upper left corner
         * @param width
         *            the width radius
         * @param height
@@ -234,4 +247,89 @@ public class UIUtils {
 
                return scroll;
        }
+
+       /**
+        * Show a confirmation message to the user to show him the changes since
+        * last version.
+        * <p>
+        * HTML 3.2 supported, links included (the user browser will be launched if
+        * possible).
+        * <p>
+        * If this is already the latest version, a message will still be displayed.
+        * 
+        * @param parentComponent
+        *            determines the {@link java.awt.Frame} in which the dialog is
+        *            displayed; if <code>null</code>, or if the
+        *            <code>parentComponent</code> has no {@link java.awt.Frame}, a
+        *            default {@link java.awt.Frame} is used
+        * @param updates
+        *            the new version
+        * @param introText
+        *            an introduction text before the list of changes
+        * @param title
+        *            the title of the dialog
+        * 
+        * @return TRUE if the user clicked on OK, false if the dialog was dismissed
+        */
+       static public boolean showUpdatedDialog(Component parentComponent,
+                       VersionCheck updates, String introText, String title) {
+               
+               StringBuilder builder = new StringBuilder();
+               final JEditorPane updateMessage = new JEditorPane("text/html", "");
+               if (introText != null && !introText.isEmpty()) {
+                       builder.append(introText);
+                       builder.append("<br>");
+                       builder.append("<br>");
+               }
+               for (Version v : updates.getNewer()) {
+                       builder.append("\t<b>" //
+                                       + "Version " + v.toString() //
+                                       + "</b>");
+                       builder.append("<br>");
+                       builder.append("<ul>");
+                       for (String item : updates.getChanges().get(v)) {
+                               builder.append("<li>" + item + "</li>");
+                       }
+                       builder.append("</ul>");
+               }
+
+               // html content
+               updateMessage.setText("<html><body>" //
+                               + builder//
+                               + "</body></html>");
+
+               // handle link events
+               updateMessage.addHyperlinkListener(new HyperlinkListener() {
+                       @Override
+                       public void hyperlinkUpdate(HyperlinkEvent e) {
+                               if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED))
+                                       try {
+                                               Desktop.getDesktop().browse(e.getURL().toURI());
+                                       } catch (IOException ee) {
+                                               Instance.getInstance().getTraceHandler().error(ee);
+                                       } catch (URISyntaxException ee) {
+                                               Instance.getInstance().getTraceHandler().error(ee);
+                                       }
+                       }
+               });
+               updateMessage.setEditable(false);
+               updateMessage.setBackground(new JLabel().getBackground());
+               updateMessage.addHyperlinkListener(new HyperlinkListener() {
+                       @Override
+                       public void hyperlinkUpdate(HyperlinkEvent evn) {
+                               if (evn.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
+                                       if (Desktop.isDesktopSupported()) {
+                                               try {
+                                                       Desktop.getDesktop().browse(evn.getURL().toURI());
+                                               } catch (IOException e) {
+                                               } catch (URISyntaxException e) {
+                                               }
+                                       }
+                               }
+                       }
+               });
+
+               return JOptionPane.showConfirmDialog(parentComponent, updateMessage,
+                               title, JOptionPane.DEFAULT_OPTION) == JOptionPane.OK_OPTION;
+       }
 }
diff --git a/src/be/nikiroo/utils/ui/ZoomBox.java b/src/be/nikiroo/utils/ui/ZoomBox.java
new file mode 100644 (file)
index 0000000..a8f9609
--- /dev/null
@@ -0,0 +1,477 @@
+package be.nikiroo.utils.ui;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+
+/**
+ * A small panel that let you choose a zoom level or an actual zoom value (when
+ * there is enough space to allow that).
+ * 
+ * @author niki
+ */
+public class ZoomBox extends ListenerPanel {
+       private static final long serialVersionUID = 1L;
+
+       /** The event that is fired on zoom change. */
+       public static final String ZOOM_CHANGED = "zoom_changed";
+
+       private enum ZoomLevel {
+               FIT_TO_WIDTH(0, true), //
+               FIT_TO_HEIGHT(0, false), //
+               ACTUAL_SIZE(1, null), //
+               HALF_SIZE(0.5, null), //
+               DOUBLE_SIZE(2, null),//
+               ;
+
+               private final double zoom;
+               private final Boolean snapMode;
+
+               private ZoomLevel(double zoom, Boolean snapMode) {
+                       this.zoom = zoom;
+                       this.snapMode = snapMode;
+               }
+
+               public double getZoom() {
+                       return zoom;
+               }
+
+               public Boolean getSnapToWidth() {
+                       return snapMode;
+               }
+
+               /**
+                * Use default values that can be understood by a human.
+                */
+               @Override
+               public String toString() {
+                       switch (this) {
+                       case FIT_TO_WIDTH:
+                               return "Fit to width";
+                       case FIT_TO_HEIGHT:
+                               return "Fit to height";
+                       case ACTUAL_SIZE:
+                               return "Actual size";
+                       case HALF_SIZE:
+                               return "Half size";
+                       case DOUBLE_SIZE:
+                               return "Double size";
+                       }
+                       return super.toString();
+               }
+
+               static ZoomLevel[] values(boolean orderedSelection) {
+                       if (orderedSelection) {
+                               return new ZoomLevel[] { //
+                                               FIT_TO_WIDTH, //
+                                               FIT_TO_HEIGHT, //
+                                               ACTUAL_SIZE, //
+                                               HALF_SIZE, //
+                                               DOUBLE_SIZE,//
+                               };
+                       }
+
+                       return values();
+               }
+       }
+
+       private boolean vertical;
+       private boolean small;
+
+       private JButton zoomIn;
+       private JButton zoomOut;
+       private JButton snapWidth;
+       private JButton snapHeight;
+       private JLabel zoomLabel;
+
+       @SuppressWarnings("rawtypes") // JComboBox<?> is not java 1.6 compatible
+       private JComboBox zoombox;
+
+       private double zoom = 1;
+       private Boolean snapMode = true;
+
+       @SuppressWarnings("rawtypes") // JComboBox<?> not compatible java 1.6
+       private DefaultComboBoxModel zoomBoxModel;
+
+       /**
+        * Create a new {@link ZoomBox}.
+        */
+       @SuppressWarnings({ "unchecked", "rawtypes" }) // JComboBox<?> not
+                                                                                                       // compatible java 1.6
+       public ZoomBox() {
+               zoomIn = new JButton();
+               zoomIn.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               zoomIn(1);
+                       }
+               });
+
+               zoomBoxModel = new DefaultComboBoxModel(ZoomLevel.values(true));
+               zoombox = new JComboBox(zoomBoxModel);
+               zoombox.setEditable(true);
+               zoombox.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               Object selected = zoomBoxModel.getSelectedItem();
+
+                               if (selected == null) {
+                                       return;
+                               }
+
+                               if (selected instanceof ZoomLevel) {
+                                       ZoomLevel selectedZoomLevel = (ZoomLevel) selected;
+                                       setZoomSnapMode(selectedZoomLevel.getZoom(),
+                                                       selectedZoomLevel.getSnapToWidth());
+                               } else {
+                                       String selectedString = selected.toString();
+                                       selectedString = selectedString.trim();
+                                       if (selectedString.endsWith("%")) {
+                                               selectedString = selectedString
+                                                               .substring(0, selectedString.length() - 1)
+                                                               .trim();
+                                       }
+
+                                       try {
+                                               int pc = Integer.parseInt(selectedString);
+                                               if (pc <= 0) {
+                                                       throw new NumberFormatException("invalid");
+                                               }
+
+                                               setZoomSnapMode(pc / 100.0, null);
+                                       } catch (NumberFormatException nfe) {
+                                       }
+                               }
+
+                               fireActionPerformed(ZOOM_CHANGED);
+                       }
+               });
+
+               zoomOut = new JButton();
+               zoomOut.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               zoomOut(1);
+                       }
+               });
+
+               snapWidth = new JButton();
+               snapWidth.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setSnapMode(true);
+                       }
+               });
+
+               snapHeight = new JButton();
+               snapHeight.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               setSnapMode(false);
+                       }
+               });
+
+               zoomLabel = new JLabel();
+               zoomLabel.setBorder(BorderFactory.createEmptyBorder(10, 10, 0, 0));
+
+               setIcons(null, null, null, null);
+               setOrientation(vertical);
+       }
+
+       /**
+        * The zoom level.
+        * <p>
+        * It usually returns 1 (default value), the value you passed yourself or 1
+        * (a snap to width or snap to height was asked by the user).
+        * <p>
+        * Will cause a fire event if needed.
+        * 
+        * @param zoom
+        *            the zoom level
+        */
+       public void setZoom(double zoom) {
+               if (this.zoom != zoom) {
+                       doSetZoom(zoom);
+                       fireActionPerformed(ZOOM_CHANGED);
+               }
+       }
+
+       /**
+        * The snap mode (NULL means no snap mode, TRUE for snap to width, FALSE for
+        * snap to height).
+        * <p>
+        * Will cause a fire event if needed.
+        * 
+        * @param snapToWidth
+        *            the snap mode
+        */
+       public void setSnapMode(Boolean snapToWidth) {
+               if (this.snapMode != snapToWidth) {
+                       doSetSnapMode(snapToWidth);
+                       fireActionPerformed(ZOOM_CHANGED);
+               }
+       }
+
+       /**
+        * Set both {@link ZoomBox#setZoom(double)} and
+        * {@link ZoomBox#setSnapMode(Boolean)} but fire only one change event.
+        * <p>
+        * Will cause a fire event if needed.
+        * 
+        * @param zoom
+        *            the zoom level
+        * @param snapMode
+        *            the snap mode
+        */
+       public void setZoomSnapMode(double zoom, Boolean snapMode) {
+               if (this.zoom != zoom || this.snapMode != snapMode) {
+                       doSetZoom(zoom);
+                       doSetSnapMode(snapMode);
+                       fireActionPerformed(ZOOM_CHANGED);
+               }
+       }
+
+       /**
+        * The zoom level.
+        * <p>
+        * It usually returns 1 (default value), the value you passed yourself or 0
+        * (a snap to width or snap to height was asked by the user).
+        * 
+        * @return the zoom level
+        */
+       public double getZoom() {
+               return zoom;
+       }
+
+       /**
+        * The snap mode (NULL means no snap mode, TRUE for snap to width, FALSE for
+        * snap to height).
+        * 
+        * @return the snap mode
+        */
+       public Boolean getSnapMode() {
+               return snapMode;
+       }
+
+       /**
+        * Zoom in, by a certain amount in "steps".
+        * <p>
+        * Note that zoomIn(-1) is the same as zoomOut(1).
+        * 
+        * @param steps
+        *            the number of zoom steps to make, can be negative
+        */
+       public void zoomIn(int steps) {
+               // TODO: redo zoomIn/zoomOut correctly
+               if (steps < 0) {
+                       zoomOut(-steps);
+                       return;
+               }
+
+               double newZoom = zoom;
+               for (int i = 0; i < steps; i++) {
+                       newZoom = newZoom + (newZoom < 0.1 ? 0.01 : 0.1);
+                       if (newZoom > 0.1) {
+                               newZoom = Math.round(newZoom * 10.0) / 10.0; // snap to 10%
+                       } else {
+                               newZoom = Math.round(newZoom * 100.0) / 100.0; // snap to 1%
+                       }
+               }
+
+               setZoomSnapMode(newZoom, null);
+               fireActionPerformed(ZOOM_CHANGED);
+       }
+
+       /**
+        * Zoom out, by a certain amount in "steps".
+        * <p>
+        * Note that zoomOut(-1) is the same as zoomIn(1).
+        * 
+        * @param steps
+        *            the number of zoom steps to make, can be negative
+        */
+       public void zoomOut(int steps) {
+               if (steps < 0) {
+                       zoomIn(-steps);
+                       return;
+               }
+
+               double newZoom = zoom;
+               for (int i = 0; i < steps; i++) {
+                       newZoom = newZoom - (newZoom > 0.19 ? 0.1 : 0.01);
+                       if (newZoom < 0.01) {
+                               newZoom = 0.01;
+                               break;
+                       }
+
+                       if (newZoom > 0.1) {
+                               newZoom = Math.round(newZoom * 10.0) / 10.0; // snap to 10%
+                       } else {
+                               newZoom = Math.round(newZoom * 100.0) / 100.0; // snap to 1%
+                       }
+               }
+
+               setZoomSnapMode(newZoom, null);
+               fireActionPerformed(ZOOM_CHANGED);
+       }
+
+       /**
+        * Set icons for the buttons instead of square brackets.
+        * <p>
+        * Any NULL value will make the button use square brackets again.
+        * 
+        * @param zoomIn
+        *            the icon of the button "go to first page"
+        * @param zoomOut
+        *            the icon of the button "go to previous page"
+        * @param snapWidth
+        *            the icon of the button "go to next page"
+        * @param snapHeight
+        *            the icon of the button "go to last page"
+        */
+       public void setIcons(Icon zoomIn, Icon zoomOut, Icon snapWidth,
+                       Icon snapHeight) {
+               this.zoomIn.setIcon(zoomIn);
+               this.zoomIn.setText(zoomIn == null ? "+" : "");
+               this.zoomOut.setIcon(zoomOut);
+               this.zoomOut.setText(zoomOut == null ? "-" : "");
+               this.snapWidth.setIcon(snapWidth);
+               this.snapWidth.setText(snapWidth == null ? "W" : "");
+               this.snapHeight.setIcon(snapHeight);
+               this.snapHeight.setText(snapHeight == null ? "H" : "");
+       }
+
+       /**
+        * A smaller {@link ZoomBox} that uses buttons instead of a big combo box
+        * for the zoom modes.
+        * <p>
+        * Always small in vertical orientation.
+        * 
+        * @return TRUE if it is small
+        */
+       public boolean getSmall() {
+               return small;
+       }
+
+       /**
+        * A smaller {@link ZoomBox} that uses buttons instead of a big combo box
+        * for the zoom modes.
+        * <p>
+        * Always small in vertical orientation.
+        * 
+        * @param small
+        *            TRUE to set it small
+        * 
+        * @return TRUE if it changed something
+        */
+       public boolean setSmall(boolean small) {
+               return setUi(small, vertical);
+       }
+
+       /**
+        * The general orientation of the component.
+        * 
+        * @return TRUE for vertical orientation, FALSE for horisontal orientation
+        */
+       public boolean getOrientation() {
+               return vertical;
+       }
+
+       /**
+        * The general orientation of the component.
+        * 
+        * @param vertical
+        *            TRUE for vertical orientation, FALSE for horisontal
+        *            orientation
+        * 
+        * @return TRUE if it changed something
+        */
+       public boolean setOrientation(boolean vertical) {
+               return setUi(small, vertical);
+       }
+
+       /**
+        * Set the zoom level, no fire event.
+        * <p>
+        * It usually returns 1 (default value), the value you passed yourself or 0
+        * (a snap to width or snap to height was asked by the user).
+        * 
+        * @param zoom
+        *            the zoom level
+        */
+       private void doSetZoom(double zoom) {
+               if (zoom > 0) {
+                       String zoomStr = Integer.toString((int) Math.round(zoom * 100))
+                                       + " %";
+                       zoomLabel.setText(zoomStr);
+                       if (snapMode == null) {
+                               zoomBoxModel.setSelectedItem(zoomStr);
+                       }
+               }
+
+               this.zoom = zoom;
+       }
+
+       /**
+        * Set the snap mode, no fire event.
+        * 
+        * @param snapToWidth
+        *            the snap mode
+        */
+       private void doSetSnapMode(Boolean snapToWidth) {
+               if (snapToWidth == null) {
+                       String zoomStr = Integer.toString((int) Math.round(zoom * 100))
+                                       + " %";
+                       if (zoom > 0) {
+                               zoomBoxModel.setSelectedItem(zoomStr);
+                       }
+               } else {
+                       for (ZoomLevel level : ZoomLevel.values()) {
+                               if (level.getSnapToWidth() == snapToWidth) {
+                                       zoomBoxModel.setSelectedItem(level);
+                               }
+                       }
+               }
+
+               this.snapMode = snapToWidth;
+       }
+
+       private boolean setUi(boolean small, boolean vertical) {
+               if (getWidth() == 0 || this.small != small
+                               || this.vertical != vertical) {
+                       this.small = small;
+                       this.vertical = vertical;
+
+                       BoxLayout layout = new BoxLayout(this,
+                                       vertical ? BoxLayout.Y_AXIS : BoxLayout.X_AXIS);
+                       this.removeAll();
+                       setLayout(layout);
+
+                       if (vertical || small) {
+                               this.add(zoomIn);
+                               this.add(snapWidth);
+                               this.add(snapHeight);
+                               this.add(zoomOut);
+                               this.add(zoomLabel);
+                       } else {
+                               this.add(zoomIn);
+                               this.add(zoombox);
+                               this.add(zoomOut);
+                       }
+
+                       this.revalidate();
+                       this.repaint();
+
+                       return true;
+               }
+
+               return false;
+       }
+}