The Battle for Wesnoth  1.19.22+dev
combobox.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2024 - 2025
3  by Subhraman Sarkar (babaissarkar) <sbmskmm@protonmail.com>
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 
18 #include "gui/widgets/combobox.hpp"
19 
20 #include "cursor.hpp"
21 #include "gui/core/log.hpp"
23 #include "gui/widgets/window.hpp"
25 #include "wml_exception.hpp"
26 
27 #include <functional>
28 
29 #define LOG_SCOPE_HEADER get_control_type() + " [" + id() + "] " + __func__
30 #define LOG_HEADER LOG_SCOPE_HEADER + ':'
31 
32 
33 namespace gui2
34 {
35 
36 // ------------ WIDGET -----------{
37 
38 REGISTER_WIDGET(combobox)
39 
40 combobox::combobox(const implementation::builder_combobox& builder)
41  : text_box_base(builder, type())
42  , max_input_length_(builder.max_input_length)
43  , text_offset_{0, 0}
44  , text_height_(0)
45  , dragging_(false)
46  , hint_text_(builder.hint_text)
47  , hint_image_(builder.hint_image)
48  , values_()
49  , selected_(0)
50 {
51  values_.emplace_back("label", this->get_label());
52 
53  set_wants_mouse_left_double_click();
54 
55  connect_signal<event::MOUSE_MOTION>(std::bind(
56  &combobox::signal_handler_mouse_motion, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5));
57  connect_signal<event::LEFT_BUTTON_DOWN>(std::bind(
58  &combobox::signal_handler_left_button_down, this, std::placeholders::_2, std::placeholders::_3));
59  connect_signal<event::LEFT_BUTTON_UP>(std::bind(
60  &combobox::signal_handler_left_button_up, this, std::placeholders::_2, std::placeholders::_3));
61  connect_signal<event::LEFT_BUTTON_DOUBLE_CLICK>(std::bind(
62  &combobox::signal_handler_left_button_double_click, this, std::placeholders::_2, std::placeholders::_3));
63  connect_signal<event::MOUSE_ENTER>(
64  std::bind(&combobox::signal_handler_mouse_enter, this, std::placeholders::_2, std::placeholders::_3));
65 
66  const auto conf = cast_config_to<combobox_definition>();
67  assert(conf);
68 
69  set_font_size(get_text_font_size());
70  set_font_style(conf->text_font_style);
71 
72  update_offsets();
73 }
74 
75 void combobox::place(const point& origin, const point& size)
76 {
77  // Inherited.
78  styled_widget::place(origin, size);
79 
82 
84 
86 }
87 
89 {
90  // Gather the info
91 
92  // Set the cursor info.
93  const unsigned start = get_selection_start();
94  const int length = get_selection_length();
95 
96  // Set the cursor info.
97  const unsigned edit_start = get_composition_start();
98  const int edit_length = get_composition_length();
99 
101 
102  PangoEllipsizeMode ellipse_mode = PANGO_ELLIPSIZE_NONE;
103  if(!can_wrap()) {
104  if((start + length) > (get_length() / 2)) {
105  ellipse_mode = PANGO_ELLIPSIZE_START;
106  } else {
107  ellipse_mode = PANGO_ELLIPSIZE_END;
108  }
109  }
110  set_ellipse_mode(ellipse_mode);
111 
112  // Set the selection info
113  unsigned start_offset = 0;
114  unsigned end_offset = 0;
115  if(length == 0) {
116  // Do nothing.
117  } else if(length > 0) {
118  start_offset = get_cursor_position(start).x;
119  end_offset = get_cursor_position(start + length).x;
120  } else {
121  start_offset = get_cursor_position(start + length).x;
122  end_offset = get_cursor_position(start).x;
123  }
124 
125  // Set the composition info
126  unsigned comp_start_offset = 0;
127  unsigned comp_end_offset = 0;
128  if(edit_length == 0) {
129  // Do nothing.
130  } else if(edit_length > 0) {
131  comp_start_offset = get_cursor_position(edit_start).x;
132  comp_end_offset = get_cursor_position(edit_start + edit_length).x;
133  } else {
134  comp_start_offset = get_cursor_position(edit_start + edit_length).x;
135  comp_end_offset = get_cursor_position(edit_start).x;
136  }
137 
138  // Set in all canvases
139 
140  const int max_width = get_text_maximum_width() - ICON_SIZE;
141  const int max_height = get_text_maximum_height();
142 
143  for(auto & tmp : get_canvases())
144  {
145 
146  tmp.set_variable("text", wfl::variant(get_value()));
147  tmp.set_variable("text_x_offset", wfl::variant(text_offset_.x));
148  tmp.set_variable("text_y_offset", wfl::variant(text_offset_.y));
149  tmp.set_variable("text_maximum_width", wfl::variant(max_width));
150  tmp.set_variable("text_maximum_height", wfl::variant(max_height));
151 
152  tmp.set_variable("cursor_offset",
153  wfl::variant(get_cursor_position(start + length).x));
154 
155  tmp.set_variable("selection_offset", wfl::variant(start_offset));
156  tmp.set_variable("selection_width", wfl::variant(end_offset - start_offset));
157  tmp.set_variable("text_wrap_mode", wfl::variant(ellipse_mode));
158 
159  tmp.set_variable("composition_offset", wfl::variant(comp_start_offset));
160  tmp.set_variable("composition_width", wfl::variant(comp_end_offset - comp_start_offset));
161 
162  tmp.set_variable("hint_text", wfl::variant(hint_text_));
163  tmp.set_variable("hint_image", wfl::variant(hint_image_));
164  }
165 }
166 
167 void combobox::delete_char(const bool before_cursor)
168 {
169  if(before_cursor) {
170  set_cursor(get_selection_start() - 1, false);
171  }
172 
174 
176 }
177 
179 {
180  if(get_selection_length() == 0) {
181  return;
182  }
183 
184  // If we have a negative range change it to a positive range.
185  // This makes the rest of the algorithms easier.
186  int len = get_selection_length();
187  unsigned start = get_selection_start();
188  if(len < 0) {
189  len = -len;
190  start -= len;
191  }
192 
193  std::string tmp = get_value();
194  set_value(utf8::erase(tmp, start, len));
195  set_cursor(start, false);
196 }
197 
198 void combobox::handle_mouse_selection(point mouse, const bool start_selection)
199 {
200  mouse.x -= get_x();
201  mouse.y -= get_y();
202  // FIXME we don't test for overflow in width
203  if(mouse.x < text_offset_.x
204  || mouse.y < text_offset_.y
205  || mouse.y >= text_offset_.y + static_cast<int>(text_height_)) {
206  return;
207  }
208 
209  int offset = get_column_line(mouse - text_offset_).x;
210 
211  if(offset < 0) {
212  return;
213  }
214 
215  set_cursor(offset, !start_selection);
216  update_canvas();
217  queue_redraw();
218  dragging_ |= start_selection;
219 }
220 
222 {
223  const auto conf = cast_config_to<combobox_definition>();
224  assert(conf);
225 
227 
228  wfl::map_formula_callable variables;
229  variables.add("height", wfl::variant(get_height()));
230  variables.add("width", wfl::variant(get_width()));
231  variables.add("text_font_height", wfl::variant(text_height_));
232 
233  text_offset_ = {
234  static_cast<int>(conf->text_x_offset(variables)),
235  static_cast<int>(conf->text_y_offset(variables))
236  };
237 
238  // Since this variable doesn't change set it here instead of in update_canvas().
239  for(auto & tmp : get_canvases())
240  {
241  tmp.set_variable("text_font_height", wfl::variant(text_height_));
242  }
243 
244  // Force an update of the canvas since now text_font_height is known.
245  update_canvas();
246 }
247 
248 void combobox::handle_key_clear_line(SDL_Keymod /*modifier*/, bool& handled)
249 {
250  handled = true;
251  set_value("");
252 }
253 
254 void combobox::handle_key_up_arrow(SDL_Keymod /*modifier*/, bool& handled)
255 {
257  handled = true;
258  if (selected_ > 0) {
259  set_selected(selected_ - 1, true);
260  }
261 }
262 
263 void combobox::handle_key_down_arrow(SDL_Keymod /*modifier*/, bool& handled)
264 {
266  handled = true;
267  if (selected_ < values_.size()-1) {
268  set_selected(selected_ + 1, true);
269  }
270 }
271 
272 std::string combobox::get_preset_value(const size_t index) const
273 {
274  auto row_cfg = values_[index];
275  return row_cfg.has_attribute("value") ? row_cfg["value"] : row_cfg["label"];
276 }
277 
278 void combobox::set_values(const std::vector<::config>& values, unsigned selected)
279 {
280  assert(selected < values.size());
281  assert(selected_ < values_.size());
282 
283  values_ = values;
284  selected_ = selected;
285 
286  std::string new_value = get_preset_value(selected_);
287  if(text_box_base::get_value() != new_value) {
288  text_box_base::set_value(new_value);
289  queue_redraw();
290  }
291 }
292 
294 {
295  std::string value = text_box_base::get_value();
296  std::string last_selected = get_preset_value(selected_);
297  return value == last_selected ? selected_ : -1;
298 }
299 
300 void combobox::set_selected(unsigned selected, bool fire_event)
301 {
302  assert(selected < values_.size());
303  assert(selected_ < values_.size());
304 
305  if(selected != selected_) {
306  queue_redraw();
307  }
308 
309  selected_ = selected;
310 
312  if (fire_event) {
313  fire(event::NOTIFY_MODIFIED, *this, nullptr);
314  }
315 }
316 
318 {
319  unsigned right_border = get_x() + this->get_size().x;
320  unsigned mouse_x = get_mouse_position().x;
321 
322  if ((mouse_x <= right_border) && (mouse_x >= right_border-ICON_SIZE)) {
324  } else {
326  }
327 }
328 
330  bool& /*handled*/)
331 {
333 }
334 
336  bool& handled,
337  const point& coordinate)
338 {
339  DBG_GUI_E << get_control_type() << "[" << id() << "]: " << event << ".";
340 
341  if(dragging_) {
343  } else {
345  }
346 
347  handled = true;
348 }
349 
351  bool& handled)
352 {
353  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
354 
355  /* get_x() is the left border
356  * this->get_size().x is the size of this widget
357  * so get_x() + this->get_size().x is the right border
358  * ICON_SIZE is the size of the icon.*/
359 
360  unsigned right_border = get_x() + this->get_size().x;
361  unsigned mouse_x = get_mouse_position().x;
362 
363  if ((mouse_x <= right_border) && (mouse_x >= right_border-ICON_SIZE)) {
364  // If a button has a retval do the default handling.
365  dialogs::drop_down_menu droplist(this, values_, selected_, false);
366 
367  if(droplist.show()) {
368  const int selected = droplist.selected_item();
369 
370  // Safety check. If the user clicks a selection in the dropdown and moves their mouse away too
371  // quickly, selected_ could be set to -1. This returns in that case, preventing crashes.
372  if(selected < 0) {
373  return;
374  }
375 
376  set_selected(selected, true);
377  }
378  } else {
379  get_window()->keyboard_capture(this);
381 
383  }
384 
385  handled = true;
386 }
387 
389  bool& handled)
390 {
391  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
392 
393  dragging_ = false;
394  handled = true;
395 }
396 
397 void
399  bool& handled)
400 {
401  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
402 
403  select_all();
404  handled = true;
405 }
406 
407 // }---------- DEFINITION ---------{
408 
411 {
412  DBG_GUI_P << "Parsing combobox " << id;
413 
414  load_resolutions<resolution>(cfg);
415 }
416 
419  , text_x_offset(cfg["text_x_offset"])
420  , text_y_offset(cfg["text_y_offset"])
421 {
422  // Note the order should be the same as the enum state_t in combobox.hpp.
423  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_enabled", missing_mandatory_wml_tag("combobox_definition][resolution", "state_enabled")));
424  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_disabled", missing_mandatory_wml_tag("combobox_definition][resolution", "state_disabled")));
425  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_focused", missing_mandatory_wml_tag("combobox_definition][resolution", "state_focused")));
426  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_hovered", missing_mandatory_wml_tag("combobox_definition][resolution", "state_hovered")));
427 }
428 
429 // }---------- BUILDER -----------{
430 
431 namespace implementation
432 {
433 
434 builder_combobox::builder_combobox(const config& cfg)
436  , max_input_length(cfg["max_input_length"].to_size_t())
437  , hint_text(cfg["hint_text"].t_str())
438  , hint_image(cfg["hint_image"])
439  , options_()
440 {
441  for(const auto& option : cfg.child_range("option")) {
442  options_.push_back(option);
443  }
444 }
445 
446 std::unique_ptr<widget> builder_combobox::build() const
447 {
448  auto widget = std::make_unique<combobox>(*this);
449 
450  // A combobox doesn't have a label but a text
451  widget->set_value(label_string);
452 
453  if(!options_.empty()) {
454  widget->set_values(options_);
455  }
456 
457  DBG_GUI_G << "Window builder: placed text box '" << id
458  << "' with definition '" << definition << "'.";
459 
460  return widget;
461 }
462 
463 } // namespace implementation
464 
465 // }------------ END --------------
466 
467 } // namespace gui2
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
child_itors child_range(std::string_view key)
Definition: config.cpp:268
Class for a combobox.
Definition: combobox.hpp:37
void signal_handler_mouse_enter(const event::ui_event, bool &)
Definition: combobox.cpp:329
void signal_handler_left_button_up(const event::ui_event event, bool &handled)
Definition: combobox.cpp:388
std::size_t max_input_length_
The maximum length of the text input.
Definition: combobox.hpp:130
std::string get_preset_value(const size_t index) const
Get the index-th value from the preset values that area shown in the dropdown.
Definition: combobox.cpp:272
void update_mouse_cursor()
Update the mouse cursor based on whether it is over button area or text area.
Definition: combobox.cpp:317
unsigned selected_
Definition: combobox.hpp:166
void delete_selection() override
Deletes the current selection.
Definition: combobox.cpp:178
void update_offsets()
Updates text_x_offset_ and text_y_offset_.
Definition: combobox.cpp:221
bool dragging_
Is the mouse in dragging mode, this affects selection in mouse move.
Definition: combobox.hpp:156
void signal_handler_mouse_motion(const event::ui_event event, bool &handled, const point &coordinate)
Definition: combobox.cpp:335
void handle_mouse_selection(point mouse, const bool start_selection)
Definition: combobox.cpp:198
virtual void update_canvas() override
Updates the canvas(ses).
Definition: combobox.cpp:88
unsigned const ICON_SIZE
Size of the dropdown icon TODO : Should be dynamically loaded from image.
Definition: combobox.hpp:135
void handle_key_clear_line(SDL_Keymod modifier, bool &handled) override
Inherited from text_box_base.
Definition: combobox.cpp:248
std::string hint_image_
Image (such as a magnifying glass) that accompanies the help text.
Definition: combobox.hpp:162
void handle_key_down_arrow(SDL_Keymod, bool &handled) override
Inherited from text_box_base.
Definition: combobox.cpp:263
void set_values(const std::vector<::config > &values, unsigned selected=0)
Definition: combobox.cpp:278
int get_selected() const
Returned last selected entry's index or -1 if typed value that does not matches any preset value.
Definition: combobox.cpp:293
void signal_handler_left_button_down(const event::ui_event event, bool &handled)
Definition: combobox.cpp:350
void delete_char(const bool before_cursor) override
Deletes the character.
Definition: combobox.cpp:167
std::vector<::config > values_
Definition: combobox.hpp:164
point text_offset_
The x, y offset in the widget where the text starts.
Definition: combobox.hpp:143
void signal_handler_left_button_double_click(const event::ui_event event, bool &handled)
Definition: combobox.cpp:398
void set_selected(unsigned selected, bool fire_event=true)
Definition: combobox.cpp:300
void handle_key_up_arrow(SDL_Keymod, bool &handled) override
Inherited from text_box_base.
Definition: combobox.cpp:254
std::string hint_text_
Helper text to display (such as "Search") if the combo box is empty.
Definition: combobox.hpp:159
unsigned text_height_
The height of the text itself.
Definition: combobox.hpp:150
virtual const std::string & get_control_type() const override
Inherited from styled_widget, implemented by REGISTER_WIDGET.
virtual void place(const point &origin, const point &size) override
See widget::place.
Definition: combobox.cpp:75
Used by the menu_button widget.
bool show(const unsigned auto_close_time=0)
Shows the window.
bool fire(const ui_event event, widget &target)
Fires an event which has no extra parameters.
Definition: dispatcher.cpp:74
std::vector< canvas > & get_canvases()
int get_text_maximum_width() const
Returns the maximum width available for the text.
int get_text_maximum_height() const
Returns the maximum height available for the text.
virtual void place(const point &origin, const point &size) override
See widget::place.
unsigned int get_text_font_size() const
Resolves and returns the text_font_size.
Abstract base class for text items.
std::size_t get_composition_length() const
Get length of composition text by IME.
std::size_t get_length() const
Wrapper function, see font::pango_text::get_length.
point get_column_line(const point &position) const
std::string get_value() const
std::size_t get_composition_start() const
point get_cursor_position(const unsigned column, const unsigned line=0) const
virtual void set_value(const std::string &text)
The set_value is virtual for the password_box class.
std::size_t get_selection_length() const
void set_maximum_height(const int height, const bool multiline)
void set_ellipse_mode(const PangoEllipsizeMode ellipse_mode)
virtual void set_cursor(const std::size_t offset, const bool select)
Moves the cursor at the wanted position.
void set_maximum_width(const int width)
std::size_t get_selection_start() const
void set_selection_length(const int selection_length)
void set_maximum_length(const std::size_t maximum_length)
void select_all()
Selects all text.
Base class for all widgets.
Definition: widget.hpp:55
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:464
int get_x() const
Definition: widget.cpp:326
unsigned get_width() const
Definition: widget.cpp:336
int get_y() const
Definition: widget.cpp:331
point get_size() const
Returns the size of the widget.
Definition: widget.cpp:316
unsigned get_height() const
Definition: widget.cpp:341
const std::string & id() const
Definition: widget.cpp:110
window * get_window()
Get the parent window.
Definition: widget.cpp:117
virtual bool can_wrap() const
Can the widget wrap.
Definition: widget.cpp:225
void keyboard_capture(widget *widget)
Definition: window.cpp:1197
void mouse_capture(const bool capture=true)
Definition: window.cpp:1191
map_formula_callable & add(const std::string &key, const variant &value)
Definition: callable.hpp:253
#define LOG_HEADER
Definition: combobox.cpp:30
#define LOG_SCOPE_HEADER
Definition: combobox.cpp:29
const config * cfg
Define the common log macros for the gui toolkit.
#define DBG_GUI_G
Definition: log.hpp:41
#define DBG_GUI_P
Definition: log.hpp:66
#define DBG_GUI_E
Definition: log.hpp:35
This file contains the window object, this object is a top level container which has the event manage...
@ NORMAL
Definition: cursor.hpp:28
@ IBEAM
Definition: cursor.hpp:28
void set(CURSOR_TYPE type)
Use the default parameter to reset cursors.
Definition: cursor.cpp:172
EXIT_STATUS start(bool clear_id, const std::string &filename, bool take_screenshot, const std::string &screenshot_filename)
Main interface for launching the editor from the title screen.
int get_max_height(unsigned size, font::family_class fclass, pango_text::FONT_STYLE style)
Returns the maximum glyph height of a font, in pixels.
Definition: text.cpp:966
bool fire_event(const ui_event event, const std::vector< std::pair< widget *, ui_event >> &event_chain, widget *dispatcher, widget *w, F &&... params)
Helper function for fire_event.
ui_event
The event sent to the dispatcher.
Definition: handler.hpp:115
@ NOTIFY_MODIFIED
Definition: handler.hpp:175
Generic file dialog.
point get_mouse_position()
Returns the current mouse position.
Definition: helper.cpp:168
Contains the implementation details for lexical_cast and shouldn't be used directly.
map_location coordinate
Contains an x and y coordinate used for starting positions in maps.
std::string & erase(std::string &str, const std::size_t start, const std::size_t len)
Erases a portion of a UTF-8 string.
Definition: unicode.cpp:105
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:81
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
constexpr auto values
Definition: ranges.hpp:46
#define REGISTER_WIDGET(id)
Wrapper for REGISTER_WIDGET3.
combobox_definition(const config &cfg)
Definition: combobox.cpp:409
std::vector<::config > options_
Definition: combobox.hpp:261
virtual std::unique_ptr< widget > build() const override
Definition: combobox.cpp:446
std::string definition
Parameters for the styled_widget.
std::vector< state_definition > state
Holds a 2D point.
Definition: point.hpp:25
std::string missing_mandatory_wml_tag(const std::string &section, const std::string &tag)
Returns a standard message for a missing wml child (tag).
Add a special kind of assert to validate whether the input from WML doesn't contain any problems that...
#define VALIDATE_WML_CHILD(cfg, key, message)