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 "../guitimer_func.h"
16 #include "dropdown_type.h"
18 #include "dropdown_widget.h"
20 #include "../safeguards.h"
23 void DropDownListItem::Draw(int left
, int right
, int top
, int bottom
, bool sel
, Colours bg_colour
) const
25 int c1
= _colour_gradient
[bg_colour
][3];
26 int c2
= _colour_gradient
[bg_colour
][7];
28 int mid
= top
+ this->Height(0) / 2;
29 GfxFillRect(left
+ 1, mid
- 2, right
- 1, mid
- 2, c1
);
30 GfxFillRect(left
+ 1, mid
- 1, right
- 1, mid
- 1, c2
);
33 uint
DropDownListStringItem::Width() const
36 GetString(buffer
, this->String(), lastof(buffer
));
37 return GetStringBoundingBox(buffer
).width
;
40 void DropDownListStringItem::Draw(int left
, int right
, int top
, int bottom
, bool sel
, Colours bg_colour
) const
42 DrawString(left
+ WD_FRAMERECT_LEFT
, right
- WD_FRAMERECT_RIGHT
, top
, this->String(), sel
? TC_WHITE
: TC_BLACK
);
46 * Natural sorting comparator function for DropDownList::sort().
47 * @param first Left side of comparison.
48 * @param second Right side of comparison.
49 * @return true if \a first precedes \a second.
50 * @warning All items in the list need to be derivates of DropDownListStringItem.
52 /* static */ bool DropDownListStringItem::NatSortFunc(std::unique_ptr
<const DropDownListItem
> const &first
, std::unique_ptr
<const DropDownListItem
> const &second
)
54 char buffer1
[512], buffer2
[512];
55 GetString(buffer1
, static_cast<const DropDownListStringItem
*>(first
.get())->String(), lastof(buffer1
));
56 GetString(buffer2
, static_cast<const DropDownListStringItem
*>(second
.get())->String(), lastof(buffer2
));
57 return strnatcmp(buffer1
, buffer2
) < 0;
60 StringID
DropDownListParamStringItem::String() const
62 for (uint i
= 0; i
< lengthof(this->decode_params
); i
++) SetDParam(i
, this->decode_params
[i
]);
66 StringID
DropDownListCharStringItem::String() const
68 SetDParamStr(0, this->raw_string
);
72 DropDownListIconItem::DropDownListIconItem(SpriteID sprite
, PaletteID pal
, StringID string
, int result
, bool masked
) : DropDownListParamStringItem(string
, result
, masked
), sprite(sprite
), pal(pal
)
74 this->dim
= GetSpriteSize(sprite
);
75 this->sprite_y
= dim
.height
;
78 uint
DropDownListIconItem::Height(uint width
) const
80 return std::max(this->dim
.height
, (uint
)FONT_HEIGHT_NORMAL
);
83 uint
DropDownListIconItem::Width() const
85 return DropDownListStringItem::Width() + this->dim
.width
+ WD_FRAMERECT_LEFT
;
88 void DropDownListIconItem::Draw(int left
, int right
, int top
, int bottom
, bool sel
, Colours bg_colour
) const
90 bool rtl
= _current_text_dir
== TD_RTL
;
91 DrawSprite(this->sprite
, this->pal
, rtl
? right
- this->dim
.width
- WD_FRAMERECT_RIGHT
: left
+ WD_FRAMERECT_LEFT
, CenterBounds(top
, bottom
, this->sprite_y
));
92 DrawString(left
+ WD_FRAMERECT_LEFT
+ (rtl
? 0 : (this->dim
.width
+ WD_FRAMERECT_LEFT
)), right
- WD_FRAMERECT_RIGHT
- (rtl
? (this->dim
.width
+ WD_FRAMERECT_RIGHT
) : 0), CenterBounds(top
, bottom
, FONT_HEIGHT_NORMAL
), this->String(), sel
? TC_WHITE
: TC_BLACK
);
95 void DropDownListIconItem::SetDimension(Dimension d
)
100 static const NWidgetPart _nested_dropdown_menu_widgets
[] = {
101 NWidget(NWID_HORIZONTAL
),
102 NWidget(WWT_PANEL
, COLOUR_END
, WID_DM_ITEMS
), SetMinimalSize(1, 1), SetScrollbar(WID_DM_SCROLL
), EndContainer(),
103 NWidget(NWID_SELECTION
, INVALID_COLOUR
, WID_DM_SHOW_SCROLL
),
104 NWidget(NWID_VSCROLLBAR
, COLOUR_END
, WID_DM_SCROLL
),
109 static WindowDesc
_dropdown_desc(
110 WDP_MANUAL
, nullptr, 0, 0,
111 WC_DROPDOWN_MENU
, WC_NONE
,
113 _nested_dropdown_menu_widgets
, lengthof(_nested_dropdown_menu_widgets
)
116 /** Drop-down menu window */
117 struct DropdownWindow
: Window
{
118 WindowClass parent_wnd_class
; ///< Parent window class.
119 WindowNumber parent_wnd_num
; ///< Parent window number.
120 int parent_button
; ///< Parent widget number where the window is dropped from.
121 const DropDownList list
; ///< List with dropdown menu items.
122 int selected_index
; ///< Index of the selected item in the list.
123 byte click_delay
; ///< Timer to delay selection.
125 bool instant_close
; ///< Close the window when the mouse button is raised.
126 int scrolling
; ///< If non-zero, auto-scroll the item list (one time).
127 GUITimer scrolling_timer
; ///< Timer for auto-scroll of the item list.
128 Point position
; ///< Position of the topleft corner of the window.
132 * Create a dropdown menu.
133 * @param parent Parent window.
134 * @param list Dropdown item list.
135 * @param selected Index of the selected item in the list.
136 * @param button Widget of the parent window doing the dropdown.
137 * @param instant_close Close the window when the mouse button is raised.
138 * @param position Topleft position of the dropdown menu window.
139 * @param size Size of the dropdown menu window.
140 * @param wi_colour Colour of the parent widget.
141 * @param scroll Dropdown menu has a scrollbar.
143 DropdownWindow(Window
*parent
, DropDownList
&&list
, int selected
, int button
, bool instant_close
, const Point
&position
, const Dimension
&size
, Colours wi_colour
, bool scroll
)
144 : Window(&_dropdown_desc
), list(std::move(list
))
146 assert(this->list
.size() > 0);
148 this->position
= position
;
150 this->CreateNestedTree();
152 this->vscroll
= this->GetScrollbar(WID_DM_SCROLL
);
154 uint items_width
= size
.width
- (scroll
? NWidgetScrollbar::GetVerticalDimension().width
: 0);
155 NWidgetCore
*nwi
= this->GetWidget
<NWidgetCore
>(WID_DM_ITEMS
);
156 nwi
->SetMinimalSizeAbsolute(items_width
, size
.height
+ 4);
157 nwi
->colour
= wi_colour
;
159 nwi
= this->GetWidget
<NWidgetCore
>(WID_DM_SCROLL
);
160 nwi
->colour
= wi_colour
;
162 this->GetWidget
<NWidgetStacked
>(WID_DM_SHOW_SCROLL
)->SetDisplayedPlane(scroll
? 0 : SZSP_NONE
);
164 this->FinishInitNested(0);
165 CLRBITS(this->flags
, WF_WHITE_BORDER
);
167 /* Total length of list */
169 for (const auto &item
: this->list
) {
170 list_height
+= item
->Height(items_width
);
173 /* Capacity is the average number of items visible */
174 this->vscroll
->SetCapacity(size
.height
* (uint16
)this->list
.size() / list_height
);
175 this->vscroll
->SetCount((uint16
)this->list
.size());
177 this->parent_wnd_class
= parent
->window_class
;
178 this->parent_wnd_num
= parent
->window_number
;
179 this->parent_button
= button
;
180 this->selected_index
= selected
;
181 this->click_delay
= 0;
182 this->drag_mode
= true;
183 this->instant_close
= instant_close
;
184 this->scrolling_timer
= GUITimer(MILLISECONDS_PER_TICK
);
187 void Close() override
189 /* Finish closing the dropdown, so it doesn't affect new window placement.
190 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
191 this->Window::Close();
193 Window
*w2
= FindWindowById(this->parent_wnd_class
, this->parent_wnd_num
);
195 Point pt
= _cursor
.pos
;
198 w2
->OnDropdownClose(pt
, this->parent_button
, this->selected_index
, this->instant_close
);
202 Point
OnInitialPosition(int16 sm_width
, int16 sm_height
, int window_number
) override
204 return this->position
;
208 * Find the dropdown item under the cursor.
209 * @param[out] value Selected item, if function returns \c true.
210 * @return Cursor points to a dropdown item.
212 bool GetDropDownItem(int &value
)
214 if (GetWidgetFromPos(this, _cursor
.pos
.x
- this->left
, _cursor
.pos
.y
- this->top
) < 0) return false;
216 NWidgetBase
*nwi
= this->GetWidget
<NWidgetBase
>(WID_DM_ITEMS
);
217 int y
= _cursor
.pos
.y
- this->top
- nwi
->pos_y
- 2;
218 int width
= nwi
->current_x
- 4;
219 int pos
= this->vscroll
->GetPosition();
221 for (const auto &item
: this->list
) {
222 /* Skip items that are scrolled up */
223 if (--pos
>= 0) continue;
225 int item_height
= item
->Height(width
);
227 if (y
< item_height
) {
228 if (item
->masked
|| !item
->Selectable()) return false;
229 value
= item
->result
;
239 void DrawWidget(const Rect
&r
, int widget
) const override
241 if (widget
!= WID_DM_ITEMS
) return;
243 Colours colour
= this->GetWidget
<NWidgetCore
>(widget
)->colour
;
246 int pos
= this->vscroll
->GetPosition();
247 for (const auto &item
: this->list
) {
248 int item_height
= item
->Height(r
.right
- r
.left
+ 1);
250 /* Skip items that are scrolled up */
251 if (--pos
>= 0) continue;
253 if (y
+ item_height
< r
.bottom
) {
254 bool selected
= (this->selected_index
== item
->result
);
255 if (selected
) GfxFillRect(r
.left
+ 2, y
, r
.right
- 1, y
+ item_height
- 1, PC_BLACK
);
257 item
->Draw(r
.left
, r
.right
, y
, y
+ item_height
, selected
, colour
);
260 GfxFillRect(r
.left
+ 1, y
, r
.right
- 1, y
+ item_height
- 1, _colour_gradient
[colour
][5], FILLRECT_CHECKER
);
267 void OnClick(Point pt
, int widget
, int click_count
) override
269 if (widget
!= WID_DM_ITEMS
) return;
271 if (this->GetDropDownItem(item
)) {
272 this->click_delay
= 4;
273 this->selected_index
= item
;
278 void OnRealtimeTick(uint delta_ms
) override
280 if (!this->scrolling_timer
.Elapsed(delta_ms
)) return;
281 this->scrolling_timer
.SetInterval(MILLISECONDS_PER_TICK
);
283 if (this->scrolling
!= 0) {
284 int pos
= this->vscroll
->GetPosition();
286 this->vscroll
->UpdatePosition(this->scrolling
);
289 if (pos
!= this->vscroll
->GetPosition()) {
295 void OnMouseLoop() override
297 Window
*w2
= FindWindowById(this->parent_wnd_class
, this->parent_wnd_num
);
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). */
307 w2
->OnDropdownSelect(this->parent_button
, this->selected_index
);
311 if (this->drag_mode
) {
314 if (!_left_button_clicked
) {
315 this->drag_mode
= false;
316 if (!this->GetDropDownItem(item
)) {
317 if (this->instant_close
) this->Close();
320 this->click_delay
= 2;
322 if (_cursor
.pos
.y
<= this->top
+ 2) {
323 /* Cursor is above the list, set scroll up */
324 this->scrolling
= -1;
326 } else if (_cursor
.pos
.y
>= this->top
+ this->height
- 2) {
327 /* Cursor is below list, set scroll down */
332 if (!this->GetDropDownItem(item
)) return;
335 if (this->selected_index
!= item
) {
336 this->selected_index
= item
;
344 * Show a drop down list.
345 * @param w Parent window for the list.
346 * @param list Prepopulated DropDownList.
347 * @param selected The initially selected list item.
348 * @param button The widget which is passed to Window::OnDropdownSelect and OnDropdownClose.
349 * Unless you override those functions, this should be then widget index of the dropdown button.
350 * @param wi_rect Coord of the parent drop down button, used to position the dropdown menu.
351 * @param auto_width The width is determined by the widest item in the list,
352 * in this case only one of \a left or \a right is used (depending on text direction).
353 * @param instant_close Set to true if releasing mouse button should close the
354 * list regardless of where the cursor is.
356 void ShowDropDownListAt(Window
*w
, DropDownList
&&list
, int selected
, int button
, Rect wi_rect
, Colours wi_colour
, bool auto_width
, bool instant_close
)
358 CloseWindowById(WC_DROPDOWN_MENU
, 0);
360 /* The preferred position is just below the dropdown calling widget */
361 int top
= w
->top
+ wi_rect
.bottom
+ 1;
363 /* The preferred width equals the calling widget */
364 uint width
= wi_rect
.right
- wi_rect
.left
+ 1;
366 /* Longest item in the list, if auto_width is enabled */
367 uint max_item_width
= 0;
369 /* Total height of list */
372 for (const auto &item
: list
) {
373 height
+= item
->Height(width
);
374 if (auto_width
) max_item_width
= std::max(max_item_width
, item
->Width() + 5);
377 /* Scrollbar needed? */
380 /* Is it better to place the dropdown above the widget? */
383 /* Available height below (or above, if the dropdown is placed above the widget). */
384 uint available_height
= std::max(GetMainViewBottom() - top
- 4, 0);
386 /* If the dropdown doesn't fully fit below the widget... */
387 if (height
> available_height
) {
389 uint available_height_above
= std::max(w
->top
+ wi_rect
.top
- GetMainViewTop() - 4, 0);
391 /* Put the dropdown above if there is more available space. */
392 if (available_height_above
> available_height
) {
394 available_height
= available_height_above
;
397 /* If the dropdown doesn't fully fit, we need a dropdown. */
398 if (height
> available_height
) {
400 uint avg_height
= height
/ (uint
)list
.size();
402 /* Check at least there is space for one item. */
403 assert(available_height
>= avg_height
);
406 uint rows
= available_height
/ avg_height
;
407 height
= rows
* avg_height
;
409 /* Add space for the scrollbar. */
410 max_item_width
+= NWidgetScrollbar::GetVerticalDimension().width
;
413 /* Set the top position if needed. */
415 top
= w
->top
+ wi_rect
.top
- height
- 4;
419 if (auto_width
) width
= std::max(width
, max_item_width
);
421 Point dw_pos
= { w
->left
+ (_current_text_dir
== TD_RTL
? wi_rect
.right
+ 1 - (int)width
: wi_rect
.left
), top
};
422 Dimension dw_size
= {width
, height
};
423 DropdownWindow
*dropdown
= new DropdownWindow(w
, std::move(list
), selected
, button
, instant_close
, dw_pos
, dw_size
, wi_colour
, scroll
);
425 /* The dropdown starts scrolling downwards when opening it towards
426 * the top and holding down the mouse button. It can be fooled by
427 * opening the dropdown scrolled to the very bottom. */
428 if (above
&& scroll
) dropdown
->vscroll
->UpdatePosition(INT_MAX
);
432 * Show a drop down list.
433 * @param w Parent window for the list.
434 * @param list Prepopulated DropDownList.
435 * @param selected The initially selected list item.
436 * @param button The widget within the parent window that is used to determine
437 * the list's location.
438 * @param width Override the width determined by the selected widget.
439 * @param auto_width Maximum width is determined by the widest item in the list.
440 * @param instant_close Set to true if releasing mouse button should close the
441 * list regardless of where the cursor is.
443 void ShowDropDownList(Window
*w
, DropDownList
&&list
, int selected
, int button
, uint width
, bool auto_width
, bool instant_close
)
445 /* Our parent's button widget is used to determine where to place the drop
446 * down list window. */
447 NWidgetCore
*nwi
= w
->GetWidget
<NWidgetCore
>(button
);
448 Rect wi_rect
= nwi
->GetCurrentRect();
449 Colours wi_colour
= nwi
->colour
;
451 if ((nwi
->type
& WWT_MASK
) == NWID_BUTTON_DROPDOWN
) {
452 nwi
->disp_flags
|= ND_DROPDOWN_ACTIVE
;
454 w
->LowerWidget(button
);
456 w
->SetWidgetDirty(button
);
459 if (_current_text_dir
== TD_RTL
) {
460 wi_rect
.left
= wi_rect
.right
+ 1 - width
;
462 wi_rect
.right
= wi_rect
.left
+ width
- 1;
466 ShowDropDownListAt(w
, std::move(list
), selected
, button
, wi_rect
, wi_colour
, auto_width
, instant_close
);
470 * Show a dropdown menu window near a widget of the parent window.
471 * The result code of the items is their index in the \a strings list.
472 * @param w Parent window that wants the dropdown menu.
473 * @param strings Menu list, end with #INVALID_STRING_ID
474 * @param selected Index of initial selected item.
475 * @param button Button widget number of the parent window \a w that wants the dropdown menu.
476 * @param disabled_mask Bitmask for disabled items (items with their bit set are displayed, but not selectable in the dropdown list).
477 * @param hidden_mask Bitmask for hidden items (items with their bit set are not copied to the dropdown list).
478 * @param width Width of the dropdown menu. If \c 0, use the width of parent widget \a button.
480 void ShowDropDownMenu(Window
*w
, const StringID
*strings
, int selected
, int button
, uint32 disabled_mask
, uint32 hidden_mask
, uint width
)
484 for (uint i
= 0; strings
[i
] != INVALID_STRING_ID
; i
++) {
485 if (!HasBit(hidden_mask
, i
)) {
486 list
.emplace_back(new DropDownListStringItem(strings
[i
], i
, HasBit(disabled_mask
, i
)));
490 if (!list
.empty()) ShowDropDownList(w
, std::move(list
), selected
, button
, width
);
494 * Delete the drop-down menu from window \a pw
495 * @param pw Parent window of the drop-down menu window
496 * @return Parent widget number if the drop-down was found and closed, \c -1 if the window was not found.
498 int HideDropDownMenu(Window
*pw
)
500 for (Window
*w
: Window::Iterate()) {
501 if (w
->window_class
!= WC_DROPDOWN_MENU
) continue;
503 DropdownWindow
*dw
= dynamic_cast<DropdownWindow
*>(w
);
504 assert(dw
!= nullptr);
505 if (pw
->window_class
== dw
->parent_wnd_class
&&
506 pw
->window_number
== dw
->parent_wnd_num
) {
507 int parent_button
= dw
->parent_button
;
509 return parent_button
;