much quicker BooksPanel display, set max title size for BookBlocks
[fanfix.git] / src / be / nikiroo / fanfix_swing / gui / BooksPanel.java
1 package be.nikiroo.fanfix_swing.gui;
2
3 import java.awt.BorderLayout;
4 import java.awt.Component;
5 import java.awt.Dimension;
6 import java.awt.Image;
7 import java.awt.Point;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.awt.event.MouseAdapter;
11 import java.awt.event.MouseEvent;
12 import java.util.ArrayList;
13 import java.util.HashMap;
14 import java.util.List;
15 import java.util.Map;
16 import java.util.concurrent.ExecutionException;
17
18 import javax.swing.DefaultListModel;
19 import javax.swing.JList;
20 import javax.swing.JPopupMenu;
21 import javax.swing.ListCellRenderer;
22 import javax.swing.ListSelectionModel;
23 import javax.swing.SwingWorker;
24
25 import be.nikiroo.fanfix.Instance;
26 import be.nikiroo.fanfix.data.MetaData;
27 import be.nikiroo.fanfix.library.BasicLibrary;
28 import be.nikiroo.fanfix_swing.Actions;
29 import be.nikiroo.fanfix_swing.gui.book.BookBlock;
30 import be.nikiroo.fanfix_swing.gui.book.BookInfo;
31 import be.nikiroo.fanfix_swing.gui.book.BookLine;
32 import be.nikiroo.fanfix_swing.gui.book.BookPopup;
33 import be.nikiroo.fanfix_swing.gui.utils.DelayWorker;
34 import be.nikiroo.fanfix_swing.gui.utils.ListenerPanel;
35 import be.nikiroo.fanfix_swing.gui.utils.UiHelper;
36
37 public class BooksPanel extends ListenerPanel {
38 private class ListModel extends DefaultListModel<BookInfo> {
39 public void fireElementChanged(BookInfo element) {
40 int index = indexOf(element);
41 if (index >= 0) {
42 fireContentsChanged(element, index, index);
43 }
44 }
45 }
46
47 static public final String INVALIDATE_CACHE = "invalidate_cache";
48
49 private List<BookInfo> bookInfos = new ArrayList<BookInfo>();
50 private Map<BookInfo, BookLine> books = new HashMap<BookInfo, BookLine>();
51 private boolean seeWordCount;
52 private boolean listMode;
53
54 private JList<BookInfo> list;
55 private int hoveredIndex = -1;
56 private ListModel data = new ListModel();
57 private DelayWorker bookCoverUpdater;
58
59 private SearchBar searchBar;
60
61 public BooksPanel(boolean listMode) {
62 setLayout(new BorderLayout());
63
64 searchBar = new SearchBar();
65 add(searchBar, BorderLayout.NORTH);
66
67 searchBar.addActionListener(new ActionListener() {
68 @Override
69 public void actionPerformed(ActionEvent e) {
70 filter(searchBar.getText());
71 }
72 });
73
74 bookCoverUpdater = new DelayWorker(20);
75 bookCoverUpdater.start();
76 add(UiHelper.scroll(initList(listMode)), BorderLayout.CENTER);
77 }
78
79 // null or empty -> all sources
80 // sources hierarchy supported ("source/" will includes all "source" and
81 // "source/*")
82 public void load(final List<String> sources, final List<String> authors,
83 final List<String> tags) {
84 new SwingWorker<List<BookInfo>, Void>() {
85 @Override
86 protected List<BookInfo> doInBackground() throws Exception {
87 List<BookInfo> bookInfos = new ArrayList<BookInfo>();
88 BasicLibrary lib = Instance.getInstance().getLibrary();
89 for (MetaData meta : lib.getList().filter(sources, authors,
90 tags)) {
91 bookInfos.add(BookInfo.fromMeta(lib, meta));
92 }
93
94 return bookInfos;
95 }
96
97 @Override
98 protected void done() {
99 try {
100 load(get());
101 } catch (InterruptedException e) {
102 e.printStackTrace();
103 } catch (ExecutionException e) {
104 e.printStackTrace();
105 }
106 // TODO: error
107 }
108 }.execute();
109 }
110
111 public void load(List<BookInfo> bookInfos) {
112 this.bookInfos.clear();
113 this.bookInfos.addAll(bookInfos);
114 bookCoverUpdater.clear();
115
116 filter(searchBar.getText());
117 }
118
119 // cannot be NULL
120 private void filter(String filter) {
121 data.clear();
122 for (BookInfo bookInfo : bookInfos) {
123 if (bookInfo.getMainInfo() == null || filter.isEmpty()
124 || bookInfo.getMainInfo().toLowerCase()
125 .contains(filter.toLowerCase())) {
126 data.addElement(bookInfo);
127 }
128 }
129 list.repaint();
130 }
131
132 /**
133 * The secondary value content: word count or author.
134 *
135 * @return TRUE to see word counts, FALSE to see authors
136 */
137 public boolean isSeeWordCount() {
138 return seeWordCount;
139 }
140
141 /**
142 * The secondary value content: word count or author.
143 *
144 * @param seeWordCount
145 * TRUE to see word counts, FALSE to see authors
146 */
147 public void setSeeWordCount(boolean seeWordCount) {
148 if (this.seeWordCount != seeWordCount) {
149 if (books != null) {
150 for (BookLine book : books.values()) {
151 book.setSeeWordCount(seeWordCount);
152 }
153
154 list.repaint();
155 }
156 }
157 }
158
159 private JList<BookInfo> initList(boolean listMode) {
160 final JList<BookInfo> list = new JList<BookInfo>(data);
161
162 final JPopupMenu popup = new BookPopup(
163 Instance.getInstance().getLibrary(), new BookPopup.Informer() {
164 @Override
165 public void setCached(BookInfo book, boolean cached) {
166 book.setCached(cached);
167 fireElementChanged(book);
168 }
169
170 public void fireElementChanged(BookInfo book) {
171 data.fireElementChanged(book);
172 }
173
174 @Override
175 public List<BookInfo> getSelected() {
176 List<BookInfo> selected = new ArrayList<BookInfo>();
177 for (int index : list.getSelectedIndices()) {
178 selected.add(data.get(index));
179 }
180
181 return selected;
182 }
183
184 @Override
185 public BookInfo getUniqueSelected() {
186 List<BookInfo> selected = getSelected();
187 if (selected.size() == 1) {
188 return selected.get(0);
189 }
190 return null;
191 }
192
193 @Override
194 public void invalidateCache() {
195 // TODO: also reset the popup menu for sources/author
196 fireActionPerformed(INVALIDATE_CACHE);
197 }
198 });
199
200 list.addMouseMotionListener(new MouseAdapter() {
201 @Override
202 public void mouseMoved(MouseEvent me) {
203 if (popup.isShowing())
204 return;
205
206 Point p = new Point(me.getX(), me.getY());
207 int index = list.locationToIndex(p);
208 if (index != hoveredIndex) {
209 hoveredIndex = index;
210 list.repaint();
211 }
212 }
213 });
214 list.addMouseListener(new MouseAdapter() {
215 @Override
216 public void mousePressed(MouseEvent e) {
217 check(e);
218 }
219
220 @Override
221 public void mouseReleased(MouseEvent e) {
222 check(e);
223 }
224
225 @Override
226 public void mouseExited(MouseEvent e) {
227 if (popup.isShowing())
228 return;
229
230 if (hoveredIndex > -1) {
231 hoveredIndex = -1;
232 list.repaint();
233 }
234 }
235
236 @Override
237 public void mouseClicked(MouseEvent e) {
238 super.mouseClicked(e);
239 if (e.getClickCount() == 2) {
240 int index = list.locationToIndex(e.getPoint());
241 list.setSelectedIndex(index);
242
243 final BookInfo book = data.get(index);
244 BasicLibrary lib = Instance.getInstance().getLibrary();
245
246 Actions.openExternal(lib, book.getMeta(), BooksPanel.this,
247 new Runnable() {
248 @Override
249 public void run() {
250 book.setCached(true);
251 data.fireElementChanged(book);
252 }
253 });
254 }
255 }
256
257 private void check(MouseEvent e) {
258 if (e.isPopupTrigger()) {
259 if (list.getSelectedIndices().length <= 1) {
260 list.setSelectedIndex(
261 list.locationToIndex(e.getPoint()));
262 }
263
264 popup.show(list, e.getX(), e.getY());
265 }
266 }
267 });
268
269 list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
270 list.setSelectedIndex(0);
271 list.setCellRenderer(generateRenderer());
272 list.setVisibleRowCount(0);
273
274 this.list = list;
275 setListMode(listMode);
276 return this.list;
277 }
278
279 private ListCellRenderer<BookInfo> generateRenderer() {
280 return new ListCellRenderer<BookInfo>() {
281 @Override
282 public Component getListCellRendererComponent(
283 JList<? extends BookInfo> list, BookInfo value, int index,
284 boolean isSelected, boolean cellHasFocus) {
285 BookLine book = books.get(value);
286 if (book == null) {
287 if (listMode) {
288 book = new BookLine(value, seeWordCount);
289 } else {
290 book = new BookBlock(value, seeWordCount);
291 startUpdateBookCover((BookBlock) book);
292 }
293 books.put(value, book);
294 }
295
296 book.setSelected(isSelected);
297 book.setHovered(index == hoveredIndex);
298 return book;
299 }
300 };
301 }
302
303 private void startUpdateBookCover(final BookBlock book) {
304 bookCoverUpdater.delay(book.getInfo().getId(),
305 new SwingWorker<Image, Void>() {
306 @Override
307 protected Image doInBackground() throws Exception {
308 BasicLibrary lib = Instance.getInstance().getLibrary();
309 return BookBlock.generateCoverImage(lib,
310 book.getInfo());
311 }
312
313 protected void done() {
314 try {
315 book.setCoverImage(get());
316 data.fireElementChanged(book.getInfo());
317 } catch (Exception e) {
318 // TODO ? probably just log
319 }
320 }
321 });
322 }
323
324 public boolean isListMode() {
325 return listMode;
326 }
327
328 public void setListMode(boolean listMode) {
329 this.listMode = listMode;
330 books.clear();
331 list.setLayoutOrientation(
332 listMode ? JList.VERTICAL : JList.HORIZONTAL_WRAP);
333
334 StringBuilder longString = new StringBuilder();
335 for (int i = 0; i < 20; i++) {
336 longString.append(
337 "Some long string, which is 50 chars long itself...");
338 }
339 if (listMode) {
340 bookCoverUpdater.clear();
341 Dimension sz = new BookLine(
342 BookInfo.fromSource(null, longString.toString()), true)
343 .getPreferredSize();
344 list.setFixedCellHeight((int) sz.getHeight());
345 list.setFixedCellWidth(list.getWidth());
346 } else {
347 Dimension sz = new BookBlock(
348 BookInfo.fromSource(null, longString.toString()), true)
349 .getPreferredSize();
350 list.setFixedCellHeight((int) sz.getHeight());
351 list.setFixedCellWidth((int) sz.getWidth());
352 }
353 }
354 }