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