The Battle for Wesnoth  1.19.13+dev
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"
23 #include "gui/widgets/listbox.hpp"
28 #include "gui/widgets/window.hpp"
29 
30 #include "sdl/rect.hpp"
31 #include <functional>
32 
33 namespace gui2::dialogs
34 {
35 REGISTER_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 
65 namespace
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 
87 drop_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 
101 drop_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 
115 {
116  if(!mouse_down_happened_) {
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 
154  } else if(!keep_open_) {
156  }
157 }
158 
160 {
161  mouse_down_happened_ = true;
162 }
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_) {
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
247  if(legacy_menu_mode_) {
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 
322 boost::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:158
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.
Definition: dispatcher.cpp:74
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:289
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.
Definition: scrollbar.cpp:147
scrollbar_base * vertical_scrollbar()
void set_tooltip(const t_string &tooltip)
Base class for all widgets.
Definition: widget.hpp:55
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
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
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:61
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition: window.hpp:395
void set_variable(const std::string &key, const wfl::variant &value)
Definition: window.hpp:422
virtual widget * find_at(const point &coordinate, const bool must_be_active) override
See widget::find_at.
Definition: window.cpp:766
std::size_t i
Definition: function.cpp:1032
This file contains the window object, this object is a top level container which has the event manage...
REGISTER_DIALOG(editor_edit_unit)
@ NOTIFY_MODIFIED
Definition: handler.hpp:175
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:189
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 img(const std::string &src, const std::string &align, bool floating)
Generates a Help markup tag corresponding to an image.
Definition: markup.cpp:31
map_location coordinate
Contains an x and y coordinate used for starting positions in maps.
std::size_t index(std::string_view str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:70
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition: general.hpp:86
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