1 #include "HImage.h"
2 #include "Input.h"
3 #include "Font.h"
4 #include "English.h"
5 #include "VObject.h"
6 #include "VSurface.h"
7 #include "Video.h"
8 #include "Debug.h"
9 #include "Cursors.h"
10 #include "Text_Input.h"
11 #include "Timer_Control.h"
12 #include "Font_Control.h"
13 #include "Sound_Control.h"
14 #include "MemMan.h"
15 #include "MouseSystem.h"
16
17 #include <string_theory/string>
18
19
20 BOOLEAN gfNoScroll = FALSE;
21
22 struct TextInputColors
23 {
24 //internal values that contain all of the colors for the text editing fields.
25 SGPFont usFont;
26 UINT16 usTextFieldColor;
27 UINT8 ubForeColor, ubShadowColor;
28 UINT8 ubHiForeColor, ubHiShadowColor, ubHiBackColor;
29 //optional -- no bevelling by default
30 BOOLEAN fBevelling;
31 UINT16 usBrighterColor, usDarkerColor;
32 //optional -- cursor color defaults to black
33 UINT16 usCursorColor;
34 //optional colors for disabled fields (defaults to 25% darker shading)
35 BOOLEAN fUseDisabledAutoShade;
36 UINT8 ubDisabledForeColor;
37 UINT8 ubDisabledShadowColor;
38 UINT16 usDisabledTextFieldColor;
39 };
40
41 static TextInputColors* pColors = NULL;
42
43 //Internal nodes for keeping track of the text and user defined fields.
44 struct TEXTINPUTNODE
45 {
46 InputType usInputType;
47 UINT8 ubID;
48 ST::string str;
49 size_t numCodepoints;
50 size_t maxCodepoints;
51 BOOLEAN fEnabled;
52 BOOLEAN fUserField;
53 MOUSE_REGION region;
54 INPUT_CALLBACK InputCallback;
55 TEXTINPUTNODE* next;
56 TEXTINPUTNODE* prev;
57 };
58
59 //Stack list containing the head nodes of each level. Only the top level is the active level.
60 struct STACKTEXTINPUTNODE
61 {
62 TEXTINPUTNODE *head;
63 TextInputColors *pColors;
64 STACKTEXTINPUTNODE* next;
65 };
66
67 static STACKTEXTINPUTNODE* pInputStack = NULL;
68
69 //Internal list vars. active always points to the currently edited field.
70 static TEXTINPUTNODE* gpTextInputHead = NULL;
71 static TEXTINPUTNODE* gpTextInputTail = NULL;
72 static TEXTINPUTNODE* gpActive = NULL;
73
74 //Saving current mode
75 static TEXTINPUTNODE* pSavedHead = NULL;
76 static TextInputColors* pSavedColors = NULL;
77 static UINT16 gusTextInputCursor = CURSOR_IBEAM;
78
79
80 //Saves the current text input mode by pushing it onto our stack, then starts a new
81 //one.
PushTextInputLevel(void)82 static void PushTextInputLevel(void)
83 {
84 STACKTEXTINPUTNODE* const pNewLevel = new STACKTEXTINPUTNODE{};
85 pNewLevel->head = gpTextInputHead;
86 pNewLevel->pColors = pColors;
87 pNewLevel->next = pInputStack;
88 pInputStack = pNewLevel;
89 DisableAllTextFields();
90 }
91
92
93 //After the currently text input mode is removed, we then restore the previous one
94 //automatically. Assert failure in this function will expose cases where you are trigger
95 //happy with killing non-existant text input modes.
PopTextInputLevel(void)96 static void PopTextInputLevel(void)
97 {
98 STACKTEXTINPUTNODE *pLevel;
99 gpTextInputHead = pInputStack->head;
100 pColors = pInputStack->pColors;
101 pLevel = pInputStack;
102 pInputStack = pInputStack->next;
103 delete pLevel;
104 pLevel = NULL;
105 EnableAllTextFields();
106 }
107
108
109 //flags for determining various editing modes.
110 static bool gfEditingText = false;
111 static BOOLEAN gfTextInputMode = FALSE;
112
SetEditingStatus(bool bIsEditing)113 void SetEditingStatus(bool bIsEditing)
114 {
115 if (bIsEditing != gfEditingText)
116 {
117 gfEditingText = bIsEditing;
118 if (bIsEditing)
119 {
120 SDL_StartTextInput();
121 }
122 else
123 {
124 SDL_StopTextInput();
125 }
126 }
127 }
128
129 //values that contain the hiliting positions and the cursor position.
130 static size_t gubCursorPos = 0;
131 static size_t gubStartHilite = 0;
132
133
134 //Simply initiates that you wish to begin inputting text. This should only apply to screen
135 //initializations that contain fields that edit text. It also verifies and clears any existing
136 //fields. Your input loop must contain the function HandleTextInput and processed if the gfTextInputMode
137 //flag is set else process your regular input handler. Note that this doesn't mean you are necessarily typing,
138 //just that there are text fields in your screen and may be inactive. The TAB key cycles through your text fields,
139 //and special fields can be defined which will call a void functionName( UINT16 usFieldNum )
InitTextInputMode()140 void InitTextInputMode()
141 {
142 if( gpTextInputHead )
143 {
144 //Instead of killing all of the currently existing text input fields, they will now (Jan16 '97)
145 //be pushed onto a stack, and preserved until we are finished with the new mode when they will
146 //automatically be re-instated when the new text input mode is killed.
147 PushTextInputLevel();
148 //KillTextInputMode();
149 }
150 gpTextInputHead = NULL;
151 pColors = new TextInputColors{};
152 gfTextInputMode = TRUE;
153 SetEditingStatus(FALSE);
154 pColors->fBevelling = FALSE;
155 pColors->fUseDisabledAutoShade = TRUE;
156 pColors->usCursorColor = 0;
157 }
158
159 //A hybrid version of InitTextInput() which uses a specific scheme. JA2's editor uses scheme 1, so
160 //feel free to add new schemes.
InitTextInputModeWithScheme(UINT8 ubSchemeID)161 void InitTextInputModeWithScheme( UINT8 ubSchemeID )
162 {
163 InitTextInputMode();
164 switch( ubSchemeID )
165 {
166 case DEFAULT_SCHEME: //yellow boxes with black text, with bluish bevelling
167 SetTextInputFont(FONT12POINT1);
168 Set16BPPTextFieldColor( Get16BPPColor(FROMRGB(250, 240, 188) ) );
169 SetBevelColors( Get16BPPColor(FROMRGB(136, 138, 135)), Get16BPPColor(FROMRGB(24, 61, 81)) );
170 SetTextInputRegularColors( FONT_BLACK, FONT_BLACK );
171 SetTextInputHilitedColors( FONT_GRAY2, FONT_GRAY2, FONT_METALGRAY );
172 break;
173 }
174 }
175
176
177 // Clear any existing fields, and end text input mode.
KillTextInputMode()178 void KillTextInputMode()
179 {
180 TEXTINPUTNODE* i = gpTextInputHead;
181 if (!i) return;
182 while (i)
183 {
184 TEXTINPUTNODE* const del = i;
185 i = i->next;
186 if (del->maxCodepoints > 0)
187 {
188 MSYS_RemoveRegion(&del->region);
189 }
190 delete del;
191 }
192 delete pColors;
193 pColors = 0;
194 gpTextInputHead = 0;
195
196 if (pInputStack)
197 {
198 PopTextInputLevel();
199 SetActiveField(0);
200 }
201 else
202 {
203 gfTextInputMode = FALSE;
204 SetEditingStatus(FALSE);
205 }
206
207 if (!gpTextInputHead) gpActive = 0;
208 }
209
210
211 //Kills all levels of text input modes. When you init a second consecutive text input mode, without
212 //first removing them, the existing mode will be preserved. This function removes all of them in one
213 //call, though doing so "may" reflect poor coding style, though I haven't thought about any really
214 //just uses for it :(
KillAllTextInputModes()215 void KillAllTextInputModes()
216 {
217 while( gpTextInputHead )
218 KillTextInputMode();
219 }
220
221
AllocateTextInputNode(BOOLEAN const start_editing)222 static TEXTINPUTNODE* AllocateTextInputNode(BOOLEAN const start_editing)
223 {
224 TEXTINPUTNODE* const n = new TEXTINPUTNODE{};
225 n->fEnabled = TRUE;
226 if (!gpTextInputHead)
227 { // First entry, so we start with text input.
228 SetEditingStatus(start_editing);
229 gpTextInputHead = n;
230 gpActive = n;
231 n->ubID = 0;
232 }
233 else
234 { // Add to the end of the list.
235 TEXTINPUTNODE* const tail = gpTextInputTail;
236 tail->next = n;
237 n->prev = tail;
238 n->ubID = tail->ubID + 1;
239 }
240 gpTextInputTail = n;
241 return n;
242 }
243
244
245 static void MouseClickedInTextRegionCallback(MOUSE_REGION* reg, INT32 reason);
246 static void MouseMovedInTextRegionCallback(MOUSE_REGION* reg, INT32 reason);
247
248
249 /* After calling InitTextInputMode, you want to define one or more text input
250 * fields. The order of calls to this function dictate the TAB order from
251 * traversing from one field to the next. This function adds mouse regions and
252 * processes them for you, as well as deleting them when you are done. */
AddTextInputField(INT16 sLeft,INT16 sTop,INT16 sWidth,INT16 sHeight,INT8 bPriority,const ST::string & str,size_t maxCodepoints,InputType usInputType)253 void AddTextInputField(INT16 sLeft, INT16 sTop, INT16 sWidth, INT16 sHeight, INT8 bPriority, const ST::string& str, size_t maxCodepoints, InputType usInputType)
254 {
255 TEXTINPUTNODE* const n = AllocateTextInputNode(TRUE);
256 //Setup the information for the node
257 n->usInputType = usInputType; //setup the filter type
258 // All 24hourclock inputtypes have 5 codepoints. 01:23
259 if (usInputType == INPUTTYPE_24HOURCLOCK) maxCodepoints = 5;
260 Assert(maxCodepoints > 0);
261 // Allocate and copy the string.
262 n->str = str;
263 n->numCodepoints = str.to_utf32().size();
264 n->maxCodepoints = maxCodepoints;
265 Assert(n->numCodepoints <= maxCodepoints);
266
267 // If this is the first field, then hilight it.
268 if (gpTextInputHead == n)
269 {
270 gubStartHilite = 0;
271 gubCursorPos = n->numCodepoints;
272 }
273 // Setup the region.
274 MSYS_DefineRegion(&n->region, sLeft, sTop, sLeft + sWidth, sTop + sHeight, bPriority, gusTextInputCursor, MouseMovedInTextRegionCallback, MouseClickedInTextRegionCallback);
275 n->region.SetUserPtr(n);
276 }
277
278
279 /* This allows you to insert special processing functions and modes that can't
280 * be determined here. An example would be a file dialog where there would be a
281 * file list. This file list would be accessed using the Win95 convention by
282 * pressing TAB. In there, your key presses would be handled differently and by
283 * adding a userinput field, you can make this hook into your function to
284 * accomplish this. In a filedialog, alpha characters would be used to jump to
285 * the file starting with that letter, and setting the field in the text input
286 * field. Pressing TAB again would place you back in the text input field.
287 * All of that stuff would be handled externally, except for the TAB keys. */
AddUserInputField(INPUT_CALLBACK const userFunction)288 void AddUserInputField(INPUT_CALLBACK const userFunction)
289 {
290 TEXTINPUTNODE* const n = AllocateTextInputNode(FALSE);
291 n->fUserField = TRUE;
292 n->InputCallback = userFunction;
293 }
294
295
GetTextInputField(UINT8 const id)296 static TEXTINPUTNODE* GetTextInputField(UINT8 const id)
297 {
298 for (TEXTINPUTNODE* i = gpTextInputHead; i; i = i->next)
299 {
300 if (i->ubID == id) return i;
301 }
302 return 0;
303 }
304
305 //Returns the gpActive field ID number. It'll return -1 if no field is active.
GetActiveFieldID()306 INT16 GetActiveFieldID()
307 {
308 if( gpActive )
309 return gpActive->ubID;
310 return -1;
311 }
312
313 //This is a useful call made from an external user input field. Using the previous file dialog example, this
314 //call would be made when the user selected a different filename in the list via clicking or scrolling with
315 //the arrows, or even using alpha chars to jump to the appropriate filename.
SetInputFieldString(UINT8 ubField,const ST::string & str)316 void SetInputFieldString(UINT8 ubField, const ST::string& str)
317 {
318 TEXTINPUTNODE* const curr = GetTextInputField(ubField);
319 if (!curr) return;
320
321 if (!str.empty())
322 {
323 curr->str = str;
324 curr->numCodepoints = str.to_utf32().size();
325 Assert(curr->numCodepoints <= curr->maxCodepoints);
326 }
327 else if (!curr->fUserField)
328 {
329 curr->str = ST::null;
330 curr->numCodepoints = 0;
331 }
332 else
333 {
334 SLOGA("Attempting to illegally set text into user field %d", curr->ubID);
335 }
336 }
337
338
GetStringFromField(UINT8 const ubField)339 ST::string GetStringFromField(UINT8 const ubField)
340 {
341 TEXTINPUTNODE const* const n = GetTextInputField(ubField);
342 return n ? n->str : ST::null;
343 }
344
345
GetNumericStrictValueFromField(UINT8 const id)346 INT32 GetNumericStrictValueFromField(UINT8 const id)
347 {
348 ST::utf32_buffer codepoints = GetStringFromField(id).to_utf32();
349 if (codepoints.size() == 0) return -1; // Blank string, so return -1
350 /* Convert the string to a number. This ensures that non-numeric values
351 * automatically return -1. */
352 INT32 total = 0;
353 for (char32_t c : codepoints)
354 {
355 if (c < U'0' || U'9' < c) return -1;
356 total = total * 10 + (c - U'0');
357 }
358 return total;
359 }
360
361
362 //Converts a number to a numeric strict value. If the number is negative, the
363 //field will be blank.
SetInputFieldStringWithNumericStrictValue(UINT8 ubField,INT32 iNumber)364 void SetInputFieldStringWithNumericStrictValue( UINT8 ubField, INT32 iNumber )
365 {
366 TEXTINPUTNODE* const curr = GetTextInputField(ubField);
367 if (!curr) return;
368
369 AssertMsg(!curr->fUserField, String("Attempting to illegally set text into user field %d", curr->ubID));
370 if (iNumber < 0) //negative number converts to blank string
371 {
372 curr->str = ST::null;
373 curr->numCodepoints = 0;
374 }
375 else
376 {
377 INT32 iMax = (INT32)pow(10.0, curr->maxCodepoints);
378 if (iNumber > iMax) //set string to max value based on number of chars.
379 curr->str = ST::format("{}", iMax - 1);
380 else //set string to the number given
381 curr->str = ST::format("{}", iNumber);
382 curr->numCodepoints = curr->str.to_utf32().size();
383 }
384 }
385
386
387 // Set the active field to the specified ID passed.
SetActiveField(UINT8 const id)388 void SetActiveField(UINT8 const id)
389 {
390 TEXTINPUTNODE* const n = GetTextInputField(id);
391 if (!n) return;
392 if (n == gpActive) return;
393 if (!n->fEnabled) return;
394
395 if (gpActive && gpActive->InputCallback) {
396 gpActive->InputCallback(gpActive->ubID, FALSE);
397 }
398
399 gpActive = n;
400 if (n->maxCodepoints > 0)
401 {
402 gubStartHilite = 0;
403 gubCursorPos = n->numCodepoints;
404 SetEditingStatus(TRUE);
405 }
406 else
407 {
408 SetEditingStatus(FALSE);
409 if (n->InputCallback) n->InputCallback(n->ubID, TRUE);
410 }
411 }
412
413
414 static void RenderInactiveTextFieldNode(TEXTINPUTNODE const*);
415
416
SelectNextField()417 void SelectNextField()
418 {
419 BOOLEAN fDone = FALSE;
420 TEXTINPUTNODE *pStart;
421
422 if( !gpActive )
423 return;
424 if (gpActive->maxCodepoints > 0)
425 RenderInactiveTextFieldNode( gpActive );
426 else if( gpActive->InputCallback )
427 (gpActive->InputCallback)(gpActive->ubID, FALSE );
428 pStart = gpActive;
429 while( !fDone )
430 {
431 gpActive = gpActive->next;
432 if( !gpActive )
433 gpActive = gpTextInputHead;
434 if( gpActive->fEnabled )
435 {
436 fDone = TRUE;
437 if (gpActive->maxCodepoints > 0)
438 {
439 gubStartHilite = 0;
440 gubCursorPos = gpActive->numCodepoints;
441 SetEditingStatus(TRUE);
442 }
443 else
444 {
445 SetEditingStatus(FALSE);
446 if( gpActive->InputCallback )
447 (gpActive->InputCallback)(gpActive->ubID, TRUE);
448 }
449 }
450 if( gpActive == pStart )
451 {
452 SetEditingStatus(FALSE);
453 return;
454 }
455 }
456 }
457
458
SelectPrevField(void)459 static void SelectPrevField(void)
460 {
461 BOOLEAN fDone = FALSE;
462 TEXTINPUTNODE *pStart;
463
464 if( !gpActive )
465 return;
466 if (gpActive->maxCodepoints > 0)
467 RenderInactiveTextFieldNode( gpActive );
468 else if( gpActive->InputCallback )
469 (gpActive->InputCallback)(gpActive->ubID, FALSE );
470 pStart = gpActive;
471 while( !fDone )
472 {
473 gpActive = gpActive->prev;
474 if( !gpActive )
475 gpActive = gpTextInputTail;
476 if( gpActive->fEnabled )
477 {
478 fDone = TRUE;
479 if (gpActive->maxCodepoints > 0)
480 {
481 gubStartHilite = 0;
482 gubCursorPos = gpActive->numCodepoints;
483 SetEditingStatus(TRUE);
484 }
485 else
486 {
487 SetEditingStatus(FALSE);
488 if( gpActive->InputCallback )
489 (gpActive->InputCallback)(gpActive->ubID, TRUE);
490 }
491 }
492 if( gpActive == pStart )
493 {
494 SetEditingStatus(FALSE);
495 return;
496 }
497 }
498 }
499
500 //These allow you to customize the general color scheme of your text input boxes. I am assuming that
501 //under no circumstances would a user want a different color for each field. It follows the Win95 convention
502 //that all text input boxes are exactly the same color scheme. However, these colors can be set at anytime,
503 //but will effect all of the colors.
SetTextInputFont(SGPFont const font)504 void SetTextInputFont(SGPFont const font)
505 {
506 pColors->usFont = font;
507 }
508
509
Set16BPPTextFieldColor(UINT16 usTextFieldColor)510 void Set16BPPTextFieldColor( UINT16 usTextFieldColor )
511 {
512 pColors->usTextFieldColor = usTextFieldColor;
513 }
514
SetTextInputRegularColors(UINT8 ubForeColor,UINT8 ubShadowColor)515 void SetTextInputRegularColors( UINT8 ubForeColor, UINT8 ubShadowColor )
516 {
517 pColors->ubForeColor = ubForeColor;
518 pColors->ubShadowColor = ubShadowColor;
519 }
520
SetTextInputHilitedColors(UINT8 ubForeColor,UINT8 ubShadowColor,UINT8 ubBackColor)521 void SetTextInputHilitedColors( UINT8 ubForeColor, UINT8 ubShadowColor, UINT8 ubBackColor )
522 {
523 pColors->ubHiForeColor = ubForeColor;
524 pColors->ubHiShadowColor = ubShadowColor;
525 pColors->ubHiBackColor = ubBackColor;
526 }
527
SetBevelColors(UINT16 usBrighterColor,UINT16 usDarkerColor)528 void SetBevelColors( UINT16 usBrighterColor, UINT16 usDarkerColor )
529 {
530 pColors->fBevelling = TRUE;
531 pColors->usBrighterColor = usBrighterColor;
532 pColors->usDarkerColor = usDarkerColor;
533 }
534
SetCursorColor(UINT16 usCursorColor)535 void SetCursorColor( UINT16 usCursorColor )
536 {
537 pColors->usCursorColor = usCursorColor;
538 }
539
540
541 static void AddChar(char32_t c);
542 static void DeleteHilitedText(void);
543 static void HandleRegularInput(char32_t c);
544 static void RemoveChars(size_t pos, size_t n);
545
546
HandleTextInput(InputAtom const * const a)547 BOOLEAN HandleTextInput(InputAtom const* const a)
548 {
549 gfNoScroll = FALSE;
550 // Not in text input mode
551 if (!gfTextInputMode) return FALSE;
552 // Unless we are psycho typers, we only want to process these key events.
553 if (a->usEvent != TEXT_INPUT && a->usEvent != KEY_DOWN && a->usEvent != KEY_REPEAT) return FALSE;
554 // Currently in a user field, so return unless TAB is pressed.
555 if (!gfEditingText && a->usParam != SDLK_TAB) return FALSE;
556
557 if (a->usEvent == TEXT_INPUT) {
558 /* If the key has no character associated, bail out */
559 AssertMsg(a->codepoints.size() > 0, "TEXT_INPUT event sent null character");
560 DeleteHilitedText();
561 for (char32_t c : a->codepoints)
562 {
563 HandleRegularInput(c);
564 }
565 return TRUE;
566 }
567
568 switch (a->usKeyState)
569 {
570 case 0:
571 switch (a->usParam)
572 {
573 /* ESC and ENTER must be handled externally, due to the infinite uses
574 * for them. */
575 case SDLK_ESCAPE: return FALSE; // ESC is equivalent to cancel
576
577 case SDLK_RETURN: // ENTER is to confirm.
578 PlayJA2Sample(REMOVING_TEXT, BTNVOLUME, 1, MIDDLEPAN);
579 return FALSE;
580
581 case SDLK_TAB:
582 /* Always select the next field, even when a user defined field is
583 * currently selected. The order in which you add your text and user
584 * fields dictates the cycling order when TAB is pressed. */
585 SelectNextField();
586 return TRUE;
587
588 case SDLK_LEFT:
589 gfNoScroll = TRUE;
590 if (gubCursorPos != 0) --gubCursorPos;
591 gubStartHilite = gubCursorPos;
592 return TRUE;
593
594 case SDLK_RIGHT:
595 if (gubCursorPos < gpActive->numCodepoints) ++gubCursorPos;
596 gubStartHilite = gubCursorPos;
597 return TRUE;
598
599 case SDLK_END:
600 gubCursorPos = gpActive->numCodepoints;
601 gubStartHilite = gubCursorPos;
602 return TRUE;
603
604 case SDLK_HOME:
605 gubCursorPos = 0;
606 gubStartHilite = gubCursorPos;
607 return TRUE;
608
609 case SDLK_DELETE:
610 /* DEL either deletes the selected text, or the character to the right
611 * of the cursor if applicable. */
612 if (gubStartHilite != gubCursorPos)
613 {
614 DeleteHilitedText();
615 }
616 else if (gubCursorPos < gpActive->numCodepoints)
617 {
618 RemoveChars(gubCursorPos, 1);
619 }
620 else
621 {
622 return TRUE;
623 }
624 break;
625
626 case SDLK_BACKSPACE:
627 /* Delete the selected text, or the character to the left of the
628 * cursor if applicable. */
629 if (gubStartHilite != gubCursorPos)
630 {
631 DeleteHilitedText();
632 }
633 else if (gubCursorPos > 0)
634 {
635 gubStartHilite = --gubCursorPos;
636 RemoveChars(gubCursorPos, 1);
637 }
638 else
639 {
640 return TRUE;
641 }
642 break;
643
644 default:
645 return TRUE;
646 }
647 break;
648
649 case SHIFT_DOWN:
650 switch (a->usParam)
651 {
652 case SDLK_TAB: // See comment for non-shifted TAB above
653 SelectPrevField();
654 return TRUE;
655
656 case SDLK_LEFT:
657 gfNoScroll = TRUE;
658 if (gubCursorPos != 0) --gubCursorPos;
659 return TRUE;
660
661 case SDLK_RIGHT:
662 if (gubCursorPos < gpActive->numCodepoints) ++gubCursorPos;
663 return TRUE;
664
665 case SDLK_END:
666 gubCursorPos = gpActive->numCodepoints;
667 return TRUE;
668
669 case SDLK_HOME:
670 gubCursorPos = 0;
671 return TRUE;
672
673 default:
674 return TRUE;
675
676 }
677
678 case CTRL_DOWN:
679 switch (a->usParam)
680 {
681 #if 0
682 case SDLK_c: ExecuteCopyCommand(); return TRUE;
683 case SDLK_x: ExecuteCutCommand(); return TRUE;
684 case SDLK_v: ExecutePasteCommand(); return TRUE;
685 #endif
686
687 case SDLK_DELETE:
688 // Delete the entire text field, regardless of hilighting.
689 gubStartHilite = 0;
690 gubCursorPos = gpActive->numCodepoints;
691 DeleteHilitedText();
692 break;
693
694 default: return FALSE;
695 }
696 break;
697
698 default: return FALSE;
699 }
700
701 PlayJA2Sample(ENTERING_TEXT, BTNVOLUME, 1, MIDDLEPAN);
702 return TRUE;
703 }
704
705
706 // All input types are handled in this function.
HandleRegularInput(char32_t c)707 static void HandleRegularInput(char32_t c)
708 {
709 TEXTINPUTNODE const& n = *gpActive;
710 switch (n.usInputType)
711 {
712 case INPUTTYPE_NUMERICSTRICT:
713 if (U'0' <= c && c <= U'9') AddChar(c);
714 break;
715
716 case INPUTTYPE_FULL_TEXT:
717 if (IsPrintableChar(c)) AddChar(c);
718 break;
719
720 case INPUTTYPE_DOSFILENAME: // DOS file names
721 if ((U'A' <= c && c <= U'Z') ||
722 (U'a' <= c && c <= U'z') ||
723 /* Cannot begin a new filename with a number */
724 (U'0' <= c && c <= U'9' && gubCursorPos != 0) ||
725 c == U'_' || c == U'.')
726 {
727 AddChar(c);
728 }
729 break;
730
731 case INPUTTYPE_COORDINATE: // coordinates such as a9, z78, etc.
732 // First char is an lower case alpha, subsequent chars are numeric
733 if (gubCursorPos == 0)
734 {
735 if (U'a' <= c && c <= U'z')
736 {
737 AddChar(c);
738 }
739 else if (U'A' <= c && c <= U'Z')
740 {
741 AddChar(static_cast<char32_t>(c + 32)); // Convert to lowercase
742 }
743 }
744 else
745 {
746 if (U'0' <= c && c <= U'9') AddChar(c);
747 }
748 break;
749
750 case INPUTTYPE_24HOURCLOCK:
751 switch (gubCursorPos)
752 {
753 case 0:
754 if (U'0' <= c && c <= U'2') AddChar(c);
755 break;
756
757 case 1:
758 if (U'0' <= c && c <= U'9')
759 {
760 if (n.str[0] == '2' && c > U'3') break;
761 AddChar(c);
762 }
763 if (n.str[2] == '\0')
764 {
765 AddChar(U':');
766 }
767 else
768 {
769 gubStartHilite = ++gubCursorPos;
770 }
771 break;
772
773 case 2:
774 if (c == U':')
775 {
776 AddChar(c);
777 }
778 else if (U'0' <= c && c <= U'9')
779 {
780 AddChar(U':');
781 AddChar(c);
782 }
783 break;
784
785 case 3:
786 if (U'0' <= c && c <= U'5') AddChar(c);
787 break;
788
789 case 4:
790 if (U'0' <= c && c <= U'9') AddChar(c);
791 break;
792 }
793 break;
794 }
795 }
796
797
AddChar(char32_t c)798 static void AddChar(char32_t c)
799 {
800 PlayJA2Sample(ENTERING_TEXT, BTNVOLUME, 1, MIDDLEPAN);
801 TEXTINPUTNODE& n = *gpActive;
802 if (n.numCodepoints >= n.maxCodepoints) return;
803 // Insert character after cursor
804 if (gubCursorPos < n.numCodepoints)
805 {
806 ST::utf32_buffer codepoints = n.str.to_utf32();
807 size_t i = 0;
808 n.str = ST::null;
809 while (i < gubCursorPos)
810 {
811 n.str += codepoints[i++];
812 }
813 n.str += c;
814 while (i < codepoints.size())
815 {
816 n.str += codepoints[i++];
817 }
818 }
819 else
820 {
821 n.str += c;
822 }
823 ++n.numCodepoints;
824 gubStartHilite = ++gubCursorPos;
825 }
826
827
DeleteHilitedText(void)828 static void DeleteHilitedText(void)
829 {
830 size_t start = gubStartHilite;
831 size_t end = gubCursorPos;
832 if (start == end) return;
833 if (start > end) Swap(start, end);
834 gubStartHilite = start;
835 gubCursorPos = start;
836 RemoveChars(start, end - start);
837 }
838
839
RemoveChars(size_t const pos,size_t const n)840 static void RemoveChars(size_t const pos, size_t const n)
841 {
842 TEXTINPUTNODE& t = *gpActive;
843 Assert(pos + n <= t.numCodepoints);
844 ST::utf32_buffer codepoints = t.str.to_utf32();
845 size_t i = 0;
846 t.str = ST::null;
847 while (i < pos)
848 {
849 t.str += codepoints[i++];
850 }
851 i += n;
852 while (i < codepoints.size())
853 {
854 t.str += codepoints[i++];
855 }
856 t.numCodepoints = codepoints.size() - n;
857 }
858
859
SetActiveFieldMouse(MOUSE_REGION const * const r)860 static void SetActiveFieldMouse(MOUSE_REGION const* const r)
861 {
862 TEXTINPUTNODE* const n = r->GetUserPtr<TEXTINPUTNODE>();
863 if (n == gpActive) return;
864 // Deselect the current text edit region if applicable, then set the new one.
865 if (gpActive && gpActive->InputCallback) {
866 gpActive->InputCallback(gpActive->ubID, FALSE);
867 }
868
869 RenderInactiveTextFieldNode(gpActive);
870 gpActive = n;
871 }
872
873
CalculateCursorPos(INT32 const click_x)874 static size_t CalculateCursorPos(INT32 const click_x)
875 {
876 SGPFont const font = pColors->usFont;
877 ST::utf32_buffer codepoints = gpActive->str.to_utf32();
878 INT32 char_pos = 0;
879 size_t i;
880 for (i = 0; codepoints[i] != U'\0'; ++i)
881 {
882 char_pos += GetCharWidth(font, codepoints[i]);
883 if (char_pos >= click_x) break;
884 }
885 return i;
886 }
887
888
889 //Internally used to continue highlighting text
MouseMovedInTextRegionCallback(MOUSE_REGION * const reg,INT32 const reason)890 static void MouseMovedInTextRegionCallback(MOUSE_REGION* const reg, INT32 const reason)
891 {
892 if (!gfLeftButtonState) return;
893
894 if (reason & MSYS_CALLBACK_REASON_MOVE ||
895 reason & MSYS_CALLBACK_REASON_LOST_MOUSE ||
896 reason & MSYS_CALLBACK_REASON_GAIN_MOUSE)
897 {
898 SetActiveFieldMouse(reg);
899 if (reason & MSYS_CALLBACK_REASON_LOST_MOUSE)
900 {
901 if (gusMouseYPos < reg->RegionTopLeftY)
902 {
903 gubCursorPos = 0;
904 }
905 else if (gusMouseYPos > reg->RegionBottomRightY)
906 {
907 gubCursorPos = gpActive->numCodepoints;
908 }
909 }
910 else
911 {
912 gubCursorPos = static_cast<UINT8>(CalculateCursorPos(gusMouseXPos - reg->RegionTopLeftX));
913 }
914 }
915 }
916
917
918 //Internally used to calculate where to place the cursor.
MouseClickedInTextRegionCallback(MOUSE_REGION * const reg,INT32 const reason)919 static void MouseClickedInTextRegionCallback(MOUSE_REGION* const reg, INT32 const reason)
920 {
921 if (reason & MSYS_CALLBACK_REASON_LBUTTON_DWN)
922 {
923 SetActiveFieldMouse(reg);
924 //Signifies that we are typing text now.
925 SetEditingStatus(TRUE);
926 UINT8 const pos = static_cast<UINT8>(CalculateCursorPos(gusMouseXPos - reg->RegionTopLeftX));
927 gubCursorPos = pos;
928 gubStartHilite = pos;
929 }
930 }
931
932
RenderBackgroundField(TEXTINPUTNODE const * const n)933 static void RenderBackgroundField(TEXTINPUTNODE const* const n)
934 {
935 INT16 const tlx = n->region.RegionTopLeftX;
936 INT16 const tly = n->region.RegionTopLeftY;
937 INT16 const brx = n->region.RegionBottomRightX;
938 INT16 const bry = n->region.RegionBottomRightY;
939 TextInputColors const& clrs = *pColors;
940
941 if (clrs.fBevelling)
942 {
943 ColorFillVideoSurfaceArea(FRAME_BUFFER, tlx, tly, brx, bry, clrs.usDarkerColor);
944 ColorFillVideoSurfaceArea(FRAME_BUFFER, tlx + 1, tly + 1, brx, bry, clrs.usBrighterColor);
945 }
946
947 UINT16 const colour = n->fEnabled || clrs.fUseDisabledAutoShade ?
948 clrs.usTextFieldColor :
949 clrs.usDisabledTextFieldColor;
950 ColorFillVideoSurfaceArea(FRAME_BUFFER, tlx + 1, tly + 1, brx - 1, bry - 1, colour);
951
952 InvalidateRegion(tlx, tly, brx, bry);
953 }
954
955
956 /* Required in your screen loop to update the values, as well as blinking the
957 * cursor. */
RenderActiveTextField(void)958 static void RenderActiveTextField(void)
959 {
960 TEXTINPUTNODE const* const n = gpActive;
961 if (!n || n->maxCodepoints == 0) return;
962
963 SaveFontSettings();
964 RenderBackgroundField(n);
965
966 TextInputColors const& clrs = *pColors;
967 SGPFont const font = clrs.usFont;
968 UINT16 const h = GetFontHeight(font);
969 INT32 const y = n->region.RegionTopLeftY + (n->region.RegionBottomRightY - n->region.RegionTopLeftY - h) / 2;
970 ST::utf32_buffer codepoints = n->str.to_utf32();
971 size_t start = gubStartHilite;
972 size_t end = gubCursorPos;
973 if (start != end)
974 { // Some or all of the text is hilighted, so we will use a different method.
975 // Sort the hilite order.
976 if (start > end) Swap(start, end);
977 // Traverse the string one character at a time, and draw the highlited part differently.
978 UINT32 x = n->region.RegionTopLeftX + 3;
979 for (size_t i = 0; i < codepoints.size(); ++i)
980 {
981 if (start <= i && i < end)
982 { // In highlighted part of text
983 SetFontAttributes(font, clrs.ubHiForeColor, clrs.ubHiShadowColor, clrs.ubHiBackColor);
984 }
985 else
986 { // In regular part of text
987 SetFontAttributes(font, clrs.ubForeColor, clrs.ubShadowColor, 0);
988 }
989 x += MPrintChar(x, y, codepoints[i]);
990 }
991 }
992 else
993 {
994 SetFontAttributes(font, clrs.ubForeColor, clrs.ubShadowColor, 0);
995 MPrint(n->region.RegionTopLeftX + 3, y, codepoints);
996 }
997
998 // Draw the blinking ibeam cursor during the on blink period.
999 if (gfEditingText && n->maxCodepoints > 0 && GetJA2Clock() % 1000 < TEXT_CURSOR_BLINK_INTERVAL)
1000 {
1001 INT32 x = n->region.RegionTopLeftX + 2;
1002 for (size_t i = 0; i < gubCursorPos; ++i)
1003 {
1004 Assert(codepoints[i] != U'\0');
1005 x += GetCharWidth(font, codepoints[i]);
1006 }
1007 ColorFillVideoSurfaceArea(FRAME_BUFFER, x, y, x + 1, y + h, clrs.usCursorColor);
1008 }
1009
1010 RestoreFontSettings();
1011 }
1012
1013
RenderInactiveTextField(UINT8 const id)1014 void RenderInactiveTextField(UINT8 const id)
1015 {
1016 TEXTINPUTNODE const* const n = GetTextInputField(id);
1017 if (!n || n->maxCodepoints == 0) return;
1018 SaveFontSettings();
1019 SetFontAttributes(pColors->usFont, pColors->ubForeColor, pColors->ubShadowColor);
1020 RenderBackgroundField(n);
1021 UINT16 const offset = (n->region.RegionBottomRightY - n->region.RegionTopLeftY - GetFontHeight(pColors->usFont)) / 2;
1022 MPrint(n->region.RegionTopLeftX + 3, n->region.RegionTopLeftY + offset, n->str);
1023 RestoreFontSettings();
1024 }
1025
1026
RenderInactiveTextFieldNode(TEXTINPUTNODE const * const n)1027 static void RenderInactiveTextFieldNode(TEXTINPUTNODE const* const n)
1028 {
1029 if (!n || n->maxCodepoints == 0) return;
1030
1031 SaveFontSettings();
1032 TextInputColors const& clrs = *pColors;
1033 bool const disabled = !n->fEnabled && clrs.fUseDisabledAutoShade;
1034 if (disabled)
1035 { // Use the color scheme specified by the user
1036 SetFontAttributes(clrs.usFont, clrs.ubDisabledForeColor, clrs.ubDisabledShadowColor);
1037 }
1038 else
1039 {
1040 SetFontAttributes(clrs.usFont, clrs.ubForeColor, clrs.ubShadowColor);
1041 }
1042 RenderBackgroundField(n);
1043 MOUSE_REGION const& r = n->region;
1044 UINT16 const y = r.RegionTopLeftY + (r.RegionBottomRightY - r.RegionTopLeftY - GetFontHeight(clrs.usFont)) / 2;
1045 MPrint(r.RegionTopLeftX + 3, y, n->str);
1046 RestoreFontSettings();
1047
1048 if (disabled)
1049 {
1050 FRAME_BUFFER->ShadowRect(r.RegionTopLeftX, r.RegionTopLeftY, r.RegionBottomRightX, r.RegionBottomRightY);
1051 }
1052 }
1053
1054
1055 // Use when you do a full interface update.
RenderAllTextFields()1056 void RenderAllTextFields()
1057 {
1058 // Render all of the other text input levels first
1059 for (STACKTEXTINPUTNODE const* stack = pInputStack; stack; stack = stack->next)
1060 {
1061 for (TEXTINPUTNODE const* i = stack->head; i; i = i->next)
1062 {
1063 RenderInactiveTextFieldNode(i);
1064 }
1065 }
1066
1067 // Render the current text input level
1068 for (TEXTINPUTNODE const* i = gpTextInputHead; i; i = i->next)
1069 {
1070 if (i != gpActive)
1071 {
1072 RenderInactiveTextFieldNode(i);
1073 }
1074 else
1075 {
1076 RenderActiveTextField();
1077 }
1078 }
1079 }
1080
DisableTextField(UINT8 const id)1081 void DisableTextField(UINT8 const id)
1082 {
1083 TEXTINPUTNODE* const n = GetTextInputField(id);
1084 if (!n) return;
1085 if (gpActive == n) SelectNextField();
1086 if (!n->fEnabled) return;
1087 n->region.Disable();
1088 n->fEnabled = FALSE;
1089 }
1090
1091
EnableTextFields(UINT8 const first_id,UINT8 const last_id)1092 void EnableTextFields(UINT8 const first_id, UINT8 const last_id)
1093 {
1094 for (TEXTINPUTNODE* i = gpTextInputHead; i; i = i->next)
1095 {
1096 if (i->ubID < first_id || last_id < i->ubID) continue;
1097 if (i->fEnabled) continue;
1098 i->region.Enable();
1099 i->fEnabled = TRUE;
1100 }
1101 }
1102
1103
DisableTextFields(UINT8 const first_id,UINT8 const last_id)1104 void DisableTextFields(UINT8 const first_id, UINT8 const last_id)
1105 {
1106 for (TEXTINPUTNODE* i = gpTextInputHead; i; i = i->next)
1107 {
1108 if (i->ubID < first_id || last_id < i->ubID) continue;
1109 if (!i->fEnabled) continue;
1110 if (gpActive == i) SelectNextField();
1111 i->region.Disable();
1112 i->fEnabled = FALSE;
1113 }
1114 }
1115
1116
EnableAllTextFields()1117 void EnableAllTextFields()
1118 {
1119 for (TEXTINPUTNODE* i = gpTextInputHead; i; i = i->next)
1120 {
1121 if (i->fEnabled) continue;
1122 i->region.Enable();
1123 i->fEnabled = TRUE;
1124 }
1125 if (!gpActive) gpActive = gpTextInputHead;
1126 }
1127
1128
DisableAllTextFields()1129 void DisableAllTextFields()
1130 {
1131 gpActive = 0;
1132 for (TEXTINPUTNODE* i = gpTextInputHead; i; i = i->next)
1133 {
1134 if (!i->fEnabled) continue;
1135 i->region.Disable();
1136 i->fEnabled = FALSE;
1137 }
1138 }
1139
1140
EditingText()1141 BOOLEAN EditingText()
1142 {
1143 return gfEditingText;
1144 }
1145
TextInputMode()1146 BOOLEAN TextInputMode()
1147 {
1148 return gfTextInputMode;
1149 }
1150
1151
1152 //Saves the current text input mode, then removes it and activates the previous text input mode,
1153 //if applicable. The second function restores the settings. Doesn't currently support nested
1154 //calls.
SaveAndRemoveCurrentTextInputMode()1155 void SaveAndRemoveCurrentTextInputMode()
1156 {
1157 AssertMsg(pSavedHead == NULL, "Attempting to save text input stack head, when one already exists.");
1158 pSavedHead = gpTextInputHead;
1159 pSavedColors = pColors;
1160 if( pInputStack )
1161 {
1162 gpTextInputHead = pInputStack->head;
1163 pColors = pInputStack->pColors;
1164 }
1165 else
1166 {
1167 gpTextInputHead = NULL;
1168 pColors = NULL;
1169 }
1170 }
1171
RestoreSavedTextInputMode()1172 void RestoreSavedTextInputMode()
1173 {
1174 AssertMsg(pSavedHead != NULL, "Attempting to restore saved text input stack head, when one doesn't exist.");
1175 gpTextInputHead = pSavedHead;
1176 pColors = pSavedColors;
1177 pSavedHead = NULL;
1178 pSavedColors = NULL;
1179 }
1180
1181
SetTextInputCursor(UINT16 const new_cursor)1182 void SetTextInputCursor(UINT16 const new_cursor)
1183 {
1184 gusTextInputCursor = new_cursor;
1185 }
1186
1187
1188 //Utility functions for the INPUTTYPE_24HOURCLOCK input type.
GetExclusive24HourTimeValueFromField(UINT8 ubField)1189 UINT16 GetExclusive24HourTimeValueFromField( UINT8 ubField )
1190 {
1191 TEXTINPUTNODE const* const curr = GetTextInputField(ubField);
1192 AssertMsg(curr, String("GetExclusive24HourTimeValueFromField: Invalid field %d", ubField));
1193 if (!curr) return 0xffff;
1194
1195 UINT16 usTime;
1196 if (curr->usInputType != INPUTTYPE_24HOURCLOCK)
1197 return 0xffff; //illegal!
1198 //First validate the hours 00-23
1199 if ((curr->str[0] == '2' && curr->str[1] >= '0' && //20-23
1200 curr->str[1] <='3') ||
1201 (curr->str[0] >= '0' && curr->str[0] <= '1' && // 00-19
1202 curr->str[1] >= '0' && curr->str[1] <= '9'))
1203 { //Next, validate the colon, and the minutes 00-59
1204 if (curr->str[2] == ':' && curr->str[5] == '\0' && // :
1205 curr->str[3] >= '0' && curr->str[3] <= '5' && // 0-5
1206 curr->str[4] >= '0' && curr->str[4] <= '9') // 0-9
1207 {
1208 //Hours
1209 usTime = ((curr->str[0]-0x30) * 10 + curr->str[1]-0x30) * 60;
1210 //Minutes
1211 usTime += (curr->str[3]-0x30) * 10 + curr->str[4]-0x30;
1212 return usTime;
1213 }
1214 }
1215 // invalid
1216 return 0xffff;
1217 }
1218
1219 //Utility functions for the INPUTTYPE_24HOURCLOCK input type.
SetExclusive24HourTimeValue(UINT8 ubField,UINT16 usTime)1220 void SetExclusive24HourTimeValue( UINT8 ubField, UINT16 usTime )
1221 {
1222 //First make sure the time is a valid time. If not, then use 23:59
1223 if( usTime == 0xffff )
1224 {
1225 SetInputFieldString(ubField, ST::null);
1226 return;
1227 }
1228 usTime = MIN( 1439, usTime );
1229
1230 TEXTINPUTNODE* const curr = GetTextInputField(ubField);
1231 if (!curr) return;
1232
1233 AssertMsg(!curr->fUserField, String("Attempting to illegally set text into user field %d", curr->ubID));
1234 curr->str = ST::null;
1235 curr->str += static_cast<char>((usTime / 600) + 0x30); //10 hours
1236 curr->str += static_cast<char>((usTime / 60 % 10) + 0x30); //1 hour
1237 usTime %= 60; //truncate the hours
1238 curr->str += ':';
1239 curr->str += static_cast<char>((usTime / 10) + 0x30); //10 minutes
1240 curr->str += static_cast<char>((usTime % 10) + 0x30); //1 minute;
1241 }
1242