Merge branch 'subtree'
[fanfix.git] / src / jexer / TEditorWindow.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer;
30
31 import java.io.File;
32 import java.io.IOException;
33 import java.text.MessageFormat;
34 import java.util.ResourceBundle;
35 import java.util.Scanner;
36
37 import jexer.TApplication;
38 import jexer.TEditorWidget;
39 import jexer.THScroller;
40 import jexer.TScrollableWindow;
41 import jexer.TVScroller;
42 import jexer.TWidget;
43 import jexer.bits.CellAttributes;
44 import jexer.bits.GraphicsChars;
45 import jexer.event.TCommandEvent;
46 import jexer.event.TKeypressEvent;
47 import jexer.event.TMenuEvent;
48 import jexer.event.TMouseEvent;
49 import jexer.event.TResizeEvent;
50 import jexer.menu.TMenu;
51 import static jexer.TCommand.*;
52 import static jexer.TKeypress.*;
53
54 /**
55 * TEditorWindow is a basic text file editor.
56 */
57 public class TEditorWindow extends TScrollableWindow {
58
59 /**
60 * Translated strings.
61 */
62 private static final ResourceBundle i18n = ResourceBundle.getBundle(TEditorWindow.class.getName());
63
64 // ------------------------------------------------------------------------
65 // Variables --------------------------------------------------------------
66 // ------------------------------------------------------------------------
67
68 /**
69 * Hang onto my TEditor so I can resize it with the window.
70 */
71 private TEditorWidget editField;
72
73 /**
74 * The fully-qualified name of the file being edited.
75 */
76 private String filename = "";
77
78 /**
79 * If true, hide the mouse after typing a keystroke.
80 */
81 private boolean hideMouseWhenTyping = true;
82
83 /**
84 * If true, the mouse should not be displayed because a keystroke was
85 * typed.
86 */
87 private boolean typingHidMouse = false;
88
89 // ------------------------------------------------------------------------
90 // Constructors -----------------------------------------------------------
91 // ------------------------------------------------------------------------
92
93 /**
94 * Public constructor sets window title.
95 *
96 * @param parent the main application
97 * @param title the window title
98 */
99 public TEditorWindow(final TApplication parent, final String title) {
100
101 super(parent, title, 0, 0, parent.getScreen().getWidth(),
102 parent.getDesktopBottom() - parent.getDesktopTop(), RESIZABLE);
103
104 editField = addEditor("", 0, 0, getWidth() - 2, getHeight() - 2);
105 setupAfterEditor();
106 }
107
108 /**
109 * Public constructor sets window title and contents.
110 *
111 * @param parent the main application
112 * @param title the window title, usually a filename
113 * @param contents the data for the editing window, usually the file data
114 */
115 public TEditorWindow(final TApplication parent, final String title,
116 final String contents) {
117
118 super(parent, title, 0, 0, parent.getScreen().getWidth(),
119 parent.getDesktopBottom() - parent.getDesktopTop(), RESIZABLE);
120
121 filename = title;
122 editField = addEditor(contents, 0, 0, getWidth() - 2, getHeight() - 2);
123 setupAfterEditor();
124 }
125
126 /**
127 * Public constructor opens a file.
128 *
129 * @param parent the main application
130 * @param file the file to open
131 * @throws IOException if a java.io operation throws
132 */
133 public TEditorWindow(final TApplication parent,
134 final File file) throws IOException {
135
136 super(parent, file.getName(), 0, 0, parent.getScreen().getWidth(),
137 parent.getDesktopBottom() - parent.getDesktopTop(), RESIZABLE);
138
139 filename = file.getName();
140 String contents = readFileData(file);
141 editField = addEditor(contents, 0, 0, getWidth() - 2, getHeight() - 2);
142 setupAfterEditor();
143 }
144
145 /**
146 * Public constructor.
147 *
148 * @param parent the main application
149 */
150 public TEditorWindow(final TApplication parent) {
151 this(parent, i18n.getString("newTextDocument"));
152 }
153
154 // ------------------------------------------------------------------------
155 // Event handlers ---------------------------------------------------------
156 // ------------------------------------------------------------------------
157
158 /**
159 * Called by application.switchWindow() when this window gets the
160 * focus, and also by application.addWindow().
161 */
162 public void onFocus() {
163 super.onFocus();
164 getApplication().enableMenuItem(TMenu.MID_UNDO);
165 getApplication().enableMenuItem(TMenu.MID_REDO);
166 }
167
168 /**
169 * Called by application.switchWindow() when another window gets the
170 * focus.
171 */
172 public void onUnfocus() {
173 super.onUnfocus();
174 getApplication().disableMenuItem(TMenu.MID_UNDO);
175 getApplication().disableMenuItem(TMenu.MID_REDO);
176 }
177
178 /**
179 * Handle mouse press events.
180 *
181 * @param mouse mouse button press event
182 */
183 @Override
184 public void onMouseDown(final TMouseEvent mouse) {
185 // Use TWidget's code to pass the event to the children.
186 super.onMouseDown(mouse);
187
188 if (hideMouseWhenTyping) {
189 typingHidMouse = false;
190 }
191
192 if (mouseOnEditor(mouse)) {
193 // The editor might have changed, update the scollbars.
194 setBottomValue(editField.getMaximumRowNumber());
195 setVerticalValue(editField.getVisibleRowNumber());
196 setRightValue(editField.getMaximumColumnNumber());
197 setHorizontalValue(editField.getEditingColumnNumber());
198 } else {
199 if (mouse.isMouseWheelUp() || mouse.isMouseWheelDown()) {
200 // Vertical scrollbar actions
201 editField.setVisibleRowNumber(getVerticalValue());
202 }
203 }
204 }
205
206 /**
207 * Handle mouse release events.
208 *
209 * @param mouse mouse button release event
210 */
211 @Override
212 public void onMouseUp(final TMouseEvent mouse) {
213 // Use TWidget's code to pass the event to the children.
214 super.onMouseUp(mouse);
215
216 if (hideMouseWhenTyping) {
217 typingHidMouse = false;
218 }
219
220 if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
221 // Clicked on vertical scrollbar
222 editField.setVisibleRowNumber(getVerticalValue());
223 }
224 if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
225 // Clicked on horizontal scrollbar
226 editField.setVisibleColumnNumber(getHorizontalValue());
227 setHorizontalValue(editField.getVisibleColumnNumber());
228 }
229 }
230
231 /**
232 * Method that subclasses can override to handle mouse movements.
233 *
234 * @param mouse mouse motion event
235 */
236 @Override
237 public void onMouseMotion(final TMouseEvent mouse) {
238 // Use TWidget's code to pass the event to the children.
239 super.onMouseMotion(mouse);
240
241 if (hideMouseWhenTyping) {
242 typingHidMouse = false;
243 }
244
245 if (mouseOnEditor(mouse) && mouse.isMouse1()) {
246 // The editor might have changed, update the scollbars.
247 setBottomValue(editField.getMaximumRowNumber());
248 setVerticalValue(editField.getVisibleRowNumber());
249 setRightValue(editField.getMaximumColumnNumber());
250 setHorizontalValue(editField.getEditingColumnNumber());
251 } else {
252 if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
253 // Clicked/dragged on vertical scrollbar
254 editField.setVisibleRowNumber(getVerticalValue());
255 }
256 if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
257 // Clicked/dragged on horizontal scrollbar
258 editField.setVisibleColumnNumber(getHorizontalValue());
259 setHorizontalValue(editField.getVisibleColumnNumber());
260 }
261 }
262
263 }
264
265 /**
266 * Handle keystrokes.
267 *
268 * @param keypress keystroke event
269 */
270 @Override
271 public void onKeypress(final TKeypressEvent keypress) {
272 if (hideMouseWhenTyping) {
273 typingHidMouse = true;
274 }
275
276 // Use TWidget's code to pass the event to the children.
277 super.onKeypress(keypress);
278
279 // The editor might have changed, update the scollbars.
280 setBottomValue(editField.getMaximumRowNumber());
281 setVerticalValue(editField.getVisibleRowNumber());
282 setRightValue(editField.getMaximumColumnNumber());
283 setHorizontalValue(editField.getEditingColumnNumber());
284 }
285
286 /**
287 * Handle window/screen resize events.
288 *
289 * @param event resize event
290 */
291 @Override
292 public void onResize(final TResizeEvent event) {
293 if (event.getType() == TResizeEvent.Type.WIDGET) {
294 // Resize the text field
295 TResizeEvent editSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
296 event.getWidth() - 2, event.getHeight() - 2);
297 editField.onResize(editSize);
298
299 // Have TScrollableWindow handle the scrollbars
300 super.onResize(event);
301 return;
302 }
303
304 // Pass to children instead
305 for (TWidget widget: getChildren()) {
306 widget.onResize(event);
307 }
308 }
309
310 /**
311 * Method that subclasses can override to handle posted command events.
312 *
313 * @param command command event
314 */
315 @Override
316 public void onCommand(final TCommandEvent command) {
317 if (command.equals(cmOpen)) {
318 try {
319 String filename = fileOpenBox(".");
320 if (filename != null) {
321 try {
322 String contents = readFileData(filename);
323 new TEditorWindow(getApplication(), filename, contents);
324 } catch (IOException e) {
325 messageBox(i18n.getString("errorDialogTitle"),
326 MessageFormat.format(i18n.
327 getString("errorReadingFile"), e.getMessage()));
328 }
329 }
330 } catch (IOException e) {
331 messageBox(i18n.getString("errorDialogTitle"),
332 MessageFormat.format(i18n.
333 getString("errorOpeningFileDialog"), e.getMessage()));
334 }
335 return;
336 }
337
338 if (command.equals(cmSave)) {
339 if (filename.length() > 0) {
340 try {
341 editField.saveToFilename(filename);
342 } catch (IOException e) {
343 messageBox(i18n.getString("errorDialogTitle"),
344 MessageFormat.format(i18n.
345 getString("errorSavingFile"), e.getMessage()));
346 }
347 }
348 return;
349 }
350
351 // Didn't handle it, let children get it instead
352 super.onCommand(command);
353 }
354
355 /**
356 * Handle posted menu events.
357 *
358 * @param menu menu event
359 */
360 @Override
361 public void onMenu(final TMenuEvent menu) {
362 switch (menu.getId()) {
363 case TMenu.MID_UNDO:
364 editField.undo();
365 break;
366 case TMenu.MID_REDO:
367 editField.redo();
368 break;
369 }
370 }
371
372 // ------------------------------------------------------------------------
373 // TWindow ----------------------------------------------------------------
374 // ------------------------------------------------------------------------
375
376 /**
377 * Draw the window.
378 */
379 @Override
380 public void draw() {
381 // Draw as normal.
382 super.draw();
383
384 // Add the row:col on the bottom row
385 CellAttributes borderColor = getBorder();
386 String location = String.format(" %d:%d ",
387 editField.getEditingRowNumber(),
388 editField.getEditingColumnNumber());
389 int colon = location.indexOf(':');
390 putStringXY(10 - colon, getHeight() - 1, location, borderColor);
391
392 if (editField.isDirty()) {
393 putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor);
394 }
395 }
396
397 /**
398 * Returns true if this window does not want the application-wide mouse
399 * cursor drawn over it.
400 *
401 * @return true if this window does not want the application-wide mouse
402 * cursor drawn over it
403 */
404 @Override
405 public boolean hasHiddenMouse() {
406 return (super.hasHiddenMouse() || typingHidMouse);
407 }
408
409 // ------------------------------------------------------------------------
410 // TEditorWindow ----------------------------------------------------------
411 // ------------------------------------------------------------------------
412
413 /**
414 * Setup other fields after the editor is created.
415 */
416 private void setupAfterEditor() {
417 hScroller = new THScroller(this, 17, getHeight() - 2, getWidth() - 20);
418 vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
419 setMinimumWindowWidth(25);
420 setMinimumWindowHeight(10);
421 setTopValue(1);
422 setBottomValue(editField.getMaximumRowNumber());
423 setLeftValue(1);
424 setRightValue(editField.getMaximumColumnNumber());
425
426 statusBar = newStatusBar(i18n.getString("statusBar"));
427 statusBar.addShortcutKeypress(kbF1, cmHelp,
428 i18n.getString("statusBarHelp"));
429 statusBar.addShortcutKeypress(kbF2, cmSave,
430 i18n.getString("statusBarSave"));
431 statusBar.addShortcutKeypress(kbF3, cmOpen,
432 i18n.getString("statusBarOpen"));
433 statusBar.addShortcutKeypress(kbF10, cmMenu,
434 i18n.getString("statusBarMenu"));
435
436 // Hide mouse when typing option
437 if (System.getProperty("jexer.TEditor.hideMouseWhenTyping",
438 "true").equals("false")) {
439
440 hideMouseWhenTyping = false;
441 }
442 }
443
444 /**
445 * Read file data into a string.
446 *
447 * @param file the file to open
448 * @return the file contents
449 * @throws IOException if a java.io operation throws
450 */
451 private String readFileData(final File file) throws IOException {
452 StringBuilder fileContents = new StringBuilder();
453 Scanner scanner = new Scanner(file);
454 String EOL = System.getProperty("line.separator");
455
456 try {
457 while (scanner.hasNextLine()) {
458 fileContents.append(scanner.nextLine() + EOL);
459 }
460 return fileContents.toString();
461 } finally {
462 scanner.close();
463 }
464 }
465
466 /**
467 * Read file data into a string.
468 *
469 * @param filename the file to open
470 * @return the file contents
471 * @throws IOException if a java.io operation throws
472 */
473 private String readFileData(final String filename) throws IOException {
474 return readFileData(new File(filename));
475 }
476
477 /**
478 * Check if a mouse press/release/motion event coordinate is over the
479 * editor.
480 *
481 * @param mouse a mouse-based event
482 * @return whether or not the mouse is on the editor
483 */
484 private boolean mouseOnEditor(final TMouseEvent mouse) {
485 if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1)
486 && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
487 && (mouse.getAbsoluteY() >= getAbsoluteY() + 1)
488 && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
489 ) {
490 return true;
491 }
492 return false;
493 }
494
495 }