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
, NULL
, 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.
104 * Create a dropdown menu.
105 * @param parent Parent window.
106 * @param list Dropdown item list.
107 * @param selected Index of the selected item in the list.
108 * @param button Widget of the parent window doing the dropdown.
109 * @param instant_close Close the window when the mouse button is raised.
110 * @param position Topleft position of the dropdown menu window.
111 * @param size Size of the dropdown menu window.
112 * @param wi_colour Colour of the parent widget.
113 * @param scroll Dropdown menu has a scrollbar.
114 * @param widget Widgets of the dropdown menu window.
116 DropdownWindow(Window
*parent
, const DropDownList
*list
, int selected
, int button
, bool instant_close
, const Point
&position
, const Dimension
&size
, Colours wi_colour
, bool scroll
)
117 : Window(&_dropdown_desc
)
119 assert(list
->Length() > 0);
121 this->position
= position
;
123 this->CreateNestedTree();
125 this->vscroll
= this->GetScrollbar(WID_DM_SCROLL
);
127 uint items_width
= size
.width
- (scroll
? NWidgetScrollbar::GetVerticalDimension().width
: 0);
128 NWidgetCore
*nwi
= this->GetWidget
<NWidgetCore
>(WID_DM_ITEMS
);
129 nwi
->SetMinimalSize(items_width
, size
.height
+ 4);
130 nwi
->colour
= wi_colour
;
132 nwi
= this->GetWidget
<NWidgetCore
>(WID_DM_SCROLL
);
133 nwi
->colour
= wi_colour
;
135 this->GetWidget
<NWidgetStacked
>(WID_DM_SHOW_SCROLL
)->SetDisplayedPlane(scroll
? 0 : SZSP_NONE
);
137 this->FinishInitNested(0);
138 CLRBITS(this->flags
, WF_WHITE_BORDER
);
140 /* Total length of list */
142 for (const DropDownListItem
* const *it
= list
->Begin(); it
!= list
->End(); ++it
) {
143 const DropDownListItem
*item
= *it
;
144 list_height
+= item
->Height(items_width
);
147 /* Capacity is the average number of items visible */
148 this->vscroll
->SetCapacity(size
.height
* (uint16
)list
->Length() / list_height
);
149 this->vscroll
->SetCount((uint16
)list
->Length());
151 this->parent_wnd_class
= parent
->window_class
;
152 this->parent_wnd_num
= parent
->window_number
;
153 this->parent_button
= button
;
155 this->selected_index
= selected
;
156 this->click_delay
= 0;
157 this->drag_mode
= true;
158 this->instant_close
= instant_close
;
163 /* Make the dropdown "invisible", so it doesn't affect new window placement.
164 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
165 this->window_class
= WC_INVALID
;
168 Window
*w2
= FindWindowById(this->parent_wnd_class
, this->parent_wnd_num
);
170 Point pt
= _cursor
.pos
;
173 w2
->OnDropdownClose(pt
, this->parent_button
, this->selected_index
, this->instant_close
);
178 virtual Point
OnInitialPosition(int16 sm_width
, int16 sm_height
, int window_number
)
180 return this->position
;
184 * Find the dropdown item under the cursor.
185 * @param value [out] Selected item, if function returns \c true.
186 * @return Cursor points to a dropdown item.
188 bool GetDropDownItem(int &value
)
190 if (GetWidgetFromPos(this, _cursor
.pos
.x
- this->left
, _cursor
.pos
.y
- this->top
) < 0) return false;
192 NWidgetBase
*nwi
= this->GetWidget
<NWidgetBase
>(WID_DM_ITEMS
);
193 int y
= _cursor
.pos
.y
- this->top
- nwi
->pos_y
- 2;
194 int width
= nwi
->current_x
- 4;
195 int pos
= this->vscroll
->GetPosition();
197 const DropDownList
*list
= this->list
;
199 for (const DropDownListItem
* const *it
= list
->Begin(); it
!= list
->End(); ++it
) {
200 /* Skip items that are scrolled up */
201 if (--pos
>= 0) continue;
203 const DropDownListItem
*item
= *it
;
204 int item_height
= item
->Height(width
);
206 if (y
< item_height
) {
207 if (item
->masked
|| !item
->Selectable()) return false;
208 value
= item
->result
;
218 virtual void DrawWidget(const Rect
&r
, int widget
) const
220 if (widget
!= WID_DM_ITEMS
) return;
222 Colours colour
= this->GetWidget
<NWidgetCore
>(widget
)->colour
;
225 int pos
= this->vscroll
->GetPosition();
226 for (const DropDownListItem
* const *it
= this->list
->Begin(); it
!= this->list
->End(); ++it
) {
227 const DropDownListItem
*item
= *it
;
228 int item_height
= item
->Height(r
.right
- r
.left
+ 1);
230 /* Skip items that are scrolled up */
231 if (--pos
>= 0) continue;
233 if (y
+ item_height
< r
.bottom
) {
234 bool selected
= (this->selected_index
== item
->result
);
235 if (selected
) GfxFillRect(r
.left
+ 2, y
, r
.right
- 1, y
+ item_height
- 1, PC_BLACK
);
237 item
->Draw(r
.left
, r
.right
, y
, y
+ item_height
, selected
, colour
);
240 GfxFillRect(r
.left
+ 1, y
, r
.right
- 1, y
+ item_height
- 1, _colour_gradient
[colour
][5], FILLRECT_CHECKER
);
247 virtual void OnClick(Point pt
, int widget
, int click_count
)
249 if (widget
!= WID_DM_ITEMS
) return;
251 if (this->GetDropDownItem(item
)) {
252 this->click_delay
= 4;
253 this->selected_index
= item
;
258 virtual void OnTick()
260 if (this->scrolling
!= 0) {
261 int pos
= this->vscroll
->GetPosition();
263 this->vscroll
->UpdatePosition(this->scrolling
);
266 if (pos
!= this->vscroll
->GetPosition()) {
272 virtual void OnMouseLoop()
274 Window
*w2
= FindWindowById(this->parent_wnd_class
, this->parent_wnd_num
);
280 if (this->click_delay
!= 0 && --this->click_delay
== 0) {
281 /* Make the dropdown "invisible", so it doesn't affect new window placement.
282 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
283 this->window_class
= WC_INVALID
;
286 w2
->OnDropdownSelect(this->parent_button
, this->selected_index
);
291 if (this->drag_mode
) {
294 if (!_left_button_clicked
) {
295 this->drag_mode
= false;
296 if (!this->GetDropDownItem(item
)) {
297 if (this->instant_close
) delete this;
300 this->click_delay
= 2;
302 if (_cursor
.pos
.y
<= this->top
+ 2) {
303 /* Cursor is above the list, set scroll up */
304 this->scrolling
= -1;
306 } else if (_cursor
.pos
.y
>= this->top
+ this->height
- 2) {
307 /* Cursor is below list, set scroll down */
312 if (!this->GetDropDownItem(item
)) return;
315 if (this->selected_index
!= item
) {
316 this->selected_index
= item
;
324 * Show a drop down list.
325 * @param w Parent window for the list.
326 * @param list Prepopulated DropDownList. Will be deleted when the list is
328 * @param selected The initially selected list item.
329 * @param button The widget which is passed to Window::OnDropdownSelect and OnDropdownClose.
330 * Unless you override those functions, this should be then widget index of the dropdown button.
331 * @param wi_rect Coord of the parent drop down button, used to position the dropdown menu.
332 * @param auto_width The width is determined by the widest item in the list,
333 * in this case only one of \a left or \a right is used (depending on text direction).
334 * @param instant_close Set to true if releasing mouse button should close the
335 * list regardless of where the cursor is.
337 void ShowDropDownListAt(Window
*w
, const DropDownList
*list
, int selected
, int button
, Rect wi_rect
, Colours wi_colour
, bool auto_width
, bool instant_close
)
339 DeleteWindowById(WC_DROPDOWN_MENU
, 0);
341 /* The preferred position is just below the dropdown calling widget */
342 int top
= w
->top
+ wi_rect
.bottom
+ 1;
344 /* The preferred width equals the calling widget */
345 uint width
= wi_rect
.right
- wi_rect
.left
+ 1;
347 /* Longest item in the list, if auto_width is enabled */
348 uint max_item_width
= 0;
350 /* Total length of list */
353 for (const DropDownListItem
* const *it
= list
->Begin(); it
!= list
->End(); ++it
) {
354 const DropDownListItem
*item
= *it
;
355 height
+= item
->Height(width
);
356 if (auto_width
) max_item_width
= max(max_item_width
, item
->Width() + 5);
359 /* Check if the status bar is visible, as we don't want to draw over it */
360 int screen_bottom
= GetMainViewBottom();
363 /* Check if the dropdown will fully fit below the widget */
364 if (top
+ height
+ 4 >= screen_bottom
) {
365 /* If not, check if it will fit above the widget */
366 int screen_top
= GetMainViewTop();
367 if (w
->top
+ wi_rect
.top
> screen_top
+ height
) {
368 top
= w
->top
+ wi_rect
.top
- height
- 4;
370 /* If it doesn't fit above the widget, we need to enable a scrollbar... */
371 int avg_height
= height
/ (int)list
->Length();
374 /* ... and choose whether to put the list above or below the widget. */
375 bool put_above
= false;
376 int available_height
= screen_bottom
- w
->top
- wi_rect
.bottom
;
377 if (w
->top
+ wi_rect
.top
- screen_top
> available_height
) {
379 available_height
= w
->top
+ wi_rect
.top
- screen_top
;
383 /* Check at least there is space for one item. */
384 assert(available_height
>= avg_height
);
386 /* And lastly, fit the list... */
387 int rows
= available_height
/ avg_height
;
388 height
= rows
* avg_height
;
390 /* Add space for the scroll bar if we automatically determined
391 * the width of the list. */
392 max_item_width
+= NWidgetScrollbar::GetVerticalDimension().width
;
394 /* ... and set the top position if needed. */
396 top
= w
->top
+ wi_rect
.top
- height
- 4;
401 if (auto_width
) width
= max(width
, max_item_width
);
403 Point dw_pos
= { w
->left
+ (_current_text_dir
== TD_RTL
? wi_rect
.right
+ 1 - (int)width
: wi_rect
.left
), top
};
404 Dimension dw_size
= {width
, (uint
)height
};
405 new DropdownWindow(w
, list
, selected
, button
, instant_close
, dw_pos
, dw_size
, wi_colour
, scroll
);
409 * Show a drop down list.
410 * @param w Parent window for the list.
411 * @param list Prepopulated DropDownList. Will be deleted when the list is
413 * @param selected The initially selected list item.
414 * @param button The widget within the parent window that is used to determine
415 * the list's location.
416 * @param width Override the width determined by the selected widget.
417 * @param auto_width Maximum width is determined by the widest item in the list.
418 * @param instant_close Set to true if releasing mouse button should close the
419 * list regardless of where the cursor is.
421 void ShowDropDownList(Window
*w
, const DropDownList
*list
, int selected
, int button
, uint width
, bool auto_width
, bool instant_close
)
423 /* Our parent's button widget is used to determine where to place the drop
424 * down list window. */
426 NWidgetCore
*nwi
= w
->GetWidget
<NWidgetCore
>(button
);
427 wi_rect
.left
= nwi
->pos_x
;
428 wi_rect
.right
= nwi
->pos_x
+ nwi
->current_x
- 1;
429 wi_rect
.top
= nwi
->pos_y
;
430 wi_rect
.bottom
= nwi
->pos_y
+ nwi
->current_y
- 1;
431 Colours wi_colour
= nwi
->colour
;
433 if ((nwi
->type
& WWT_MASK
) == NWID_BUTTON_DROPDOWN
) {
434 nwi
->disp_flags
|= ND_DROPDOWN_ACTIVE
;
436 w
->LowerWidget(button
);
438 w
->SetWidgetDirty(button
);
441 if (_current_text_dir
== TD_RTL
) {
442 wi_rect
.left
= wi_rect
.right
+ 1 - width
;
444 wi_rect
.right
= wi_rect
.left
+ width
- 1;
448 ShowDropDownListAt(w
, list
, selected
, button
, wi_rect
, wi_colour
, auto_width
, instant_close
);
452 * Show a dropdown menu window near a widget of the parent window.
453 * The result code of the items is their index in the \a strings list.
454 * @param w Parent window that wants the dropdown menu.
455 * @param strings Menu list, end with #INVALID_STRING_ID
456 * @param selected Index of initial selected item.
457 * @param button Button widget number of the parent window \a w that wants the dropdown menu.
458 * @param disabled_mask Bitmask for disabled items (items with their bit set are displayed, but not selectable in the dropdown list).
459 * @param hidden_mask Bitmask for hidden items (items with their bit set are not copied to the dropdown list).
460 * @param width Width of the dropdown menu. If \c 0, use the width of parent widget \a button.
462 void ShowDropDownMenu(Window
*w
, const StringID
*strings
, int selected
, int button
, uint32 disabled_mask
, uint32 hidden_mask
, uint width
)
464 DropDownList
*list
= new DropDownList();
466 for (uint i
= 0; strings
[i
] != INVALID_STRING_ID
; i
++) {
467 if (!HasBit(hidden_mask
, i
)) {
468 *list
->Append() = new DropDownListStringItem(strings
[i
], i
, HasBit(disabled_mask
, i
));
472 /* No entries in the list? */
473 if (list
->Length() == 0) {
478 ShowDropDownList(w
, list
, selected
, button
, width
);
482 * Delete the drop-down menu from window \a pw
483 * @param pw Parent window of the drop-down menu window
484 * @return Parent widget number if the drop-down was found and closed, \c -1 if the window was not found.
486 int HideDropDownMenu(Window
*pw
)
489 FOR_ALL_WINDOWS_FROM_BACK(w
) {
490 if (w
->window_class
!= WC_DROPDOWN_MENU
) continue;
492 DropdownWindow
*dw
= dynamic_cast<DropdownWindow
*>(w
);
494 if (pw
->window_class
== dw
->parent_wnd_class
&&
495 pw
->window_number
== dw
->parent_wnd_num
) {
496 int parent_button
= dw
->parent_button
;
498 return parent_button
;