New: new functions in UiUtils and IOUtils
[nikiroo-utils.git] / src / be / nikiroo / utils / ui / Item.java
CommitLineData
4a7f3067
NR
1package be.nikiroo.utils.ui;
2
3import java.awt.BorderLayout;
4import java.awt.Color;
5import java.awt.Dimension;
6import java.awt.Graphics;
7import java.awt.Graphics2D;
8import java.awt.Image;
9import java.awt.Rectangle;
10import java.awt.image.BufferedImage;
11import java.util.HashMap;
12import java.util.Map;
13
14import javax.swing.BorderFactory;
15import javax.swing.JComponent;
16import javax.swing.JLabel;
17import javax.swing.JPanel;
18import javax.swing.SwingConstants;
19
20/**
21 * A graphical item that can be presented in a list and supports user
22 * interaction.
23 * <p>
24 * Can be selected, hovered...
25 *
26 * @author niki
27 */
28abstract public class Item extends JPanel {
29 static private final long serialVersionUID = 1L;
30
31 static private Map<Dimension, BufferedImage> empty = new HashMap<Dimension, BufferedImage>();
32 static private Map<Dimension, BufferedImage> error = new HashMap<Dimension, BufferedImage>();
33 static private Map<Color, JComponent> statuses = new HashMap<Color, JComponent>();
34
35 private String id;
36 private boolean selected;
37 private boolean hovered;
38
39 private String mainTemplate;
40 private String secondaryTemplate;
41
42 private boolean hasImage;
43 private JLabel title;
44 private JLabel secondary;
45 private JLabel statusIndicatorOn;
46 private JLabel statusIndicatorOff;
47 private JLabel statusIndicatorUnknown;
48 private Image image;
49 private boolean imageError;
50
51 private String cachedMain;
52 private String cachedOptSecondary;
53 private Integer cachedStatus;
54
55 /**
56 * Create a new {@link Item}
57 *
58 * @param id
59 * an ID that represents this {@link Item} (can be NULL)
60 * @param hasImage
61 * this {@link Item} will contain an image
62 */
63 public Item(String id, boolean hasImage) {
64 this.id = id;
65 this.hasImage = hasImage;
66 init(hasImage);
67 }
68
69 // Configuration :
70
71 protected int getMaxDisplaySize() {
72 return 40;
73 }
74
75 protected int getCoverWidth() {
76 return 100;
77 }
78
79 protected int getCoverHeight() {
80 return 150;
81 }
82
83 protected int getTextWidth() {
84 return getCoverWidth() + 40;
85 }
86
87 protected int getTextHeight() {
88 return 50;
89 }
90
91 protected int getCoverVOffset() {
92 return 20;
93 }
94
95 protected int getCoverHOffset() {
96 return 0;
97 }
98
99 protected int getHGap() {
100 return 10;
101 }
102
103 /** Colour used for the secondary item (author/word count). */
104 protected Color getSecondaryColor() {
105 return new Color(128, 128, 128);
106 }
107
108 /**
109 * Return a display-ready version of the main information to show.
110 * <p>
111 * Note that you can make use of {@link Item#limit(String)}.
112 *
113 * @return the main info in a ready-to-display version, cannot be NULL
114 */
115 abstract protected String getMainInfoDisplay();
116
117 /**
118 * Return a display-ready version of the secondary information to show.
119 * <p>
120 * Note that you can make use of {@link Item#limit(String)}.
121 *
122 * @return the main info in a ready-to-display version, cannot be NULL
123 */
124 abstract protected String getSecondaryInfoDisplay();
125
126 /**
127 * The current status for the status indicator.
128 * <p>
129 * Note that NULL and negative values will create "hollow" indicators, while
130 * other values will create "filled" indicators.
131 *
132 * @return the status which can be NULL, presumably for "Unknown"
133 */
134 abstract protected Integer getStatus();
135
136 /**
137 * Get the background colour to use according to the given state.
138 * <p>
139 * Since it is an overlay, an opaque colour will of course mask everything.
140 *
141 * @param enabled
142 * the item is enabled
143 * @param selected
144 * the item is selected
145 * @param hovered
146 * the mouse cursor currently hovers over the item
147 *
148 * @return the correct background colour to use
149 */
150 abstract protected Color getOverlayColor(boolean enabled, boolean selected,
151 boolean hovered);
152
153 /**
154 * Get the colour to use for the status indicator.
155 * <p>
156 * Return NULL if you don't want a status indicator for this state.
157 *
158 * @param status
159 * the current status as returned by {@link Item#getStatus()}
160 *
161 * @return the base colour to use, or NULL for no status indicator
162 */
163 abstract protected Color getStatusIndicatorColor(Integer status);
164
165 /**
166 * Initialise this {@link Item}.
167 */
168 private void init(boolean hasImage) {
169 if (!hasImage) {
170 title = new JLabel();
171 mainTemplate = "${MAIN}";
172 secondary = new JLabel();
173 secondaryTemplate = "${SECONDARY}";
174 secondary.setForeground(getSecondaryColor());
175
176 JPanel idTitle = null;
177 if (id != null && !id.isEmpty()) {
178 JLabel idLabel = new JLabel(id);
179 idLabel.setPreferredSize(new JLabel(" 999 ").getPreferredSize());
180 idLabel.setForeground(Color.gray);
181 idLabel.setHorizontalAlignment(SwingConstants.CENTER);
182
183 idTitle = new JPanel(new BorderLayout());
184 idTitle.setOpaque(false);
185 idTitle.add(idLabel, BorderLayout.WEST);
186 idTitle.add(title, BorderLayout.CENTER);
187 }
188
189 setLayout(new BorderLayout());
190 if (idTitle != null)
191 add(idTitle, BorderLayout.CENTER);
192 add(secondary, BorderLayout.EAST);
193 } else {
194 image = null;
195 title = new JLabel();
196 secondary = new JLabel();
197 secondaryTemplate = "";
198
199 String color = String.format("#%X%X%X", getSecondaryColor()
200 .getRed(), getSecondaryColor().getGreen(),
201 getSecondaryColor().getBlue());
202 mainTemplate = String
203 .format("<html>"
204 + "<body style='width: %d px; height: %d px; text-align: center;'>"
205 + "${MAIN}" + "<br>" + "<span style='color: %s;'>"
206 + "${SECONDARY}" + "</span>" + "</body>"
207 + "</html>", getTextWidth(), getTextHeight(), color);
208
209 int ww = Math.max(getCoverWidth(), getTextWidth());
210 int hh = getCoverHeight() + getCoverVOffset() + getHGap()
211 + getTextHeight();
212
213 JPanel placeholder = new JPanel();
214 placeholder
215 .setPreferredSize(new Dimension(ww, hh - getTextHeight()));
216 placeholder.setOpaque(false);
217
218 JPanel titlePanel = new JPanel(new BorderLayout());
219 titlePanel.setOpaque(false);
220 titlePanel.add(title, BorderLayout.NORTH);
221
222 titlePanel.setBorder(BorderFactory.createEmptyBorder());
223
224 setLayout(new BorderLayout());
225 add(placeholder, BorderLayout.NORTH);
226 add(titlePanel, BorderLayout.CENTER);
227 }
228
229 // Cached values are NULL, so it will be updated
230 updateData();
231 }
232
233 /**
234 * The book current selection state.
235 *
236 * @return the selection state
237 */
238 public boolean isSelected() {
239 return selected;
240 }
241
242 /**
243 * The book current selection state,
244 *
245 * @param selected
246 * TRUE if it is selected
247 */
248 public void setSelected(boolean selected) {
249 if (this.selected != selected) {
250 this.selected = selected;
251 repaint();
252 }
253 }
254
255 /**
256 * The item mouse-hover state.
257 *
258 * @return TRUE if it is mouse-hovered
259 */
260 public boolean isHovered() {
261 return this.hovered;
262 }
263
264 /**
265 * The item mouse-hover state.
266 *
267 * @param hovered
268 * TRUE if it is mouse-hovered
269 */
270 public void setHovered(boolean hovered) {
271 if (this.hovered != hovered) {
272 this.hovered = hovered;
273 repaint();
274 }
275 }
276
277 /**
278 * Update the title, paint the item.
279 */
280 @Override
281 public void paint(Graphics g) {
282 Rectangle clip = g.getClipBounds();
283 if (clip == null || clip.getWidth() <= 0 || clip.getHeight() <= 0) {
284 return;
285 }
286
287 updateData();
288
289 super.paint(g);
290 if (hasImage) {
291 Image img = image == null ? getBlank(false) : image;
292 if (isImageError())
293 img = getBlank(true);
294
295 int xOff = getCoverHOffset() + (getWidth() - getCoverWidth()) / 2;
296 g.drawImage(img, xOff, getCoverVOffset(), null);
297
298 Integer status = getStatus();
299 boolean filled = status != null && status > 0;
300 Color indicatorColor = getStatusIndicatorColor(status);
301 if (indicatorColor != null) {
302 UIUtils.drawEllipse3D(g, indicatorColor, getCoverWidth() + xOff
303 + 10, 10, 20, 20, filled);
304 }
305 }
306
307 Color bg = getOverlayColor(isEnabled(), isSelected(), isHovered());
308 g.setColor(bg);
309 g.fillRect(clip.x, clip.y, clip.width, clip.height);
310 }
311
312 /**
313 * The image to display on image {@link Item} (NULL for non-image
314 * {@link Item}s).
315 *
316 * @return the image or NULL for the empty image or for non image
317 * {@link Item}s
318 */
319 public Image getImage() {
320 return hasImage ? image : null;
321 }
322
323 /**
324 * Change the image to display (does not work for non-image {@link Item}s).
325 * <p>
326 * NULL is allowed, an empty image will then be shown.
327 *
328 * @param image
329 * the new {@link Image} or NULL
330 *
331 */
332 public void setImage(Image image) {
333 this.image = hasImage ? image : null;
334 }
335
336 /**
337 * Use the ERROR image instead of the real one or the empty one.
338 *
339 * @return TRUE if we force use the error image
340 */
341 public boolean isImageError() {
342 return imageError;
343 }
344
345 /**
346 * Use the ERROR image instead of the real one or the empty one.
347 *
348 * @param imageError
349 * TRUE to force use the error image
350 */
351 public void setImageError(boolean imageError) {
352 this.imageError = imageError;
353 }
354
355 /**
356 * Make the given {@link String} display-ready (i.e., shorten it if it is
357 * too long).
358 *
359 * @param value
360 * the full value
361 *
362 * @return the display-ready value
363 */
364 protected String limit(String value) {
365 if (value == null)
366 value = "";
367
368 if (value.length() > getMaxDisplaySize()) {
369 value = value.substring(0, getMaxDisplaySize() - 3) + "...";
370 }
371
372 return value;
373 }
374
375 /**
376 * Update the title with the currently registered information.
377 */
378 private void updateData() {
379 String main = getMainInfoDisplay();
380 String optSecondary = getSecondaryInfoDisplay();
381 Integer status = getStatus();
382
383 // Cached values can be NULL the first time
384 if (!main.equals(cachedMain)
385 || !optSecondary.equals(cachedOptSecondary)
386 || status != cachedStatus) {
387 title.setText(mainTemplate //
388 .replace("${MAIN}", main) //
389 .replace("${SECONDARY}", optSecondary) //
390 );
391 secondary.setText(secondaryTemplate//
392 .replace("${MAIN}", main) //
393 .replace("${SECONDARY}", optSecondary) //
394 + " ");
395
396 Color bg = getOverlayColor(isEnabled(), isSelected(), isHovered());
397 setBackground(bg);
398
399 if (!hasImage) {
400 remove(statusIndicatorUnknown);
401 remove(statusIndicatorOn);
402 remove(statusIndicatorOff);
403
404 Color k = getStatusIndicatorColor(getStatus());
405 JComponent statusIndicator = statuses.get(k);
406 if (!statuses.containsKey(k)) {
407 statusIndicator = generateStatusIndicator(k);
408 statuses.put(k, statusIndicator);
409 }
410
411 if (statusIndicator != null)
412 add(statusIndicator, BorderLayout.WEST);
413 }
414
415 validate();
416 }
417
418 this.cachedMain = main;
419 this.cachedOptSecondary = optSecondary;
420 this.cachedStatus = status;
421 }
422
423 /**
424 * Generate a status indicator for the given colour.
425 *
426 * @param color
427 * the colour to use
428 *
429 * @return a status indicator ready to be used
430 */
431 private JLabel generateStatusIndicator(Color color) {
432 JLabel indicator = new JLabel(" ") {
433 private static final long serialVersionUID = 1L;
434
435 @Override
436 public void paint(Graphics g) {
437 super.paint(g);
438
439 if (color != null) {
440 Dimension sz = statusIndicatorOn.getSize();
441 int s = Math.min(sz.width, sz.height);
442 int x = Math.max(0, (sz.width - sz.height) / 2);
443 int y = Math.max(0, (sz.height - sz.width) / 2);
444
445 UIUtils.drawEllipse3D(g, color, x, y, s, s, true);
446 }
447 }
448 };
449
450 indicator.setBackground(color);
451 return indicator;
452 }
453
454 private Image getBlank(boolean error) {
455 Dimension key = new Dimension(getCoverWidth(), getCoverHeight());
456 Map<Dimension, BufferedImage> images = error ? Item.error : Item.empty;
457
458 BufferedImage blank = images.get(key);
459 if (blank == null) {
460 blank = new BufferedImage(getCoverWidth(), getCoverHeight(),
461 BufferedImage.TYPE_4BYTE_ABGR);
462
463 Graphics2D g = blank.createGraphics();
464 try {
465 g.setColor(Color.white);
466 g.fillRect(0, 0, getCoverWidth(), getCoverHeight());
467
468 g.setColor(error ? Color.red : Color.black);
469 g.drawLine(0, 0, getCoverWidth(), getCoverHeight());
470 g.drawLine(getCoverWidth(), 0, 0, getCoverHeight());
471 } finally {
472 g.dispose();
473 }
474 images.put(key, blank);
475 }
476
477 return blank;
478 }
479}