Fix #10490: Allow ships to exit depots if another is not moving at the exit point...
[openttd-github.git] / src / osk_gui.cpp
blob00d311ff24d40bc05e11e0fa8e7869d5276dc9e0
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 osk_gui.cpp The On Screen Keyboard GUI */
10 #include "stdafx.h"
11 #include "string_func.h"
12 #include "strings_func.h"
13 #include "debug.h"
14 #include "window_func.h"
15 #include "gfx_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];
30 enum KeyStateBits {
31 KEYS_NONE,
32 KEYS_SHIFT,
33 KEYS_CAPS
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 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;
63 this->InitNested(0);
64 this->SetFocusedWidget(WID_OSK_TEXT);
66 /* Not needed by default. */
67 this->DisableWidget(WID_OSK_SPECIAL);
69 this->UpdateOskState();
72 /**
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.
77 void UpdateOskState()
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();
117 this->SetDirty();
119 return;
122 switch (widget) {
123 case WID_OSK_BACKSPACE:
124 if (this->qs->text.DeleteChar(WKC_BACKSPACE)) this->OnEditboxChanged(WID_OSK_TEXT);
125 break;
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.
133 break;
135 case WID_OSK_CAPS:
136 ToggleBit(_keystate, KEYS_CAPS);
137 this->UpdateOskState();
138 this->SetDirty();
139 break;
141 case WID_OSK_SHIFT:
142 ToggleBit(_keystate, KEYS_SHIFT);
143 this->UpdateOskState();
144 this->SetDirty();
145 break;
147 case WID_OSK_SPACE:
148 if (this->qs->text.InsertChar(' ')) this->OnEditboxChanged(WID_OSK_TEXT);
149 break;
151 case WID_OSK_LEFT:
152 if (this->qs->text.MovePos(WKC_LEFT)) this->InvalidateData();
153 break;
155 case WID_OSK_RIGHT:
156 if (this->qs->text.MovePos(WKC_RIGHT)) this->InvalidateData();
157 break;
159 case WID_OSK_OK:
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. */
165 return;
168 this->Close();
169 break;
171 case WID_OSK_CANCEL:
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. */
175 return;
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);
180 this->Close();
182 break;
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 if (!hor->IsEmpty()) key_width += INTER_KEY_SPACE;
231 auto spc = std::make_unique<NWidgetSpacer>(key_width, 0);
232 spc->SetMinimalTextLines(1, pad_y, FS_NORMAL);
233 hor->Add(std::move(spc));
234 } else {
235 if (!hor->IsEmpty()) {
236 auto spc = std::make_unique<NWidgetSpacer>(INTER_KEY_SPACE, 0);
237 spc->SetMinimalTextLines(1, pad_y, FS_NORMAL);
238 hor->Add(std::move(spc));
240 auto leaf = std::make_unique<NWidgetLeaf>(widtype, COLOUR_GREY, widnum, widdata, STR_NULL);
241 leaf->SetMinimalSize(key_width, 0);
242 leaf->SetMinimalTextLines(1, pad_y, FS_NORMAL);
243 hor->Add(std::move(leaf));
247 /** Construct the top row keys (cancel, ok, backspace). */
248 static std::unique_ptr<NWidgetBase> MakeTopKeys()
250 auto hor = std::make_unique<NWidgetHorizontal>();
252 AddKey(hor, TOP_KEY_PADDING, 6 * 2, WWT_TEXTBTN, WID_OSK_CANCEL, STR_BUTTON_CANCEL);
253 AddKey(hor, TOP_KEY_PADDING, 6 * 2, WWT_TEXTBTN, WID_OSK_OK, STR_BUTTON_OK );
254 AddKey(hor, TOP_KEY_PADDING, 2 * 2, WWT_PUSHIMGBTN, WID_OSK_BACKSPACE, SPR_OSK_BACKSPACE);
255 return hor;
258 /** Construct the row containing the digit keys. */
259 static std::unique_ptr<NWidgetBase> MakeNumberKeys()
261 std::unique_ptr<NWidgetHorizontal> hor = std::make_unique<NWidgetHorizontalLTR>();
263 for (WidgetID widnum = WID_OSK_NUMBERS_FIRST; widnum <= WID_OSK_NUMBERS_LAST; widnum++) {
264 AddKey(hor, KEY_PADDING, 2, WWT_PUSHBTN, widnum, 0x0);
266 return hor;
269 /** Construct the qwerty row keys. */
270 static std::unique_ptr<NWidgetBase> MakeQwertyKeys()
272 std::unique_ptr<NWidgetHorizontal> hor = std::make_unique<NWidgetHorizontalLTR>();
274 AddKey(hor, KEY_PADDING, 3, WWT_PUSHIMGBTN, WID_OSK_SPECIAL, SPR_OSK_SPECIAL);
275 for (WidgetID widnum = WID_OSK_QWERTY_FIRST; widnum <= WID_OSK_QWERTY_LAST; widnum++) {
276 AddKey(hor, KEY_PADDING, 2, WWT_PUSHBTN, widnum, 0x0);
278 AddKey(hor, KEY_PADDING, 1, NWID_SPACER, 0, 0);
279 return hor;
282 /** Construct the asdfg row keys. */
283 static std::unique_ptr<NWidgetBase> MakeAsdfgKeys()
285 std::unique_ptr<NWidgetHorizontal> hor = std::make_unique<NWidgetHorizontalLTR>();
287 AddKey(hor, KEY_PADDING, 4, WWT_IMGBTN, WID_OSK_CAPS, SPR_OSK_CAPS);
288 for (WidgetID widnum = WID_OSK_ASDFG_FIRST; widnum <= WID_OSK_ASDFG_LAST; widnum++) {
289 AddKey(hor, KEY_PADDING, 2, WWT_PUSHBTN, widnum, 0x0);
291 return hor;
294 /** Construct the zxcvb row keys. */
295 static std::unique_ptr<NWidgetBase> MakeZxcvbKeys()
297 std::unique_ptr<NWidgetHorizontal> hor = std::make_unique<NWidgetHorizontalLTR>();
299 AddKey(hor, KEY_PADDING, 3, WWT_IMGBTN, WID_OSK_SHIFT, SPR_OSK_SHIFT);
300 for (WidgetID widnum = WID_OSK_ZXCVB_FIRST; widnum <= WID_OSK_ZXCVB_LAST; widnum++) {
301 AddKey(hor, KEY_PADDING, 2, WWT_PUSHBTN, widnum, 0x0);
303 AddKey(hor, KEY_PADDING, 1, NWID_SPACER, 0, 0);
304 return hor;
307 /** Construct the spacebar row keys. */
308 static std::unique_ptr<NWidgetBase> MakeSpacebarKeys()
310 auto hor = std::make_unique<NWidgetHorizontal>();
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);
317 return hor;
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, 12), SetPadding(2, 2, 2, 2),
325 EndContainer(),
326 NWidget(WWT_PANEL, COLOUR_GREY), SetPIP(5, 2, 3),
327 NWidgetFunction(MakeTopKeys), SetPadding(0, 3, 0, 3),
328 NWidgetFunction(MakeNumberKeys), SetPadding(0, 3, 0, 3),
329 NWidgetFunction(MakeQwertyKeys), SetPadding(0, 3, 0, 3),
330 NWidgetFunction(MakeAsdfgKeys), SetPadding(0, 3, 0, 3),
331 NWidgetFunction(MakeZxcvbKeys), SetPadding(0, 3, 0, 3),
332 NWidgetFunction(MakeSpacebarKeys), SetPadding(0, 3, 0, 3),
333 EndContainer(),
336 static WindowDesc _osk_desc(__FILE__, __LINE__,
337 WDP_CENTER, nullptr, 0, 0,
338 WC_OSK, WC_NONE,
340 std::begin(_nested_osk_widgets), std::end(_nested_osk_widgets)
344 * Retrieve keyboard layout from language string or (if set) config file.
345 * Also check for invalid characters.
347 void GetKeyboardLayout()
349 std::string keyboard[2];
350 std::string errormark[2]; // used for marking invalid chars
351 bool has_error = false; // true when an invalid char is detected
353 keyboard[0] = _keyboard_opt[0].empty() ? GetString(STR_OSK_KEYBOARD_LAYOUT) : _keyboard_opt[0];
354 keyboard[1] = _keyboard_opt[1].empty() ? GetString(STR_OSK_KEYBOARD_LAYOUT_CAPS) : _keyboard_opt[1];
356 for (uint j = 0; j < 2; j++) {
357 auto kbd = keyboard[j].begin();
358 bool ended = false;
359 for (uint i = 0; i < OSK_KEYBOARD_ENTRIES; i++) {
360 _keyboard[j][i] = Utf8Consume(kbd);
362 /* Be lenient when the last characters are missing (is quite normal) */
363 if (_keyboard[j][i] == '\0' || ended) {
364 ended = true;
365 _keyboard[j][i] = ' ';
366 continue;
369 if (IsPrintable(_keyboard[j][i])) {
370 errormark[j] += ' ';
371 } else {
372 has_error = true;
373 errormark[j] += '^';
374 _keyboard[j][i] = ' ';
379 if (has_error) {
380 ShowInfo("The keyboard layout you selected contains invalid chars. Please check those chars marked with ^.");
381 ShowInfo("Normal keyboard: {}", keyboard[0]);
382 ShowInfo(" {}", errormark[0]);
383 ShowInfo("Caps Lock: {}", keyboard[1]);
384 ShowInfo(" {}", errormark[1]);
389 * Show the on-screen keyboard (osk) associated with a given textbox
390 * @param parent pointer to the Window where this keyboard originated from
391 * @param button widget number of parent's textbox
393 void ShowOnScreenKeyboard(Window *parent, WidgetID button)
395 CloseWindowById(WC_OSK, 0);
397 GetKeyboardLayout();
398 new OskWindow(&_osk_desc, parent, button);
402 * Updates the original text of the OSK so when the 'parent' changes the
403 * original and you press on cancel you won't get the 'old' original text
404 * but the updated one.
405 * @param parent window that just updated its original text
406 * @param button widget number of parent's textbox to update
408 void UpdateOSKOriginalText(const Window *parent, WidgetID button)
410 OskWindow *osk = dynamic_cast<OskWindow *>(FindWindowById(WC_OSK, 0));
411 if (osk == nullptr || osk->parent != parent || osk->text_btn != button) return;
413 osk->orig_str = osk->qs->text.buf;
415 osk->SetDirty();
419 * Check whether the OSK is opened for a specific editbox.
420 * @param w Window to check for
421 * @param button Editbox of \a w to check for
422 * @return true if the OSK is opened for \a button.
424 bool IsOSKOpenedFor(const Window *w, WidgetID button)
426 OskWindow *osk = dynamic_cast<OskWindow *>(FindWindowById(WC_OSK, 0));
427 return osk != nullptr && osk->parent == w && osk->text_btn == button;