Fix #10490: Allow ships to exit depots if another is not moving at the exit point...
[openttd-github.git] / src / widgets / dropdown.cpp
blobb6a39596fc71e8e228ee014ab567a17dee1310f5
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 "../window_gui.h"
12 #include "../string_func.h"
13 #include "../strings_func.h"
14 #include "../window_func.h"
15 #include "../zoom_func.h"
16 #include "../timer/timer.h"
17 #include "../timer/timer_window.h"
18 #include "dropdown_type.h"
20 #include "dropdown_widget.h"
22 #include "../safeguards.h"
25 static constexpr NWidgetPart _nested_dropdown_menu_widgets[] = {
26 NWidget(NWID_HORIZONTAL),
27 NWidget(WWT_PANEL, COLOUR_END, WID_DM_ITEMS), SetScrollbar(WID_DM_SCROLL), EndContainer(),
28 NWidget(NWID_SELECTION, INVALID_COLOUR, WID_DM_SHOW_SCROLL),
29 NWidget(NWID_VSCROLLBAR, COLOUR_END, WID_DM_SCROLL),
30 EndContainer(),
31 EndContainer(),
34 static WindowDesc _dropdown_desc(__FILE__, __LINE__,
35 WDP_MANUAL, nullptr, 0, 0,
36 WC_DROPDOWN_MENU, WC_NONE,
37 WDF_NO_FOCUS,
38 std::begin(_nested_dropdown_menu_widgets), std::end(_nested_dropdown_menu_widgets)
41 /** Drop-down menu window */
42 struct DropdownWindow : Window {
43 WidgetID parent_button; ///< Parent widget number where the window is dropped from.
44 Rect wi_rect; ///< Rect of the button that opened the dropdown.
45 DropDownList list; ///< List with dropdown menu items.
46 int selected_result; ///< Result value of the selected item in the list.
47 byte click_delay = 0; ///< Timer to delay selection.
48 bool drag_mode = true;
49 bool instant_close; ///< Close the window when the mouse button is raised.
50 bool persist; ///< Persist dropdown menu.
51 int scrolling = 0; ///< If non-zero, auto-scroll the item list (one time).
52 Point position; ///< Position of the topleft corner of the window.
53 Scrollbar *vscroll;
55 Dimension items_dim; ///< Calculated cropped and padded dimension for the items widget.
57 /**
58 * Create a dropdown menu.
59 * @param parent Parent window.
60 * @param list Dropdown item list.
61 * @param selected Initial selected result of the list.
62 * @param button Widget of the parent window doing the dropdown.
63 * @param wi_rect Rect of the button that opened the dropdown.
64 * @param instant_close Close the window when the mouse button is raised.
65 * @param wi_colour Colour of the parent widget.
66 * @param persist Dropdown menu will persist.
68 DropdownWindow(Window *parent, DropDownList &&list, int selected, WidgetID button, const Rect wi_rect, bool instant_close, Colours wi_colour, bool persist)
69 : Window(&_dropdown_desc)
70 , parent_button(button)
71 , wi_rect(wi_rect)
72 , list(std::move(list))
73 , selected_result(selected)
74 , instant_close(instant_close)
75 , persist(persist)
77 assert(!this->list.empty());
79 this->parent = parent;
81 this->CreateNestedTree();
83 this->GetWidget<NWidgetCore>(WID_DM_ITEMS)->colour = wi_colour;
84 this->GetWidget<NWidgetCore>(WID_DM_SCROLL)->colour = wi_colour;
85 this->vscroll = this->GetScrollbar(WID_DM_SCROLL);
86 this->UpdateSizeAndPosition();
88 this->FinishInitNested(0);
89 CLRBITS(this->flags, WF_WHITE_BORDER);
92 void Close([[maybe_unused]] int data = 0) override
94 /* Finish closing the dropdown, so it doesn't affect new window placement.
95 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
96 this->Window::Close();
98 Point pt = _cursor.pos;
99 pt.x -= this->parent->left;
100 pt.y -= this->parent->top;
101 this->parent->OnDropdownClose(pt, this->parent_button, this->selected_result, this->instant_close);
103 /* Set flag on parent widget to indicate that we have just closed. */
104 NWidgetCore *nwc = this->parent->GetWidget<NWidgetCore>(this->parent_button);
105 if (nwc != nullptr) SetBit(nwc->disp_flags, NDB_DROPDOWN_CLOSED);
108 void OnFocusLost(bool closing) override
110 if (!closing) {
111 this->instant_close = false;
112 this->Close();
117 * Fit dropdown list into available height, rounding to average item size. Width is adjusted if scrollbar is present.
118 * @param[in,out] desired Desired dimensions of dropdown list.
119 * @param list Dimensions of the list itself, without padding or cropping.
120 * @param available_height Available height to fit list within.
122 void FitAvailableHeight(Dimension &desired, const Dimension &list, uint available_height)
124 if (desired.height < available_height) return;
126 /* If the dropdown doesn't fully fit, we a need a dropdown. */
127 uint avg_height = list.height / (uint)this->list.size();
128 uint rows = std::max((available_height - WidgetDimensions::scaled.dropdownlist.Vertical()) / avg_height, 1U);
130 desired.width = std::max(list.width, desired.width - NWidgetScrollbar::GetVerticalDimension().width);
131 desired.height = rows * avg_height + WidgetDimensions::scaled.dropdownlist.Vertical();
135 * Update size and position of window to fit dropdown list into available space.
137 void UpdateSizeAndPosition()
139 Rect button_rect = this->wi_rect.Translate(this->parent->left, this->parent->top);
141 /* Get the dimensions required for the list. */
142 Dimension list_dim = GetDropDownListDimension(this->list);
144 /* Set up dimensions for the items widget. */
145 Dimension widget_dim = list_dim;
146 widget_dim.width += WidgetDimensions::scaled.dropdownlist.Horizontal();
147 widget_dim.height += WidgetDimensions::scaled.dropdownlist.Vertical();
149 /* Width should match at least the width of the parent widget. */
150 widget_dim.width = std::max<uint>(widget_dim.width, button_rect.Width());
152 /* Available height below (or above, if the dropdown is placed above the widget). */
153 uint available_height_below = std::max(GetMainViewBottom() - button_rect.bottom - 1, 0);
154 uint available_height_above = std::max(button_rect.top - 1 - GetMainViewTop(), 0);
156 /* Is it better to place the dropdown above the widget? */
157 if (widget_dim.height > available_height_below && available_height_above > available_height_below) {
158 FitAvailableHeight(widget_dim, list_dim, available_height_above);
159 this->position.y = button_rect.top - widget_dim.height;
160 } else {
161 FitAvailableHeight(widget_dim, list_dim, available_height_below);
162 this->position.y = button_rect.bottom + 1;
165 this->position.x = (_current_text_dir == TD_RTL) ? button_rect.right + 1 - (int)widget_dim.width : button_rect.left;
167 this->items_dim = widget_dim;
168 this->GetWidget<NWidgetStacked>(WID_DM_SHOW_SCROLL)->SetDisplayedPlane(list_dim.height > widget_dim.height ? 0 : SZSP_NONE);
170 /* Capacity is the average number of items visible */
171 this->vscroll->SetCapacity((widget_dim.height - WidgetDimensions::scaled.dropdownlist.Vertical()) * this->list.size() / list_dim.height);
172 this->vscroll->SetCount(this->list.size());
174 /* If the dropdown is positioned above the parent widget, start selection at the bottom. */
175 if (this->position.y < button_rect.top && list_dim.height > widget_dim.height) this->vscroll->UpdatePosition(INT_MAX);
178 void UpdateWidgetSize(WidgetID widget, Dimension *size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension *fill, [[maybe_unused]] Dimension *resize) override
180 if (widget == WID_DM_ITEMS) *size = this->items_dim;
183 Point OnInitialPosition([[maybe_unused]] int16_t sm_width, [[maybe_unused]] int16_t sm_height, [[maybe_unused]] int window_number) override
185 return this->position;
189 * Find the dropdown item under the cursor.
190 * @param[out] value Selected item, if function returns \c true.
191 * @return Cursor points to a dropdown item.
193 bool GetDropDownItem(int &value)
195 if (GetWidgetFromPos(this, _cursor.pos.x - this->left, _cursor.pos.y - this->top) < 0) return false;
197 const Rect &r = this->GetWidget<NWidgetBase>(WID_DM_ITEMS)->GetCurrentRect().Shrink(WidgetDimensions::scaled.dropdownlist);
198 int y = _cursor.pos.y - this->top - r.top;
199 int pos = this->vscroll->GetPosition();
201 for (const auto &item : this->list) {
202 /* Skip items that are scrolled up */
203 if (--pos >= 0) continue;
205 int item_height = item->Height();
207 if (y < item_height) {
208 if (item->masked || !item->Selectable()) return false;
209 value = item->result;
210 return true;
213 y -= item_height;
216 return false;
219 void DrawWidget(const Rect &r, WidgetID widget) const override
221 if (widget != WID_DM_ITEMS) return;
223 Colours colour = this->GetWidget<NWidgetCore>(widget)->colour;
225 Rect ir = r.Shrink(WidgetDimensions::scaled.dropdownlist);
226 int y = ir.top;
227 int pos = this->vscroll->GetPosition();
228 for (const auto &item : this->list) {
229 int item_height = item->Height();
231 /* Skip items that are scrolled up */
232 if (--pos >= 0) continue;
234 if (y + item_height - 1 <= ir.bottom) {
235 Rect full{ir.left, y, ir.right, y + item_height - 1};
237 bool selected = (this->selected_result == item->result) && item->Selectable();
238 if (selected) GfxFillRect(full, PC_BLACK);
240 item->Draw(full, full.Shrink(WidgetDimensions::scaled.dropdowntext, RectPadding::zero), selected, colour);
242 y += item_height;
246 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
248 if (widget != WID_DM_ITEMS) return;
249 int item;
250 if (this->GetDropDownItem(item)) {
251 this->click_delay = 4;
252 this->selected_result = item;
253 this->SetDirty();
257 /** Rate limit how fast scrolling happens. */
258 IntervalTimer<TimerWindow> scroll_interval = {std::chrono::milliseconds(30), [this](auto) {
259 if (this->scrolling == 0) return;
261 if (this->vscroll->UpdatePosition(this->scrolling)) this->SetDirty();
263 this->scrolling = 0;
266 void OnMouseLoop() override
268 if (this->click_delay != 0 && --this->click_delay == 0) {
269 /* Close the dropdown, so it doesn't affect new window placement.
270 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
271 if (!this->persist) this->Close();
272 this->parent->OnDropdownSelect(this->parent_button, this->selected_result);
273 return;
276 if (this->drag_mode) {
277 int item;
279 if (!_left_button_clicked) {
280 this->drag_mode = false;
281 if (!this->GetDropDownItem(item)) {
282 if (this->instant_close) this->Close();
283 return;
285 this->click_delay = 2;
286 } else {
287 if (_cursor.pos.y <= this->top + 2) {
288 /* Cursor is above the list, set scroll up */
289 this->scrolling = -1;
290 return;
291 } else if (_cursor.pos.y >= this->top + this->height - 2) {
292 /* Cursor is below list, set scroll down */
293 this->scrolling = 1;
294 return;
297 if (!this->GetDropDownItem(item)) return;
300 if (this->selected_result != item) {
301 this->selected_result = item;
302 this->SetDirty();
307 void ReplaceList(DropDownList &&list)
309 this->list = std::move(list);
310 this->UpdateSizeAndPosition();
311 this->ReInit(0, 0);
312 this->InitializePositionSize(this->position.x, this->position.y, this->nested_root->smallest_x, this->nested_root->smallest_y);
313 this->SetDirty();
317 void ReplaceDropDownList(Window *parent, DropDownList &&list)
319 DropdownWindow *ddw = dynamic_cast<DropdownWindow *>(parent->FindChildWindow(WC_DROPDOWN_MENU));
320 if (ddw != nullptr) ddw->ReplaceList(std::move(list));
324 * Determine width and height required to fully display a DropDownList
325 * @param list The list.
326 * @return Dimension required to display the list.
328 Dimension GetDropDownListDimension(const DropDownList &list)
330 Dimension dim{};
331 for (const auto &item : list) {
332 dim.height += item->Height();
333 dim.width = std::max(dim.width, item->Width());
335 dim.width += WidgetDimensions::scaled.dropdowntext.Horizontal();
336 return dim;
340 * Show a drop down list.
341 * @param w Parent window for the list.
342 * @param list Prepopulated DropDownList.
343 * @param selected The initially selected list item.
344 * @param button The widget which is passed to Window::OnDropdownSelect and OnDropdownClose.
345 * Unless you override those functions, this should be then widget index of the dropdown button.
346 * @param wi_rect Coord of the parent drop down button, used to position the dropdown menu.
347 * @param instant_close Set to true if releasing mouse button should close the
348 * list regardless of where the cursor is.
349 * @param persist Set if this dropdown should stay open after an option is selected.
351 void ShowDropDownListAt(Window *w, DropDownList &&list, int selected, WidgetID button, Rect wi_rect, Colours wi_colour, bool instant_close, bool persist)
353 CloseWindowByClass(WC_DROPDOWN_MENU);
354 new DropdownWindow(w, std::move(list), selected, button, wi_rect, instant_close, wi_colour, persist);
358 * Show a drop down list.
359 * @param w Parent window for the list.
360 * @param list Prepopulated DropDownList.
361 * @param selected The initially selected list item.
362 * @param button The widget within the parent window that is used to determine
363 * the list's location.
364 * @param width Override the minimum width determined by the selected widget and list contents.
365 * @param instant_close Set to true if releasing mouse button should close the
366 * list regardless of where the cursor is.
367 * @param persist Set if this dropdown should stay open after an option is selected.
369 void ShowDropDownList(Window *w, DropDownList &&list, int selected, WidgetID button, uint width, bool instant_close, bool persist)
371 /* Our parent's button widget is used to determine where to place the drop
372 * down list window. */
373 NWidgetCore *nwi = w->GetWidget<NWidgetCore>(button);
374 Rect wi_rect = nwi->GetCurrentRect();
375 Colours wi_colour = nwi->colour;
377 if ((nwi->type & WWT_MASK) == NWID_BUTTON_DROPDOWN) {
378 nwi->disp_flags |= ND_DROPDOWN_ACTIVE;
379 } else {
380 nwi->SetLowered(true);
382 nwi->SetDirty(w);
384 if (width != 0) {
385 if (_current_text_dir == TD_RTL) {
386 wi_rect.left = wi_rect.right + 1 - ScaleGUITrad(width);
387 } else {
388 wi_rect.right = wi_rect.left + ScaleGUITrad(width) - 1;
392 ShowDropDownListAt(w, std::move(list), selected, button, wi_rect, wi_colour, instant_close, persist);
396 * Show a dropdown menu window near a widget of the parent window.
397 * The result code of the items is their index in the \a strings list.
398 * @param w Parent window that wants the dropdown menu.
399 * @param strings Menu list, end with #INVALID_STRING_ID
400 * @param selected Index of initial selected item.
401 * @param button Button widget number of the parent window \a w that wants the dropdown menu.
402 * @param disabled_mask Bitmask for disabled items (items with their bit set are displayed, but not selectable in the dropdown list).
403 * @param hidden_mask Bitmask for hidden items (items with their bit set are not copied to the dropdown list).
404 * @param width Minimum width of the dropdown menu.
406 void ShowDropDownMenu(Window *w, const StringID *strings, int selected, WidgetID button, uint32_t disabled_mask, uint32_t hidden_mask, uint width)
408 DropDownList list;
410 for (uint i = 0; strings[i] != INVALID_STRING_ID; i++) {
411 if (!HasBit(hidden_mask, i)) {
412 list.push_back(std::make_unique<DropDownListStringItem>(strings[i], i, HasBit(disabled_mask, i)));
416 if (!list.empty()) ShowDropDownList(w, std::move(list), selected, button, width);