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/>.
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
),
34 static WindowDesc
_dropdown_desc(__FILE__
, __LINE__
,
35 WDP_MANUAL
, nullptr, 0, 0,
36 WC_DROPDOWN_MENU
, WC_NONE
,
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.
55 Dimension items_dim
; ///< Calculated cropped and padded dimension for the items widget.
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
)
72 , list(std::move(list
))
73 , selected_result(selected
)
74 , instant_close(instant_close
)
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
111 this->instant_close
= false;
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
;
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
;
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
);
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
);
246 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
248 if (widget
!= WID_DM_ITEMS
) return;
250 if (this->GetDropDownItem(item
)) {
251 this->click_delay
= 4;
252 this->selected_result
= item
;
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();
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
);
276 if (this->drag_mode
) {
279 if (!_left_button_clicked
) {
280 this->drag_mode
= false;
281 if (!this->GetDropDownItem(item
)) {
282 if (this->instant_close
) this->Close();
285 this->click_delay
= 2;
287 if (_cursor
.pos
.y
<= this->top
+ 2) {
288 /* Cursor is above the list, set scroll up */
289 this->scrolling
= -1;
291 } else if (_cursor
.pos
.y
>= this->top
+ this->height
- 2) {
292 /* Cursor is below list, set scroll down */
297 if (!this->GetDropDownItem(item
)) return;
300 if (this->selected_result
!= item
) {
301 this->selected_result
= item
;
307 void ReplaceList(DropDownList
&&list
)
309 this->list
= std::move(list
);
310 this->UpdateSizeAndPosition();
312 this->InitializePositionSize(this->position
.x
, this->position
.y
, this->nested_root
->smallest_x
, this->nested_root
->smallest_y
);
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
)
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();
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
;
380 nwi
->SetLowered(true);
385 if (_current_text_dir
== TD_RTL
) {
386 wi_rect
.left
= wi_rect
.right
+ 1 - ScaleGUITrad(width
);
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
)
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
);