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