Remove or move java.awt dependencies
[nikiroo-utils.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderBook.java
1 package be.nikiroo.fanfix.reader.ui;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Graphics;
6 import java.awt.Graphics2D;
7 import java.awt.Polygon;
8 import java.awt.Rectangle;
9 import java.awt.event.MouseEvent;
10 import java.awt.event.MouseListener;
11 import java.awt.image.BufferedImage;
12 import java.io.ByteArrayInputStream;
13 import java.io.ByteArrayOutputStream;
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.net.MalformedURLException;
17 import java.util.ArrayList;
18 import java.util.Date;
19 import java.util.EventListener;
20 import java.util.List;
21
22 import javax.imageio.ImageIO;
23 import javax.swing.ImageIcon;
24 import javax.swing.JLabel;
25 import javax.swing.JPanel;
26
27 import be.nikiroo.fanfix.Instance;
28 import be.nikiroo.fanfix.data.MetaData;
29 import be.nikiroo.fanfix.data.Story;
30 import be.nikiroo.fanfix.reader.Reader;
31 import be.nikiroo.utils.Image;
32 import be.nikiroo.utils.ui.ImageUtilsAwt;
33 import be.nikiroo.utils.ui.UIUtils;
34
35 /**
36 * A book item presented in a {@link GuiReaderFrame}.
37 *
38 * @author niki
39 */
40 class GuiReaderBook extends JPanel {
41 /**
42 * Action on a book item.
43 *
44 * @author niki
45 */
46 interface BookActionListener extends EventListener {
47 /**
48 * The book was selected (single click).
49 *
50 * @param book
51 * the {@link GuiReaderBook} itself
52 */
53 public void select(GuiReaderBook book);
54
55 /**
56 * The book was double-clicked.
57 *
58 * @param book
59 * the {@link GuiReaderBook} itself
60 */
61 public void action(GuiReaderBook book);
62
63 /**
64 * A popup menu was requested for this {@link GuiReaderBook}.
65 *
66 * @param book
67 * the {@link GuiReaderBook} itself
68 * @param e
69 * the {@link MouseEvent} that generated this call
70 */
71 public void popupRequested(GuiReaderBook book, MouseEvent e);
72 }
73
74 private static final long serialVersionUID = 1L;
75
76 // TODO: export some of the configuration options?
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";
87 private static final Color BORDER = Color.black;
88 private static final long doubleClickDelay = 200; // in ms
89 //
90
91 private JLabel icon;
92 private JLabel title;
93 private boolean selected;
94 private boolean hovered;
95 private Date lastClick;
96
97 private List<BookActionListener> listeners;
98 private Reader reader;
99 private MetaData meta;
100 private boolean cached;
101
102 /**
103 * Create a new {@link GuiReaderBook} item for the given {@link Story}.
104 *
105 * @param reader
106 * the associated reader
107 * @param meta
108 * the story {@link MetaData} or source (if no LUID)
109 * @param cached
110 * TRUE if it is locally cached
111 * @param seeWordCount
112 * TRUE to see word counts, FALSE to see authors
113 */
114 public GuiReaderBook(Reader reader, MetaData meta, boolean cached,
115 boolean seeWordCount) {
116 this.reader = reader;
117 this.cached = cached;
118 this.meta = meta;
119
120 String optSecondary = meta.getAuthor();
121 if (seeWordCount) {
122 if (meta.getWords() >= 4000) {
123 optSecondary = "" + (meta.getWords() / 1000) + "k";
124 } else if (meta.getWords() > 0) {
125 optSecondary = "" + meta.getWords();
126 } else {
127 optSecondary = "";
128 }
129
130 if (!optSecondary.isEmpty()) {
131 if (meta.isImageDocument()) {
132 optSecondary += " images";
133 } else {
134 optSecondary += " words";
135 }
136 }
137 }
138
139 if (optSecondary != null && !optSecondary.isEmpty()) {
140 optSecondary = "(" + optSecondary + ")";
141 } else {
142 optSecondary = "";
143 }
144
145 icon = new JLabel(generateCoverIcon());
146 title = new JLabel(
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,
153 optSecondary));
154
155 setLayout(new BorderLayout(10, 10));
156 add(icon, BorderLayout.CENTER);
157 add(title, BorderLayout.SOUTH);
158
159 setupListeners();
160 }
161
162 /**
163 * The book current selection state.
164 *
165 * @return the selection state
166 */
167 public boolean isSelected() {
168 return selected;
169 }
170
171 /**
172 * The book current selection state.
173 *
174 * @param selected
175 * TRUE if it is selected
176 */
177 public void setSelected(boolean selected) {
178 if (this.selected != selected) {
179 this.selected = selected;
180 repaint();
181 }
182 }
183
184 /**
185 * The item mouse-hover state.
186 *
187 * @param hovered
188 * TRUE if it is mouse-hovered
189 */
190 private void setHovered(boolean hovered) {
191 if (this.hovered != hovered) {
192 this.hovered = hovered;
193 repaint();
194 }
195 }
196
197 /**
198 * Setup the mouse listener that will activate {@link BookActionListener}
199 * events.
200 */
201 private void setupListeners() {
202 listeners = new ArrayList<GuiReaderBook.BookActionListener>();
203 addMouseListener(new MouseListener() {
204 @Override
205 public void mouseReleased(MouseEvent e) {
206 if (e.isPopupTrigger()) {
207 popup(e);
208 }
209 }
210
211 @Override
212 public void mousePressed(MouseEvent e) {
213 if (e.isPopupTrigger()) {
214 popup(e);
215 }
216 }
217
218 @Override
219 public void mouseExited(MouseEvent e) {
220 setHovered(false);
221 }
222
223 @Override
224 public void mouseEntered(MouseEvent e) {
225 setHovered(true);
226 }
227
228 @Override
229 public void mouseClicked(MouseEvent e) {
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 }
238
239 lastClick = now;
240 }
241 }
242
243 private void click(boolean doubleClick) {
244 for (BookActionListener listener : listeners) {
245 if (doubleClick) {
246 listener.action(GuiReaderBook.this);
247 } else {
248 listener.select(GuiReaderBook.this);
249 }
250 }
251 }
252
253 private void popup(MouseEvent e) {
254 for (BookActionListener listener : listeners) {
255 listener.select((GuiReaderBook.this));
256 listener.popupRequested(GuiReaderBook.this, e);
257 }
258 }
259 });
260 }
261
262 /**
263 * Add a new {@link BookActionListener} on this item.
264 *
265 * @param listener
266 * the listener
267 */
268 public void addActionListener(BookActionListener listener) {
269 listeners.add(listener);
270 }
271
272 /**
273 * The Library {@link MetaData} of the book represented by this item.
274 *
275 * @return the meta
276 */
277 public MetaData getMeta() {
278 return meta;
279 }
280
281 /**
282 * This item {@link GuiReader} library cache state.
283 *
284 * @return TRUE if it is present in the {@link GuiReader} cache
285 */
286 public boolean isCached() {
287 return cached;
288 }
289
290 /**
291 * This item {@link GuiReader} library cache state.
292 *
293 * @param cached
294 * TRUE if it is present in the {@link GuiReader} cache
295 */
296 public void setCached(boolean cached) {
297 if (this.cached != cached) {
298 this.cached = cached;
299 repaint();
300 }
301 }
302
303 /**
304 * Paint the item, then call {@link GuiReaderBook#paintOverlay(Graphics)}.
305 */
306 @Override
307 public void paint(Graphics g) {
308 super.paint(g);
309 paintOverlay(g);
310 }
311
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.
316 *
317 * @param g
318 * the {@link Graphics} to paint onto
319 */
320 public void paintOverlay(Graphics g) {
321 Rectangle clip = g.getClipBounds();
322 if (clip.getWidth() <= 0 || clip.getHeight() <= 0) {
323 return;
324 }
325
326 int h = COVER_HEIGHT;
327 int w = COVER_WIDTH;
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 }
340
341 int[] xs = new int[] { xOffset, xOffset + SPINE_WIDTH,
342 xOffset + w + SPINE_WIDTH, xOffset + w };
343 int[] ys = new int[] { yOffset + h, yOffset + h + SPINE_HEIGHT,
344 yOffset + h + SPINE_HEIGHT, yOffset + h };
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 };
349 ys = new int[] { yOffset, yOffset + SPINE_HEIGHT,
350 yOffset + h + SPINE_HEIGHT, yOffset + h };
351 g.setColor(SPINE_COLOR_RIGHT);
352 g.fillPolygon(new Polygon(xs, ys, xs.length));
353
354 Color color = new Color(255, 255, 255, 0);
355 if (!isEnabled()) {
356 } else if (selected && !hovered) {
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
364 g.setColor(color);
365 g.fillRect(clip.x, clip.y, clip.width, clip.height);
366
367 if (cached) {
368 UIUtils.drawEllipse3D(g, Color.green.darker(), COVER_WIDTH
369 + HOFFSET + 30, 10, 20, 20);
370 }
371 }
372
373 /**
374 * Generate a cover icon based upon the given {@link MetaData}.
375 *
376 * @return the icon
377 */
378 private ImageIcon generateCoverIcon() {
379 BufferedImage resizedImage = null;
380 String id = getIconId(meta);
381
382 InputStream in = Instance.getCache().getFromCache(id);
383 if (in != null) {
384 try {
385 resizedImage = ImageUtilsAwt.fromImage(new Image(in));
386 in.close();
387 in = null;
388 } catch (IOException e) {
389 Instance.getTraceHandler().error(e);
390 }
391 }
392
393 if (resizedImage == null) {
394 try {
395 Image cover = null;
396 if (meta.getLuid() == null) {
397 cover = reader.getLibrary()
398 .getSourceCover(meta.getSource());
399 } else {
400 cover = reader.getLibrary().getCover(meta.getLuid());
401 }
402
403 BufferedImage coverb = ImageUtilsAwt.fromImage(cover);
404
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) {
412 g.drawImage(coverb, 0, HOFFSET, COVER_WIDTH, COVER_HEIGHT,
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
421 if (id != null) {
422 ByteArrayOutputStream out = new ByteArrayOutputStream();
423 ImageIO.write(resizedImage, "png", out);
424 byte[] imageBytes = out.toByteArray();
425 in = new ByteArrayInputStream(imageBytes);
426 Instance.getCache().addToCache(in, id);
427 in.close();
428 in = null;
429 }
430 } catch (MalformedURLException e) {
431 Instance.getTraceHandler().error(e);
432 } catch (IOException e) {
433 Instance.getTraceHandler().error(e);
434 }
435 }
436
437 if (resizedImage != null) {
438 return new ImageIcon(resizedImage);
439 }
440
441 return null;
442 }
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 }
477 }