From 6b47e4ba241652a5891b85b1650f3c145b4df14a Mon Sep 17 00:00:00 2001 From: rubidium Date: Wed, 13 Nov 2013 21:28:21 +0000 Subject: [PATCH] (svn r25979) [1.3] -Backport from trunk: - Fix: [OSX] The new 10.7 fullscreen code can now also be compiled with older SDK versions [FS#4744] (r25656) - Fix: [OSX] Mouse cursor was not displayed properly after switching to fullscreen on 10.7+ (r25655) - Fix: Improve character and word deletion for CJK languages and complex scripts (r25654, r25653) - Fix: [OSX] Define version constants before they are used (r25643) --- projects/openttd_vs100.vcxproj | 1 + projects/openttd_vs100.vcxproj.filters | 3 + projects/openttd_vs80.vcproj | 4 + projects/openttd_vs90.vcproj | 4 + source.list | 1 + src/console_gui.cpp | 35 +-- src/gfx.cpp | 14 ++ src/gfx_func.h | 1 + src/gfx_layout.cpp | 55 ++++- src/gfx_layout.h | 3 + src/misc_gui.cpp | 50 ---- src/os/macosx/macos.h | 26 -- src/os/macosx/osx_stdafx.h | 31 +++ src/querystring_gui.h | 13 - src/string.cpp | 256 ++++++++++++++++++++ src/string_base.h | 66 +++++ src/string_func.h | 53 +++- src/textbuf.cpp | 323 +++++++++++-------------- src/textbuf_type.h | 36 ++- src/video/cocoa/cocoa_v.h | 1 + src/video/cocoa/cocoa_v.mm | 8 + src/video/cocoa/wnd_quartz.mm | 13 +- src/window.cpp | 19 +- 23 files changed, 682 insertions(+), 334 deletions(-) create mode 100644 src/string_base.h diff --git a/projects/openttd_vs100.vcxproj b/projects/openttd_vs100.vcxproj index 9f5a991f0c..dcef8274fc 100644 --- a/projects/openttd_vs100.vcxproj +++ b/projects/openttd_vs100.vcxproj @@ -573,6 +573,7 @@ + diff --git a/projects/openttd_vs100.vcxproj.filters b/projects/openttd_vs100.vcxproj.filters index 0ecfeeb3a3..87463f0af2 100644 --- a/projects/openttd_vs100.vcxproj.filters +++ b/projects/openttd_vs100.vcxproj.filters @@ -948,6 +948,9 @@ Header Files + + Header Files + Header Files diff --git a/projects/openttd_vs80.vcproj b/projects/openttd_vs80.vcproj index 1eec157596..0271ee5e22 100644 --- a/projects/openttd_vs80.vcproj +++ b/projects/openttd_vs80.vcproj @@ -1566,6 +1566,10 @@ RelativePath=".\..\src\strgen\strgen.h" > + + diff --git a/projects/openttd_vs90.vcproj b/projects/openttd_vs90.vcproj index 22a7302cf2..50f9d26643 100644 --- a/projects/openttd_vs90.vcproj +++ b/projects/openttd_vs90.vcproj @@ -1563,6 +1563,10 @@ RelativePath=".\..\src\strgen\strgen.h" > + + diff --git a/source.list b/source.list index da9a409f58..d2c9dc8a1f 100644 --- a/source.list +++ b/source.list @@ -306,6 +306,7 @@ station_type.h statusbar_gui.h stdafx.h strgen/strgen.h +string_base.h string_func.h string_type.h stringfilter_type.h diff --git a/src/console_gui.cpp b/src/console_gui.cpp index e97e458ff7..ca0a01853d 100644 --- a/src/console_gui.cpp +++ b/src/console_gui.cpp @@ -290,46 +290,13 @@ struct IConsoleWindow : Window MarkWholeScreenDirty(); break; -#ifdef WITH_COCOA - case (WKC_META | 'V'): -#endif - case (WKC_CTRL | 'V'): - if (_iconsole_cmdline.InsertClipboard()) { - IConsoleResetHistoryPos(); - this->SetDirty(); - } - break; - case (WKC_CTRL | 'L'): IConsoleCmdExec("clear"); break; -#ifdef WITH_COCOA - case (WKC_META | 'U'): -#endif - case (WKC_CTRL | 'U'): - _iconsole_cmdline.DeleteAll(); - this->SetDirty(); - break; - - case WKC_BACKSPACE: case WKC_DELETE: - if (_iconsole_cmdline.DeleteChar(keycode)) { - IConsoleResetHistoryPos(); - this->SetDirty(); - } - break; - - case WKC_LEFT: case WKC_RIGHT: case WKC_END: case WKC_HOME: - if (_iconsole_cmdline.MovePos(keycode)) { - IConsoleResetHistoryPos(); - this->SetDirty(); - } - break; - default: - if (IsValidChar(key, CS_ALPHANUMERAL)) { + if (_iconsole_cmdline.HandleKeyPress(key, keycode) != HKPR_NOT_HANDLED) { IConsoleWindow::scroll = 0; - _iconsole_cmdline.InsertChar(key); IConsoleResetHistoryPos(); this->SetDirty(); } else { diff --git a/src/gfx.cpp b/src/gfx.cpp index e0a7c060fe..b1851f0ebe 100644 --- a/src/gfx.cpp +++ b/src/gfx.cpp @@ -639,6 +639,20 @@ Dimension GetStringBoundingBox(StringID strid) return GetStringBoundingBox(buffer); } +/** + * Get the leading corner of a character in a single-line string relative + * to the start of the string. + * @param str String containing the character. + * @param ch Pointer to the character in the string. + * @param start_fontsize Font size to start the text with. + * @return Upper left corner of the glyph associated with the character. + */ +Point GetCharPosInString(const char *str, const char *ch, FontSize start_fontsize) +{ + Layouter layout(str, INT32_MAX, TC_FROMSTRING, start_fontsize); + return layout.GetCharPosition(ch); +} + /** * Draw single character horizontally centered around (x,y) * @param c Character (glyph) to draw diff --git a/src/gfx_func.h b/src/gfx_func.h index 6ec36ee1b1..1226816b4f 100644 --- a/src/gfx_func.h +++ b/src/gfx_func.h @@ -126,6 +126,7 @@ int GetStringLineCount(StringID str, int maxw); Dimension GetStringMultiLineBoundingBox(StringID str, const Dimension &suggestion); Dimension GetStringMultiLineBoundingBox(const char *str, const Dimension &suggestion); void LoadStringWidthTable(bool monospace = false); +Point GetCharPosInString(const char *str, const char *ch, FontSize start_fontsize = FS_NORMAL); void DrawDirtyBlocks(); void SetDirtyBlocks(int left, int top, int right, int bottom); diff --git a/src/gfx_layout.cpp b/src/gfx_layout.cpp index b477093743..165edee3b6 100644 --- a/src/gfx_layout.cpp +++ b/src/gfx_layout.cpp @@ -423,7 +423,7 @@ ParagraphLayout *Layouter::GetParagraphLayout(WChar *buff, WChar *buff_end, Font * @param colour The colour of the font. * @param fontsize The size of font to use. */ -Layouter::Layouter(const char *str, int maxw, TextColour colour, FontSize fontsize) +Layouter::Layouter(const char *str, int maxw, TextColour colour, FontSize fontsize) : string(str) { FontState state(colour, fontsize); WChar c = 0; @@ -512,6 +512,59 @@ Dimension Layouter::GetBounds() return d; } +/** + * Get the position of a character in the layout. + * @param ch Character to get the position of. + * @return Upper left corner of the character relative to the start of the string. + * @note Will only work right for single-line strings. + */ +Point Layouter::GetCharPosition(const char *ch) const +{ + /* Find the code point index which corresponds to the char + * pointer into our UTF-8 source string. */ + size_t index = 0; + const char *str = this->string; + while (str < ch) { + WChar c; + size_t len = Utf8Decode(&c, str); + if (c == '\0' || c == '\n') break; + str += len; +#ifdef WITH_ICU + /* ICU uses UTF-16 internally which means we need to account for surrogate pairs. */ + index += len < 4 ? 1 : 2; +#else + index++; +#endif + } + + if (str == ch) { + /* Valid character. */ + const ParagraphLayout::Line *line = *this->Begin(); + + /* Pointer to the end-of-string/line marker? Return total line width. */ + if (*ch == '\0' || *ch == '\n') { + Point p = { line->getWidth(), 0 }; + return p; + } + + /* Scan all runs until we've found our code point index. */ + for (int run_index = 0; run_index < line->countRuns(); run_index++) { + const ParagraphLayout::VisualRun *run = line->getVisualRun(run_index); + + for (int i = 0; i < run->getGlyphCount(); i++) { + /* Matching glyph? Return position. */ + if ((size_t)run->getGlyphToCharMap()[i] == index) { + Point p = { run->getPositions()[i * 2], run->getPositions()[i * 2 + 1] }; + return p; + } + } + } + } + + Point p = { 0, 0 }; + return p; +} + /** * Get a static font instance. */ diff --git a/src/gfx_layout.h b/src/gfx_layout.h index c252d15ebd..27c1e9c082 100644 --- a/src/gfx_layout.h +++ b/src/gfx_layout.h @@ -168,6 +168,8 @@ class Layouter : public AutoDeleteSmallVector { typedef WChar CharType; ///< The type of character used within the layouter. #endif /* WITH_ICU */ + const char *string; ///< Pointer to the original string. + size_t AppendToBuffer(CharType *buff, const CharType *buffer_last, WChar c); ParagraphLayout *GetParagraphLayout(CharType *buff, CharType *buff_end, FontMap &fontMapping); @@ -209,6 +211,7 @@ class Layouter : public AutoDeleteSmallVector { public: Layouter(const char *str, int maxw = INT32_MAX, TextColour colour = TC_FROMSTRING, FontSize fontsize = FS_NORMAL); Dimension GetBounds(); + Point GetCharPosition(const char *ch) const; static void ResetFontCache(FontSize size); static void ResetLineCache(); diff --git a/src/misc_gui.cpp b/src/misc_gui.cpp index 7b763e7839..9cea9f8762 100644 --- a/src/misc_gui.cpp +++ b/src/misc_gui.cpp @@ -715,56 +715,6 @@ void GuiShowTooltips(Window *parent, StringID str, uint paramcount, const uint64 new TooltipsWindow(parent, str, paramcount, params, close_tooltip); } -HandleEditBoxResult QueryString::HandleEditBoxKey(Window *w, int wid, uint16 key, uint16 keycode, EventState &state) -{ - if (!w->IsWidgetGloballyFocused(wid)) return HEBR_NOT_FOCUSED; - - state = ES_HANDLED; - - bool edited = false; - - switch (keycode) { - case WKC_ESC: return HEBR_CANCEL; - - case WKC_RETURN: case WKC_NUM_ENTER: return HEBR_CONFIRM; - -#ifdef WITH_COCOA - case (WKC_META | 'V'): -#endif - case (WKC_CTRL | 'V'): - edited = this->text.InsertClipboard(); - break; - -#ifdef WITH_COCOA - case (WKC_META | 'U'): -#endif - case (WKC_CTRL | 'U'): - this->text.DeleteAll(); - edited = true; - break; - - case WKC_BACKSPACE: case WKC_DELETE: - case WKC_CTRL | WKC_BACKSPACE: case WKC_CTRL | WKC_DELETE: - edited = this->text.DeleteChar(keycode); - break; - - case WKC_LEFT: case WKC_RIGHT: case WKC_END: case WKC_HOME: - case WKC_CTRL | WKC_LEFT: case WKC_CTRL | WKC_RIGHT: - this->text.MovePos(keycode); - break; - - default: - if (IsValidChar(key, this->text.afilter)) { - edited = this->text.InsertChar(key); - } else { - state = ES_NOT_HANDLED; - } - break; - } - - return edited ? HEBR_EDITING : HEBR_CURSOR; -} - void QueryString::HandleEditBox(Window *w, int wid) { if (w->IsWidgetGloballyFocused(wid) && this->text.HandleCaret()) { diff --git a/src/os/macosx/macos.h b/src/os/macosx/macos.h index d147dfb0b0..16f34a3dc7 100644 --- a/src/os/macosx/macos.h +++ b/src/os/macosx/macos.h @@ -12,32 +12,6 @@ #ifndef MACOS_H #define MACOS_H -/* It would seem that to ensure backward compability we have to ensure that we have defined MAC_OS_X_VERSION_10_x everywhere */ -#ifndef MAC_OS_X_VERSION_10_3 -#define MAC_OS_X_VERSION_10_3 1030 -#endif - -#ifndef MAC_OS_X_VERSION_10_4 -#define MAC_OS_X_VERSION_10_4 1040 -#endif - -#ifndef MAC_OS_X_VERSION_10_5 -#define MAC_OS_X_VERSION_10_5 1050 -#endif - -#ifndef MAC_OS_X_VERSION_10_6 -#define MAC_OS_X_VERSION_10_6 1060 -#endif - -#ifndef MAC_OS_X_VERSION_10_7 -#define MAC_OS_X_VERSION_10_7 1070 -#endif - -#ifndef MAC_OS_X_VERSION_10_8 -#define MAC_OS_X_VERSION_10_8 1080 -#endif - - /** Helper function displaying a message the best possible way. */ void ShowMacDialog(const char *title, const char *message, const char *button_label); diff --git a/src/os/macosx/osx_stdafx.h b/src/os/macosx/osx_stdafx.h index ad9c5220c1..5adbde1792 100644 --- a/src/os/macosx/osx_stdafx.h +++ b/src/os/macosx/osx_stdafx.h @@ -12,6 +12,37 @@ #ifndef MACOS_STDAFX_H #define MACOS_STDAFX_H + +/* It would seem that to ensure backward compability we have to ensure that we have defined MAC_OS_X_VERSION_10_x everywhere */ +#ifndef MAC_OS_X_VERSION_10_3 +#define MAC_OS_X_VERSION_10_3 1030 +#endif + +#ifndef MAC_OS_X_VERSION_10_4 +#define MAC_OS_X_VERSION_10_4 1040 +#endif + +#ifndef MAC_OS_X_VERSION_10_5 +#define MAC_OS_X_VERSION_10_5 1050 +#endif + +#ifndef MAC_OS_X_VERSION_10_6 +#define MAC_OS_X_VERSION_10_6 1060 +#endif + +#ifndef MAC_OS_X_VERSION_10_7 +#define MAC_OS_X_VERSION_10_7 1070 +#endif + +#ifndef MAC_OS_X_VERSION_10_8 +#define MAC_OS_X_VERSION_10_8 1080 +#endif + +#ifndef MAC_OS_X_VERSION_10_9 +#define MAC_OS_X_VERSION_10_9 1090 +#endif + + #define __STDC_LIMIT_MACROS #include diff --git a/src/querystring_gui.h b/src/querystring_gui.h index 6ec93ffd9a..b2c91f3540 100644 --- a/src/querystring_gui.h +++ b/src/querystring_gui.h @@ -16,18 +16,6 @@ #include "textbuf_gui.h" #include "window_gui.h" -/** - * Return values for HandleEditBoxKey - */ -enum HandleEditBoxResult -{ - HEBR_EDITING, ///< Editbox content changed. - HEBR_CURSOR, ///< Non-text change, e.g. cursor position. - HEBR_CONFIRM, ///< Return or enter key pressed. - HEBR_CANCEL, ///< Escape key pressed. - HEBR_NOT_FOCUSED, ///< Edit box widget not focused. -}; - /** * Data stored about a string that can be modified in the GUI */ @@ -65,7 +53,6 @@ public: void DrawEditBox(const Window *w, int wid) const; void ClickEditBox(Window *w, Point pt, int wid, int click_count, bool focus_changed); void HandleEditBox(Window *w, int wid); - HandleEditBoxResult HandleEditBoxKey(Window *w, int wid, uint16 key, uint16 keycode, EventState &state); }; void ShowOnScreenKeyboard(Window *parent, int button); diff --git a/src/string.cpp b/src/string.cpp index 39fc8479c9..ada9f9022a 100644 --- a/src/string.cpp +++ b/src/string.cpp @@ -14,6 +14,7 @@ #include "core/alloc_func.hpp" #include "core/math_func.hpp" #include "string_func.h" +#include "string_base.h" #include "table/control_codes.h" @@ -650,3 +651,258 @@ int strnatcmp(const char *s1, const char *s2, bool ignore_garbage_at_front) /* Do a normal comparison if ICU is missing or if we cannot create a collator. */ return strcasecmp(s1, s2); } + +#ifdef WITH_ICU + +#include +#include + +/** String iterator using ICU as a backend. */ +class IcuStringIterator : public StringIterator +{ + icu::BreakIterator *char_itr; ///< ICU iterator for characters. + icu::BreakIterator *word_itr; ///< ICU iterator for words. + const char *string; ///< Iteration string in UTF-8. + + SmallVector utf16_str; ///< UTF-16 copy of the string. + SmallVector utf16_to_utf8; ///< Mapping from UTF-16 code point position to index in the UTF-8 source string. + +public: + IcuStringIterator() : char_itr(NULL), word_itr(NULL) + { + UErrorCode status = U_ZERO_ERROR; + this->char_itr = icu::BreakIterator::createCharacterInstance(icu::Locale(_current_language != NULL ? _current_language->isocode : "en"), status); + this->word_itr = icu::BreakIterator::createWordInstance(icu::Locale(_current_language != NULL ? _current_language->isocode : "en"), status); + + *this->utf16_str.Append() = '\0'; + *this->utf16_to_utf8.Append() = 0; + } + + virtual ~IcuStringIterator() + { + delete this->char_itr; + delete this->word_itr; + } + + virtual void SetString(const char *s) + { + this->string = s; + + /* Unfortunately current ICU versions only provide rudimentary support + * for word break iterators (especially for CJK languages) in combination + * with UTF-8 input. As a work around we have to convert the input to + * UTF-16 and create a mapping back to UTF-8 character indices. */ + this->utf16_str.Clear(); + this->utf16_to_utf8.Clear(); + + while (*s != '\0') { + size_t idx = s - this->string; + + WChar c = Utf8Consume(&s); + if (c < 0x10000) { + *this->utf16_str.Append() = (UChar)c; + } else { + /* Make a surrogate pair. */ + *this->utf16_str.Append() = (UChar)(0xD800 + ((c - 0x10000) >> 10)); + *this->utf16_str.Append() = (UChar)(0xDC00 + ((c - 0x10000) & 0x3FF)); + *this->utf16_to_utf8.Append() = idx; + } + *this->utf16_to_utf8.Append() = idx; + } + *this->utf16_str.Append() = '\0'; + *this->utf16_to_utf8.Append() = s - this->string; + + UText text = UTEXT_INITIALIZER; + UErrorCode status = U_ZERO_ERROR; + utext_openUChars(&text, this->utf16_str.Begin(), this->utf16_str.Length() - 1, &status); + this->char_itr->setText(&text, status); + this->word_itr->setText(&text, status); + this->char_itr->first(); + this->word_itr->first(); + } + + virtual size_t SetCurPosition(size_t pos) + { + /* Convert incoming position to an UTF-16 string index. */ + uint utf16_pos = 0; + for (uint i = 0; i < this->utf16_to_utf8.Length(); i++) { + if (this->utf16_to_utf8[i] == pos) { + utf16_pos = i; + break; + } + } + + /* isBoundary has the documented side-effect of setting the current + * position to the first valid boundary equal to or greater than + * the passed value. */ + this->char_itr->isBoundary(utf16_pos); + return this->utf16_to_utf8[this->char_itr->current()]; + } + + virtual size_t Next(IterType what) + { + int32_t pos; + switch (what) { + case ITER_CHARACTER: + pos = this->char_itr->next(); + break; + + case ITER_WORD: + pos = this->word_itr->following(this->char_itr->current()); + /* The ICU word iterator considers both the start and the end of a word a valid + * break point, but we only want word starts. Move to the next location in + * case the new position points to whitespace. */ + while (pos != icu::BreakIterator::DONE && IsWhitespace(Utf16DecodeChar((const uint16 *)&this->utf16_str[pos]))) pos = this->word_itr->next(); + + this->char_itr->isBoundary(pos); + break; + + default: + NOT_REACHED(); + } + + return pos == icu::BreakIterator::DONE ? END : this->utf16_to_utf8[pos]; + } + + virtual size_t Prev(IterType what) + { + int32_t pos; + switch (what) { + case ITER_CHARACTER: + pos = this->char_itr->previous(); + break; + + case ITER_WORD: + pos = this->word_itr->preceding(this->char_itr->current()); + /* The ICU word iterator considers both the start and the end of a word a valid + * break point, but we only want word starts. Move to the previous location in + * case the new position points to whitespace. */ + while (pos != icu::BreakIterator::DONE && IsWhitespace(Utf16DecodeChar((const uint16 *)&this->utf16_str[pos]))) pos = this->word_itr->previous(); + + this->char_itr->isBoundary(pos); + break; + + default: + NOT_REACHED(); + } + + return pos == icu::BreakIterator::DONE ? END : this->utf16_to_utf8[pos]; + } +}; + +/* static */ StringIterator *StringIterator::Create() +{ + return new IcuStringIterator(); +} + +#else + +/** Fallback simple string iterator. */ +class DefaultStringIterator : public StringIterator +{ + const char *string; ///< Current string. + size_t len; ///< String length. + size_t cur_pos; ///< Current iteration position. + +public: + DefaultStringIterator() : string(NULL) + { + } + + virtual void SetString(const char *s) + { + this->string = s; + this->len = strlen(s); + this->cur_pos = 0; + } + + virtual size_t SetCurPosition(size_t pos) + { + assert(this->string != NULL && pos <= this->len); + /* Sanitize in case we get a position inside an UTF-8 sequence. */ + while (pos > 0 && IsUtf8Part(this->string[pos])) pos--; + return this->cur_pos = pos; + } + + virtual size_t Next(IterType what) + { + assert(this->string != NULL); + + /* Already at the end? */ + if (this->cur_pos >= this->len) return END; + + switch (what) { + case ITER_CHARACTER: { + WChar c; + this->cur_pos += Utf8Decode(&c, this->string + this->cur_pos); + return this->cur_pos; + } + + case ITER_WORD: { + WChar c; + /* Consume current word. */ + size_t offs = Utf8Decode(&c, this->string + this->cur_pos); + while (this->cur_pos < this->len && !IsWhitespace(c)) { + this->cur_pos += offs; + offs = Utf8Decode(&c, this->string + this->cur_pos); + } + /* Consume whitespace to the next word. */ + while (this->cur_pos < this->len && IsWhitespace(c)) { + this->cur_pos += offs; + offs = Utf8Decode(&c, this->string + this->cur_pos); + } + + return this->cur_pos; + } + + default: + NOT_REACHED(); + } + + return END; + } + + virtual size_t Prev(IterType what) + { + assert(this->string != NULL); + + /* Already at the beginning? */ + if (this->cur_pos == 0) return END; + + switch (what) { + case ITER_CHARACTER: + return this->cur_pos = Utf8PrevChar(this->string + this->cur_pos) - this->string; + + case ITER_WORD: { + const char *s = this->string + this->cur_pos; + WChar c; + /* Consume preceding whitespace. */ + do { + s = Utf8PrevChar(s); + Utf8Decode(&c, s); + } while (s > this->string && IsWhitespace(c)); + /* Consume preceding word. */ + while (s > this->string && !IsWhitespace(c)) { + s = Utf8PrevChar(s); + Utf8Decode(&c, s); + } + /* Move caret back to the beginning of the word. */ + if (IsWhitespace(c)) Utf8Consume(&s); + + return this->cur_pos = s - this->string; + } + + default: + NOT_REACHED(); + } + + return END; + } +}; + +/* static */ StringIterator *StringIterator::Create() +{ + return new DefaultStringIterator(); +} + +#endif diff --git a/src/string_base.h b/src/string_base.h new file mode 100644 index 0000000000..e1eaed3496 --- /dev/null +++ b/src/string_base.h @@ -0,0 +1,66 @@ +/* $Id$ */ + +/* + * This file is part of OpenTTD. + * 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. + * 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. + * 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 . + */ + +#ifndef STRING_BASE_H +#define STRING_BASE_H + +#include "string_type.h" + +/** Class for iterating over different kind of parts of a string. */ +class StringIterator { +public: + /** Type of the iterator. */ + enum IterType { + ITER_CHARACTER, ///< Iterate over characters (or more exactly grapheme clusters). + ITER_WORD, ///< Iterate over words. + }; + + /** Sentinel to indicate end-of-iteration. */ + static const size_t END = SIZE_MAX; + + /** + * Create a new iterator instance. + * @return New iterator instance. + */ + static StringIterator *Create(); + + virtual ~StringIterator() {} + + /** + * Set a new iteration string. Must also be called if the string contents + * changed. The cursor is reset to the start of the string. + * @param s New string. + */ + virtual void SetString(const char *s) = 0; + + /** + * Change the current string cursor. + * @param p New cursor position. + * @return Actual new cursor position at the next valid character boundary. + * @pre p has to be inside the current string. + */ + virtual size_t SetCurPosition(size_t pos) = 0; + + /** + * Advance the cursor by one iteration unit. + * @return New cursor position (in bytes) or #END if the cursor is already at the end of the string. + */ + virtual size_t Next(IterType what = ITER_CHARACTER) = 0; + + /** + * Move the cursor back by one iteration unit. + * @return New cursor position (in bytes) or #END if the cursor is already at the start of the string. + */ + virtual size_t Prev(IterType what = ITER_CHARACTER) = 0; + +protected: + StringIterator() {} +}; + +#endif /* STRING_BASE_H */ diff --git a/src/string_func.h b/src/string_func.h index 25608889dd..d7056f1be1 100644 --- a/src/string_func.h +++ b/src/string_func.h @@ -90,7 +90,6 @@ static inline WChar Utf8Consume(const char **s) return c; } - /** * Return the length of a UTF-8 encoded character. * @param c Unicode character. @@ -147,8 +146,60 @@ static inline char *Utf8PrevChar(char *s) return ret; } +static inline const char *Utf8PrevChar(const char *s) +{ + const char *ret = s; + while (IsUtf8Part(*--ret)) {} + return ret; +} + size_t Utf8StringLength(const char *s); +/** + * Is the given character a lead surrogate code point? + * @param c The character to test. + * @return True if the character is a lead surrogate code point. + */ +static inline bool Utf16IsLeadSurrogate(uint c) +{ + return c >= 0xD800 && c <= 0xDBFF; +} + +/** + * Is the given character a lead surrogate code point? + * @param c The character to test. + * @return True if the character is a lead surrogate code point. + */ +static inline bool Utf16IsTrailSurrogate(uint c) +{ + return c >= 0xDC00 && c <= 0xDFFF; +} + +/** + * Convert an UTF-16 surrogate pair to the corresponding Unicode character. + * @param lead Lead surrogate code point. + * @param trail Trail surrogate code point. + * @return Decoded Unicode character. + */ +static inline WChar Utf16DecodeSurrogate(uint lead, uint trail) +{ + return 0x10000 + (((lead - 0xD800) << 10) | (trail - 0xDC00)); +} + +/** + * Decode an UTF-16 character. + * @param c Pointer to one or two UTF-16 code points. + * @return Decoded Unicode character. + */ +static inline WChar Utf16DecodeChar(const uint16 *c) +{ + if (Utf16IsLeadSurrogate(c[0])) { + return Utf16DecodeSurrogate(c[0], c[1]); + } else { + return *c; + } +} + /** * Is the given character a text direction character. * @param c The character to test. diff --git a/src/textbuf.cpp b/src/textbuf.cpp index f38bfd17b2..c7071565ab 100644 --- a/src/textbuf.cpp +++ b/src/textbuf.cpp @@ -43,101 +43,68 @@ bool Textbuf::CanDelChar(bool backspace) } /** - * Get the next character that will be removed by DelChar. - * @param backspace if set, delete the character before the caret, - * otherwise, delete the character after it. - * @return the next character that will be removed by DelChar. - * @warning You should ensure Textbuf::CanDelChar returns true before calling this function. + * Delete a character from a textbuffer, either with 'Delete' or 'Backspace' + * The character is delete from the position the caret is at + * @param keycode Type of deletion, either WKC_BACKSPACE or WKC_DELETE + * @return Return true on successful change of Textbuf, or false otherwise */ -WChar Textbuf::GetNextDelChar(bool backspace) +bool Textbuf::DeleteChar(uint16 keycode) { - assert(this->CanDelChar(backspace)); + bool word = (keycode & WKC_CTRL) != 0; - const char *s; - if (backspace) { - s = Utf8PrevChar(this->buf + this->caretpos); - } else { - s = this->buf + this->caretpos; - } + keycode &= ~WKC_SPECIAL_KEYS; + if (keycode != WKC_BACKSPACE && keycode != WKC_DELETE) return false; - WChar c; - Utf8Decode(&c, s); - return c; -} + bool backspace = keycode == WKC_BACKSPACE; -/** - * Delete a character at the caret position in a text buf. - * @param backspace if set, delete the character before the caret, - * else delete the character after it. - * @warning You should ensure Textbuf::CanDelChar returns true before calling this function. - */ -void Textbuf::DelChar(bool backspace) -{ - assert(this->CanDelChar(backspace)); + if (!CanDelChar(backspace)) return false; - WChar c; char *s = this->buf + this->caretpos; + uint16 len = 0; - if (backspace) s = Utf8PrevChar(s); - - uint16 len = (uint16)Utf8Decode(&c, s); - uint width = GetCharacterWidth(FS_NORMAL, c); - - this->pixels -= width; - if (backspace) { - this->caretpos -= len; - this->caretxoffs -= width; + if (word) { + /* Delete a complete word. */ + if (backspace) { + /* Delete whitespace and word in front of the caret. */ + len = this->caretpos - (uint16)this->char_iter->Prev(StringIterator::ITER_WORD); + s -= len; + } else { + /* Delete word and following whitespace following the caret. */ + len = (uint16)this->char_iter->Next(StringIterator::ITER_WORD) - this->caretpos; + } + /* Update character count. */ + for (const char *ss = s; ss < s + len; Utf8Consume(&ss)) { + this->chars--; + } + } else { + /* Delete a single character. */ + if (backspace) { + /* Delete the last code point in front of the caret. */ + s = Utf8PrevChar(s); + WChar c; + len = (uint16)Utf8Decode(&c, s); + this->chars--; + } else { + /* Delete the complete character following the caret. */ + len = (uint16)this->char_iter->Next(StringIterator::ITER_CHARACTER) - this->caretpos; + /* Update character count. */ + for (const char *ss = s; ss < s + len; Utf8Consume(&ss)) { + this->chars--; + } + } } /* Move the remaining characters over the marker */ memmove(s, s + len, this->bytes - (s - this->buf) - len); this->bytes -= len; - this->chars--; -} -/** - * Delete a character from a textbuffer, either with 'Delete' or 'Backspace' - * The character is delete from the position the caret is at - * @param delmode Type of deletion, either WKC_BACKSPACE or WKC_DELETE - * @return Return true on successful change of Textbuf, or false otherwise - */ -bool Textbuf::DeleteChar(int delmode) -{ - if (delmode == WKC_BACKSPACE || delmode == WKC_DELETE) { - bool backspace = delmode == WKC_BACKSPACE; - if (CanDelChar(backspace)) { - this->DelChar(backspace); - return true; - } - return false; - } + if (backspace) this->caretpos -= len; - if (delmode == (WKC_CTRL | WKC_BACKSPACE) || delmode == (WKC_CTRL | WKC_DELETE)) { - bool backspace = delmode == (WKC_CTRL | WKC_BACKSPACE); + this->UpdateStringIter(); + this->UpdateWidth(); + this->UpdateCaretPosition(); - if (!CanDelChar(backspace)) return false; - WChar c = this->GetNextDelChar(backspace); - - /* Backspace: Delete left whitespaces. - * Delete: Delete right word. - */ - while (backspace ? IsWhitespace(c) : !IsWhitespace(c)) { - this->DelChar(backspace); - if (!this->CanDelChar(backspace)) return true; - c = this->GetNextDelChar(backspace); - } - /* Backspace: Delete left word. - * Delete: Delete right whitespaces. - */ - while (backspace ? !IsWhitespace(c) : IsWhitespace(c)) { - this->DelChar(backspace); - if (!this->CanDelChar(backspace)) return true; - c = this->GetNextDelChar(backspace); - } - return true; - } - - return false; + return true; } /** @@ -148,6 +115,7 @@ void Textbuf::DeleteAll() memset(this->buf, 0, this->max_bytes); this->bytes = this->chars = 1; this->pixels = this->caretpos = this->caretxoffs = 0; + this->UpdateStringIter(); } /** @@ -159,17 +127,17 @@ void Textbuf::DeleteAll() */ bool Textbuf::InsertChar(WChar key) { - const byte charwidth = GetCharacterWidth(FS_NORMAL, key); uint16 len = (uint16)Utf8CharLen(key); if (this->bytes + len <= this->max_bytes && this->chars + 1 <= this->max_chars) { memmove(this->buf + this->caretpos + len, this->buf + this->caretpos, this->bytes - this->caretpos); Utf8Encode(this->buf + this->caretpos, key); this->chars++; - this->bytes += len; - this->pixels += charwidth; + this->bytes += len; + this->caretpos += len; - this->caretpos += len; - this->caretxoffs += charwidth; + this->UpdateStringIter(); + this->UpdateWidth(); + this->UpdateCaretPosition(); return true; } return false; @@ -187,7 +155,7 @@ bool Textbuf::InsertClipboard() if (!GetClipboardContents(utf8_buf, lengthof(utf8_buf))) return false; - uint16 pixels = 0, bytes = 0, chars = 0; + uint16 bytes = 0, chars = 0; WChar c; for (const char *ptr = utf8_buf; (c = Utf8Consume(&ptr)) != '\0';) { if (!IsValidChar(c, this->afilter)) break; @@ -196,9 +164,6 @@ bool Textbuf::InsertClipboard() if (this->bytes + bytes + len > this->max_bytes) break; if (this->chars + chars + 1 > this->max_chars) break; - byte char_pixels = GetCharacterWidth(FS_NORMAL, c); - - pixels += char_pixels; bytes += len; chars++; } @@ -207,8 +172,6 @@ bool Textbuf::InsertClipboard() memmove(this->buf + this->caretpos + bytes, this->buf + this->caretpos, this->bytes - this->caretpos); memcpy(this->buf + this->caretpos, utf8_buf, bytes); - this->pixels += pixels; - this->caretxoffs += pixels; this->bytes += bytes; this->chars += chars; @@ -217,131 +180,76 @@ bool Textbuf::InsertClipboard() assert(this->chars <= this->max_chars); this->buf[this->bytes - 1] = '\0'; // terminating zero + this->UpdateStringIter(); + this->UpdateWidth(); + this->UpdateCaretPosition(); + return true; } -/** - * Checks if it is possible to move caret to the left - * @return true if the caret can be moved to the left, otherwise false. - */ -bool Textbuf::CanMoveCaretLeft() +/** Update the character iter after the text has changed. */ +void Textbuf::UpdateStringIter() { - return this->caretpos != 0; + this->char_iter->SetString(this->buf); + size_t pos = this->char_iter->SetCurPosition(this->caretpos); + this->caretpos = pos == StringIterator::END ? 0 : (uint16)pos; } -/** - * Moves the caret to the left. - * @pre Ensure that Textbuf::CanMoveCaretLeft returns true - * @return The character under the caret. - */ -WChar Textbuf::MoveCaretLeft() +/** Update pixel width of the text. */ +void Textbuf::UpdateWidth() { - assert(this->CanMoveCaretLeft()); - - WChar c; - const char *s = Utf8PrevChar(this->buf + this->caretpos); - Utf8Decode(&c, s); - this->caretpos = s - this->buf; - this->caretxoffs -= GetCharacterWidth(FS_NORMAL, c); - - return c; + this->pixels = GetStringBoundingBox(this->buf, FS_NORMAL).width; } -/** - * Checks if it is possible to move caret to the right - * @return true if the caret can be moved to the right, otherwise false. - */ -bool Textbuf::CanMoveCaretRight() +/** Update pixel position of the caret. */ +void Textbuf::UpdateCaretPosition() { - return this->caretpos < this->bytes - 1; -} - -/** - * Moves the caret to the right. - * @pre Ensure that Textbuf::CanMoveCaretRight returns true - * @return The character under the caret. - */ -WChar Textbuf::MoveCaretRight() -{ - assert(this->CanMoveCaretRight()); - - WChar c; - this->caretpos += (uint16)Utf8Decode(&c, this->buf + this->caretpos); - this->caretxoffs += GetCharacterWidth(FS_NORMAL, c); - - Utf8Decode(&c, this->buf + this->caretpos); - return c; + this->caretxoffs = this->chars > 1 ? GetCharPosInString(this->buf, this->buf + this->caretpos, FS_NORMAL).x : 0; } /** * Handle text navigation with arrow keys left/right. * This defines where the caret will blink and the next character interaction will occur - * @param navmode Direction in which navigation occurs (WKC_CTRL |) WKC_LEFT, (WKC_CTRL |) WKC_RIGHT, WKC_END, WKC_HOME + * @param keycode Direction in which navigation occurs (WKC_CTRL |) WKC_LEFT, (WKC_CTRL |) WKC_RIGHT, WKC_END, WKC_HOME * @return Return true on successful change of Textbuf, or false otherwise */ -bool Textbuf::MovePos(int navmode) +bool Textbuf::MovePos(uint16 keycode) { - switch (navmode) { + switch (keycode) { case WKC_LEFT: - if (this->CanMoveCaretLeft()) { - this->MoveCaretLeft(); - return true; - } - break; - case WKC_CTRL | WKC_LEFT: { - if (!this->CanMoveCaretLeft()) break; + if (this->caretpos == 0) break; - /* Unconditionally move one char to the left. */ - WChar c = this->MoveCaretLeft(); - /* Consume left whitespaces. */ - while (IsWhitespace(c)) { - if (!this->CanMoveCaretLeft()) return true; - c = this->MoveCaretLeft(); - } - /* Consume left word. */ - while (!IsWhitespace(c)) { - if (!this->CanMoveCaretLeft()) return true; - c = this->MoveCaretLeft(); - } - /* Place caret at the beginning of the left word. */ - this->MoveCaretRight(); + size_t pos = this->char_iter->Prev(keycode & WKC_CTRL ? StringIterator::ITER_WORD : StringIterator::ITER_CHARACTER); + if (pos == StringIterator::END) return true; + + this->caretpos = (uint16)pos; + this->UpdateCaretPosition(); return true; } case WKC_RIGHT: - if (this->CanMoveCaretRight()) { - this->MoveCaretRight(); - return true; - } - break; - case WKC_CTRL | WKC_RIGHT: { - if (!this->CanMoveCaretRight()) break; + if (this->caretpos >= this->bytes - 1) break; - /* Unconditionally move one char to the right. */ - WChar c = this->MoveCaretRight(); - /* Continue to consume current word. */ - while (!IsWhitespace(c)) { - if (!this->CanMoveCaretRight()) return true; - c = this->MoveCaretRight(); - } - /* Consume right whitespaces. */ - while (IsWhitespace(c)) { - if (!this->CanMoveCaretRight()) return true; - c = this->MoveCaretRight(); - } + size_t pos = this->char_iter->Next(keycode & WKC_CTRL ? StringIterator::ITER_WORD : StringIterator::ITER_CHARACTER); + if (pos == StringIterator::END) return true; + + this->caretpos = (uint16)pos; + this->UpdateCaretPosition(); return true; } case WKC_HOME: this->caretpos = 0; - this->caretxoffs = 0; + this->char_iter->SetCurPosition(this->caretpos); + this->UpdateCaretPosition(); return true; case WKC_END: this->caretpos = this->bytes - 1; - this->caretxoffs = this->pixels; + this->char_iter->SetCurPosition(this->caretpos); + this->UpdateCaretPosition(); return true; default: @@ -364,6 +272,8 @@ Textbuf::Textbuf(uint16 max_bytes, uint16 max_chars) assert(max_bytes != 0); assert(max_chars != 0); + this->char_iter = StringIterator::Create(); + this->afilter = CS_ALPHANUMERAL; this->max_bytes = max_bytes; this->max_chars = max_chars == UINT16_MAX ? max_bytes : max_chars; @@ -373,6 +283,7 @@ Textbuf::Textbuf(uint16 max_bytes, uint16 max_chars) Textbuf::~Textbuf() { + delete this->char_iter; free(this->buf); } @@ -418,21 +329,21 @@ void Textbuf::UpdateSize() { const char *buf = this->buf; - this->pixels = 0; this->chars = this->bytes = 1; // terminating zero WChar c; while ((c = Utf8Consume(&buf)) != '\0') { - this->pixels += GetCharacterWidth(FS_NORMAL, c); this->bytes += Utf8CharLen(c); this->chars++; } - assert(this->bytes <= this->max_bytes); assert(this->chars <= this->max_chars); this->caretpos = this->bytes - 1; - this->caretxoffs = this->pixels; + this->UpdateStringIter(); + this->UpdateWidth(); + + this->UpdateCaretPosition(); } /** @@ -450,3 +361,49 @@ bool Textbuf::HandleCaret() } return false; } + +HandleKeyPressResult Textbuf::HandleKeyPress(uint16 key, uint16 keycode) +{ + bool edited = false; + + switch (keycode) { + case WKC_ESC: return HKPR_CANCEL; + + case WKC_RETURN: case WKC_NUM_ENTER: return HKPR_CONFIRM; + +#ifdef WITH_COCOA + case (WKC_META | 'V'): +#endif + case (WKC_CTRL | 'V'): + edited = this->InsertClipboard(); + break; + +#ifdef WITH_COCOA + case (WKC_META | 'U'): +#endif + case (WKC_CTRL | 'U'): + this->DeleteAll(); + edited = true; + break; + + case WKC_BACKSPACE: case WKC_DELETE: + case WKC_CTRL | WKC_BACKSPACE: case WKC_CTRL | WKC_DELETE: + edited = this->DeleteChar(keycode); + break; + + case WKC_LEFT: case WKC_RIGHT: case WKC_END: case WKC_HOME: + case WKC_CTRL | WKC_LEFT: case WKC_CTRL | WKC_RIGHT: + this->MovePos(keycode); + break; + + default: + if (IsValidChar(key, this->afilter)) { + edited = this->InsertChar(key); + } else { + return HKPR_NOT_HANDLED; + } + break; + } + + return edited ? HKPR_EDITING : HKPR_CURSOR; +} diff --git a/src/textbuf_type.h b/src/textbuf_type.h index 2582d1e252..4d1a926fbe 100644 --- a/src/textbuf_type.h +++ b/src/textbuf_type.h @@ -14,6 +14,19 @@ #include "string_type.h" #include "strings_type.h" +#include "string_base.h" + +/** + * Return values for Textbuf::HandleKeypress + */ +enum HandleKeyPressResult +{ + HKPR_EDITING, ///< Textbuf content changed. + HKPR_CURSOR, ///< Non-text change, e.g. cursor position. + HKPR_CONFIRM, ///< Return or enter key pressed. + HKPR_CANCEL, ///< Escape key pressed. + HKPR_NOT_HANDLED, ///< Key does not affect editboxes. +}; /** Helper/buffer for input fields. */ struct Textbuf { @@ -36,23 +49,26 @@ struct Textbuf { void CDECL Print(const char *format, ...) WARN_FORMAT(2, 3); void DeleteAll(); - bool DeleteChar(int delmode); - bool InsertChar(uint32 key); bool InsertClipboard(); - bool MovePos(int navmode); + + bool InsertChar(uint32 key); + + bool DeleteChar(uint16 keycode); + bool MovePos(uint16 keycode); + + HandleKeyPressResult HandleKeyPress(uint16 key, uint16 keycode); bool HandleCaret(); void UpdateSize(); private: - bool CanDelChar(bool backspace); - WChar GetNextDelChar(bool backspace); - void DelChar(bool backspace); - bool CanMoveCaretLeft(); - WChar MoveCaretLeft(); - bool CanMoveCaretRight(); - WChar MoveCaretRight(); + StringIterator *char_iter; + bool CanDelChar(bool backspace); + + void UpdateStringIter(); + void UpdateWidth(); + void UpdateCaretPosition(); }; #endif /* TEXTBUF_TYPE_H */ diff --git a/src/video/cocoa/cocoa_v.h b/src/video/cocoa/cocoa_v.h index 9f7f55c7ce..06bbbfdb2a 100644 --- a/src/video/cocoa/cocoa_v.h +++ b/src/video/cocoa/cocoa_v.h @@ -253,6 +253,7 @@ uint QZ_ListModes(OTTD_Point *modes, uint max_modes, CGDirectDisplayID display_i - (void)setDriver:(CocoaSubdriver*)drv; - (BOOL)windowShouldClose:(id)sender; +- (void)windowDidEnterFullScreen:(NSNotification *)aNotification; @end diff --git a/src/video/cocoa/cocoa_v.mm b/src/video/cocoa/cocoa_v.mm index 9e883928bc..ca81715b18 100644 --- a/src/video/cocoa/cocoa_v.mm +++ b/src/video/cocoa/cocoa_v.mm @@ -776,6 +776,14 @@ void cocoaReleaseAutoreleasePool() { driver->active = false; } +/** Window entered fullscreen mode (10.7). */ +- (void)windowDidEnterFullScreen:(NSNotification *)aNotification +{ + NSPoint loc = [ driver->cocoaview convertPoint:[ [ aNotification object ] mouseLocationOutsideOfEventStream ] fromView:nil ]; + BOOL inside = ([ driver->cocoaview hitTest:loc ] == driver->cocoaview); + + if (inside) [ driver->cocoaview mouseEntered:NULL ]; +} @end diff --git a/src/video/cocoa/wnd_quartz.mm b/src/video/cocoa/wnd_quartz.mm index 5d45dcbab3..8033840052 100644 --- a/src/video/cocoa/wnd_quartz.mm +++ b/src/video/cocoa/wnd_quartz.mm @@ -290,16 +290,15 @@ bool WindowQuartzSubdriver::SetVideoMode(int width, int height, int bpp) const int NSWindowCollectionBehaviorFullScreenPrimary = 1 << 7; const int NSWindowFullScreenButton = 7; - NSWindowCollectionBehavior behavior = [this->window collectionBehavior]; + NSWindowCollectionBehavior behavior = [ this->window collectionBehavior ]; behavior |= NSWindowCollectionBehaviorFullScreenPrimary; - [window setCollectionBehavior:behavior]; + [ this->window setCollectionBehavior:behavior ]; - NSButton* fullscreenButton = - [this->window standardWindowButton:NSWindowFullScreenButton]; - [fullscreenButton setAction:@selector(toggleFullScreen:)]; - [fullscreenButton setTarget:this->window]; + NSButton* fullscreenButton = [ this->window standardWindowButton:NSWindowFullScreenButton ]; + [ fullscreenButton setAction:@selector(toggleFullScreen:) ]; + [ fullscreenButton setTarget:this->window ]; - [this->window setCollectionBehavior: NSWindowCollectionBehaviorFullScreenPrimary]; + [ this->window setCollectionBehavior: NSWindowCollectionBehaviorFullScreenPrimary ]; } #endif diff --git a/src/window.cpp b/src/window.cpp index f818471bd1..d223f5cfd6 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -2250,26 +2250,24 @@ static bool MaybeBringWindowToFront(Window *w) */ EventState Window::HandleEditBoxKey(int wid, uint16 key, uint16 keycode) { - EventState state = ES_NOT_HANDLED; - QueryString *query = this->GetQueryString(wid); - if (query == NULL) return state; + if (query == NULL) return ES_NOT_HANDLED; int action = QueryString::ACTION_NOTHING; - switch (query->HandleEditBoxKey(this, wid, key, keycode, state)) { - case HEBR_EDITING: + switch (query->text.HandleKeyPress(key, keycode)) { + case HKPR_EDITING: this->SetWidgetDirty(wid); this->OnEditboxChanged(wid); break; - case HEBR_CURSOR: + case HKPR_CURSOR: this->SetWidgetDirty(wid); /* For the OSK also invalidate the parent window */ if (this->window_class == WC_OSK) this->InvalidateData(); break; - case HEBR_CONFIRM: + case HKPR_CONFIRM: if (this->window_class == WC_OSK) { this->OnClick(Point(), WID_OSK_OK, 1); } else if (query->ok_button >= 0) { @@ -2279,7 +2277,7 @@ EventState Window::HandleEditBoxKey(int wid, uint16 key, uint16 keycode) } break; - case HEBR_CANCEL: + case HKPR_CANCEL: if (this->window_class == WC_OSK) { this->OnClick(Point(), WID_OSK_CANCEL, 1); } else if (query->cancel_button >= 0) { @@ -2289,6 +2287,9 @@ EventState Window::HandleEditBoxKey(int wid, uint16 key, uint16 keycode) } break; + case HKPR_NOT_HANDLED: + return ES_NOT_HANDLED; + default: break; } @@ -2307,7 +2308,7 @@ EventState Window::HandleEditBoxKey(int wid, uint16 key, uint16 keycode) break; } - return state; + return ES_HANDLED; } /**