#19 expose home/end for TField
[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 home();
320 return;
321 }
322
323 if (keypress.equals(kbEnd)) {
324 end();
325 return;
326 }
327
328 if (keypress.equals(kbDel)) {
329 if ((text.length() > 0) && (position < text.length())) {
330 text = text.substring(0, position)
331 + text.substring(position + 1);
332 }
333 dispatch(false);
334 return;
335 }
336
337 if (keypress.equals(kbBackspace) || keypress.equals(kbBackspaceDel)) {
338 if (position > 0) {
339 position--;
340 text = text.substring(0, position)
341 + text.substring(position + 1);
342 }
343 if (fixed == false) {
344 if ((position == windowStart)
345 && (windowStart > 0)
346 ) {
347 windowStart--;
348 }
349 }
350 dispatch(false);
351 normalizeWindowStart();
352 return;
353 }
354
355 if (!keypress.getKey().isFnKey()
356 && !keypress.getKey().isAlt()
357 && !keypress.getKey().isCtrl()
358 ) {
359 // Plain old keystroke, process it
360 if ((position == text.length())
361 && (text.length() < getWidth())) {
362
363 // Append case
364 appendChar(keypress.getKey().getChar());
365 } else if ((position < text.length())
366 && (text.length() < getWidth())) {
367
368 // Overwrite or insert a character
369 if (insertMode == false) {
370 // Replace character
371 text = text.substring(0, position)
372 + keypress.getKey().getChar()
373 + text.substring(position + 1);
374 position++;
375 } else {
376 // Insert character
377 insertChar(keypress.getKey().getChar());
378 }
379 } else if ((position < text.length())
380 && (text.length() >= getWidth())) {
381
382 // Multiple cases here
383 if ((fixed == true) && (insertMode == true)) {
384 // Buffer is full, do nothing
385 } else if ((fixed == true) && (insertMode == false)) {
386 // Overwrite the last character, maybe move position
387 text = text.substring(0, position)
388 + keypress.getKey().getChar()
389 + text.substring(position + 1);
390 if (position < getWidth() - 1) {
391 position++;
392 }
393 } else if ((fixed == false) && (insertMode == false)) {
394 // Overwrite the last character, definitely move position
395 text = text.substring(0, position)
396 + keypress.getKey().getChar()
397 + text.substring(position + 1);
398 position++;
399 } else {
400 if (position == text.length()) {
401 // Append this character
402 appendChar(keypress.getKey().getChar());
403 } else {
404 // Insert this character
405 insertChar(keypress.getKey().getChar());
406 }
407 }
408 } else {
409 assert (!fixed);
410
411 // Append this character
412 appendChar(keypress.getKey().getChar());
413 }
414 dispatch(false);
415 return;
416 }
417
418 // Pass to parent for the things we don't care about.
419 super.onKeypress(keypress);
420 }
421
422 /**
423 * Append char to the end of the field.
424 *
425 * @param ch = char to append
426 */
427 protected void appendChar(final char ch) {
428 // Append the LAST character
429 text += ch;
430 position++;
431
432 assert (position == text.length());
433
434 if (fixed) {
435 if (position == getWidth()) {
436 position--;
437 }
438 } else {
439 if ((position - windowStart) == getWidth()) {
440 windowStart++;
441 }
442 }
443 }
444
445 /**
446 * Insert char somewhere in the middle of the field.
447 *
448 * @param ch char to append
449 */
450 protected void insertChar(final char ch) {
451 text = text.substring(0, position) + ch + text.substring(position);
452 position++;
453 if ((position - windowStart) == getWidth()) {
454 assert (!fixed);
455 windowStart++;
456 }
457 }
458
459 /**
460 * Position the cursor at the first column. The field may adjust the
461 * window start to show as much of the field as possible.
462 */
463 public void home() {
464 position = 0;
465 windowStart = 0;
466 }
467
468 /**
469 * Set the editing position to the last filled character. The field may
470 * adjust the window start to show as much of the field as possible.
471 */
472 public void end() {
473 position = text.length();
474 if (fixed == true) {
475 if (position >= getWidth()) {
476 position = text.length() - 1;
477 }
478 } else {
479 windowStart = text.length() - getWidth() + 1;
480 if (windowStart < 0) {
481 windowStart = 0;
482 }
483 }
484 }
485
486 /**
487 * Set the editing position. The field may adjust the window start to
488 * show as much of the field as possible.
489 *
490 * @param position the new position
491 * @throws IndexOutOfBoundsException if position is outside the range of
492 * the available text
493 */
494 public void setPosition(final int position) {
495 if ((position < 0) || (position >= text.length())) {
496 throw new IndexOutOfBoundsException("Max length is " +
497 text.length() + ", requested position " + position);
498 }
499 this.position = position;
500 normalizeWindowStart();
501 }
502
503 }