From: Niki Roo Date: Mon, 21 Dec 2020 21:23:49 +0000 (+0100) Subject: New: ui.Item X-Git-Url: https://git.nikiroo.be/?a=commitdiff_plain;h=4a7f3067529c133ae280fa982372ec6c84f90c0a;p=nikiroo-utils.git New: ui.Item --- diff --git a/src/be/nikiroo/utils/ui/Item.java b/src/be/nikiroo/utils/ui/Item.java new file mode 100644 index 0000000..c8afc7f --- /dev/null +++ b/src/be/nikiroo/utils/ui/Item.java @@ -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. + *

+ * Can be selected, hovered... + * + * @author niki + */ +abstract public class Item extends JPanel { + static private final long serialVersionUID = 1L; + + static private Map empty = new HashMap(); + static private Map error = new HashMap(); + static private Map statuses = new HashMap(); + + 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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("" + + "" + + "${MAIN}" + "
" + "" + + "${SECONDARY}" + "" + "" + + "", 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). + *

+ * 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 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; + } +}