* 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");
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.
*
--- /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)//
+ );
+ }
+ }
* @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 <tt>def</tt> if not found (not present
+ * in the resource file)
*/
public String getString(E id, String def) {
return getString(id, def, -1);
* 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, <tt>def</tt> 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);
rep = getMetaDef(id.name());
}
- if (rep == null || rep.isEmpty()) {
+ if (rep.isEmpty()) {
return def;
}
* 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, <tt>def</tt> 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()
}
/**
- * 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")
*/
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) {
}
}
}
}
}
});
-
+
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();
}
});
+ 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?
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;
import javax.imageio.ImageIO;
+ import be.nikiroo.utils.IOUtils;
import be.nikiroo.utils.Image;
import be.nikiroo.utils.ImageUtils;
import be.nikiroo.utils.StringUtils;
* @author niki
*/
public class ImageUtilsAwt extends ImageUtils {
+ /**
+ * A rotation to perform on an image.
+ *
+ * @author niki
+ */
+ public enum Rotation {
+ /** No rotation */
+ NONE,
+ /** Rotate the image to the right */
+ RIGHT,
+ /** Rotate the image to the left */
+ LEFT,
+ /** Rotate the image by 180° */
+ UTURN
+ }
+
@Override
protected boolean check() {
// Will not work if ImageIO is not available
* 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 {
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";
}
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);
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;
+ }
}
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
public class ListModel<T> extends DefaultListModel6<T> {
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
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,
- TooltipCreator<T> 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());
fireElementChanged(index);
if (ListModel.this.tooltipCreator != null) {
+ showTooltip(null);
+
tooltipWatcher.delay("tooltip",
new SwingWorker<Void, Void>() {
@Override
@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));
}
});
}
@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;
}
list.locationToIndex(e.getPoint()));
}
- popup.show(list, e.getX(), e.getY());
+ showTooltip(null);
+ 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 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.
--- /dev/null
+ 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.
+ * <p>
+ * The minimum must be lower or equal to the maximum, but a max of "-1"
+ * means "infinite".
+ * <p>
+ * 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.
+ * <p>
+ * May update the index if needed (if the index is < the new min).
+ * <p>
+ * 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).
+ * <p>
+ * May update the index if needed (if the index is > the new max).
+ * <p>
+ * 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.
+ * <p>
+ * 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);
+ }
+ }
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.
*/
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).
* <p>
* <b>Must</b> 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).
+ * <p>
+ * <b>Must</b> 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;
}
/**
* @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.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<Void, Void>() {
+ @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).
+ * <p>
+ * Will also dispose the {@link WaitingDialog}.
+ */
+ public void dismiss() {
+ synchronized (waitLock) {
+ if (waitScreen) {
+ setVisible(false);
+ }
+ waitScreen = true;
+ }
+
+ if (pg != null) {
+ pg.removeProgressListener(pgl);
+ }
+
+ dispose();
+ }
+ }
--- /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;
+ }
+ }
- package be.nikiroo.utils.compat;
+ package be.nikiroo.utils.ui.compat;
import javax.swing.DefaultListModel;
import javax.swing.JList;
- package be.nikiroo.utils.compat;
+ package be.nikiroo.utils.ui.compat;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
- package be.nikiroo.utils.compat;
+ package be.nikiroo.utils.ui.compat;
import java.awt.Component;
- package be.nikiroo.utils.compat;
+ package be.nikiroo.utils.ui.compat;
import javax.swing.JList;