1 /*
2 This file is part of Warzone 2100.
3 Copyright (C) 1999-2004 Eidos Interactive
4 Copyright (C) 2005-2020 Warzone 2100 Project
5
6 Warzone 2100 is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 Warzone 2100 is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Warzone 2100; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20 /** @file
21 * Functions for the edit box widget.
22 */
23
24 #include <string.h>
25
26 #include "lib/framework/frame.h"
27 #include "lib/framework/utf.h"
28 #include "lib/framework/wzapp.h"
29 #include "widget.h"
30 #include "widgint.h"
31 #include "editbox.h"
32 #include "form.h"
33 #include "lib/ivis_opengl/pieblitfunc.h"
34
35
36 /* Pixel gap between edge of edit box and text */
37 #define WEDB_XGAP 4
38
39 /* Size of the overwrite cursor */
40 #define WEDB_CURSORSIZE 8
41
42 /* Whether the cursor blinks or not */
43 #define CURSOR_BLINK 1
44
45 /* The time the cursor blinks for */
46 #define WEDB_BLINKRATE 800
47
48 /* Number of characters to jump the edit box text when moving the cursor */
49 #define WEDB_CHARJUMP 6
50
51 // Max size for a string in a editbox
52 #define EB_MAX_STRINGSIZE 72
53
W_EDBINIT()54 W_EDBINIT::W_EDBINIT()
55 : pText(nullptr)
56 , FontID(font_regular)
57 , pBoxDisplay(nullptr)
58 {}
59
W_EDITBOX(W_EDBINIT const * init)60 W_EDITBOX::W_EDITBOX(W_EDBINIT const *init)
61 : WIDGET(init, WIDG_EDITBOX)
62 , state(WEDBS_FIXED)
63 , FontID(init->FontID)
64 , blinkOffset(wzGetTicks())
65 , maxStringSize(EB_MAX_STRINGSIZE)
66 , insPos(0)
67 , printStart(0)
68 , printChars(0)
69 , printWidth(0)
70 , pBoxDisplay(init->pBoxDisplay)
71 , HilightAudioID(WidgGetHilightAudioID())
72 , ClickedAudioID(WidgGetClickedAudioID())
73 , ErrorAudioID(WidgGetErrorAudioID())
74 , AudioCallback(WidgGetAudioCallback())
75 , boxColourFirst(WZCOL_FORM_DARK)
76 , boxColourSecond(WZCOL_FORM_LIGHT)
77 , boxColourBackground(WZCOL_FORM_BACKGROUND)
78 {
79 char const *text = init->pText;
80 if (!text)
81 {
82 text = "";
83 }
84 aText = WzString::fromUtf8(text);
85
86 initialise();
87
88 ASSERT((init->style & ~(WEDB_PLAIN | WIDG_HIDDEN)) == 0, "Unknown edit box style");
89 }
90
W_EDITBOX()91 W_EDITBOX::W_EDITBOX()
92 : WIDGET()
93 , state(WEDBS_FIXED)
94 , FontID(font_regular)
95 , blinkOffset(wzGetTicks())
96 , maxStringSize(EB_MAX_STRINGSIZE)
97 , insPos(0)
98 , printStart(0)
99 , printChars(0)
100 , printWidth(0)
101 , pBoxDisplay(nullptr)
102 , HilightAudioID(WidgGetHilightAudioID())
103 , ClickedAudioID(WidgGetClickedAudioID())
104 , ErrorAudioID(WidgGetErrorAudioID())
105 , AudioCallback(WidgGetAudioCallback())
106 , boxColourFirst(WZCOL_FORM_DARK)
107 , boxColourSecond(WZCOL_FORM_LIGHT)
108 , boxColourBackground(WZCOL_FORM_BACKGROUND)
109 {}
110
~W_EDITBOX()111 W_EDITBOX::~W_EDITBOX()
112 {
113 /* Note the edit state */
114 unsigned editState = state & WEDBS_MASK;
115
116 /* Only have anything to do if the widget is being edited */
117 if ((editState & WEDBS_MASK) == WEDBS_FIXED)
118 {
119 return;
120 }
121
122 // If the edit box still somehow has focus, and is editable, need to StopTextInput()
123 // (May be able to remove this once more refactoring of the game menus / in-game UI occurs)
124 debug(LOG_INFO, "Editbox seems to still have focus, and is editable, as it's being destroyed.");
125 StopTextInput(this); // force-stop text input if this EditBox somehow still has the input
126 }
127
initialise()128 void W_EDITBOX::initialise()
129 {
130 state = WEDBS_FIXED;
131 printStart = 0;
132 maxStringSize = EB_MAX_STRINGSIZE;
133 fitStringStart();
134 }
135
136
137 /* Insert a character into a text buffer */
insertChar(WzUniCodepoint ch)138 bool W_EDITBOX::insertChar(WzUniCodepoint ch)
139 {
140 if (ch.isNull())
141 {
142 return false;
143 }
144
145 ASSERT(insPos <= aText.length(), "Invalid insertion point");
146 if (aText.length() >= maxStringSize)
147 {
148 if (AudioCallback)
149 {
150 AudioCallback(ErrorAudioID);
151 }
152 return false; // string too big, just return
153 }
154 /* Move the end of the string up by one (including terminating \0) */
155 /* Insert the character */
156 aText.insert(insPos, ch);
157
158 /* Update the insertion point */
159 ++insPos;
160
161 return true;
162 }
163
164
165 /* Put a character into a text buffer overwriting any text under the cursor */
overwriteChar(WzUniCodepoint ch)166 bool W_EDITBOX::overwriteChar(WzUniCodepoint ch)
167 {
168 if (ch.isNull())
169 {
170 return false;
171 }
172
173 ASSERT(insPos <= aText.length(), "overwriteChar: Invalid insertion point");
174 dirty = true;
175
176 if (insPos == aText.length())
177 {
178 // At end of string.
179 return insertChar(ch);
180 }
181
182 /* Store the character */
183 aText[insPos] = ch;
184
185 /* Update the insertion point */
186 ++insPos;
187
188 return true;
189 }
190
191
192 /* Delete a character to the right of the position */
delCharRight()193 void W_EDITBOX::delCharRight()
194 {
195 ASSERT(insPos <= aText.length(), "Invalid deletion point");
196
197 /* Can't delete if we are at the end of the string */
198 /* Move the end of the string down by one */
199 aText.remove(insPos, 1);
200 }
201
202
203 /* Delete a character to the left of the position */
delCharLeft()204 void W_EDITBOX::delCharLeft()
205 {
206 /* Can't delete if we are at the start of the string */
207 if (insPos == 0)
208 {
209 return;
210 }
211
212 --insPos;
213 delCharRight();
214 }
215
216
geometryChanged()217 void W_EDITBOX::geometryChanged()
218 {
219 /* Note the edit state */
220 unsigned editState = state & WEDBS_MASK;
221
222 /* For now, only handle fit recalculation if not being edited */
223 if (!((editState & WEDBS_MASK) == WEDBS_FIXED))
224 {
225 return;
226 }
227 fitStringStart();
228 }
229
230
231 /* Calculate how much of the start of a string can fit into the edit box */
fitStringStart()232 void W_EDITBOX::fitStringStart()
233 {
234 // We need to calculate the whole string's pixel size.
235 // From QuesoGLC's notes: additional processing like kerning creates strings of text whose dimensions are not directly
236 // related to the simple juxtaposition of individual glyph metrics. For example, the advance width of "VA" isn't the
237 // sum of the advances of "V" and "A" taken separately.
238 WzString tmp = aText;
239 tmp.remove(0, printStart); // Ignore the first printStart characters.
240
241 while (!tmp.isEmpty())
242 {
243 int pixelWidth = iV_GetTextWidth(tmp.toUtf8().c_str(), FontID);
244
245 if (pixelWidth <= width() - (WEDB_XGAP * 2 + WEDB_CURSORSIZE))
246 {
247 printChars = tmp.length();
248 printWidth = pixelWidth;
249 return;
250 }
251
252 tmp.remove(tmp.length() - 1, 1); // Erase last char.
253 }
254
255 printChars = 0;
256 printWidth = 0;
257 }
258
259
260 /* Calculate how much of the end of a string can fit into the edit box */
fitStringEnd()261 void W_EDITBOX::fitStringEnd()
262 {
263 WzString tmp = aText;
264
265 printStart = 0;
266
267 while (!tmp.isEmpty())
268 {
269 int pixelWidth = iV_GetTextWidth(tmp.toUtf8().c_str(), FontID);
270
271 if (pixelWidth <= width() - (WEDB_XGAP * 2 + WEDB_CURSORSIZE))
272 {
273 printChars = tmp.length();
274 printWidth = pixelWidth;
275 return;
276 }
277
278 tmp.remove(0, 1); // Erase first char.
279 ++printStart;
280 }
281
282 printChars = 0;
283 printWidth = 0;
284 }
285
setCursorPosPixels(int xPos)286 void W_EDITBOX::setCursorPosPixels(int xPos)
287 {
288 WzString tmp = aText;
289 tmp.remove(0, printStart); // Consider only the visible text.
290 tmp.remove(printChars, tmp.length());
291
292 int prevDelta = INT32_MAX;
293 int prevPos = printStart + tmp.length();
294 while (!tmp.isEmpty())
295 {
296 int pixelWidth = iV_GetTextWidth(tmp.toUtf8().c_str(), FontID);
297 int delta = pixelWidth - (xPos - (WEDB_XGAP + WEDB_CURSORSIZE / 2));
298 int pos = printStart + tmp.length();
299
300 if (delta <= 0)
301 {
302 insPos = -delta < prevDelta ? pos : prevPos;
303 return;
304 }
305
306 tmp.remove(tmp.length() - 1, 1); // Erase last char.
307
308 prevDelta = delta;
309 prevPos = pos;
310 }
311
312 insPos = printStart;
313 }
314
315
run(W_CONTEXT * psContext)316 void W_EDITBOX::run(W_CONTEXT *psContext)
317 {
318 /* Note the edit state */
319 unsigned editState = state & WEDBS_MASK;
320
321 /* Only have anything to do if the widget is being edited */
322 if ((editState & WEDBS_MASK) == WEDBS_FIXED)
323 {
324 return;
325 }
326 dirty = true;
327 StartTextInput(this);
328 /* If there is a mouse click outside of the edit box - stop editing */
329 int mx = psContext->mx;
330 int my = psContext->my;
331 if (mousePressed(MOUSE_LMB) && !geometry().contains(mx, my))
332 {
333 StopTextInput(this);
334 if (auto lockedScreen = screenPointer.lock())
335 {
336 lockedScreen->setFocus(nullptr);
337 }
338 return;
339 }
340
341 /* Loop through the characters in the input buffer */
342 bool done = false;
343 utf_32_char unicode;
344 for (unsigned key = inputGetKey(&unicode); key != 0 && !done; key = inputGetKey(&unicode))
345 {
346 // Don't blink while typing.
347 blinkOffset = wzGetTicks();
348
349 int len = 0;
350
351 /* Deal with all the control keys, assume anything else is a printable character */
352 switch (key)
353 {
354 case INPBUF_LEFT :
355 /* Move the cursor left */
356 insPos = MAX(insPos - 1, 0);
357
358 /* If the cursor has gone off the left of the edit box,
359 * need to update the printable text.
360 */
361 if (insPos < printStart)
362 {
363 printStart = MAX(printStart - WEDB_CHARJUMP, 0);
364 fitStringStart();
365 }
366 debug(LOG_INPUT, "EditBox cursor left");
367 break;
368 case INPBUF_RIGHT :
369 /* Move the cursor right */
370 len = aText.length();
371 insPos = MIN(insPos + 1, len);
372
373 /* If the cursor has gone off the right of the edit box,
374 * need to update the printable text.
375 */
376 if (insPos > printStart + printChars)
377 {
378 printStart = MIN(printStart + WEDB_CHARJUMP, len - 1);
379 fitStringStart();
380 }
381 debug(LOG_INPUT, "EditBox cursor right (%d, %d, %d)", insPos, printStart, printChars);
382 break;
383 case INPBUF_UP :
384 debug(LOG_INPUT, "EditBox cursor up");
385 break;
386 case INPBUF_DOWN :
387 debug(LOG_INPUT, "EditBox cursor down");
388 break;
389 case INPBUF_HOME :
390 /* Move the cursor to the start of the buffer */
391 insPos = 0;
392 printStart = 0;
393 fitStringStart();
394 debug(LOG_INPUT, "EditBox cursor home");
395 break;
396 case INPBUF_END :
397 /* Move the cursor to the end of the buffer */
398 insPos = aText.length();
399 if (insPos != printStart + printChars)
400 {
401 fitStringEnd();
402 }
403 debug(LOG_INPUT, "EditBox cursor end");
404 break;
405 case INPBUF_INS :
406 if (editState == WEDBS_INSERT)
407 {
408 editState = WEDBS_OVER;
409 }
410 else
411 {
412 editState = WEDBS_INSERT;
413 }
414 debug(LOG_INPUT, "EditBox cursor insert");
415 break;
416 case INPBUF_DEL :
417 delCharRight();
418
419 /* Update the printable text */
420 fitStringStart();
421 debug(LOG_INPUT, "EditBox cursor delete");
422 break;
423 case INPBUF_PGUP :
424 debug(LOG_INPUT, "EditBox cursor page up");
425 break;
426 case INPBUF_PGDN :
427 debug(LOG_INPUT, "EditBox cursor page down");
428 break;
429 case INPBUF_BKSPACE :
430 /* Delete the character to the left of the cursor */
431 delCharLeft();
432
433 /* Update the printable text */
434 if (insPos <= printStart)
435 {
436 printStart = MAX(printStart - WEDB_CHARJUMP, 0);
437 }
438 fitStringStart();
439 debug(LOG_INPUT, "EditBox cursor backspace");
440 break;
441 case INPBUF_TAB :
442 debug(LOG_INPUT, "EditBox cursor tab");
443 break;
444 case INPBUF_CR :
445 case KEY_KPENTER: // either normal return key || keypad enter
446 /* Finish editing */
447 StopTextInput(this);
448 if (auto lockedScreen = screenPointer.lock())
449 {
450 lockedScreen->setFocus(nullptr);
451 }
452 debug(LOG_INPUT, "EditBox cursor return");
453 return;
454 break;
455 case INPBUF_ESC :
456 debug(LOG_INPUT, "EditBox cursor escape");
457 if (aText.length() > 0)
458 {
459 // hitting ESC while the editbox contains text clears the text
460 aText.clear();
461 insPos = 0;
462 printStart = 0;
463 fitStringStart();
464 inputLoseFocus(); // clear the input buffer.
465 }
466 else
467 {
468 // hitting ESC while the editbox is empty ends editing mode
469 StopTextInput(this);
470 if (auto lockedScreen = screenPointer.lock())
471 {
472 lockedScreen->setFocus(nullptr);
473 }
474 inputLoseFocus(); // clear the input buffer.
475 return;
476 }
477 break;
478
479 default:
480 if (keyDown(KEY_LCTRL) || keyDown(KEY_RCTRL))
481 {
482 switch (key)
483 {
484 case KEY_V:
485 aText = wzGetSelection();
486 if (aText.length() >= maxStringSize)
487 {
488 aText.truncate(maxStringSize);
489 }
490 insPos = aText.length();
491 /* Update the printable text */
492 fitStringEnd();
493 debug(LOG_INPUT, "EditBox paste");
494 break;
495 default:
496 break;
497 }
498 break;
499 }
500 /* Dealt with everything else this must be a printable character */
501 bool changedText = false;
502 if (editState == WEDBS_INSERT)
503 {
504 changedText = insertChar(WzUniCodepoint::fromUTF32(unicode));
505 }
506 else
507 {
508 changedText = overwriteChar(WzUniCodepoint::fromUTF32(unicode));
509 }
510 if (changedText)
511 {
512 len = aText.length();
513 /* Update the printable chars */
514 if (insPos == len)
515 {
516 fitStringEnd();
517 }
518 else
519 {
520 fitStringStart();
521 if (insPos > printStart + printChars)
522 {
523 printStart = MIN(printStart + WEDB_CHARJUMP, len - 1);
524 if (printStart >= len)
525 {
526 fitStringStart();
527 }
528 }
529 }
530 }
531 break;
532 }
533 }
534
535 /* Store the current widget state */
536 state = (state & ~WEDBS_MASK) | editState;
537 }
538
getString() const539 WzString W_EDITBOX::getString() const
540 {
541 return aText;
542 }
543
544 /* Set the current string for the edit box */
setString(WzString string)545 void W_EDITBOX::setString(WzString string)
546 {
547 aText = string;
548 initialise();
549 dirty = true;
550 }
551
simulateClick(W_CONTEXT * psContext,bool silenceClickAudio,WIDGET_KEY key)552 void W_EDITBOX::simulateClick(W_CONTEXT *psContext, bool silenceClickAudio /*= false*/, WIDGET_KEY key /*= WKEY_PRIMARY*/)
553 {
554 if (silenceClickAudio)
555 {
556 suppressAudioCallback = true;
557 }
558 clicked(psContext, key);
559 if (silenceClickAudio)
560 {
561 suppressAudioCallback = false;
562 }
563 }
564
565 /* Respond to a mouse click */
clicked(W_CONTEXT * psContext,WIDGET_KEY)566 void W_EDITBOX::clicked(W_CONTEXT *psContext, WIDGET_KEY)
567 {
568 if (state & WEDBS_DISABLE) // disabled button.
569 {
570 return;
571 }
572
573 // Set cursor position to the click location.
574 setCursorPosPixels(psContext->mx - x());
575
576 // Cursor should be visible instantly.
577 blinkOffset = wzGetTicks();
578
579 if ((state & WEDBS_MASK) == WEDBS_FIXED)
580 {
581 if (AudioCallback && !suppressAudioCallback)
582 {
583 AudioCallback(ClickedAudioID);
584 }
585
586 /* Set up the widget state */
587 state = (state & ~WEDBS_MASK) | WEDBS_INSERT;
588
589 /* Calculate how much of the string can appear in the box */
590 fitStringEnd();
591
592 /* Clear the input buffer */
593 inputClearBuffer();
594
595 /* Tell the form that the edit box has focus */
596 if (auto lockedScreen = screenPointer.lock())
597 {
598 lockedScreen->setFocus(shared_from_this());
599 }
600 }
601 dirty = true;
602 }
603
604
605 /* Respond to loss of focus */
focusLost()606 void W_EDITBOX::focusLost()
607 {
608 ASSERT(!(state & WEDBS_DISABLE), "editBoxFocusLost: disabled edit box");
609
610 /* Stop editing the widget */
611 state = WEDBS_FIXED;
612 printStart = 0;
613 fitStringStart();
614 StopTextInput(this);
615
616 if (auto lockedScreen = screenPointer.lock())
617 {
618 lockedScreen->setReturn(shared_from_this());
619 }
620 dirty = true;
621 }
622
623
624 /* Respond to a mouse moving over an edit box */
highlight(W_CONTEXT *)625 void W_EDITBOX::highlight(W_CONTEXT *)
626 {
627 W_EDITBOX *psWidget = this;
628 if (psWidget->state & WEDBS_DISABLE)
629 {
630 return;
631 }
632
633 if (psWidget->AudioCallback)
634 {
635 psWidget->AudioCallback(psWidget->HilightAudioID);
636 }
637
638 psWidget->state |= WEDBS_HILITE;
639 }
640
641
642 /* Respond to the mouse moving off an edit box */
highlightLost()643 void W_EDITBOX::highlightLost()
644 {
645 W_EDITBOX *psWidget = this;
646 if (psWidget->state & WEDBS_DISABLE)
647 {
648 return;
649 }
650
651 psWidget->state = psWidget->state & WEDBS_MASK;
652 }
653
setBoxColours(PIELIGHT first,PIELIGHT second,PIELIGHT background)654 void W_EDITBOX::setBoxColours(PIELIGHT first, PIELIGHT second, PIELIGHT background)
655 {
656 boxColourFirst = first;
657 boxColourSecond = second;
658 boxColourBackground = background;
659 }
660
display(int xOffset,int yOffset)661 void W_EDITBOX::display(int xOffset, int yOffset)
662 {
663 int x0 = x() + xOffset;
664 int y0 = y() + yOffset;
665 int x1 = x0 + width();
666 int y1 = y0 + height();
667
668 if (pBoxDisplay != nullptr)
669 {
670 pBoxDisplay(this, xOffset, yOffset);
671 }
672 else
673 {
674 iV_ShadowBox(x0, y0, x1, y1, 0, boxColourFirst, boxColourSecond, boxColourBackground);
675 }
676
677 int fx = x0 + WEDB_XGAP;// + (psEdBox->width - fw) / 2;
678
679 int fy = y0 + (height() - iV_GetTextLineSize(FontID)) / 2 - iV_GetTextAboveBase(FontID);
680
681 /* If there is more text than will fit into the box, display the bit with the cursor in it */
682 WzString displayedText = aText;
683 displayedText.remove(0, printStart); // Erase anything there isn't room to display.
684 displayedText.remove(printChars, displayedText.length());
685
686 displayCache.wzDisplayedText.setText(displayedText.toUtf8(), FontID);
687 displayCache.wzDisplayedText.render(fx, fy, WZCOL_FORM_TEXT);
688
689 // Display the cursor if editing
690 #if CURSOR_BLINK
691 bool blink = !(((wzGetTicks() - blinkOffset) / WEDB_BLINKRATE) % 2);
692 if ((state & WEDBS_MASK) == WEDBS_INSERT && blink)
693 #else
694 if ((state & WEDBS_MASK) == WEDBS_INSERT)
695 #endif
696 {
697 // insert mode
698 WzString tmp = aText;
699 tmp.remove(insPos, tmp.length()); // Erase from the cursor on, to find where the cursor should be.
700 tmp.remove(0, printStart);
701
702 displayCache.modeText.setText(tmp.toUtf8(), FontID);
703
704 int cx = x0 + WEDB_XGAP + displayCache.modeText.width();
705 int cy = fy;
706 iV_Line(cx, cy + iV_GetTextAboveBase(FontID), cx, cy - iV_GetTextBelowBase(FontID), WZCOL_FORM_CURSOR);
707 }
708 #if CURSOR_BLINK
709 else if ((state & WEDBS_MASK) == WEDBS_OVER && blink)
710 #else
711 else if ((state & WEDBS_MASK) == WEDBS_OVER)
712 #endif
713 {
714 // overwrite mode
715 WzString tmp = aText;
716 tmp.remove(insPos, tmp.length()); // Erase from the cursor on, to find where the cursor should be.
717 tmp.remove(0, printStart);
718
719 displayCache.modeText.setText(tmp.toUtf8(), FontID);
720
721 int cx = x0 + WEDB_XGAP + displayCache.modeText.width();
722 int cy = fy;
723 iV_Line(cx, cy, cx + WEDB_CURSORSIZE, cy, WZCOL_FORM_CURSOR);
724 }
725
726 if (pBoxDisplay == nullptr)
727 {
728 if ((state & WEDBS_HILITE) != 0)
729 {
730 /* Display the button hilite */
731 iV_Box(x0 - 2, y0 - 2, x1 + 2, y1 + 2, WZCOL_FORM_HILITE);
732 }
733 }
734 }
735
setMaxStringSize(int size)736 void W_EDITBOX::setMaxStringSize(int size)
737 {
738 maxStringSize = size;
739 }
740
setState(unsigned newState)741 void W_EDITBOX::setState(unsigned newState)
742 {
743 unsigned mask = WEDBS_DISABLE;
744 state = (state & ~mask) | (newState & mask);
745 }
746