1 /*****************************************************************************
2 * Copyright (c) 2014-2020 OpenRCT2 developers
3 *
4 * For a complete list of all authors, please refer to contributors.md
5 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
6 *
7 * OpenRCT2 is licensed under the GNU General Public License version 3.
8 *****************************************************************************/
9
10 #include "../input/ShortcutManager.h"
11 #include "Window.h"
12
13 #include <openrct2-ui/interface/Widget.h>
14 #include <openrct2/drawing/Drawing.h>
15 #include <openrct2/localisation/Localisation.h>
16 #include <openrct2/sprites.h>
17
18 using namespace OpenRCT2;
19 using namespace OpenRCT2::Ui;
20
21 static constexpr const rct_string_id WINDOW_TITLE = STR_SHORTCUTS_TITLE;
22 static constexpr const int32_t WW = 420;
23 static constexpr const int32_t WH = 280;
24
25 static constexpr const int32_t WW_SC_MAX = 1200;
26 static constexpr const int32_t WH_SC_MAX = 800;
27
28 enum WINDOW_SHORTCUT_WIDGET_IDX
29 {
30 WIDX_BACKGROUND,
31 WIDX_TITLE,
32 WIDX_CLOSE,
33 WIDX_TAB_CONTENT_PANEL,
34 WIDX_SCROLL,
35 WIDX_RESET,
36 WIDX_TAB_0,
37 };
38
39 // clang-format off
40 static rct_widget window_shortcut_widgets[] = {
41 WINDOW_SHIM(WINDOW_TITLE, WW, WH),
42 MakeWidget({0, 43}, {350, 287}, WindowWidgetType::Resize, WindowColour::Secondary),
43 MakeWidget({4, 47}, {412, 245}, WindowWidgetType::Scroll, WindowColour::Primary, SCROLL_VERTICAL, STR_SHORTCUT_LIST_TIP ),
44 MakeWidget({4, WH-15}, {150, 12}, WindowWidgetType::Button, WindowColour::Primary, STR_SHORTCUT_ACTION_RESET, STR_SHORTCUT_ACTION_RESET_TIP),
45 WIDGETS_END,
46 };
47 // clang-format on
48
49 static constexpr const rct_string_id CHANGE_WINDOW_TITLE = STR_SHORTCUT_CHANGE_TITLE;
50 static constexpr const int32_t CHANGE_WW = 250;
51 static constexpr const int32_t CHANGE_WH = 80;
52
53 enum
54 {
55 WIDX_REMOVE = 3
56 };
57
58 // clang-format off
59 static rct_widget window_shortcut_change_widgets[] = {
60 WINDOW_SHIM(CHANGE_WINDOW_TITLE, CHANGE_WW, CHANGE_WH),
61 MakeWidget({ 75, 56 }, { 100, 14 }, WindowWidgetType::Button, WindowColour::Primary, STR_SHORTCUT_REMOVE, STR_SHORTCUT_REMOVE_TIP),
62 WIDGETS_END,
63 };
64 // clang-format on
65
66 class ChangeShortcutWindow final : public Window
67 {
68 private:
69 std::string _shortcutId;
70 rct_string_id _shortcutLocalisedName{};
71 std::string _shortcutCustomName;
72
73 public:
Open(std::string_view shortcutId)74 static ChangeShortcutWindow* Open(std::string_view shortcutId)
75 {
76 auto& shortcutManager = GetShortcutManager();
77 auto registeredShortcut = shortcutManager.GetShortcut(shortcutId);
78 if (registeredShortcut != nullptr)
79 {
80 window_close_by_class(WC_CHANGE_KEYBOARD_SHORTCUT);
81 auto w = WindowCreate<ChangeShortcutWindow>(WC_CHANGE_KEYBOARD_SHORTCUT, CHANGE_WW, CHANGE_WH, WF_CENTRE_SCREEN);
82 if (w != nullptr)
83 {
84 w->_shortcutId = shortcutId;
85 w->_shortcutLocalisedName = registeredShortcut->LocalisedName;
86 w->_shortcutCustomName = registeredShortcut->CustomName;
87 shortcutManager.SetPendingShortcutChange(registeredShortcut->Id);
88 return w;
89 }
90 }
91 return nullptr;
92 }
93
OnOpen()94 void OnOpen() override
95 {
96 widgets = window_shortcut_change_widgets;
97 enabled_widgets = (1ULL << WIDX_CLOSE) | (1ULL << WIDX_REMOVE);
98 WindowInitScrollWidgets(this);
99 }
100
OnClose()101 void OnClose() override
102 {
103 auto& shortcutManager = GetShortcutManager();
104 shortcutManager.SetPendingShortcutChange({});
105 NotifyShortcutKeysWindow();
106 }
107
OnMouseUp(rct_widgetindex widgetIndex)108 void OnMouseUp(rct_widgetindex widgetIndex) override
109 {
110 switch (widgetIndex)
111 {
112 case WIDX_CLOSE:
113 Close();
114 break;
115 case WIDX_REMOVE:
116 Remove();
117 break;
118 }
119 }
120
OnDraw(rct_drawpixelinfo & dpi)121 void OnDraw(rct_drawpixelinfo& dpi) override
122 {
123 DrawWidgets(dpi);
124
125 ScreenCoordsXY stringCoords(windowPos.x + 125, windowPos.y + 30);
126
127 auto ft = Formatter();
128 if (_shortcutCustomName.empty())
129 {
130 ft.Add<rct_string_id>(_shortcutLocalisedName);
131 }
132 else
133 {
134 ft.Add<rct_string_id>(STR_STRING);
135 ft.Add<const char*>(_shortcutCustomName.c_str());
136 }
137 DrawTextWrapped(&dpi, stringCoords, 242, STR_SHORTCUT_CHANGE_PROMPT, ft, { TextAlignment::CENTRE });
138 }
139
140 private:
141 void NotifyShortcutKeysWindow();
142
Remove()143 void Remove()
144 {
145 auto& shortcutManager = GetShortcutManager();
146 auto* shortcut = shortcutManager.GetShortcut(_shortcutId);
147 if (shortcut != nullptr)
148 {
149 shortcut->Current.clear();
150 shortcutManager.SaveUserBindings();
151 }
152 Close();
153 }
154 };
155
156 class ShortcutKeysWindow final : public Window
157 {
158 private:
159 struct ShortcutStringPair
160 {
161 std::string ShortcutId;
162 rct_string_id StringId = STR_NONE;
163 std::string CustomString;
164 std::string Binding;
165 };
166
167 struct ShortcutTabDesc
168 {
169 std::string_view IdGroup;
170 uint32_t ImageId;
171 uint32_t ImageDivisor;
172 uint32_t ImageNumFrames;
173 };
174
175 std::vector<ShortcutTabDesc> _tabs;
176 std::vector<rct_widget> _widgets;
177 std::vector<ShortcutStringPair> _list;
178 std::optional<size_t> _highlightedItem;
179 size_t _currentTabIndex{};
180 uint32_t _tabAnimationIndex{};
181
182 public:
OnOpen()183 void OnOpen() override
184 {
185 InitialiseTabs();
186 InitialiseWidgets();
187 InitialiseList();
188
189 min_width = WW;
190 min_height = WH;
191 max_width = WW_SC_MAX;
192 max_height = WH_SC_MAX;
193 }
194
OnResize()195 void OnResize() override
196 {
197 window_set_resize(this, min_width, min_height, max_width, max_height);
198 }
199
OnUpdate()200 void OnUpdate() override
201 {
202 _tabAnimationIndex++;
203 InvalidateWidget(static_cast<rct_widgetindex>(WIDX_TAB_0 + _currentTabIndex));
204 }
205
OnMouseUp(rct_widgetindex widgetIndex)206 void OnMouseUp(rct_widgetindex widgetIndex) override
207 {
208 switch (widgetIndex)
209 {
210 case WIDX_CLOSE:
211 Close();
212 break;
213 case WIDX_RESET:
214 ResetAll();
215 break;
216 default:
217 {
218 auto tabIndex = static_cast<size_t>(widgetIndex - WIDX_TAB_0);
219 if (tabIndex < _tabs.size())
220 {
221 SetTab(tabIndex);
222 }
223 }
224 }
225 }
226
OnPrepareDraw()227 void OnPrepareDraw() override
228 {
229 widgets[WIDX_BACKGROUND].right = width - 1;
230 widgets[WIDX_BACKGROUND].bottom = height - 1;
231 widgets[WIDX_TITLE].right = width - 2;
232 widgets[WIDX_CLOSE].right = width - 3;
233 widgets[WIDX_CLOSE].left = width - 13;
234 widgets[WIDX_TAB_CONTENT_PANEL].right = width - 1;
235 widgets[WIDX_TAB_CONTENT_PANEL].bottom = height - 1;
236 widgets[WIDX_SCROLL].right = width - 5;
237 widgets[WIDX_SCROLL].bottom = height - 19;
238 widgets[WIDX_RESET].top = height - 16;
239 widgets[WIDX_RESET].bottom = height - 5;
240 window_align_tabs(this, WIDX_TAB_0, static_cast<rct_widgetindex>(WIDX_TAB_0 + _tabs.size()));
241
242 // Set selected tab
243 for (size_t i = 0; i < _tabs.size(); i++)
244 {
245 SetWidgetPressed(static_cast<rct_widgetindex>(WIDX_TAB_0 + i), false);
246 }
247 SetWidgetPressed(static_cast<rct_widgetindex>(WIDX_TAB_0 + _currentTabIndex), true);
248 }
249
OnDraw(rct_drawpixelinfo & dpi)250 void OnDraw(rct_drawpixelinfo& dpi) override
251 {
252 DrawWidgets(dpi);
253 DrawTabImages(dpi);
254 }
255
OnScrollGetSize(int32_t scrollIndex)256 ScreenSize OnScrollGetSize(int32_t scrollIndex) override
257 {
258 auto h = static_cast<int32_t>(_list.size() * SCROLLABLE_ROW_HEIGHT);
259 auto bottom = std::max(0, h - widgets[WIDX_SCROLL].bottom + widgets[WIDX_SCROLL].top + 21);
260 if (bottom < scrolls[0].v_top)
261 {
262 scrolls[0].v_top = bottom;
263 Invalidate();
264 }
265 return { 0, h };
266 }
267
OnScrollMouseOver(int32_t scrollIndex,const ScreenCoordsXY & screenCoords)268 void OnScrollMouseOver(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override
269 {
270 auto index = static_cast<size_t>((screenCoords.y - 1) / SCROLLABLE_ROW_HEIGHT);
271 if (index < _list.size())
272 {
273 _highlightedItem = index;
274 Invalidate();
275 }
276 }
277
OnScrollMouseDown(int32_t scrollIndex,const ScreenCoordsXY & screenCoords)278 void OnScrollMouseDown(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override
279 {
280 auto selectedItem = static_cast<size_t>((screenCoords.y - 1) / SCROLLABLE_ROW_HEIGHT);
281 if (selectedItem < _list.size())
282 {
283 // Is this a separator?
284 if (!_list[selectedItem].ShortcutId.empty())
285 {
286 auto& shortcut = _list[selectedItem];
287 ChangeShortcutWindow::Open(shortcut.ShortcutId);
288 }
289 }
290 }
291
OnScrollDraw(int32_t scrollIndex,rct_drawpixelinfo & dpi)292 void OnScrollDraw(int32_t scrollIndex, rct_drawpixelinfo& dpi) override
293 {
294 auto dpiCoords = ScreenCoordsXY{ dpi.x, dpi.y };
295 gfx_fill_rect(
296 &dpi, { dpiCoords, dpiCoords + ScreenCoordsXY{ dpi.width - 1, dpi.height - 1 } }, ColourMapA[colours[1]].mid_light);
297
298 // TODO: the line below is a workaround for what is presumably a bug with dpi->width
299 // see https://github.com/OpenRCT2/OpenRCT2/issues/11238 for details
300 const auto scrollWidth = width - SCROLLBAR_WIDTH - 10;
301
302 for (size_t i = 0; i < _list.size(); ++i)
303 {
304 auto y = static_cast<int32_t>(1 + i * SCROLLABLE_ROW_HEIGHT);
305 if (y > dpi.y + dpi.height)
306 {
307 break;
308 }
309
310 if (y + SCROLLABLE_ROW_HEIGHT < dpi.y)
311 {
312 continue;
313 }
314
315 // Is this a separator?
316 if (_list[i].ShortcutId.empty())
317 {
318 DrawSeparator(dpi, y, scrollWidth);
319 }
320 else
321 {
322 auto isHighlighted = _highlightedItem == i;
323 DrawItem(dpi, y, scrollWidth, _list[i], isHighlighted);
324 }
325 }
326 }
327
RefreshBindings()328 void RefreshBindings()
329 {
330 InitialiseList();
331 }
332
333 private:
IsInCurrentTab(const RegisteredShortcut & shortcut)334 bool IsInCurrentTab(const RegisteredShortcut& shortcut)
335 {
336 auto groupFilter = _tabs[_currentTabIndex].IdGroup;
337 auto group = shortcut.GetTopLevelGroup();
338 if (groupFilter.empty())
339 {
340 // Check it doesn't belong in any other tab
341 for (const auto& tab : _tabs)
342 {
343 if (!tab.IdGroup.empty())
344 {
345 if (tab.IdGroup == group)
346 {
347 return false;
348 }
349 }
350 }
351 return true;
352 }
353
354 return group == groupFilter;
355 }
356
InitialiseList()357 void InitialiseList()
358 {
359 // Get shortcuts and sort by group
360 auto shortcuts = GetShortcutsForCurrentTab();
361 std::stable_sort(shortcuts.begin(), shortcuts.end(), [](const RegisteredShortcut* a, const RegisteredShortcut* b) {
362 return a->GetGroup().compare(b->GetGroup()) < 0;
363 });
364
365 // Create list items with a separator between each group
366 _list.clear();
367 size_t index = 0;
368 std::string group;
369 for (const auto* shortcut : shortcuts)
370 {
371 if (group.empty())
372 {
373 group = shortcut->GetGroup();
374 }
375 else
376 {
377 auto groupName = shortcut->GetGroup();
378 if (group != groupName)
379 {
380 // Add separator
381 group = groupName;
382 _list.emplace_back();
383 }
384 }
385
386 ShortcutStringPair ssp;
387 ssp.ShortcutId = shortcut->Id;
388 ssp.StringId = shortcut->LocalisedName;
389 ssp.CustomString = shortcut->CustomName;
390 ssp.Binding = shortcut->GetDisplayString();
391 _list.push_back(std::move(ssp));
392 index++;
393 }
394
395 Invalidate();
396 }
397
GetShortcutsForCurrentTab()398 std::vector<const RegisteredShortcut*> GetShortcutsForCurrentTab()
399 {
400 std::vector<const RegisteredShortcut*> result;
401 auto& shortcutManager = GetShortcutManager();
402 for (const auto& shortcut : shortcutManager.Shortcuts)
403 {
404 if (IsInCurrentTab(shortcut.second))
405 {
406 result.push_back(&shortcut.second);
407 }
408 }
409 return result;
410 }
411
InitialiseTabs()412 void InitialiseTabs()
413 {
414 _tabs.clear();
415 _tabs.push_back({ "interface", SPR_TAB_GEARS_0, 2, 4 });
416 _tabs.push_back({ "view", SPR_G2_VIEW, 0, 0 });
417 _tabs.push_back({ "window", SPR_TAB_PARK_ENTRANCE, 0, 0 });
418 _tabs.push_back({ {}, SPR_TAB_WRENCH_0, 2, 16 });
419 }
420
InitialiseWidgets()421 void InitialiseWidgets()
422 {
423 enabled_widgets = (1ULL << WIDX_CLOSE) | (1ULL << WIDX_RESET);
424
425 _widgets.clear();
426 _widgets.insert(_widgets.begin(), std::begin(window_shortcut_widgets), std::end(window_shortcut_widgets) - 1);
427
428 int32_t x = 3;
429 for (size_t i = 0; i < _tabs.size(); i++)
430 {
431 auto tab = MakeTab({ x, 17 }, STR_NONE);
432 _widgets.push_back(tab);
433 x += 31;
434
435 enabled_widgets |= (1ULL << (WIDX_TAB_0 + i));
436 }
437
438 _widgets.push_back(WIDGETS_END);
439 widgets = _widgets.data();
440
441 WindowInitScrollWidgets(this);
442 }
443
SetTab(size_t index)444 void SetTab(size_t index)
445 {
446 if (_currentTabIndex != index)
447 {
448 _currentTabIndex = index;
449 _tabAnimationIndex = 0;
450 InitialiseList();
451 }
452 }
453
ResetAll()454 void ResetAll()
455 {
456 auto& shortcutManager = GetShortcutManager();
457 for (const auto& item : _list)
458 {
459 auto shortcut = shortcutManager.GetShortcut(item.ShortcutId);
460 if (shortcut != nullptr)
461 {
462 shortcut->Current = shortcut->Default;
463 }
464 }
465 shortcutManager.SaveUserBindings();
466 RefreshBindings();
467 }
468
DrawTabImages(rct_drawpixelinfo & dpi) const469 void DrawTabImages(rct_drawpixelinfo& dpi) const
470 {
471 for (size_t i = 0; i < _tabs.size(); i++)
472 {
473 DrawTabImage(dpi, i);
474 }
475 }
476
DrawTabImage(rct_drawpixelinfo & dpi,size_t tabIndex) const477 void DrawTabImage(rct_drawpixelinfo& dpi, size_t tabIndex) const
478 {
479 const auto& tabDesc = _tabs[tabIndex];
480 auto widgetIndex = static_cast<rct_widgetindex>(WIDX_TAB_0 + tabIndex);
481 if (!IsWidgetDisabled(widgetIndex))
482 {
483 auto imageId = tabDesc.ImageId;
484 if (imageId != 0)
485 {
486 if (tabIndex == _currentTabIndex && tabDesc.ImageDivisor != 0 && tabDesc.ImageNumFrames != 0)
487 {
488 auto frame = _tabAnimationIndex / tabDesc.ImageDivisor;
489 imageId += frame % tabDesc.ImageNumFrames;
490 }
491
492 const auto& widget = widgets[widgetIndex];
493 gfx_draw_sprite(&dpi, ImageId(imageId), windowPos + ScreenCoordsXY{ widget.left, widget.top });
494 }
495 }
496 }
497
DrawSeparator(rct_drawpixelinfo & dpi,int32_t y,int32_t scrollWidth)498 void DrawSeparator(rct_drawpixelinfo& dpi, int32_t y, int32_t scrollWidth)
499 {
500 const int32_t top = y + (SCROLLABLE_ROW_HEIGHT / 2) - 1;
501 gfx_fill_rect(&dpi, { { 0, top }, { scrollWidth, top } }, ColourMapA[colours[0]].mid_dark);
502 gfx_fill_rect(&dpi, { { 0, top + 1 }, { scrollWidth, top + 1 } }, ColourMapA[colours[0]].lightest);
503 }
504
DrawItem(rct_drawpixelinfo & dpi,int32_t y,int32_t scrollWidth,const ShortcutStringPair & shortcut,bool isHighlighted)505 void DrawItem(
506 rct_drawpixelinfo& dpi, int32_t y, int32_t scrollWidth, const ShortcutStringPair& shortcut, bool isHighlighted)
507 {
508 auto format = STR_BLACK_STRING;
509 if (isHighlighted)
510 {
511 format = STR_WINDOW_COLOUR_2_STRINGID;
512 gfx_filter_rect(&dpi, { 0, y - 1, scrollWidth, y + (SCROLLABLE_ROW_HEIGHT - 2) }, FilterPaletteID::PaletteDarken1);
513 }
514
515 auto bindingOffset = (scrollWidth * 2) / 3;
516 auto ft = Formatter();
517 ft.Add<rct_string_id>(STR_SHORTCUT_ENTRY_FORMAT);
518 if (shortcut.CustomString.empty())
519 {
520 ft.Add<rct_string_id>(shortcut.StringId);
521 }
522 else
523 {
524 ft.Add<rct_string_id>(STR_STRING);
525 ft.Add<const char*>(shortcut.CustomString.c_str());
526 }
527 DrawTextEllipsised(&dpi, { 0, y - 1 }, bindingOffset, format, ft);
528
529 if (!shortcut.Binding.empty())
530 {
531 ft = Formatter();
532 ft.Add<rct_string_id>(STR_STRING);
533 ft.Add<const char*>(shortcut.Binding.c_str());
534 DrawTextEllipsised(&dpi, { bindingOffset, y - 1 }, 150, format, ft);
535 }
536 }
537 };
538
NotifyShortcutKeysWindow()539 void ChangeShortcutWindow::NotifyShortcutKeysWindow()
540 {
541 auto w = window_find_by_class(WC_KEYBOARD_SHORTCUT_LIST);
542 if (w != nullptr)
543 {
544 static_cast<ShortcutKeysWindow*>(w)->RefreshBindings();
545 }
546 }
547
window_shortcut_keys_open()548 rct_window* window_shortcut_keys_open()
549 {
550 auto w = window_bring_to_front_by_class(WC_KEYBOARD_SHORTCUT_LIST);
551 if (w == nullptr)
552 {
553 w = WindowCreate<ShortcutKeysWindow>(WC_KEYBOARD_SHORTCUT_LIST, WW, WH, WF_RESIZABLE);
554 }
555 return w;
556 }
557