ft2-clone

Fasttracker 2 clone
Log | Files | Refs | README | LICENSE

ft2_textboxes.c (25651B)


      1 // for finding memory leaks in debug mode with Visual Studio
      2 #if defined _DEBUG && defined _MSC_VER
      3 #include <crtdbg.h>
      4 #endif
      5 
      6 #include <stdint.h>
      7 #include <stdbool.h>
      8 #include "ft2_header.h"
      9 #include "ft2_gui.h"
     10 #include "ft2_video.h"
     11 #include "ft2_config.h"
     12 #include "ft2_diskop.h"
     13 #include "ft2_keyboard.h"
     14 #include "ft2_mouse.h"
     15 #include "ft2_bmp.h"
     16 #include "ft2_structs.h"
     17 
     18 textBox_t textBoxes[NUM_TEXTBOXES] =
     19 {
     20 	// ------ RESERVED TEXTBOXES ------
     21 	{ 0 },
     22 
     23 	/*
     24 	** -- STRUCT INFO: --
     25 	**  x    = x position
     26 	**  y    = y position
     27 	**  w    = width
     28 	**  h    = height
     29 	**  tx   = left padding for text
     30 	**  ty   = top padding for text
     31 	**  maxc = max characters in box string
     32 	**  rmb  = can only be triggered with right mouse button (f.ex. tracker instrument texts)
     33 	**  cmc  = change mouse cursor when hovering over it
     34 	*/
     35 
     36 	// ------ INSTRUMENT/SAMPLE/SONG NAME TEXTBOXES ------
     37 	// x,   y,   w,   h,  tx,ty, maxc,       rmb,   cmc
     38 	{  446,   5, 140,  10, 1, 0, 22,         true,  false },
     39 	{  446,  16, 140,  10, 1, 0, 22,         true,  false },
     40 	{  446,  27, 140,  10, 1, 0, 22,         true,  false },
     41 	{  446,  38, 140,  10, 1, 0, 22,         true,  false },
     42 	{  446,  49, 140,  10, 1, 0, 22,         true,  false },
     43 	{  446,  60, 140,  10, 1, 0, 22,         true,  false },
     44 	{  446,  71, 140,  10, 1, 0, 22,         true,  false },
     45 	{  446,  82, 140,  10, 1, 0, 22,         true,  false },
     46 	{  446,  99, 116,  10, 1, 0, 22,         true,  false },
     47 	{  446, 110, 116,  10, 1, 0, 22,         true,  false },
     48 	{  446, 121, 116,  10, 1, 0, 22,         true,  false },
     49 	{  446, 132, 116,  10, 1, 0, 22,         true,  false },
     50 	{  446, 143, 116,  10, 1, 0, 22,         true,  false },
     51 	{  424, 158, 160,  12, 2, 1, 20,         false, true  },
     52 
     53 	// ------ DISK OP. TEXTBOXES ------
     54 	// x,   y,   w,   h,  tx,ty, maxc,       rmb,   cmc
     55 	{   31, 158, 134,  12, 2, 1, PATH_MAX,   false, true },
     56 
     57 	// ------ CONFIG TEXTBOXES ------
     58 	// x,   y,   w,   h,  tx,ty, maxc,       rmb,   cmc
     59 	{ 486,   16, 143,  12, 2, 1, 80,         false, true },
     60 	{ 486,   31, 143,  12, 2, 1, 80,         false, true },
     61 	{ 486,   46, 143,  12, 2, 1, 80,         false, true },
     62 	{ 486,   61, 143,  12, 2, 1, 80,         false, true },
     63 	{ 486,   76, 143,  12, 2, 1, 80,         false, true }
     64 };
     65 
     66 static int16_t markX1, markX2;
     67 static uint16_t oldCursorPos;
     68 static int32_t oldMouseX;
     69 
     70 static void moveTextCursorLeft(int16_t i, bool updateTextBox);
     71 static void moveTextCursorRight(int16_t i, bool updateTextBox);
     72 
     73 static void setSongModifiedFlagIfNeeded(void) // called during keystrokes in text boxes
     74 {
     75 	if (mouse.lastEditBox == TB_SONG_NAME ||
     76 	   (mouse.lastEditBox >= TB_INST1 && mouse.lastEditBox <= TB_INST8) ||
     77 	   (mouse.lastEditBox >= TB_SAMP1 && mouse.lastEditBox <= TB_SAMP5))
     78 	{
     79 		setSongModifiedFlag();
     80 	}
     81 }
     82 
     83 bool textIsMarked(void)
     84 {
     85 	if (markX1 == markX2)
     86 		return false;
     87 
     88 	return true;
     89 }
     90 
     91 static void removeTextMarking(void)
     92 {
     93 	markX1 = 0;
     94 	markX2 = 0;
     95 }
     96 
     97 static int16_t getTextMarkStart(void)
     98 {
     99 	if (markX2 < markX1)
    100 		return markX2;
    101 
    102 	return markX1;
    103 }
    104 
    105 static int16_t getTextMarkEnd(void)
    106 {
    107 	if (markX1 > markX2)
    108 		return markX1;
    109 
    110 	return markX2;
    111 }
    112 
    113 static int16_t getTextLength(textBox_t *t, uint16_t offset)
    114 {
    115 	uint16_t i;
    116 
    117 	if (t->textPtr == NULL || offset >= t->maxChars)
    118 		return 0;
    119 
    120 	// count number of characters in text
    121 	for (i = offset; i < t->maxChars; i++)
    122 	{
    123 		if (t->textPtr[i] == '\0')
    124 			break;
    125 	}
    126 
    127 	i -= offset; // i now contains string length
    128 	assert(i <= t->maxChars);
    129 
    130 	return i;
    131 }
    132 
    133 static void deleteMarkedText(textBox_t *t)
    134 {
    135 	if (!textIsMarked())
    136 		return;
    137 
    138 	const int16_t start = getTextMarkStart();
    139 	const int16_t end = getTextMarkEnd();
    140 
    141 	assert(start < t->maxChars && end <= t->maxChars);
    142 
    143 	// calculate pixel width of string to delete
    144 	int32_t deleteTextWidth = 0;
    145 	for (int32_t i = start; i < end; i++)
    146 		deleteTextWidth += charWidth(t->textPtr[i]);
    147 
    148 	// copy markEnd part to markStart, and add NUL-termination
    149 	const int32_t length = (int32_t)strlen(&t->textPtr[end]);
    150 	if (length > 0)
    151 		memcpy(&t->textPtr[start], &t->textPtr[end], length);
    152 	t->textPtr[start+length] = '\0';
    153 
    154 	// scroll buffer offset to the left if we are scrolled
    155 	if (t->bufOffset >= deleteTextWidth)
    156 		t->bufOffset -= deleteTextWidth;
    157 	else
    158 		t->bufOffset = 0;
    159 
    160 	// set text cursor to markStart
    161 	t->cursorPos = start;
    162 
    163 	setSongModifiedFlagIfNeeded();
    164 }
    165 
    166 static void setCursorToMarkStart(textBox_t *t)
    167 {
    168 	if (!textIsMarked())
    169 		return;
    170 
    171 	const int16_t start = getTextMarkStart();
    172 	assert(start < t->maxChars);
    173 	t->cursorPos = start;
    174 
    175 	int32_t startXPos = 0;
    176 	for (int32_t i = 0; i < start; i++)
    177 	{
    178 		const char ch = t->textPtr[i];
    179 		if (ch == '\0')
    180 			break;
    181 
    182 		startXPos += charWidth(ch);
    183 	}
    184 
    185 	// change buffer offset, if needed
    186 	if (startXPos < t->bufOffset)
    187 		t->bufOffset = startXPos;
    188 }
    189 
    190 static void setCursorToMarkEnd(textBox_t *t)
    191 {
    192 	if (!textIsMarked())
    193 		return;
    194 
    195 	const int16_t end = getTextMarkEnd();
    196 	assert(end <= t->maxChars);
    197 	t->cursorPos = end;
    198 
    199 	int32_t endXPos = 0;
    200 	for (int32_t i = 0; i < end; i++)
    201 	{
    202 		const char ch = t->textPtr[i];
    203 		if (ch == '\0')
    204 			break;
    205 
    206 		endXPos += charWidth(ch);
    207 	}
    208 
    209 	// change buffer offset, if needed
    210 	if (endXPos > t->bufOffset+t->renderW)
    211 		t->bufOffset = endXPos - t->renderW;
    212 }
    213 
    214 static void copyMarkedText(textBox_t *t)
    215 {
    216 	if (!textIsMarked())
    217 		return;
    218 
    219 	const int32_t start = getTextMarkStart();
    220 	const int32_t end = getTextMarkEnd();
    221 
    222 	assert(start < t->maxChars && end <= t->maxChars);
    223 
    224 	const int32_t length = end - start;
    225 	if (length < 1)
    226 		return;
    227 
    228 	/* Change mark-end character to NUL so that we
    229 	** we only copy the marked section of the string.
    230 	** There's always room for a NUL at the end of
    231 	** the text box string, so this is safe.
    232 	**/
    233 	const char oldChar = t->textPtr[end];
    234 	t->textPtr[end] = '\0';
    235 
    236 	char *utf8Text = cp850ToUtf8(&t->textPtr[start]);
    237 	if (utf8Text != NULL)
    238 	{
    239 		SDL_SetClipboardText(utf8Text);
    240 		free(utf8Text);
    241 	}
    242 
    243 	t->textPtr[end] = oldChar; // set back original character
    244 }
    245 
    246 static void cutMarkedText(textBox_t *t)
    247 {
    248 	if (!textIsMarked())
    249 		return;
    250 
    251 	copyMarkedText(t);
    252 	deleteMarkedText(t);
    253 	removeTextMarking();
    254 
    255 	drawTextBox(mouse.lastEditBox);
    256 }
    257 
    258 static void pasteText(textBox_t *t)
    259 {
    260 	char *endPart;
    261 
    262 	if (!SDL_HasClipboardText())
    263 		return;
    264 
    265 	// if we've marked text, delete it and remove text marking
    266 	if (textIsMarked())
    267 	{
    268 		deleteMarkedText(t);
    269 		removeTextMarking();
    270 	}
    271 
    272 	if (t->cursorPos >= t->maxChars)
    273 		return;
    274 
    275 	const int32_t textLength = getTextLength(t, 0);
    276 
    277 	const int32_t roomLeft = t->maxChars - textLength;
    278 	if (roomLeft <= 0)
    279 		return; // no more room!
    280  
    281 	char *copiedTextUtf8 = SDL_GetClipboardText();
    282 
    283 	char *copiedText = utf8ToCp850(copiedTextUtf8, true);
    284 	if (copiedText == NULL)
    285 		return;
    286 
    287 	int32_t copiedTextLength = (int32_t)strlen(copiedText);
    288 	if (copiedTextLength > roomLeft)
    289 		copiedTextLength = roomLeft;
    290 
    291 	const uint16_t endOffset = t->cursorPos;
    292 	endPart = NULL; // prevent false compiler warning
    293 
    294 	const int32_t endPartLength = getTextLength(t, endOffset);
    295 	if (endPartLength > 0)
    296 	{
    297 		endPart = (char *)malloc(endPartLength+1);
    298 		if (endPart == NULL)
    299 		{
    300 			free(copiedText);
    301 			okBox(0, "System message", "Not enough memory!", NULL);
    302 			return;
    303 		}
    304 	}
    305 
    306 	// make a copy of end data
    307 	if (endPartLength > 0)
    308 	{
    309 		memcpy(endPart, &t->textPtr[endOffset], endPartLength);
    310 		endPart[endPartLength] = '\0';
    311 	}
    312 
    313 	// paste copied data
    314 	memcpy(&t->textPtr[endOffset], copiedText, copiedTextLength);
    315 	t->textPtr[endOffset+copiedTextLength] = '\0';
    316 	free(copiedText);
    317 
    318 	// append end data
    319 	if (endPartLength > 0)
    320 	{
    321 		strcat(&t->textPtr[endOffset+copiedTextLength], endPart);
    322 		free(endPart);
    323 	}
    324 
    325 	for (int32_t i = 0; i < copiedTextLength; i++)
    326 		moveTextCursorRight(mouse.lastEditBox, TEXTBOX_NO_UPDATE);
    327 
    328 	drawTextBox(mouse.lastEditBox);
    329 
    330 	setSongModifiedFlagIfNeeded();
    331 }
    332 
    333 void exitTextEditing(void)
    334 {
    335 	if (!editor.editTextFlag)
    336 		return;
    337 
    338 	if (mouse.lastEditBox >= 0 && mouse.lastEditBox < NUM_TEXTBOXES)
    339 	{
    340 		textBoxes[mouse.lastEditBox].bufOffset = 0;
    341 		removeTextMarking();
    342 		drawTextBox(mouse.lastEditBox);
    343 	}
    344 
    345 	if (mouse.lastEditBox == TB_DISKOP_FILENAME && getDiskOpItem() == DISKOP_ITEM_MODULE)
    346 	{
    347 		updateCurrSongFilename(); // for window title
    348 		updateWindowTitle(true);
    349 	}
    350 
    351 	keyb.ignoreCurrKeyUp = true; // prevent a note being played (on enter key)
    352 	editor.editTextFlag = false;
    353 
    354 	hideSprite(SPRITE_TEXT_CURSOR);
    355 	SDL_StopTextInput();
    356 }
    357 
    358 static int16_t cursorPosToX(textBox_t *t)
    359 {
    360 	assert(t->textPtr != NULL);
    361 
    362 	int32_t x = -1; // cursor starts one pixel before character
    363 	for (int16_t i = 0; i < t->cursorPos; i++)
    364 		x += charWidth(t->textPtr[i]);
    365  
    366 	x -= t->bufOffset; // subtract by buffer offset to get real X position
    367 	return (int16_t)x;
    368 }
    369 
    370 int16_t getTextCursorX(textBox_t *t)
    371 {
    372 	return t->x + t->tx + cursorPosToX(t);
    373 }
    374 
    375 int16_t getTextCursorY(textBox_t *t)
    376 {
    377 	return t->y + t->ty;
    378 }
    379 
    380 static void scrollTextBufferLeft(textBox_t *t)
    381 {
    382 	// scroll buffer and clamp
    383 	t->bufOffset -= TEXT_SCROLL_VALUE;
    384 	if (t->bufOffset < 0)
    385 		t->bufOffset = 0;
    386 }
    387 
    388 static void scrollTextBufferRight(textBox_t *t, uint16_t numCharsInText)
    389 {
    390 	assert(numCharsInText <= t->maxChars);
    391 
    392 	// get end of text position
    393 	int32_t textEnd = 0;
    394 	for (uint16_t j = 0; j < numCharsInText; j++)
    395 		textEnd += charWidth(t->textPtr[j]);
    396 
    397 	// subtract by text box width and clamp to 0
    398 	textEnd -= t->renderW;
    399 	if (textEnd < 0)
    400 		textEnd = 0;
    401 
    402 	// scroll buffer and clamp
    403 	t->bufOffset += TEXT_SCROLL_VALUE;
    404 	if (t->bufOffset > textEnd)
    405 		t->bufOffset = textEnd;
    406 }
    407 
    408 static void moveTextCursorToMouseX(uint16_t textBoxID)
    409 {
    410 	int16_t i;
    411 
    412 	textBox_t *t = &textBoxes[textBoxID];
    413 	if ((mouse.x == t->x && t->bufOffset == 0) || t->textPtr == NULL || t->textPtr[0] == '\0')
    414 	{
    415 		t->cursorPos = 0;
    416 		return;
    417 	}
    418 
    419 	int16_t numChars = getTextLength(t, 0);
    420 
    421 	// find out what character we are clicking at, and set cursor to that character
    422 	const int32_t mx = t->bufOffset + mouse.x;
    423 	int32_t tx = (t->x + t->tx) - 1;
    424 	int32_t cw = -1;
    425 
    426 	for (i = 0; i < numChars; i++)
    427 	{
    428 		cw = charWidth(t->textPtr[i]);
    429 		const int32_t tx2 = tx + cw;
    430 
    431 		if (mx >= tx && mx < tx2)
    432 		{
    433 			t->cursorPos = i;
    434 			break;
    435 		}
    436 
    437 		tx += cw;
    438 	}
    439 
    440 	// set to last character if we clicked outside the end of the text
    441 	if (i == numChars && mx >= tx)
    442 		t->cursorPos = numChars;
    443 
    444 	if (cw != -1)
    445 	{
    446 		const int16_t cursorPos = cursorPosToX(t);
    447 
    448 		// scroll buffer to the right if needed
    449 		if (cursorPos+cw > t->renderW)
    450 			scrollTextBufferRight(t, numChars);
    451 
    452 		// scroll buffer to the left if needed
    453 		else if (cursorPos < 0-1)
    454 			scrollTextBufferLeft(t);
    455 	}
    456 
    457 	editor.textCursorBlinkCounter = 0;
    458 }
    459 
    460 static void textOutBuf(uint8_t *dstBuffer, uint32_t dstWidth, uint8_t paletteIndex, char *text, uint32_t maxTextLen)
    461 {
    462 	assert(text != NULL);
    463 	if (*text == '\0')
    464 		return; // empty string
    465 
    466 	uint16_t currX = 0;
    467 	for (uint32_t i = 0; i < maxTextLen; i++)
    468 	{
    469 		const char chr = *text++ & 0x7F;
    470 		if (chr == '\0')
    471 			break;
    472 
    473 		if (chr != ' ')
    474 		{
    475 			const uint8_t *srcPtr = &bmp.font1[chr * FONT1_CHAR_W];
    476 			uint8_t *dstPtr = &dstBuffer[currX];
    477 
    478 			for (uint32_t y = 0; y < FONT1_CHAR_H; y++)
    479 			{
    480 				for (uint32_t x = 0; x < FONT1_CHAR_W; x++)
    481 				{
    482 					if (srcPtr[x])
    483 						dstPtr[x] = paletteIndex;
    484 				}
    485 
    486 				srcPtr += FONT1_WIDTH;
    487 				dstPtr += dstWidth;
    488 			}
    489 		}
    490 
    491 		currX += charWidth(chr);
    492 	}
    493 }
    494 
    495  // a lot of filling here, but textboxes are small so no problem...
    496 void drawTextBox(uint16_t textBoxID)
    497 {
    498 	int8_t cw;
    499 	uint8_t pal;
    500 
    501 	assert(textBoxID < NUM_TEXTBOXES);
    502 	textBox_t *t = &textBoxes[textBoxID];
    503 	if (!t->visible)
    504 		return;
    505 
    506 	// test if buffer offset is not overflowing
    507 #ifdef _DEBUG
    508 	if (t->renderBufW > t->renderW)
    509 		assert(t->bufOffset <= t->renderBufW-t->renderW);
    510 #endif
    511 
    512 	// fill text rendering buffer with transparency key
    513 	memset(t->renderBuf, PAL_TRANSPR, t->renderBufW * t->renderBufH);
    514 
    515 	if (t->textPtr == NULL)
    516 		return;
    517 
    518 	// draw text mark background (if this is the textbox we last interacted with)
    519 	if (mouse.lastEditBox == textBoxID)
    520 	{
    521 		if (textIsMarked())
    522 		{
    523 			hideSprite(SPRITE_TEXT_CURSOR);
    524 
    525 			int32_t start = getTextMarkStart();
    526 			int32_t end = getTextMarkEnd();
    527 
    528 			assert(start < t->maxChars && end <= t->maxChars);
    529 
    530 			// find pixel start/length from markX1 and markX2
    531 
    532 			int32_t x1 = 0;
    533 			int32_t x2 = 0;
    534 
    535 			for (int32_t i = 0; i < end; i++)
    536 			{
    537 				const char ch = t->textPtr[i];
    538 				if (ch == '\0')
    539 					break;
    540 
    541 				cw = charWidth(ch);
    542 				if (i < start)
    543 					x1 += cw;
    544 
    545 				x2 += cw;
    546 			}
    547 
    548 			// render text mark background
    549 			if (x1 != x2)
    550 			{
    551 				start = x1;
    552 				const int32_t length = x2 - x1;
    553 
    554 				assert(start+length <= t->renderBufW);
    555 
    556 				uint8_t *ptr32 = &t->renderBuf[start];
    557 				for (uint16_t y = 0; y < t->renderBufH; y++, ptr32 += t->renderBufW)
    558 					memset(ptr32, PAL_TEXTMRK, length);
    559 			}
    560 		}
    561 	}
    562 
    563 	// render text to text render buffer
    564 	textOutBuf(t->renderBuf, t->renderBufW, PAL_FORGRND, t->textPtr, t->maxChars);
    565 
    566 	// fill screen rect background color (not always needed, but I'm lazy)
    567 	pal = video.frameBuffer[(t->y * SCREEN_W) + t->x] >> 24; // get background palette (stored in alpha channel)
    568 	fillRect(t->x + t->tx, t->y + t->ty, t->renderW, 10, pal); // 10 = tallest possible glyph/char height
    569 
    570 	// render visible part of text render buffer to screen
    571 	blitClipX(t->x + t->tx, t->y + t->ty, &t->renderBuf[t->bufOffset], t->renderBufW, t->renderBufH, t->renderW);
    572 }
    573 
    574 void showTextBox(uint16_t textBoxID)
    575 {
    576 	assert(textBoxID < NUM_TEXTBOXES);
    577 	textBoxes[textBoxID].visible = true;
    578 }
    579 
    580 void hideTextBox(uint16_t textBoxID)
    581 {
    582 	assert(textBoxID < NUM_TEXTBOXES);
    583 	hideSprite(SPRITE_TEXT_CURSOR);
    584 	textBoxes[textBoxID].visible = false;
    585 }
    586 
    587 static void setMarkX2ToMouseX(textBox_t *t)
    588 {
    589 	int16_t i;
    590 
    591 	if (t->textPtr == NULL || t->textPtr[0] == '\0')
    592 	{
    593 		removeTextMarking();
    594 		return;
    595 	}
    596 
    597 	if (markX2 < markX1 && mouse.x < t->x+t->tx)
    598 	{
    599 		markX2 = 0;
    600 		return;
    601 	}
    602 
    603 	const int16_t numChars = getTextLength(t, 0);
    604 
    605 	// find out what character we are clicking at, and set markX2 to that character
    606 	const int32_t mx = t->bufOffset + mouse.x;
    607 	int32_t tx = (t->x + t->tx) - 1;
    608 
    609 	for (i = 0; i < numChars; i++)
    610 	{
    611 		const int32_t cw = charWidth(t->textPtr[i]);
    612 		const int32_t tx2 = tx + cw;
    613 
    614 		if (mx >= tx && mx < tx2)
    615 		{
    616 			markX2 = i;
    617 			break;
    618 		}
    619 
    620 		tx += cw;
    621 	}
    622 
    623 	// set to last character if we clicked outside the end of the text
    624 	if (i == numChars && mx >= tx)
    625 		markX2 = numChars;
    626 
    627 	if (mouse.x >= t->x+t->w-3)
    628 	{
    629 		scrollTextBufferRight(t, numChars);
    630 		if (++markX2 > numChars)
    631 			markX2 = numChars;
    632 	}
    633 	else if (mouse.x <= t->x+t->tx+3)
    634 	{
    635 		if (t->bufOffset > 0)
    636 		{
    637 			scrollTextBufferLeft(t);
    638 			if (--markX2 < 0)
    639 				markX2 = 0;
    640 		}
    641 	}
    642 
    643 	t->cursorPos = markX2;
    644 	assert(t->cursorPos >= 0 && t->cursorPos <= getTextLength(t, 0));
    645 
    646 	editor.textCursorBlinkCounter = 0;
    647 }
    648 
    649 void handleTextBoxWhileMouseDown(void)
    650 {
    651 	assert(mouse.lastUsedObjectID >= 0 && mouse.lastUsedObjectID < NUM_TEXTBOXES);
    652 	textBox_t *t = &textBoxes[mouse.lastUsedObjectID];
    653 	if (!t->visible)
    654 		return;
    655 
    656 	if (mouse.x != oldMouseX)
    657 	{
    658 		oldMouseX = mouse.x;
    659 
    660 		markX1 = oldCursorPos;
    661 		setMarkX2ToMouseX(t);
    662 
    663 		drawTextBox(mouse.lastUsedObjectID);
    664 	}
    665 }
    666 
    667 bool testTextBoxMouseDown(void)
    668 {
    669 	uint16_t start, end;
    670 
    671 	oldMouseX = mouse.x;
    672 	oldCursorPos = 0;
    673 
    674 	if (ui.sysReqShown)
    675 	{
    676 		// if a system request is open, only test the first textbox (reserved)
    677 		start = 0;
    678 		end = 1;
    679 	}
    680 	else
    681 	{
    682 		start = 1;
    683 		end = NUM_TEXTBOXES;
    684 	}
    685 
    686 	const int32_t mx = mouse.x;
    687 	const int32_t my = mouse.y;
    688 
    689 	textBox_t *t = &textBoxes[start];
    690 	for (uint16_t i = start; i < end; i++, t++)
    691 	{
    692 		if (!t->visible || t->textPtr == NULL)
    693 			continue;
    694 
    695 		if (my >= t->y && my < t->y+t->h &&
    696 		    mx >= t->x && mx < t->x+t->w)
    697 		{
    698 			if (!mouse.rightButtonPressed && t->rightMouseButton)
    699 				break;
    700 
    701 			// if we were editing another text box and clicked on another one, properly end it
    702 			if (editor.editTextFlag && i != mouse.lastEditBox)
    703 				exitTextEditing();
    704 
    705 			mouse.lastEditBox = i;
    706 			moveTextCursorToMouseX(mouse.lastEditBox);
    707 
    708 			oldCursorPos = t->cursorPos;
    709 			removeTextMarking();
    710 			drawTextBox(mouse.lastEditBox);
    711 
    712 			editor.textCursorBlinkCounter  = 0;
    713 			mouse.lastUsedObjectType = OBJECT_TEXTBOX;
    714 			mouse.lastUsedObjectID = i;
    715 
    716 			editor.editTextFlag = true;
    717 
    718 			SDL_StartTextInput();
    719 			return true;
    720 		}
    721 	}
    722 
    723 	// if we were editing text and we clicked outside of a text box, exit text editing
    724 	if (editor.editTextFlag)
    725 	{
    726 		exitTextEditing();
    727 		keyb.ignoreCurrKeyUp = false; // if we exited with mouse, don't handle key-up kludge
    728 	}
    729 
    730 	return false;
    731 }
    732 
    733 void updateTextBoxPointers(void)
    734 {
    735 	int32_t i;
    736 	instr_t *curIns = instr[editor.curInstr];
    737 
    738 	// instrument names
    739 	for (i = 0; i < 8; i++)
    740 		textBoxes[TB_INST1+i].textPtr = song.instrName[1+editor.instrBankOffset+i];
    741 
    742 	// sample names
    743 	if (editor.curInstr == 0 || curIns == NULL)
    744 	{
    745 		for (i = 0; i < 5; i++)
    746 			textBoxes[TB_SAMP1+i].textPtr = NULL;
    747 	}
    748 	else
    749 	{
    750 		for (i = 0; i < 5; i++)
    751 			textBoxes[TB_SAMP1+i].textPtr = curIns->smp[editor.sampleBankOffset+i].name;
    752 	}
    753 
    754 	// song name
    755 	textBoxes[TB_SONG_NAME].textPtr = song.name;
    756 }
    757 
    758 void setupInitialTextBoxPointers(void)
    759 {
    760 	textBoxes[TB_CONF_DEF_MODS_DIR].textPtr = config.modulesPath;
    761 	textBoxes[TB_CONF_DEF_INSTRS_DIR].textPtr = config.instrPath;
    762 	textBoxes[TB_CONF_DEF_SAMPS_DIR].textPtr = config.samplesPath;
    763 	textBoxes[TB_CONF_DEF_PATTS_DIR].textPtr = config.patternsPath;
    764 	textBoxes[TB_CONF_DEF_TRACKS_DIR].textPtr = config.tracksPath;
    765 }
    766 
    767 void setTextCursorToEnd(textBox_t *t)
    768 {
    769 	uint16_t numChars;
    770 
    771 	// count number of chars and get full text width
    772 	uint32_t textWidth = 0;
    773 	for (numChars = 0; numChars < t->maxChars; numChars++)
    774 	{
    775 		const char ch = t->textPtr[numChars];
    776 		if (ch == '\0')
    777 			break;
    778 
    779 		textWidth += charWidth(ch);
    780 	}
    781 
    782 	// if cursor is not at the end, handle text marking
    783 	if (t->cursorPos < numChars)
    784 	{
    785 		if (keyb.leftShiftPressed)
    786 		{
    787 			if (!textIsMarked())
    788 				markX1 = t->cursorPos;
    789 
    790 			markX2 = numChars;
    791 		}
    792 		else
    793 		{
    794 			removeTextMarking();
    795 		}
    796 	}
    797 
    798 	t->cursorPos = numChars;
    799 
    800 	t->bufOffset = 0;
    801 	if (textWidth > t->renderW)
    802 		t->bufOffset = textWidth - t->renderW;
    803 
    804 	drawTextBox(mouse.lastEditBox);
    805 	editor.textCursorBlinkCounter = 0;
    806 }
    807 
    808 void handleTextEditControl(SDL_Keycode keycode)
    809 {
    810 	int16_t i;
    811 	uint16_t numChars;
    812 	int32_t textLength;
    813 	uint32_t textWidth;
    814 
    815 	assert(mouse.lastEditBox >= 0 && mouse.lastEditBox < NUM_TEXTBOXES);
    816 
    817 	textBox_t *t = &textBoxes[mouse.lastEditBox];
    818 	assert(t->textPtr != NULL);
    819 
    820 	switch (keycode)
    821 	{
    822 		case SDLK_ESCAPE:
    823 		{
    824 			removeTextMarking();
    825 			exitTextEditing();
    826 		}
    827 		break;
    828 
    829 		case SDLK_a:
    830 		{
    831 			// CTRL+A - mark all text
    832 #ifdef __APPLE__
    833 			if (keyb.leftCtrlPressed || keyb.leftCommandPressed)
    834 #else
    835 			if (keyb.leftCtrlPressed)
    836 #endif
    837 			{
    838 				// count number of chars and get full text width
    839 				textWidth = 0;
    840 				for (numChars = 0; numChars < t->maxChars; numChars++)
    841 				{
    842 					if (t->textPtr[numChars] == '\0')
    843 						break;
    844 
    845 					textWidth += charWidth(t->textPtr[numChars]);
    846 				}
    847 
    848 				markX1 = 0;
    849 				markX2 = numChars;
    850 				t->cursorPos = markX2;
    851 
    852 				t->bufOffset = 0;
    853 				if (textWidth > t->renderW)
    854 					t->bufOffset = textWidth - t->renderW;
    855 
    856 				drawTextBox(mouse.lastEditBox);
    857 			}
    858 		}
    859 		break;
    860 
    861 		case SDLK_x:
    862 		{
    863 			// CTRL+X - cut marked text
    864 #ifdef __APPLE__
    865 			if (keyb.leftCtrlPressed || keyb.leftCommandPressed)
    866 #else
    867 			if (keyb.leftCtrlPressed)
    868 #endif
    869 				cutMarkedText(t);
    870 		}
    871 		break;
    872 
    873 		case SDLK_c:
    874 		{
    875 			// CTRL+C - copy marked text
    876 #ifdef __APPLE__
    877 			if (keyb.leftCtrlPressed || keyb.leftCommandPressed)
    878 #else
    879 			if (keyb.leftCtrlPressed)
    880 #endif
    881 				copyMarkedText(t);
    882 		}
    883 		break;
    884 
    885 		case SDLK_v:
    886 		{
    887 			// CTRL+V - paste text
    888 #ifdef __APPLE__
    889 			if (keyb.leftCtrlPressed || keyb.leftCommandPressed)
    890 #else
    891 			if (keyb.leftCtrlPressed)
    892 #endif
    893 				pasteText(t);
    894 		}
    895 		break;
    896 
    897 		case SDLK_KP_ENTER:
    898 		case SDLK_RETURN:
    899 		{
    900 			// ALT+ENTER = toggle fullscreen, even while text editing
    901 			if (keyb.leftAltPressed)
    902 				toggleFullscreen();
    903 			else
    904 				exitTextEditing();
    905 		}
    906 		break;
    907 
    908 		case SDLK_LEFT:
    909 		{
    910 			if (keyb.leftShiftPressed)
    911 			{
    912 				if (!textIsMarked())
    913 				{
    914 					// no marking, mark character to left from cursor
    915 					if (t->cursorPos > 0)
    916 					{
    917 						markX1 = t->cursorPos;
    918 						moveTextCursorLeft(mouse.lastEditBox, TEXTBOX_NO_UPDATE);
    919 						markX2 = t->cursorPos;
    920 
    921 						drawTextBox(mouse.lastEditBox);
    922 					}
    923 				}
    924 				else
    925 				{
    926 					// marking, extend/shrink marking
    927 					if (markX2 > 0)
    928 					{
    929 						t->cursorPos = markX2;
    930 						markX2--;
    931 						moveTextCursorLeft(mouse.lastEditBox, TEXTBOX_NO_UPDATE);
    932 						drawTextBox(mouse.lastEditBox);
    933 					}
    934 				}
    935 			}
    936 			else
    937 			{
    938 				if (textIsMarked())
    939 				{
    940 					setCursorToMarkStart(t);
    941 					removeTextMarking();
    942 				}
    943 				else
    944 				{
    945 					removeTextMarking();
    946 					moveTextCursorLeft(mouse.lastEditBox, TEXTBOX_NO_UPDATE);
    947 				}
    948 
    949 				drawTextBox(mouse.lastEditBox);
    950 			}
    951 		}
    952 		break;
    953 
    954 		case SDLK_RIGHT:
    955 		{
    956 			if (keyb.leftShiftPressed)
    957 			{
    958 				textLength = getTextLength(t, 0);
    959 
    960 				if (!textIsMarked())
    961 				{
    962 					if (t->cursorPos < textLength)
    963 					{
    964 						markX1 = t->cursorPos;
    965 						moveTextCursorRight(mouse.lastEditBox, TEXTBOX_NO_UPDATE);
    966 						markX2 = t->cursorPos;
    967 
    968 						drawTextBox(mouse.lastEditBox);
    969 					}
    970 				}
    971 				else
    972 				{
    973 					// marking, extend/shrink marking
    974 					if (markX2 < textLength)
    975 					{
    976 						t->cursorPos = markX2;
    977 						markX2++;
    978 						moveTextCursorRight(mouse.lastEditBox, TEXTBOX_NO_UPDATE);
    979 						drawTextBox(mouse.lastEditBox);
    980 					}
    981 				}
    982 			}
    983 			else
    984 			{
    985 				if (textIsMarked())
    986 				{
    987 					setCursorToMarkEnd(t);
    988 					removeTextMarking();
    989 				}
    990 				else
    991 				{
    992 					removeTextMarking();
    993 					moveTextCursorRight(mouse.lastEditBox, TEXTBOX_NO_UPDATE);
    994 				}
    995 
    996 				drawTextBox(mouse.lastEditBox);
    997 			}
    998 		}
    999 		break;
   1000 
   1001 		case SDLK_BACKSPACE:
   1002 		{
   1003 			if (textIsMarked())
   1004 			{
   1005 				deleteMarkedText(t);
   1006 				removeTextMarking();
   1007 				drawTextBox(mouse.lastEditBox);
   1008 				break;
   1009 			}
   1010 
   1011 			removeTextMarking();
   1012 
   1013 			if (t->cursorPos > 0 && t->textPtr[0] != '\0')
   1014 			{
   1015 				// scroll buffer offset if we are scrolled
   1016 				if (t->bufOffset > 0)
   1017 				{
   1018 					t->bufOffset -= charWidth(t->textPtr[t->cursorPos-1]);
   1019 					if (t->bufOffset < 0)
   1020 						t->bufOffset = 0;
   1021 				}
   1022 
   1023 				moveTextCursorLeft(mouse.lastEditBox, TEXTBOX_UPDATE);
   1024 
   1025 				i = t->cursorPos;
   1026 				while (i < t->maxChars)
   1027 				{
   1028 					t->textPtr[i] = t->textPtr[i+1];
   1029 					if (t->textPtr[i] == '\0')
   1030 						break;
   1031 					i++;
   1032 				}
   1033 
   1034 				drawTextBox(mouse.lastEditBox);
   1035 				setSongModifiedFlagIfNeeded();
   1036 			}
   1037 		}
   1038 		break;
   1039 
   1040 		case SDLK_DELETE:
   1041 		{
   1042 			if (textIsMarked())
   1043 			{
   1044 				deleteMarkedText(t);
   1045 				removeTextMarking();
   1046 				drawTextBox(mouse.lastEditBox);
   1047 				break;
   1048 			}
   1049 
   1050 			if (t->textPtr[t->cursorPos] != '\0' && t->textPtr[0] != '\0' && t->cursorPos < t->maxChars)
   1051 			{
   1052 				// scroll buffer offset if we are scrolled
   1053 				if (t->bufOffset > 0)
   1054 				{
   1055 					t->bufOffset -= charWidth(t->textPtr[t->cursorPos]);
   1056 					if (t->bufOffset < 0)
   1057 						t->bufOffset = 0;
   1058 				}
   1059 
   1060 				i = t->cursorPos;
   1061 				while (i < t->maxChars)
   1062 				{
   1063 					if (t->textPtr[i] == '\0')
   1064 						break;
   1065 
   1066 					t->textPtr[i] = t->textPtr[i+1];
   1067 					i++;
   1068 				}
   1069 
   1070 				drawTextBox(mouse.lastEditBox);
   1071 				setSongModifiedFlagIfNeeded();
   1072 			}
   1073 		}
   1074 		break;
   1075 
   1076 		case SDLK_HOME:
   1077 		{
   1078 			if (keyb.leftShiftPressed)
   1079 			{
   1080 				if (!textIsMarked())
   1081 					markX1 = t->cursorPos;
   1082 
   1083 				markX2 = 0;
   1084 				t->bufOffset = 0;
   1085 				t->cursorPos = 0;
   1086 			}
   1087 			else
   1088 			{
   1089 				removeTextMarking();
   1090 
   1091 				if (t->cursorPos > 0)
   1092 				{
   1093 					t->cursorPos = 0;
   1094 					t->bufOffset = 0;
   1095 
   1096 					editor.textCursorBlinkCounter = 0;
   1097 				}
   1098 			}
   1099 
   1100 			drawTextBox(mouse.lastEditBox);
   1101 		}
   1102 		break;
   1103 
   1104 		case SDLK_END:
   1105 		{
   1106 			setTextCursorToEnd(t);
   1107 		}
   1108 		break;
   1109 
   1110 		default: break;
   1111 	}
   1112 }
   1113 
   1114 void handleTextEditInputChar(char textChar)
   1115 {
   1116 	assert(mouse.lastEditBox >= 0 && mouse.lastEditBox < NUM_TEXTBOXES);
   1117 
   1118 	textBox_t *t = &textBoxes[mouse.lastEditBox];
   1119 	if (t->textPtr == NULL)
   1120 		return;
   1121 
   1122 	const int8_t ch = (const int8_t)textChar;
   1123 	if (ch < 32 &&
   1124 		ch != -124 && ch != -108 && ch != -122 && ch != -114 && ch != -103 &&
   1125 		ch != -113 && ch != -101 && ch != -99 && ch != -111 && ch != -110)
   1126 	{
   1127 		return; // only allow certain codepage 850 nordic characters
   1128 	}
   1129 
   1130 	if (textIsMarked())
   1131 	{
   1132 		deleteMarkedText(t);
   1133 		removeTextMarking();
   1134 	}
   1135 
   1136 	if (t->cursorPos >= 0 && t->cursorPos < t->maxChars)
   1137 	{
   1138 		int32_t i = getTextLength(t, 0);
   1139 		if (i < t->maxChars) // do we have room for a new character?
   1140 		{
   1141 			t->textPtr[i+1] = '\0';
   1142 
   1143 			// if string not empty, shift string to the right to make space for char insertion
   1144 			if (i > 0)
   1145 			{
   1146 				for (; i > t->cursorPos; i--)
   1147 					t->textPtr[i] = t->textPtr[i-1];
   1148 			}
   1149 
   1150 			t->textPtr[t->cursorPos] = textChar;
   1151 
   1152 			moveTextCursorRight(mouse.lastEditBox, TEXTBOX_UPDATE); // also updates textbox
   1153 			setSongModifiedFlagIfNeeded();
   1154 		}
   1155 	}
   1156 }
   1157 
   1158 static void moveTextCursorLeft(int16_t i, bool updateTextBox)
   1159 {
   1160 	textBox_t *t = &textBoxes[i];
   1161 	if (t->cursorPos == 0)
   1162 		return;
   1163 
   1164 	t->cursorPos--;
   1165 
   1166 	// scroll buffer if needed
   1167 	if (cursorPosToX(t) < 0-1)
   1168 		scrollTextBufferLeft(t);
   1169 
   1170 	if (updateTextBox)
   1171 		drawTextBox(i);
   1172 
   1173 	editor.textCursorBlinkCounter = 0; // reset text cursor blink timer
   1174 }
   1175 
   1176 static void moveTextCursorRight(int16_t i, bool updateTextBox)
   1177 {
   1178 	textBox_t *t = &textBoxes[i];
   1179 
   1180 	const uint16_t numChars = getTextLength(t, 0);
   1181 	if (t->cursorPos >= numChars)
   1182 		return;
   1183 
   1184 	t->cursorPos++;
   1185 
   1186 	// scroll buffer if needed
   1187 	if (cursorPosToX(t) >= t->renderW)
   1188 		scrollTextBufferRight(t, numChars);
   1189 
   1190 	if (updateTextBox)
   1191 		drawTextBox(i);
   1192 
   1193 	editor.textCursorBlinkCounter = 0; // reset text cursor blink timer
   1194 }
   1195 
   1196 void freeTextBoxes(void)
   1197 {
   1198 	// free text box buffers (skip first entry, it's reserved for inputBox())
   1199 	textBox_t *t = &textBoxes[1];
   1200 	for (int32_t i = 1; i < NUM_TEXTBOXES; i++, t++)
   1201 	{
   1202 		if (t->renderBuf != NULL)
   1203 		{
   1204 			free(t->renderBuf);
   1205 			t->renderBuf = NULL;
   1206 		}
   1207 	}
   1208 }