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