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