The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
drop_down_menu.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2008 - 2025
3 by Mark de Wever <koraq@xs4all.nl>
4 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY.
12
13 See the COPYING file for more details.
14*/
15
16#define GETTEXT_DOMAIN "wesnoth-lib"
17
19
22#include "gui/widgets/image.hpp"
29
30#include "sdl/rect.hpp"
31#include <functional>
32
33namespace gui2::dialogs
34{
35REGISTER_DIALOG(drop_down_menu)
36
38 : checkbox()
39 , radio()
40 , icon(cfg["icon"].str())
41 , image()
42 , label(cfg["label"].t_str())
43 , details()
44 , tooltip(cfg["tooltip"].t_str())
45{
46 // Checkboxes take precedence in column 1
47 if(cfg.has_attribute("checkbox")) {
48 checkbox = cfg["checkbox"].to_bool(false);
49 }
50
51 if(cfg.has_attribute("radio")) {
52 radio = cfg["radio"].to_bool(false);
53 }
54
55 // Images take precedence in column 2
56 if(cfg.has_attribute("image")) {
57 image = cfg["image"].str();
58 }
59
60 if(cfg.has_attribute("details")) {
61 details = cfg["details"].t_str();
62 }
63}
64
65namespace
66{
67 void callback_flip_embedded_toggle(window& window)
68 {
69 listbox& list = window.find_widget<listbox>("list", true);
70
71 /* If the currently selected row has a toggle button, toggle it.
72 * Note this cannot be handled in mouse_up_callback since at that point the new row selection has not registered,
73 * meaning the currently selected row's button is toggled.
74 */
75 grid* row_grid = list.get_row_grid(list.get_selected_row());
76 if(toggle_button* checkbox = row_grid->find_widget<toggle_button>("checkbox", false, false)) {
77 checkbox->set_value_bool(!checkbox->get_value_bool(), true);
78 }
79 }
80
81 void resize_callback(window& window)
82 {
84 }
85}
86
87drop_down_menu::drop_down_menu(styled_widget* parent, const std::vector<config>& items, int selected_item, bool keep_open)
88 : modal_dialog(window_id())
89 , parent_(parent)
90 , items_(items.begin(), items.end())
91 , radio_manager_()
92 , button_pos_(parent->get_rectangle())
93 , selected_item_(selected_item)
94 , selected_item_pos_(-1, -1)
95 , use_markup_(parent->get_use_markup())
96 , keep_open_(keep_open)
97 , mouse_down_happened_(false)
98{
99}
100
101drop_down_menu::drop_down_menu(rect button_pos, const std::vector<config>& items, int selected_item, bool use_markup, bool keep_open)
102 : modal_dialog(window_id())
103 , parent_(nullptr)
104 , items_(items.begin(), items.end())
105 , radio_manager_()
106 , button_pos_(button_pos)
107 , selected_item_(selected_item)
108 , use_markup_(use_markup)
109 , keep_open_(keep_open)
110 , mouse_down_happened_(false)
111{
112}
113
114void drop_down_menu::mouse_up_callback(bool&, bool&, const point& coordinate)
115{
117 return;
118 }
119
120 listbox& list = find_widget<listbox>("list", true);
121
122 /* Disregard clicks on scrollbars and toggle buttons so the dropdown menu can be scrolled or have an embedded
123 * toggle button selected without the menu closing.
124 *
125 * This works since this mouse_up_callback function is called before widgets' left-button-up handlers.
126 *
127 * Additionally, this is done before row deselection so selecting/deselecting a toggle button doesn't also leave
128 * the list with no row visually selected. Oddly, the visual deselection doesn't seem to cause any crashes, and
129 * the previously selected row is reselected when the menu is opened again. Still, it's odd to see your selection
130 * vanish.
131 */
133 return;
134 }
135
136 if(dynamic_cast<toggle_button*>(find_at(coordinate, true)) != nullptr) {
137 return;
138 }
139
140 /* FIXME: This dialog uses a listbox with 'has_minimum = false'. This allows a listbox to have 0 or 1 selections,
141 * and selecting the same entry toggles that entry's state (ie, if it was selected, it will be deselected). Because
142 * of this, selecting the same entry in the dropdown list essentially sets the list's selected row to -1, causing problems.
143 *
144 * In order to work around this, we first manually deselect the selected entry here. This handler is called *before*
145 * the listbox's click handler, and as such the selected item will remain toggled on when the click handler fires.
146 */
147 const int sel = list.get_selected_row();
148 if(sel >= 0) {
149 list.select_row(sel, false);
150 }
151
152 if(!get_rectangle().contains(coordinate)) {
154 } else if(!keep_open_) {
156 }
157}
158
163
165{
166 set_variable("button_x", wfl::variant(button_pos_.x));
167 set_variable("button_y", wfl::variant(button_pos_.y));
168 set_variable("button_w", wfl::variant(button_pos_.w));
169 set_variable("button_h", wfl::variant(button_pos_.h));
170
171 listbox& list = find_widget<listbox>("list", true);
172
173 for(const auto& entry : items_) {
175 widget_item item;
176
177 //
178 // These widgets can be initialized here since they don't require widget type swapping.
179 //
180 item["use_markup"] = utils::bool_string(use_markup_);
181 if(!entry.checkbox) {
182 item["label"] = entry.icon;
183 data.emplace("icon", item);
184 }
185
186 if(!entry.image) {
187 item["label"] = entry.label;
188 data.emplace("label", item);
189 }
190
191 if(entry.details) {
192 item["label"] = *entry.details;
193 data.emplace("details", item);
194 }
195
196 grid& new_row = list.add_row(data);
197 grid& mi_grid = new_row.find_widget<grid>("menu_item");
198
199 // Set the tooltip on the whole panel
200 new_row.find_widget<toggle_panel>("panel").set_tooltip(entry.tooltip);
201
202 if(entry.checkbox) {
203 auto checkbox = build_single_widget_instance<toggle_button>(config{"definition", "no_label"});
204 checkbox->set_id("checkbox");
205 checkbox->set_value_bool(*entry.checkbox);
206 checkbox->set_linked_group("icons");
207
208 // Fire a NOTIFIED_MODIFIED event in the parent widget when the toggle state changes
209 if(parent_) {
210 connect_signal_notify_modified(
211 *checkbox, [this](auto&&...) { parent_->fire(event::NOTIFY_MODIFIED, *parent_, nullptr); });
212 }
213
214 // TODO: remove this block once the main UI uses GUI2
215 else if(legacy_menu_mode_) {
216 // For the in-game theme menus, it's easier to disable explicit UI interaction with
217 // the checkboxes; most of the time we want the dialog to close regardless of whether
218 // you selected them directly or they were toggled by the row click handler.
219 checkbox->set_active(false);
220
221 // We still register a callback for cases where the dialog *is* explicitly kept open,
222 // since we need to handle the resulting action here rather than after the menu closes.
223 if(keep_open_) {
224 connect_signal_notify_modified(*checkbox, [this, index = list.get_item_count() - 1](auto&&...) {
225 if(theme_transition_toggle_callback_) {
226 theme_transition_toggle_callback_(index);
227 }
228 });
229 }
230 }
231
232 mi_grid.swap_child("icon", std::move(checkbox), false);
233 }
234
235 if(entry.radio) {
236 // Initialize the group manager as soon as we see a radio button
237 if(!radio_manager_) {
238 radio_manager_.emplace();
239 }
240
241 auto radio = build_single_widget_instance<toggle_button>(config{"definition", "radio_no_label"});
242 radio->set_id("checkbox"); // Yes, the id should be 'checkbox'
243 radio->set_value_bool(*entry.radio);
244 radio->set_linked_group("icons");
245
246 // TODO: remove this block once the main UI uses GUI2
248 // See comment above
249 radio->set_active(false);
250 }
251
252 // Associate this widget with its current row index
253 auto row_index = list.get_item_count() - 1;
254 radio_manager_->add_member(radio.get(), row_index);
255
256 mi_grid.swap_child("icon", std::move(radio), false);
257 }
258
259 if(entry.image) {
260 auto img = build_single_widget_instance<image>();
261 img->set_label(*entry.image);
262
263 mi_grid.swap_child("label", std::move(img), false);
264 }
265 }
266
267 if(radio_manager_) {
268 radio_manager_->on_modified([this](widget&, int index) {
269 // Fire a NOTIFIED_MODIFIED event in the parent widget when the toggle state changes
270 // TODO: verify whether this is the best way to pass radio button events up to the parent
271 if(parent_) {
272 parent_->fire(event::NOTIFY_MODIFIED, *parent_, nullptr);
273 }
274
275 // TODO: remove this block once the main UI uses GUI2
276 else if(legacy_menu_mode_ && keep_open_ && theme_transition_toggle_callback_) {
277 theme_transition_toggle_callback_(index);
278 }
279 });
280 }
281
282 if(selected_item_ >= 0 && static_cast<unsigned>(selected_item_) < list.get_item_count()) {
283 list.select_row(selected_item_);
284 }
285
286 keyboard_capture(&list);
287
288 // Dismiss on clicking outside the window.
289 connect_signal<event::SDL_LEFT_BUTTON_UP>(
290 std::bind(&drop_down_menu::mouse_up_callback, this, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5), event::dispatcher::front_child);
291
292 connect_signal<event::SDL_RIGHT_BUTTON_UP>(
293 std::bind(&drop_down_menu::mouse_up_callback, this, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5), event::dispatcher::front_child);
294
295 connect_signal<event::SDL_LEFT_BUTTON_DOWN>(
297
298 // Dismiss on resize.
299 connect_signal<event::SDL_VIDEO_RESIZE>(
300 [this](auto&&...) { resize_callback(*this); }, event::dispatcher::front_child);
301
302 // Handle embedded button toggling.
304 [this](auto&&...) { callback_flip_embedded_toggle(*this); });
305}
306
308{
309 const listbox& list = find_widget<listbox>("list", true);
311 if(selected_item_ != -1) {
312 const grid* row_grid = list.get_row_grid(selected_item_);
313 if(row_grid) {
314 selected_item_pos_.x = row_grid->get_x();
315 selected_item_pos_.y = row_grid->get_y();
316 }
317 } else {
319 }
320}
321
322boost::dynamic_bitset<> drop_down_menu::get_toggle_states() const
323{
324 const listbox& list = find_widget<const listbox>("list", true);
325 boost::dynamic_bitset<> states;
326
327 for(unsigned i = 0; i < list.get_item_count(); ++i) {
328 const grid* row_grid = list.get_row_grid(i);
329
330 if(const toggle_button* checkbox = row_grid->find_widget<const toggle_button>("checkbox", false, false)) {
331 states.push_back(checkbox->get_value_bool());
332 } else {
333 states.push_back(false);
334 }
335 }
336
337 return states;
338}
339
340} // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
bool has_attribute(std::string_view key) const
Definition config.cpp:157
Used by the menu_button widget.
std::vector< entry_data > items_
Configuration of each row.
virtual void post_show() override
Actions to be taken after the window has been shown.
bool keep_open_
Whether to keep this dialog open after a click occurs not handled by special exceptions such as scrol...
utils::optional< gui2::group< int > > radio_manager_
Manages radio buttons, if present.
virtual void pre_show() override
Actions to be taken before showing the window.
void mouse_up_callback(bool &, bool &, const point &coordinate)
drop_down_menu(styled_widget *parent, const std::vector< config > &items, int selected_item, bool keep_open)
Menu was invoked from a widget (currently a [multi]menu_button).
boost::dynamic_bitset get_toggle_states() const
If a toggle button widget is present, returns the toggled state of each row's button.
rect button_pos_
The screen location of the menu_button button that triggered this droplist.
bool mouse_down_happened_
When menu is invoked on a long-touch timer, a following mouse-up event will close it.
styled_widget * parent_
The widget that invoked this dialog, if applicable.
bool legacy_menu_mode_
If true, enables special handling of embedded toggle buttons.
Abstract base class for all modal dialogs.
At the moment two kinds of tips are known:
Definition tooltip.cpp:41
bool fire(const ui_event event, widget &target)
Fires an event which has no extra parameters.
Base container class.
Definition grid.hpp:32
std::unique_ptr< widget > swap_child(const std::string &id, std::unique_ptr< widget > w, const bool recurse, widget *new_parent=nullptr)
Exchanges a child in the grid.
Definition grid.cpp:101
The listbox class.
Definition listbox.hpp:41
grid & add_row(const widget_item &item, const int index=-1)
When an item in the list is selected by the user we need to update the state.
Definition listbox.cpp:92
const grid * get_row_grid(const unsigned row) const
Returns the grid of the wanted row.
Definition listbox.cpp:256
bool select_row(const unsigned row, const bool select=true)
Selects a row.
Definition listbox.cpp:267
int get_selected_row() const
Returns the first selected row.
Definition listbox.cpp:290
unsigned get_item_count() const
Returns the number of items in the listbox.
Definition listbox.cpp:153
virtual unsigned get_state() const override
See styled_widget::get_state.
scrollbar_base * vertical_scrollbar()
void set_tooltip(const t_string &tooltip)
Base class for all widgets.
Definition widget.hpp:55
T * find_widget(const std::string_view id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition widget.hpp:747
int get_x() const
Definition widget.cpp:326
int get_y() const
Definition widget.cpp:331
rect get_rectangle() const
Gets the bounding rectangle of the widget on the screen.
Definition widget.cpp:321
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition window.hpp:59
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition window.hpp:387
void set_variable(const std::string &key, const wfl::variant &value)
Definition window.hpp:409
virtual widget * find_at(const point &coordinate, const bool must_be_active) override
See widget::find_at.
Definition window.cpp:765
const config * cfg
This file contains the window object, this object is a top level container which has the event manage...
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
std::map< std::string, widget_item > widget_data
Definition widget.hpp:36
std::map< std::string, t_string > widget_item
Definition widget.hpp:33
@ OK
Dialog was closed with the OK button.
Definition retval.hpp:35
@ CANCEL
Dialog was closed with the CANCEL button.
Definition retval.hpp:38
Functions to load and save images from/to disk.
std::string bool_string(const bool value)
Converts a bool value to 'true' or 'false'.
std::string_view data
Definition picture.cpp:188
Contains the SDL_Rect helper code.
Holds a 2D point.
Definition point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition rect.hpp:49
#define i