Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d'
[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.TMouseEvent;
48 import jexer.event.TResizeEvent;
49 import static jexer.TCommand.*;
50 import static jexer.TKeypress.*;
51
52 /**
53 * TEditorWindow is a basic text file editor.
54 */
55 public class TEditorWindow extends TScrollableWindow {
56
57 /**
58 * Translated strings.
59 */
60 private static final ResourceBundle i18n = ResourceBundle.getBundle(TEditorWindow.class.getName());
61
62 // ------------------------------------------------------------------------
63 // Variables --------------------------------------------------------------
64 // ------------------------------------------------------------------------
65
66 /**
67 * Hang onto my TEditor so I can resize it with the window.
68 */
69 private TEditorWidget editField;
70
71 /**
72 * The fully-qualified name of the file being edited.
73 */
74 private String filename = "";
75
76 /**
77 * If true, hide the mouse after typing a keystroke.
78 */
79 private boolean hideMouseWhenTyping = true;
80
81 /**
82 * If true, the mouse should not be displayed because a keystroke was
83 * typed.
84 */
85 private boolean typingHidMouse = false;
86
87 // ------------------------------------------------------------------------
88 // Constructors -----------------------------------------------------------
89 // ------------------------------------------------------------------------
90
91 /**
92 * Public constructor sets window title.
93 *
94 * @param parent the main application
95 * @param title the window title
96 */
97 public TEditorWindow(final TApplication parent, final String title) {
98
99 super(parent, title, 0, 0, parent.getScreen().getWidth(),
100 parent.getDesktopBottom() - parent.getDesktopTop(), RESIZABLE);
101
102 editField = addEditor("", 0, 0, getWidth() - 2, getHeight() - 2);
103 setupAfterEditor();
104 }
105
106 /**
107 * Public constructor sets window title and contents.
108 *
109 * @param parent the main application
110 * @param title the window title, usually a filename
111 * @param contents the data for the editing window, usually the file data
112 */
113 public TEditorWindow(final TApplication parent, final String title,
114 final String contents) {
115
116 super(parent, title, 0, 0, parent.getScreen().getWidth(),
117 parent.getDesktopBottom() - parent.getDesktopTop(), RESIZABLE);
118
119 filename = title;
120 editField = addEditor(contents, 0, 0, getWidth() - 2, getHeight() - 2);
121 setupAfterEditor();
122 }
123
124 /**
125 * Public constructor opens a file.
126 *
127 * @param parent the main application
128 * @param file the file to open
129 * @throws IOException if a java.io operation throws
130 */
131 public TEditorWindow(final TApplication parent,
132 final File file) throws IOException {
133
134 super(parent, file.getName(), 0, 0, parent.getScreen().getWidth(),
135 parent.getDesktopBottom() - parent.getDesktopTop(), RESIZABLE);
136
137 filename = file.getName();
138 String contents = readFileData(file);
139 editField = addEditor(contents, 0, 0, getWidth() - 2, getHeight() - 2);
140 setupAfterEditor();
141 }
142
143 /**
144 * Public constructor.
145 *
146 * @param parent the main application
147 */
148 public TEditorWindow(final TApplication parent) {
149 this(parent, i18n.getString("newTextDocument"));
150 }
151
152 // ------------------------------------------------------------------------
153 // TWindow ----------------------------------------------------------------
154 // ------------------------------------------------------------------------
155
156 /**
157 * Draw the window.
158 */
159 @Override
160 public void draw() {
161 // Draw as normal.
162 super.draw();
163
164 // Add the row:col on the bottom row
165 CellAttributes borderColor = getBorder();
166 String location = String.format(" %d:%d ",
167 editField.getEditingRowNumber(),
168 editField.getEditingColumnNumber());
169 int colon = location.indexOf(':');
170 putStringXY(10 - colon, getHeight() - 1, location, borderColor);
171
172 if (editField.isDirty()) {
173 putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor);
174 }
175 }
176
177 /**
178 * Handle mouse press events.
179 *
180 * @param mouse mouse button press event
181 */
182 @Override
183 public void onMouseDown(final TMouseEvent mouse) {
184 // Use TWidget's code to pass the event to the children.
185 super.onMouseDown(mouse);
186
187 if (hideMouseWhenTyping) {
188 typingHidMouse = false;
189 }
190
191 if (mouseOnEditor(mouse)) {
192 // The editor might have changed, update the scollbars.
193 setBottomValue(editField.getMaximumRowNumber());
194 setVerticalValue(editField.getVisibleRowNumber());
195 setRightValue(editField.getMaximumColumnNumber());
196 setHorizontalValue(editField.getEditingColumnNumber());
197 } else {
198 if (mouse.isMouseWheelUp() || mouse.isMouseWheelDown()) {
199 // Vertical scrollbar actions
200 editField.setVisibleRowNumber(getVerticalValue());
201 }
202 }
203 }
204
205 /**
206 * Handle mouse release events.
207 *
208 * @param mouse mouse button release event
209 */
210 @Override
211 public void onMouseUp(final TMouseEvent mouse) {
212 // Use TWidget's code to pass the event to the children.
213 super.onMouseUp(mouse);
214
215 if (hideMouseWhenTyping) {
216 typingHidMouse = false;
217 }
218
219 if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
220 // Clicked on vertical scrollbar
221 editField.setVisibleRowNumber(getVerticalValue());
222 }
223 if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
224 // Clicked on horizontal scrollbar
225 editField.setVisibleColumnNumber(getHorizontalValue());
226 setHorizontalValue(editField.getVisibleColumnNumber());
227 }
228 }
229
230 /**
231 * Method that subclasses can override to handle mouse movements.
232 *
233 * @param mouse mouse motion event
234 */
235 @Override
236 public void onMouseMotion(final TMouseEvent mouse) {
237 // Use TWidget's code to pass the event to the children.
238 super.onMouseMotion(mouse);
239
240 if (hideMouseWhenTyping) {
241 typingHidMouse = false;
242 }
243
244 if (mouseOnEditor(mouse) && mouse.isMouse1()) {
245 // The editor might have changed, update the scollbars.
246 setBottomValue(editField.getMaximumRowNumber());
247 setVerticalValue(editField.getVisibleRowNumber());
248 setRightValue(editField.getMaximumColumnNumber());
249 setHorizontalValue(editField.getEditingColumnNumber());
250 } else {
251 if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
252 // Clicked/dragged on vertical scrollbar
253 editField.setVisibleRowNumber(getVerticalValue());
254 }
255 if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
256 // Clicked/dragged on horizontal scrollbar
257 editField.setVisibleColumnNumber(getHorizontalValue());
258 setHorizontalValue(editField.getVisibleColumnNumber());
259 }
260 }
261
262 }
263
264 /**
265 * Handle keystrokes.
266 *
267 * @param keypress keystroke event
268 */
269 @Override
270 public void onKeypress(final TKeypressEvent keypress) {
271 if (hideMouseWhenTyping) {
272 typingHidMouse = true;
273 }
274
275 // Use TWidget's code to pass the event to the children.
276 super.onKeypress(keypress);
277
278 // The editor might have changed, update the scollbars.
279 setBottomValue(editField.getMaximumRowNumber());
280 setVerticalValue(editField.getVisibleRowNumber());
281 setRightValue(editField.getMaximumColumnNumber());
282 setHorizontalValue(editField.getEditingColumnNumber());
283 }
284
285 /**
286 * Handle window/screen resize events.
287 *
288 * @param event resize event
289 */
290 @Override
291 public void onResize(final TResizeEvent event) {
292 if (event.getType() == TResizeEvent.Type.WIDGET) {
293 // Resize the text field
294 TResizeEvent editSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
295 event.getWidth() - 2, event.getHeight() - 2);
296 editField.onResize(editSize);
297
298 // Have TScrollableWindow handle the scrollbars
299 super.onResize(event);
300 return;
301 }
302
303 // Pass to children instead
304 for (TWidget widget: getChildren()) {
305 widget.onResize(event);
306 }
307 }
308
309 /**
310 * Method that subclasses can override to handle posted command events.
311 *
312 * @param command command event
313 */
314 @Override
315 public void onCommand(final TCommandEvent command) {
316 if (command.equals(cmOpen)) {
317 try {
318 String filename = fileOpenBox(".");
319 if (filename != null) {
320 try {
321 String contents = readFileData(filename);
322 new TEditorWindow(getApplication(), filename, contents);
323 } catch (IOException e) {
324 messageBox(i18n.getString("errorDialogTitle"),
325 MessageFormat.format(i18n.
326 getString("errorReadingFile"), e.getMessage()));
327 }
328 }
329 } catch (IOException e) {
330 messageBox(i18n.getString("errorDialogTitle"),
331 MessageFormat.format(i18n.
332 getString("errorOpeningFileDialog"), e.getMessage()));
333 }
334 return;
335 }
336
337 if (command.equals(cmSave)) {
338 if (filename.length() > 0) {
339 try {
340 editField.saveToFilename(filename);
341 } catch (IOException e) {
342 messageBox(i18n.getString("errorDialogTitle"),
343 MessageFormat.format(i18n.
344 getString("errorSavingFile"), e.getMessage()));
345 }
346 }
347 return;
348 }
349
350 // Didn't handle it, let children get it instead
351 super.onCommand(command);
352 }
353
354 /**
355 * Returns true if this window does not want the application-wide mouse
356 * cursor drawn over it.
357 *
358 * @return true if this window does not want the application-wide mouse
359 * cursor drawn over it
360 */
361 @Override
362 public boolean hasHiddenMouse() {
363 return (super.hasHiddenMouse() || typingHidMouse);
364 }
365
366 // ------------------------------------------------------------------------
367 // TEditorWindow ----------------------------------------------------------
368 // ------------------------------------------------------------------------
369
370 /**
371 * Setup other fields after the editor is created.
372 */
373 private void setupAfterEditor() {
374 hScroller = new THScroller(this, 17, getHeight() - 2, getWidth() - 20);
375 vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
376 setMinimumWindowWidth(25);
377 setMinimumWindowHeight(10);
378 setTopValue(1);
379 setBottomValue(editField.getMaximumRowNumber());
380 setLeftValue(1);
381 setRightValue(editField.getMaximumColumnNumber());
382
383 statusBar = newStatusBar(i18n.getString("statusBar"));
384 statusBar.addShortcutKeypress(kbF1, cmHelp,
385 i18n.getString("statusBarHelp"));
386 statusBar.addShortcutKeypress(kbF2, cmSave,
387 i18n.getString("statusBarSave"));
388 statusBar.addShortcutKeypress(kbF3, cmOpen,
389 i18n.getString("statusBarOpen"));
390 statusBar.addShortcutKeypress(kbF10, cmMenu,
391 i18n.getString("statusBarMenu"));
392
393 // Hide mouse when typing option
394 if (System.getProperty("jexer.TEditor.hideMouseWhenTyping",
395 "true").equals("false")) {
396
397 hideMouseWhenTyping = false;
398 }
399 }
400
401 /**
402 * Read file data into a string.
403 *
404 * @param file the file to open
405 * @return the file contents
406 * @throws IOException if a java.io operation throws
407 */
408 private String readFileData(final File file) throws IOException {
409 StringBuilder fileContents = new StringBuilder();
410 Scanner scanner = new Scanner(file);
411 String EOL = System.getProperty("line.separator");
412
413 try {
414 while (scanner.hasNextLine()) {
415 fileContents.append(scanner.nextLine() + EOL);
416 }
417 return fileContents.toString();
418 } finally {
419 scanner.close();
420 }
421 }
422
423 /**
424 * Read file data into a string.
425 *
426 * @param filename the file to open
427 * @return the file contents
428 * @throws IOException if a java.io operation throws
429 */
430 private String readFileData(final String filename) throws IOException {
431 return readFileData(new File(filename));
432 }
433
434 /**
435 * Check if a mouse press/release/motion event coordinate is over the
436 * editor.
437 *
438 * @param mouse a mouse-based event
439 * @return whether or not the mouse is on the editor
440 */
441 private boolean mouseOnEditor(final TMouseEvent mouse) {
442 if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1)
443 && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
444 && (mouse.getAbsoluteY() >= getAbsoluteY() + 1)
445 && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
446 ) {
447 return true;
448 }
449 return false;
450 }
451
452 }