Change: Reword Finance window's Net Profit to Profit
[openttd-github.git] / src / osk_gui.cpp
blob630cd18f2d751f67491ca5e50f5ea0e2d0d932e3
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 WChar _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 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);
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 ~OskWindow()
74 free(this->orig_str_buf);
77 /**
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.
82 void UpdateOskState()
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();
122 this->SetDirty();
124 return;
127 switch (widget) {
128 case WID_OSK_BACKSPACE:
129 if (this->qs->text.DeleteChar(WKC_BACKSPACE)) this->OnEditboxChanged(WID_OSK_TEXT);
130 break;
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.
138 break;
140 case WID_OSK_CAPS:
141 ToggleBit(_keystate, KEYS_CAPS);
142 this->UpdateOskState();
143 this->SetDirty();
144 break;
146 case WID_OSK_SHIFT:
147 ToggleBit(_keystate, KEYS_SHIFT);
148 this->UpdateOskState();
149 this->SetDirty();
150 break;
152 case WID_OSK_SPACE:
153 if (this->qs->text.InsertChar(' ')) this->OnEditboxChanged(WID_OSK_TEXT);
154 break;
156 case WID_OSK_LEFT:
157 if (this->qs->text.MovePos(WKC_LEFT)) this->InvalidateData();
158 break;
160 case WID_OSK_RIGHT:
161 if (this->qs->text.MovePos(WKC_RIGHT)) this->InvalidateData();
162 break;
164 case WID_OSK_OK:
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. */
170 return;
173 this->Close();
174 break;
176 case WID_OSK_CANCEL:
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. */
180 return;
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);
185 this->Close();
187 break;
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();
208 this->Close();
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);
237 hor->Add(spc);
238 } else {
239 if (!hor->IsEmpty()) {
240 NWidgetSpacer *spc = new NWidgetSpacer(INTER_KEY_SPACE, 0);
241 spc->SetMinimalTextLines(1, pad_y, FS_NORMAL);
242 hor->Add(spc);
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);
247 hor->Add(leaf);
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);
261 return hor;
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);
272 return hor;
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);
285 return hor;
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);
297 return hor;
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);
310 return hor;
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);
323 return hor;
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),
331 EndContainer(),
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),
339 EndContainer(),
342 static WindowDesc _osk_desc(
343 WDP_CENTER, "query_osk", 0, 0,
344 WC_OSK, WC_NONE,
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]));
361 } else {
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]));
367 } else {
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];
373 bool ended = false;
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) {
379 ended = true;
380 _keyboard[j][i] = ' ';
381 continue;
384 if (IsPrintable(_keyboard[j][i])) {
385 errormark[j][i] = ' ';
386 } else {
387 has_error = true;
388 errormark[j][i] = '^';
389 _keyboard[j][i] = ' ';
394 if (has_error) {
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);
412 GetKeyboardLayout();
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);
431 osk->SetDirty();
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;