Merge with master
authorNiki Roo <niki@nikiroo.be>
Tue, 22 Dec 2020 13:32:34 +0000 (14:32 +0100)
committerNiki Roo <niki@nikiroo.be>
Tue, 22 Dec 2020 13:32:34 +0000 (14:32 +0100)
IOUtils.java
ui/Item.java [new file with mode: 0644]
ui/SearchBar.java [new file with mode: 0644]
ui/UIUtils.java
ui/clear-16x16.png [new file with mode: 0644]
ui/search-16x16.png [new file with mode: 0644]

index 3d252eac126df091a7fa8abf55999d95af6f288f..439964dfc11dee8a317bf4e63a5251deaef7f17f 100644 (file)
@@ -403,6 +403,34 @@ public class IOUtils {
 
                return loader.getResourceAsStream(name);
        }
+       
+       /**
+        * Return the running directory/file, that is, the root binary directory for
+        * running java classes or the running JAR file for JAR files.
+        * 
+        * @param clazz
+        *            a Class from the running program (will only have an impact
+        *            when not running from a JAR file)
+        * @param base
+        *            return the base directory (the one where the binary root or
+        *            the JAR file resides)
+        * 
+        * @return the directory or file
+        */
+       public static File getRunningDirectory(
+                       @SuppressWarnings("rawtypes") Class clazz, boolean base) {
+               String uri = clazz.getProtectionDomain().getCodeSource().getLocation()
+                               .toString();
+               if (uri.startsWith("file:"))
+                       uri = uri.substring("file:".length());
+               File root = new File(uri);
+
+               if (base) {
+                       root = root.getParentFile();
+               }
+
+               return root;
+       }
 
        /**
         * Return a resetable {@link InputStream} from this stream, and reset it.
diff --git a/ui/Item.java b/ui/Item.java
new file mode 100644 (file)
index 0000000..c8afc7f
--- /dev/null
@@ -0,0 +1,479 @@
+package be.nikiroo.utils.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.Rectangle;
+import java.awt.image.BufferedImage;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.BorderFactory;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.SwingConstants;
+
+/**
+ * A graphical item that can be presented in a list and supports user
+ * interaction.
+ * <p>
+ * Can be selected, hovered...
+ * 
+ * @author niki
+ */
+abstract public class Item extends JPanel {
+       static private final long serialVersionUID = 1L;
+
+       static private Map<Dimension, BufferedImage> empty = new HashMap<Dimension, BufferedImage>();
+       static private Map<Dimension, BufferedImage> error = new HashMap<Dimension, BufferedImage>();
+       static private Map<Color, JComponent> statuses = new HashMap<Color, JComponent>();
+
+       private String id;
+       private boolean selected;
+       private boolean hovered;
+
+       private String mainTemplate;
+       private String secondaryTemplate;
+
+       private boolean hasImage;
+       private JLabel title;
+       private JLabel secondary;
+       private JLabel statusIndicatorOn;
+       private JLabel statusIndicatorOff;
+       private JLabel statusIndicatorUnknown;
+       private Image image;
+       private boolean imageError;
+
+       private String cachedMain;
+       private String cachedOptSecondary;
+       private Integer cachedStatus;
+
+       /**
+        * Create a new {@link Item}
+        * 
+        * @param id
+        *            an ID that represents this {@link Item} (can be NULL)
+        * @param hasImage
+        *            this {@link Item} will contain an image
+        */
+       public Item(String id, boolean hasImage) {
+               this.id = id;
+               this.hasImage = hasImage;
+               init(hasImage);
+       }
+
+       // Configuration :
+
+       protected int getMaxDisplaySize() {
+               return 40;
+       }
+
+       protected int getCoverWidth() {
+               return 100;
+       }
+
+       protected int getCoverHeight() {
+               return 150;
+       }
+
+       protected int getTextWidth() {
+               return getCoverWidth() + 40;
+       }
+
+       protected int getTextHeight() {
+               return 50;
+       }
+
+       protected int getCoverVOffset() {
+               return 20;
+       }
+
+       protected int getCoverHOffset() {
+               return 0;
+       }
+
+       protected int getHGap() {
+               return 10;
+       }
+
+       /** Colour used for the secondary item (author/word count). */
+       protected Color getSecondaryColor() {
+               return new Color(128, 128, 128);
+       }
+
+       /**
+        * Return a display-ready version of the main information to show.
+        * <p>
+        * Note that you can make use of {@link Item#limit(String)}.
+        * 
+        * @return the main info in a ready-to-display version, cannot be NULL
+        */
+       abstract protected String getMainInfoDisplay();
+
+       /**
+        * Return a display-ready version of the secondary information to show.
+        * <p>
+        * Note that you can make use of {@link Item#limit(String)}.
+        * 
+        * @return the main info in a ready-to-display version, cannot be NULL
+        */
+       abstract protected String getSecondaryInfoDisplay();
+
+       /**
+        * The current status for the status indicator.
+        * <p>
+        * Note that NULL and negative values will create "hollow" indicators, while
+        * other values will create "filled" indicators.
+        * 
+        * @return the status which can be NULL, presumably for "Unknown"
+        */
+       abstract protected Integer getStatus();
+
+       /**
+        * Get the background colour to use according to the given state.
+        * <p>
+        * Since it is an overlay, an opaque colour will of course mask everything.
+        * 
+        * @param enabled
+        *            the item is enabled
+        * @param selected
+        *            the item is selected
+        * @param hovered
+        *            the mouse cursor currently hovers over the item
+        * 
+        * @return the correct background colour to use
+        */
+       abstract protected Color getOverlayColor(boolean enabled, boolean selected,
+                       boolean hovered);
+
+       /**
+        * Get the colour to use for the status indicator.
+        * <p>
+        * Return NULL if you don't want a status indicator for this state.
+        * 
+        * @param status
+        *            the current status as returned by {@link Item#getStatus()}
+        * 
+        * @return the base colour to use, or NULL for no status indicator
+        */
+       abstract protected Color getStatusIndicatorColor(Integer status);
+
+       /**
+        * Initialise this {@link Item}.
+        */
+       private void init(boolean hasImage) {
+               if (!hasImage) {
+                       title = new JLabel();
+                       mainTemplate = "${MAIN}";
+                       secondary = new JLabel();
+                       secondaryTemplate = "${SECONDARY}";
+                       secondary.setForeground(getSecondaryColor());
+
+                       JPanel idTitle = null;
+                       if (id != null && !id.isEmpty()) {
+                               JLabel idLabel = new JLabel(id);
+                               idLabel.setPreferredSize(new JLabel(" 999 ").getPreferredSize());
+                               idLabel.setForeground(Color.gray);
+                               idLabel.setHorizontalAlignment(SwingConstants.CENTER);
+
+                               idTitle = new JPanel(new BorderLayout());
+                               idTitle.setOpaque(false);
+                               idTitle.add(idLabel, BorderLayout.WEST);
+                               idTitle.add(title, BorderLayout.CENTER);
+                       }
+
+                       setLayout(new BorderLayout());
+                       if (idTitle != null)
+                               add(idTitle, BorderLayout.CENTER);
+                       add(secondary, BorderLayout.EAST);
+               } else {
+                       image = null;
+                       title = new JLabel();
+                       secondary = new JLabel();
+                       secondaryTemplate = "";
+
+                       String color = String.format("#%X%X%X", getSecondaryColor()
+                                       .getRed(), getSecondaryColor().getGreen(),
+                                       getSecondaryColor().getBlue());
+                       mainTemplate = String
+                                       .format("<html>"
+                                                       + "<body style='width: %d px; height: %d px; text-align: center;'>"
+                                                       + "${MAIN}" + "<br>" + "<span style='color: %s;'>"
+                                                       + "${SECONDARY}" + "</span>" + "</body>"
+                                                       + "</html>", getTextWidth(), getTextHeight(), color);
+
+                       int ww = Math.max(getCoverWidth(), getTextWidth());
+                       int hh = getCoverHeight() + getCoverVOffset() + getHGap()
+                                       + getTextHeight();
+
+                       JPanel placeholder = new JPanel();
+                       placeholder
+                                       .setPreferredSize(new Dimension(ww, hh - getTextHeight()));
+                       placeholder.setOpaque(false);
+
+                       JPanel titlePanel = new JPanel(new BorderLayout());
+                       titlePanel.setOpaque(false);
+                       titlePanel.add(title, BorderLayout.NORTH);
+
+                       titlePanel.setBorder(BorderFactory.createEmptyBorder());
+
+                       setLayout(new BorderLayout());
+                       add(placeholder, BorderLayout.NORTH);
+                       add(titlePanel, BorderLayout.CENTER);
+               }
+
+               // Cached values are NULL, so it will be updated
+               updateData();
+       }
+
+       /**
+        * The book current selection state.
+        * 
+        * @return the selection state
+        */
+       public boolean isSelected() {
+               return selected;
+       }
+
+       /**
+        * The book current selection state,
+        * 
+        * @param selected
+        *            TRUE if it is selected
+        */
+       public void setSelected(boolean selected) {
+               if (this.selected != selected) {
+                       this.selected = selected;
+                       repaint();
+               }
+       }
+
+       /**
+        * The item mouse-hover state.
+        * 
+        * @return TRUE if it is mouse-hovered
+        */
+       public boolean isHovered() {
+               return this.hovered;
+       }
+
+       /**
+        * The item mouse-hover state.
+        * 
+        * @param hovered
+        *            TRUE if it is mouse-hovered
+        */
+       public void setHovered(boolean hovered) {
+               if (this.hovered != hovered) {
+                       this.hovered = hovered;
+                       repaint();
+               }
+       }
+
+       /**
+        * Update the title, paint the item.
+        */
+       @Override
+       public void paint(Graphics g) {
+               Rectangle clip = g.getClipBounds();
+               if (clip == null || clip.getWidth() <= 0 || clip.getHeight() <= 0) {
+                       return;
+               }
+
+               updateData();
+
+               super.paint(g);
+               if (hasImage) {
+                       Image img = image == null ? getBlank(false) : image;
+                       if (isImageError())
+                               img = getBlank(true);
+
+                       int xOff = getCoverHOffset() + (getWidth() - getCoverWidth()) / 2;
+                       g.drawImage(img, xOff, getCoverVOffset(), null);
+
+                       Integer status = getStatus();
+                       boolean filled = status != null && status > 0;
+                       Color indicatorColor = getStatusIndicatorColor(status);
+                       if (indicatorColor != null) {
+                               UIUtils.drawEllipse3D(g, indicatorColor, getCoverWidth() + xOff
+                                               + 10, 10, 20, 20, filled);
+                       }
+               }
+
+               Color bg = getOverlayColor(isEnabled(), isSelected(), isHovered());
+               g.setColor(bg);
+               g.fillRect(clip.x, clip.y, clip.width, clip.height);
+       }
+
+       /**
+        * The image to display on image {@link Item} (NULL for non-image
+        * {@link Item}s).
+        * 
+        * @return the image or NULL for the empty image or for non image
+        *         {@link Item}s
+        */
+       public Image getImage() {
+               return hasImage ? image : null;
+       }
+
+       /**
+        * Change the image to display (does not work for non-image {@link Item}s).
+        * <p>
+        * NULL is allowed, an empty image will then be shown.
+        * 
+        * @param image
+        *            the new {@link Image} or NULL
+        * 
+        */
+       public void setImage(Image image) {
+               this.image = hasImage ? image : null;
+       }
+
+       /**
+        * Use the ERROR image instead of the real one or the empty one.
+        * 
+        * @return TRUE if we force use the error image
+        */
+       public boolean isImageError() {
+               return imageError;
+       }
+
+       /**
+        * Use the ERROR image instead of the real one or the empty one.
+        * 
+        * @param imageError
+        *            TRUE to force use the error image
+        */
+       public void setImageError(boolean imageError) {
+               this.imageError = imageError;
+       }
+
+       /**
+        * Make the given {@link String} display-ready (i.e., shorten it if it is
+        * too long).
+        * 
+        * @param value
+        *            the full value
+        * 
+        * @return the display-ready value
+        */
+       protected String limit(String value) {
+               if (value == null)
+                       value = "";
+
+               if (value.length() > getMaxDisplaySize()) {
+                       value = value.substring(0, getMaxDisplaySize() - 3) + "...";
+               }
+
+               return value;
+       }
+
+       /**
+        * Update the title with the currently registered information.
+        */
+       private void updateData() {
+               String main = getMainInfoDisplay();
+               String optSecondary = getSecondaryInfoDisplay();
+               Integer status = getStatus();
+
+               // Cached values can be NULL the first time
+               if (!main.equals(cachedMain)
+                               || !optSecondary.equals(cachedOptSecondary)
+                               || status != cachedStatus) {
+                       title.setText(mainTemplate //
+                                       .replace("${MAIN}", main) //
+                                       .replace("${SECONDARY}", optSecondary) //
+                       );
+                       secondary.setText(secondaryTemplate//
+                                       .replace("${MAIN}", main) //
+                                       .replace("${SECONDARY}", optSecondary) //
+                                       + " ");
+
+                       Color bg = getOverlayColor(isEnabled(), isSelected(), isHovered());
+                       setBackground(bg);
+
+                       if (!hasImage) {
+                               remove(statusIndicatorUnknown);
+                               remove(statusIndicatorOn);
+                               remove(statusIndicatorOff);
+
+                               Color k = getStatusIndicatorColor(getStatus());
+                               JComponent statusIndicator = statuses.get(k);
+                               if (!statuses.containsKey(k)) {
+                                       statusIndicator = generateStatusIndicator(k);
+                                       statuses.put(k, statusIndicator);
+                               }
+
+                               if (statusIndicator != null)
+                                       add(statusIndicator, BorderLayout.WEST);
+                       }
+
+                       validate();
+               }
+
+               this.cachedMain = main;
+               this.cachedOptSecondary = optSecondary;
+               this.cachedStatus = status;
+       }
+
+       /**
+        * Generate a status indicator for the given colour.
+        * 
+        * @param color
+        *            the colour to use
+        * 
+        * @return a status indicator ready to be used
+        */
+       private JLabel generateStatusIndicator(Color color) {
+               JLabel indicator = new JLabel("   ") {
+                       private static final long serialVersionUID = 1L;
+
+                       @Override
+                       public void paint(Graphics g) {
+                               super.paint(g);
+
+                               if (color != null) {
+                                       Dimension sz = statusIndicatorOn.getSize();
+                                       int s = Math.min(sz.width, sz.height);
+                                       int x = Math.max(0, (sz.width - sz.height) / 2);
+                                       int y = Math.max(0, (sz.height - sz.width) / 2);
+
+                                       UIUtils.drawEllipse3D(g, color, x, y, s, s, true);
+                               }
+                       }
+               };
+
+               indicator.setBackground(color);
+               return indicator;
+       }
+
+       private Image getBlank(boolean error) {
+               Dimension key = new Dimension(getCoverWidth(), getCoverHeight());
+               Map<Dimension, BufferedImage> images = error ? Item.error : Item.empty;
+
+               BufferedImage blank = images.get(key);
+               if (blank == null) {
+                       blank = new BufferedImage(getCoverWidth(), getCoverHeight(),
+                                       BufferedImage.TYPE_4BYTE_ABGR);
+
+                       Graphics2D g = blank.createGraphics();
+                       try {
+                               g.setColor(Color.white);
+                               g.fillRect(0, 0, getCoverWidth(), getCoverHeight());
+
+                               g.setColor(error ? Color.red : Color.black);
+                               g.drawLine(0, 0, getCoverWidth(), getCoverHeight());
+                               g.drawLine(getCoverWidth(), 0, 0, getCoverHeight());
+                       } finally {
+                               g.dispose();
+                       }
+                       images.put(key, blank);
+               }
+
+               return blank;
+       }
+}
diff --git a/ui/SearchBar.java b/ui/SearchBar.java
new file mode 100644 (file)
index 0000000..ffbc78b
--- /dev/null
@@ -0,0 +1,148 @@
+package be.nikiroo.utils.ui;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JTextField;
+import javax.swing.SwingUtilities;
+
+import be.nikiroo.utils.IOUtils;
+
+/**
+ * A generic search/filter bar.
+ * 
+ * @author niki
+ */
+public class SearchBar extends ListenerPanel {
+       static private final long serialVersionUID = 1L;
+       static private ImageIcon searchIcon;
+       static private ImageIcon clearIcon;
+
+       private JButton search;
+       private JTextField text;
+       private JButton clear;
+
+       private boolean realTime;
+
+       /**
+        * Create a new {@link SearchBar}.
+        */
+       public SearchBar() {
+               setLayout(new BorderLayout());
+
+               // TODO: make an option to change the default setting here:
+               // (can already be manually toggled by the user)
+               realTime = true;
+
+               if (searchIcon == null)
+                       searchIcon = getIcon("search-16x16.png");
+               if (clearIcon == null)
+                       clearIcon = getIcon("clear-16x16.png");
+
+               search = new JButton(searchIcon);
+               if (searchIcon == null) {
+                       search.setText("[s]");
+               }
+               UIUtils.setButtonPressed(search, realTime);
+               search.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               realTime = !realTime;
+                               UIUtils.setButtonPressed(search, realTime);
+                               text.requestFocus();
+
+                               if (realTime) {
+                                       fireActionPerformed(getText());
+                               }
+                       }
+               });
+
+               text = new JTextField();
+               text.addKeyListener(new KeyAdapter() {
+                       @Override
+                       public void keyTyped(final KeyEvent e) {
+                               super.keyTyped(e);
+                               SwingUtilities.invokeLater(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               boolean empty = (text.getText().isEmpty());
+                                               clear.setVisible(!empty);
+
+                                               if (realTime) {
+                                                       fireActionPerformed(getText());
+                                               }
+                                       }
+                               });
+                       }
+               });
+               text.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               if (!realTime) {
+                                       fireActionPerformed(getText());
+                               }
+                       }
+               });
+
+               clear = new JButton(clearIcon);
+               if (clearIcon == null) {
+                       clear.setText("(c)");
+               }
+               clear.setBackground(text.getBackground());
+               clear.setVisible(false);
+               clear.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               text.setText("");
+                               clear.setVisible(false);
+                               text.requestFocus();
+
+                               fireActionPerformed(getText());
+                       }
+               });
+
+               add(search, BorderLayout.WEST);
+               add(text, BorderLayout.CENTER);
+               add(clear, BorderLayout.EAST);
+       }
+
+       /**
+        * Return the current text displayed by this {@link SearchBar}, or an empty
+        * {@link String} if none.
+        * 
+        * @return the text, cannot be NULL
+        */
+       public String getText() {
+               // Should usually not be NULL, but not impossible
+               String text = this.text.getText();
+               return text == null ? "" : text;
+       }
+
+       @Override
+       public void setEnabled(boolean enabled) {
+               search.setEnabled(enabled);
+               clear.setEnabled(enabled);
+               text.setEnabled(enabled);
+               super.setEnabled(enabled);
+       }
+
+       private ImageIcon getIcon(String name) {
+               InputStream in = IOUtils.openResource(SearchBar.class, name);
+               if (in != null) {
+                       try {
+                               return new ImageIcon(IOUtils.toByteArray(in));
+                       } catch (IOException e) {
+                               e.printStackTrace();
+                       }
+               }
+
+               return null;
+       }
+}
index e4eb000c6876f0a2b610da928fb16592cd1d3a4e..ce7bcc1e866ff1ca7ec31ae35a11a0be94924a37 100644 (file)
@@ -12,6 +12,7 @@ import java.awt.RenderingHints;
 import java.io.IOException;
 import java.net.URISyntaxException;
 
