From: Niki Roo Date: Thu, 7 May 2020 20:30:40 +0000 (+0200) Subject: Merge commit '7ce18848c8327967ce27b90abf2e280953530b5f' X-Git-Url: http://git.nikiroo.be/?p=nikiroo-utils.git;a=commitdiff_plain;h=5ddc36eacad78641be59db473f9bae9bad47eb20;hp=0cf9d0459e5e67ef3f97c57cb5acb51fb5da97d7 Merge commit '7ce18848c8327967ce27b90abf2e280953530b5f' --- diff --git a/src/be/nikiroo/utils/Downloader.java b/src/be/nikiroo/utils/Downloader.java index 0487933..4191d0a 100644 --- a/src/be/nikiroo/utils/Downloader.java +++ b/src/be/nikiroo/utils/Downloader.java @@ -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"); diff --git a/src/be/nikiroo/utils/ImageUtils.java b/src/be/nikiroo/utils/ImageUtils.java index fb86929..877c8fa 100644 --- a/src/be/nikiroo/utils/ImageUtils.java +++ b/src/be/nikiroo/utils/ImageUtils.java @@ -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 index 0000000..0e16c2b --- /dev/null +++ b/src/be/nikiroo/utils/VersionCheck.java @@ -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 newer; + private Map> 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 newer, + Map> 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 getNewer() { + return newer; + } + + /** + * The list of changes for each available {@link Version} newer than the + * current one. + * + * @return the list of changes + */ + public Map> 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 newer = new ArrayList(); + Map> changes = new HashMap>(); + + // 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()); + } else { + version = new Version(); + } + } else if (!version.isEmpty() && !newer.isEmpty() + && !line.isEmpty()) { + List 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)// + ); + } +} diff --git a/src/be/nikiroo/utils/resources/Bundle.java b/src/be/nikiroo/utils/resources/Bundle.java index c757e2b..84efcea 100644 --- a/src/be/nikiroo/utils/resources/Bundle.java +++ b/src/be/nikiroo/utils/resources/Bundle.java @@ -133,8 +133,8 @@ public class Bundle> { * @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 def 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> { * 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, def 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> { rep = getMetaDef(id.name()); } - if (rep == null || rep.isEmpty()) { + if (rep.isEmpty()) { return def; } @@ -273,8 +274,9 @@ public class Bundle> { * 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, def 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> { } /** - * 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> { */ 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) { } } } diff --git a/src/be/nikiroo/utils/ui/BreadCrumbsBar.java b/src/be/nikiroo/utils/ui/BreadCrumbsBar.java index a0e205c..ed7e0bb 100644 --- a/src/be/nikiroo/utils/ui/BreadCrumbsBar.java +++ b/src/be/nikiroo/utils/ui/BreadCrumbsBar.java @@ -39,12 +39,13 @@ public class BreadCrumbsBar 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 extends ListenerPanel { } }); + setSelectedNode(new DataNode(null, null)); + new SwingWorker, Void>() { @Override protected DataNode doInBackground() throws Exception { @@ -122,7 +125,10 @@ public class BreadCrumbsBar extends ListenerPanel { @Override protected void done() { try { - node = get(); + DataNode node = get(); + + setSelectedNode(null); + BreadCrumbsBar.this.node = node; addCrumb(node); // TODO: option? diff --git a/src/be/nikiroo/utils/ui/ImageUtilsAwt.java b/src/be/nikiroo/utils/ui/ImageUtilsAwt.java index 4cf12c0..c273e0d 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,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; + } } diff --git a/src/be/nikiroo/utils/ui/ListModel.java b/src/be/nikiroo/utils/ui/ListModel.java index cf16d5f..7cc23b8 100644 --- a/src/be/nikiroo/utils/ui/ListModel.java +++ b/src/be/nikiroo/utils/ui/ListModel.java @@ -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 extends DefaultListModel6 { 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 extends DefaultListModel6 { private List items = new ArrayList(); private boolean keepSelection = true; + private DelayWorker tooltipWatcher; + private JPopupMenu popup; private TooltipCreator tooltipCreator; private Window tooltip; @@ -129,50 +134,6 @@ public class ListModel extends DefaultListModel6 { 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 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 list, TooltipCreator 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 list, JPopupMenu popup, - TooltipCreator tooltipCreator) { - this((JList) list, popup, tooltipCreator); - } - /** * Create a new {@link ListModel}. *

@@ -185,84 +146,21 @@ public class ListModel extends DefaultListModel6 { * 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}. - *

- * 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}. - *

