1 package be
.nikiroo
.fanfix
.reader
;
3 import java
.awt
.BorderLayout
;
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
.util
.ArrayList
;
13 import java
.util
.Date
;
14 import java
.util
.EventListener
;
15 import java
.util
.List
;
17 import javax
.swing
.ImageIcon
;
18 import javax
.swing
.JLabel
;
19 import javax
.swing
.JPanel
;
21 import be
.nikiroo
.fanfix
.data
.MetaData
;
22 import be
.nikiroo
.fanfix
.data
.Story
;
23 import be
.nikiroo
.utils
.ui
.UIUtils
;
26 * A book item presented in a {@link LocalReaderFrame}.
30 class LocalReaderBook
extends JPanel
{
32 * Action on a book item.
36 interface BookActionListener
extends EventListener
{
38 * The book was selected (single click).
41 * the {@link LocalReaderBook} itself
43 public void select(LocalReaderBook book
);
46 * The book was double-clicked.
49 * the {@link LocalReaderBook} itself
51 public void action(LocalReaderBook book
);
54 * A popup menu was requested for this {@link LocalReaderBook}.
57 * the {@link LocalReaderBook} itself
59 * the {@link MouseEvent} that generated this call
61 public void popupRequested(LocalReaderBook book
, MouseEvent e
);
64 private static final long serialVersionUID
= 1L;
66 // TODO: export some of the configuration options?
67 private static final int COVER_WIDTH
= 100;
68 private static final int COVER_HEIGHT
= 150;
69 private static final int SPINE_WIDTH
= 5;
70 private static final int SPINE_HEIGHT
= 5;
71 private static final int HOFFSET
= 20;
72 private static final Color SPINE_COLOR_BOTTOM
= new Color(180, 180, 180);
73 private static final Color SPINE_COLOR_RIGHT
= new Color(100, 100, 100);
74 private static final int TEXT_WIDTH
= COVER_WIDTH
+ 40;
75 private static final int TEXT_HEIGHT
= 50;
76 private static final String AUTHOR_COLOR
= "#888888";
77 private static final Color BORDER
= Color
.black
;
78 private static final long doubleClickDelay
= 200; // in ms
83 private boolean selected
;
84 private boolean hovered
;
85 private Date lastClick
;
87 private List
<BookActionListener
> listeners
;
89 private boolean cached
;
92 * Create a new {@link LocalReaderBook} item for the given {@link Story}.
95 * the story {@code}link MetaData}
97 * TRUE if it is locally cached
99 public LocalReaderBook(MetaData meta
, boolean cached
) {
100 this.cached
= cached
;
101 luid
= meta
.getLuid();
103 String optAuthor
= meta
.getAuthor();
104 if (optAuthor
!= null && !optAuthor
.isEmpty()) {
105 optAuthor
= "(" + optAuthor
+ ")";
108 icon
= new JLabel(generateCoverIcon(meta
.getCover()));
113 + "<body style='width: %d px; height: %d px; text-align: center'>"
114 + "%s" + "<br>" + "<span style='color: %s;'>"
115 + "%s" + "</span>" + "</body>" + "</html>",
116 TEXT_WIDTH
, TEXT_HEIGHT
, meta
.getTitle(), AUTHOR_COLOR
,
119 setLayout(new BorderLayout(10, 10));
120 add(icon
, BorderLayout
.CENTER
);
121 add(title
, BorderLayout
.SOUTH
);
127 * The book current selection state.
129 * @return the selection state
131 public boolean isSelected() {
136 * The book current selection state.
139 * TRUE if it is selected
141 public void setSelected(boolean selected
) {
142 if (this.selected
!= selected
) {
143 this.selected
= selected
;
149 * The item mouse-hover state.
152 * TRUE if it is mouse-hovered
154 private void setHovered(boolean hovered
) {
155 if (this.hovered
!= hovered
) {
156 this.hovered
= hovered
;
162 * Setup the mouse listener that will activate {@link BookActionListener}
165 private void setupListeners() {
166 listeners
= new ArrayList
<LocalReaderBook
.BookActionListener
>();
167 addMouseListener(new MouseListener() {
168 public void mouseReleased(MouseEvent e
) {
169 if (e
.isPopupTrigger()) {
174 public void mousePressed(MouseEvent e
) {
175 if (e
.isPopupTrigger()) {
180 public void mouseExited(MouseEvent e
) {
184 public void mouseEntered(MouseEvent e
) {
188 public void mouseClicked(MouseEvent e
) {
190 Date now
= new Date();
191 if (lastClick
!= null
192 && now
.getTime() - lastClick
.getTime() < doubleClickDelay
) {
202 private void click(boolean doubleClick
) {
203 for (BookActionListener listener
: listeners
) {
205 listener
.action(LocalReaderBook
.this);
207 listener
.select(LocalReaderBook
.this);
212 private void popup(MouseEvent e
) {
213 for (BookActionListener listener
: listeners
) {
214 listener
.select((LocalReaderBook
.this));
215 listener
.popupRequested(LocalReaderBook
.this, e
);
222 * Add a new {@link BookActionListener} on this item.
227 public void addActionListener(BookActionListener listener
) {
228 listeners
.add(listener
);
232 * The Library UID of the book represented by this item.
236 public String
getLuid() {
241 * This item {@link LocalReader} library cache state.
243 * @return TRUE if it is present in the {@link LocalReader} cache
245 public boolean isCached() {
250 * This item {@link LocalReader} library cache state.
253 * TRUE if it is present in the {@link LocalReader} cache
255 public void setCached(boolean cached
) {
256 if (this.cached
!= cached
) {
257 this.cached
= cached
;
263 * Paint the item, then call {@link LocalReaderBook#paintOverlay(Graphics)}.
266 public void paint(Graphics g
) {
272 * Draw a partially transparent overlay if needed depending upon the
273 * selection and mouse-hover states on top of the normal component, as well
274 * as a possible "cached" icon if the item is cached.
276 public void paintOverlay(Graphics g
) {
277 Rectangle clip
= g
.getClipBounds();
278 if (clip
.getWidth() <= 0 || clip
.getHeight() <= 0) {
282 int h
= COVER_HEIGHT
;
284 int xOffset
= (TEXT_WIDTH
- COVER_WIDTH
) - 1;
285 int yOffset
= HOFFSET
;
287 if (BORDER
!= null) {
288 if (BORDER
!= null) {
290 g
.drawRect(xOffset
, yOffset
, COVER_WIDTH
, COVER_HEIGHT
);
297 int[] xs
= new int[] { xOffset
, xOffset
+ SPINE_WIDTH
,
298 xOffset
+ w
+ SPINE_WIDTH
, xOffset
+ w
};
299 int[] ys
= new int[] { yOffset
+ h
, yOffset
+ h
+ SPINE_HEIGHT
,
300 yOffset
+ h
+ SPINE_HEIGHT
, yOffset
+ h
};
301 g
.setColor(SPINE_COLOR_BOTTOM
);
302 g
.fillPolygon(new Polygon(xs
, ys
, xs
.length
));
303 xs
= new int[] { xOffset
+ w
, xOffset
+ w
+ SPINE_WIDTH
,
304 xOffset
+ w
+ SPINE_WIDTH
, xOffset
+ w
};
305 ys
= new int[] { yOffset
, yOffset
+ SPINE_HEIGHT
,
306 yOffset
+ h
+ SPINE_HEIGHT
, yOffset
+ h
};
307 g
.setColor(SPINE_COLOR_RIGHT
);
308 g
.fillPolygon(new Polygon(xs
, ys
, xs
.length
));
310 Color color
= new Color(255, 255, 255, 0);
312 } else if (selected
&& !hovered
) {
313 color
= new Color(80, 80, 100, 40);
314 } else if (!selected
&& hovered
) {
315 color
= new Color(230, 230, 255, 100);
316 } else if (selected
&& hovered
) {
317 color
= new Color(200, 200, 255, 100);
321 g
.fillRect(clip
.x
, clip
.y
, clip
.width
, clip
.height
);
324 UIUtils
.drawEllipse3D(g
, Color
.green
.darker(), COVER_WIDTH
325 + HOFFSET
+ 30, 10, 20, 20);
330 * Generate a cover icon based upon the given cover image (which may be
334 * the cover image, or NULL for none
338 private ImageIcon
generateCoverIcon(BufferedImage image
) {
339 BufferedImage resizedImage
= new BufferedImage(SPINE_WIDTH
340 + COVER_WIDTH
, SPINE_HEIGHT
+ COVER_HEIGHT
+ HOFFSET
,
341 BufferedImage
.TYPE_4BYTE_ABGR
);
342 Graphics2D g
= resizedImage
.createGraphics();
343 g
.setColor(Color
.white
);
344 g
.fillRect(0, HOFFSET
, COVER_WIDTH
, COVER_HEIGHT
);
346 g
.drawImage(image
, 0, HOFFSET
, COVER_WIDTH
, COVER_HEIGHT
, null);
348 g
.setColor(Color
.black
);
349 g
.drawLine(0, HOFFSET
, COVER_WIDTH
, HOFFSET
+ COVER_HEIGHT
);
350 g
.drawLine(COVER_WIDTH
, HOFFSET
, 0, HOFFSET
+ COVER_HEIGHT
);
354 return new ImageIcon(resizedImage
);