gui: use submenus for subdirs
[fanfix.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderFrame.java
CommitLineData
16a81ef7 1package be.nikiroo.fanfix.reader.ui;
a6395bef 2
d3c84ac3 3import java.awt.BorderLayout;
b4dc6ab5 4import java.awt.Color;
df6e2d88 5import java.awt.Font;
4d205683 6import java.awt.Frame;
5130ce84
NR
7import java.awt.Toolkit;
8import java.awt.datatransfer.DataFlavor;
a6395bef
NR
9import java.awt.event.ActionEvent;
10import java.awt.event.ActionListener;
333f0e7b 11import java.awt.event.KeyEvent;
9843a5e5 12import java.awt.event.MouseEvent;
333f0e7b
NR
13import java.awt.event.WindowEvent;
14import java.io.File;
a6395bef 15import java.io.IOException;
4d205683 16import java.net.URL;
fd1d31c2 17import java.net.UnknownHostException;
70c9b112 18import java.util.ArrayList;
5f42f329 19import java.util.Arrays;
4d205683 20import java.util.HashMap;
a6395bef 21import java.util.List;
4d205683
NR
22import java.util.Map;
23import java.util.Map.Entry;
a6395bef 24
df6e2d88 25import javax.swing.BorderFactory;
4310bae9 26import javax.swing.BoxLayout;
df6e2d88 27import javax.swing.ImageIcon;
10d558d2 28import javax.swing.JFileChooser;
a6395bef 29import javax.swing.JFrame;
99ccbdf6 30import javax.swing.JLabel;
333f0e7b
NR
31import javax.swing.JMenu;
32import javax.swing.JMenuBar;
33import javax.swing.JMenuItem;
34import javax.swing.JOptionPane;
35import javax.swing.JPanel;
9843a5e5 36import javax.swing.JPopupMenu;
d3c84ac3 37import javax.swing.JScrollPane;
df6e2d88 38import javax.swing.JTextArea;
99ccbdf6 39import javax.swing.SwingConstants;
3b2b638f 40import javax.swing.SwingUtilities;
4d205683
NR
41import javax.swing.filechooser.FileFilter;
42import javax.swing.filechooser.FileNameExtensionFilter;
a6395bef
NR
43
44import be.nikiroo.fanfix.Instance;
e8eeea0a 45import be.nikiroo.fanfix.bundles.Config;
b4dc6ab5 46import be.nikiroo.fanfix.bundles.UiConfig;
a6395bef 47import be.nikiroo.fanfix.data.MetaData;
4d205683 48import be.nikiroo.fanfix.data.Story;
e6249b0f
NR
49import be.nikiroo.fanfix.library.BasicLibrary;
50import be.nikiroo.fanfix.library.BasicLibrary.Status;
e42573a0 51import be.nikiroo.fanfix.library.LocalLibrary;
4d205683 52import be.nikiroo.fanfix.output.BasicOutput.OutputType;
16a81ef7
NR
53import be.nikiroo.fanfix.reader.BasicReader;
54import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
3b2b638f 55import be.nikiroo.utils.Progress;
39c3c689 56import be.nikiroo.utils.Version;
e8eeea0a 57import be.nikiroo.utils.ui.ConfigEditor;
3b2b638f 58import be.nikiroo.utils.ui.ProgressBar;
a6395bef 59
4d205683 60/**
5dd985cf 61 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
4d205683 62 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
5dd985cf
NR
63 * way to copy them to the {@link GuiReader} cache (
64 * {@link BasicReader#getLibrary()}), read them, delete them...
4d205683
NR
65 *
66 * @author niki
67 */
5dd985cf 68class GuiReaderFrame extends JFrame {
a6395bef 69 private static final long serialVersionUID = 1L;
5dd985cf
NR
70 private GuiReader reader;
71 private Map<GuiReaderGroup, String> booksByType;
72 private Map<GuiReaderGroup, String> booksByAuthor;
4310bae9 73 private JPanel pane;
b4dc6ab5 74 private Color color;
3b2b638f
NR
75 private ProgressBar pgBar;
76 private JMenuBar bar;
5dd985cf 77 private GuiReaderBook selectedBook;
793f1071 78 private boolean words; // words or authors (secondary info on books)
a6395bef 79
c8faa52a
NR
80 /**
81 * A {@link Runnable} with a {@link Story} parameter.
82 *
83 * @author niki
84 */
85 private interface StoryRunnable {
86 /**
87 * Run the action.
88 *
89 * @param story
90 * the story
91 */
92 public void run(Story story);
93 }
94
4d205683 95 /**
5dd985cf 96 * Create a new {@link GuiReaderFrame}.
4d205683
NR
97 *
98 * @param reader
5dd985cf
NR
99 * the associated {@link GuiReader} to forward some commands and
100 * access its {@link LocalLibrary}
4d205683
NR
101 * @param type
102 * the type of {@link Story} to load, or NULL for all types
103 */
5dd985cf 104 public GuiReaderFrame(GuiReader reader, String type) {
39c3c689 105 super(String.format("Fanfix %s Library", Version.getCurrentVersion()));
a6395bef
NR
106
107 this.reader = reader;
108
109 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
110 setSize(800, 600);
d3c84ac3 111 setLayout(new BorderLayout());
a6395bef 112
4310bae9
NR
113 pane = new JPanel();
114 pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
b4dc6ab5 115
16a81ef7
NR
116 Integer icolor = Instance.getUiConfig().getColor(
117 UiConfig.BACKGROUND_COLOR);
118 if (icolor != null) {
119 color = new Color(icolor);
b4dc6ab5 120 setBackground(color);
4310bae9 121 pane.setBackground(color);
b4dc6ab5 122 }
d3c84ac3 123
4310bae9 124 JScrollPane scroll = new JScrollPane(pane);
b4dc6ab5
NR
125 scroll.getVerticalScrollBar().setUnitIncrement(16);
126 add(scroll, BorderLayout.CENTER);
a6395bef 127
99ccbdf6
NR
128 String message = reader.getLibrary().getLibraryName();
129 if (!message.isEmpty()) {
130 JLabel name = new JLabel(message, SwingConstants.CENTER);
131 add(name, BorderLayout.NORTH);
132 }
133
3b2b638f
NR
134 pgBar = new ProgressBar();
135 add(pgBar, BorderLayout.SOUTH);
136
754a5bc2 137 pgBar.addActionListener(new ActionListener() {
211f7ddb 138 @Override
754a5bc2
NR
139 public void actionPerformed(ActionEvent e) {
140 invalidate();
141 pgBar.setProgress(null);
142 validate();
143 setEnabled(true);
144 }
145 });
146
147 pgBar.addUpdateListener(new ActionListener() {
211f7ddb 148 @Override
754a5bc2 149 public void actionPerformed(ActionEvent e) {
80791ae8
NR
150 invalidate();
151 validate();
754a5bc2
NR
152 repaint();
153 }
154 });
155
5dd985cf
NR
156 booksByType = new HashMap<GuiReaderGroup, String>();
157 booksByAuthor = new HashMap<GuiReaderGroup, String>();
4310bae9 158
4f661b2b
NR
159 pane.setVisible(false);
160 final Progress pg = new Progress();
161 final String typeF = type;
162 outOfUi(pg, new Runnable() {
211f7ddb 163 @Override
4f661b2b 164 public void run() {
e6249b0f
NR
165 BasicLibrary lib = GuiReaderFrame.this.reader.getLibrary();
166 Status status = lib.getStatus();
167
168 if (status == Status.READY) {
169 lib.refresh(pg);
170 invalidate();
171 setJMenuBar(createMenu(true));
172 addBookPane(typeF, true);
173 refreshBooks();
174 validate();
175 pane.setVisible(true);
176 } else {
177 invalidate();
178 setJMenuBar(createMenu(false));
179 validate();
180
181 String err = lib.getLibraryName() + "\n";
182 switch (status) {
183 case INVALID:
184 err += "Library not valid";
185 break;
186
187 case UNAUTORIZED:
188 err += "You are not allowed to access this library";
189 break;
190
191 case UNAVAILABLE:
c3b229a1 192 err += "Library currently unavailable";
e6249b0f
NR
193 break;
194
195 default:
196 err += "An error occured when contacting the library";
197 break;
198 }
199
200 error(err, "Library error", null);
201 }
4f661b2b
NR
202 }
203 });
4310bae9 204
333f0e7b
NR
205 setVisible(true);
206 }
207
14b57448
NR
208 private void addSourcePanes() {
209 // Sources -> i18n
210 GuiReaderGroup bookPane = new GuiReaderGroup(reader, "Sources", color);
211
212 List<MetaData> sources = new ArrayList<MetaData>();
213 for (String source : reader.getLibrary().getSources()) {
214 MetaData mSource = new MetaData();
215 mSource.setLuid(null);
216 mSource.setTitle(source);
217 mSource.setSource(source);
218 sources.add(mSource);
219 }
220
221 bookPane.refreshBooks(sources, false);
222
223 this.invalidate();
224 pane.invalidate();
225 pane.add(bookPane);
226 pane.validate();
227 this.validate();
228
229 bookPane.setActionListener(new BookActionListener() {
230 @Override
231 public void select(GuiReaderBook book) {
232 selectedBook = book;
233 }
234
235 @Override
236 public void popupRequested(GuiReaderBook book, MouseEvent e) {
237 JPopupMenu popup = new JPopupMenu();
238 popup.add(createMenuItemOpenBook());
239 popup.show(e.getComponent(), e.getX(), e.getY());
240 }
241
242 @Override
243 public void action(final GuiReaderBook book) {
244 removeBookPanes();
245 addBookPane(book.getMeta().getSource(), true);
246 refreshBooks();
247 }
248 });
249 }
250
4d205683 251 /**
5dd985cf
NR
252 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
253 * selected type or author.
4d205683 254 *
4310bae9 255 * @param value
70c9b112
NR
256 * the author or the type, or NULL to get all the
257 * authors-or-types
4d205683 258 * @param type
4310bae9 259 * TRUE for type, FALSE for author
4d205683 260 */
4310bae9
NR
261 private void addBookPane(String value, boolean type) {
262 if (value == null) {
263 if (type) {
14b57448
NR
264 if (Instance.getUiConfig().getBoolean(UiConfig.SOURCE_PAGE,
265 false)) {
266 addSourcePanes();
267 } else {
268 for (String tt : reader.getLibrary().getSources()) {
269 if (tt != null) {
270 addBookPane(tt, type);
271 }
4310bae9
NR
272 }
273 }
274 } else {
e42573a0 275 for (String tt : reader.getLibrary().getAuthors()) {
4310bae9
NR
276 if (tt != null) {
277 addBookPane(tt, type);
278 }
279 }
b4dc6ab5
NR
280 }
281
4310bae9
NR
282 return;
283 }
4d205683 284
5dd985cf 285 GuiReaderGroup bookPane = new GuiReaderGroup(reader, value, color);
4310bae9
NR
286 if (type) {
287 booksByType.put(bookPane, value);
288 } else {
289 booksByAuthor.put(bookPane, value);
290 }
333f0e7b 291
4310bae9
NR
292 this.invalidate();
293 pane.invalidate();
294 pane.add(bookPane);
295 pane.validate();
296 this.validate();
9843a5e5 297
4310bae9 298 bookPane.setActionListener(new BookActionListener() {
211f7ddb 299 @Override
5dd985cf 300 public void select(GuiReaderBook book) {
4310bae9
NR
301 selectedBook = book;
302 }
303
211f7ddb 304 @Override
5dd985cf 305 public void popupRequested(GuiReaderBook book, MouseEvent e) {
4310bae9
NR
306 JPopupMenu popup = new JPopupMenu();
307 popup.add(createMenuItemOpenBook());
308 popup.addSeparator();
309 popup.add(createMenuItemExport());
c8d48938 310 popup.add(createMenuItemMoveTo(true));
14b57448 311 popup.add(createMenuItemSetCover());
4310bae9
NR
312 popup.add(createMenuItemClearCache());
313 popup.add(createMenuItemRedownload());
314 popup.addSeparator();
c8d48938
NR
315 popup.add(createMenuItemRename(true));
316 popup.add(createMenuItemSetAuthor(true));
317 popup.addSeparator();
4310bae9 318 popup.add(createMenuItemDelete());
df6e2d88
NR
319 popup.addSeparator();
320 popup.add(createMenuItemProperties());
4310bae9
NR
321 popup.show(e.getComponent(), e.getX(), e.getY());
322 }
323
211f7ddb 324 @Override
5dd985cf 325 public void action(final GuiReaderBook book) {
4310bae9
NR
326 openBook(book);
327 }
328 });
329 }
a6395bef 330
4310bae9
NR
331 private void removeBookPanes() {
332 booksByType.clear();
333 booksByAuthor.clear();
334 pane.invalidate();
335 this.invalidate();
336 pane.removeAll();
337 pane.validate();
338 this.validate();
339 }
340
341 /**
5dd985cf 342 * Refresh the list of {@link GuiReaderBook}s from disk.
4310bae9
NR
343 */
344 private void refreshBooks() {
5dd985cf 345 for (GuiReaderGroup group : booksByType.keySet()) {
e42573a0 346 List<MetaData> stories = reader.getLibrary().getListBySource(
4310bae9 347 booksByType.get(group));
793f1071 348 group.refreshBooks(stories, words);
4310bae9
NR
349 }
350
5dd985cf 351 for (GuiReaderGroup group : booksByAuthor.keySet()) {
e42573a0 352 List<MetaData> stories = reader.getLibrary().getListByAuthor(
4310bae9 353 booksByAuthor.get(group));
793f1071 354 group.refreshBooks(stories, words);
a6395bef
NR
355 }
356
4310bae9
NR
357 pane.repaint();
358 this.repaint();
333f0e7b
NR
359 }
360
4d205683
NR
361 /**
362 * Create the main menu bar.
363 *
e6249b0f
NR
364 * @param libOk
365 * the library can be queried
366 *
4d205683
NR
367 * @return the bar
368 */
e6249b0f 369 private JMenuBar createMenu(boolean libOk) {
3b2b638f 370 bar = new JMenuBar();
333f0e7b
NR
371
372 JMenu file = new JMenu("File");
10d558d2 373 file.setMnemonic(KeyEvent.VK_F);
333f0e7b 374
22848428 375 JMenuItem imprt = new JMenuItem("Import URL...", KeyEvent.VK_U);
333f0e7b 376 imprt.addActionListener(new ActionListener() {
211f7ddb 377 @Override
333f0e7b 378 public void actionPerformed(ActionEvent e) {
10d558d2
NR
379 imprt(true);
380 }
381 });
22848428 382 JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
10d558d2 383 imprtF.addActionListener(new ActionListener() {
211f7ddb 384 @Override
10d558d2
NR
385 public void actionPerformed(ActionEvent e) {
386 imprt(false);
387 }
388 });
389 JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X);
390 exit.addActionListener(new ActionListener() {
211f7ddb 391 @Override
10d558d2 392 public void actionPerformed(ActionEvent e) {
5dd985cf
NR
393 GuiReaderFrame.this.dispatchEvent(new WindowEvent(
394 GuiReaderFrame.this, WindowEvent.WINDOW_CLOSING));
10d558d2
NR
395 }
396 });
3b2b638f 397
9843a5e5
NR
398 file.add(createMenuItemOpenBook());
399 file.add(createMenuItemExport());
c8d48938 400 file.add(createMenuItemMoveTo(libOk));
9843a5e5 401 file.addSeparator();
10d558d2
NR
402 file.add(imprt);
403 file.add(imprtF);
404 file.addSeparator();
c8d48938
NR
405 file.add(createMenuItemRename(libOk));
406 file.add(createMenuItemSetAuthor(libOk));
407 file.addSeparator();
10d558d2 408 file.add(exit);
3b2b638f 409
10d558d2
NR
410 bar.add(file);
411
412 JMenu edit = new JMenu("Edit");
413 edit.setMnemonic(KeyEvent.VK_E);
414
22848428
NR
415 edit.add(createMenuItemClearCache());
416 edit.add(createMenuItemRedownload());
9843a5e5
NR
417 edit.addSeparator();
418 edit.add(createMenuItemDelete());
419
420 bar.add(edit);
421
793f1071
NR
422 JMenu view = new JMenu("View");
423 view.setMnemonic(KeyEvent.VK_V);
424 JMenuItem vauthors = new JMenuItem("Author");
425 vauthors.setMnemonic(KeyEvent.VK_A);
426 vauthors.addActionListener(new ActionListener() {
211f7ddb 427 @Override
793f1071
NR
428 public void actionPerformed(ActionEvent e) {
429 words = false;
430 refreshBooks();
431 }
432 });
433 view.add(vauthors);
434 JMenuItem vwords = new JMenuItem("Word count");
435 vwords.setMnemonic(KeyEvent.VK_W);
436 vwords.addActionListener(new ActionListener() {
211f7ddb 437 @Override
793f1071
NR
438 public void actionPerformed(ActionEvent e) {
439 words = true;
440 refreshBooks();
441 }
442 });
443 view.add(vwords);
444 bar.add(view);
445
4310bae9
NR
446 JMenu sources = new JMenu("Sources");
447 sources.setMnemonic(KeyEvent.VK_S);
9843a5e5 448
c1b93db3 449 Map<String, List<String>> groupedSources = new HashMap<String, List<String>>();
e6249b0f 450 if (libOk) {
c1b93db3 451 groupedSources = reader.getLibrary().getSourcesGrouped();
e6249b0f 452 }
e6249b0f 453
c1b93db3
NR
454 JMenuItem item = new JMenuItem("All");
455 item.addActionListener(getActionOpenSource(null));
456 sources.add(item);
457 sources.addSeparator();
458
459 for (final String type : groupedSources.keySet()) {
460 List<String> list = groupedSources.get(type);
461 if (list.size() == 1 && list.get(0).isEmpty()) {
462 item = new JMenuItem(type);
463 item.addActionListener(getActionOpenSource(type));
464 sources.add(item);
465 } else {
466 JMenu dir = new JMenu(type);
467 for (String sub : list) {
468 // " " instead of "" for the visual height
469 String itemName = sub.isEmpty() ? " " : sub;
470 String actualType = type;
471 if (!sub.isEmpty()) {
472 actualType += "/" + sub;
473 }
9843a5e5 474
c1b93db3
NR
475 item = new JMenuItem(itemName);
476 item.addActionListener(getActionOpenSource(actualType));
477 dir.add(item);
478 }
479 sources.add(dir);
9843a5e5
NR
480 }
481 }
10d558d2 482
4310bae9
NR
483 bar.add(sources);
484
485 JMenu authors = new JMenu("Authors");
486 authors.setMnemonic(KeyEvent.VK_A);
487
5f42f329
NR
488 List<Entry<String, List<String>>> authorGroups = reader.getLibrary()
489 .getAuthorsGrouped();
490 if (authorGroups.size() > 1) {
491 // Multiple groups
4310bae9 492
5f42f329
NR
493 // null -> "All" authors special item
494 populateMenuAuthorList(authors, Arrays.asList((String) null));
495
496 for (Entry<String, List<String>> group : authorGroups) {
497 JMenu thisGroup = new JMenu(group.getKey());
498 populateMenuAuthorList(thisGroup, group.getValue());
499 authors.add(thisGroup);
4310bae9 500 }
5f42f329
NR
501 } else {
502 // Only one group
503
504 // null -> "All" authors special item
505 List<String> authorNames = new ArrayList<String>();
506 authorNames.add(null);
507 if (authorGroups.size() > 0) {
508 authorNames.addAll(authorGroups.get(0).getValue());
509 }
510 populateMenuAuthorList(authors, authorNames);
4310bae9
NR
511 }
512
513 bar.add(authors);
9843a5e5 514
e8eeea0a
NR
515 JMenu options = new JMenu("Options");
516 options.setMnemonic(KeyEvent.VK_O);
517 options.add(createMenuItemConfig());
518 options.add(createMenuItemUiConfig());
519 bar.add(options);
520
9843a5e5
NR
521 return bar;
522 }
523
c1b93db3
NR
524 /**
525 * Return an {@link ActionListener} that will set the given source (type) as
526 * the selected/displayed one.
527 *
528 * @param type
529 * the type (source) to select
530 *
531 * @return the {@link ActionListener}
532 */
533 private ActionListener getActionOpenSource(final String type) {
534 return new ActionListener() {
535 @Override
536 public void actionPerformed(ActionEvent e) {
537 removeBookPanes();
538 addBookPane(type, true);
539 refreshBooks();
540 }
541 };
542 }
543
5f42f329
NR
544 /**
545 * Populate a list of authors as {@link JMenuItem}s into the given
546 * {@link JMenu}.
547 * <p>
548 * Each item will select the author when clicked.
549 *
550 * @param authors
551 * the parent {@link JMenuItem}
552 * @param names
553 * the authors' names
554 */
555 private void populateMenuAuthorList(JMenu authors, List<String> names) {
556 for (final String name : names) {
557 JMenuItem item = new JMenuItem(name == null ? "All"
558 : name.isEmpty() ? "[unknown]" : name);
559 item.addActionListener(new ActionListener() {
560 @Override
561 public void actionPerformed(ActionEvent e) {
562 removeBookPanes();
563 addBookPane(name, false);
564 refreshBooks();
565 }
566 });
567 authors.add(item);
568
569 if (name == null || name.isEmpty()) {
570 authors.addSeparator();
571 }
572 }
573 }
574
e8eeea0a
NR
575 /**
576 * Create the Fanfix Configuration menu item.
577 *
578 * @return the item
579 */
580 private JMenuItem createMenuItemConfig() {
581 final String title = "Fanfix Configuration";
582 JMenuItem item = new JMenuItem(title);
583 item.setMnemonic(KeyEvent.VK_F);
584
585 item.addActionListener(new ActionListener() {
211f7ddb 586 @Override
e8eeea0a
NR
587 public void actionPerformed(ActionEvent e) {
588 ConfigEditor<Config> ed = new ConfigEditor<Config>(
589 Config.class, Instance.getConfig(),
590 "This is where you configure the options of the program.");
591 JFrame frame = new JFrame(title);
592 frame.add(ed);
593 frame.setSize(800, 600);
594 frame.setVisible(true);
595 }
596 });
597
598 return item;
599 }
600
601 /**
602 * Create the UI Configuration menu item.
603 *
604 * @return the item
605 */
606 private JMenuItem createMenuItemUiConfig() {
607 final String title = "UI Configuration";
608 JMenuItem item = new JMenuItem(title);
609 item.setMnemonic(KeyEvent.VK_U);
610
611 item.addActionListener(new ActionListener() {
211f7ddb 612 @Override
e8eeea0a
NR
613 public void actionPerformed(ActionEvent e) {
614 ConfigEditor<UiConfig> ed = new ConfigEditor<UiConfig>(
615 UiConfig.class, Instance.getUiConfig(),
616 "This is where you configure the graphical appearence of the program.");
617 JFrame frame = new JFrame(title);
618 frame.add(ed);
619 frame.setSize(800, 600);
620 frame.setVisible(true);
621 }
622 });
623
624 return item;
625 }
626
4d205683
NR
627 /**
628 * Create the export menu item.
629 *
630 * @return the item
631 */
9843a5e5 632 private JMenuItem createMenuItemExport() {
4d205683
NR
633 final JFileChooser fc = new JFileChooser();
634 fc.setAcceptAllFileFilterUsed(false);
635
636 final Map<FileFilter, OutputType> filters = new HashMap<FileFilter, OutputType>();
637 for (OutputType type : OutputType.values()) {
638 String ext = type.getDefaultExtension(false);
639 String desc = type.getDesc(false);
b2612f9d 640
4d205683
NR
641 if (ext == null || ext.isEmpty()) {
642 filters.put(createAllFilter(desc), type);
643 } else {
644 filters.put(new FileNameExtensionFilter(desc, ext), type);
645 }
646 }
647
648 // First the "ALL" filters, then, the extension filters
649 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
650 if (!(entry.getKey() instanceof FileNameExtensionFilter)) {
651 fc.addChoosableFileFilter(entry.getKey());
652 }
653 }
654 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
655 if (entry.getKey() instanceof FileNameExtensionFilter) {
656 fc.addChoosableFileFilter(entry.getKey());
657 }
658 }
659 //
9843a5e5 660
4d205683 661 JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S);
10d558d2 662 export.addActionListener(new ActionListener() {
211f7ddb 663 @Override
10d558d2 664 public void actionPerformed(ActionEvent e) {
4d205683 665 if (selectedBook != null) {
5dd985cf 666 fc.showDialog(GuiReaderFrame.this, "Save");
b2612f9d
NR
667 if (fc.getSelectedFile() != null) {
668 final OutputType type = filters.get(fc.getFileFilter());
669 final String path = fc.getSelectedFile()
670 .getAbsolutePath()
671 + type.getDefaultExtension(false);
672 final Progress pg = new Progress();
673 outOfUi(pg, new Runnable() {
211f7ddb 674 @Override
b2612f9d
NR
675 public void run() {
676 try {
e42573a0 677 reader.getLibrary().export(
b2612f9d
NR
678 selectedBook.getMeta().getLuid(),
679 type, path, pg);
680 } catch (IOException e) {
62c63b07 681 Instance.getTraceHandler().error(e);
b2612f9d 682 }
4d205683 683 }
b2612f9d
NR
684 });
685 }
4d205683 686 }
10d558d2
NR
687 }
688 });
689
9843a5e5
NR
690 return export;
691 }
692
edd46289
NR
693 /**
694 * Create a {@link FileFilter} that accepts all files and return the given
695 * description.
696 *
697 * @param desc
698 * the description
699 *
700 * @return the filter
701 */
4d205683
NR
702 private FileFilter createAllFilter(final String desc) {
703 return new FileFilter() {
704 @Override
705 public String getDescription() {
706 return desc;
707 }
708
709 @Override
710 public boolean accept(File f) {
711 return true;
712 }
713 };
714 }
715
716 /**
717 * Create the refresh (delete cache) menu item.
718 *
719 * @return the item
720 */
22848428 721 private JMenuItem createMenuItemClearCache() {
4d205683 722 JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
10d558d2 723 refresh.addActionListener(new ActionListener() {
211f7ddb 724 @Override
10d558d2
NR
725 public void actionPerformed(ActionEvent e) {
726 if (selectedBook != null) {
727 outOfUi(null, new Runnable() {
211f7ddb 728 @Override
10d558d2 729 public void run() {
754a5bc2
NR
730 reader.clearLocalReaderCache(selectedBook.getMeta()
731 .getLuid());
10d558d2 732 selectedBook.setCached(false);
df6e2d88
NR
733 GuiReaderCoverImager.clearIcon(selectedBook
734 .getMeta());
3b2b638f 735 SwingUtilities.invokeLater(new Runnable() {
211f7ddb 736 @Override
3b2b638f 737 public void run() {
10d558d2 738 selectedBook.repaint();
3b2b638f
NR
739 }
740 });
741 }
742 });
333f0e7b
NR
743 }
744 }
745 });
10d558d2 746
9843a5e5
NR
747 return refresh;
748 }
749
70c9b112 750 /**
c8d48938 751 * Create the "move to" menu item.
70c9b112 752 *
e6249b0f
NR
753 * @param libOk
754 * the library can be queried
755 *
70c9b112
NR
756 * @return the item
757 */
c8d48938 758 private JMenuItem createMenuItemMoveTo(boolean libOk) {
c1b93db3 759 JMenu changeTo = new JMenu("Move to");
c8d48938 760 changeTo.setMnemonic(KeyEvent.VK_M);
70c9b112 761
c1b93db3 762 Map<String, List<String>> groupedSources = new HashMap<String, List<String>>();
e6249b0f 763 if (libOk) {
c1b93db3 764 groupedSources = reader.getLibrary().getSourcesGrouped();
e6249b0f 765 }
70c9b112 766
c1b93db3
NR
767 JMenuItem item = new JMenuItem("New type...");
768 item.addActionListener(createMoveAction("SOURCE", null));
769 changeTo.add(item);
770 changeTo.addSeparator();
70c9b112 771
c1b93db3
NR
772 for (final String type : groupedSources.keySet()) {
773 List<String> list = groupedSources.get(type);
774 if (list.size() == 1 && list.get(0).isEmpty()) {
775 item = new JMenuItem(type);
776 item.addActionListener(createMoveAction("SOURCE", type));
777 changeTo.add(item);
778 } else {
779 JMenu dir = new JMenu(type);
780 for (String sub : list) {
781 // " " instead of "" for the visual height
782 String itemName = sub.isEmpty() ? " " : sub;
783 String actualType = type;
784 if (!sub.isEmpty()) {
785 actualType += "/" + sub;
786 }
70c9b112 787
c1b93db3
NR
788 item = new JMenuItem(itemName);
789 item.addActionListener(createMoveAction("SOURCE",
790 actualType));
791 dir.add(item);
792 }
793 changeTo.add(dir);
c8d48938
NR
794 }
795 }
211f7ddb 796
c8d48938
NR
797 return changeTo;
798 }
70c9b112 799
c8d48938
NR
800 /**
801 * Create the "set author" menu item.
802 *
803 * @param libOk
804 * the library can be queried
805 *
806 * @return the item
807 */
808 private JMenuItem createMenuItemSetAuthor(boolean libOk) {
809 JMenu changeTo = new JMenu("Set author");
810 changeTo.setMnemonic(KeyEvent.VK_A);
70c9b112 811
c8d48938
NR
812 // New author
813 JMenuItem newItem = new JMenuItem("New author...");
814 changeTo.add(newItem);
815 changeTo.addSeparator();
816 newItem.addActionListener(createMoveAction("AUTHOR", null));
70c9b112 817
c8d48938
NR
818 // Existing authors
819 if (libOk) {
820 List<Entry<String, List<String>>> authorGroups = reader
821 .getLibrary().getAuthorsGrouped();
822
823 if (authorGroups.size() > 1) {
824 for (Entry<String, List<String>> entry : authorGroups) {
825 JMenu group = new JMenu(entry.getKey());
826 for (String value : entry.getValue()) {
827 JMenuItem item = new JMenuItem(value);
828 item.addActionListener(createMoveAction("AUTHOR", value));
829 group.add(item);
70c9b112 830 }
c8d48938 831 changeTo.add(group);
70c9b112 832 }
c8d48938
NR
833 } else if (authorGroups.size() == 1) {
834 for (String value : authorGroups.get(0).getValue()) {
835 JMenuItem item = new JMenuItem(value);
836 item.addActionListener(createMoveAction("AUTHOR", value));
837 changeTo.add(item);
838 }
839 }
70c9b112
NR
840 }
841
c8d48938
NR
842 return changeTo;
843 }
844
845 /**
846 * Create the "rename" menu item.
847 *
848 * @param libOk
849 * the library can be queried
850 *
851 * @return the item
852 */
853 private JMenuItem createMenuItemRename(
854 @SuppressWarnings("unused") boolean libOk) {
855 JMenuItem changeTo = new JMenuItem("Rename...");
856 changeTo.setMnemonic(KeyEvent.VK_R);
857 changeTo.addActionListener(createMoveAction("TITLE", null));
858 return changeTo;
859 }
860
861 private ActionListener createMoveAction(final String what, final String type) {
862 return new ActionListener() {
863 @Override
864 public void actionPerformed(ActionEvent e) {
865 if (selectedBook != null) {
866 String changeTo = type;
867 if (type == null) {
868 String init = "";
869 if (what.equals("SOURCE")) {
870 init = selectedBook.getMeta().getSource();
871 } else if (what.equals("TITLE")) {
872 init = selectedBook.getMeta().getTitle();
873 } else if (what.equals("AUTHOR")) {
874 init = selectedBook.getMeta().getAuthor();
875 }
876
877 Object rep = JOptionPane.showInputDialog(
878 GuiReaderFrame.this, "Move to:",
879 "Moving story", JOptionPane.QUESTION_MESSAGE,
880 null, null, init);
881
882 if (rep == null) {
883 return;
884 }
885
886 changeTo = rep.toString();
887 }
888
889 final String fChangeTo = changeTo;
890 outOfUi(null, new Runnable() {
891 @Override
892 public void run() {
893 if (what.equals("SOURCE")) {
894 reader.changeSource(selectedBook.getMeta()
895 .getLuid(), fChangeTo);
896 } else if (what.equals("TITLE")) {
897 reader.changeTitle(selectedBook.getMeta()
898 .getLuid(), fChangeTo);
899 } else if (what.equals("AUTHOR")) {
900 reader.changeAuthor(selectedBook.getMeta()
901 .getLuid(), fChangeTo);
902 }
903
904 selectedBook = null;
905
906 SwingUtilities.invokeLater(new Runnable() {
907 @Override
908 public void run() {
909 setJMenuBar(createMenu(true));
910 }
911 });
912 }
913 });
914 }
915 }
916 };
70c9b112
NR
917 }
918
22848428
NR
919 /**
920 * Create the redownload (then delete original) menu item.
921 *
922 * @return the item
923 */
924 private JMenuItem createMenuItemRedownload() {
925 JMenuItem refresh = new JMenuItem("Redownload", KeyEvent.VK_R);
926 refresh.addActionListener(new ActionListener() {
211f7ddb 927 @Override
22848428
NR
928 public void actionPerformed(ActionEvent e) {
929 if (selectedBook != null) {
754a5bc2 930 final MetaData meta = selectedBook.getMeta();
c8faa52a 931 imprt(meta.getUrl(), new StoryRunnable() {
211f7ddb 932 @Override
c8faa52a 933 public void run(Story story) {
754a5bc2 934 reader.delete(meta.getLuid());
5dd985cf 935 GuiReaderFrame.this.selectedBook = null;
c8faa52a
NR
936 MetaData newMeta = story.getMeta();
937 if (!newMeta.getSource().equals(meta.getSource())) {
c3b229a1 938 reader.changeSource(newMeta.getLuid(),
c8faa52a
NR
939 meta.getSource());
940 }
22848428 941 }
754a5bc2 942 }, "Removing old copy");
22848428
NR
943 }
944 }
945 });
946
947 return refresh;
948 }
949
4d205683
NR
950 /**
951 * Create the delete menu item.
952 *
953 * @return the item
954 */
9843a5e5 955 private JMenuItem createMenuItemDelete() {
10d558d2
NR
956 JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D);
957 delete.addActionListener(new ActionListener() {
211f7ddb 958 @Override
10d558d2
NR
959 public void actionPerformed(ActionEvent e) {
960 if (selectedBook != null) {
961 outOfUi(null, new Runnable() {
211f7ddb 962 @Override
10d558d2 963 public void run() {
22848428 964 reader.delete(selectedBook.getMeta().getLuid());
10d558d2 965 selectedBook = null;
10d558d2
NR
966 }
967 });
968 }
969 }
970 });
971
9843a5e5
NR
972 return delete;
973 }
10d558d2 974
df6e2d88
NR
975 /**
976 * Create the properties menu item.
977 *
978 * @return the item
979 */
980 private JMenuItem createMenuItemProperties() {
981 JMenuItem delete = new JMenuItem("Properties", KeyEvent.VK_P);
982 delete.addActionListener(new ActionListener() {
983 @Override
984 public void actionPerformed(ActionEvent e) {
985 if (selectedBook != null) {
986 outOfUi(null, new Runnable() {
987 @Override
988 public void run() {
989 final MetaData meta = selectedBook.getMeta();
990 new JFrame() {
991 private static final long serialVersionUID = 1L;
992 @SuppressWarnings("unused")
993 private Object init = init();
994
995 private Object init() {
996 // Borders
997 int top = 20;
998 int space = 10;
999
1000 // Image
1001 ImageIcon img = GuiReaderCoverImager
1002 .generateCoverIcon(
1003 reader.getLibrary(), meta);
1004
1005 // frame
1006 setTitle(meta.getLuid() + ": "
1007 + meta.getTitle());
1008
1009 setSize(800, img.getIconHeight() + 2 * top);
1010 setLayout(new BorderLayout());
1011
1012 // Main panel
1013 JPanel mainPanel = new JPanel(
1014 new BorderLayout());
1015 JPanel mainPanelKeys = new JPanel();
1016 mainPanelKeys.setLayout(new BoxLayout(
1017 mainPanelKeys, BoxLayout.Y_AXIS));
1018 JPanel mainPanelValues = new JPanel();
1019 mainPanelValues.setLayout(new BoxLayout(
1020 mainPanelValues, BoxLayout.Y_AXIS));
1021
1022 mainPanel.add(mainPanelKeys,
1023 BorderLayout.WEST);
1024 mainPanel.add(mainPanelValues,
1025 BorderLayout.CENTER);
1026
1027 List<Entry<String, String>> infos = BasicReader
1028 .getMetaDesc(meta);
1029
1030 Color trans = new Color(0, 0, 0, 1);
1031 for (Entry<String, String> info : infos) {
1032 JTextArea key = new JTextArea(info
1033 .getKey());
1034 key.setFont(new Font(key.getFont()
1035 .getFontName(), Font.BOLD, key
1036 .getFont().getSize()));
1037 key.setEditable(false);
1038 key.setLineWrap(false);
1039 key.setBackground(trans);
1040 mainPanelKeys.add(key);
1041
1042 JTextArea value = new JTextArea(info
1043 .getValue());
1044 value.setEditable(false);
1045 value.setLineWrap(false);
1046 value.setBackground(trans);
1047 mainPanelValues.add(value);
1048 }
1049
1050 // Image
1051 JLabel imgLabel = new JLabel(img);
1052 imgLabel.setVerticalAlignment(JLabel.TOP);
1053
1054 // Borders
1055 mainPanelKeys.setBorder(BorderFactory
1056 .createEmptyBorder(top, space, 0, 0));
1057 mainPanelValues.setBorder(BorderFactory
1058 .createEmptyBorder(top, space, 0, 0));
1059 imgLabel.setBorder(BorderFactory
1060 .createEmptyBorder(0, space, 0, 0));
1061
1062 // Add all
1063 add(imgLabel, BorderLayout.WEST);
1064 add(mainPanel, BorderLayout.CENTER);
1065
1066 return null;
1067 }
1068
1069 }.setVisible(true);
1070 }
1071 });
1072 }
1073 }
1074 });
1075
1076 return delete;
1077 }
1078
4d205683 1079 /**
14b57448 1080 * Create the open menu item for a book or a source (no LUID).
4d205683
NR
1081 *
1082 * @return the item
1083 */
9843a5e5
NR
1084 private JMenuItem createMenuItemOpenBook() {
1085 JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
1086 open.addActionListener(new ActionListener() {
211f7ddb 1087 @Override
9843a5e5
NR
1088 public void actionPerformed(ActionEvent e) {
1089 if (selectedBook != null) {
14b57448
NR
1090 if (selectedBook.getMeta().getLuid() == null) {
1091 removeBookPanes();
1092 addBookPane(selectedBook.getMeta().getSource(), true);
1093 refreshBooks();
1094 } else {
1095 openBook(selectedBook);
1096 }
1097 }
1098 }
1099 });
1100
1101 return open;
1102 }
1103
1104 /**
1105 * Create the SetCover menu item for a book to change the linked source
1106 * cover.
1107 *
1108 * @return the item
1109 */
1110 private JMenuItem createMenuItemSetCover() {
1111 JMenuItem open = new JMenuItem("Set as cover for source", KeyEvent.VK_C);
1112 open.addActionListener(new ActionListener() {
1113 @Override
1114 public void actionPerformed(ActionEvent e) {
1115 if (selectedBook != null) {
1116 reader.getLibrary().setSourceCover(
1117 selectedBook.getMeta().getSource(),
1118 selectedBook.getMeta().getLuid());
085a2f9a
NR
1119 MetaData source = selectedBook.getMeta().clone();
1120 source.setLuid(null);
df6e2d88 1121 GuiReaderCoverImager.clearIcon(source);
9843a5e5
NR
1122 }
1123 }
1124 });
10d558d2 1125
9843a5e5
NR
1126 return open;
1127 }
10d558d2 1128
4d205683 1129 /**
5dd985cf 1130 * Open a {@link GuiReaderBook} item.
4d205683
NR
1131 *
1132 * @param book
5dd985cf 1133 * the {@link GuiReaderBook} to open
4d205683 1134 */
5dd985cf 1135 private void openBook(final GuiReaderBook book) {
9843a5e5
NR
1136 final Progress pg = new Progress();
1137 outOfUi(pg, new Runnable() {
211f7ddb 1138 @Override
9843a5e5
NR
1139 public void run() {
1140 try {
350bc060 1141 reader.read(book.getMeta().getLuid(), false, pg);
edd46289 1142 SwingUtilities.invokeLater(new Runnable() {
211f7ddb 1143 @Override
edd46289
NR
1144 public void run() {
1145 book.setCached(true);
9843a5e5 1146 }
edd46289 1147 });
9843a5e5 1148 } catch (IOException e) {
edd46289 1149 // TODO: error message?
62c63b07 1150 Instance.getTraceHandler().error(e);
333f0e7b 1151 }
10d558d2 1152 }
9843a5e5 1153 });
a6395bef 1154 }
3b2b638f 1155
4d205683
NR
1156 /**
1157 * Process the given action out of the Swing UI thread and link the given
1158 * {@link ProgressBar} to the action.
1159 * <p>
1160 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
1161 * to done when the action is done.
1162 *
5dd985cf 1163 * @param progress
4d205683
NR
1164 * the {@link ProgressBar} or NULL
1165 * @param run
1166 * the action to run
1167 */
754a5bc2
NR
1168 private void outOfUi(Progress progress, final Runnable run) {
1169 final Progress pg = new Progress();
d5a7153c 1170 final Progress reload = new Progress("Reload books");
754a5bc2
NR
1171 if (progress == null) {
1172 progress = new Progress();
1173 }
3b2b638f 1174
754a5bc2
NR
1175 pg.addProgress(progress, 90);
1176 pg.addProgress(reload, 10);
1177
1178 invalidate();
1179 pgBar.setProgress(pg);
1180 validate();
a4143cd7 1181 setEnabled(false);
3b2b638f
NR
1182
1183 new Thread(new Runnable() {
211f7ddb 1184 @Override
3b2b638f 1185 public void run() {
925298fd
NR
1186 try {
1187 run.run();
1188 refreshBooks();
1189 } finally {
1190 reload.done();
1191 if (!pg.isDone()) {
1192 // will trigger pgBar ActionListener:
1193 pg.done();
1194 }
3b2b638f
NR
1195 }
1196 }
754a5bc2 1197 }, "outOfUi thread").start();
3b2b638f
NR
1198 }
1199
4d205683 1200 /**
68e2c6d2 1201 * Import a {@link Story} into the main {@link LocalLibrary}.
22848428
NR
1202 * <p>
1203 * Should be called inside the UI thread.
4d205683
NR
1204 *
1205 * @param askUrl
1206 * TRUE for an {@link URL}, false for a {@link File}
1207 */
10d558d2
NR
1208 private void imprt(boolean askUrl) {
1209 JFileChooser fc = new JFileChooser();
1210
5130ce84 1211 Object url;
10d558d2 1212 if (askUrl) {
5130ce84
NR
1213 String clipboard = "";
1214 try {
1215 clipboard = ("" + Toolkit.getDefaultToolkit()
1216 .getSystemClipboard().getData(DataFlavor.stringFlavor))
1217 .trim();
1218 } catch (Exception e) {
1219 // No data will be handled
1220 }
1221
1222 if (clipboard == null || !clipboard.startsWith("http")) {
1223 clipboard = "";
1224 }
1225
5dd985cf 1226 url = JOptionPane.showInputDialog(GuiReaderFrame.this,
10d558d2 1227 "url of the story to import?", "Importing from URL",
5130ce84 1228 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
10d558d2
NR
1229 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
1230 url = fc.getSelectedFile().getAbsolutePath();
1231 } else {
1232 url = null;
1233 }
1234
5130ce84 1235 if (url != null && !url.toString().isEmpty()) {
754a5bc2 1236 imprt(url.toString(), null, null);
22848428
NR
1237 }
1238 }
10d558d2 1239
22848428 1240 /**
68e2c6d2 1241 * Actually import the {@link Story} into the main {@link LocalLibrary}.
22848428
NR
1242 * <p>
1243 * Should be called inside the UI thread.
1244 *
1245 * @param url
1246 * the {@link Story} to import by {@link URL}
1247 * @param onSuccess
1248 * Action to execute on success
1249 */
c8faa52a 1250 private void imprt(final String url, final StoryRunnable onSuccess,
754a5bc2
NR
1251 String onSuccessPgName) {
1252 final Progress pg = new Progress();
1253 final Progress pgImprt = new Progress();
1254 final Progress pgOnSuccess = new Progress(onSuccessPgName);
1255 pg.addProgress(pgImprt, 95);
1256 pg.addProgress(pgOnSuccess, 5);
1257
22848428 1258 outOfUi(pg, new Runnable() {
211f7ddb 1259 @Override
22848428
NR
1260 public void run() {
1261 Exception ex = null;
c8faa52a 1262 Story story = null;
22848428 1263 try {
c8faa52a
NR
1264 story = reader.getLibrary().imprt(BasicReader.getUrl(url),
1265 pgImprt);
22848428
NR
1266 } catch (IOException e) {
1267 ex = e;
1268 }
10d558d2 1269
22848428
NR
1270 final Exception e = ex;
1271
1272 final boolean ok = (e == null);
754a5bc2
NR
1273
1274 pgOnSuccess.setProgress(0);
1275 if (!ok) {
fd1d31c2 1276 if (e instanceof UnknownHostException) {
925298fd 1277 error("URL not supported: " + url, "Cannot import URL",
fd1d31c2
NR
1278 null);
1279 } else {
1280 error("Failed to import " + url + ": \n"
1281 + e.getMessage(), "Cannot import URL", e);
1282 }
754a5bc2
NR
1283 } else {
1284 if (onSuccess != null) {
c8faa52a 1285 onSuccess.run(story);
22848428 1286 }
754a5bc2 1287 }
5ce869b8 1288 pgOnSuccess.done();
22848428
NR
1289 }
1290 });
10d558d2
NR
1291 }
1292
4d205683
NR
1293 /**
1294 * Enables or disables this component, depending on the value of the
1295 * parameter <code>b</code>. An enabled component can respond to user input
1296 * and generate events. Components are enabled initially by default.
1297 * <p>
1298 * Disabling this component will also affect its children.
1299 *
1300 * @param b
1301 * If <code>true</code>, this component is enabled; otherwise
1302 * this component is disabled
1303 */
1304 @Override
1305 public void setEnabled(boolean b) {
4f661b2b
NR
1306 if (bar != null) {
1307 bar.setEnabled(b);
1308 }
1309
5dd985cf 1310 for (GuiReaderGroup group : booksByType.keySet()) {
4310bae9
NR
1311 group.setEnabled(b);
1312 }
5dd985cf 1313 for (GuiReaderGroup group : booksByAuthor.keySet()) {
4310bae9
NR
1314 group.setEnabled(b);
1315 }
4d205683 1316 super.setEnabled(b);
3b2b638f
NR
1317 repaint();
1318 }
e6249b0f
NR
1319
1320 /**
1321 * Display an error message and log the linked {@link Exception}.
1322 *
1323 * @param message
1324 * the message
1325 * @param title
1326 * the title of the error message
1327 * @param e
1328 * the exception to log if any
1329 */
1330 private void error(final String message, final String title, Exception e) {
1331 Instance.getTraceHandler().error(title + ": " + message);
1332 if (e != null) {
1333 Instance.getTraceHandler().error(e);
1334 }
1335
1336 SwingUtilities.invokeLater(new Runnable() {
1337 @Override
1338 public void run() {
1339 JOptionPane.showMessageDialog(GuiReaderFrame.this, message,
1340 title, JOptionPane.ERROR_MESSAGE);
1341 }
1342 });
1343 }
a6395bef 1344}