From: Niki Roo Date: Tue, 5 May 2020 20:39:36 +0000 (+0200) Subject: Merge branch 'subtree' X-Git-Url: http://git.nikiroo.be/?p=nikiroo-utils.git;a=commitdiff_plain;h=26a1f3fe044c616802367e8a0f2cd4f099413ceb;hp=ae5672397c765fff97844f43d587faa7ccfef630 Merge branch 'subtree' --- diff --git a/src/be/nikiroo/utils/ImageUtils.java b/src/be/nikiroo/utils/ImageUtils.java index fb86929..2b8ff8f 100644 --- a/src/be/nikiroo/utils/ImageUtils.java +++ b/src/be/nikiroo/utils/ImageUtils.java @@ -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. * diff --git a/src/be/nikiroo/utils/ui/BreadCrumbsBar.java b/src/be/nikiroo/utils/ui/BreadCrumbsBar.java index a0e205c..da382f9 100644 --- a/src/be/nikiroo/utils/ui/BreadCrumbsBar.java +++ b/src/be/nikiroo/utils/ui/BreadCrumbsBar.java @@ -44,7 +44,7 @@ public class BreadCrumbsBar 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(); diff --git a/src/be/nikiroo/utils/ui/ImageUtilsAwt.java b/src/be/nikiroo/utils/ui/ImageUtilsAwt.java index 4cf12c0..19c16a0 100644 --- a/src/be/nikiroo/utils/ui/ImageUtilsAwt.java +++ b/src/be/nikiroo/utils/ui/ImageUtilsAwt.java @@ -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 index 0000000..e99115c --- /dev/null +++ b/src/be/nikiroo/utils/ui/NavBar.java @@ -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. + *

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

+ * Note than a max of "-1" means "infinite". + * + * @param min + * the minimum page number (cannot be negative) + * @param max + * the maximum page number (cannot be lower than min, except if + * -1 (infinite)) + * + * @throws IndexOutOfBoundsException + * if min > max and max is not "-1" + */ + public NavBar(int min, int max) { + if (min > max && max != -1) { + throw new IndexOutOfBoundsException( + String.format("min (%d) > max (%d)", min, max)); + } + + LayoutManager layout = new BoxLayout(this, BoxLayout.X_AXIS); + setLayout(layout); + + // 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. + *

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

+ * Will also (always) update the label and enable/disable the required + * buttons. + * + * @param min + * the new min + */ + public 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). + *

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

+ * Will also (always) update the label and enable/disable the required + * buttons. + * + * @param max + * the new max + */ + public 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. + *

+ * 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 index 0000000..0fd4574 --- /dev/null +++ b/src/be/nikiroo/utils/ui/WaitingDialog.java @@ -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() { + @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). + *

+ * Will also dispose the {@link WaitingDialog}. + */ + public void dismiss() { + synchronized (waitLock) { + if (waitScreen) { + setVisible(false); + } + waitScreen = true; + } + + if (pg != null) { + pg.removeProgressListener(pgl); + } + + dispose(); + } +}