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 WChar _keyboard
[2][OSK_KEYBOARD_ENTRIES
];
35 static byte _keystate
= KEYS_NONE
;
37 struct OskWindow
: public Window
{
38 StringID caption
; ///< the caption for this window.
39 QueryString
*qs
; ///< text-input
40 int text_btn
; ///< widget number of parent's text field
41 Textbuf
*text
; ///< pointer to parent's textbuffer (to update caret position)
42 char *orig_str_buf
; ///< Original string.
43 bool shift
; ///< Is the shift effectively pressed?
45 OskWindow(WindowDesc
*desc
, Window
*parent
, int 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
.Contains(button
));
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_buf
= stredup(this->qs
->text
.buf
);
64 this->SetFocusedWidget(WID_OSK_TEXT
);
66 /* Not needed by default. */
67 this->DisableWidget(WID_OSK_SPECIAL
);
69 this->UpdateOskState();
74 free(this->orig_str_buf
);
78 * Only show valid characters; do not show characters that would
79 * only insert a space when we have a spacebar to do that or
80 * characters that are not allowed to be entered.
84 this->shift
= HasBit(_keystate
, KEYS_CAPS
) ^ HasBit(_keystate
, KEYS_SHIFT
);
86 for (uint i
= 0; i
< OSK_KEYBOARD_ENTRIES
; i
++) {
87 this->SetWidgetDisabledState(WID_OSK_LETTERS
+ i
,
88 !IsValidChar(_keyboard
[this->shift
][i
], this->qs
->text
.afilter
) || _keyboard
[this->shift
][i
] == ' ');
90 this->SetWidgetDisabledState(WID_OSK_SPACE
, !IsValidChar(' ', this->qs
->text
.afilter
));
92 this->SetWidgetLoweredState(WID_OSK_SHIFT
, HasBit(_keystate
, KEYS_SHIFT
));
93 this->SetWidgetLoweredState(WID_OSK_CAPS
, HasBit(_keystate
, KEYS_CAPS
));
96 void SetStringParameters(int widget
) const override
98 if (widget
== WID_OSK_CAPTION
) SetDParam(0, this->caption
);
101 void DrawWidget(const Rect
&r
, int widget
) const override
103 if (widget
< WID_OSK_LETTERS
) return;
105 widget
-= WID_OSK_LETTERS
;
106 DrawCharCentered(_keyboard
[this->shift
][widget
], r
, TC_BLACK
);
109 void OnClick(Point pt
, int widget
, int click_count
) override
111 /* clicked a letter */
112 if (widget
>= WID_OSK_LETTERS
) {
113 WChar c
= _keyboard
[this->shift
][widget
- WID_OSK_LETTERS
];
115 if (!IsValidChar(c
, this->qs
->text
.afilter
)) return;
117 if (this->qs
->text
.InsertChar(c
)) this->OnEditboxChanged(WID_OSK_TEXT
);
119 if (HasBit(_keystate
, KEYS_SHIFT
)) {
120 ToggleBit(_keystate
, KEYS_SHIFT
);
121 this->UpdateOskState();
128 case WID_OSK_BACKSPACE
:
129 if (this->qs
->text
.DeleteChar(WKC_BACKSPACE
)) this->OnEditboxChanged(WID_OSK_TEXT
);
132 case WID_OSK_SPECIAL
:
134 * Anything device specific can go here.
135 * The button itself is hidden by default, and when you need it you
136 * can not hide it in the create event.
141 ToggleBit(_keystate
, KEYS_CAPS
);
142 this->UpdateOskState();
147 ToggleBit(_keystate
, KEYS_SHIFT
);
148 this->UpdateOskState();
153 if (this->qs
->text
.InsertChar(' ')) this->OnEditboxChanged(WID_OSK_TEXT
);
157 if (this->qs
->text
.MovePos(WKC_LEFT
)) this->InvalidateData();
161 if (this->qs
->text
.MovePos(WKC_RIGHT
)) this->InvalidateData();
165 if (this->qs
->orig
== nullptr || strcmp(this->qs
->text
.buf
, this->qs
->orig
) != 0) {
166 /* pass information by simulating a button press on parent window */
167 if (this->qs
->ok_button
>= 0) {
168 this->parent
->OnClick(pt
, this->qs
->ok_button
, 1);
169 /* Window gets deleted when the parent window removes itself. */
177 if (this->qs
->cancel_button
>= 0) { // pass a cancel event to the parent window
178 this->parent
->OnClick(pt
, this->qs
->cancel_button
, 1);
179 /* Window gets deleted when the parent window removes itself. */
181 } else { // or reset to original string
182 qs
->text
.Assign(this->orig_str_buf
);
183 qs
->text
.MovePos(WKC_END
);
184 this->OnEditboxChanged(WID_OSK_TEXT
);
191 void OnEditboxChanged(int widget
) override
193 this->SetWidgetDirty(WID_OSK_TEXT
);
194 this->parent
->OnEditboxChanged(this->text_btn
);
195 this->parent
->SetWidgetDirty(this->text_btn
);
198 void OnInvalidateData(int data
= 0, bool gui_scope
= true) override
200 if (!gui_scope
) return;
201 this->SetWidgetDirty(WID_OSK_TEXT
);
202 this->parent
->SetWidgetDirty(this->text_btn
);
205 void OnFocusLost() override
207 VideoDriver::GetInstance()->EditBoxLostFocus();
212 static const int HALF_KEY_WIDTH
= 7; // Width of 1/2 key in pixels.
213 static const int INTER_KEY_SPACE
= 2; // Number of pixels between two keys.
215 static const int TOP_KEY_PADDING
= 2; // Vertical padding for the top row of keys.
216 static const int KEY_PADDING
= 6; // Vertical padding for remaining key rows.
219 * Add a key widget to a row of the keyboard.
220 * @param hor Row container to add key widget to.
221 * @param pad_y Vertical padding of the key (all keys in a row should have equal padding).
222 * @param num_half Number of 1/2 key widths that this key has.
223 * @param widtype Widget type of the key. Must be either \c NWID_SPACER for an invisible key, or a \c WWT_* widget.
224 * @param widnum Widget number of the key.
225 * @param widdata Data value of the key widget.
226 * @param biggest_index Collected biggest widget index so far.
227 * @note Key width is measured in 1/2 keys to allow for 1/2 key shifting between rows.
229 static void AddKey(NWidgetHorizontal
*hor
, int pad_y
, int num_half
, WidgetType widtype
, int widnum
, uint16 widdata
, int *biggest_index
)
231 int key_width
= HALF_KEY_WIDTH
+ (INTER_KEY_SPACE
+ HALF_KEY_WIDTH
) * (num_half
- 1);
233 if (widtype
== NWID_SPACER
) {
234 if (!hor
->IsEmpty()) key_width
+= INTER_KEY_SPACE
;
235 NWidgetSpacer
*spc
= new NWidgetSpacer(key_width
, 0);
236 spc
->SetMinimalTextLines(1, pad_y
, FS_NORMAL
);
239 if (!hor
->IsEmpty()) {
240 NWidgetSpacer
*spc
= new NWidgetSpacer(INTER_KEY_SPACE
, 0);
241 spc
->SetMinimalTextLines(1, pad_y
, FS_NORMAL
);
244 NWidgetLeaf
*leaf
= new NWidgetLeaf(widtype
, COLOUR_GREY
, widnum
, widdata
, STR_NULL
);
245 leaf
->SetMinimalSize(key_width
, 0);
246 leaf
->SetMinimalTextLines(1, pad_y
, FS_NORMAL
);
250 *biggest_index
= std::max(*biggest_index
, widnum
);
253 /** Construct the top row keys (cancel, ok, backspace). */
254 static NWidgetBase
*MakeTopKeys(int *biggest_index
)
256 NWidgetHorizontal
*hor
= new NWidgetHorizontal();
258 AddKey(hor
, TOP_KEY_PADDING
, 6 * 2, WWT_TEXTBTN
, WID_OSK_CANCEL
, STR_BUTTON_CANCEL
, biggest_index
);
259 AddKey(hor
, TOP_KEY_PADDING
, 6 * 2, WWT_TEXTBTN
, WID_OSK_OK
, STR_BUTTON_OK
, biggest_index
);
260 AddKey(hor
, TOP_KEY_PADDING
, 2 * 2, WWT_PUSHIMGBTN
, WID_OSK_BACKSPACE
, SPR_OSK_BACKSPACE
, biggest_index
);
264 /** Construct the row containing the digit keys. */
265 static NWidgetBase
*MakeNumberKeys(int *biggest_index
)
267 NWidgetHorizontal
*hor
= new NWidgetHorizontalLTR();
269 for (int widnum
= WID_OSK_NUMBERS_FIRST
; widnum
<= WID_OSK_NUMBERS_LAST
; widnum
++) {
270 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHBTN
, widnum
, 0x0, biggest_index
);
275 /** Construct the qwerty row keys. */
276 static NWidgetBase
*MakeQwertyKeys(int *biggest_index
)
278 NWidgetHorizontal
*hor
= new NWidgetHorizontalLTR();
280 AddKey(hor
, KEY_PADDING
, 3, WWT_PUSHIMGBTN
, WID_OSK_SPECIAL
, SPR_OSK_SPECIAL
, biggest_index
);
281 for (int widnum
= WID_OSK_QWERTY_FIRST
; widnum
<= WID_OSK_QWERTY_LAST
; widnum
++) {
282 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHBTN
, widnum
, 0x0, biggest_index
);
284 AddKey(hor
, KEY_PADDING
, 1, NWID_SPACER
, 0, 0, biggest_index
);
288 /** Construct the asdfg row keys. */
289 static NWidgetBase
*MakeAsdfgKeys(int *biggest_index
)
291 NWidgetHorizontal
*hor
= new NWidgetHorizontalLTR();
293 AddKey(hor
, KEY_PADDING
, 4, WWT_IMGBTN
, WID_OSK_CAPS
, SPR_OSK_CAPS
, biggest_index
);
294 for (int widnum
= WID_OSK_ASDFG_FIRST
; widnum
<= WID_OSK_ASDFG_LAST
; widnum
++) {
295 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHBTN
, widnum
, 0x0, biggest_index
);
300 /** Construct the zxcvb row keys. */
301 static NWidgetBase
*MakeZxcvbKeys(int *biggest_index
)
303 NWidgetHorizontal
*hor
= new NWidgetHorizontalLTR();
305 AddKey(hor
, KEY_PADDING
, 3, WWT_IMGBTN
, WID_OSK_SHIFT
, SPR_OSK_SHIFT
, biggest_index
);
306 for (int widnum
= WID_OSK_ZXCVB_FIRST
; widnum
<= WID_OSK_ZXCVB_LAST
; widnum
++) {
307 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHBTN
, widnum
, 0x0, biggest_index
);
309 AddKey(hor
, KEY_PADDING
, 1, NWID_SPACER
, 0, 0, biggest_index
);
313 /** Construct the spacebar row keys. */
314 static NWidgetBase
*MakeSpacebarKeys(int *biggest_index
)
316 NWidgetHorizontal
*hor
= new NWidgetHorizontal();
318 AddKey(hor
, KEY_PADDING
, 8, NWID_SPACER
, 0, 0, biggest_index
);
319 AddKey(hor
, KEY_PADDING
, 13, WWT_PUSHTXTBTN
, WID_OSK_SPACE
, STR_EMPTY
, biggest_index
);
320 AddKey(hor
, KEY_PADDING
, 3, NWID_SPACER
, 0, 0, biggest_index
);
321 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHIMGBTN
, WID_OSK_LEFT
, SPR_OSK_LEFT
, biggest_index
);
322 AddKey(hor
, KEY_PADDING
, 2, WWT_PUSHIMGBTN
, WID_OSK_RIGHT
, SPR_OSK_RIGHT
, biggest_index
);
327 static const NWidgetPart _nested_osk_widgets
[] = {
328 NWidget(WWT_CAPTION
, COLOUR_GREY
, WID_OSK_CAPTION
), SetDataTip(STR_WHITE_STRING
, STR_NULL
),
329 NWidget(WWT_PANEL
, COLOUR_GREY
),
330 NWidget(WWT_EDITBOX
, COLOUR_GREY
, WID_OSK_TEXT
), SetMinimalSize(252, 12), SetPadding(2, 2, 2, 2),
332 NWidget(WWT_PANEL
, COLOUR_GREY
), SetPIP(5, 2, 3),
333 NWidgetFunction(MakeTopKeys
), SetPadding(0, 3, 0, 3),
334 NWidgetFunction(MakeNumberKeys
), SetPadding(0, 3, 0, 3),
335 NWidgetFunction(MakeQwertyKeys
), SetPadding(0, 3, 0, 3),
336 NWidgetFunction(MakeAsdfgKeys
), SetPadding(0, 3, 0, 3),
337 NWidgetFunction(MakeZxcvbKeys
), SetPadding(0, 3, 0, 3),
338 NWidgetFunction(MakeSpacebarKeys
), SetPadding(0, 3, 0, 3),
342 static WindowDesc
_osk_desc(
343 WDP_CENTER
, "query_osk", 0, 0,
346 _nested_osk_widgets
, lengthof(_nested_osk_widgets
)
350 * Retrieve keyboard layout from language string or (if set) config file.
351 * Also check for invalid characters.
353 void GetKeyboardLayout()
355 char keyboard
[2][OSK_KEYBOARD_ENTRIES
* 4 + 1];
356 char errormark
[2][OSK_KEYBOARD_ENTRIES
+ 1]; // used for marking invalid chars
357 bool has_error
= false; // true when an invalid char is detected
359 if (_keyboard_opt
[0].empty()) {
360 GetString(keyboard
[0], STR_OSK_KEYBOARD_LAYOUT
, lastof(keyboard
[0]));
362 strecpy(keyboard
[0], _keyboard_opt
[0].c_str(), lastof(keyboard
[0]));
365 if (_keyboard_opt
[1].empty()) {
366 GetString(keyboard
[1], STR_OSK_KEYBOARD_LAYOUT_CAPS
, lastof(keyboard
[1]));
368 strecpy(keyboard
[1], _keyboard_opt
[1].c_str(), lastof(keyboard
[1]));
371 for (uint j
= 0; j
< 2; j
++) {
372 const char *kbd
= keyboard
[j
];
374 for (uint i
= 0; i
< OSK_KEYBOARD_ENTRIES
; i
++) {
375 _keyboard
[j
][i
] = Utf8Consume(&kbd
);
377 /* Be lenient when the last characters are missing (is quite normal) */
378 if (_keyboard
[j
][i
] == '\0' || ended
) {
380 _keyboard
[j
][i
] = ' ';
384 if (IsPrintable(_keyboard
[j
][i
])) {
385 errormark
[j
][i
] = ' ';
388 errormark
[j
][i
] = '^';
389 _keyboard
[j
][i
] = ' ';
395 ShowInfoF("The keyboard layout you selected contains invalid chars. Please check those chars marked with ^.");
396 ShowInfoF("Normal keyboard: %s", keyboard
[0]);
397 ShowInfoF(" %s", errormark
[0]);
398 ShowInfoF("Caps Lock: %s", keyboard
[1]);
399 ShowInfoF(" %s", errormark
[1]);
404 * Show the on-screen keyboard (osk) associated with a given textbox
405 * @param parent pointer to the Window where this keyboard originated from
406 * @param button widget number of parent's textbox
408 void ShowOnScreenKeyboard(Window
*parent
, int button
)
410 CloseWindowById(WC_OSK
, 0);
413 new OskWindow(&_osk_desc
, parent
, button
);
417 * Updates the original text of the OSK so when the 'parent' changes the
418 * original and you press on cancel you won't get the 'old' original text
419 * but the updated one.
420 * @param parent window that just updated its original text
421 * @param button widget number of parent's textbox to update
423 void UpdateOSKOriginalText(const Window
*parent
, int button
)
425 OskWindow
*osk
= dynamic_cast<OskWindow
*>(FindWindowById(WC_OSK
, 0));
426 if (osk
== nullptr || osk
->parent
!= parent
|| osk
->text_btn
!= button
) return;
428 free(osk
->orig_str_buf
);
429 osk
->orig_str_buf
= stredup(osk
->qs
->text
.buf
);
435 * Check whether the OSK is opened for a specific editbox.
436 * @param w Window to check for
437 * @param button Editbox of \a w to check for
438 * @return true if the OSK is opened for \a button.
440 bool IsOSKOpenedFor(const Window
*w
, int button
)
442 OskWindow
*osk
= dynamic_cast<OskWindow
*>(FindWindowById(WC_OSK
, 0));
443 return osk
!= nullptr && osk
->parent
== w
&& osk
->text_btn
== button
;