4 * This file is part of OpenTTD.
5 * 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.
6 * 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.
7 * 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/>.
10 /** @file dropdown.cpp Implementation of the dropdown widget. */
12 #include "../stdafx.h"
13 #include "../window_gui.h"
14 #include "../string_func.h"
15 #include "../strings_func.h"
16 #include "../window_func.h"
17 #include "dropdown_type.h"
19 #include "dropdown_widget.h"
21 #include "../safeguards.h"
24 void DropDownListItem::Draw(int left
, int right
, int top
, int bottom
, bool sel
, int bg_colour
) const
26 int c1
= _colour_gradient
[bg_colour
][3];
27 int c2
= _colour_gradient
[bg_colour
][7];
29 int mid
= top
+ this->Height(0) / 2;
30 GfxFillRect(left
+ 1, mid
- 2, right
- 1, mid
- 2, c1
);
31 GfxFillRect(left
+ 1, mid
- 1, right
- 1, mid
- 1, c2
);
34 uint
DropDownListStringItem::Width() const
37 GetString(buffer
, this->String(), lastof(buffer
));
38 return GetStringBoundingBox(buffer
).width
;
41 void DropDownListStringItem::Draw(int left
, int right
, int top
, int bottom
, bool sel
, int bg_colour
) const
43 DrawString(left
+ WD_FRAMERECT_LEFT
, right
- WD_FRAMERECT_RIGHT
, top
, this->String(), sel
? TC_WHITE
: TC_BLACK
);
47 * Natural sorting comparator function for DropDownList::sort().
48 * @param first Left side of comparison.
49 * @param second Right side of comparison.
50 * @return true if \a first precedes \a second.
51 * @warning All items in the list need to be derivates of DropDownListStringItem.
53 /* static */ int DropDownListStringItem::NatSortFunc(const DropDownListItem
* const *first
, const DropDownListItem
* const * second
)
55 char buffer1
[512], buffer2
[512];
56 GetString(buffer1
, static_cast<const DropDownListStringItem
*>(*first
)->String(), lastof(buffer1
));
57 GetString(buffer2
, static_cast<const DropDownListStringItem
*>(*second
)->String(), lastof(buffer2
));
58 return strnatcmp(buffer1
, buffer2
);
61 StringID
DropDownListParamStringItem::String() const
63 for (uint i
= 0; i
< lengthof(this->decode_params
); i
++) SetDParam(i
, this->decode_params
[i
]);
67 StringID
DropDownListCharStringItem::String() const
69 SetDParamStr(0, this->raw_string
);
73 static const NWidgetPart _nested_dropdown_menu_widgets
[] = {
74 NWidget(NWID_HORIZONTAL
),
75 NWidget(WWT_PANEL
, COLOUR_END
, WID_DM_ITEMS
), SetMinimalSize(1, 1), SetScrollbar(WID_DM_SCROLL
), EndContainer(),
76 NWidget(NWID_SELECTION
, INVALID_COLOUR
, WID_DM_SHOW_SCROLL
),
77 NWidget(NWID_VSCROLLBAR
, COLOUR_END
, WID_DM_SCROLL
),
82 static WindowDesc
_dropdown_desc(
83 WDP_MANUAL
, nullptr, 0, 0,
84 WC_DROPDOWN_MENU
, WC_NONE
,
86 _nested_dropdown_menu_widgets
, lengthof(_nested_dropdown_menu_widgets
)
89 /** Drop-down menu window */
90 struct DropdownWindow
: Window
{
91 WindowClass parent_wnd_class
; ///< Parent window class.
92 WindowNumber parent_wnd_num
; ///< Parent window number.
93 int parent_button
; ///< Parent widget number where the window is dropped from.
94 const DropDownList
*list
; ///< List with dropdown menu items.
95 int selected_index
; ///< Index of the selected item in the list.
96 byte click_delay
; ///< Timer to delay selection.
98 bool instant_close
; ///< Close the window when the mouse button is raised.
99 int scrolling
; ///< If non-zero, auto-scroll the item list (one time).
100 Point position
; ///< Position of the topleft corner of the window.
102 DropDownSyncFocus sync_parent_focus
; ///< Call parent window's OnFocus[Lost]().
105 * Create a dropdown menu.
106 * @param parent Parent window.
107 * @param list Dropdown item list.
108 * @param selected Index of the selected item in the list.
109 * @param button Widget of the parent window doing the dropdown.
110 * @param instant_close Close the window when the mouse button is raised.
111 * @param position Topleft position of the dropdown menu window.
112 * @param size Size of the dropdown menu window.
113 * @param wi_colour Colour of the parent widget.
114 * @param scroll Dropdown menu has a scrollbar.
115 * @param widget Widgets of the dropdown menu window.
117 DropdownWindow(Window
*parent
, const DropDownList
*list
, int selected
, int button
, bool instant_close
, const Point
&position
, const Dimension
&size
, Colours wi_colour
, bool scroll
, DropDownSyncFocus sync_parent_focus
)
118 : Window(&_dropdown_desc
)
120 assert(list
->Length() > 0);
122 this->position
= position
;
123 this->parent_wnd_class
= parent
->window_class
;
124 this->parent_wnd_num
= parent
->window_number
;
125 this->sync_parent_focus
= sync_parent_focus
;
127 this->CreateNestedTree();
129 this->vscroll
= this->GetScrollbar(WID_DM_SCROLL
);
131 uint items_width
= size
.width
- (scroll
? NWidgetScrollbar::GetVerticalDimension().width
: 0);
132 NWidgetCore
*nwi
= this->GetWidget
<NWidgetCore
>(WID_DM_ITEMS
);
133 nwi
->SetMinimalSize(items_width
, size
.height
+ 4);
134 nwi
->colour
= wi_colour
;
136 nwi
= this->GetWidget
<NWidgetCore
>(WID_DM_SCROLL
);
137 nwi
->colour
= wi_colour
;
139 this->GetWidget
<NWidgetStacked
>(WID_DM_SHOW_SCROLL
)->SetDisplayedPlane(scroll
? 0 : SZSP_NONE
);
141 this->FinishInitNested(0);
142 CLRBITS(this->flags
, WF_WHITE_BORDER
);
144 /* Total length of list */
146 for (const DropDownListItem
* const *it
= list
->Begin(); it
!= list
->End(); ++it
) {
147 const DropDownListItem
*item
= *it
;
148 list_height
+= item
->Height(items_width
);
151 /* Capacity is the average number of items visible */
152 this->vscroll
->SetCapacity(size
.height
* (uint16
)list
->Length() / list_height
);
153 this->vscroll
->SetCount((uint16
)list
->Length());
155 this->parent_button
= button
;
157 this->selected_index
= selected
;
158 this->click_delay
= 0;
159 this->drag_mode
= true;
160 this->instant_close
= instant_close
;
165 /* Make the dropdown "invisible", so it doesn't affect new window placement.
166 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
167 this->window_class
= WC_INVALID
;
170 Window
*w2
= FindWindowById(this->parent_wnd_class
, this->parent_wnd_num
);
172 Point pt
= _cursor
.pos
;
175 w2
->OnDropdownClose(pt
, this->parent_button
, this->selected_index
, this->instant_close
);
176 if (_focused_window
== this) {
177 SetFocusedWindow(w2
);
183 virtual Point
OnInitialPosition(int16 sm_width
, int16 sm_height
, int window_number
)
185 return this->position
;
189 * Find the dropdown item under the cursor.
190 * @param value [out] 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 NWidgetBase
*nwi
= this->GetWidget
<NWidgetBase
>(WID_DM_ITEMS
);
198 int y
= _cursor
.pos
.y
- this->top
- nwi
->pos_y
- 2;
199 int width
= nwi
->current_x
- 4;
200 int pos
= this->vscroll
->GetPosition();
202 const DropDownList
*list
= this->list
;
204 for (const DropDownListItem
* const *it
= list
->Begin(); it
!= list
->End(); ++it
) {
205 /* Skip items that are scrolled up */
206 if (--pos
>= 0) continue;
208 const DropDownListItem
*item
= *it
;
209 int item_height
= item
->Height(width
);
211 if (y
< item_height
) {
212 if (item
->masked
|| !item
->Selectable()) return false;
213 value
= item
->result
;
223 virtual void DrawWidget(const Rect
&r
, int widget
) const
225 if (widget
!= WID_DM_ITEMS
) return;
227 Colours colour
= this->GetWidget
<NWidgetCore
>(widget
)->colour
;
230 int pos
= this->vscroll
->GetPosition();
231 for (const DropDownListItem
* const *it
= this->list
->Begin(); it
!= this->list
->End(); ++it
) {
232 const DropDownListItem
*item
= *it
;
233 int item_height
= item
->Height(r
.right
- r
.left
+ 1);
235 /* Skip items that are scrolled up */
236 if (--pos
>= 0) continue;
238 if (y
+ item_height
< r
.bottom
) {
239 bool selected
= (this->selected_index
== item
->result
);
240 if (selected
) GfxFillRect(r
.left
+ 2, y
, r
.right
- 1, y
+ item_height
- 1, PC_BLACK
);
242 item
->Draw(r
.left
, r
.right
, y
, y
+ item_height
, selected
, colour
);
245 GfxFillRect(r
.left
+ 1, y
, r
.right
- 1, y
+ item_height
- 1, _colour_gradient
[colour
][5], FILLRECT_CHECKER
);
252 virtual void OnClick(Point pt
, int widget
, int click_count
)
254 if (widget
!= WID_DM_ITEMS
) return;
256 if (this->GetDropDownItem(item
)) {
257 this->click_delay
= 4;
258 this->selected_index
= item
;
263 virtual void OnTick()
265 if (this->scrolling
!= 0) {
266 int pos
= this->vscroll
->GetPosition();
268 this->vscroll
->UpdatePosition(this->scrolling
);
271 if (pos
!= this->vscroll
->GetPosition()) {
277 virtual void OnMouseLoop()
279 Window
*w2
= FindWindowById(this->parent_wnd_class
, this->parent_wnd_num
);
285 if (this->click_delay
!= 0 && --this->click_delay
== 0) {
286 /* Make the dropdown "invisible", so it doesn't affect new window placement.
287 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
288 this->window_class
= WC_INVALID
;
291 w2
->OnDropdownSelect(this->parent_button
, this->selected_index
);
296 if (this->drag_mode
) {
299 if (!_left_button_clicked
) {
300 this->drag_mode
= false;
301 if (!this->GetDropDownItem(item
)) {
302 if (this->instant_close
) delete this;
305 this->click_delay
= 2;
307 if (_cursor
.pos
.y
<= this->top
+ 2) {
308 /* Cursor is above the list, set scroll up */
309 this->scrolling
= -1;
311 } else if (_cursor
.pos
.y
>= this->top
+ this->height
- 2) {
312 /* Cursor is below list, set scroll down */
317 if (!this->GetDropDownItem(item
)) return;
320 if (this->selected_index
!= item
) {
321 this->selected_index
= item
;
327 virtual void OnFocus(Window
*previously_focused_window
)
329 if (this->sync_parent_focus
& DDSF_RECV_FOCUS
) {
330 Window
*parent
= FindWindowById(this->parent_wnd_class
, this->parent_wnd_num
);
331 if (parent
) parent
->OnFocus(previously_focused_window
);
335 virtual void OnFocusLost(Window
*newly_focused_window
)
337 if (this->sync_parent_focus
& DDSF_LOST_FOCUS
) {
338 Window
*parent
= FindWindowById(this->parent_wnd_class
, this->parent_wnd_num
);
339 if (parent
) parent
->OnFocusLost(newly_focused_window
);
345 * Show a drop down list.
346 * @param w Parent window for the list.
347 * @param list Prepopulated DropDownList. Will be deleted when the list is
349 * @param selected The initially selected list item.
350 * @param button The widget which is passed to Window::OnDropdownSelect and OnDropdownClose.
351 * Unless you override those functions, this should be then widget index of the dropdown button.
352 * @param wi_rect Coord of the parent drop down button, used to position the dropdown menu.
353 * @param auto_width The width is determined by the widest item in the list,
354 * in this case only one of \a left or \a right is used (depending on text direction).
355 * @param instant_close Set to true if releasing mouse button should close the
356 * list regardless of where the cursor is.
358 void ShowDropDownListAt(Window
*w
, const DropDownList
*list
, int selected
, int button
, Rect wi_rect
, Colours wi_colour
, bool auto_width
, bool instant_close
, DropDownSyncFocus sync_parent_focus
)
360 DeleteWindowById(WC_DROPDOWN_MENU
, 0);
362 /* The preferred position is just below the dropdown calling widget */
363 int top
= w
->top
+ wi_rect
.bottom
+ 1;
365 /* The preferred width equals the calling widget */
366 uint width
= wi_rect
.right
- wi_rect
.left
+ 1;
368 /* Longest item in the list, if auto_width is enabled */
369 uint max_item_width
= 0;
371 /* Total length of list */
374 for (const DropDownListItem
* const *it
= list
->Begin(); it
!= list
->End(); ++it
) {
375 const DropDownListItem
*item
= *it
;
376 height
+= item
->Height(width
);
377 if (auto_width
) max_item_width
= max(max_item_width
, item
->Width() + 5);
380 /* Check if the status bar is visible, as we don't want to draw over it */
381 int screen_bottom
= GetMainViewBottom();
384 /* Check if the dropdown will fully fit below the widget */
385 if (top
+ height
+ 4 >= screen_bottom
) {
386 /* If not, check if it will fit above the widget */
387 int screen_top
= GetMainViewTop();
388 if (w
->top
+ wi_rect
.top
> screen_top
+ height
) {
389 top
= w
->top
+ wi_rect
.top
- height
- 4;
391 /* If it doesn't fit above the widget, we need to enable a scrollbar... */
392 int avg_height
= height
/ (int)list
->Length();
395 /* ... and choose whether to put the list above or below the widget. */
396 bool put_above
= false;
397 int available_height
= screen_bottom
- w
->top
- wi_rect
.bottom
;
398 if (w
->top
+ wi_rect
.top
- screen_top
> available_height
) {
400 available_height
= w
->top
+ wi_rect
.top
- screen_top
;
404 /* Check at least there is space for one item. */
405 assert(available_height
>= avg_height
);
407 /* And lastly, fit the list... */
408 int rows
= available_height
/ avg_height
;
409 height
= rows
* avg_height
;
411 /* Add space for the scroll bar if we automatically determined
412 * the width of the list. */
413 max_item_width
+= NWidgetScrollbar::GetVerticalDimension().width
;
415 /* ... and set the top position if needed. */
417 top
= w
->top
+ wi_rect
.top
- height
- 4;
422 if (auto_width
) width
= max(width
, max_item_width
);
424 Point dw_pos
= { w
->left
+ (_current_text_dir
== TD_RTL
? wi_rect
.right
+ 1 - (int)width
: wi_rect
.left
), top
};
425 Dimension dw_size
= {width
, (uint
)height
};
426 new DropdownWindow(w
, list
, selected
, button
, instant_close
, dw_pos
, dw_size
, wi_colour
, scroll
, sync_parent_focus
);
430 * Show a drop down list.
431 * @param w Parent window for the list.
432 * @param list Prepopulated DropDownList. Will be deleted when the list is
434 * @param selected The initially selected list item.
435 * @param button The widget within the parent window that is used to determine
436 * the list's location.
437 * @param width Override the width determined by the selected widget.
438 * @param auto_width Maximum width is determined by the widest item in the list.
439 * @param instant_close Set to true if releasing mouse button should close the
440 * list regardless of where the cursor is.
442 void ShowDropDownList(Window
*w
, const DropDownList
*list
, int selected
, int button
, uint width
, bool auto_width
, bool instant_close
, DropDownSyncFocus sync_parent_focus
)
444 /* Our parent's button widget is used to determine where to place the drop
445 * down list window. */
447 NWidgetCore
*nwi
= w
->GetWidget
<NWidgetCore
>(button
);
448 wi_rect
.left
= nwi
->pos_x
;
449 wi_rect
.right
= nwi
->pos_x
+ nwi
->current_x
- 1;
450 wi_rect
.top
= nwi
->pos_y
;
451 wi_rect
.bottom
= nwi
->pos_y
+ nwi
->current_y
- 1;
452 Colours wi_colour
= nwi
->colour
;
454 if ((nwi
->type
& WWT_MASK
) == NWID_BUTTON_DROPDOWN
) {
455 nwi
->disp_flags
|= ND_DROPDOWN_ACTIVE
;
457 w
->LowerWidget(button
);
459 w
->SetWidgetDirty(button
);
462 if (_current_text_dir
== TD_RTL
) {
463 wi_rect
.left
= wi_rect
.right
+ 1 - width
;
465 wi_rect
.right
= wi_rect
.left
+ width
- 1;
469 ShowDropDownListAt(w
, list
, selected
, button
, wi_rect
, wi_colour
, auto_width
, instant_close
, sync_parent_focus
);
473 * Show a dropdown menu window near a widget of the parent window.
474 * The result code of the items is their index in the \a strings list.
475 * @param w Parent window that wants the dropdown menu.
476 * @param strings Menu list, end with #INVALID_STRING_ID
477 * @param selected Index of initial selected item.
478 * @param button Button widget number of the parent window \a w that wants the dropdown menu.
479 * @param disabled_mask Bitmask for disabled items (items with their bit set are displayed, but not selectable in the dropdown list).
480 * @param hidden_mask Bitmask for hidden items (items with their bit set are not copied to the dropdown list).
481 * @param width Width of the dropdown menu. If \c 0, use the width of parent widget \a button.
483 void ShowDropDownMenu(Window
*w
, const StringID
*strings
, int selected
, int button
, uint32 disabled_mask
, uint32 hidden_mask
, uint width
, DropDownSyncFocus sync_parent_focus
)
485 DropDownList
*list
= new DropDownList();
487 for (uint i
= 0; strings
[i
] != INVALID_STRING_ID
; i
++) {
488 if (!HasBit(hidden_mask
, i
)) {
489 *list
->Append() = new DropDownListStringItem(strings
[i
], i
, HasBit(disabled_mask
, i
));
493 /* No entries in the list? */
494 if (list
->Length() == 0) {
499 ShowDropDownList(w
, list
, selected
, button
, width
, false, false, sync_parent_focus
);
503 * Delete the drop-down menu from window \a pw
504 * @param pw Parent window of the drop-down menu window
505 * @return Parent widget number if the drop-down was found and closed, \c -1 if the window was not found.
507 int HideDropDownMenu(Window
*pw
)
510 FOR_ALL_WINDOWS_FROM_BACK(w
) {
511 if (w
->window_class
!= WC_DROPDOWN_MENU
) continue;
513 DropdownWindow
*dw
= dynamic_cast<DropdownWindow
*>(w
);
514 assert(dw
!= nullptr);
515 if (pw
->window_class
== dw
->parent_wnd_class
&&
516 pw
->window_number
== dw
->parent_wnd_num
) {
517 int parent_button
= dw
->parent_button
;
519 return parent_button
;
526 void GetParentWindowInfo(Window
*w
, WindowClass
&parent_wc
, WindowNumber
&parent_wn
)
528 DropdownWindow
*dw
= dynamic_cast<DropdownWindow
*>(w
);
529 assert(dw
!= nullptr);
530 parent_wc
= dw
->parent_wnd_class
;
531 parent_wn
= dw
->parent_wnd_num
;