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