Remove or move java.awt dependencies
[nikiroo-utils.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderBook.java
CommitLineData
16a81ef7 1package be.nikiroo.fanfix.reader.ui;
333f0e7b
NR
2
3import java.awt.BorderLayout;
4import java.awt.Color;
d3c84ac3 5import java.awt.Graphics;
333f0e7b 6import java.awt.Graphics2D;
b4dc6ab5 7import java.awt.Polygon;
d3c84ac3 8import java.awt.Rectangle;
333f0e7b
NR
9import java.awt.event.MouseEvent;
10import java.awt.event.MouseListener;
11import java.awt.image.BufferedImage;
57f02339
NR
12import java.io.ByteArrayInputStream;
13import java.io.ByteArrayOutputStream;
14import java.io.IOException;
15import java.io.InputStream;
16import java.net.MalformedURLException;
333f0e7b
NR
17import java.util.ArrayList;
18import java.util.Date;
19import java.util.EventListener;
20import java.util.List;
21
57f02339 22import javax.imageio.ImageIO;
333f0e7b
NR
23import javax.swing.ImageIcon;
24import javax.swing.JLabel;
25import javax.swing.JPanel;
26
57f02339 27import be.nikiroo.fanfix.Instance;
333f0e7b 28import be.nikiroo.fanfix.data.MetaData;
4d205683 29import be.nikiroo.fanfix.data.Story;
16a81ef7
NR
30import be.nikiroo.fanfix.reader.Reader;
31import be.nikiroo.utils.Image;
32import be.nikiroo.utils.ui.ImageUtilsAwt;
c39a3e30 33import be.nikiroo.utils.ui.UIUtils;
333f0e7b
NR
34
35/**
5dd985cf 36 * A book item presented in a {@link GuiReaderFrame}.
333f0e7b
NR
37 *
38 * @author niki
39 */
5dd985cf 40class GuiReaderBook extends JPanel {
333f0e7b
NR
41 /**
42 * Action on a book item.
43 *
44 * @author niki
45 */
92fb0719 46 interface BookActionListener extends EventListener {
333f0e7b
NR
47 /**
48 * The book was selected (single click).
49 *
50 * @param book
5dd985cf 51 * the {@link GuiReaderBook} itself
333f0e7b 52 */
5dd985cf 53 public void select(GuiReaderBook book);
333f0e7b
NR
54
55 /**
56 * The book was double-clicked.
57 *
58 * @param book
5dd985cf 59 * the {@link GuiReaderBook} itself
333f0e7b 60 */
5dd985cf 61 public void action(GuiReaderBook book);
9843a5e5
NR
62
63 /**
5dd985cf 64 * A popup menu was requested for this {@link GuiReaderBook}.
9843a5e5
NR
65 *
66 * @param book
5dd985cf 67 * the {@link GuiReaderBook} itself
9843a5e5
NR
68 * @param e
69 * the {@link MouseEvent} that generated this call
70 */
5dd985cf 71 public void popupRequested(GuiReaderBook book, MouseEvent e);
333f0e7b
NR
72 }
73
4d205683
NR
74 private static final long serialVersionUID = 1L;
75
76 // TODO: export some of the configuration options?
b4dc6ab5
NR
77 private static final int COVER_WIDTH = 100;
78 private static final int COVER_HEIGHT = 150;
79 private static final int SPINE_WIDTH = 5;
80 private static final int SPINE_HEIGHT = 5;
81 private static final int HOFFSET = 20;
82 private static final Color SPINE_COLOR_BOTTOM = new Color(180, 180, 180);
83 private static final Color SPINE_COLOR_RIGHT = new Color(100, 100, 100);
84 private static final int TEXT_WIDTH = COVER_WIDTH + 40;
85 private static final int TEXT_HEIGHT = 50;
86 private static final String AUTHOR_COLOR = "#888888";
4d205683
NR
87 private static final Color BORDER = Color.black;
88 private static final long doubleClickDelay = 200; // in ms
89 //
3b2b638f 90
333f0e7b 91 private JLabel icon;
3b2b638f 92 private JLabel title;
333f0e7b
NR
93 private boolean selected;
94 private boolean hovered;
95 private Date lastClick;
4d205683 96
92fb0719 97 private List<BookActionListener> listeners;
e42573a0 98 private Reader reader;
22848428 99 private MetaData meta;
10d558d2
NR
100 private boolean cached;
101
4d205683 102 /**
5dd985cf 103 * Create a new {@link GuiReaderBook} item for the given {@link Story}.
4d205683 104 *
e42573a0
NR
105 * @param reader
106 * the associated reader
4d205683 107 * @param meta
14b57448 108 * the story {@link MetaData} or source (if no LUID)
4d205683
NR
109 * @param cached
110 * TRUE if it is locally cached
5dd985cf 111 * @param seeWordCount
793f1071 112 * TRUE to see word counts, FALSE to see authors
4d205683 113 */
e42573a0
NR
114 public GuiReaderBook(Reader reader, MetaData meta, boolean cached,
115 boolean seeWordCount) {
116 this.reader = reader;
10d558d2 117 this.cached = cached;
22848428 118 this.meta = meta;
333f0e7b 119
793f1071
NR
120 String optSecondary = meta.getAuthor();
121 if (seeWordCount) {
122 if (meta.getWords() >= 4000) {
c8faa52a 123 optSecondary = "" + (meta.getWords() / 1000) + "k";
793f1071 124 } else if (meta.getWords() > 0) {
c8faa52a 125 optSecondary = "" + meta.getWords();
793f1071 126 } else {
a3641e4b 127 optSecondary = "";
793f1071 128 }
c8faa52a
NR
129
130 if (!optSecondary.isEmpty()) {
131 if (meta.isImageDocument()) {
132 optSecondary += " images";
133 } else {
134 optSecondary += " words";
135 }
136 }
793f1071
NR
137 }
138
139 if (optSecondary != null && !optSecondary.isEmpty()) {
140 optSecondary = "(" + optSecondary + ")";
14b57448
NR
141 } else {
142 optSecondary = "";
b4dc6ab5 143 }
4d205683 144
085a2f9a 145 icon = new JLabel(generateCoverIcon());
3b2b638f 146 title = new JLabel(
b4dc6ab5
NR
147 String.format(
148 "<html>"
149 + "<body style='width: %d px; height: %d px; text-align: center'>"
150 + "%s" + "<br>" + "<span style='color: %s;'>"
151 + "%s" + "</span>" + "</body>" + "</html>",
152 TEXT_WIDTH, TEXT_HEIGHT, meta.getTitle(), AUTHOR_COLOR,
793f1071 153 optSecondary));
b4dc6ab5 154
edd46289
NR
155 setLayout(new BorderLayout(10, 10));
156 add(icon, BorderLayout.CENTER);
157 add(title, BorderLayout.SOUTH);
333f0e7b
NR
158
159 setupListeners();
333f0e7b
NR
160 }
161
162 /**
163 * The book current selection state.
164 *
4d205683 165 * @return the selection state
333f0e7b
NR
166 */
167 public boolean isSelected() {
168 return selected;
169 }
170
171 /**
172 * The book current selection state.
173 *
174 * @param selected
4d205683 175 * TRUE if it is selected
333f0e7b
NR
176 */
177 public void setSelected(boolean selected) {
edd46289
NR
178 if (this.selected != selected) {
179 this.selected = selected;
180 repaint();
181 }
333f0e7b
NR
182 }
183
4d205683
NR
184 /**
185 * The item mouse-hover state.
186 *
187 * @param hovered
188 * TRUE if it is mouse-hovered
189 */
333f0e7b 190 private void setHovered(boolean hovered) {
edd46289
NR
191 if (this.hovered != hovered) {
192 this.hovered = hovered;
193 repaint();
194 }
333f0e7b
NR
195 }
196
4d205683
NR
197 /**
198 * Setup the mouse listener that will activate {@link BookActionListener}
199 * events.
200 */
333f0e7b 201 private void setupListeners() {
5dd985cf 202 listeners = new ArrayList<GuiReaderBook.BookActionListener>();
333f0e7b 203 addMouseListener(new MouseListener() {
211f7ddb 204 @Override
333f0e7b 205 public void mouseReleased(MouseEvent e) {
9843a5e5
NR
206 if (e.isPopupTrigger()) {
207 popup(e);
208 }
333f0e7b
NR
209 }
210
211f7ddb 211 @Override
333f0e7b 212 public void mousePressed(MouseEvent e) {
9843a5e5
NR
213 if (e.isPopupTrigger()) {
214 popup(e);
215 }
333f0e7b
NR
216 }
217
211f7ddb 218 @Override
333f0e7b
NR
219 public void mouseExited(MouseEvent e) {
220 setHovered(false);
221 }
222
211f7ddb 223 @Override
333f0e7b
NR
224 public void mouseEntered(MouseEvent e) {
225 setHovered(true);
226 }
227
211f7ddb 228 @Override
333f0e7b 229 public void mouseClicked(MouseEvent e) {
3b2b638f
NR
230 if (isEnabled()) {
231 Date now = new Date();
232 if (lastClick != null
233 && now.getTime() - lastClick.getTime() < doubleClickDelay) {
234 click(true);
235 } else {
236 click(false);
237 }
9843a5e5 238
3b2b638f 239 lastClick = now;
333f0e7b 240 }
333f0e7b 241 }
333f0e7b 242
9843a5e5
NR
243 private void click(boolean doubleClick) {
244 for (BookActionListener listener : listeners) {
245 if (doubleClick) {
5dd985cf 246 listener.action(GuiReaderBook.this);
9843a5e5 247 } else {
5dd985cf 248 listener.select(GuiReaderBook.this);
9843a5e5
NR
249 }
250 }
333f0e7b 251 }
9843a5e5
NR
252
253 private void popup(MouseEvent e) {
254 for (BookActionListener listener : listeners) {
5dd985cf
NR
255 listener.select((GuiReaderBook.this));
256 listener.popupRequested(GuiReaderBook.this, e);
9843a5e5
NR
257 }
258 }
259 });
333f0e7b
NR
260 }
261
4d205683
NR
262 /**
263 * Add a new {@link BookActionListener} on this item.
264 *
265 * @param listener
266 * the listener
267 */
92fb0719 268 public void addActionListener(BookActionListener listener) {
333f0e7b
NR
269 listeners.add(listener);
270 }
d3c84ac3 271
4d205683 272 /**
5dd985cf 273 * The Library {@link MetaData} of the book represented by this item.
4d205683 274 *
22848428 275 * @return the meta
4d205683 276 */
22848428
NR
277 public MetaData getMeta() {
278 return meta;
10d558d2
NR
279 }
280
281 /**
5dd985cf 282 * This item {@link GuiReader} library cache state.
10d558d2 283 *
5dd985cf 284 * @return TRUE if it is present in the {@link GuiReader} cache
10d558d2
NR
285 */
286 public boolean isCached() {
287 return cached;
288 }
289
290 /**
5dd985cf 291 * This item {@link GuiReader} library cache state.
10d558d2
NR
292 *
293 * @param cached
5dd985cf 294 * TRUE if it is present in the {@link GuiReader} cache
10d558d2
NR
295 */
296 public void setCached(boolean cached) {
f977d05b
NR
297 if (this.cached != cached) {
298 this.cached = cached;
edd46289 299 repaint();
f977d05b 300 }
10d558d2
NR
301 }
302
4d205683 303 /**
5dd985cf 304 * Paint the item, then call {@link GuiReaderBook#paintOverlay(Graphics)}.
4d205683 305 */
d3c84ac3
NR
306 @Override
307 public void paint(Graphics g) {
308 super.paint(g);
edd46289
NR
309 paintOverlay(g);
310 }
d3c84ac3 311
edd46289
NR
312 /**
313 * Draw a partially transparent overlay if needed depending upon the
314 * selection and mouse-hover states on top of the normal component, as well
315 * as a possible "cached" icon if the item is cached.
5dd985cf
NR
316 *
317 * @param g
318 * the {@link Graphics} to paint onto
edd46289
NR
319 */
320 public void paintOverlay(Graphics g) {
4d205683
NR
321 Rectangle clip = g.getClipBounds();
322 if (clip.getWidth() <= 0 || clip.getHeight() <= 0) {
323 return;
324 }
325
b4dc6ab5
NR
326 int h = COVER_HEIGHT;
327 int w = COVER_WIDTH;
4d205683
NR
328 int xOffset = (TEXT_WIDTH - COVER_WIDTH) - 1;
329 int yOffset = HOFFSET;
330
331 if (BORDER != null) {
332 if (BORDER != null) {
333 g.setColor(BORDER);
334 g.drawRect(xOffset, yOffset, COVER_WIDTH, COVER_HEIGHT);
335 }
336
337 xOffset++;
338 yOffset++;
339 }
b4dc6ab5
NR
340
341 int[] xs = new int[] { xOffset, xOffset + SPINE_WIDTH,
342 xOffset + w + SPINE_WIDTH, xOffset + w };
4d205683
NR
343 int[] ys = new int[] { yOffset + h, yOffset + h + SPINE_HEIGHT,
344 yOffset + h + SPINE_HEIGHT, yOffset + h };
b4dc6ab5
NR
345 g.setColor(SPINE_COLOR_BOTTOM);
346 g.fillPolygon(new Polygon(xs, ys, xs.length));
347 xs = new int[] { xOffset + w, xOffset + w + SPINE_WIDTH,
348 xOffset + w + SPINE_WIDTH, xOffset + w };
4d205683
NR
349 ys = new int[] { yOffset, yOffset + SPINE_HEIGHT,
350 yOffset + h + SPINE_HEIGHT, yOffset + h };
b4dc6ab5
NR
351 g.setColor(SPINE_COLOR_RIGHT);
352 g.fillPolygon(new Polygon(xs, ys, xs.length));
353
d3c84ac3 354 Color color = new Color(255, 255, 255, 0);
3b2b638f
NR
355 if (!isEnabled()) {
356 } else if (selected && !hovered) {
d3c84ac3
NR
357 color = new Color(80, 80, 100, 40);
358 } else if (!selected && hovered) {
359 color = new Color(230, 230, 255, 100);
360 } else if (selected && hovered) {
361 color = new Color(200, 200, 255, 100);
362 }
363
4d205683
NR
364 g.setColor(color);
365 g.fillRect(clip.x, clip.y, clip.width, clip.height);
366
10d558d2 367 if (cached) {
c39a3e30
NR
368 UIUtils.drawEllipse3D(g, Color.green.darker(), COVER_WIDTH
369 + HOFFSET + 30, 10, 20, 20);
10d558d2 370 }
d3c84ac3 371 }
edd46289
NR
372
373 /**
57f02339 374 * Generate a cover icon based upon the given {@link MetaData}.
edd46289 375 *
edd46289
NR
376 * @return the icon
377 */
085a2f9a 378 private ImageIcon generateCoverIcon() {
57f02339 379 BufferedImage resizedImage = null;
085a2f9a 380 String id = getIconId(meta);
e604986c 381
e604986c
NR
382 InputStream in = Instance.getCache().getFromCache(id);
383 if (in != null) {
384 try {
16a81ef7 385 resizedImage = ImageUtilsAwt.fromImage(new Image(in));
e604986c
NR
386 in.close();
387 in = null;
388 } catch (IOException e) {
62c63b07 389 Instance.getTraceHandler().error(e);
57f02339
NR
390 }
391 }
392
393 if (resizedImage == null) {
394 try {
16a81ef7 395 Image cover = null;
14b57448
NR
396 if (meta.getLuid() == null) {
397 cover = reader.getLibrary()
398 .getSourceCover(meta.getSource());
399 } else {
400 cover = reader.getLibrary().getCover(meta.getLuid());
401 }
57f02339 402
16a81ef7
NR
403 BufferedImage coverb = ImageUtilsAwt.fromImage(cover);
404
57f02339
NR
405 resizedImage = new BufferedImage(SPINE_WIDTH + COVER_WIDTH,
406 SPINE_HEIGHT + COVER_HEIGHT + HOFFSET,
407 BufferedImage.TYPE_4BYTE_ABGR);
408 Graphics2D g = resizedImage.createGraphics();
409 g.setColor(Color.white);
410 g.fillRect(0, HOFFSET, COVER_WIDTH, COVER_HEIGHT);
411 if (cover != null) {
16a81ef7 412 g.drawImage(coverb, 0, HOFFSET, COVER_WIDTH, COVER_HEIGHT,
57f02339
NR
413 null);
414 } else {
415 g.setColor(Color.black);
416 g.drawLine(0, HOFFSET, COVER_WIDTH, HOFFSET + COVER_HEIGHT);
417 g.drawLine(COVER_WIDTH, HOFFSET, 0, HOFFSET + COVER_HEIGHT);
418 }
419 g.dispose();
420
14b57448
NR
421 if (id != null) {
422 ByteArrayOutputStream out = new ByteArrayOutputStream();
423 ImageIO.write(resizedImage, "png", out);
424 byte[] imageBytes = out.toByteArray();
e604986c 425 in = new ByteArrayInputStream(imageBytes);
14b57448
NR
426 Instance.getCache().addToCache(in, id);
427 in.close();
428 in = null;
429 }
57f02339 430 } catch (MalformedURLException e) {
62c63b07 431 Instance.getTraceHandler().error(e);
57f02339 432 } catch (IOException e) {
62c63b07 433 Instance.getTraceHandler().error(e);
57f02339 434 }
edd46289 435 }
edd46289 436
16a81ef7
NR
437 if (resizedImage != null) {
438 return new ImageIcon(resizedImage);
439 }
440
441 return null;
edd46289 442 }
085a2f9a
NR
443
444 /**
445 * Manually clear the icon set for this item.
446 *
447 * @param meta
448 * the meta of the story or source (if luid is null)
449 */
450 public static void clearIcon(MetaData meta) {
451 String id = getIconId(meta);
452 Instance.getCache().removeFromCache(id);
453 }
454
455 /**
456 * Get a unique ID from this meta (note that if the luid is null, it is
457 * considered a source and not a {@link Story}).
458 *
459 * @param meta
460 * the meta
461 * @return the unique ID
462 */
463 private static String getIconId(MetaData meta) {
464 String id = null;
465
466 String key = meta.getUuid();
467 if (meta.getLuid() == null) {
468 // a fake meta (== a source)
469 key = "source_" + meta.getSource();
470 }
471
472 id = key + ".thumb_" + SPINE_WIDTH + "x" + COVER_WIDTH + "+"
473 + SPINE_HEIGHT + "+" + COVER_HEIGHT + "@" + HOFFSET;
474
475 return id;
476 }
333f0e7b 477}