- * 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 tooltipCreator) { - this(list, null, tooltipCreator); - } - - /** - * Create a new {@link ListModel}. - *

- * 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 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 extends DefaultListModel6 { fireElementChanged(index); if (ListModel.this.tooltipCreator != null) { + showTooltip(null); + tooltipWatcher.delay("tooltip", new SwingWorker() { @Override @@ -284,18 +184,20 @@ public class ListModel extends DefaultListModel6 { @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 extends DefaultListModel6 { @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 extends DefaultListModel6 { } private void check(MouseEvent e) { - if (popup == null) { + if (ListModel.this.popup == null) { return; } @@ -337,7 +240,8 @@ public class ListModel extends DefaultListModel6 { 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 extends DefaultListModel6 { 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 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 tooltipCreator) { + this.tooltipCreator = tooltipCreator; + } + /** * Check if this element is currently under the mouse. * @@ -554,29 +498,45 @@ public class ListModel extends DefaultListModel6 { 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 index 0000000..607b2cf --- /dev/null +++ b/src/be/nikiroo/utils/ui/NavBar.java @@ -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. + *

+ * The minimum must be lower or equal to the maximum, but a max of "-1" + * means "infinite". + *

+ * A {@link NavBar#PAGE_CHANGED} event will be fired on startup. + * + * @param min + * the minimum page number (cannot be negative) + * @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)); + } + + // 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. + *

+ * 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 ? ">>" : ""); + } + + /** + * 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); + } +} diff --git a/src/be/nikiroo/utils/ui/UIUtils.java b/src/be/nikiroo/utils/ui/UIUtils.java index 5861d00..6c40389 100644 --- a/src/be/nikiroo/utils/ui/UIUtils.java +++ b/src/be/nikiroo/utils/ui/UIUtils.java @@ -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 & feel" for the application if possible + * Set a fake "native Look & Feel" for the application if possible * (check for the one currently in use, then try GTK). *

* Must 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 & Feel for the application if possible + * (check for the one currently in use, then try GTK). + *

+ * Must be called prior to any GUI work. + * + * @param laf + * the Look & 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. + *

+ * HTML 3.2 supported, links included (the user browser will be launched if + * possible). + *

+ * 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 null, or if the + * parentComponent 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("
"); + builder.append("
"); + } + for (Version v : updates.getNewer()) { + builder.append("\t" // + + "Version " + v.toString() // + + ""); + builder.append("
"); + builder.append("

    "); + for (String item : updates.getChanges().get(v)) { + builder.append("
  • " + item + "
  • "); + } + builder.append("
"); + } + + // html content + updateMessage.setText("" // + + builder// + + ""); + + // 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 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(); + } +} diff --git a/src/be/nikiroo/utils/ui/ZoomBox.java b/src/be/nikiroo/utils/ui/ZoomBox.java new file mode 100644 index 0000000..a8f9609 --- /dev/null +++ b/src/be/nikiroo/utils/ui/ZoomBox.java @@ -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. + *

+ * 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). + *

+ * 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). + *

+ * 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. + *

+ * 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. + *

+ * 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". + *

+ * 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". + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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; + } +} diff --git a/src/be/nikiroo/utils/compat/DefaultListModel6.java b/src/be/nikiroo/utils/ui/compat/DefaultListModel6.java similarity index 94% rename from src/be/nikiroo/utils/compat/DefaultListModel6.java rename to src/be/nikiroo/utils/ui/compat/DefaultListModel6.java index 114ac42..3f7552f 100644 --- a/src/be/nikiroo/utils/compat/DefaultListModel6.java +++ b/src/be/nikiroo/utils/ui/compat/DefaultListModel6.java @@ -1,4 +1,4 @@ -package be.nikiroo.utils.compat; +package be.nikiroo.utils.ui.compat; import javax.swing.DefaultListModel; import javax.swing.JList; diff --git a/src/be/nikiroo/utils/compat/JList6.java b/src/be/nikiroo/utils/ui/compat/JList6.java similarity index 98% rename from src/be/nikiroo/utils/compat/JList6.java rename to src/be/nikiroo/utils/ui/compat/JList6.java index ca44165..a504abb 100644 --- a/src/be/nikiroo/utils/compat/JList6.java +++ b/src/be/nikiroo/utils/ui/compat/JList6.java @@ -1,4 +1,4 @@ -package be.nikiroo.utils.compat; +package be.nikiroo.utils.ui.compat; import javax.swing.JList; import javax.swing.ListCellRenderer; diff --git a/src/be/nikiroo/utils/compat/ListCellRenderer6.java b/src/be/nikiroo/utils/ui/compat/ListCellRenderer6.java similarity index 98% rename from src/be/nikiroo/utils/compat/ListCellRenderer6.java rename to src/be/nikiroo/utils/ui/compat/ListCellRenderer6.java index d004849..bc76e80 100644 --- a/src/be/nikiroo/utils/compat/ListCellRenderer6.java +++ b/src/be/nikiroo/utils/ui/compat/ListCellRenderer6.java @@ -1,4 +1,4 @@ -package be.nikiroo.utils.compat; +package be.nikiroo.utils.ui.compat; import java.awt.Component; diff --git a/src/be/nikiroo/utils/compat/ListModel6.java b/src/be/nikiroo/utils/ui/compat/ListModel6.java similarity index 93% rename from src/be/nikiroo/utils/compat/ListModel6.java rename to src/be/nikiroo/utils/ui/compat/ListModel6.java index a1f8c60..938da14 100644 --- a/src/be/nikiroo/utils/compat/ListModel6.java +++ b/src/be/nikiroo/utils/ui/compat/ListModel6.java @@ -1,4 +1,4 @@ -package be.nikiroo.utils.compat; +package be.nikiroo.utils.ui.compat; import javax.swing.JList;