From: Niki Roo Date: Thu, 7 May 2020 20:28:23 +0000 (+0200) Subject: Merge branch 'subtree' X-Git-Url: http://git.nikiroo.be/?p=nikiroo-utils.git;a=commitdiff_plain;h=844d50dbf3ceb3480b0effc9085752de503856aa;hp=60e34c3416389a03970e55744a58bde0fa644689 Merge branch 'subtree' --- 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 2b8ff8f..877c8fa 100644 --- a/src/be/nikiroo/utils/ImageUtils.java +++ b/src/be/nikiroo/utils/ImageUtils.java @@ -44,40 +44,39 @@ public abstract class ImageUtils { /** * Scale a dimension. * - * @param areaWidth - * the base width of the target dimension for snap sizes - * @param areaHeight - * the base height of the target dimension for snap sizes + * * @param imageWidth * the actual image width * @param imageHeight * the actual image height + * @param 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, or -1 for snap size - * @param zoomSnapWidth - * if snap size, TRUE to snap to width (and FALSE, snap to - * height) + * 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 areaWidth, int areaHeight, - int imageWidth, int imageHeight, double zoom, boolean zoomSnapWidth) { + protected static Integer[] scaleSize(int imageWidth, int imageHeight, + int areaWidth, int areaHeight, double zoom, Boolean snapMode) { int width; int height; - if (zoom > 0) { + 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 { - if (zoomSnapWidth) { - width = areaWidth; - height = (int) Math.round( - (((double) areaWidth) / imageWidth) * imageHeight); - } else { - height = areaHeight; - width = (int) Math.round( - (((double) areaHeight) / imageHeight) * imageWidth); + height = areaHeight; + width = (int) Math + .round((((double) areaHeight) / imageHeight) * imageWidth); - } } if (width < 1) 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/ui/BreadCrumbsBar.java b/src/be/nikiroo/utils/ui/BreadCrumbsBar.java index da382f9..ed7e0bb 100644 --- a/src/be/nikiroo/utils/ui/BreadCrumbsBar.java +++ b/src/be/nikiroo/utils/ui/BreadCrumbsBar.java @@ -39,11 +39,12 @@ public class BreadCrumbsBar extends ListenerPanel { } } }); - + this.add(button, BorderLayout.CENTER); } - if (!node.getChildren().isEmpty()) { + 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 19c16a0..c273e0d 100644 --- a/src/be/nikiroo/utils/ui/ImageUtilsAwt.java +++ b/src/be/nikiroo/utils/ui/ImageUtilsAwt.java @@ -262,32 +262,69 @@ 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 zoom - * the zoom factor or -1 for snap size - * @param zoomSnapWidth - * if snap size, TRUE to snap to width (and FALSE, snap to - * height) + * @param targetSize + * the target size * * @return a new, resized image */ - public static BufferedImage scaleImage(Dimension areaSize, - BufferedImage image, double zoom, boolean zoomSnapWidth) { - Integer scaledSize[] = scaleSize(areaSize.width, areaSize.height, - image.getWidth(), image.getHeight(), zoom, zoomSnapWidth); - int width = scaledSize[0]; - int height = scaledSize[1]; - BufferedImage resizedImage = new BufferedImage(width, height, - BufferedImage.TYPE_4BYTE_ABGR); + 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, width, height, null); + g.drawImage(image, 0, 0, targetSize.width, targetSize.height, null); } finally { g.dispose(); } diff --git a/src/be/nikiroo/utils/ui/ListModel.java b/src/be/nikiroo/utils/ui/ListModel.java index d76c5dc..7cc23b8 100644 --- a/src/be/nikiroo/utils/ui/ListModel.java +++ b/src/be/nikiroo/utils/ui/ListModel.java @@ -115,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; @@ -132,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}. *

