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