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