Merge commit '7ce18848c8327967ce27b90abf2e280953530b5f'
authorNiki Roo <niki@nikiroo.be>
Thu, 7 May 2020 20:30:40 +0000 (22:30 +0200)
committerNiki Roo <niki@nikiroo.be>
Thu, 7 May 2020 20:30:40 +0000 (22:30 +0200)
15 files changed:
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/resources/Bundle.java
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 [new file with mode: 0644]
src/be/nikiroo/utils/ui/UIUtils.java
src/be/nikiroo/utils/ui/WaitingDialog.java [new file with mode: 0644]
src/be/nikiroo/utils/ui/ZoomBox.java [new file with mode: 0644]
src/be/nikiroo/utils/ui/compat/DefaultListModel6.java [moved from src/be/nikiroo/utils/compat/DefaultListModel6.java with 94% similarity]
src/be/nikiroo/utils/ui/compat/JList6.java [moved from src/be/nikiroo/utils/compat/JList6.java with 98% similarity]
src/be/nikiroo/utils/ui/compat/ListCellRenderer6.java [moved from src/be/nikiroo/utils/compat/ListCellRenderer6.java with 98% similarity]
src/be/nikiroo/utils/ui/compat/ListModel6.java [moved from src/be/nikiroo/utils/compat/ListModel6.java with 93% similarity]

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 fb869294f2029bd1c0fd7f4a64680259ca4bc4b0..877c8fa8c26b6b8a1a585505cf70f4a37d3d4fc7 100644 (file)
@@ -41,6 +41,52 @@ public abstract class ImageUtils {
        public abstract void saveAsImage(Image img, File target, String format)
                        throws IOException;
 
+       /**
+        * Scale a dimension.
+        * 
+        * 
+        * @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 (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 imageWidth, int imageHeight,
+                       int areaWidth, int areaHeight, double zoom, Boolean snapMode) {
+               int width;
+               int height;
+               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 {
+                       height = areaHeight;
+                       width = (int) Math
+                                       .round((((double) areaHeight) / imageHeight) * imageWidth);
+
+               }
+
+               if (width < 1)
+                       width = 1;
+               if (height < 1)
+                       height = 1;
+
+               return new Integer[] { width, height };
+       }
+       
        /**
         * Return the EXIF transformation flag of this image if any.
         * 
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 c757e2b1d705dc005b52ac425f7a9a89a14deabd..84efceafabfacac5d1edf887dc49f3542bd76725 100644 (file)
@@ -133,8 +133,8 @@ public class Bundle<E extends Enum<E>> {
         * @param def
         *            the default value when it is not present in the config file
         * 
-        * @return the associated value, or NULL if not found (not present in the
-        *         resource file)
+        * @return the associated value, or <tt>def</tt> if not found (not present
+        *         in the resource file)
         */
        public String getString(E id, String def) {
                return getString(id, def, -1);
@@ -154,8 +154,9 @@ public class Bundle<E extends Enum<E>> {
         *            the item number to get for an array of values, or -1 for
         *            non-arrays
         * 
-        * @return the associated value, or NULL if not found (not present in the
-        *         resource file)
+        * @return the associated value, <tt>def</tt> if not found (not present in
+        *         the resource file) or NULL if the item is specified (not -1) and
+        *         does not exist
         */
        public String getString(E id, String def, int item) {
                String rep = getString(id.name(), null);
@@ -163,7 +164,7 @@ public class Bundle<E extends Enum<E>> {
                        rep = getMetaDef(id.name());
                }
 
-               if (rep == null || rep.isEmpty()) {
+               if (rep.isEmpty()) {
                        return def;
                }
 
@@ -273,8 +274,9 @@ public class Bundle<E extends Enum<E>> {
         *            the item number to get for an array of values, or -1 for
         *            non-arrays
         * 
-        * @return the associated value, or NULL if not found (not present in the
-        *         resource file)
+        * @return the associated value, <tt>def</tt> if not found (not present in
+        *         the resource file), NULL if the item is specified (not -1) but
+        *         does not exist and NULL if bad key
         */
        public String getStringX(E id, String suffix, String def, int item) {
                String key = id.name()
@@ -932,7 +934,7 @@ public class Bundle<E extends Enum<E>> {
        }
 
        /**
-        * The default {@link MetaInfo.def} value for the given enumeration name.
+        * The default {@link Meta#def()} value for the given enumeration name.
         * 
         * @param id
         *            the enumeration name (the "id")
@@ -1209,22 +1211,18 @@ public class Bundle<E extends Enum<E>> {
         */
        protected void resetMap(ResourceBundle bundle) {
                this.map.clear();
-               for (Field field : type.getDeclaredFields()) {
-                       try {
-                               Meta meta = field.getAnnotation(Meta.class);
-                               if (meta != null) {
-                                       E id = Enum.valueOf(type, field.getName());
-
-                                       String value;
-                                       if (bundle != null) {
-                                               value = bundle.getString(id.name());
-                                       } else {
-                                               value = null;
+               if (bundle != null) {
+                       for (Field field : type.getDeclaredFields()) {
+                               try {
+                                       Meta meta = field.getAnnotation(Meta.class);
+                                       if (meta != null) {
+                                               E id = Enum.valueOf(type, field.getName());
+                                               String value = bundle.getString(id.name());
+                                               this.map.put(id.name(),
+                                                               value == null ? null : value.trim());
                                        }
-
-                                       this.map.put(id.name(), value == null ? null : value.trim());
+                               } catch (MissingResourceException e) {
                                }
-                       } catch (MissingResourceException e) {
                        }
                }
        }
index a0e205c9d2152c6c8d0404a42217bdb42e420748..ed7e0bb0f9012d710ea113c9064117ec09c0759a 100644 (file)
@@ -39,12 +39,13 @@ public class BreadCrumbsBar<T> extends ListenerPanel {
                                                }
                                        }
                                });
-                               
+
                                this.add(button, BorderLayout.CENTER);
                        }
 
-                       if (!node.getChildren().isEmpty()) {
-                               // TODO (see things with icons included in viewer)
+                       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 4cf12c04cd0573629d90ed6e376be7c5ca6e7155..c273e0d45879628543dcab75f2c7c88f133224d9 100644 (file)
@@ -1,5 +1,7 @@
 package be.nikiroo.utils.ui;
 
+import java.awt.Dimension;
+import java.awt.Graphics2D;
 import java.awt.geom.AffineTransform;
 import java.awt.image.AffineTransformOp;
 import java.awt.image.BufferedImage;
@@ -9,6 +11,7 @@ import java.io.InputStream;
 
 import javax.imageio.ImageIO;
 
+import be.nikiroo.utils.IOUtils;
 import be.nikiroo.utils.Image;
 import be.nikiroo.utils.ImageUtils;
 import be.nikiroo.utils.StringUtils;
@@ -19,6 +22,22 @@ import be.nikiroo.utils.StringUtils;
  * @author niki
  */
 public class ImageUtilsAwt extends ImageUtils {
+       /**
+        * A rotation to perform on an image.
+        * 
+        * @author niki
+        */
+       public enum Rotation {
+               /** No rotation */
+               NONE,
+               /** Rotate the image to the right */
+               RIGHT,
+               /** Rotate the image to the left */
+               LEFT,
+               /** Rotate the image by 180° */
+               UTURN
+       }
+
        @Override
        protected boolean check() {
                // Will not work if ImageIO is not available
@@ -76,6 +95,26 @@ public class ImageUtilsAwt extends ImageUtils {
         *             in case of IO error
         */
        public static BufferedImage fromImage(Image img) throws IOException {
+               return fromImage(img, Rotation.NONE);
+       }
+
+       /**
+        * Convert the given {@link Image} into a {@link BufferedImage} object,
+        * respecting the EXIF transformations if any.
+        * 
+        * @param img
+        *            the {@link Image}
+        * @param rotation
+        *            the rotation to apply, if any (can be null, same as
+        *            {@link Rotation#NONE})
+        * 
+        * @return the {@link Image} object
+        * 
+        * @throws IOException
+        *             in case of IO error
+        */
+       public static BufferedImage fromImage(Image img, Rotation rotation)
+                       throws IOException {
                InputStream in = img.newInputStream();
                BufferedImage image;
                try {
@@ -102,8 +141,14 @@ public class ImageUtilsAwt extends ImageUtils {
                                String extra = "";
                                if (img.getSize() <= 2048) {
                                        try {
-                                               extra = ", content: "
-                                                               + new String(img.getData(), "UTF-8");
+                                               byte[] data = null;
+                                               InputStream inData = img.newInputStream();
+                                               try {
+                                                       data = IOUtils.toByteArray(inData);
+                                               } finally {
+                                                       inData.close();
+                                               }
+                                               extra = ", content: " + new String(data, "UTF-8");
                                        } catch (Exception e) {
                                                extra = ", content unavailable";
                                        }
@@ -159,6 +204,45 @@ public class ImageUtilsAwt extends ImageUtils {
                                break;
                        }
 
+                       if (rotation == null)
+                               rotation = Rotation.NONE;
+
+                       switch (rotation) {
+                       case RIGHT:
+                               if (affineTransform == null) {
+                                       affineTransform = new AffineTransform();
+                               }
+                               affineTransform.translate(height, 0);
+                               affineTransform.rotate(Math.PI / 2);
+
+                               int tmp = width;
+                               width = height;
+                               height = tmp;
+
+                               break;
+                       case LEFT:
+                               if (affineTransform == null) {
+                                       affineTransform = new AffineTransform();
+                               }
+                               affineTransform.translate(0, width);
+                               affineTransform.rotate(3 * Math.PI / 2);
+
+                               int temp = width;
+                               width = height;
+                               height = temp;
+
+                               break;
+                       case UTURN:
+                               if (affineTransform == null) {
+                                       affineTransform = new AffineTransform();
+                               }
+                               affineTransform.translate(width, height);
+                               affineTransform.rotate(Math.PI);
+                               break;
+                       default:
+                               break;
+                       }
+
                        if (affineTransform != null) {
                                AffineTransformOp affineTransformOp = new AffineTransformOp(
                                                affineTransform, AffineTransformOp.TYPE_BILINEAR);
@@ -177,4 +261,74 @@ 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 targetSize
+        *            the target size
+        * 
+        * @return a new, resized image
+        */
+       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, targetSize.width, targetSize.height, null);
+               } finally {
+                       g.dispose();
+               }
+
+               return resizedImage;
+       }
 }
index cf16d5f0a111e660171102b3efad3bdefe1d6d48..7cc23b8c34b78262b33b5dc065e525d6e2a1ce90 100644 (file)
@@ -14,9 +14,9 @@ import javax.swing.JPopupMenu;
 import javax.swing.ListCellRenderer;
 import javax.swing.SwingWorker;
 
-import be.nikiroo.utils.compat.DefaultListModel6;
-import be.nikiroo.utils.compat.JList6;
-import be.nikiroo.utils.compat.ListCellRenderer6;
+import be.nikiroo.utils.ui.compat.DefaultListModel6;
+import be.nikiroo.utils.ui.compat.JList6;
+import be.nikiroo.utils.ui.compat.ListCellRenderer6;
 
 /**
  * A {@link javax.swing.ListModel} that can maintain 2 lists; one with the
@@ -34,6 +34,9 @@ import be.nikiroo.utils.compat.ListCellRenderer6;
 public class ListModel<T> extends DefaultListModel6<T> {
        private static final long serialVersionUID = 1L;
 
+       /** How long to wait before displaying a tooltip, in milliseconds. */
+       private static final int DELAY_TOOLTIP_MS = 1000;
+
        /**
         * A filter interface, to check for a condition (note that a Predicate class
         * already exists in Java 1.8+, and is compatible with this one if you
@@ -112,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;
 
@@ -129,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>
@@ -185,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,
-                       TooltipCreator<T> tooltipCreator) {
+       public ListModel(final JList list) {
                this.list = list;
-               this.tooltipCreator = tooltipCreator;
 
                list.setModel(this);
 
-               final DelayWorker tooltipWatcher = new DelayWorker(500);
-               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());
@@ -274,6 +172,8 @@ public class ListModel<T> extends DefaultListModel6<T> {
                                        fireElementChanged(index);
 
                                        if (ListModel.this.tooltipCreator != null) {
+                                               showTooltip(null);
+
                                                tooltipWatcher.delay("tooltip",
                                                                new SwingWorker<Void, Void>() {
                                                                        @Override
@@ -284,18 +184,20 @@ public class ListModel<T> extends DefaultListModel6<T> {
 
                                                                        @Override
                                                                        protected void done() {
-                                                                               Window oldTooltip = tooltip;
-                                                                               tooltip = null;
-                                                                               if (oldTooltip != null) {
-                                                                                       oldTooltip.setVisible(false);
-                                                                               }
+                                                                               showTooltip(null);
 
                                                                                if (index < 0
                                                                                                || index != hoveredIndex) {
                                                                                        return;
                                                                                }
 
-                                                                               tooltip = newTooltip(index, me);
+                                                                               if (ListModel.this.popup != null
+                                                                                               && ListModel.this.popup
+                                                                                                               .isShowing()) {
+                                                                                       return;
+                                                                               }
+
+                                                                               showTooltip(newTooltip(index, me));
                                                                        }
                                                                });
                                        }
@@ -316,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) {
@@ -327,7 +230,7 @@ public class ListModel<T> extends DefaultListModel6<T> {
                        }
 
                        private void check(MouseEvent e) {
-                               if (popup == null) {
+                               if (ListModel.this.popup == null) {
                                        return;
                                }
 
@@ -337,7 +240,8 @@ public class ListModel<T> extends DefaultListModel6<T> {
                                                                list.locationToIndex(e.getPoint()));
                                        }
 
-                                       popup.show(list, e.getX(), e.getY());
+                                       showTooltip(null);
+                                       ListModel.this.popup.show(list, e.getX(), e.getY());
                                }
                        }
 
@@ -369,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.
         * 
@@ -554,29 +498,45 @@ public class ListModel<T> extends DefaultListModel6<T> {
 
        private Window newTooltip(final int index, final MouseEvent me) {
                final T value = ListModel.this.get(index);
-
                final Window newTooltip = tooltipCreator.generateTooltip(value, true);
-
                if (newTooltip != null) {
                        newTooltip.addMouseListener(new MouseAdapter() {
                                @Override
                                public void mouseClicked(MouseEvent e) {
-
                                        Window promotedTooltip = tooltipCreator
                                                        .generateTooltip(value, false);
-                                       promotedTooltip.setLocation(newTooltip.getLocation());
+                                       if (promotedTooltip != null) {
+                                               promotedTooltip.setLocation(me.getXOnScreen(),
+                                                               me.getYOnScreen());
+                                               promotedTooltip.setVisible(true);
+                                       }
+
                                        newTooltip.setVisible(false);
-                                       promotedTooltip.setVisible(true);
                                }
                        });
-                       newTooltip.setLocation(me.getXOnScreen(), me.getYOnScreen());
 
-                       newTooltip.setVisible(true);
+                       newTooltip.setLocation(me.getXOnScreen(), me.getYOnScreen());
+                       showTooltip(newTooltip);
                }
 
                return newTooltip;
        }
 
+       private void showTooltip(Window tooltip) {
+               synchronized (tooltipWatcher) {
+                       if (this.tooltip != null) {
+                               this.tooltip.setVisible(false);
+                               this.tooltip.dispose();
+                       }
+
+                       this.tooltip = tooltip;
+
+                       if (tooltip != null) {
+                               tooltip.setVisible(true);
+                       }
+               }
+       }
+
        /**
         * Generate a {@link ListCellRenderer} that supports {@link Hoverable}
         * elements.
diff --git a/src/be/nikiroo/utils/ui/NavBar.java b/src/be/nikiroo/utils/ui/NavBar.java
new file mode 100644 (file)
index 0000000..607b2cf
--- /dev/null
@@ -0,0 +1,414 @@
+package be.nikiroo.utils.ui;
+
+import java.awt.Dimension;
+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;
+import javax.swing.JLabel;
+import javax.swing.JTextField;
+
+/**
+ * A Swing-based navigation bar, that displays first/previous/next/last page
+ * buttons.
+ * 
+ * @author niki
+ */
+public class NavBar extends ListenerPanel {
+       private static final long serialVersionUID = 1L;
+
+       /** The event that is fired on page change. */
+       public static final String PAGE_CHANGED = "page changed";
+
+       private JTextField page;
+       private JLabel pageLabel;
+       private JLabel maxPage;
+       private JLabel label;
+
+       private int index = 0;
+       private int min = 0;
+       private int max = 0;
+       private String extraLabel = null;
+
+       private boolean vertical;
+
+       private JButton first;
+       private JButton previous;
+       private JButton next;
+       private JButton last;
+
+       /**
+        * Create a new navigation bar.
+        * <p>
+        * The minimum must be lower or equal to the maximum, but a max of "-1"
+        * means "infinite".
+        * <p>
+        * A {@link NavBar#PAGE_CHANGED} event will be fired on startup.
+        * 
+        * @param min
+        *            the minimum page number (cannot be negative)
+        * @param max
+        *            the maximum page number (cannot be lower than min, except if
+        *            -1 (infinite))
+        * 
+        * @throws IndexOutOfBoundsException
+        *             if min &gt; max and max is not "-1"
+        */
+       public NavBar(int min, int max) {
+               if (min > max && max != -1) {
+                       throw new IndexOutOfBoundsException(
+                                       String.format("min (%d) > max (%d)", min, max));
+               }
+
+               // Page navigation
+               first = new JButton();
+               first.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               first();
+                       }
+               });
+
+               previous = new JButton();
+               previous.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               previous();
+                       }
+               });
+
+               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(width4, defaultHeight));
+               page.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               try {
+                                       int pageNb = Integer.parseInt(page.getText());
+                                       if (pageNb < NavBar.this.min || pageNb > NavBar.this.max) {
+                                               throw new NumberFormatException("invalid");
+                                       }
+
+                                       if (setIndex(pageNb))
+                                               fireActionPerformed(PAGE_CHANGED);
+                               } catch (NumberFormatException nfe) {
+                                       page.setText(Integer.toString(index));
+                               }
+                       }
+               });
+
+               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() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               next();
+                       }
+               });
+
+               last = new JButton();
+               last.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               last();
+                       }
+               });
+
+               label = new JLabel("");
+
+               // Set the << < > >> "icons"
+               setIcons(null, null, null, null);
+
+               this.min = min;
+               this.max = max;
+               this.index = min;
+
+               updateEnabled();
+               updateLabel();
+               setOrientation(vertical);
+
+               fireActionPerformed(PAGE_CHANGED);
+       }
+
+       /**
+        * 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, should be between {@link NavBar#min} and
+        * {@link NavBar#max}, both inclusive.
+        * 
+        * @param index
+        *            the new index
+        * 
+        * @return TRUE if the index changed, FALSE if not (either it was already at
+        *         that value, or it is outside of the bounds set by
+        *         {@link NavBar#min} and {@link NavBar#max})
+        */
+       public synchronized boolean setIndex(int index) {
+               if (index != this.index) {
+                       if (index < min || (index > max && max != -1)) {
+                               return false;
+                       }
+
+                       this.index = index;
+                       updateLabel();
+                       updateEnabled();
+
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * The minimun page number. Cannot be negative.
+        * 
+        * @return the min
+        */
+       public int getMin() {
+               return min;
+       }
+
+       /**
+        * The minimum page number. Cannot be negative.
+        * <p>
+        * May update the index if needed (if the index is &lt; the new min).
+        * <p>
+        * Will also (always) update the label and enable/disable the required
+        * buttons.
+        * 
+        * @param min
+        *            the new min
+        */
+       public synchronized void setMin(int min) {
+               this.min = min;
+               if (index < min) {
+                       index = min;
+               }
+
+               updateEnabled();
+               updateLabel();
+       }
+
+       /**
+        * The maximum page number. Cannot be lower than min, except if -1
+        * (infinite).
+        * 
+        * @return the max
+        */
+       public int getMax() {
+               return max;
+       }
+
+       /**
+        * The maximum page number. Cannot be lower than min, except if -1
+        * (infinite).
+        * <p>
+        * May update the index if needed (if the index is &gt; the new max).
+        * <p>
+        * Will also (always) update the label and enable/disable the required
+        * buttons.
+        * 
+        * @param max
+        *            the new max
+        */
+       public synchronized void setMax(int max) {
+               this.max = max;
+               if (index > max && max != -1) {
+                       index = max;
+               }
+
+               maxPage.setText("of " + max);
+               updateEnabled();
+               updateLabel();
+       }
+
+       /**
+        * The current extra label to display.
+        * 
+        * @return the current label
+        */
+       public String getExtraLabel() {
+               return extraLabel;
+       }
+
+       /**
+        * The current extra label to display.
+        * 
+        * @param currentLabel
+        *            the new current label
+        */
+       public void setExtraLabel(String currentLabel) {
+               this.extraLabel = currentLabel;
+               updateLabel();
+       }
+
+       /**
+        * Change the page to the next one.
+        * 
+        * @return TRUE if it changed
+        */
+       public synchronized boolean next() {
+               if (setIndex(index + 1)) {
+                       fireActionPerformed(PAGE_CHANGED);
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Change the page to the previous one.
+        * 
+        * @return TRUE if it changed
+        */
+       public synchronized boolean previous() {
+               if (setIndex(index - 1)) {
+                       fireActionPerformed(PAGE_CHANGED);
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Change the page to the first one.
+        * 
+        * @return TRUE if it changed
+        */
+       public synchronized boolean first() {
+               if (setIndex(min)) {
+                       fireActionPerformed(PAGE_CHANGED);
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Change the page to the last one.
+        * 
+        * @return TRUE if it changed
+        */
+       public synchronized boolean last() {
+               if (setIndex(max)) {
+                       fireActionPerformed(PAGE_CHANGED);
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Set icons for the buttons instead of square brackets.
+        * <p>
+        * Any NULL value will make the button use square brackets again.
+        * 
+        * @param first
+        *            the icon of the button "go to first page"
+        * @param previous
+        *            the icon of the button "go to previous page"
+        * @param next
+        *            the icon of the button "go to next page"
+        * @param last
+        *            the icon of the button "go to last page"
+        */
+       public void setIcons(Icon first, Icon previous, Icon next, Icon last) {
+               this.first.setIcon(first);
+               this.first.setText(first == null ? "<<" : "");
+               this.previous.setIcon(previous);
+               this.previous.setText(previous == null ? "<" : "");
+               this.next.setIcon(next);
+               this.next.setText(next == null ? ">" : "");
+               this.last.setIcon(last);
+               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));
+       }
+
+       /**
+        * Update the navigation buttons "enabled" state according to the current
+        * index value.
+        */
+       private synchronized void updateEnabled() {
+               first.setEnabled(index > min);
+               previous.setEnabled(index > min);
+               next.setEnabled(index < max || max == -1);
+               last.setEnabled(index < max || max == -1);
+       }
+}
index 5861d00fc3a6e3a254230c8bd5d43a17d26299db..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.
@@ -20,24 +33,45 @@ import javax.swing.UnsupportedLookAndFeelException;
  */
 public class UIUtils {
        /**
-        * Set a fake "native look &amp; feel" for the application if possible
+        * Set a fake "native Look &amp; Feel" for the application if possible
         * (check for the one currently in use, then try GTK).
         * <p>
         * <b>Must</b> be called prior to any GUI work.
+        * 
+        * @return TRUE if it succeeded
         */
-       static public void setLookAndFeel() {
+       static public boolean setLookAndFeel() {
                // native look & feel
+               String noLF = "javax.swing.plaf.metal.MetalLookAndFeel";
+               String lf = UIManager.getSystemLookAndFeelClassName();
+               if (lf.equals(noLF))
+                       lf = "com.sun.java.swing.plaf.gtk.GTKLookAndFeel";
+               
+               return setLookAndFeel(lf);
+       }
+
+       /**
+        * Switch to the given Look &amp; Feel for the application if possible
+        * (check for the one currently in use, then try GTK).
+        * <p>
+        * <b>Must</b> be called prior to any GUI work.
+        * 
+        * @param laf
+        *            the Look &amp; Feel to use
+        * 
+        * @return TRUE if it succeeded
+        */
+       static public boolean setLookAndFeel(String laf) {
                try {
-                       String noLF = "javax.swing.plaf.metal.MetalLookAndFeel";
-                       String lf = UIManager.getSystemLookAndFeelClassName();
-                       if (lf.equals(noLF))
-                               lf = "com.sun.java.swing.plaf.gtk.GTKLookAndFeel";
-                       UIManager.setLookAndFeel(lf);
+                       UIManager.setLookAndFeel(laf);
+                       return true;
                } catch (InstantiationException e) {
                } catch (ClassNotFoundException e) {
                } catch (UnsupportedLookAndFeelException e) {
                } catch (IllegalAccessException e) {
                }
+
+               return false;
        }
 
        /**
@@ -73,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
@@ -213,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/WaitingDialog.java b/src/be/nikiroo/utils/ui/WaitingDialog.java
new file mode 100644 (file)
index 0000000..0fd4574
--- /dev/null
@@ -0,0 +1,176 @@
+package be.nikiroo.utils.ui;
+
+import java.awt.Window;
+
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.SwingWorker;
+import javax.swing.border.EmptyBorder;
+
+import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.Progress.ProgressListener;
+
+/**
+ * A small waiting dialog that will show only if more than X milliseconds passed
+ * before we dismiss it.
+ * 
+ * @author niki
+ */
+public class WaitingDialog extends JDialog {
+       private static final long serialVersionUID = 1L;
+
+       private boolean waitScreen;
+       private Object waitLock = new Object();
+
+       private Progress pg;
+       private ProgressListener pgl;
+
+       /**
+        * Create a new {@link WaitingDialog}.
+        * 
+        * @param parent
+        *            the parent/owner of this {@link WaitingDialog}
+        * @param delayMs
+        *            the delay after which to show the dialog if it is still not
+        *            dismiss (see {@link WaitingDialog#dismiss()})
+        */
+       public WaitingDialog(Window parent, long delayMs) {
+               this(parent, delayMs, null, null);
+       }
+
+       /**
+        * Create a new {@link WaitingDialog}.
+        * 
+        * @param parent
+        *            the parent/owner of this {@link WaitingDialog}
+        * @param delayMs
+        *            the delay after which to show the dialog if it is still not
+        *            dismiss (see {@link WaitingDialog#dismiss()})
+        * @param pg
+        *            the {@link Progress} to listen on -- when it is
+        *            {@link Progress#done()}, this {@link WaitingDialog} will
+        *            automatically be dismissed as if
+        *            {@link WaitingDialog#dismiss()} was called
+        */
+       public WaitingDialog(Window parent, long delayMs, Progress pg) {
+               this(parent, delayMs, pg, null);
+       }
+
+       /**
+        * Create a new {@link WaitingDialog}.
+        * 
+        * @param parent
+        *            the parent/owner of this {@link WaitingDialog}
+        * @param delayMs
+        *            the delay after which to show the dialog if it is still not
+        *            dismiss (see {@link WaitingDialog#dismiss()})
+        * @param waitingText
+        *            a waiting text to display (note: you may want to subclass it
+        *            for nicer UI)
+        */
+       public WaitingDialog(Window parent, long delayMs, String waitingText) {
+               this(parent, delayMs, null, waitingText);
+       }
+
+       /**
+        * Create a new {@link WaitingDialog}.
+        * 
+        * @param parent
+        *            the parent/owner of this {@link WaitingDialog}
+        * @param delayMs
+        *            the delay after which to show the dialog if it is still not
+        *            dismiss (see {@link WaitingDialog#dismiss()})
+        * @param pg
+        *            the {@link Progress} to listen on -- when it is
+        *            {@link Progress#done()}, this {@link WaitingDialog} will
+        *            automatically be dismissed as if
+        *            {@link WaitingDialog#dismiss()} was called
+        * @param waitingText
+        *            a waiting text to display (note: you may want to subclass it
+        *            for nicer UI)
+        */
+       public WaitingDialog(Window parent, long delayMs, Progress pg,
+                       String waitingText) {
+               super(parent);
+
+               this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+
+               this.pg = pg;
+
+               if (waitingText != null) {
+                       JLabel waitingTextLabel = new JLabel(waitingText);
+                       this.add(waitingTextLabel);
+                       waitingTextLabel.setBorder(new EmptyBorder(10, 10, 10, 10));
+                       this.pack();
+               }
+
+               if (pg != null) {
+                       pgl = new ProgressListener() {
+                               @Override
+                               public void progress(Progress progress, String name) {
+                                       if (WaitingDialog.this.pg.isDone()) {
+                                               // Must be done out of this thread (cannot remove a pgl
+                                               // from a running pgl)
+                                               new SwingWorker<Void, Void>() {
+                                                       @Override
+                                                       protected Void doInBackground() throws Exception {
+                                                               return null;
+                                                       }
+
+                                                       @Override
+                                                       public void done() {
+                                                               dismiss();
+                                                       }
+                                               }.execute();
+                                       }
+                               }
+                       };
+
+                       pg.addProgressListener(pgl);
+
+                       if (pg.isDone()) {
+                               dismiss();
+                               return;
+                       }
+               }
+
+               final long delay = delayMs;
+               new Thread(new Runnable() {
+                       @Override
+                       public void run() {
+                               try {
+                                       Thread.sleep(delay);
+                               } catch (InterruptedException e) {
+                               }
+
+                               synchronized (waitLock) {
+                                       if (!waitScreen) {
+                                               waitScreen = true;
+                                               setVisible(true);
+                                       }
+                               }
+                       }
+               }).start();
+       }
+
+       /**
+        * Notify this {@link WaitingDialog} that the job is done, and dismiss it if
+        * it was already showing on screen (or never show it if it was not).
+        * <p>
+        * Will also dispose the {@link WaitingDialog}.
+        */
+       public void dismiss() {
+               synchronized (waitLock) {
+                       if (waitScreen) {
+                               setVisible(false);
+                       }
+                       waitScreen = true;
+               }
+
+               if (pg != null) {
+                       pg.removeProgressListener(pgl);
+               }
+
+               dispose();
+       }
+}
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;
+       }
+}
similarity index 94%
rename from src/be/nikiroo/utils/compat/DefaultListModel6.java
rename to src/be/nikiroo/utils/ui/compat/DefaultListModel6.java
index 114ac42eac73fd02ab7a9cbd8381ce5e00c00e5a..3f7552f879c7b42d32638a526ca5555a9f233a78 100644 (file)
@@ -1,4 +1,4 @@
-package be.nikiroo.utils.compat;
+package be.nikiroo.utils.ui.compat;
 
 import javax.swing.DefaultListModel;
 import javax.swing.JList;
similarity index 98%
rename from src/be/nikiroo/utils/compat/JList6.java
rename to src/be/nikiroo/utils/ui/compat/JList6.java
index ca44165067d0676ad62861c3005dc5452fb19469..a504abb0db7519ddd63ab45b105f7dd6496093e7 100644 (file)
@@ -1,4 +1,4 @@
-package be.nikiroo.utils.compat;
+package be.nikiroo.utils.ui.compat;
 
 import javax.swing.JList;
 import javax.swing.ListCellRenderer;
similarity index 98%
rename from src/be/nikiroo/utils/compat/ListCellRenderer6.java
rename to src/be/nikiroo/utils/ui/compat/ListCellRenderer6.java
index d00484982599dd6cd4e074743b1b6a3693ee3685..bc76e8002910fabe0022d0ff50525abe142c1853 100644 (file)
@@ -1,4 +1,4 @@
-package be.nikiroo.utils.compat;
+package be.nikiroo.utils.ui.compat;
 
 import java.awt.Component;
 
similarity index 93%
rename from src/be/nikiroo/utils/compat/ListModel6.java
rename to src/be/nikiroo/utils/ui/compat/ListModel6.java
index a1f8c60766a29a2e54913359f3d56308ae392d8a..938da143759dbb8fbac1998fdea67170f01c925d 100644 (file)
@@ -1,4 +1,4 @@
-package be.nikiroo.utils.compat;
+package be.nikiroo.utils.ui.compat;
 
 import javax.swing.JList;