work on refresh after popup action
[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(null).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 (filter.isEmpty() || bookInfo.getMainInfo().toLowerCase().contains(filter.toLowerCase())) {
168 data.addElement(bookInfo);
169 }
170 }
171 list.repaint();
172 }
173
174 /**
175 * The secondary value content: word count or author.
176 *
177 * @return TRUE to see word counts, FALSE to see authors
178 */
179 public boolean isSeeWordCount() {
180 return seeWordCount;
181 }
182
183 /**
184 * The secondary value content: word count or author.
185 *
186 * @param seeWordCount TRUE to see word counts, FALSE to see authors
187 */
188 public void setSeeWordCount(boolean seeWordCount) {
189 if (this.seeWordCount != seeWordCount) {
190 if (books != null) {
191 for (BookLine book : books.values()) {
192 book.setSeeWordCount(seeWordCount);
193 }
194
195 list.repaint();
196 }
197 }
198 }
199
200 private JList<BookInfo> initList(boolean listMode) {
201 final JList<BookInfo> list = new JList<BookInfo>(data);
202
203 final JPopupMenu popup = new BookPopup(Instance.getInstance().getLibrary(), new BookPopup.Informer() {
204 @Override
205 public void setCached(BookInfo book, boolean cached) {
206 book.setCached(cached);
207 fireElementChanged(book);
208 }
209
210 public void fireElementChanged(BookInfo book) {
211 data.fireElementChanged(book);
212 }
213
214 @Override
215 public List<BookInfo> getSelected() {
216 List<BookInfo> selected = new ArrayList<BookInfo>();
217 for (int index : list.getSelectedIndices()) {
218 selected.add(data.get(index));
219 }
220
221 return selected;
222 }
223
224 @Override
225 public BookInfo getUniqueSelected() {
226 List<BookInfo> selected = getSelected();
227 if (selected.size() == 1) {
228 return selected.get(0);
229 }
230 return null;
231 }
232
233 @Override
234 public void invalidateCache() {
235 fireActionPerformed(INVALIDATE_CACHE);
236 }
237 });
238
239 list.addMouseMotionListener(new MouseAdapter() {
240 @Override
241 public void mouseMoved(MouseEvent me) {
242 if (popup.isShowing())
243 return;
244
245 Point p = new Point(me.getX(), me.getY());
246 int index = list.locationToIndex(p);
247 if (index != hoveredIndex) {
248 hoveredIndex = index;
249 list.repaint();
250 }
251 }
252 });
253 list.addMouseListener(new MouseAdapter() {
254 @Override
255 public void mousePressed(MouseEvent e) {
256 check(e);
257 }
258
259 @Override
260 public void mouseReleased(MouseEvent e) {
261 check(e);
262 }
263
264 @Override
265 public void mouseExited(MouseEvent e) {
266 if (popup.isShowing())
267 return;
268
269 if (hoveredIndex > -1) {
270 hoveredIndex = -1;
271 list.repaint();
272 }
273 }
274
275 @Override
276 public void mouseClicked(MouseEvent e) {
277 super.mouseClicked(e);
278 if (e.getClickCount() == 2) {
279 int index = list.locationToIndex(e.getPoint());
280 list.setSelectedIndex(index);
281
282 final BookInfo book = data.get(index);
283 BasicLibrary lib = Instance.getInstance().getLibrary();
284
285 Actions.openExternal(lib, book.getMeta(), BooksPanel.this, new Runnable() {
286 @Override
287 public void run() {
288 book.setCached(true);
289 data.fireElementChanged(book);
290 }
291 });
292 }
293 }
294
295 private void check(MouseEvent e) {
296 if (e.isPopupTrigger()) {
297 if (list.getSelectedIndices().length <= 1) {
298 list.setSelectedIndex(list.locationToIndex(e.getPoint()));
299 }
300
301 popup.show(list, e.getX(), e.getY());
302 }
303 }
304 });
305
306 list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
307 list.setSelectedIndex(0);
308 list.setCellRenderer(generateRenderer());
309 list.setVisibleRowCount(0);
310
311 this.list = list;
312 setListMode(listMode);
313 return this.list;
314 }
315
316 private ListCellRenderer<BookInfo> generateRenderer() {
317 return new ListCellRenderer<BookInfo>() {
318 @Override
319 public Component getListCellRendererComponent(JList<? extends BookInfo> list, BookInfo value, int index,
320 boolean isSelected, boolean cellHasFocus) {
321 BookLine book = books.get(value);
322 if (book == null) {
323 if (listMode) {
324 book = new BookLine(value, seeWordCount);
325 } else {
326 book = new BookBlock(value, seeWordCount);
327 synchronized (updateBookQueueLock) {
328 updateBookQueue.add((BookBlock) book);
329 }
330 }
331 books.put(value, book);
332 }
333
334 book.setSelected(isSelected);
335 book.setHovered(index == hoveredIndex);
336 return book;
337 }
338 };
339 }
340
341 public boolean isListMode() {
342 return listMode;
343 }
344
345 public void setListMode(boolean listMode) {
346 this.listMode = listMode;
347 books.clear();
348 list.setLayoutOrientation(listMode ? JList.VERTICAL : JList.HORIZONTAL_WRAP);
349
350 if (listMode) {
351 synchronized (updateBookQueueLock) {
352 updateBookQueue.clear();
353 }
354 }
355 }
356 }