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/>.
8 /** @file osk_gui.cpp The On Screen Keyboard GUI */
11 #include "string_func.h"
12 #include "strings_func.h"
14 #include "window_func.h"
16 #include "querystring_gui.h"
17 #include "video/video_driver.hpp"
18 #include "zoom_func.h"
20 #include "widgets/osk_widget.h"
22 #include "table/sprites.h"
23 #include "table/strings.h"
25 #include "safeguards.h"
27 std::string _keyboard_opt
[2];
28 static char32_t _keyboard
[2][OSK_KEYBOARD_ENTRIES
];
35 static uint8_t _keystate
= KEYS_NONE
;
37 struct OskWindow
: public Window
{
38 StringID caption
; ///< the caption for this window.
39 QueryString
*qs
; ///< text-input
40 WidgetID text_btn
; ///< widget number of parent's text field
41 Textbuf
*text
; ///< pointer to parent's textbuffer (to update caret position)
42 std::string orig_str
; ///< Original string.
43 bool shift
; ///< Is the shift effectively pressed?
45 OskWindow(WindowDesc
&desc
, Window
*parent
, WidgetID button
) : Window(desc
)
47 this->parent
= parent
;
48 assert(parent
!= nullptr);
50 NWidgetCore
*par_wid
= parent
->GetWidget
<NWidgetCore
>(button
);
51 assert(par_wid
!= nullptr);
53 assert(parent
->querystrings
.count(button
) != 0);
54 this->qs
= parent
->querystrings
.find(button
)->second
;
55 this->caption
= (par_wid
->widget_data
!= STR_NULL
) ? par_wid
->widget_data
: this->qs
->caption
;
56 this->text_btn
= button
;
57 this->text
= &this->qs
->text
;
58 this->querystrings
[WID_OSK_TEXT
] = this->qs
;
60 /* make a copy in case we need to reset later */
61 this->orig_str
= this->qs
->text
.buf
;
64 this->SetFocusedWidget(WID_OSK_TEXT
);
66 /* Not needed by default. */
67 this->DisableWidget(WID_OSK_SPECIAL
);
69 this->UpdateOskState();
73 * Only show valid characters; do not show characters that would
74 * only insert a space when we have a spacebar to do that or
75 * characters that are not allowed to be entered.
79 this->shift
= HasBit(_keystate
, KEYS_CAPS
) ^ HasBit(_keystate
, KEYS_SHIFT
);
81 for (uint i
= 0; i
< OSK_KEYBOARD_ENTRIES
; i
++) {
82 this->SetWidgetDisabledState(WID_OSK_LETTERS
+ i
,
83 !IsValidChar(_keyboard
[this->shift
][i
], this->qs
->text
.afilter
) || _keyboard
[this->shift
][i
] == ' ');
85 this->SetWidgetDisabledState(WID_OSK_SPACE
, !IsValidChar(' ', this->qs
->text
.afilter
));
87 this->SetWidgetLoweredState(WID_OSK_SHIFT
, HasBit(_keystate
, KEYS_SHIFT
));
88 this->SetWidgetLoweredState(WID_OSK_CAPS
, HasBit(_keystate
, KEYS_CAPS
));
91 void SetStringParameters(WidgetID widget
) const override
93 if (widget
== WID_OSK_CAPTION
) SetDParam(0, this->caption
);
96 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
98 if (widget
< WID_OSK_LETTERS
) return;
100 widget
-= WID_OSK_LETTERS
;
101 DrawCharCentered(_keyboard
[this->shift
][widget
], r
, TC_BLACK
);
104 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
106 /* clicked a letter */
107 if (widget
>= WID_OSK_LETTERS
) {
108 char32_t c
= _keyboard
[this->shift
][widget
- WID_OSK_LETTERS
];
110 if (!IsValidChar(c
, this->qs
->text
.afilter
)) return;
112 if (this->qs
->text
.InsertChar(c
)) this->OnEditboxChanged(WID_OSK_TEXT
);
114 if (HasBit(_keystate
, KEYS_SHIFT
)) {
115 ToggleBit(_keystate
, KEYS_SHIFT
);
116 this->UpdateOskState();
123 case WID_OSK_BACKSPACE
:
124 if (this->qs
->text
.DeleteChar(WKC_BACKSPACE
)) this->OnEditboxChanged(WID_OSK_TEXT
);
127 case WID_OSK_SPECIAL
:
129 * Anything device specific can go here.
130 * The button itself is hidden by default, and when you need it you
131 * can not hide it in the create event.
136 ToggleBit(_keystate
, KEYS_CAPS
);
137 this->UpdateOskState();
142 ToggleBit(_keystate
, KEYS_SHIFT
);
143 this->UpdateOskState();
148 if (this->qs
->text
.InsertChar(' ')) this->OnEditboxChanged(WID_OSK_TEXT
);
152 if (this->qs
->text
.MovePos(WKC_LEFT
)) this->InvalidateData();
156 if (this->qs
->text
.MovePos(WKC_RIGHT
)) this->InvalidateData();
160 if (!this->qs
->orig
.has_value() || this->qs
->text
.buf
!= this->qs
->orig
) {
161 /* pass information by simulating a button press on parent window */
162 if (this->qs
->ok_button
>= 0) {
163 this->parent
->OnClick(pt
, this->qs
->ok_button
, 1);
164 /* Window gets deleted when the parent window removes itself. */
172 if (this->qs
->cancel_button
>= 0) { // pass a cancel event to the parent window
173 this->parent
->OnClick(pt
, this->qs
->cancel_button
, 1);
174 /* Window gets deleted when the parent window removes itself. */
176 } else { // or reset to original string
177 qs
->text
.Assign(this->orig_str
);
178 qs
->text
.MovePos(WKC_END
);
179 this->OnEditboxChanged(WID_OSK_TEXT
);
186 void OnEditboxChanged(WidgetID widget
) override
188 if (widget
== WID_OSK_TEXT
) {
189 this->SetWidgetDirty(WID_OSK_TEXT
);
190 this->parent
->OnEditboxChanged(this->text_btn
);
191 this->parent
->SetWidgetDirty(this->text_btn
);
195 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
197 if (!gui_scope
) return;
198 this->SetWidgetDirty(WID_OSK_TEXT
);
199 this->parent
->SetWidgetDirty(this->text_btn
);
202 void OnFocusLost(bool closing
) override
204 VideoDriver::GetInstance()->EditBoxLostFocus();
205 if (!closing
) this->Close();
209 static const int HALF_KEY_WIDTH
= 7; // Width of 1/2 key in pixels.
210 static const int INTER_KEY_SPACE
= 2; // Number of pixels between two keys.
212 static const int TOP_KEY_PADDING
= 2; // Vertical padding for the top row of keys.
213 static const int KEY_PADDING
= 6; // Vertical padding for remaining key rows.
216 * Add a key widget to a row of the keyboard.
217 * @param hor Row container to add key widget to.
218 * @param pad_y Vertical padding of the key (all keys in a row should have equal padding).
219 * @param num_half Number of 1/2 key widths that this key has.
220 * @param widtype Widget type of the key. Must be either \c NWID_SPACER for an invisible key, or a \c WWT_* widget.
221 * @param widnum Widget number of the key.
222 * @param widdata Data value of the key widget.
223 * @note Key width is measured in 1/2 keys to allow for 1/2 key shifting between rows.
225 static void AddKey(std::unique_ptr
<NWidgetHorizontal
> &hor
, int pad_y
, int num_half
, WidgetType widtype
, WidgetID widnum
, uint16_t widdata
)
227 int key_width
= HALF_KEY_WIDTH
+ (INTER_KEY_SPACE
+ HALF_KEY_WIDTH
) * (num_half
- 1);
229 if (widtype
== NWID_SPACER
) {
230 auto spc
= std::make_unique
<NWidgetSpacer
>(key_width
, 0);
231 spc
->SetMinimalTextLines(1, pad_y
, FS_NORMAL
);
232 hor
->Add(std::move(spc
));
234 auto leaf
= std::make_unique
<NWidgetLeaf
>(widtype
, COLOUR_GREY
, widnum
, widdata
, STR_NULL
);
235 leaf
->SetMinimalSize(key_width
, 0);
236 leaf
->SetMinimalTextLines(1, pad_y
, FS_NORMAL
);
237 hor
->Add(std::move(leaf
));
241 /** Construct the top row keys (cancel, ok, backspace). */
242 static std::unique_ptr
<NWidgetBase
> MakeTopKeys()
244 auto hor
= std::make_unique
<NWidgetHorizontal
>();
245 hor
->SetPIP(0, INTER_KEY_SPACE
, 0);
247 AddKey(hor
, TOP_KEY_PADDING
, 6 * 2, WWT_TEXTBTN
, WID_OSK_CANCEL
, STR_BUTTON_CANCEL
);
248 AddKey(hor
, TOP_KEY_PADDING
, 6 * 2, WWT_TEXTBTN
, WID_OSK_OK
, STR_BUTTON_OK
);
249 AddKey(hor
, TOP_KEY_PADDING
, 2 * 2, WWT_PUSHIMGBTN
, WID_OSK_BACKSPACE
, SPR_OSK_BACKSPACE
);
253 /** Construct the row containing the digit keys. */
254 static std::unique_ptr
<NWidgetBase
> MakeNumberKeys()
256 std::unique_ptr
<NWidgetHorizontal
> hor
= std::make_unique
<NWidgetHorizontalLTR
>();
257 hor
->SetPIP(0, INTER_KEY_SPACE
, 0);
259 for (WidgetID widnum
= WID_OSK_NUMBERS_FIRST
; widnum
<= WID_OSK_NUMBERS_LAST
; widnum
++) {
260 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHBTN
, widnum
, 0x0);
265 /** Construct the qwerty row keys. */
266 static std::unique_ptr
<NWidgetBase
> MakeQwertyKeys()
268 std::unique_ptr
<NWidgetHorizontal
> hor
= std::make_unique
<NWidgetHorizontalLTR
>();
269 hor
->SetPIP(0, INTER_KEY_SPACE
, 0);
271 AddKey(hor
, KEY_PADDING
, 3, WWT_PUSHIMGBTN
, WID_OSK_SPECIAL
, SPR_OSK_SPECIAL
);
272 for (WidgetID widnum
= WID_OSK_QWERTY_FIRST
; widnum
<= WID_OSK_QWERTY_LAST
; widnum
++) {
273 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHBTN
, widnum
, 0x0);
275 AddKey(hor
, KEY_PADDING
, 1, NWID_SPACER
, 0, 0);
279 /** Construct the asdfg row keys. */
280 static std::unique_ptr
<NWidgetBase
> MakeAsdfgKeys()
282 std::unique_ptr
<NWidgetHorizontal
> hor
= std::make_unique
<NWidgetHorizontalLTR
>();
283 hor
->SetPIP(0, INTER_KEY_SPACE
, 0);
285 AddKey(hor
, KEY_PADDING
, 4, WWT_IMGBTN
, WID_OSK_CAPS
, SPR_OSK_CAPS
);
286 for (WidgetID widnum
= WID_OSK_ASDFG_FIRST
; widnum
<= WID_OSK_ASDFG_LAST
; widnum
++) {
287 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHBTN
, widnum
, 0x0);
292 /** Construct the zxcvb row keys. */
293 static std::unique_ptr
<NWidgetBase
> MakeZxcvbKeys()
295 std::unique_ptr
<NWidgetHorizontal
> hor
= std::make_unique
<NWidgetHorizontalLTR
>();
296 hor
->SetPIP(0, INTER_KEY_SPACE
, 0);
298 AddKey(hor
, KEY_PADDING
, 3, WWT_IMGBTN
, WID_OSK_SHIFT
, SPR_OSK_SHIFT
);
299 for (WidgetID widnum
= WID_OSK_ZXCVB_FIRST
; widnum
<= WID_OSK_ZXCVB_LAST
; widnum
++) {
300 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHBTN
, widnum
, 0x0);
302 AddKey(hor
, KEY_PADDING
, 1, NWID_SPACER
, 0, 0);
306 /** Construct the spacebar row keys. */
307 static std::unique_ptr
<NWidgetBase
> MakeSpacebarKeys()
309 auto hor
= std::make_unique
<NWidgetHorizontal
>();
310 hor
->SetPIP(0, INTER_KEY_SPACE
, 0);
312 AddKey(hor
, KEY_PADDING
, 8, NWID_SPACER
, 0, 0);
313 AddKey(hor
, KEY_PADDING
, 13, WWT_PUSHTXTBTN
, WID_OSK_SPACE
, STR_EMPTY
);
314 AddKey(hor
, KEY_PADDING
, 3, NWID_SPACER
, 0, 0);
315 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHIMGBTN
, WID_OSK_LEFT
, SPR_OSK_LEFT
);
316 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHIMGBTN
, WID_OSK_RIGHT
, SPR_OSK_RIGHT
);
321 static constexpr NWidgetPart _nested_osk_widgets
[] = {
322 NWidget(WWT_CAPTION
, COLOUR_GREY
, WID_OSK_CAPTION
), SetDataTip(STR_JUST_STRING
, STR_NULL
), SetTextStyle(TC_WHITE
),
323 NWidget(WWT_PANEL
, COLOUR_GREY
),
324 NWidget(WWT_EDITBOX
, COLOUR_GREY
, WID_OSK_TEXT
), SetMinimalSize(252, 0), SetPadding(2, 2, 2, 2),
326 NWidget(WWT_PANEL
, COLOUR_GREY
),
327 NWidget(NWID_VERTICAL
), SetPadding(3), SetPIP(0, INTER_KEY_SPACE
, 0),
328 NWidgetFunction(MakeTopKeys
),
329 NWidgetFunction(MakeNumberKeys
),
330 NWidgetFunction(MakeQwertyKeys
),
331 NWidgetFunction(MakeAsdfgKeys
),
332 NWidgetFunction(MakeZxcvbKeys
),
333 NWidgetFunction(MakeSpacebarKeys
),
338 static WindowDesc
_osk_desc(
339 WDP_CENTER
, nullptr, 0, 0,
346 * Retrieve keyboard layout from language string or (if set) config file.
347 * Also check for invalid characters.
349 void GetKeyboardLayout()
351 std::string keyboard
[2];
352 std::string errormark
[2]; // used for marking invalid chars
353 bool has_error
= false; // true when an invalid char is detected
355 keyboard
[0] = _keyboard_opt
[0].empty() ? GetString(STR_OSK_KEYBOARD_LAYOUT
) : _keyboard_opt
[0];
356 keyboard
[1] = _keyboard_opt
[1].empty() ? GetString(STR_OSK_KEYBOARD_LAYOUT_CAPS
) : _keyboard_opt
[1];
358 for (uint j
= 0; j
< 2; j
++) {
359 auto kbd
= keyboard
[j
].begin();
361 for (uint i
= 0; i
< OSK_KEYBOARD_ENTRIES
; i
++) {
362 _keyboard
[j
][i
] = Utf8Consume(kbd
);
364 /* Be lenient when the last characters are missing (is quite normal) */
365 if (_keyboard
[j
][i
] == '\0' || ended
) {
367 _keyboard
[j
][i
] = ' ';
371 if (IsPrintable(_keyboard
[j
][i
])) {
376 _keyboard
[j
][i
] = ' ';
382 ShowInfo("The keyboard layout you selected contains invalid chars. Please check those chars marked with ^.");
383 ShowInfo("Normal keyboard: {}", keyboard
[0]);
384 ShowInfo(" {}", errormark
[0]);
385 ShowInfo("Caps Lock: {}", keyboard
[1]);
386 ShowInfo(" {}", errormark
[1]);
391 * Show the on-screen keyboard (osk) associated with a given textbox
392 * @param parent pointer to the Window where this keyboard originated from
393 * @param button widget number of parent's textbox
395 void ShowOnScreenKeyboard(Window
*parent
, WidgetID button
)
397 CloseWindowById(WC_OSK
, 0);
400 new OskWindow(_osk_desc
, parent
, button
);
404 * Updates the original text of the OSK so when the 'parent' changes the
405 * original and you press on cancel you won't get the 'old' original text
406 * but the updated one.
407 * @param parent window that just updated its original text
408 * @param button widget number of parent's textbox to update
410 void UpdateOSKOriginalText(const Window
*parent
, WidgetID button
)
412 OskWindow
*osk
= dynamic_cast<OskWindow
*>(FindWindowById(WC_OSK
, 0));
413 if (osk
== nullptr || osk
->parent
!= parent
|| osk
->text_btn
!= button
) return;
415 osk
->orig_str
= osk
->qs
->text
.buf
;
421 * Check whether the OSK is opened for a specific editbox.
422 * @param w Window to check for
423 * @param button Editbox of \a w to check for
424 * @return true if the OSK is opened for \a button.
426 bool IsOSKOpenedFor(const Window
*w
, WidgetID button
)
428 OskWindow
*osk
= dynamic_cast<OskWindow
*>(FindWindowById(WC_OSK
, 0));
429 return osk
!= nullptr && osk
->parent
== w
&& osk
->text_btn
== button
;