gui: French translation
[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.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, MouseEvent e) {
309 JPopupMenu popup = helper.createBookPopup();
310 popup.show(e.getComponent(), e.getX(), e.getY());
311 }
312
313 @Override
314 public void action(final GuiReaderBook book) {
315 openBook(book);
316 }
317 });
318 }
319
320 /**
321 * Clear the pane from any book that may be present, usually prior to adding
322 * new ones.
323 * <p>
324 * Will invalidate the layout.
325 */
326 public void removeBookPanes() {
327 books.clear();
328 pane.invalidate();
329 pane.removeAll();
330 }
331
332 /**
333 * Refresh the list of {@link GuiReaderBook}s from disk.
334 * <p>
335 * Will validate the layout, as it is a "refresh" operation.
336 */
337 public void refreshBooks() {
338 BasicLibrary lib = helper.getReader().getLibrary();
339 for (String value : books.keySet()) {
340 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
341
342 List<MetaData> metas;
343 if (currentType) {
344 metas = lib.getListBySource(value);
345 } else {
346 metas = lib.getListByAuthor(value);
347 }
348 for (MetaData meta : metas) {
349 infos.add(GuiReaderBookInfo.fromMeta(meta));
350 }
351
352 books.get(value).refreshBooks(infos, words);
353 }
354
355 if (bookPane != null) {
356 bookPane.refreshBooks(words);
357 }
358
359 this.validate();
360 }
361
362 /**
363 * Open a {@link GuiReaderBook} item.
364 *
365 * @param book
366 * the {@link GuiReaderBook} to open
367 */
368 public void openBook(final GuiReaderBook book) {
369 final Progress pg = new Progress();
370 outOfUi(pg, false, new Runnable() {
371 @Override
372 public void run() {
373 try {
374 helper.getReader().read(book.getInfo().getMeta().getLuid(),
375 false, pg);
376 SwingUtilities.invokeLater(new Runnable() {
377 @Override
378 public void run() {
379 book.setCached(true);
380 }
381 });
382 } catch (IOException e) {
383 Instance.getTraceHandler().error(e);
384 error(GuiReader.trans(StringIdGui.ERROR_CANNOT_OPEN),
385 GuiReader.trans(StringIdGui.TITLE_ERROR), e);
386 }
387 }
388 });
389 }
390
391 /**
392 * Process the given action out of the Swing UI thread and link the given
393 * {@link ProgressBar} to the action.
394 * <p>
395 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
396 * to done when the action is done.
397 *
398 * @param progress
399 * the {@link ProgressBar} or NULL
400 * @param refreshBooks
401 * TRUE to refresh the books after
402 * @param run
403 * the action to run
404 */
405 public void outOfUi(Progress progress, final boolean refreshBooks,
406 final Runnable run) {
407 final Progress pg = new Progress();
408 final Progress reload = new Progress(
409 GuiReader.trans(StringIdGui.PROGRESS_OUT_OF_UI_RELOAD_BOOKS));
410
411 if (progress == null) {
412 progress = new Progress();
413 }
414
415 if (refreshBooks) {
416 pg.addProgress(progress, 100);
417 } else {
418 pg.addProgress(progress, 90);
419 pg.addProgress(reload, 10);
420 }
421
422 invalidate();
423 pgBar.setProgress(pg);
424 validate();
425 setEnabled(false);
426
427 new Thread(new Runnable() {
428 @Override
429 public void run() {
430 try {
431 run.run();
432 if (refreshBooks) {
433 refreshBooks();
434 }
435 } finally {
436 reload.done();
437 if (!pg.isDone()) {
438 // will trigger pgBar ActionListener:
439 pg.done();
440 }
441 }
442 }
443 }, "outOfUi thread").start();
444 }
445
446 /**
447 * Process the given action in the main Swing UI thread.
448 * <p>
449 * The code will make sure the current thread is the main UI thread and, if
450 * not, will switch to it before executing the runnable.
451 * <p>
452 * Synchronous operation.
453 *
454 * @param run
455 * the action to run
456 */
457 public void inUi(final Runnable run) {
458 if (EventQueue.isDispatchThread()) {
459 run.run();
460 } else {
461 try {
462 EventQueue.invokeAndWait(run);
463 } catch (InterruptedException e) {
464 Instance.getTraceHandler().error(e);
465 } catch (InvocationTargetException e) {
466 Instance.getTraceHandler().error(e);
467 }
468 }
469 }
470
471 /**
472 * Import a {@link Story} into the main {@link LocalLibrary}.
473 * <p>
474 * Should be called inside the UI thread.
475 *
476 * @param askUrl
477 * TRUE for an {@link URL}, false for a {@link File}
478 */
479 public void imprt(boolean askUrl) {
480 JFileChooser fc = new JFileChooser();
481
482 Object url;
483 if (askUrl) {
484 String clipboard = "";
485 try {
486 clipboard = ("" + Toolkit.getDefaultToolkit()
487 .getSystemClipboard().getData(DataFlavor.stringFlavor))
488 .trim();
489 } catch (Exception e) {
490 // No data will be handled
491 }
492
493 if (clipboard == null || !clipboard.startsWith("http")) {
494 clipboard = "";
495 }
496
497 url = JOptionPane.showInputDialog(GuiReaderMainPanel.this,
498 GuiReader.trans(StringIdGui.SUBTITLE_IMPORT_URL),
499 GuiReader.trans(StringIdGui.TITLE_IMPORT_URL),
500 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
501 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
502 url = fc.getSelectedFile().getAbsolutePath();
503 } else {
504 url = null;
505 }
506
507 if (url != null && !url.toString().isEmpty()) {
508 imprt(url.toString(), null, null);
509 }
510 }
511
512 /**
513 * Actually import the {@link Story} into the main {@link LocalLibrary}.
514 * <p>
515 * Should be called inside the UI thread.
516 *
517 * @param url
518 * the {@link Story} to import by {@link URL}
519 * @param onSuccess
520 * Action to execute on success
521 * @param onSuccessPgName
522 * the name to use for the onSuccess progress bar
523 */
524 public void imprt(final String url, final StoryRunnable onSuccess,
525 String onSuccessPgName) {
526 final Progress pg = new Progress();
527 final Progress pgImprt = new Progress();
528 final Progress pgOnSuccess = new Progress(onSuccessPgName);
529 pg.addProgress(pgImprt, 95);
530 pg.addProgress(pgOnSuccess, 5);
531
532 outOfUi(pg, true, new Runnable() {
533 @Override
534 public void run() {
535 Exception ex = null;
536 Story story = null;
537 try {
538 story = helper.getReader().getLibrary()
539 .imprt(BasicReader.getUrl(url), pgImprt);
540 } catch (IOException e) {
541 ex = e;
542 }
543
544 final Exception e = ex;
545
546 final boolean ok = (e == null);
547
548 pgOnSuccess.setProgress(0);
549 if (!ok) {
550 if (e instanceof UnknownHostException) {
551 error(GuiReader.trans(
552 StringIdGui.ERROR_URL_NOT_SUPPORTED, url),
553 GuiReader.trans(StringIdGui.TITLE_ERROR), null);
554 } else {
555 error(GuiReader.trans(
556 StringIdGui.ERROR_URL_IMPORT_FAILED, url,
557 e.getMessage()), GuiReader
558 .trans(StringIdGui.TITLE_ERROR), e);
559 }
560 } else {
561 if (onSuccess != null) {
562 onSuccess.run(story);
563 }
564 }
565 pgOnSuccess.done();
566 }
567 });
568 }
569
570 /**
571 * Enables or disables this component, depending on the value of the
572 * parameter <code>b</code>. An enabled component can respond to user input
573 * and generate events. Components are enabled initially by default.
574 * <p>
575 * Enabling or disabling <b>this</b> component will also affect its
576 * children.
577 *
578 * @param b
579 * If <code>true</code>, this component is enabled; otherwise
580 * this component is disabled
581 */
582 @Override
583 public void setEnabled(boolean b) {
584 if (bar != null) {
585 bar.setEnabled(b);
586 }
587
588 for (GuiReaderGroup group : books.values()) {
589 group.setEnabled(b);
590 }
591 super.setEnabled(b);
592 repaint();
593 }
594
595 public void setWords(boolean words) {
596 this.words = words;
597 }
598
599 public GuiReaderBook getSelectedBook() {
600 return selectedBook;
601 }
602
603 public void unsetSelectedBook() {
604 selectedBook = null;
605 }
606
607 private void addListPane(String name, List<String> values,
608 final boolean type) {
609 GuiReader reader = helper.getReader();
610 BasicLibrary lib = reader.getLibrary();
611
612 bookPane = new GuiReaderGroup(reader, name, color);
613
614 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
615 for (String value : values) {
616 if (type) {
617 infos.add(GuiReaderBookInfo.fromSource(lib, value));
618 } else {
619 infos.add(GuiReaderBookInfo.fromAuthor(lib, value));
620 }
621 }
622
623 bookPane.refreshBooks(infos, words);
624
625 this.invalidate();
626 pane.invalidate();
627 pane.add(bookPane);
628 pane.validate();
629 this.validate();
630
631 bookPane.setActionListener(new BookActionListener() {
632 @Override
633 public void select(GuiReaderBook book) {
634 selectedBook = book;
635 }
636
637 @Override
638 public void popupRequested(GuiReaderBook book, MouseEvent e) {
639 JPopupMenu popup = helper.createSourceAuthorPopup();
640 popup.show(e.getComponent(), e.getX(), e.getY());
641 }
642
643 @Override
644 public void action(final GuiReaderBook book) {
645 removeBookPanes();
646 addBookPane(book.getInfo().getMainInfo(), type);
647 refreshBooks();
648 }
649 });
650 }
651
652 /**
653 * Display an error message and log the linked {@link Exception}.
654 *
655 * @param message
656 * the message
657 * @param title
658 * the title of the error message
659 * @param e
660 * the exception to log if any
661 */
662 private void error(final String message, final String title, Exception e) {
663 Instance.getTraceHandler().error(title + ": " + message);
664 if (e != null) {
665 Instance.getTraceHandler().error(e);
666 }
667
668 SwingUtilities.invokeLater(new Runnable() {
669 @Override
670 public void run() {
671 JOptionPane.showMessageDialog(GuiReaderMainPanel.this, message,
672 title, JOptionPane.ERROR_MESSAGE);
673 }
674 });
675 }
676 }