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