* 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);
* 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)
*/
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");
/**
* 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)
--- /dev/null
+ 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<Version> newer;
+ private Map<Version, List<String>> 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<Version> newer,
+ Map<Version, List<String>> 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<Version> getNewer() {
+ return newer;
+ }
+
+ /**
+ * The list of changes for each available {@link Version} newer than the
+ * current one.
+ *
+ * @return the list of changes
+ */
+ public Map<Version, List<String>> 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<Version> newer = new ArrayList<Version>();
+ Map<Version, List<String>> changes = new HashMap<Version, List<String>>();
+
+ // 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<String>());
+ } else {
+ version = new Version();
+ }
+ } else if (!version.isEmpty() && !newer.isEmpty()
+ && !line.isEmpty()) {
+ List<String> 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)//
+ );
+ }
+ }
}
}
});
-
+
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();
}
});
+ setSelectedNode(new DataNode<T>(null, null));
+
new SwingWorker<DataNode<T>, Void>() {
@Override
protected DataNode<T> doInBackground() throws Exception {
@Override
protected void done() {
try {
- node = get();
+ DataNode<T> node = get();
+
+ setSelectedNode(null);
+ BreadCrumbsBar.this.node = node;
addCrumb(node);
// TODO: option?
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();
}
private List<T> items = new ArrayList<T>();
private boolean keepSelection = true;
+ private DelayWorker tooltipWatcher;
+ private JPopupMenu popup;
private TooltipCreator<T> tooltipCreator;
private Window tooltip;
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<T> 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<T> list, TooltipCreator<T> 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<T> list, JPopupMenu popup,
- TooltipCreator<T> tooltipCreator) {
- this((JList) list, popup, tooltipCreator);
- }
-
/**
* Create a new {@link ListModel}.
* <p>
* 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}.
- * <p>
- * 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}.
- * <p>
- * 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<T> tooltipCreator) {
- this(list, null, tooltipCreator);
- }
-
- /**
- * Create a new {@link ListModel}.
- * <p>
- * 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<T> 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());
fireElementChanged(oldIndex);
fireElementChanged(index);
- if (tooltipCreator != null) {
+ if (ListModel.this.tooltipCreator != null) {
showTooltip(null);
tooltipWatcher.delay("tooltip",
return;
}
- if (popup != null
- && popup.isShowing()) {
+ if (ListModel.this.popup != null
+ && ListModel.this.popup
+ .isShowing()) {
return;
}
@Override
public void mouseExited(MouseEvent e) {
- if (popup != null && popup.isShowing())
+ if (ListModel.this.popup != null
+ && ListModel.this.popup.isShowing())
return;
if (hoveredIndex > -1) {
}
private void check(MouseEvent e) {
- if (popup == null) {
+ if (ListModel.this.popup == null) {
return;
}
}
showTooltip(null);
- popup.show(list, e.getX(), e.getY());
+ ListModel.this.popup.show(list, e.getX(), e.getY());
}
}
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<T> 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<T> tooltipCreator) {
+ this.tooltipCreator = tooltipCreator;
+ }
+
/**
* Check if this element is currently under the mouse.
*
}
private void showTooltip(Window tooltip) {
- synchronized (tooltipCreator) {
+ synchronized (tooltipWatcher) {
if (this.tooltip != null) {
this.tooltip.setVisible(false);
this.tooltip.dispose();
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;
public static final String PAGE_CHANGED = "page changed";
private JTextField page;
+ private JLabel pageLabel;
private JLabel maxPage;
private JLabel label;
private int max = 0;
private String extraLabel = null;
+ private boolean vertical;
+
private JButton first;
private JButton previous;
private JButton next;
/**
* Create a new navigation bar.
* <p>
- * 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".
* <p>
- * 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)
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() {
}
});
+ 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) {
}
});
+ 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() {
}
});
+ 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);
}
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));
}
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.
* @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
return scroll;
}
+
+ /**
+ * Show a confirmation message to the user to show him the changes since
+ * last version.
+ * <p>
+ * HTML 3.2 supported, links included (the user browser will be launched if
+ * possible).
+ * <p>
+ * 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 <code>null</code>, or if the
+ * <code>parentComponent</code> 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("<br>");
+ builder.append("<br>");
+ }
+ for (Version v : updates.getNewer()) {
+ builder.append("\t<b>" //
+ + "Version " + v.toString() //
+ + "</b>");
+ builder.append("<br>");
+ builder.append("<ul>");
+ for (String item : updates.getChanges().get(v)) {
+ builder.append("<li>" + item + "</li>");
+ }
+ builder.append("</ul>");
+ }
+
+ // html content
+ updateMessage.setText("<html><body>" //
+ + builder//
+ + "</body></html>");
+
+ // 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;
+ }
}
--- /dev/null
+ 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.
+ * <p>
+ * 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).
+ * <p>
+ * 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).
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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".
+ * <p>
+ * 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".
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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;
+ }
+ }