1 /*
2  * This file is part of OpenTTD.
3  * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4  * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5  * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6  */
7 
8 /** @file textbuf.cpp Textbuffer handling. */
9 
10 #include "stdafx.h"
11 #include <stdarg.h>
12 
13 #include "textbuf_type.h"
14 #include "string_func.h"
15 #include "strings_func.h"
16 #include "gfx_type.h"
17 #include "gfx_func.h"
18 #include "window_func.h"
19 #include "core/alloc_func.hpp"
20 
21 #include "safeguards.h"
22 
23 /**
24  * Try to retrieve the current clipboard contents.
25  *
26  * @note OS-specific function.
27  * @param buffer Clipboard content.
28  * @param last The pointer to the last element of the destination buffer
29  * @return True if some text could be retrieved.
30  */
31 bool GetClipboardContents(char *buffer, const char *last);
32 
33 int _caret_timer;
34 
35 
36 /**
37  * Checks if it is possible to delete a character.
38  * @param backspace if set, delete the character before the caret,
39  * otherwise, delete the character after it.
40  * @return true if a character can be deleted in the given direction.
41  */
CanDelChar(bool backspace)42 bool Textbuf::CanDelChar(bool backspace)
43 {
44 	return backspace ? this->caretpos != 0 : this->caretpos < this->bytes - 1;
45 }
46 
47 /**
48  * Delete a character from a textbuffer, either with 'Delete' or 'Backspace'
49  * The character is delete from the position the caret is at
50  * @param keycode Type of deletion, either WKC_BACKSPACE or WKC_DELETE
51  * @return Return true on successful change of Textbuf, or false otherwise
52  */
DeleteChar(uint16 keycode)53 bool Textbuf::DeleteChar(uint16 keycode)
54 {
55 	bool word = (keycode & WKC_CTRL) != 0;
56 
57 	keycode &= ~WKC_SPECIAL_KEYS;
58 	if (keycode != WKC_BACKSPACE && keycode != WKC_DELETE) return false;
59 
60 	bool backspace = keycode == WKC_BACKSPACE;
61 
62 	if (!CanDelChar(backspace)) return false;
63 
64 	char *s = this->buf + this->caretpos;
65 	uint16 len = 0;
66 
67 	if (word) {
68 		/* Delete a complete word. */
69 		if (backspace) {
70 			/* Delete whitespace and word in front of the caret. */
71 			len = this->caretpos - (uint16)this->char_iter->Prev(StringIterator::ITER_WORD);
72 			s -= len;
73 		} else {
74 			/* Delete word and following whitespace following the caret. */
75 			len = (uint16)this->char_iter->Next(StringIterator::ITER_WORD) - this->caretpos;
76 		}
77 		/* Update character count. */
78 		for (const char *ss = s; ss < s + len; Utf8Consume(&ss)) {
79 			this->chars--;
80 		}
81 	} else {
82 		/* Delete a single character. */
83 		if (backspace) {
84 			/* Delete the last code point in front of the caret. */
85 			s = Utf8PrevChar(s);
86 			WChar c;
87 			len = (uint16)Utf8Decode(&c, s);
88 			this->chars--;
89 		} else {
90 			/* Delete the complete character following the caret. */
91 			len = (uint16)this->char_iter->Next(StringIterator::ITER_CHARACTER) - this->caretpos;
92 			/* Update character count. */
93 			for (const char *ss = s; ss < s + len; Utf8Consume(&ss)) {
94 				this->chars--;
95 			}
96 		}
97 	}
98 
99 	/* Move the remaining characters over the marker */
100 	memmove(s, s + len, this->bytes - (s - this->buf) - len);
101 	this->bytes -= len;
102 
103 	if (backspace) this->caretpos -= len;
104 
105 	this->UpdateStringIter();
106 	this->UpdateWidth();
107 	this->UpdateCaretPosition();
108 	this->UpdateMarkedText();
109 
110 	return true;
111 }
112 
113 /**
114  * Delete every character in the textbuffer
115  */
DeleteAll()116 void Textbuf::DeleteAll()
117 {
118 	memset(this->buf, 0, this->max_bytes);
119 	this->bytes = this->chars = 1;
120 	this->pixels = this->caretpos = this->caretxoffs = 0;
121 	this->markpos = this->markend = this->markxoffs = this->marklength = 0;
122 	this->UpdateStringIter();
123 }
124 
125 /**
126  * Insert a character to a textbuffer. If maxwidth of the Textbuf is zero,
127  * we don't care about the visual-length but only about the physical
128  * length of the string
129  * @param key Character to be inserted
130  * @return Return true on successful change of Textbuf, or false otherwise
131  */
InsertChar(WChar key)132 bool Textbuf::InsertChar(WChar key)
133 {
134 	uint16 len = (uint16)Utf8CharLen(key);
135 	if (this->bytes + len <= this->max_bytes && this->chars + 1 <= this->max_chars) {
136 		memmove(this->buf + this->caretpos + len, this->buf + this->caretpos, this->bytes - this->caretpos);
137 		Utf8Encode(this->buf + this->caretpos, key);
138 		this->chars++;
139 		this->bytes    += len;
140 		this->caretpos += len;
141 
142 		this->UpdateStringIter();
143 		this->UpdateWidth();
144 		this->UpdateCaretPosition();
145 		this->UpdateMarkedText();
146 		return true;
147 	}
148 	return false;
149 }
150 
151 /**
152  * Insert a string into the text buffer. If maxwidth of the Textbuf is zero,
153  * we don't care about the visual-length but only about the physical
154  * length of the string.
155  * @param str String to insert.
156  * @param marked Replace the currently marked text with the new text.
157  * @param caret Move the caret to this point in the insertion string.
158  * @param insert_location Position at which to insert the string.
159  * @param replacement_end Replace all characters from #insert_location up to this location with the new string.
160  * @return True on successful change of Textbuf, or false otherwise.
161  */
InsertString(const char * str,bool marked,const char * caret,const char * insert_location,const char * replacement_end)162 bool Textbuf::InsertString(const char *str, bool marked, const char *caret, const char *insert_location, const char *replacement_end)
163 {
164 	uint16 insertpos = (marked && this->marklength != 0) ? this->markpos : this->caretpos;
165 	if (insert_location != nullptr) {
166 		insertpos = insert_location - this->buf;
167 		if (insertpos > this->bytes) return false;
168 
169 		if (replacement_end != nullptr) {
170 			this->DeleteText(insertpos, replacement_end - this->buf, str == nullptr);
171 		}
172 	} else {
173 		if (marked) this->DiscardMarkedText(str == nullptr);
174 	}
175 
176 	if (str == nullptr) return false;
177 
178 	uint16 bytes = 0, chars = 0;
179 	WChar c;
180 	for (const char *ptr = str; (c = Utf8Consume(&ptr)) != '\0';) {
181 		if (!IsValidChar(c, this->afilter)) break;
182 
183 		byte len = Utf8CharLen(c);
184 		if (this->bytes + bytes + len > this->max_bytes) break;
185 		if (this->chars + chars + 1   > this->max_chars) break;
186 
187 		bytes += len;
188 		chars++;
189 
190 		/* Move caret if needed. */
191 		if (ptr == caret) this->caretpos = insertpos + bytes;
192 	}
193 
194 	if (bytes == 0) return false;
195 
196 	if (marked) {
197 		this->markpos = insertpos;
198 		this->markend = insertpos + bytes;
199 	}
200 
201 	memmove(this->buf + insertpos + bytes, this->buf + insertpos, this->bytes - insertpos);
202 	memcpy(this->buf + insertpos, str, bytes);
203 
204 	this->bytes += bytes;
205 	this->chars += chars;
206 	if (!marked && caret == nullptr) this->caretpos += bytes;
207 	assert(this->bytes <= this->max_bytes);
208 	assert(this->chars <= this->max_chars);
209 	this->buf[this->bytes - 1] = '\0'; // terminating zero
210 
211 	this->UpdateStringIter();
212 	this->UpdateWidth();
213 	this->UpdateCaretPosition();
214 	this->UpdateMarkedText();
215 
216 	return true;
217 }
218 
219 /**
220  * Insert a chunk of text from the clipboard onto the textbuffer. Get TEXT clipboard
221  * and append this up to the maximum length (either absolute or screenlength). If maxlength
222  * is zero, we don't care about the screenlength but only about the physical length of the string
223  * @return true on successful change of Textbuf, or false otherwise
224  */
InsertClipboard()225 bool Textbuf::InsertClipboard()
226 {
227 	char utf8_buf[512];
228 
229 	if (!GetClipboardContents(utf8_buf, lastof(utf8_buf))) return false;
230 
231 	return this->InsertString(utf8_buf, false);
232 }
233 
234 /**
235  * Delete a part of the text.
236  * @param from Start of the text to delete.
237  * @param to End of the text to delete.
238  * @param update Set to true if the internal state should be updated.
239  */
DeleteText(uint16 from,uint16 to,bool update)240 void Textbuf::DeleteText(uint16 from, uint16 to, bool update)
241 {
242 	uint c = 0;
243 	const char *s = this->buf + from;
244 	while (s < this->buf + to) {
245 		Utf8Consume(&s);
246 		c++;
247 	}
248 
249 	/* Strip marked characters from buffer. */
250 	memmove(this->buf + from, this->buf + to, this->bytes - to);
251 	this->bytes -= to - from;
252 	this->chars -= c;
253 
254 	/* Fixup caret if needed. */
255 	if (this->caretpos > from) {
256 		if (this->caretpos <= to) {
257 			this->caretpos = from;
258 		} else {
259 			this->caretpos -= to - from;
260 		}
261 	}
262 
263 	if (update) {
264 		this->UpdateStringIter();
265 		this->UpdateCaretPosition();
266 		this->UpdateMarkedText();
267 	}
268 }
269 
270 /**
271  * Discard any marked text.
272  * @param update Set to true if the internal state should be updated.
273  */
DiscardMarkedText(bool update)274 void Textbuf::DiscardMarkedText(bool update)
275 {
276 	if (this->markend == 0) return;
277 
278 	this->DeleteText(this->markpos, this->markend, update);
279 	this->markpos = this->markend = this->markxoffs = this->marklength = 0;
280 }
281 
282 /** Update the character iter after the text has changed. */
UpdateStringIter()283 void Textbuf::UpdateStringIter()
284 {
285 	this->char_iter->SetString(this->buf);
286 	size_t pos = this->char_iter->SetCurPosition(this->caretpos);
287 	this->caretpos = pos == StringIterator::END ? 0 : (uint16)pos;
288 }
289 
290 /** Update pixel width of the text. */
UpdateWidth()291 void Textbuf::UpdateWidth()
292 {
293 	this->pixels = GetStringBoundingBox(this->buf, FS_NORMAL).width;
294 }
295 
296 /** Update pixel position of the caret. */
UpdateCaretPosition()297 void Textbuf::UpdateCaretPosition()
298 {
299 	this->caretxoffs = this->chars > 1 ? GetCharPosInString(this->buf, this->buf + this->caretpos, FS_NORMAL).x : 0;
300 }
301 
302 /** Update pixel positions of the marked text area. */
UpdateMarkedText()303 void Textbuf::UpdateMarkedText()
304 {
305 	if (this->markend != 0) {
306 		this->markxoffs  = GetCharPosInString(this->buf, this->buf + this->markpos, FS_NORMAL).x;
307 		this->marklength = GetCharPosInString(this->buf, this->buf + this->markend, FS_NORMAL).x - this->markxoffs;
308 	} else {
309 		this->markxoffs = this->marklength = 0;
310 	}
311 }
312 
313 /**
314  * Handle text navigation with arrow keys left/right.
315  * This defines where the caret will blink and the next character interaction will occur
316  * @param keycode Direction in which navigation occurs (WKC_CTRL |) WKC_LEFT, (WKC_CTRL |) WKC_RIGHT, WKC_END, WKC_HOME
317  * @return Return true on successful change of Textbuf, or false otherwise
318  */
MovePos(uint16 keycode)319 bool Textbuf::MovePos(uint16 keycode)
320 {
321 	switch (keycode) {
322 		case WKC_LEFT:
323 		case WKC_CTRL | WKC_LEFT: {
324 			if (this->caretpos == 0) break;
325 
326 			size_t pos = this->char_iter->Prev(keycode & WKC_CTRL ? StringIterator::ITER_WORD : StringIterator::ITER_CHARACTER);
327 			if (pos == StringIterator::END) return true;
328 
329 			this->caretpos = (uint16)pos;
330 			this->UpdateCaretPosition();
331 			return true;
332 		}
333 
334 		case WKC_RIGHT:
335 		case WKC_CTRL | WKC_RIGHT: {
336 			if (this->caretpos >= this->bytes - 1) break;
337 
338 			size_t pos = this->char_iter->Next(keycode & WKC_CTRL ? StringIterator::ITER_WORD : StringIterator::ITER_CHARACTER);
339 			if (pos == StringIterator::END) return true;
340 
341 			this->caretpos = (uint16)pos;
342 			this->UpdateCaretPosition();
343 			return true;
344 		}
345 
346 		case WKC_HOME:
347 			this->caretpos = 0;
348 			this->char_iter->SetCurPosition(this->caretpos);
349 			this->UpdateCaretPosition();
350 			return true;
351 
352 		case WKC_END:
353 			this->caretpos = this->bytes - 1;
354 			this->char_iter->SetCurPosition(this->caretpos);
355 			this->UpdateCaretPosition();
356 			return true;
357 
358 		default:
359 			break;
360 	}
361 
362 	return false;
363 }
364 
365 /**
366  * Initialize the textbuffer by supplying it the buffer to write into
367  * and the maximum length of this buffer
368  * @param max_bytes maximum size in bytes, including terminating '\0'
369  * @param max_chars maximum size in chars, including terminating '\0'
370  */
Textbuf(uint16 max_bytes,uint16 max_chars)371 Textbuf::Textbuf(uint16 max_bytes, uint16 max_chars)
372 	: buf(MallocT<char>(max_bytes))
373 {
374 	assert(max_bytes != 0);
375 	assert(max_chars != 0);
376 
377 	this->char_iter = StringIterator::Create();
378 
379 	this->afilter    = CS_ALPHANUMERAL;
380 	this->max_bytes  = max_bytes;
381 	this->max_chars  = max_chars == UINT16_MAX ? max_bytes : max_chars;
382 	this->caret      = true;
383 	this->DeleteAll();
384 }
385 
~Textbuf()386 Textbuf::~Textbuf()
387 {
388 	delete this->char_iter;
389 	free(this->buf);
390 }
391 
392 /**
393  * Render a string into the textbuffer.
394  * @param string String
395  */
Assign(StringID string)396 void Textbuf::Assign(StringID string)
397 {
398 	GetString(this->buf, string, &this->buf[this->max_bytes - 1]);
399 	this->UpdateSize();
400 }
401 
402 /**
403  * Copy a string into the textbuffer.
404  * @param text Source.
405  */
Assign(const char * text)406 void Textbuf::Assign(const char *text)
407 {
408 	strecpy(this->buf, text, &this->buf[this->max_bytes - 1]);
409 	this->UpdateSize();
410 }
411 
412 /**
413  * Print a formatted string into the textbuffer.
414  */
Print(const char * format,...)415 void Textbuf::Print(const char *format, ...)
416 {
417 	va_list va;
418 	va_start(va, format);
419 	vseprintf(this->buf, &this->buf[this->max_bytes - 1], format, va);
420 	va_end(va);
421 	this->UpdateSize();
422 }
423 
424 
425 /**
426  * Update Textbuf type with its actual physical character and screenlength
427  * Get the count of characters in the string as well as the width in pixels.
428  * Useful when copying in a larger amount of text at once
429  */
UpdateSize()430 void Textbuf::UpdateSize()
431 {
432 	const char *buf = this->buf;
433 
434 	this->chars = this->bytes = 1; // terminating zero
435 
436 	WChar c;
437 	while ((c = Utf8Consume(&buf)) != '\0') {
438 		this->bytes += Utf8CharLen(c);
439 		this->chars++;
440 	}
441 	assert(this->bytes <= this->max_bytes);
442 	assert(this->chars <= this->max_chars);
443 
444 	this->caretpos = this->bytes - 1;
445 	this->UpdateStringIter();
446 	this->UpdateWidth();
447 	this->UpdateMarkedText();
448 
449 	this->UpdateCaretPosition();
450 }
451 
452 /**
453  * Handle the flashing of the caret.
454  * @return True if the caret state changes.
455  */
HandleCaret()456 bool Textbuf::HandleCaret()
457 {
458 	/* caret changed? */
459 	bool b = !!(_caret_timer & 0x20);
460 
461 	if (b != this->caret) {
462 		this->caret = b;
463 		return true;
464 	}
465 	return false;
466 }
467 
HandleKeyPress(WChar key,uint16 keycode)468 HandleKeyPressResult Textbuf::HandleKeyPress(WChar key, uint16 keycode)
469 {
470 	bool edited = false;
471 
472 	switch (keycode) {
473 		case WKC_ESC: return HKPR_CANCEL;
474 
475 		case WKC_RETURN: case WKC_NUM_ENTER: return HKPR_CONFIRM;
476 
477 		case (WKC_CTRL | 'V'):
478 			edited = this->InsertClipboard();
479 			break;
480 
481 		case (WKC_CTRL | 'U'):
482 			this->DeleteAll();
483 			edited = true;
484 			break;
485 
486 		case WKC_BACKSPACE: case WKC_DELETE:
487 		case WKC_CTRL | WKC_BACKSPACE: case WKC_CTRL | WKC_DELETE:
488 			edited = this->DeleteChar(keycode);
489 			break;
490 
491 		case WKC_LEFT: case WKC_RIGHT: case WKC_END: case WKC_HOME:
492 		case WKC_CTRL | WKC_LEFT: case WKC_CTRL | WKC_RIGHT:
493 			this->MovePos(keycode);
494 			break;
495 
496 		default:
497 			if (IsValidChar(key, this->afilter)) {
498 				edited = this->InsertChar(key);
499 			} else {
500 				return HKPR_NOT_HANDLED;
501 			}
502 			break;
503 	}
504 
505 	return edited ? HKPR_EDITING : HKPR_CURSOR;
506 }
507