revalidate: not in java 1.6
[fanfix.git] / src / be / nikiroo / fanfix / reader / LocalReaderFrame.java
1 package be.nikiroo.fanfix.reader;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Frame;
6 import java.awt.Toolkit;
7 import java.awt.datatransfer.DataFlavor;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.awt.event.KeyEvent;
11 import java.awt.event.MouseEvent;
12 import java.awt.event.WindowEvent;
13 import java.io.File;
14 import java.io.IOException;
15 import java.net.URL;
16 import java.util.HashMap;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.Map.Entry;
20
21 import javax.swing.BoxLayout;
22 import javax.swing.JFileChooser;
23 import javax.swing.JFrame;
24 import javax.swing.JMenu;
25 import javax.swing.JMenuBar;
26 import javax.swing.JMenuItem;
27 import javax.swing.JOptionPane;
28 import javax.swing.JPanel;
29 import javax.swing.JPopupMenu;
30 import javax.swing.JScrollPane;
31 import javax.swing.SwingUtilities;
32 import javax.swing.filechooser.FileFilter;
33 import javax.swing.filechooser.FileNameExtensionFilter;
34
35 import be.nikiroo.fanfix.Instance;
36 import be.nikiroo.fanfix.Library;
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.output.BasicOutput.OutputType;
41 import be.nikiroo.fanfix.reader.LocalReaderBook.BookActionListener;
42 import be.nikiroo.utils.Progress;
43 import be.nikiroo.utils.Version;
44 import be.nikiroo.utils.ui.ProgressBar;
45
46 /**
47 * A {@link Frame} that will show a {@link LocalReaderBook} item for each
48 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
49 * way to copy them to the {@link LocalReader} cache ({@link LocalReader#lib}),
50 * read them, delete them...
51 *
52 * @author niki
53 */
54 class LocalReaderFrame extends JFrame {
55 private static final long serialVersionUID = 1L;
56 private LocalReader reader;
57 private Map<LocalReaderGroup, String> booksByType;
58 private Map<LocalReaderGroup, String> booksByAuthor;
59 private JPanel pane;
60 private Color color;
61 private ProgressBar pgBar;
62 private JMenuBar bar;
63 private LocalReaderBook selectedBook;
64 private boolean words; // words or authors (secondary info on books)
65
66 /**
67 * Create a new {@link LocalReaderFrame}.
68 *
69 * @param reader
70 * the associated {@link LocalReader} to forward some commands
71 * and access its {@link Library}
72 * @param type
73 * the type of {@link Story} to load, or NULL for all types
74 */
75 public LocalReaderFrame(LocalReader reader, String type) {
76 super(String.format("Fanfix %s Library", Version.getCurrentVersion()));
77
78 this.reader = reader;
79
80 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
81 setSize(800, 600);
82 setLayout(new BorderLayout());
83
84 pane = new JPanel();
85 pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
86
87 color = Instance.getUiConfig().getColor(UiConfig.BACKGROUND_COLOR);
88 if (color != null) {
89 setBackground(color);
90 pane.setBackground(color);
91 }
92
93 JScrollPane scroll = new JScrollPane(pane);
94 scroll.getVerticalScrollBar().setUnitIncrement(16);
95 add(scroll, BorderLayout.CENTER);
96
97 pgBar = new ProgressBar();
98 add(pgBar, BorderLayout.SOUTH);
99
100 pgBar.addActionListener(new ActionListener() {
101 public void actionPerformed(ActionEvent e) {
102 invalidate();
103 pgBar.setProgress(null);
104 validate();
105 setEnabled(true);
106 }
107 });
108
109 pgBar.addUpdateListener(new ActionListener() {
110 public void actionPerformed(ActionEvent e) {
111 invalidate();
112 validate();
113 repaint();
114 }
115 });
116
117 setJMenuBar(createMenu());
118
119 booksByType = new HashMap<LocalReaderGroup, String>();
120 booksByAuthor = new HashMap<LocalReaderGroup, String>();
121
122 addBookPane(type, true);
123 refreshBooks();
124
125 setVisible(true);
126 }
127
128 /**
129 * Add a new {@link LocalReaderGroup} on the frame to display the books of
130 * the selected type or author.
131 *
132 * @param value
133 * the author or the type
134 * @param type
135 * TRUE for type, FALSE for author
136 */
137 private void addBookPane(String value, boolean type) {
138 if (value == null) {
139 if (type) {
140 for (String tt : Instance.getLibrary().getTypes()) {
141 if (tt != null) {
142 addBookPane(tt, type);
143 }
144 }
145 } else {
146 for (String tt : Instance.getLibrary().getAuthors()) {
147 if (tt != null) {
148 addBookPane(tt, type);
149 }
150 }
151 }
152
153 return;
154 }
155
156 LocalReaderGroup bookPane = new LocalReaderGroup(reader, value, color);
157 if (type) {
158 booksByType.put(bookPane, value);
159 } else {
160 booksByAuthor.put(bookPane, value);
161 }
162
163 this.invalidate();
164 pane.invalidate();
165 pane.add(bookPane);
166 pane.validate();
167 this.validate();
168
169 bookPane.setActionListener(new BookActionListener() {
170 public void select(LocalReaderBook book) {
171 selectedBook = book;
172 }
173
174 public void popupRequested(LocalReaderBook book, MouseEvent e) {
175 JPopupMenu popup = new JPopupMenu();
176 popup.add(createMenuItemOpenBook());
177 popup.addSeparator();
178 popup.add(createMenuItemExport());
179 popup.add(createMenuItemClearCache());
180 popup.add(createMenuItemRedownload());
181 popup.addSeparator();
182 popup.add(createMenuItemDelete());
183 popup.show(e.getComponent(), e.getX(), e.getY());
184 }
185
186 public void action(final LocalReaderBook book) {
187 openBook(book);
188 }
189 });
190 }
191
192 private void removeBookPanes() {
193 booksByType.clear();
194 booksByAuthor.clear();
195 pane.invalidate();
196 this.invalidate();
197 pane.removeAll();
198 pane.validate();
199 this.validate();
200 }
201
202 /**
203 * Refresh the list of {@link LocalReaderBook}s from disk.
204 *
205 */
206 private void refreshBooks() {
207 for (LocalReaderGroup group : booksByType.keySet()) {
208 List<MetaData> stories = Instance.getLibrary().getListByType(
209 booksByType.get(group));
210 group.refreshBooks(stories, words);
211 }
212
213 for (LocalReaderGroup group : booksByAuthor.keySet()) {
214 List<MetaData> stories = Instance.getLibrary().getListByAuthor(
215 booksByAuthor.get(group));
216 group.refreshBooks(stories, words);
217 }
218
219 pane.repaint();
220 this.repaint();
221 }
222
223 /**
224 * Create the main menu bar.
225 *
226 * @return the bar
227 */
228 private JMenuBar createMenu() {
229 bar = new JMenuBar();
230
231 JMenu file = new JMenu("File");
232 file.setMnemonic(KeyEvent.VK_F);
233
234 JMenuItem imprt = new JMenuItem("Import URL...", KeyEvent.VK_U);
235 imprt.addActionListener(new ActionListener() {
236 public void actionPerformed(ActionEvent e) {
237 imprt(true);
238 }
239 });
240 JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
241 imprtF.addActionListener(new ActionListener() {
242 public void actionPerformed(ActionEvent e) {
243 imprt(false);
244 }
245 });
246 JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X);
247 exit.addActionListener(new ActionListener() {
248 public void actionPerformed(ActionEvent e) {
249 LocalReaderFrame.this.dispatchEvent(new WindowEvent(
250 LocalReaderFrame.this, WindowEvent.WINDOW_CLOSING));
251 }
252 });
253
254 file.add(createMenuItemOpenBook());
255 file.add(createMenuItemExport());
256 file.addSeparator();
257 file.add(imprt);
258 file.add(imprtF);
259 file.addSeparator();
260 file.add(exit);
261
262 bar.add(file);
263
264 JMenu edit = new JMenu("Edit");
265 edit.setMnemonic(KeyEvent.VK_E);
266
267 edit.add(createMenuItemClearCache());
268 edit.add(createMenuItemRedownload());
269 edit.addSeparator();
270 edit.add(createMenuItemDelete());
271
272 bar.add(edit);
273
274 JMenu view = new JMenu("View");
275 view.setMnemonic(KeyEvent.VK_V);
276 JMenuItem vauthors = new JMenuItem("Author");
277 vauthors.setMnemonic(KeyEvent.VK_A);
278 vauthors.addActionListener(new ActionListener() {
279 public void actionPerformed(ActionEvent e) {
280 words = false;
281 refreshBooks();
282 }
283 });
284 view.add(vauthors);
285 JMenuItem vwords = new JMenuItem("Word count");
286 vwords.setMnemonic(KeyEvent.VK_W);
287 vwords.addActionListener(new ActionListener() {
288 public void actionPerformed(ActionEvent e) {
289 words = true;
290 refreshBooks();
291 }
292 });
293 view.add(vwords);
294 bar.add(view);
295
296 JMenu sources = new JMenu("Sources");
297 sources.setMnemonic(KeyEvent.VK_S);
298
299 List<String> tt = Instance.getLibrary().getTypes();
300 tt.add(0, null);
301 for (final String type : tt) {
302 JMenuItem item = new JMenuItem(type == null ? "All" : type);
303 item.addActionListener(new ActionListener() {
304 public void actionPerformed(ActionEvent e) {
305 removeBookPanes();
306 addBookPane(type, true);
307 refreshBooks();
308 }
309 });
310 sources.add(item);
311
312 if (type == null) {
313 sources.addSeparator();
314 }
315 }
316
317 bar.add(sources);
318
319 JMenu authors = new JMenu("Authors");
320 authors.setMnemonic(KeyEvent.VK_A);
321
322 List<String> aa = Instance.getLibrary().getAuthors();
323 aa.add(0, null);
324 for (final String author : aa) {
325 JMenuItem item = new JMenuItem(author == null ? "All"
326 : author.isEmpty() ? "[unknown]" : author);
327 item.addActionListener(new ActionListener() {
328 public void actionPerformed(ActionEvent e) {
329 removeBookPanes();
330 addBookPane(author, false);
331 refreshBooks();
332 }
333 });
334 authors.add(item);
335
336 if (author == null || author.isEmpty()) {
337 authors.addSeparator();
338 }
339 }
340
341 bar.add(authors);
342
343 return bar;
344 }
345
346 /**
347 * Create the export menu item.
348 *
349 * @return the item
350 */
351 private JMenuItem createMenuItemExport() {
352 final JFileChooser fc = new JFileChooser();
353 fc.setAcceptAllFileFilterUsed(false);
354
355 final Map<FileFilter, OutputType> filters = new HashMap<FileFilter, OutputType>();
356 for (OutputType type : OutputType.values()) {
357 String ext = type.getDefaultExtension(false);
358 String desc = type.getDesc(false);
359
360 if (ext == null || ext.isEmpty()) {
361 filters.put(createAllFilter(desc), type);
362 } else {
363 filters.put(new FileNameExtensionFilter(desc, ext), type);
364 }
365 }
366
367 // First the "ALL" filters, then, the extension filters
368 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
369 if (!(entry.getKey() instanceof FileNameExtensionFilter)) {
370 fc.addChoosableFileFilter(entry.getKey());
371 }
372 }
373 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
374 if (entry.getKey() instanceof FileNameExtensionFilter) {
375 fc.addChoosableFileFilter(entry.getKey());
376 }
377 }
378 //
379
380 JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S);
381 export.addActionListener(new ActionListener() {
382 public void actionPerformed(ActionEvent e) {
383 if (selectedBook != null) {
384 fc.showDialog(LocalReaderFrame.this, "Save");
385 if (fc.getSelectedFile() != null) {
386 final OutputType type = filters.get(fc.getFileFilter());
387 final String path = fc.getSelectedFile()
388 .getAbsolutePath()
389 + type.getDefaultExtension(false);
390 final Progress pg = new Progress();
391 outOfUi(pg, new Runnable() {
392 public void run() {
393 try {
394 Instance.getLibrary().export(
395 selectedBook.getMeta().getLuid(),
396 type, path, pg);
397 } catch (IOException e) {
398 Instance.syserr(e);
399 }
400 }
401 });
402 }
403 }
404 }
405 });
406
407 return export;
408 }
409
410 /**
411 * Create a {@link FileFilter} that accepts all files and return the given
412 * description.
413 *
414 * @param desc
415 * the description
416 *
417 * @return the filter
418 */
419 private FileFilter createAllFilter(final String desc) {
420 return new FileFilter() {
421 @Override
422 public String getDescription() {
423 return desc;
424 }
425
426 @Override
427 public boolean accept(File f) {
428 return true;
429 }
430 };
431 }
432
433 /**
434 * Create the refresh (delete cache) menu item.
435 *
436 * @return the item
437 */
438 private JMenuItem createMenuItemClearCache() {
439 JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
440 refresh.addActionListener(new ActionListener() {
441 public void actionPerformed(ActionEvent e) {
442 if (selectedBook != null) {
443 outOfUi(null, new Runnable() {
444 public void run() {
445 reader.clearLocalReaderCache(selectedBook.getMeta()
446 .getLuid());
447 selectedBook.setCached(false);
448 SwingUtilities.invokeLater(new Runnable() {
449 public void run() {
450 selectedBook.repaint();
451 }
452 });
453 }
454 });
455 }
456 }
457 });
458
459 return refresh;
460 }
461
462 /**
463 * Create the redownload (then delete original) menu item.
464 *
465 * @return the item
466 */
467 private JMenuItem createMenuItemRedownload() {
468 JMenuItem refresh = new JMenuItem("Redownload", KeyEvent.VK_R);
469 refresh.addActionListener(new ActionListener() {
470 public void actionPerformed(ActionEvent e) {
471 if (selectedBook != null) {
472 final MetaData meta = selectedBook.getMeta();
473 imprt(meta.getUrl(), new Runnable() {
474 public void run() {
475 reader.delete(meta.getLuid());
476 LocalReaderFrame.this.selectedBook = null;
477 }
478 }, "Removing old copy");
479 }
480 }
481 });
482
483 return refresh;
484 }
485
486 /**
487 * Create the delete menu item.
488 *
489 * @return the item
490 */
491 private JMenuItem createMenuItemDelete() {
492 JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D);
493 delete.addActionListener(new ActionListener() {
494 public void actionPerformed(ActionEvent e) {
495 if (selectedBook != null) {
496 outOfUi(null, new Runnable() {
497 public void run() {
498 reader.delete(selectedBook.getMeta().getLuid());
499 selectedBook = null;
500 }
501 });
502 }
503 }
504 });
505
506 return delete;
507 }
508
509 /**
510 * Create the open menu item.
511 *
512 * @return the item
513 */
514 private JMenuItem createMenuItemOpenBook() {
515 JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
516 open.addActionListener(new ActionListener() {
517 public void actionPerformed(ActionEvent e) {
518 if (selectedBook != null) {
519 openBook(selectedBook);
520 }
521 }
522 });
523
524 return open;
525 }
526
527 /**
528 * Open a {@link LocalReaderBook} item.
529 *
530 * @param book
531 * the {@link LocalReaderBook} to open
532 */
533 private void openBook(final LocalReaderBook book) {
534 final Progress pg = new Progress();
535 outOfUi(pg, new Runnable() {
536 public void run() {
537 try {
538 reader.open(book.getMeta().getLuid(), pg);
539 SwingUtilities.invokeLater(new Runnable() {
540 public void run() {
541 book.setCached(true);
542 }
543 });
544 } catch (IOException e) {
545 // TODO: error message?
546 Instance.syserr(e);
547 }
548 }
549 });
550 }
551
552 /**
553 * Process the given action out of the Swing UI thread and link the given
554 * {@link ProgressBar} to the action.
555 * <p>
556 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
557 * to done when the action is done.
558 *
559 * @param pg
560 * the {@link ProgressBar} or NULL
561 * @param run
562 * the action to run
563 */
564 private void outOfUi(Progress progress, final Runnable run) {
565 final Progress pg = new Progress();
566 final Progress reload = new Progress("Reload books");
567 if (progress == null) {
568 progress = new Progress();
569 }
570
571 pg.addProgress(progress, 90);
572 pg.addProgress(reload, 10);
573
574 invalidate();
575 pgBar.setProgress(pg);
576 validate();
577 setEnabled(false);
578
579 new Thread(new Runnable() {
580 public void run() {
581 run.run();
582 refreshBooks();
583 reload.setProgress(100);
584 if (!pg.isDone()) {
585 // will trigger pgBar ActionListener:
586 pg.setProgress(pg.getMax());
587 }
588 }
589 }, "outOfUi thread").start();
590 }
591
592 /**
593 * Import a {@link Story} into the main {@link Library}.
594 * <p>
595 * Should be called inside the UI thread.
596 *
597 * @param askUrl
598 * TRUE for an {@link URL}, false for a {@link File}
599 */
600 private void imprt(boolean askUrl) {
601 JFileChooser fc = new JFileChooser();
602
603 Object url;
604 if (askUrl) {
605 String clipboard = "";
606 try {
607 clipboard = ("" + Toolkit.getDefaultToolkit()
608 .getSystemClipboard().getData(DataFlavor.stringFlavor))
609 .trim();
610 } catch (Exception e) {
611 // No data will be handled
612 }
613
614 if (clipboard == null || !clipboard.startsWith("http")) {
615 clipboard = "";
616 }
617
618 url = JOptionPane.showInputDialog(LocalReaderFrame.this,
619 "url of the story to import?", "Importing from URL",
620 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
621 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
622 url = fc.getSelectedFile().getAbsolutePath();
623 } else {
624 url = null;
625 }
626
627 if (url != null && !url.toString().isEmpty()) {
628 imprt(url.toString(), null, null);
629 }
630 }
631
632 /**
633 * Actually import the {@link Story} into the main {@link Library}.
634 * <p>
635 * Should be called inside the UI thread.
636 *
637 * @param url
638 * the {@link Story} to import by {@link URL}
639 * @param onSuccess
640 * Action to execute on success
641 */
642 private void imprt(final String url, final Runnable onSuccess,
643 String onSuccessPgName) {
644 final Progress pg = new Progress();
645 final Progress pgImprt = new Progress();
646 final Progress pgOnSuccess = new Progress(onSuccessPgName);
647 pg.addProgress(pgImprt, 95);
648 pg.addProgress(pgOnSuccess, 5);
649
650 outOfUi(pg, new Runnable() {
651 public void run() {
652 Exception ex = null;
653 try {
654 Instance.getLibrary().imprt(BasicReader.getUrl(url),
655 pgImprt);
656 } catch (IOException e) {
657 ex = e;
658 }
659
660 final Exception e = ex;
661
662 final boolean ok = (e == null);
663
664 pgOnSuccess.setProgress(0);
665 if (!ok) {
666 Instance.syserr(e);
667 SwingUtilities.invokeLater(new Runnable() {
668 public void run() {
669 JOptionPane.showMessageDialog(
670 LocalReaderFrame.this, "Cannot import: "
671 + url, e.getMessage(),
672 JOptionPane.ERROR_MESSAGE);
673 }
674 });
675 } else {
676 if (onSuccess != null) {
677 onSuccess.run();
678 }
679 }
680 pgOnSuccess.setProgress(100);
681 }
682 });
683 }
684
685 /**
686 * Enables or disables this component, depending on the value of the
687 * parameter <code>b</code>. An enabled component can respond to user input
688 * and generate events. Components are enabled initially by default.
689 * <p>
690 * Disabling this component will also affect its children.
691 *
692 * @param b
693 * If <code>true</code>, this component is enabled; otherwise
694 * this component is disabled
695 */
696 @Override
697 public void setEnabled(boolean b) {
698 bar.setEnabled(b);
699 for (LocalReaderGroup group : booksByType.keySet()) {
700 group.setEnabled(b);
701 }
702 for (LocalReaderGroup group : booksByAuthor.keySet()) {
703 group.setEnabled(b);
704 }
705 super.setEnabled(b);
706 repaint();
707 }
708 }