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