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