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=-c Merge commit '7ce18848c8327967ce27b90abf2e280953530b5f' --- 5ddc36eacad78641be59db473f9bae9bad47eb20 diff --combined src/be/nikiroo/utils/Downloader.java index 0487933,4191d0a..4191d0a --- a/src/be/nikiroo/utils/Downloader.java +++ b/src/be/nikiroo/utils/Downloader.java @@@ -39,7 -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 +52,7 @@@ * 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 +433,9 @@@ 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 --combined src/be/nikiroo/utils/ImageUtils.java index fb86929,877c8fa..877c8fa --- a/src/be/nikiroo/utils/ImageUtils.java +++ b/src/be/nikiroo/utils/ImageUtils.java @@@ -41,6 -41,52 +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 --combined src/be/nikiroo/utils/VersionCheck.java index 0000000,0e16c2b..0e16c2b mode 000000,100644..100644 --- a/src/be/nikiroo/utils/VersionCheck.java +++ b/src/be/nikiroo/utils/VersionCheck.java @@@ -1,0 -1,172 +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 --combined src/be/nikiroo/utils/resources/Bundle.java index c757e2b,84efcea..84efcea --- a/src/be/nikiroo/utils/resources/Bundle.java +++ b/src/be/nikiroo/utils/resources/Bundle.java @@@ -133,8 -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 +154,9 @@@ * 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 +164,7 @@@ rep = getMetaDef(id.name()); } - if (rep == null || rep.isEmpty()) { + if (rep.isEmpty()) { return def; } @@@ -273,8 -274,9 +274,9 @@@ * 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 +934,7 @@@ } /** - * 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 +1211,18 @@@ */ 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 --combined src/be/nikiroo/utils/ui/BreadCrumbsBar.java index a0e205c,ed7e0bb..ed7e0bb --- a/src/be/nikiroo/utils/ui/BreadCrumbsBar.java +++ b/src/be/nikiroo/utils/ui/BreadCrumbsBar.java @@@ -39,12 -39,13 +39,13 @@@ public class BreadCrumbsBar extends } } }); - + 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 +113,8 @@@ } }); + setSelectedNode(new DataNode(null, null)); + new SwingWorker, Void>() { @Override protected DataNode doInBackground() throws Exception { @@@ -122,7 -125,10 +125,10 @@@ @Override protected void done() { try { - node = get(); + DataNode node = get(); + + setSelectedNode(null); + BreadCrumbsBar.this.node = node; addCrumb(node); // TODO: option? diff --combined src/be/nikiroo/utils/ui/ImageUtilsAwt.java index 4cf12c0,c273e0d..c273e0d --- a/src/be/nikiroo/utils/ui/ImageUtilsAwt.java +++ b/src/be/nikiroo/utils/ui/ImageUtilsAwt.java @@@ -1,5 -1,7 +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 +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 +22,22 @@@ * @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 +95,26 @@@ * 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 +141,14 @@@ 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 +204,45 @@@ 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 +261,74 @@@ 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 --combined src/be/nikiroo/utils/ui/ListModel.java index cf16d5f,7cc23b8..7cc23b8 --- a/src/be/nikiroo/utils/ui/ListModel.java +++ b/src/be/nikiroo/utils/ui/ListModel.java @@@ -14,9 -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 +34,9 @@@ 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 +115,8 @@@ 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 +134,6 @@@ 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 +146,21 @@@ * 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 +172,8 @@@ fireElementChanged(index); if (ListModel.this.tooltipCreator != null) { + showTooltip(null); + tooltipWatcher.delay("tooltip", new SwingWorker() { @Override @@@ -284,18 -184,20 +184,20 @@@ @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 +218,8 @@@ @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 +230,7 @@@ } private void check(MouseEvent e) { - if (popup == null) { + if (ListModel.this.popup == null) { return; } @@@ -337,7 -240,8 +240,8 @@@ 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 +273,46 @@@ 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 +498,45 @@@ 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 --combined src/be/nikiroo/utils/ui/NavBar.java index 0000000,607b2cf..607b2cf mode 000000,100644..100644 --- a/src/be/nikiroo/utils/ui/NavBar.java +++ b/src/be/nikiroo/utils/ui/NavBar.java @@@ -1,0 -1,414 +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 --combined src/be/nikiroo/utils/ui/UIUtils.java index 5861d00,6c40389..6c40389 --- a/src/be/nikiroo/utils/ui/UIUtils.java +++ b/src/be/nikiroo/utils/ui/UIUtils.java @@@ -1,17 -1,30 +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 +33,45 @@@ */ 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 +107,9 @@@ * @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 +247,89 @@@ 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 --combined src/be/nikiroo/utils/ui/WaitingDialog.java index 0000000,0fd4574..0fd4574 mode 000000,100644..100644 --- a/src/be/nikiroo/utils/ui/WaitingDialog.java +++ b/src/be/nikiroo/utils/ui/WaitingDialog.java @@@ -1,0 -1,176 +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 --combined src/be/nikiroo/utils/ui/ZoomBox.java index 0000000,a8f9609..a8f9609 mode 000000,100644..100644 --- a/src/be/nikiroo/utils/ui/ZoomBox.java +++ b/src/be/nikiroo/utils/ui/ZoomBox.java @@@ -1,0 -1,477 +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 --combined src/be/nikiroo/utils/ui/compat/DefaultListModel6.java index 114ac42,3f7552f..3f7552f --- a/src/be/nikiroo/utils/ui/compat/DefaultListModel6.java +++ b/src/be/nikiroo/utils/ui/compat/DefaultListModel6.java @@@ -1,4 -1,4 +1,4 @@@ - package be.nikiroo.utils.compat; + package be.nikiroo.utils.ui.compat; import javax.swing.DefaultListModel; import javax.swing.JList; diff --combined src/be/nikiroo/utils/ui/compat/JList6.java index ca44165,a504abb..a504abb --- a/src/be/nikiroo/utils/ui/compat/JList6.java +++ b/src/be/nikiroo/utils/ui/compat/JList6.java @@@ -1,4 -1,4 +1,4 @@@ - package be.nikiroo.utils.compat; + package be.nikiroo.utils.ui.compat; import javax.swing.JList; import javax.swing.ListCellRenderer; diff --combined src/be/nikiroo/utils/ui/compat/ListCellRenderer6.java index d004849,bc76e80..bc76e80 --- a/src/be/nikiroo/utils/ui/compat/ListCellRenderer6.java +++ b/src/be/nikiroo/utils/ui/compat/ListCellRenderer6.java @@@ -1,4 -1,4 +1,4 @@@ - package be.nikiroo.utils.compat; + package be.nikiroo.utils.ui.compat; import java.awt.Component; diff --combined src/be/nikiroo/utils/ui/compat/ListModel6.java index a1f8c60,938da14..938da14 --- a/src/be/nikiroo/utils/ui/compat/ListModel6.java +++ b/src/be/nikiroo/utils/ui/compat/ListModel6.java @@@ -1,4 -1,4 +1,4 @@@ - package be.nikiroo.utils.compat; + package be.nikiroo.utils.ui.compat; import javax.swing.JList;