+import javax.swing.JButton;
 import javax.swing.JComponent;
 import javax.swing.JEditorPane;
 import javax.swing.JLabel;
@@ -31,6 +32,9 @@ import be.nikiroo.utils.VersionCheck;
  * @author niki
  */
 public class UIUtils {
+       static private Color buttonNormal;
+       static private Color buttonPressed;
+       
        /**
         * Set a fake "native Look &amp; Feel" for the application if possible
         * (check for the one currently in use, then try GTK).
@@ -331,4 +335,37 @@ public class UIUtils {
                return JOptionPane.showConfirmDialog(parentComponent, updateMessage,
                                title, JOptionPane.DEFAULT_OPTION) == JOptionPane.OK_OPTION;
        }
+       
+       /**
+        * Set the given {@link JButton} as "pressed" (selected, but with more UI
+        * visibility).
+        * <p>
+        * The {@link JButton} will answer {@link JButton#isSelected()} if it is
+        * pressed.
+        * 
+        * @param button
+        *            the button to select/press
+        * @param pressed
+        *            the new "pressed" state
+        */
+       static public void setButtonPressed(JButton button, boolean pressed) {
+               if (buttonNormal == null) {
+                       JButton defButton = new JButton(" ");
+                       buttonNormal = defButton.getBackground();
+                       if (buttonNormal.getBlue() >= 128) {
+                               buttonPressed = new Color( //
+                                               Math.max(buttonNormal.getRed() - 100, 0), //
+                                               Math.max(buttonNormal.getGreen() - 100, 0), //
+                                               Math.max(buttonNormal.getBlue() - 100, 0));
+                       } else {
+                               buttonPressed = new Color( //
+                                               Math.min(buttonNormal.getRed() + 100, 255), //
+                                               Math.min(buttonNormal.getGreen() + 100, 255), //
+                                               Math.min(buttonNormal.getBlue() + 100, 255));
+                       }
+               }
+
+               button.setSelected(pressed);
+               button.setBackground(pressed ? buttonPressed : buttonNormal);
+       }
 }
diff --git a/ui/clear-16x16.png b/ui/clear-16x16.png
new file mode 100644 (file)
index 0000000..3ea2622
Binary files /dev/null and b/ui/clear-16x16.png differ
diff --git a/ui/search-16x16.png b/ui/search-16x16.png
new file mode 100644 (file)
index 0000000..0164b31
Binary files /dev/null and b/ui/search-16x16.png differ