@@ -188,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, - final TooltipCreator tooltipCreator) { + public ListModel(final JList list) { this.list = list; - this.tooltipCreator = tooltipCreator; list.setModel(this); - final DelayWorker tooltipWatcher = new DelayWorker(DELAY_TOOLTIP_MS); - 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()); @@ -276,7 +171,7 @@ public class ListModel extends DefaultListModel6 { fireElementChanged(oldIndex); fireElementChanged(index); - if (tooltipCreator != null) { + if (ListModel.this.tooltipCreator != null) { showTooltip(null); tooltipWatcher.delay("tooltip", @@ -296,8 +191,9 @@ public class ListModel extends DefaultListModel6 { return; } - if (popup != null - && popup.isShowing()) { + if (ListModel.this.popup != null + && ListModel.this.popup + .isShowing()) { return; } @@ -322,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) { @@ -333,7 +230,7 @@ public class ListModel extends DefaultListModel6 { } private void check(MouseEvent e) { - if (popup == null) { + if (ListModel.this.popup == null) { return; } @@ -344,7 +241,7 @@ public class ListModel extends DefaultListModel6 { } showTooltip(null); - popup.show(list, e.getX(), e.getY()); + ListModel.this.popup.show(list, e.getX(), e.getY()); } } @@ -376,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. * @@ -586,7 +523,7 @@ public class ListModel extends DefaultListModel6 { } private void showTooltip(Window tooltip) { - synchronized (tooltipCreator) { + synchronized (tooltipWatcher) { if (this.tooltip != null) { this.tooltip.setVisible(false); this.tooltip.dispose(); diff --git a/src/be/nikiroo/utils/ui/NavBar.java b/src/be/nikiroo/utils/ui/NavBar.java index e99115c..607b2cf 100644 --- a/src/be/nikiroo/utils/ui/NavBar.java +++ b/src/be/nikiroo/utils/ui/NavBar.java @@ -1,10 +1,10 @@ package be.nikiroo.utils.ui; import java.awt.Dimension; -import java.awt.LayoutManager; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.Icon; import javax.swing.JButton; @@ -24,6 +24,7 @@ public class NavBar extends ListenerPanel { public static final String PAGE_CHANGED = "page changed"; private JTextField page; + private JLabel pageLabel; private JLabel maxPage; private JLabel label; @@ -32,6 +33,8 @@ public class NavBar extends ListenerPanel { private int max = 0; private String extraLabel = null; + private boolean vertical; + private JButton first; private JButton previous; private JButton next; @@ -40,9 +43,10 @@ public class NavBar extends ListenerPanel { /** * Create a new navigation bar. *

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

- * Note than 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) @@ -59,9 +63,6 @@ public class NavBar extends ListenerPanel { String.format("min (%d) > max (%d)", min, max)); } - LayoutManager layout = new BoxLayout(this, BoxLayout.X_AXIS); - setLayout(layout); - // Page navigation first = new JButton(); first.addActionListener(new ActionListener() { @@ -79,12 +80,11 @@ public class NavBar extends ListenerPanel { } }); + 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(new JButton("1234").getPreferredSize().width, - new JButton("dummy").getPreferredSize().height)); - page.setMaximumSize(new Dimension(Integer.MAX_VALUE, - page.getPreferredSize().height)); + page.setPreferredSize(new Dimension(width4, defaultHeight)); page.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -102,7 +102,11 @@ public class NavBar extends ListenerPanel { } }); + 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() { @@ -120,27 +124,19 @@ public class NavBar extends ListenerPanel { } }); + label = new JLabel(""); + // Set the << < > >> "icons" setIcons(null, null, null, null); - this.add(first); - this.add(previous); - this.add(new JLabel(" ")); - this.add(page); - this.add(new JLabel(" ")); - this.add(maxPage); - this.add(new JLabel(" ")); - this.add(next); - this.add(last); - - this.add(label = new JLabel("")); - this.min = min; this.max = max; this.index = min; updateEnabled(); updateLabel(); + setOrientation(vertical); + fireActionPerformed(PAGE_CHANGED); } @@ -345,11 +341,63 @@ public class NavBar extends ListenerPanel { 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)); } diff --git a/src/be/nikiroo/utils/ui/UIUtils.java b/src/be/nikiroo/utils/ui/UIUtils.java index 7d78d1e..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. @@ -94,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 @@ -234,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/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; + } +}