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