Update: Translations from eints
[openttd-github.git] / src / dropdown.cpp
blob3fde9ef74fee5060e42ae57558df66799a1093f0
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 dropdown.cpp Implementation of the dropdown widget. */
10 #include "stdafx.h"
11 #include "dropdown_type.h"
12 #include "dropdown_func.h"
13 #include "dropdown_common_type.h"
14 #include "strings_func.h"
15 #include "timer/timer.h"
16 #include "timer/timer_window.h"
17 #include "window_gui.h"
18 #include "window_func.h"
19 #include "zoom_func.h"
21 #include "widgets/dropdown_widget.h"
23 #include "safeguards.h"
25 std::unique_ptr<DropDownListItem> MakeDropDownListDividerItem()
27 return std::make_unique<DropDownListDividerItem>(-1);
30 std::unique_ptr<DropDownListItem> MakeDropDownListStringItem(StringID str, int value, bool masked, bool shaded)
32 return std::make_unique<DropDownListStringItem>(str, value, masked, shaded);
35 std::unique_ptr<DropDownListItem> MakeDropDownListStringItem(const std::string &str, int value, bool masked, bool shaded)
37 return std::make_unique<DropDownListStringItem>(str, value, masked, shaded);
40 std::unique_ptr<DropDownListItem> MakeDropDownListIconItem(SpriteID sprite, PaletteID palette, StringID str, int value, bool masked, bool shaded)
42 return std::make_unique<DropDownListIconItem>(sprite, palette, str, value, masked, shaded);
45 std::unique_ptr<DropDownListItem> MakeDropDownListIconItem(const Dimension &dim, SpriteID sprite, PaletteID palette, StringID str, int value, bool masked, bool shaded)
47 return std::make_unique<DropDownListIconItem>(dim, sprite, palette, str, value, masked, shaded);
50 std::unique_ptr<DropDownListItem> MakeDropDownListCheckedItem(bool checked, StringID str, int value, bool masked, bool shaded)
52 return std::make_unique<DropDownListCheckedItem>(checked, str, value, masked, shaded);
55 static constexpr NWidgetPart _nested_dropdown_menu_widgets[] = {
56 NWidget(NWID_HORIZONTAL),
57 NWidget(WWT_PANEL, COLOUR_END, WID_DM_ITEMS), SetScrollbar(WID_DM_SCROLL), EndContainer(),
58 NWidget(NWID_SELECTION, INVALID_COLOUR, WID_DM_SHOW_SCROLL),
59 NWidget(NWID_VSCROLLBAR, COLOUR_END, WID_DM_SCROLL),
60 EndContainer(),
61 EndContainer(),
64 static WindowDesc _dropdown_desc(
65 WDP_MANUAL, nullptr, 0, 0,
66 WC_DROPDOWN_MENU, WC_NONE,
67 WDF_NO_FOCUS,
68 _nested_dropdown_menu_widgets
71 /** Drop-down menu window */
72 struct DropdownWindow : Window {
73 WidgetID parent_button; ///< Parent widget number where the window is dropped from.
74 Rect wi_rect; ///< Rect of the button that opened the dropdown.
75 DropDownList list; ///< List with dropdown menu items.
76 int selected_result; ///< Result value of the selected item in the list.
77 uint8_t click_delay = 0; ///< Timer to delay selection.
78 bool drag_mode = true;
79 bool instant_close; ///< Close the window when the mouse button is raised.
80 bool persist; ///< Persist dropdown menu.
81 int scrolling = 0; ///< If non-zero, auto-scroll the item list (one time).
82 Point position; ///< Position of the topleft corner of the window.
83 Scrollbar *vscroll;
85 Dimension items_dim; ///< Calculated cropped and padded dimension for the items widget.
87 /**
88 * Create a dropdown menu.
89 * @param parent Parent window.
90 * @param list Dropdown item list.
91 * @param selected Initial selected result of the list.
92 * @param button Widget of the parent window doing the dropdown.
93 * @param wi_rect Rect of the button that opened the dropdown.
94 * @param instant_close Close the window when the mouse button is raised.
95 * @param wi_colour Colour of the parent widget.
96 * @param persist Dropdown menu will persist.
98 DropdownWindow(Window *parent, DropDownList &&list, int selected, WidgetID button, const Rect wi_rect, bool instant_close, Colours wi_colour, bool persist)
99 : Window(_dropdown_desc)
100 , parent_button(button)
101 , wi_rect(wi_rect)
102 , list(std::move(list))
103 , selected_result(selected)
104 , instant_close(instant_close)
105 , persist(persist)
107 assert(!this->list.empty());
109 this->parent = parent;
111 this->CreateNestedTree();
113 this->GetWidget<NWidgetCore>(WID_DM_ITEMS)->colour = wi_colour;
114 this->GetWidget<NWidgetCore>(WID_DM_SCROLL)->colour = wi_colour;
115 this->vscroll = this->GetScrollbar(WID_DM_SCROLL);
116 this->UpdateSizeAndPosition();
118 this->FinishInitNested(0);
119 CLRBITS(this->flags, WF_WHITE_BORDER);
122 void Close([[maybe_unused]] int data = 0) override
124 /* Finish closing the dropdown, so it doesn't affect new window placement.
125 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
126 this->Window::Close();
128 Point pt = _cursor.pos;
129 pt.x -= this->parent->left;
130 pt.y -= this->parent->top;
131 this->parent->OnDropdownClose(pt, this->parent_button, this->selected_result, this->instant_close);
133 /* Set flag on parent widget to indicate that we have just closed. */
134 NWidgetCore *nwc = this->parent->GetWidget<NWidgetCore>(this->parent_button);
135 if (nwc != nullptr) SetBit(nwc->disp_flags, NDB_DROPDOWN_CLOSED);
138 void OnFocusLost(bool closing) override
140 if (!closing) {
141 this->instant_close = false;
142 this->Close();
147 * Fit dropdown list into available height, rounding to average item size. Width is adjusted if scrollbar is present.
148 * @param[in,out] desired Desired dimensions of dropdown list.
149 * @param list Dimensions of the list itself, without padding or cropping.
150 * @param available_height Available height to fit list within.
152 void FitAvailableHeight(Dimension &desired, const Dimension &list, uint available_height)
154 if (desired.height < available_height) return;
156 /* If the dropdown doesn't fully fit, we a need a dropdown. */
157 uint avg_height = list.height / (uint)this->list.size();
158 uint rows = std::max((available_height - WidgetDimensions::scaled.dropdownlist.Vertical()) / avg_height, 1U);
160 desired.width = std::max(list.width, desired.width - NWidgetScrollbar::GetVerticalDimension().width);
161 desired.height = rows * avg_height + WidgetDimensions::scaled.dropdownlist.Vertical();
165 * Update size and position of window to fit dropdown list into available space.
167 void UpdateSizeAndPosition()
169 Rect button_rect = this->wi_rect.Translate(this->parent->left, this->parent->top);
171 /* Get the dimensions required for the list. */
172 Dimension list_dim = GetDropDownListDimension(this->list);
174 /* Set up dimensions for the items widget. */
175 Dimension widget_dim = list_dim;
176 widget_dim.width += WidgetDimensions::scaled.dropdownlist.Horizontal();
177 widget_dim.height += WidgetDimensions::scaled.dropdownlist.Vertical();
179 /* Width should match at least the width of the parent widget. */
180 widget_dim.width = std::max<uint>(widget_dim.width, button_rect.Width());
182 /* Available height below (or above, if the dropdown is placed above the widget). */
183 uint available_height_below = std::max(GetMainViewBottom() - button_rect.bottom - 1, 0);
184 uint available_height_above = std::max(button_rect.top - 1 - GetMainViewTop(), 0);
186 /* Is it better to place the dropdown above the widget? */
187 if (widget_dim.height > available_height_below && available_height_above > available_height_below) {
188 FitAvailableHeight(widget_dim, list_dim, available_height_above);
189 this->position.y = button_rect.top - widget_dim.height;
190 } else {
191 FitAvailableHeight(widget_dim, list_dim, available_height_below);
192 this->position.y = button_rect.bottom + 1;
195 if (_current_text_dir == TD_RTL) {
196 /* In case the list is wider than the parent button, the list should be right aligned to the button and overflow to the left. */
197 this->position.x = button_rect.right + 1 - (int)(widget_dim.width + (list_dim.height > widget_dim.height ? NWidgetScrollbar::GetVerticalDimension().width : 0));
198 } else {
199 this->position.x = button_rect.left;
202 this->items_dim = widget_dim;
203 this->GetWidget<NWidgetStacked>(WID_DM_SHOW_SCROLL)->SetDisplayedPlane(list_dim.height > widget_dim.height ? 0 : SZSP_NONE);
205 /* Capacity is the average number of items visible */
206 this->vscroll->SetCapacity((widget_dim.height - WidgetDimensions::scaled.dropdownlist.Vertical()) * this->list.size() / list_dim.height);
207 this->vscroll->SetCount(this->list.size());
209 /* If the dropdown is positioned above the parent widget, start selection at the bottom. */
210 if (this->position.y < button_rect.top && list_dim.height > widget_dim.height) this->vscroll->UpdatePosition(INT_MAX);
213 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
215 if (widget == WID_DM_ITEMS) size = this->items_dim;
218 Point OnInitialPosition([[maybe_unused]] int16_t sm_width, [[maybe_unused]] int16_t sm_height, [[maybe_unused]] int window_number) override
220 return this->position;
224 * Find the dropdown item under the cursor.
225 * @param[out] value Selected item, if function returns \c true.
226 * @return Cursor points to a dropdown item.
228 bool GetDropDownItem(int &value)
230 if (GetWidgetFromPos(this, _cursor.pos.x - this->left, _cursor.pos.y - this->top) < 0) return false;
232 const Rect &r = this->GetWidget<NWidgetBase>(WID_DM_ITEMS)->GetCurrentRect().Shrink(WidgetDimensions::scaled.dropdownlist);
233 int y = _cursor.pos.y - this->top - r.top;
234 int pos = this->vscroll->GetPosition();
236 for (const auto &item : this->list) {
237 /* Skip items that are scrolled up */
238 if (--pos >= 0) continue;
240 int item_height = item->Height();
242 if (y < item_height) {
243 if (item->masked || !item->Selectable()) return false;
244 value = item->result;
245 return true;
248 y -= item_height;
251 return false;
254 void DrawWidget(const Rect &r, WidgetID widget) const override
256 if (widget != WID_DM_ITEMS) return;
258 Colours colour = this->GetWidget<NWidgetCore>(widget)->colour;
260 Rect ir = r.Shrink(WidgetDimensions::scaled.dropdownlist);
261 int y = ir.top;
262 int pos = this->vscroll->GetPosition();
263 for (const auto &item : this->list) {
264 int item_height = item->Height();
266 /* Skip items that are scrolled up */
267 if (--pos >= 0) continue;
269 if (y + item_height - 1 <= ir.bottom) {
270 Rect full{ir.left, y, ir.right, y + item_height - 1};
272 bool selected = (this->selected_result == item->result) && item->Selectable();
273 if (selected) GfxFillRect(full, PC_BLACK);
275 item->Draw(full, full.Shrink(WidgetDimensions::scaled.dropdowntext, RectPadding::zero), selected, colour);
277 y += item_height;
281 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
283 if (widget != WID_DM_ITEMS) return;
284 int item;
285 if (this->GetDropDownItem(item)) {
286 this->click_delay = 4;
287 this->selected_result = item;
288 this->SetDirty();
292 /** Rate limit how fast scrolling happens. */
293 IntervalTimer<TimerWindow> scroll_interval = {std::chrono::milliseconds(30), [this](auto) {
294 if (this->scrolling == 0) return;
296 if (this->vscroll->UpdatePosition(this->scrolling)) this->SetDirty();
298 this->scrolling = 0;
301 void OnMouseLoop() override
303 if (this->click_delay != 0 && --this->click_delay == 0) {
304 /* Close the dropdown, so it doesn't affect new window placement.
305 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
306 if (!this->persist) this->Close();
307 this->parent->OnDropdownSelect(this->parent_button, this->selected_result);
308 return;
311 if (this->drag_mode) {
312 int item;
314 if (!_left_button_clicked) {
315 this->drag_mode = false;
316 if (!this->GetDropDownItem(item)) {
317 if (this->instant_close) this->Close();
318 return;
320 this->click_delay = 2;
321 } else {
322 if (_cursor.pos.y <= this->top + WidgetDimensions::scaled.dropdownlist.top) {
323 /* Cursor is above the list, set scroll up */
324 this->scrolling = -1;
325 return;
326 } else if (_cursor.pos.y >= this->top + this->height - WidgetDimensions::scaled.dropdownlist.bottom) {
327 /* Cursor is below list, set scroll down */
328 this->scrolling = 1;
329 return;
332 if (!this->GetDropDownItem(item)) return;
335 if (this->selected_result != item) {
336 this->selected_result = item;
337 this->SetDirty();
342 void ReplaceList(DropDownList &&list)
344 this->list = std::move(list);
345 this->UpdateSizeAndPosition();
346 this->ReInit(0, 0);
347 this->InitializePositionSize(this->position.x, this->position.y, this->nested_root->smallest_x, this->nested_root->smallest_y);
348 this->SetDirty();
352 void ReplaceDropDownList(Window *parent, DropDownList &&list)
354 DropdownWindow *ddw = dynamic_cast<DropdownWindow *>(parent->FindChildWindow(WC_DROPDOWN_MENU));
355 if (ddw != nullptr) ddw->ReplaceList(std::move(list));
359 * Determine width and height required to fully display a DropDownList
360 * @param list The list.
361 * @return Dimension required to display the list.
363 Dimension GetDropDownListDimension(const DropDownList &list)
365 Dimension dim{};
366 for (const auto &item : list) {
367 dim.height += item->Height();
368 dim.width = std::max(dim.width, item->Width());
370 dim.width += WidgetDimensions::scaled.dropdowntext.Horizontal();
371 return dim;
375 * Show a drop down list.
376 * @param w Parent window for the list.
377 * @param list Prepopulated DropDownList.
378 * @param selected The initially selected list item.
379 * @param button The widget which is passed to Window::OnDropdownSelect and OnDropdownClose.
380 * Unless you override those functions, this should be then widget index of the dropdown button.
381 * @param wi_rect Coord of the parent drop down button, used to position the dropdown menu.
382 * @param instant_close Set to true if releasing mouse button should close the
383 * list regardless of where the cursor is.
384 * @param persist Set if this dropdown should stay open after an option is selected.
386 void ShowDropDownListAt(Window *w, DropDownList &&list, int selected, WidgetID button, Rect wi_rect, Colours wi_colour, bool instant_close, bool persist)
388 CloseWindowByClass(WC_DROPDOWN_MENU);
389 new DropdownWindow(w, std::move(list), selected, button, wi_rect, instant_close, wi_colour, persist);
393 * Show a drop down list.
394 * @param w Parent window for the list.
395 * @param list Prepopulated DropDownList.
396 * @param selected The initially selected list item.
397 * @param button The widget within the parent window that is used to determine
398 * the list's location.
399 * @param width Override the minimum width determined by the selected widget and list contents.
400 * @param instant_close Set to true if releasing mouse button should close the
401 * list regardless of where the cursor is.
402 * @param persist Set if this dropdown should stay open after an option is selected.
404 void ShowDropDownList(Window *w, DropDownList &&list, int selected, WidgetID button, uint width, bool instant_close, bool persist)
406 /* Our parent's button widget is used to determine where to place the drop
407 * down list window. */
408 NWidgetCore *nwi = w->GetWidget<NWidgetCore>(button);
409 Rect wi_rect = nwi->GetCurrentRect();
410 Colours wi_colour = nwi->colour;
412 if ((nwi->type & WWT_MASK) == NWID_BUTTON_DROPDOWN) {
413 nwi->disp_flags |= ND_DROPDOWN_ACTIVE;
414 } else {
415 nwi->SetLowered(true);
417 nwi->SetDirty(w);
419 if (width != 0) {
420 if (_current_text_dir == TD_RTL) {
421 wi_rect.left = wi_rect.right + 1 - ScaleGUITrad(width);
422 } else {
423 wi_rect.right = wi_rect.left + ScaleGUITrad(width) - 1;
427 ShowDropDownListAt(w, std::move(list), selected, button, wi_rect, wi_colour, instant_close, persist);
431 * Show a dropdown menu window near a widget of the parent window.
432 * The result code of the items is their index in the \a strings list.
433 * @param w Parent window that wants the dropdown menu.
434 * @param strings Menu list.
435 * @param selected Index of initial selected item.
436 * @param button Button widget number of the parent window \a w that wants the dropdown menu.
437 * @param disabled_mask Bitmask for disabled items (items with their bit set are displayed, but not selectable in the dropdown list).
438 * @param hidden_mask Bitmask for hidden items (items with their bit set are not copied to the dropdown list).
439 * @param width Minimum width of the dropdown menu.
441 void ShowDropDownMenu(Window *w, std::span<const StringID> strings, int selected, WidgetID button, uint32_t disabled_mask, uint32_t hidden_mask, uint width)
443 DropDownList list;
445 uint i = 0;
446 for (auto string : strings) {
447 if (!HasBit(hidden_mask, i)) {
448 list.push_back(MakeDropDownListStringItem(string, i, HasBit(disabled_mask, i)));
450 ++i;
453 if (!list.empty()) ShowDropDownList(w, std::move(list), selected, button, width);