Fix: CmdSetAutoReplace didn't validate group type and engine type match (#9950)
[openttd-github.git] / src / textbuf.cpp
blob75e1f4484842167248648fcad0c63e2fb5249f43
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 */
8 /** @file textbuf.cpp Textbuffer handling. */
10 #include "stdafx.h"
11 #include <stdarg.h>
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"
21 #include "safeguards.h"
23 /**
24 * Try to retrieve the current clipboard contents.
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.
31 bool GetClipboardContents(char *buffer, const char *last);
33 int _caret_timer;
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.
42 bool Textbuf::CanDelChar(bool backspace)
44 return backspace ? this->caretpos != 0 : this->caretpos < this->bytes - 1;
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
53 bool Textbuf::DeleteChar(uint16 keycode)
55 bool word = (keycode & WKC_CTRL) != 0;
57 keycode &= ~WKC_SPECIAL_KEYS;
58 if (keycode != WKC_BACKSPACE && keycode != WKC_DELETE) return false;
60 bool backspace = keycode == WKC_BACKSPACE;
62 if (!CanDelChar(backspace)) return false;
64 char *s = this->buf + this->caretpos;
65 uint16 len = 0;
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;
77 /* Update character count. */
78 for (const char *ss = s; ss < s + len; Utf8Consume(&ss)) {
79 this->chars--;
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--;
99 /* Move the remaining characters over the marker */
100 memmove(s, s + len, this->bytes - (s - this->buf) - len);
101 this->bytes -= len;
103 if (backspace) this->caretpos -= len;
105 this->UpdateStringIter();
106 this->UpdateWidth();
107 this->UpdateCaretPosition();
108 this->UpdateMarkedText();
110 return true;
114 * Delete every character in the textbuffer
116 void Textbuf::DeleteAll()
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();
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
132 bool Textbuf::InsertChar(WChar key)
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;
142 this->UpdateStringIter();
143 this->UpdateWidth();
144 this->UpdateCaretPosition();
145 this->UpdateMarkedText();
146 return true;
148 return false;
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.
162 bool Textbuf::InsertString(const char *str, bool marked, const char *caret, const char *insert_location, const char *replacement_end)
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;
169 if (replacement_end != nullptr) {
170 this->DeleteText(insertpos, replacement_end - this->buf, str == nullptr);
172 } else {
173 if (marked) this->DiscardMarkedText(str == nullptr);
176 if (str == nullptr) return false;
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;
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;
187 bytes += len;
188 chars++;
190 /* Move caret if needed. */
191 if (ptr == caret) this->caretpos = insertpos + bytes;
194 if (bytes == 0) return false;
196 if (marked) {
197 this->markpos = insertpos;
198 this->markend = insertpos + bytes;
201 memmove(this->buf + insertpos + bytes, this->buf + insertpos, this->bytes - insertpos);
202 memcpy(this->buf + insertpos, str, bytes);
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
211 this->UpdateStringIter();
212 this->UpdateWidth();
213 this->UpdateCaretPosition();
214 this->UpdateMarkedText();
216 return true;
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
225 bool Textbuf::InsertClipboard()
227 char utf8_buf[512];
229 if (!GetClipboardContents(utf8_buf, lastof(utf8_buf))) return false;
231 return this->InsertString(utf8_buf, false);
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.
240 void Textbuf::DeleteText(uint16 from, uint16 to, bool update)
242 uint c = 0;
243 const char *s = this->buf + from;
244 while (s < this->buf + to) {
245 Utf8Consume(&s);
246 c++;
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;
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;
263 if (update) {
264 this->UpdateStringIter();
265 this->UpdateCaretPosition();
266 this->UpdateMarkedText();
271 * Discard any marked text.
272 * @param update Set to true if the internal state should be updated.
274 void Textbuf::DiscardMarkedText(bool update)
276 if (this->markend == 0) return;
278 this->DeleteText(this->markpos, this->markend, update);
279 this->markpos = this->markend = this->markxoffs = this->marklength = 0;
282 /** Update the character iter after the text has changed. */
283 void Textbuf::UpdateStringIter()
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;
290 /** Update pixel width of the text. */
291 void Textbuf::UpdateWidth()
293 this->pixels = GetStringBoundingBox(this->buf, FS_NORMAL).width;
296 /** Update pixel position of the caret. */
297 void Textbuf::UpdateCaretPosition()
299 this->caretxoffs = this->chars > 1 ? GetCharPosInString(this->buf, this->buf + this->caretpos, FS_NORMAL).x : 0;
302 /** Update pixel positions of the marked text area. */
303 void Textbuf::UpdateMarkedText()
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;
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
319 bool Textbuf::MovePos(uint16 keycode)
321 switch (keycode) {
322 case WKC_LEFT:
323 case WKC_CTRL | WKC_LEFT: {
324 if (this->caretpos == 0) break;
326 size_t pos = this->char_iter->Prev(keycode & WKC_CTRL ? StringIterator::ITER_WORD : StringIterator::ITER_CHARACTER);
327 if (pos == StringIterator::END) return true;
329 this->caretpos = (uint16)pos;
330 this->UpdateCaretPosition();
331 return true;
334 case WKC_RIGHT:
335 case WKC_CTRL | WKC_RIGHT: {
336 if (this->caretpos >= this->bytes - 1) break;
338 size_t pos = this->char_iter->Next(keycode & WKC_CTRL ? StringIterator::ITER_WORD : StringIterator::ITER_CHARACTER);
339 if (pos == StringIterator::END) return true;
341 this->caretpos = (uint16)pos;
342 this->UpdateCaretPosition();
343 return true;
346 case WKC_HOME:
347 this->caretpos = 0;
348 this->char_iter->SetCurPosition(this->caretpos);
349 this->UpdateCaretPosition();
350 return true;
352 case WKC_END:
353 this->caretpos = this->bytes - 1;
354 this->char_iter->SetCurPosition(this->caretpos);
355 this->UpdateCaretPosition();
356 return true;
358 default:
359 break;
362 return false;
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'
371 Textbuf::Textbuf(uint16 max_bytes, uint16 max_chars)
372 : buf(MallocT<char>(max_bytes))
374 assert(max_bytes != 0);
375 assert(max_chars != 0);
377 this->char_iter = StringIterator::Create();
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();
386 Textbuf::~Textbuf()
388 delete this->char_iter;
389 free(this->buf);
393 * Render a string into the textbuffer.
394 * @param string String
396 void Textbuf::Assign(StringID string)
398 GetString(this->buf, string, &this->buf[this->max_bytes - 1]);
399 this->UpdateSize();
403 * Copy a string into the textbuffer.
404 * @param text Source.
406 void Textbuf::Assign(const char *text)
408 strecpy(this->buf, text, &this->buf[this->max_bytes - 1]);
409 this->UpdateSize();
413 * Print a formatted string into the textbuffer.
415 void Textbuf::Print(const char *format, ...)
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();
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
430 void Textbuf::UpdateSize()
432 const char *buf = this->buf;
434 this->chars = this->bytes = 1; // terminating zero
436 WChar c;
437 while ((c = Utf8Consume(&buf)) != '\0') {
438 this->bytes += Utf8CharLen(c);
439 this->chars++;
441 assert(this->bytes <= this->max_bytes);
442 assert(this->chars <= this->max_chars);
444 this->caretpos = this->bytes - 1;
445 this->UpdateStringIter();
446 this->UpdateWidth();
447 this->UpdateMarkedText();
449 this->UpdateCaretPosition();
453 * Handle the flashing of the caret.
454 * @return True if the caret state changes.
456 bool Textbuf::HandleCaret()
458 /* caret changed? */
459 bool b = !!(_caret_timer & 0x20);
461 if (b != this->caret) {
462 this->caret = b;
463 return true;
465 return false;
468 HandleKeyPressResult Textbuf::HandleKeyPress(WChar key, uint16 keycode)
470 bool edited = false;
472 switch (keycode) {
473 case WKC_ESC: return HKPR_CANCEL;
475 case WKC_RETURN: case WKC_NUM_ENTER: return HKPR_CONFIRM;
477 case (WKC_CTRL | 'V'):
478 case (WKC_SHIFT | WKC_INSERT):
479 edited = this->InsertClipboard();
480 break;
482 case (WKC_CTRL | 'U'):
483 this->DeleteAll();
484 edited = true;
485 break;
487 case WKC_BACKSPACE: case WKC_DELETE:
488 case WKC_CTRL | WKC_BACKSPACE: case WKC_CTRL | WKC_DELETE:
489 edited = this->DeleteChar(keycode);
490 break;
492 case WKC_LEFT: case WKC_RIGHT: case WKC_END: case WKC_HOME:
493 case WKC_CTRL | WKC_LEFT: case WKC_CTRL | WKC_RIGHT:
494 this->MovePos(keycode);
495 break;
497 default:
498 if (IsValidChar(key, this->afilter)) {
499 edited = this->InsertChar(key);
500 } else {
501 return HKPR_NOT_HANDLED;
503 break;
506 return edited ? HKPR_EDITING : HKPR_CURSOR;