PMD code sweep, #6 don't add MyWindow twice to MyApplication
[fanfix.git] / src / jexer / TField.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2017 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 jexer.bits.CellAttributes;
32 import jexer.bits.GraphicsChars;
33 import jexer.event.TKeypressEvent;
34 import jexer.event.TMouseEvent;
35 import static jexer.TKeypress.*;
36
37 /**
38 * TField implements an editable text field.
39 */
40 public class TField extends TWidget {
41
42 // ------------------------------------------------------------------------
43 // Variables --------------------------------------------------------------
44 // ------------------------------------------------------------------------
45
46 /**
47 * Field text.
48 */
49 protected String text = "";
50
51 /**
52 * If true, only allow enough characters that will fit in the width. If
53 * false, allow the field to scroll to the right.
54 */
55 protected boolean fixed = false;
56
57 /**
58 * Current editing position within text.
59 */
60 protected int position = 0;
61
62 /**
63 * Beginning of visible portion.
64 */
65 protected int windowStart = 0;
66
67 /**
68 * If true, new characters are inserted at position.
69 */
70 protected boolean insertMode = true;
71
72 /**
73 * Remember mouse state.
74 */
75 protected TMouseEvent mouse;
76
77 /**
78 * The action to perform when the user presses enter.
79 */
80 protected TAction enterAction;
81
82 /**
83 * The action to perform when the text is updated.
84 */
85 protected TAction updateAction;
86
87 // ------------------------------------------------------------------------
88 // Constructors -----------------------------------------------------------
89 // ------------------------------------------------------------------------
90
91 /**
92 * Public constructor.
93 *
94 * @param parent parent widget
95 * @param x column relative to parent
96 * @param y row relative to parent
97 * @param width visible text width
98 * @param fixed if true, the text cannot exceed the display width
99 */
100 public TField(final TWidget parent, final int x, final int y,
101 final int width, final boolean fixed) {
102
103 this(parent, x, y, width, fixed, "", null, null);
104 }
105
106 /**
107 * Public constructor.
108 *
109 * @param parent parent widget
110 * @param x column relative to parent
111 * @param y row relative to parent
112 * @param width visible text width
113 * @param fixed if true, the text cannot exceed the display width
114 * @param text initial text, default is empty string
115 */
116 public TField(final TWidget parent, final int x, final int y,
117 final int width, final boolean fixed, final String text) {
118
119 this(parent, x, y, width, fixed, text, null, null);
120 }
121
122 /**
123 * Public constructor.
124 *
125 * @param parent parent widget
126 * @param x column relative to parent
127 * @param y row relative to parent
128 * @param width visible text width
129 * @param fixed if true, the text cannot exceed the display width
130 * @param text initial text, default is empty string
131 * @param enterAction function to call when enter key is pressed
132 * @param updateAction function to call when the text is updated
133 */
134 public TField(final TWidget parent, final int x, final int y,
135 final int width, final boolean fixed, final String text,
136 final TAction enterAction, final TAction updateAction) {
137
138 // Set parent and window
139 super(parent, x, y, width, 1);
140
141 setCursorVisible(true);
142 this.fixed = fixed;
143 this.text = text;
144 this.enterAction = enterAction;
145 this.updateAction = updateAction;
146 }
147
148 // ------------------------------------------------------------------------
149 // Event handlers ---------------------------------------------------------
150 // ------------------------------------------------------------------------
151
152 /**
153 * Returns true if the mouse is currently on the field.
154 *
155 * @return if true the mouse is currently on the field
156 */
157 protected boolean mouseOnField() {
158 int rightEdge = getWidth() - 1;
159 if ((mouse != null)
160 && (mouse.getY() == 0)
161 && (mouse.getX() >= 0)
162 && (mouse.getX() <= rightEdge)
163 ) {
164 return true;
165 }
166 return false;
167 }
168
169 /**
170 * Handle mouse button presses.
171 *
172 * @param mouse mouse button event
173 */
174 @Override
175 public void onMouseDown(final TMouseEvent mouse) {
176 this.mouse = mouse;
177
178 if ((mouseOnField()) && (mouse.isMouse1())) {
179 // Move cursor
180 int deltaX = mouse.getX() - getCursorX();
181 position += deltaX;
182 if (position > text.length()) {
183 position = text.length();
184 }
185 updateCursor();
186 return;
187 }
188 }
189
190 /**
191 * Handle keystrokes.
192 *
193 * @param keypress keystroke event
194 */
195 @Override
196 public void onKeypress(final TKeypressEvent keypress) {
197
198 if (keypress.equals(kbLeft)) {
199 if (position > 0) {
200 position--;
201 }
202 if (fixed == false) {
203 if ((position == windowStart) && (windowStart > 0)) {
204 windowStart--;
205 }
206 }
207 normalizeWindowStart();
208 return;
209 }
210
211 if (keypress.equals(kbRight)) {
212 if (position < text.length()) {
213 position++;
214 if (fixed == true) {
215 if (position == getWidth()) {
216 position--;
217 }
218 } else {
219 if ((position - windowStart) == getWidth()) {
220 windowStart++;
221 }
222 }
223 }
224 return;
225 }
226
227 if (keypress.equals(kbEnter)) {
228 dispatch(true);
229 return;
230 }
231
232 if (keypress.equals(kbIns)) {
233 insertMode = !insertMode;
234 return;
235 }
236 if (keypress.equals(kbHome)) {
237 home();
238 return;
239 }
240
241 if (keypress.equals(kbEnd)) {
242 end();
243 return;
244 }
245
246 if (keypress.equals(kbDel)) {
247 if ((text.length() > 0) && (position < text.length())) {
248 text = text.substring(0, position)
249 + text.substring(position + 1);
250 }
251 dispatch(false);
252 return;
253 }
254
255 if (keypress.equals(kbBackspace) || keypress.equals(kbBackspaceDel)) {
256 if (position > 0) {
257 position--;
258 text = text.substring(0, position)
259 + text.substring(position + 1);
260 }
261 if (fixed == false) {
262 if ((position == windowStart)
263 && (windowStart > 0)
264 ) {
265 windowStart--;
266 }
267 }
268 dispatch(false);
269 normalizeWindowStart();
270 return;
271 }
272
273 if (!keypress.getKey().isFnKey()
274 && !keypress.getKey().isAlt()
275 && !keypress.getKey().isCtrl()
276 ) {
277 // Plain old keystroke, process it
278 if ((position == text.length())
279 && (text.length() < getWidth())) {
280
281 // Append case
282 appendChar(keypress.getKey().getChar());
283 } else if ((position < text.length())
284 && (text.length() < getWidth())) {
285
286 // Overwrite or insert a character
287 if (insertMode == false) {
288 // Replace character
289 text = text.substring(0, position)
290 + keypress.getKey().getChar()
291 + text.substring(position + 1);
292 position++;
293 } else {
294 // Insert character
295 insertChar(keypress.getKey().getChar());
296 }
297 } else if ((position < text.length())
298 && (text.length() >= getWidth())) {
299
300 // Multiple cases here
301 if ((fixed == true) && (insertMode == true)) {
302 // Buffer is full, do nothing
303 } else if ((fixed == true) && (insertMode == false)) {
304 // Overwrite the last character, maybe move position
305 text = text.substring(0, position)
306 + keypress.getKey().getChar()
307 + text.substring(position + 1);
308 if (position < getWidth() - 1) {
309 position++;
310 }
311 } else if ((fixed == false) && (insertMode == false)) {
312 // Overwrite the last character, definitely move position
313 text = text.substring(0, position)
314 + keypress.getKey().getChar()
315 + text.substring(position + 1);
316 position++;
317 } else {
318 if (position == text.length()) {
319 // Append this character
320 appendChar(keypress.getKey().getChar());
321 } else {
322 // Insert this character
323 insertChar(keypress.getKey().getChar());
324 }
325 }
326 } else {
327 assert (!fixed);
328
329 // Append this character
330 appendChar(keypress.getKey().getChar());
331 }
332 dispatch(false);
333 return;
334 }
335
336 // Pass to parent for the things we don't care about.
337 super.onKeypress(keypress);
338 }
339
340 // ------------------------------------------------------------------------
341 // TWidget ----------------------------------------------------------------
342 // ------------------------------------------------------------------------
343
344 /**
345 * Draw the text field.
346 */
347 @Override
348 public void draw() {
349 CellAttributes fieldColor;
350
351 if (isAbsoluteActive()) {
352 fieldColor = getTheme().getColor("tfield.active");
353 } else {
354 fieldColor = getTheme().getColor("tfield.inactive");
355 }
356
357 int end = windowStart + getWidth();
358 if (end > text.length()) {
359 end = text.length();
360 }
361 getScreen().hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor);
362 getScreen().putStringXY(0, 0, text.substring(windowStart, end),
363 fieldColor);
364
365 // Fix the cursor, it will be rendered by TApplication.drawAll().
366 updateCursor();
367 }
368
369 // ------------------------------------------------------------------------
370 // TField -----------------------------------------------------------------
371 // ------------------------------------------------------------------------
372
373 /**
374 * Get field text.
375 *
376 * @return field text
377 */
378 public final String getText() {
379 return text;
380 }
381
382 /**
383 * Set field text.
384 *
385 * @param text the new field text
386 */
387 public final void setText(String text) {
388 this.text = text;
389 position = 0;
390 windowStart = 0;
391 }
392
393 /**
394 * Dispatch to the action function.
395 *
396 * @param enter if true, the user pressed Enter, else this was an update
397 * to the text.
398 */
399 protected void dispatch(final boolean enter) {
400 if (enter) {
401 if (enterAction != null) {
402 enterAction.DO();
403 }
404 } else {
405 if (updateAction != null) {
406 updateAction.DO();
407 }
408 }
409 }
410
411 /**
412 * Update the visible cursor position to match the location of position
413 * and windowStart.
414 */
415 protected void updateCursor() {
416 if ((position > getWidth()) && fixed) {
417 setCursorX(getWidth());
418 } else if ((position - windowStart == getWidth()) && !fixed) {
419 setCursorX(getWidth() - 1);
420 } else {
421 setCursorX(position - windowStart);
422 }
423 }
424
425 /**
426 * Normalize windowStart such that most of the field data if visible.
427 */
428 protected void normalizeWindowStart() {
429 if (fixed) {
430 // windowStart had better be zero, there is nothing to do here.
431 assert (windowStart == 0);
432 return;
433 }
434 windowStart = position - (getWidth() - 1);
435 if (windowStart < 0) {
436 windowStart = 0;
437 }
438
439 updateCursor();
440 }
441
442 /**
443 * Append char to the end of the field.
444 *
445 * @param ch = char to append
446 */
447 protected void appendChar(final char ch) {
448 // Append the LAST character
449 text += ch;
450 position++;
451
452 assert (position == text.length());
453
454 if (fixed) {
455 if (position == getWidth()) {
456 position--;
457 }
458 } else {
459 if ((position - windowStart) == getWidth()) {
460 windowStart++;
461 }
462 }
463 }
464
465 /**
466 * Insert char somewhere in the middle of the field.
467 *
468 * @param ch char to append
469 */
470 protected void insertChar(final char ch) {
471 text = text.substring(0, position) + ch + text.substring(position);
472 position++;
473 if ((position - windowStart) == getWidth()) {
474 assert (!fixed);
475 windowStart++;
476 }
477 }
478
479 /**
480 * Position the cursor at the first column. The field may adjust the
481 * window start to show as much of the field as possible.
482 */
483 public void home() {
484 position = 0;
485 windowStart = 0;
486 }
487
488 /**
489 * Set the editing position to the last filled character. The field may
490 * adjust the window start to show as much of the field as possible.
491 */
492 public void end() {
493 position = text.length();
494 if (fixed == true) {
495 if (position >= getWidth()) {
496 position = text.length() - 1;
497 }
498 } else {
499 windowStart = text.length() - getWidth() + 1;
500 if (windowStart < 0) {
501 windowStart = 0;
502 }
503 }
504 }
505
506 /**
507 * Set the editing position. The field may adjust the window start to
508 * show as much of the field as possible.
509 *
510 * @param position the new position
511 * @throws IndexOutOfBoundsException if position is outside the range of
512 * the available text
513 */
514 public void setPosition(final int position) {
515 if ((position < 0) || (position >= text.length())) {
516 throw new IndexOutOfBoundsException("Max length is " +
517 text.length() + ", requested position " + position);
518 }
519 this.position = position;
520 normalizeWindowStart();
521 }
522
523 }