reader: sync/async work
[fanfix.git] / src / be / nikiroo / fanfix / reader / tui / TuiReaderApplication.java
1 package be.nikiroo.fanfix.reader.tui;
2
3 import java.awt.Toolkit;
4 import java.awt.datatransfer.DataFlavor;
5 import java.io.IOException;
6 import java.net.URL;
7 import java.net.UnknownHostException;
8
9 import jexer.TApplication;
10 import jexer.TCommand;
11 import jexer.TKeypress;
12 import jexer.TMessageBox;
13 import jexer.TMessageBox.Result;
14 import jexer.TMessageBox.Type;
15 import jexer.TStatusBar;
16 import jexer.TWidget;
17 import jexer.TWindow;
18 import jexer.event.TCommandEvent;
19 import jexer.event.TMenuEvent;
20 import jexer.menu.TMenu;
21 import be.nikiroo.fanfix.Instance;
22 import be.nikiroo.fanfix.data.MetaData;
23 import be.nikiroo.fanfix.data.Story;
24 import be.nikiroo.fanfix.library.BasicLibrary;
25 import be.nikiroo.fanfix.reader.BasicReader;
26 import be.nikiroo.fanfix.reader.Reader;
27 import be.nikiroo.fanfix.reader.tui.TuiReaderMainWindow.Mode;
28 import be.nikiroo.utils.Progress;
29
30 /**
31 * Manages the TUI general mode and links and manages the {@link TWindow}s.
32 * <p>
33 * It will also enclose a {@link Reader} and simply handle the reading part
34 * differently (it will create the required sub-windows and display them).
35 *
36 * @author niki
37 */
38 class TuiReaderApplication extends TApplication implements Reader {
39 public static final int MENU_OPEN = 1025;
40 public static final int MENU_IMPORT_URL = 1026;
41 public static final int MENU_IMPORT_FILE = 1027;
42 public static final int MENU_EXPORT = 1028;
43 public static final int MENU_DELETE = 1029;
44 public static final int MENU_LIBRARY = 1030;
45 public static final int MENU_EXIT = 1031;
46
47 public static final TCommand CMD_EXIT = new TCommand(MENU_EXIT) {
48 };
49
50 private Reader reader;
51 private TuiReaderMainWindow main;
52
53 // start reading if meta present
54 public TuiReaderApplication(Reader reader, BackendType backend)
55 throws Exception {
56 super(backend);
57 init(reader);
58
59 if (getMeta() != null) {
60 read(false);
61 }
62 }
63
64 public TuiReaderApplication(Reader reader, String source,
65 TApplication.BackendType backend) throws Exception {
66 super(backend);
67 init(reader);
68
69 showMain();
70 main.setMode(Mode.SOURCE, source);
71 }
72
73 @Override
74 public void read(boolean sync) throws IOException {
75 read(getStory(null), sync);
76 }
77
78 @Override
79 public MetaData getMeta() {
80 return reader.getMeta();
81 }
82
83 @Override
84 public Story getStory(Progress pg) {
85 return reader.getStory(pg);
86 }
87
88 @Override
89 public BasicLibrary getLibrary() {
90 return reader.getLibrary();
91 }
92
93 @Override
94 public void setLibrary(BasicLibrary lib) {
95 reader.setLibrary(lib);
96 }
97
98 @Override
99 public void setMeta(MetaData meta) throws IOException {
100 reader.setMeta(meta);
101 }
102
103 @Override
104 public void setMeta(String luid) throws IOException {
105 reader.setMeta(luid);
106 }
107
108 @Override
109 public void setMeta(URL source, Progress pg) throws IOException {
110 reader.setMeta(source, pg);
111 }
112
113 @Override
114 public void browse(String source) {
115 reader.browse(source);
116 }
117
118 @Override
119 public int getChapter() {
120 return reader.getChapter();
121 }
122
123 @Override
124 public void setChapter(int chapter) {
125 reader.setChapter(chapter);
126 }
127
128 /**
129 * Open the given {@link Story} for reading. This may or may not start an
130 * external program to read said {@link Story}.
131 *
132 * @param story
133 * the {@link Story} to read
134 * @param sync
135 * execute the process synchronously (wait until it is terminated
136 * before returning)
137 *
138 * @throws IOException
139 * in case of I/O errors
140 */
141 public void read(Story story, boolean sync) throws IOException {
142 if (story == null) {
143 throw new IOException("No story to read");
144 }
145
146 // TODO: open in editor + external option
147 if (!story.getMeta().isImageDocument()) {
148 TWindow window = new TuiReaderStoryWindow(this, story, getChapter());
149 window.maximize();
150 } else {
151 try {
152 openExternal(getLibrary(), story.getMeta().getLuid(), sync);
153 } catch (IOException e) {
154 messageBox("Error when trying to open the story",
155 e.getMessage(), TMessageBox.Type.OK);
156 }
157 }
158 }
159
160 /**
161 * Set the default status bar when this window appear.
162 * <p>
163 * Some shortcuts are always visible, and will be put here.
164 * <p>
165 * Note that shortcuts placed this way on menu won't work unless the menu
166 * also implement them.
167 *
168 * @param window
169 * the new window or menu on screen
170 * @param description
171 * the description to show on the status ba
172 */
173 public TStatusBar setStatusBar(TWindow window, String description) {
174 TStatusBar statusBar = window.newStatusBar(description);
175 statusBar.addShortcutKeypress(TKeypress.kbF10, CMD_EXIT, "Exit");
176 return statusBar;
177
178 }
179
180 private void showMain() {
181 if (main != null && main.isVisible()) {
182 main.activate();
183 } else {
184 if (main != null) {
185 main.close();
186 }
187 main = new TuiReaderMainWindow(this);
188 main.maximize();
189 }
190 }
191
192 private void init(Reader reader) {
193 this.reader = reader;
194
195 // TODO: traces/errors?
196 Instance.setTraceHandler(null);
197
198 // Add the menus TODO: i18n
199 TMenu fileMenu = addMenu("&File");
200 fileMenu.addItem(MENU_OPEN, "&Open...");
201 fileMenu.addItem(MENU_EXPORT, "&Save as...");
202 fileMenu.addItem(MENU_DELETE, "&Delete...");
203 // TODO: Move to...
204 fileMenu.addSeparator();
205 fileMenu.addItem(MENU_IMPORT_URL, "Import &URL...");
206 fileMenu.addItem(MENU_IMPORT_FILE, "Import &file...");
207 fileMenu.addSeparator();
208 fileMenu.addItem(MENU_LIBRARY, "Lib&rary");
209 fileMenu.addSeparator();
210 fileMenu.addItem(MENU_EXIT, "E&xit");
211
212 setStatusBar(fileMenu, "File-management "
213 + "commands (Open, Save, Print, etc.)");
214
215 // TODO: Edit: re-download, delete
216
217 //
218
219 addWindowMenu();
220
221 getBackend().setTitle("Fanfix");
222 }
223
224 @Override
225 protected boolean onCommand(TCommandEvent command) {
226 if (command.getCmd().equals(TuiReaderMainWindow.CMD_SEARCH)) {
227 messageBox("title", "caption");
228 return true;
229 }
230 return super.onCommand(command);
231 }
232
233 @Override
234 protected boolean onMenu(TMenuEvent menu) {
235 // TODO: i18n
236 switch (menu.getId()) {
237 case MENU_EXIT:
238 close(this);
239 return true;
240 case MENU_OPEN:
241 String openfile = null;
242 try {
243 openfile = fileOpenBox(".");
244 reader.setMeta(BasicReader.getUrl(openfile), null);
245 read(false);
246 } catch (IOException e) {
247 // TODO: i18n
248 error("Fail to open file"
249 + (openfile == null ? "" : ": " + openfile),
250 "Import error", e);
251 }
252
253 return true;
254 case MENU_DELETE:
255 String luid = null;
256 String story = null;
257 MetaData meta = null;
258 if (main != null) {
259 meta = main.getSelectedMeta();
260 }
261 if (meta != null) {
262 luid = meta.getLuid();
263 story = luid + ": " + meta.getTitle();
264 }
265
266 // TODO: i18n
267 TMessageBox mbox = messageBox("Delete story", "Delete story \""
268 + story + "\"", Type.OKCANCEL);
269 if (mbox.getResult() == Result.OK) {
270 try {
271 reader.getLibrary().delete(luid);
272 if (main != null) {
273 main.refreshStories();
274 }
275 } catch (IOException e) {
276 // TODO: i18n
277 error("Fail to delete the story: \"" + story + "\"",
278 "Error", e);
279 }
280 }
281
282 return true;
283 case MENU_IMPORT_URL:
284 String clipboard = "";
285 try {
286 clipboard = ("" + Toolkit.getDefaultToolkit()
287 .getSystemClipboard().getData(DataFlavor.stringFlavor))
288 .trim();
289 } catch (Exception e) {
290 // No data will be handled
291 }
292
293 if (clipboard == null || !clipboard.startsWith("http")) {
294 clipboard = "";
295 }
296
297 String url = inputBox("Import story", "URL to import", clipboard)
298 .getText();
299
300 try {
301 if (!imprt(url)) {
302 // TODO: i18n
303 error("URK not supported: " + url, "Import error");
304 }
305 } catch (IOException e) {
306 // TODO: i18n
307 error("Fail to import URL: " + url, "Import error", e);
308 }
309
310 return true;
311 case MENU_IMPORT_FILE:
312 String filename = null;
313 try {
314 filename = fileOpenBox(".");
315 if (!imprt(filename)) {
316 // TODO: i18n
317 error("File not supported: " + filename, "Import error");
318 }
319 } catch (IOException e) {
320 // TODO: i18n
321 error("Fail to import file"
322 + (filename == null ? "" : ": " + filename),
323 "Import error", e);
324 }
325 return true;
326 case MENU_LIBRARY:
327 showMain();
328 return true;
329 }
330
331 return super.onMenu(menu);
332 }
333
334 /**
335 * Import the given url.
336 * <p>
337 * Can fail if the host is not supported.
338 *
339 * @param url
340 *
341 * @return TRUE in case of success, FALSE if the host is not supported
342 *
343 * @throws IOException
344 * in case of I/O error
345 */
346 private boolean imprt(String url) throws IOException {
347 try {
348 reader.getLibrary().imprt(BasicReader.getUrl(url), null);
349 main.refreshStories();
350 return true;
351 } catch (UnknownHostException e) {
352 return false;
353 }
354 }
355
356 @Override
357 public void openExternal(BasicLibrary lib, String luid, boolean sync)
358 throws IOException {
359 reader.openExternal(lib, luid, sync);
360 }
361
362 /**
363 * Display an error message and log it.
364 *
365 * @param message
366 * the message
367 * @param title
368 * the title of the error message
369 */
370 private void error(String message, String title) {
371 error(message, title, null);
372 }
373
374 /**
375 * Display an error message and log it, including the linked
376 * {@link Exception}.
377 *
378 * @param message
379 * the message
380 * @param title
381 * the title of the error message
382 * @param e
383 * the exception to log if any (can be NULL)
384 */
385 private void error(String message, String title, Exception e) {
386 Instance.getTraceHandler().error(title + ": " + message);
387 if (e != null) {
388 Instance.getTraceHandler().error(e);
389 }
390
391 if (e != null) {
392 messageBox(title, message //
393 + "\n" + e.getMessage());
394 } else {
395 messageBox(title, message);
396 }
397 }
398
399 /**
400 * Ask the user and, if confirmed, close the {@link TApplication} this
401 * {@link TWidget} is running on.
402 * <p>
403 * This should result in the program terminating.
404 *
405 * @param widget
406 * the {@link TWidget}
407 */
408 static public void close(TWidget widget) {
409 close(widget.getApplication());
410 }
411
412 /**
413 * Ask the user and, if confirmed, close the {@link TApplication}.
414 * <p>
415 * This should result in the program terminating.
416 *
417 * @param app
418 * the {@link TApplication}
419 */
420 static void close(TApplication app) {
421 // TODO: i18n
422 if (app.messageBox("Confirmation", "(TODO: i18n) Exit application?",
423 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
424 app.exit();
425 }
426 }
427 }