9a63f76e8ffb832ebc975af2b24e8526e080d3a9
[fanfix.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderMainPanel.java
1 package be.nikiroo.fanfix.reader.ui;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.EventQueue;
6 import java.awt.Frame;
7 import java.awt.Toolkit;
8 import java.awt.datatransfer.DataFlavor;
9 import java.awt.event.ActionEvent;
10 import java.awt.event.ActionListener;
11 import java.awt.event.MouseEvent;
12 import java.io.File;
13 import java.io.IOException;
14 import java.lang.reflect.InvocationTargetException;
15 import java.net.URL;
16 import java.net.UnknownHostException;
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.TreeMap;
21
22 import javax.swing.BoxLayout;
23 import javax.swing.JFileChooser;
24 import javax.swing.JLabel;
25 import javax.swing.JMenuBar;
26 import javax.swing.JOptionPane;
27 import javax.swing.JPanel;
28 import javax.swing.JPopupMenu;
29 import javax.swing.JScrollPane;
30 import javax.swing.SwingConstants;
31 import javax.swing.SwingUtilities;
32
33 import be.nikiroo.fanfix.Instance;
34 import be.nikiroo.fanfix.bundles.UiConfig;
35 import be.nikiroo.fanfix.data.MetaData;
36 import be.nikiroo.fanfix.data.Story;
37 import be.nikiroo.fanfix.library.BasicLibrary;
38 import be.nikiroo.fanfix.library.BasicLibrary.Status;
39 import be.nikiroo.fanfix.library.LocalLibrary;
40 import be.nikiroo.fanfix.reader.BasicReader;
41 import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
42 import be.nikiroo.utils.Progress;
43 import be.nikiroo.utils.ui.ProgressBar;
44
45 /**
46 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
47 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
48 * way to copy them to the {@link GuiReader} cache (
49 * {@link BasicReader#getLibrary()}), read them, delete them...
50 *
51 * @author niki
52 */
53 class GuiReaderMainPanel extends JPanel {
54 private static final long serialVersionUID = 1L;
55 private FrameHelper helper;
56 private Map<String, GuiReaderGroup> books;
57 private GuiReaderGroup bookPane; // for more "All"
58 private JPanel pane;
59 private Color color;
60 private ProgressBar pgBar;
61 private JMenuBar bar;
62 private GuiReaderBook selectedBook;
63 private boolean words; // words or authors (secondary info on books)
64 private boolean currentType; // type/source or author mode (All and Listing)
65
66 /**
67 * An object that offers some helper methods to access the frame that host
68 * it and the Fanfix-related functions.
69 *
70 * @author niki
71 */
72 public interface FrameHelper {
73 /**
74 * Return the reader associated to this {@link FrameHelper}.
75 *
76 * @return the reader
77 */
78 public GuiReader getReader();
79
80 /**
81 * Create the main menu bar.
82 * <p>
83 * Wil invalidate the layout.
84 *
85 * @param libOk
86 * the library can be queried
87 *
88 * @return the bar
89 */
90 public void createMenu(boolean b);
91
92 /**
93 * Create a popup menu for a {@link GuiReaderBook} that represents a
94 * story.
95 *
96 * @return the popup menu to display
97 */
98 public JPopupMenu createBookPopup();
99
100 /**
101 * Create a popup menu for a {@link GuiReaderBook} that represents a
102 * source/type or an author.
103 *
104 * @return the popup menu to display
105 */
106 public JPopupMenu createSourceAuthorPopup();
107 }
108
109 /**
110 * A {@link Runnable} with a {@link Story} parameter.
111 *
112 * @author niki
113 */
114 public interface StoryRunnable {
115 /**
116 * Run the action.
117 *
118 * @param story
119 * the story
120 */
121 public void run(Story story);
122 }
123
124 /**
125 * Create a new {@link GuiReaderMainPanel}.
126 *
127 * @param reader
128 * the associated {@link GuiReader} to forward some commands and
129 * access its {@link LocalLibrary}
130 * @param type
131 * the type of {@link Story} to load, or NULL for all types
132 */
133 public GuiReaderMainPanel(FrameHelper parent, String type) {
134 super(new BorderLayout(), true);
135
136 this.helper = parent;
137
138 pane = new JPanel();
139 pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
140
141 Integer icolor = Instance.getUiConfig().getColor(
142 UiConfig.BACKGROUND_COLOR);
143 if (icolor != null) {
144 color = new Color(icolor);
145 setBackground(color);
146 pane.setBackground(color);
147 }
148
149 JScrollPane scroll = new JScrollPane(pane);
150 scroll.getVerticalScrollBar().setUnitIncrement(16);
151 add(scroll, BorderLayout.CENTER);
152
153 String message = parent.getReader().getLibrary().getLibraryName();
154 if (!message.isEmpty()) {
155 JLabel name = new JLabel(message, SwingConstants.CENTER);
156 add(name, BorderLayout.NORTH);
157 }
158
159 pgBar = new ProgressBar();
160 add(pgBar, BorderLayout.SOUTH);
161
162 pgBar.addActionListener(new ActionListener() {
163 @Override
164 public void actionPerformed(ActionEvent e) {
165 pgBar.invalidate();
166 pgBar.setProgress(null);
167 setEnabled(true);
168 validate();
169 }
170 });
171
172 pgBar.addUpdateListener(new ActionListener() {
173 @Override
174 public void actionPerformed(ActionEvent e) {
175 pgBar.invalidate();
176 validate();
177 repaint();
178 }
179 });
180
181 books = new TreeMap<String, GuiReaderGroup>();
182
183 pane.setVisible(false);
184 final Progress pg = new Progress();
185 final String typeF = type;
186 outOfUi(pg, new Runnable() {
187 @Override
188 public void run() {
189 final BasicLibrary lib = helper.getReader().getLibrary();
190 final Status status = lib.getStatus();
191
192 if (status == Status.READY) {
193 lib.refresh(pg);
194 }
195
196 inUi(new Runnable() {
197 @Override
198 public void run() {
199 if (status == Status.READY) {
200 helper.createMenu(true);
201 if (typeF == null) {
202 addBookPane(true, false);
203 } else {
204 addBookPane(typeF, true);
205 }
206 pane.setVisible(true);
207 refreshBooks();
208 } else {
209 helper.createMenu(false);
210 validate();
211
212 String err = lib.getLibraryName() + "\n";
213 switch (status) {
214 case INVALID:
215 err += "Library not valid";
216 break;
217
218 case UNAUTORIZED:
219 err += "You are not allowed to access this library";
220 break;
221
222 case UNAVAILABLE:
223 err += "Library currently unavailable";
224 break;
225
226 default:
227 err += "An error occured when contacting the library";
228 break;
229 }
230
231 error(err, "Library error", null);
232 }
233 }
234 });
235 }
236 });
237 }
238
239 public boolean getCurrentType() {
240 return currentType;
241 }
242
243 /**
244 * Add a new {@link GuiReaderGroup} on the frame to display all the
245 * sources/types or all the authors, or a listing of all the books sorted
246 * either by source or author.
247 * <p>
248 * A display of all the sources/types or all the authors will show one icon
249 * per source/type or author.
250 * <p>
251 * A listing of all the books sorted by source/type or author will display
252 * all the books.
253 *
254 * @param type
255 * TRUE for type/source, FALSE for author
256 * @param listMode
257 * TRUE to get a listing of all the sources or authors, FALSE to
258 * get one icon per source or author
259 */
260 public void addBookPane(boolean type, boolean listMode) {
261 this.currentType = type;
262 BasicLibrary lib = helper.getReader().getLibrary();
263 if (type) {
264 if (!listMode) {
265 addListPane("Sources", lib.getSources(), type);
266 } else {
267 for (String tt : lib.getSources()) {
268 if (tt != null) {
269 addBookPane(tt, type);
270 }
271 }
272 }
273 } else {
274 if (!listMode) {
275 addListPane("Authors", lib.getAuthors(), type);
276 } else {
277 for (String tt : lib.getAuthors()) {
278 if (tt != null) {
279 addBookPane(tt, type);
280 }
281 }
282 }
283 }
284 }
285
286 /**
287 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
288 * selected type or author.
289 * <p>
290 * Will invalidate the layout.
291 *
292 * @param value
293 * the author or the type, or NULL to get all the
294 * authors-or-types
295 * @param type
296 * TRUE for type/source, FALSE for author
297 */
298 public void addBookPane(String value, boolean type) {
299 this.currentType = type;
300
301 GuiReaderGroup bookPane = new GuiReaderGroup(helper.getReader(), value,
302 color);
303
304 books.put(value, bookPane);
305
306 pane.invalidate();
307 pane.add(bookPane);
308
309 bookPane.setActionListener(new BookActionListener() {
310 @Override
311 public void select(GuiReaderBook book) {
312 selectedBook = book;
313 }
314
315 @Override
316 public void popupRequested(GuiReaderBook book, MouseEvent e) {
317 JPopupMenu popup = helper.createBookPopup();
318 popup.show(e.getComponent(), e.getX(), e.getY());
319 }
320
321 @Override
322 public void action(final GuiReaderBook book) {
323 openBook(book);
324 }
325 });
326 }
327
328 /**
329 * Clear the pane from any book that may be present, usually prior to adding
330 * new ones.
331 * <p>
332 * Will invalidate the layout.
333 */
334 public void removeBookPanes() {
335 books.clear();
336 pane.invalidate();
337 pane.removeAll();
338 }
339
340 /**
341 * Refresh the list of {@link GuiReaderBook}s from disk.
342 * <p>
343 * Will validate the layout, as it is a "refresh" operation.
344 */
345 public void refreshBooks() {
346 BasicLibrary lib = helper.getReader().getLibrary();
347 for (String value : books.keySet()) {
348 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
349
350 List<MetaData> metas;
351 if (currentType) {
352 metas = lib.getListBySource(value);
353 } else {
354 metas = lib.getListByAuthor(value);
355 }
356 for (MetaData meta : metas) {
357 infos.add(GuiReaderBookInfo.fromMeta(meta));
358 }
359
360 books.get(value).refreshBooks(infos, words);
361 }
362
363 if (bookPane != null) {
364 bookPane.refreshBooks(words);
365 }
366
367 this.validate();
368 }
369
370 /**
371 * Open a {@link GuiReaderBook} item.
372 *
373 * @param book
374 * the {@link GuiReaderBook} to open
375 */
376 public void openBook(final GuiReaderBook book) {
377 final Progress pg = new Progress();
378 outOfUi(pg, new Runnable() {
379 @Override
380 public void run() {
381 try {
382 helper.getReader().read(book.getInfo().getMeta().getLuid(),
383 false, pg);
384 SwingUtilities.invokeLater(new Runnable() {
385 @Override
386 public void run() {
387 book.setCached(true);
388 }
389 });
390 } catch (IOException e) {
391 Instance.getTraceHandler().error(e);
392 error("Cannot open the selected book", "Error", e);
393 }
394 }
395 });
396 }
397
398 /**
399 * Process the given action out of the Swing UI thread and link the given
400 * {@link ProgressBar} to the action.
401 * <p>
402 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
403 * to done when the action is done.
404 *
405 * @param progress
406 * the {@link ProgressBar} or NULL
407 * @param run
408 * the action to run
409 */
410 public void outOfUi(Progress progress, final Runnable run) {
411 final Progress pg = new Progress();
412 final Progress reload = new Progress("Reload books");
413 if (progress == null) {
414 progress = new Progress();
415 }
416
417 pg.addProgress(progress, 90);
418 pg.addProgress(reload, 10);
419
420 invalidate();
421 pgBar.setProgress(pg);
422 validate();
423 setEnabled(false);
424
425 new Thread(new Runnable() {
426 @Override
427 public void run() {
428 try {
429 run.run();
430 refreshBooks();
431 } finally {
432 reload.done();
433 if (!pg.isDone()) {
434 // will trigger pgBar ActionListener:
435 pg.done();
436 }
437 }
438 }
439 }, "outOfUi thread").start();
440 }
441
442 /**
443 * Process the given action in the main Swing UI thread.
444 * <p>
445 * The code will make sure the current thread is the main UI thread and, if
446 * not, will switch to it before executing the runnable.
447 * <p>
448 * Synchronous operation.
449 *
450 * @param run
451 * the action to run
452 */
453 public void inUi(final Runnable run) {
454 if (EventQueue.isDispatchThread()) {
455 run.run();
456 } else {
457 try {
458 EventQueue.invokeAndWait(run);
459 } catch (InterruptedException e) {
460 Instance.getTraceHandler().error(e);
461 } catch (InvocationTargetException e) {
462 Instance.getTraceHandler().error(e);
463 }
464 }
465 }
466
467 /**
468 * Import a {@link Story} into the main {@link LocalLibrary}.
469 * <p>
470 * Should be called inside the UI thread.
471 *
472 * @param askUrl
473 * TRUE for an {@link URL}, false for a {@link File}
474 */
475 public void imprt(boolean askUrl) {
476 JFileChooser fc = new JFileChooser();
477
478 Object url;
479 if (askUrl) {
480 String clipboard = "";
481 try {
482 clipboard = ("" + Toolkit.getDefaultToolkit()
483 .getSystemClipboard().getData(DataFlavor.stringFlavor))
484 .trim();
485 } catch (Exception e) {
486 // No data will be handled
487 }
488
489 if (clipboard == null || !clipboard.startsWith("http")) {
490 clipboard = "";
491 }
492
493 url = JOptionPane.showInputDialog(GuiReaderMainPanel.this,
494 "url of the story to import?", "Importing from URL",
495 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
496 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
497 url = fc.getSelectedFile().getAbsolutePath();
498 } else {
499 url = null;
500 }
501
502 if (url != null && !url.toString().isEmpty()) {
503 imprt(url.toString(), null, null);
504 }
505 }
506
507 /**
508 * Actually import the {@link Story} into the main {@link LocalLibrary}.
509 * <p>
510 * Should be called inside the UI thread.
511 *
512 * @param url
513 * the {@link Story} to import by {@link URL}
514 * @param onSuccess
515 * Action to execute on success
516 */
517 public void imprt(final String url, final StoryRunnable onSuccess,
518 String onSuccessPgName) {
519 final Progress pg = new Progress();
520 final Progress pgImprt = new Progress();
521 final Progress pgOnSuccess = new Progress(onSuccessPgName);
522 pg.addProgress(pgImprt, 95);
523 pg.addProgress(pgOnSuccess, 5);
524
525 outOfUi(pg, new Runnable() {
526 @Override
527 public void run() {
528 Exception ex = null;
529 Story story = null;
530 try {
531 story = helper.getReader().getLibrary()
532 .imprt(BasicReader.getUrl(url), pgImprt);
533 } catch (IOException e) {
534 ex = e;
535 }
536
537 final Exception e = ex;
538
539 final boolean ok = (e == null);
540
541 pgOnSuccess.setProgress(0);
542 if (!ok) {
543 if (e instanceof UnknownHostException) {
544 error("URL not supported: " + url, "Cannot import URL",
545 null);
546 } else {
547 error("Failed to import " + url + ": \n"
548 + e.getMessage(), "Cannot import URL", e);
549 }
550 } else {
551 if (onSuccess != null) {
552 onSuccess.run(story);
553 }
554 }
555 pgOnSuccess.done();
556 }
557 });
558 }
559
560 /**
561 * Enables or disables this component, depending on the value of the
562 * parameter <code>b</code>. An enabled component can respond to user input
563 * and generate events. Components are enabled initially by default.
564 * <p>
565 * Enabling or disabling <b>this</b> component will also affect its
566 * children.
567 *
568 * @param b
569 * If <code>true</code>, this component is enabled; otherwise
570 * this component is disabled
571 */
572 @Override
573 public void setEnabled(boolean b) {
574 if (bar != null) {
575 bar.setEnabled(b);
576 }
577
578 for (GuiReaderGroup group : books.values()) {
579 group.setEnabled(b);
580 }
581 super.setEnabled(b);
582 repaint();
583 }
584
585 public void setWords(boolean words) {
586 this.words = words;
587 }
588
589 public GuiReaderBook getSelectedBook() {
590 return selectedBook;
591 }
592
593 public void unsetSelectedBook() {
594 selectedBook = null;
595 }
596
597 private void addListPane(String name, List<String> values,
598 final boolean type) {
599 GuiReader reader = helper.getReader();
600 BasicLibrary lib = reader.getLibrary();
601
602 bookPane = new GuiReaderGroup(reader, name, color);
603
604 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
605 for (String value : values) {
606 if (type) {
607 infos.add(GuiReaderBookInfo.fromSource(lib, value));
608 } else {
609 infos.add(GuiReaderBookInfo.fromAuthor(lib, value));
610 }
611 }
612
613 bookPane.refreshBooks(infos, words);
614
615 this.invalidate();
616 pane.invalidate();
617 pane.add(bookPane);
618 pane.validate();
619 this.validate();
620
621 bookPane.setActionListener(new BookActionListener() {
622 @Override
623 public void select(GuiReaderBook book) {
624 selectedBook = book;
625 }
626
627 @Override
628 public void popupRequested(GuiReaderBook book, MouseEvent e) {
629 JPopupMenu popup = helper.createSourceAuthorPopup();
630 popup.show(e.getComponent(), e.getX(), e.getY());
631 }
632
633 @Override
634 public void action(final GuiReaderBook book) {
635 removeBookPanes();
636 addBookPane(book.getInfo().getMainInfo(), type);
637 refreshBooks();
638 }
639 });
640 }
641
642 /**
643 * Display an error message and log the linked {@link Exception}.
644 *
645 * @param message
646 * the message
647 * @param title
648 * the title of the error message
649 * @param e
650 * the exception to log if any
651 */
652 private void error(final String message, final String title, Exception e) {
653 Instance.getTraceHandler().error(title + ": " + message);
654 if (e != null) {
655 Instance.getTraceHandler().error(e);
656 }
657
658 SwingUtilities.invokeLater(new Runnable() {
659 @Override
660 public void run() {
661 JOptionPane.showMessageDialog(GuiReaderMainPanel.this, message,
662 title, JOptionPane.ERROR_MESSAGE);
663 }
664 });
665 }
666 }