4 * This file is part of OpenTTD.
5 * 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.
6 * 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.
7 * 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/>.
10 /** @file textbuf.cpp Textbuffer handling. */
15 #include "textbuf_type.h"
17 #include "strings_func.h"
20 #include "gfx_layout.h"
21 #include "window_func.h"
25 * Try to retrieve the current clipboard contents.
27 * @note OS-specific function.
28 * @return True if some text could be retrieved.
30 bool GetClipboardContents(char *buffer
, size_t buff_len
);
35 /** Sentinel to indicate end-of-iteration. */
36 static const size_t END
= SIZE_MAX
;
41 void Textbuf::SetString (const char *s
)
43 const char *string_base
= s
;
45 /* Unfortunately current ICU versions only provide rudimentary support
46 * for word break iterators (especially for CJK languages) in combination
47 * with UTF-8 input. As a work around we have to convert the input to
48 * UTF-16 and create a mapping back to UTF-8 character indices. */
49 this->utf16_str
.Clear();
50 this->utf16_to_utf8
.Clear();
53 size_t idx
= s
- string_base
;
55 WChar c
= Utf8Consume(&s
);
57 *this->utf16_str
.Append() = (UChar
)c
;
59 /* Make a surrogate pair. */
60 *this->utf16_str
.Append() = (UChar
)(0xD800 + ((c
- 0x10000) >> 10));
61 *this->utf16_str
.Append() = (UChar
)(0xDC00 + ((c
- 0x10000) & 0x3FF));
62 *this->utf16_to_utf8
.Append() = idx
;
64 *this->utf16_to_utf8
.Append() = idx
;
66 *this->utf16_str
.Append() = '\0';
67 *this->utf16_to_utf8
.Append() = s
- string_base
;
69 UText text
= UTEXT_INITIALIZER
;
70 UErrorCode status
= U_ZERO_ERROR
;
71 utext_openUChars(&text
, this->utf16_str
.Begin(), this->utf16_str
.Length() - 1, &status
);
72 this->char_itr
->setText(&text
, status
);
73 this->word_itr
->setText(&text
, status
);
74 this->char_itr
->first();
75 this->word_itr
->first();
78 size_t Textbuf::SetCurPosition (size_t pos
)
80 /* Convert incoming position to an UTF-16 string index. */
82 for (uint i
= 0; i
< this->utf16_to_utf8
.Length(); i
++) {
83 if (this->utf16_to_utf8
[i
] == pos
) {
89 /* isBoundary has the documented side-effect of setting the current
90 * position to the first valid boundary equal to or greater than
91 * the passed value. */
92 this->char_itr
->isBoundary(utf16_pos
);
93 return this->utf16_to_utf8
[this->char_itr
->current()];
96 size_t Textbuf::Next (bool word
)
101 pos
= this->word_itr
->following (this->char_itr
->current());
102 /* The ICU word iterator considers both the start and the end of a word a valid
103 * break point, but we only want word starts. Move to the next location in
104 * case the new position points to whitespace. */
105 while (pos
!= icu::BreakIterator::DONE
&&
106 IsWhitespace (Utf16DecodeChar ((const uint16
*)&this->utf16_str
[pos
]))) {
107 int32_t new_pos
= this->word_itr
->next();
108 /* Don't set it to DONE if it was valid before. Otherwise we'll return END
109 * even though the iterator wasn't at the end of the string before. */
110 if (new_pos
== icu::BreakIterator::DONE
) break;
114 this->char_itr
->isBoundary (pos
);
117 pos
= this->char_itr
->next();
120 return pos
== icu::BreakIterator::DONE
? END
: this->utf16_to_utf8
[pos
];
123 size_t Textbuf::Prev (bool word
)
128 pos
= this->word_itr
->preceding (this->char_itr
->current());
129 /* The ICU word iterator considers both the start and the end of a word a valid
130 * break point, but we only want word starts. Move to the previous location in
131 * case the new position points to whitespace. */
132 while (pos
!= icu::BreakIterator::DONE
&&
133 IsWhitespace (Utf16DecodeChar ((const uint16
*)&this->utf16_str
[pos
]))) {
134 int32_t new_pos
= this->word_itr
->previous();
135 /* Don't set it to DONE if it was valid before. Otherwise we'll return END
136 * even though the iterator wasn't at the start of the string before. */
137 if (new_pos
== icu::BreakIterator::DONE
) break;
141 this->char_itr
->isBoundary (pos
);
144 pos
= this->char_itr
->previous();
147 return pos
== icu::BreakIterator::DONE
? END
: this->utf16_to_utf8
[pos
];
152 inline void Textbuf::SetString (const char *s
)
157 size_t Textbuf::SetCurPosition (size_t pos
)
159 assert (pos
<= this->len
);
160 /* Sanitize in case we get a position inside an UTF-8 sequence. */
161 while (pos
> 0 && IsUtf8Part(this->buffer
[pos
])) pos
--;
162 return this->cur_pos
= pos
;
165 size_t Textbuf::Next (bool word
)
167 /* Already at the end? */
168 if (this->cur_pos
>= this->len
) return END
;
172 /* Consume current word. */
173 size_t offs
= Utf8Decode (&c
, this->buffer
+ this->cur_pos
);
174 while (this->cur_pos
< this->len
&& !IsWhitespace(c
)) {
175 this->cur_pos
+= offs
;
176 offs
= Utf8Decode (&c
, this->buffer
+ this->cur_pos
);
178 /* Consume whitespace to the next word. */
179 while (this->cur_pos
< this->len
&& IsWhitespace(c
)) {
180 this->cur_pos
+= offs
;
181 offs
= Utf8Decode (&c
, this->buffer
+ this->cur_pos
);
184 return this->cur_pos
;
188 this->cur_pos
+= Utf8Decode (&c
, this->buffer
+ this->cur_pos
);
189 return this->cur_pos
;
193 size_t Textbuf::Prev (bool word
)
195 /* Already at the beginning? */
196 if (this->cur_pos
== 0) return END
;
199 const char *s
= this->buffer
+ this->cur_pos
;
201 /* Consume preceding whitespace. */
203 s
= Utf8PrevChar (s
);
205 } while (s
> this->buffer
&& IsWhitespace(c
));
206 /* Consume preceding word. */
207 while (s
> this->buffer
&& !IsWhitespace(c
)) {
208 s
= Utf8PrevChar (s
);
211 /* Move caret back to the beginning of the word. */
212 if (IsWhitespace(c
)) Utf8Consume (&s
);
214 return this->cur_pos
= s
- this->buffer
;
217 return this->cur_pos
= Utf8PrevChar (this->buffer
+ this->cur_pos
) - this->buffer
;
225 * Get the positions of two characters relative to the start of the string.
226 * @param c1 Pointer to the first character in the string.
227 * @param[out] x1 Left position of the glyph associated with c1.
228 * @param c2 Pointer to the second character in the string.
229 * @param[out] x2 Left position of the glyph associated with c2.
231 void Textbuf::GetCharPositions (const char *c1
, int *x1
, const char *c2
, int *x2
) const
233 Layouter
layout (this->GetText());
234 *x1
= layout
.front()->GetCharPosition (this->GetText(), c1
);
235 *x2
= (c2
== c1
) ? *x1
: layout
.front()->GetCharPosition (this->GetText(), c2
);
239 * Get the character that is drawn at a specific position.
240 * @param x Position relative to the start of the string.
241 * @return Pointer to the character at the position or NULL if no character is at the position.
243 const char *Textbuf::GetCharAtPosition (int x
) const
245 if (x
< 0) return NULL
;
247 Layouter
layout (this->GetText());
248 return layout
.front()->GetCharAtPosition (this->GetText(), x
);
252 * Delete a character from a textbuffer, either with 'Delete' or 'Backspace'
253 * The character is delete from the position the caret is at
254 * @param backspace Delete backwards
255 * @param word Delete a whole word
256 * @return Return true on successful change of Textbuf, or false otherwise
258 bool Textbuf::DeleteChar (bool backspace
, bool word
)
260 if (this->caretpos
== (backspace
? 0 : this->length())) return false;
262 char *s
= this->buffer
+ this->caretpos
;
266 /* Delete a complete word. */
268 /* Delete whitespace and word in front of the caret. */
269 len
= this->caretpos
- (uint16
)this->Prev(true);
272 /* Delete word and following whitespace following the caret. */
273 len
= (uint16
)this->Next(true) - this->caretpos
;
275 /* Update character count. */
276 for (const char *ss
= s
; ss
< s
+ len
; Utf8Consume(&ss
)) {
280 /* Delete a single character. */
282 /* Delete the last code point in front of the caret. */
285 len
= (uint16
)Utf8Decode(&c
, s
);
288 /* Delete the complete character following the caret. */
289 len
= (uint16
)this->Next(false) - this->caretpos
;
290 /* Update character count. */
291 for (const char *ss
= s
; ss
< s
+ len
; Utf8Consume(&ss
)) {
297 /* Move the remaining characters over the marker */
298 memmove(s
, s
+ len
, this->len
+ 1 - (s
- this->buffer
) - len
);
301 if (backspace
) this->caretpos
-= len
;
303 this->UpdateStringIter();
305 this->UpdateCaretPosition();
306 this->UpdateMarkedText();
312 * Delete every character in the textbuffer
314 void Textbuf::DeleteAll()
318 this->pixels
= this->caretpos
= this->caretxoffs
= 0;
319 this->markpos
= this->markend
= this->markxoffs
= this->marklength
= 0;
320 this->UpdateStringIter();
324 * Insert a character to a textbuffer. If maxwidth of the Textbuf is zero,
325 * we don't care about the visual-length but only about the physical
326 * length of the string
327 * @param key Character to be inserted
328 * @return Return true on successful change of Textbuf, or false otherwise
330 bool Textbuf::InsertChar(WChar key
)
332 uint16 len
= (uint16
)Utf8CharLen(key
);
333 if (this->len
+ len
< this->capacity
&& this->chars
+ 1 <= this->max_chars
) {
334 memmove(this->buffer
+ this->caretpos
+ len
, this->buffer
+ this->caretpos
, this->len
+ 1 - this->caretpos
);
335 Utf8Encode(this->buffer
+ this->caretpos
, key
);
338 this->caretpos
+= len
;
340 this->UpdateStringIter();
342 this->UpdateCaretPosition();
343 this->UpdateMarkedText();
350 * Insert a string into the text buffer. If maxwidth of the Textbuf is zero,
351 * we don't care about the visual-length but only about the physical
352 * length of the string.
353 * @param str String to insert.
354 * @param marked Replace the currently marked text with the new text.
355 * @param caret Move the caret to this point in the insertion string.
356 * @param insert_location Position at which to insert the string.
357 * @param replacement_end Replace all characters from #insert_location up to this location with the new string.
358 * @return True on successful change of Textbuf, or false otherwise.
360 bool Textbuf::InsertString(const char *str
, bool marked
, const char *caret
, const char *insert_location
, const char *replacement_end
)
362 uint16 insertpos
= (marked
&& this->marklength
!= 0) ? this->markpos
: this->caretpos
;
363 if (insert_location
!= NULL
) {
364 insertpos
= insert_location
- this->buffer
;
365 if (insertpos
> this->len
+ 1) return false;
367 if (replacement_end
!= NULL
) {
368 this->DeleteText(insertpos
, replacement_end
- this->buffer
, str
== NULL
);
371 if (marked
&& (this->markend
!= 0)) {
372 this->DeleteText (this->markpos
, this->markend
, str
== NULL
);
373 this->markpos
= this->markend
= this->markxoffs
= this->marklength
= 0;
377 if (str
== NULL
) return false;
379 uint16 bytes
= 0, chars
= 0;
381 for (const char *ptr
= str
; (c
= Utf8Consume(&ptr
)) != '\0';) {
382 if (!IsValidChar(c
, this->afilter
)) break;
384 byte len
= Utf8CharLen(c
);
385 if (this->len
+ bytes
+ len
>= this->capacity
) break;
386 if (this->chars
+ chars
+ 1 > this->max_chars
) break;
391 /* Move caret if needed. */
392 if (ptr
== caret
) this->caretpos
= insertpos
+ bytes
;
395 if (bytes
== 0) return false;
398 this->markpos
= insertpos
;
399 this->markend
= insertpos
+ bytes
;
402 memmove(this->buffer
+ insertpos
+ bytes
, this->buffer
+ insertpos
, this->len
+ 1 - insertpos
);
403 memcpy(this->buffer
+ insertpos
, str
, bytes
);
406 this->chars
+= chars
;
407 if (!marked
&& caret
== NULL
) this->caretpos
+= bytes
;
408 assert(this->len
< this->capacity
);
409 assert(this->chars
<= this->max_chars
);
410 this->buffer
[this->len
] = '\0'; // terminating zero
412 this->UpdateStringIter();
414 this->UpdateCaretPosition();
415 this->UpdateMarkedText();
421 * Insert a chunk of text from the clipboard onto the textbuffer. Get TEXT clipboard
422 * and append this up to the maximum length (either absolute or screenlength). If maxlength
423 * is zero, we don't care about the screenlength but only about the physical length of the string
424 * @return true on successful change of Textbuf, or false otherwise
426 bool Textbuf::InsertClipboard()
430 if (!GetClipboardContents(utf8_buf
, lengthof(utf8_buf
))) return false;
432 return this->InsertString(utf8_buf
, false);
436 * Delete a part of the text.
437 * @param from Start of the text to delete.
438 * @param to End of the text to delete.
439 * @param update Set to true if the internal state should be updated.
441 void Textbuf::DeleteText(uint16 from
, uint16 to
, bool update
)
444 const char *s
= this->buffer
+ from
;
445 while (s
< this->buffer
+ to
) {
450 /* Strip marked characters from buffer. */
451 memmove(this->buffer
+ from
, this->buffer
+ to
, this->len
+ 1 - to
);
452 this->len
-= to
- from
;
455 /* Fixup caret if needed. */
456 if (this->caretpos
> from
) {
457 if (this->caretpos
<= to
) {
458 this->caretpos
= from
;
460 this->caretpos
-= to
- from
;
465 this->UpdateStringIter();
466 this->UpdateCaretPosition();
467 this->UpdateMarkedText();
471 /** Update the character iter after the text has changed. */
472 void Textbuf::UpdateStringIter()
474 this->SetString (this->buffer
);
475 size_t pos
= this->SetCurPosition (this->caretpos
);
476 this->caretpos
= pos
== END
? 0 : (uint16
)pos
;
479 /** Update pixel width of the text. */
480 void Textbuf::UpdateWidth()
482 this->pixels
= GetStringBoundingBox(this->buffer
, FS_NORMAL
).width
;
485 /** Update pixel position of the caret. */
486 void Textbuf::UpdateCaretPosition()
488 if (this->chars
> 1) {
489 Layouter
layout (this->GetText());
490 this->caretxoffs
= layout
.front()->GetCharPosition (this->GetText(), this->buffer
+ this->caretpos
);
492 this->caretxoffs
= 0;
496 /** Update pixel positions of the marked text area. */
497 void Textbuf::UpdateMarkedText()
499 if (this->markend
!= 0) {
501 this->GetCharPositions (this->buffer
+ this->markpos
, &x1
,
502 this->buffer
+ this->markend
, &x2
);
503 this->markxoffs
= x1
;
504 this->marklength
= x2
- x1
;
506 this->markxoffs
= this->marklength
= 0;
511 * Handle text navigation to the left.
512 * @param word Whether to move a whole word left.
513 * @return Return true on successful change of Textbuf, or false otherwise
515 bool Textbuf::MoveLeft (bool word
)
517 if (this->caretpos
== 0) return false;
519 size_t pos
= this->Prev (word
);
520 if (pos
== END
) return true;
522 this->caretpos
= (uint16
)pos
;
523 this->UpdateCaretPosition();
528 * Handle text navigation to the right.
529 * @param word Whether to move a whole word right.
530 * @return Return true on successful change of Textbuf, or false otherwise
532 bool Textbuf::MoveRight (bool word
)
534 if (this->caretpos
>= this->length()) return false;
536 size_t pos
= this->Next (word
);
537 if (pos
== END
) return true;
539 this->caretpos
= (uint16
)pos
;
540 this->UpdateCaretPosition();
545 * Handle text navigation to the end of the text.
546 * @return Return true on successful change of Textbuf, or false otherwise
548 bool Textbuf::MoveEnd (void)
550 this->caretpos
= this->length();
551 this->SetCurPosition (this->caretpos
);
552 this->UpdateCaretPosition();
557 * Initialize the textbuffer by supplying it the buffer to write into
558 * and the maximum length of this buffer
559 * @param buf the buffer that will be holding the data for input
560 * @param max_bytes maximum size in bytes, including terminating '\0'
561 * @param max_chars maximum size in chars, including terminating '\0'
563 Textbuf::Textbuf (uint16 max_bytes
, char *buf
, uint16 max_chars
)
564 : stringb (max_bytes
, buf
),
565 max_chars(max_chars
== UINT16_MAX
? max_bytes
: max_chars
)
567 assert(max_bytes
!= 0);
568 assert(max_chars
!= 0);
569 assert (buffer
[0] == '\0'); // should have been set by stringb constructor
572 const char *isocode
= _current_language
!= NULL
? _current_language
->isocode
: "en";
573 UErrorCode status
= U_ZERO_ERROR
;
574 this->char_itr
.reset (icu::BreakIterator::createCharacterInstance (icu::Locale(isocode
), status
));
575 this->word_itr
.reset (icu::BreakIterator::createWordInstance (icu::Locale(isocode
), status
));
577 *this->utf16_str
.Append() = '\0';
578 *this->utf16_to_utf8
.Append() = 0;
584 this->afilter
= CS_ALPHANUMERAL
;
590 * Render a string into the textbuffer.
591 * @param string String
593 void Textbuf::Assign(StringID string
)
595 GetString (this, string
);
600 * Copy a string into the textbuffer.
601 * @param text Source.
603 void Textbuf::Assign(const char *text
)
610 * Print a formatted string into the textbuffer.
612 void Textbuf::Print(const char *format
, ...)
615 va_start(va
, format
);
616 this->vfmt (format
, va
);
623 * Update Textbuf type with its actual physical character and screenlength
624 * Get the count of characters in the string as well as the width in pixels.
625 * Useful when copying in a larger amount of text at once
627 void Textbuf::UpdateSize()
629 const char *buf
= this->buffer
;
630 assert (strlen(buf
) == this->len
);
631 assert (this->len
< this->capacity
);
632 assert (this->max_chars
> 1);
634 this->chars
= 1; // terminating zero
636 if (Utf8Consume(&buf
) == '\0') {
637 assert (buf
== this->buffer
+ this->len
+ 1);
641 if (this->chars
== this->max_chars
) {
642 this->truncate (buf
- this->buffer
);
647 this->caretpos
= this->len
;
648 this->UpdateStringIter();
650 this->UpdateMarkedText();
652 this->UpdateCaretPosition();
656 * Handle the flashing of the caret.
657 * @return True if the caret state changes.
659 bool Textbuf::HandleCaret()
662 bool b
= !!(_caret_timer
& 0x20);
664 if (b
!= this->caret
) {
671 HandleKeyPressResult
Textbuf::HandleKeyPress(WChar key
, uint16 keycode
)
676 case WKC_ESC
: return HKPR_CANCEL
;
678 case WKC_RETURN
: case WKC_NUM_ENTER
: return HKPR_CONFIRM
;
680 case (WKC_CTRL
| 'V'):
681 edited
= this->InsertClipboard();
684 case (WKC_CTRL
| 'U'):
689 case WKC_BACKSPACE
: case WKC_DELETE
:
690 case WKC_CTRL
| WKC_BACKSPACE
: case WKC_CTRL
| WKC_DELETE
: {
691 bool word
= (keycode
& WKC_CTRL
) != 0;
692 bool backspace
= (keycode
& ~WKC_SPECIAL_KEYS
) == WKC_BACKSPACE
;
693 edited
= this->DeleteChar (backspace
, word
);
698 case WKC_CTRL
| WKC_LEFT
:
699 this->MoveLeft (keycode
& WKC_CTRL
);
703 case WKC_CTRL
| WKC_RIGHT
:
704 this->MoveRight (keycode
& WKC_CTRL
);
709 this->SetCurPosition (this->caretpos
);
710 this->UpdateCaretPosition();
718 if (IsValidChar(key
, this->afilter
)) {
719 edited
= this->InsertChar(key
);
721 return HKPR_NOT_HANDLED
;
726 return edited
? HKPR_EDITING
: HKPR_CURSOR
;