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