Merge branch 'subtree'
authorNiki Roo <niki@nikiroo.be>
Tue, 5 May 2020 20:39:36 +0000 (22:39 +0200)
committerNiki Roo <niki@nikiroo.be>
Tue, 5 May 2020 20:39:36 +0000 (22:39 +0200)
src/be/nikiroo/utils/ImageUtils.java
src/be/nikiroo/utils/ui/BreadCrumbsBar.java
src/be/nikiroo/utils/ui/ImageUtilsAwt.java
src/be/nikiroo/utils/ui/NavBar.java [new file with mode: 0644]
src/be/nikiroo/utils/ui/WaitingDialog.java [new file with mode: 0644]

index fb869294f2029bd1c0fd7f4a64680259ca4bc4b0..2b8ff8fa9ec722056904d310343761f979285924 100644 (file)
@@ -41,6 +41,53 @@ public abstract class ImageUtils {
        public abstract void saveAsImage(Image img, File target, String format)
                        throws IOException;
 
+       /**
+        * 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 zoom
+        *            the zoom factor, or -1 for snap size
+        * @param zoomSnapWidth
+        *            if snap size, TRUE to snap to width (and FALSE, 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) {
+               int width;
+               int height;
+               if (zoom > 0) {
+                       width = (int) Math.round(imageWidth * zoom);
+                       height = (int) Math.round(imageHeight * zoom);
+               } else {
+                       if (zoomSnapWidth) {
+                               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.
         * 
index a0e205c9d2152c6c8d0404a42217bdb42e420748..da382f9ceaeb9ef8c5638dca51c09576db75a79d 100644 (file)
@@ -44,7 +44,7 @@ public class BreadCrumbsBar<T> extends ListenerPanel {
                        }
 
                        if (!node.getChildren().isEmpty()) {
-                               // TODO (see things with icons included in viewer)
+                               // TODO allow an image or ">", viewer
                                down = new JToggleButton(">");
                                final JPopupMenu popup = new JPopupMenu();
 
index 4cf12c04cd0573629d90ed6e376be7c5ca6e7155..19c16a01749eff75fc430d68222fb7e243b2f5b4 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,37 @@ public class ImageUtilsAwt extends ImageUtils {
 
                return image;
        }
+
+       /**
+        * Resize the given image.
+        * 
+        * @param areaSize
+        *            the base size of the target dimension for snap sizes
+        * @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)
+        * 
+        * @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);
+               Graphics2D g = resizedImage.createGraphics();
+               try {
+                       g.drawImage(image, 0, 0, width, height, null);
+               } finally {
+                       g.dispose();
+               }
+
+               return resizedImage;
+       }
 }
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..e99115c
--- /dev/null
@@ -0,0 +1,366 @@
+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.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 maxPage;
+       private JLabel label;
+
+       private int index = 0;
+       private int min = 0;
+       private int max = 0;
+       private String extraLabel = null;
+
+       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.
+        * <p>
+        * Note than a max of "-1" means "infinite".
+        * 
+        * @param min
+        *            the minimum page number (cannot be negative)
+        * @param max
+        *            the maximum page number (cannot be lower than min, except if
+        *            -1 (infinite))
+        * 
+        * @throws IndexOutOfBoundsException
+        *             if min &gt; max and max is not "-1"
+        */
+       public NavBar(int min, int max) {
+               if (min > max && max != -1) {
+                       throw new IndexOutOfBoundsException(
+                                       String.format("min (%d) > max (%d)", min, max));
+               }
+
+               LayoutManager layout = new BoxLayout(this, BoxLayout.X_AXIS);
+               setLayout(layout);
+
+               // 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();
+                       }
+               });
+
+               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.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));
+                               }
+                       }
+               });
+
+               maxPage = new JLabel("of " + max);
+
+               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();
+                       }
+               });
+
+               // 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();
+               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 ? ">>" : "");
+       }
+
+       /**
+        * Update the label displayed in the UI.
+        */
+       private void updateLabel() {
+               label.setText(getExtraLabel());
+               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);
+       }
+}
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();
+       }
+}