The Battle for Wesnoth  1.19.5+dev
combobox.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2024
3  by Subhraman Sarkar (babaissarkar) <suvrax@gmail.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 "gettext.hpp"
22 #include "gui/core/log.hpp"
24 #include "gui/widgets/settings.hpp"
25 #include "gui/widgets/window.hpp"
27 #include "wml_exception.hpp"
28 
29 #include <functional>
30 
31 #define LOG_SCOPE_HEADER get_control_type() + " [" + id() + "] " + __func__
32 #define LOG_HEADER LOG_SCOPE_HEADER + ':'
33 
34 
35 namespace gui2
36 {
37 
38 // ------------ WIDGET -----------{
39 
40 REGISTER_WIDGET(combobox)
41 
42 combobox::combobox(const implementation::builder_styled_widget& builder)
43  : text_box_base(builder, type())
44  , max_input_length_(0)
45  , text_x_offset_(0)
46  , text_y_offset_(0)
47  , text_height_(0)
48  , dragging_(false)
49  , values_()
50  , selected_(0)
51 {
52  values_.emplace_back("label", this->get_label());
53 
54  set_wants_mouse_left_double_click();
55 
56  connect_signal<event::MOUSE_MOTION>(std::bind(
57  &combobox::signal_handler_mouse_motion, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5));
58  connect_signal<event::LEFT_BUTTON_DOWN>(std::bind(
59  &combobox::signal_handler_left_button_down, this, std::placeholders::_2, std::placeholders::_3));
60  connect_signal<event::LEFT_BUTTON_UP>(std::bind(
61  &combobox::signal_handler_left_button_up, this, std::placeholders::_2, std::placeholders::_3));
62  connect_signal<event::LEFT_BUTTON_DOUBLE_CLICK>(std::bind(
63  &combobox::signal_handler_left_button_double_click, this, std::placeholders::_2, std::placeholders::_3));
64  connect_signal<event::MOUSE_ENTER>(
65  std::bind(&combobox::signal_handler_mouse_enter, this, std::placeholders::_2, std::placeholders::_3));
66 
67  const auto conf = cast_config_to<combobox_definition>();
68  assert(conf);
69 
70  set_font_size(get_text_font_size());
71  set_font_style(conf->text_font_style);
72 
73  update_offsets();
74 }
75 
76 void combobox::place(const point& origin, const point& size)
77 {
78  // Inherited.
79  styled_widget::place(origin, size);
80 
83 
85 
87 }
88 
90 {
91  // Gather the info
92 
93  // Set the cursor info.
94  const unsigned start = get_selection_start();
95  const int length = get_selection_length();
96 
97  // Set the cursor info.
98  const unsigned edit_start = get_composition_start();
99  const int edit_length = get_composition_length();
100 
102 
103  PangoEllipsizeMode ellipse_mode = PANGO_ELLIPSIZE_NONE;
104  if(!can_wrap()) {
105  if((start + length) > (get_length() / 2)) {
106  ellipse_mode = PANGO_ELLIPSIZE_START;
107  } else {
108  ellipse_mode = PANGO_ELLIPSIZE_END;
109  }
110  }
111  set_ellipse_mode(ellipse_mode);
112 
113  // Set the selection info
114  unsigned start_offset = 0;
115  unsigned end_offset = 0;
116  if(length == 0) {
117  // Do nothing.
118  } else if(length > 0) {
119  start_offset = get_cursor_position(start).x;
120  end_offset = get_cursor_position(start + length).x;
121  } else {
122  start_offset = get_cursor_position(start + length).x;
123  end_offset = get_cursor_position(start).x;
124  }
125 
126  // Set the composition info
127  unsigned comp_start_offset = 0;
128  unsigned comp_end_offset = 0;
129  if(edit_length == 0) {
130  // Do nothing.
131  } else if(edit_length > 0) {
132  comp_start_offset = get_cursor_position(edit_start).x;
133  comp_end_offset = get_cursor_position(edit_start + edit_length).x;
134  } else {
135  comp_start_offset = get_cursor_position(edit_start + edit_length).x;
136  comp_end_offset = get_cursor_position(edit_start).x;
137  }
138 
139  // Set in all canvases
140 
141  const int max_width = get_text_maximum_width() - ICON_SIZE;
142  const int max_height = get_text_maximum_height();
143 
144  for(auto & tmp : get_canvases())
145  {
146 
147  tmp.set_variable("text", wfl::variant(get_value()));
148  tmp.set_variable("text_x_offset", wfl::variant(text_x_offset_));
149  tmp.set_variable("text_y_offset", wfl::variant(text_y_offset_));
150  tmp.set_variable("text_maximum_width", wfl::variant(max_width));
151  tmp.set_variable("text_maximum_height", wfl::variant(max_height));
152 
153  tmp.set_variable("cursor_offset",
154  wfl::variant(get_cursor_position(start + length).x));
155 
156  tmp.set_variable("selection_offset", wfl::variant(start_offset));
157  tmp.set_variable("selection_width", wfl::variant(end_offset - start_offset));
158  tmp.set_variable("text_wrap_mode", wfl::variant(ellipse_mode));
159 
160  tmp.set_variable("composition_offset", wfl::variant(comp_start_offset));
161  tmp.set_variable("composition_width", wfl::variant(comp_end_offset - comp_start_offset));
162 
163  tmp.set_variable("hint_text", wfl::variant(hint_text_));
164  tmp.set_variable("hint_image", wfl::variant(hint_image_));
165  }
166 }
167 
168 void combobox::delete_char(const bool before_cursor)
169 {
170  if(before_cursor) {
171  set_cursor(get_selection_start() - 1, false);
172  }
173 
175 
177 }
178 
180 {
181  if(get_selection_length() == 0) {
182  return;
183  }
184 
185  // If we have a negative range change it to a positive range.
186  // This makes the rest of the algorithms easier.
187  int len = get_selection_length();
188  unsigned start = get_selection_start();
189  if(len < 0) {
190  len = -len;
191  start -= len;
192  }
193 
194  std::string tmp = get_value();
195  set_value(utf8::erase(tmp, start, len));
196  set_cursor(start, false);
197 }
198 
199 void combobox::handle_mouse_selection(point mouse, const bool start_selection)
200 {
201  mouse.x -= get_x();
202  mouse.y -= get_y();
203  // FIXME we don't test for overflow in width
204  if(mouse.x < static_cast<int>(text_x_offset_)
205  || mouse.y < static_cast<int>(text_y_offset_)
206  || mouse.y >= static_cast<int>(text_y_offset_ + text_height_)) {
207  return;
208  }
209 
210  int offset = get_column_line(point(mouse.x - text_x_offset_, mouse.y - text_y_offset_)).x;
211 
212  if(offset < 0) {
213  return;
214  }
215 
216 
217  set_cursor(offset, !start_selection);
218  update_canvas();
219  queue_redraw();
220  dragging_ |= start_selection;
221 }
222 
224 {
225  const auto conf = cast_config_to<combobox_definition>();
226  assert(conf);
227 
229 
230  wfl::map_formula_callable variables;
231  variables.add("height", wfl::variant(get_height()));
232  variables.add("width", wfl::variant(get_width()));
233  variables.add("text_font_height", wfl::variant(text_height_));
234 
235  text_x_offset_ = conf->text_x_offset(variables);
236  text_y_offset_ = conf->text_y_offset(variables);
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_ > 1) {
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 void combobox::set_values(const std::vector<::config>& values, unsigned selected)
273 {
274  assert(selected < values.size());
275  assert(selected_ < values_.size());
276 
277  if(values[selected]["label"] != values_[selected_]["label"]) {
278  queue_redraw();
279  }
280 
281  values_ = values;
283 
285 }
286 
288 {
289  assert(selected < values_.size());
290  assert(selected_ < values_.size());
291 
292  if(selected != selected_) {
293  queue_redraw();
294  }
295 
297 
299  if (fire_event) {
300  fire(event::NOTIFY_MODIFIED, *this, nullptr);
301  }
302 }
303 
305 {
306  unsigned right_border = get_x() + this->get_size().x;
307  unsigned mouse_x = get_mouse_position().x;
308 
309  if ((mouse_x <= right_border) && (mouse_x >= right_border-ICON_SIZE)) {
311  } else {
313  }
314 }
315 
317  bool& /*handled*/)
318 {
320 }
321 
323  bool& handled,
324  const point& coordinate)
325 {
326  DBG_GUI_E << get_control_type() << "[" << id() << "]: " << event << ".";
327 
328  if(dragging_) {
330  } else {
332  }
333 
334  handled = true;
335 }
336 
338  bool& handled)
339 {
340  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
341 
342  /* get_x() is the left border
343  * this->get_size().x is the size of this widget
344  * so get_x() + this->get_size().x is the right border
345  * ICON_SIZE is the size of the icon.*/
346 
347  unsigned right_border = get_x() + this->get_size().x;
348  unsigned mouse_x = get_mouse_position().x;
349 
350  if ((mouse_x <= right_border) && (mouse_x >= right_border-ICON_SIZE)) {
351  // If a button has a retval do the default handling.
352  dialogs::drop_down_menu droplist(this, values_, selected_, false);
353 
354  if(droplist.show()) {
355  const int selected = droplist.selected_item();
356 
357  // Safety check. If the user clicks a selection in the dropdown and moves their mouse away too
358  // quickly, selected_ could be set to -1. This returns in that case, preventing crashes.
359  if(selected < 0) {
360  return;
361  }
362 
363  set_selected(selected, true);
364  }
365  } else {
366  get_window()->keyboard_capture(this);
368 
370  }
371 
372  handled = true;
373 }
374 
376  bool& handled)
377 {
378  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
379 
380  dragging_ = false;
381  handled = true;
382 }
383 
384 void
386  bool& handled)
387 {
388  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
389 
390  select_all();
391  handled = true;
392 }
393 
394 // }---------- DEFINITION ---------{
395 
398 {
399  DBG_GUI_P << "Parsing combobox " << id;
400 
401  load_resolutions<resolution>(cfg);
402 }
403 
405  : resolution_definition(cfg)
406  , text_x_offset(cfg["text_x_offset"])
407  , text_y_offset(cfg["text_y_offset"])
408 {
409  // Note the order should be the same as the enum state_t in combobox.hpp.
410  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_enabled", missing_mandatory_wml_tag("combobox_definition][resolution", "state_enabled")));
411  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_disabled", missing_mandatory_wml_tag("combobox_definition][resolution", "state_disabled")));
412  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_focused", missing_mandatory_wml_tag("combobox_definition][resolution", "state_focused")));
413  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_hovered", missing_mandatory_wml_tag("combobox_definition][resolution", "state_hovered")));
414 }
415 
416 // }---------- BUILDER -----------{
417 
418 namespace implementation
419 {
420 
421 builder_combobox::builder_combobox(const config& cfg)
422  : builder_styled_widget(cfg)
423  , max_input_length(cfg["max_input_length"].to_size_t())
424  , hint_text(cfg["hint_text"].t_str())
425  , hint_image(cfg["hint_image"])
426  , options_()
427 {
428  for(const auto& option : cfg.child_range("option")) {
429  options_.push_back(option);
430  }
431 }
432 
433 std::unique_ptr<widget> builder_combobox::build() const
434 {
435  auto widget = std::make_unique<combobox>(*this);
436 
437  // A combobox doesn't have a label but a text
438  widget->set_value(label_string);
439 
440  if(!options_.empty()) {
441  widget->set_values(options_);
442  }
443 
444  widget->set_max_input_length(max_input_length);
445  widget->set_hint_data(hint_text, hint_image);
446 
447  DBG_GUI_G << "Window builder: placed text box '" << id
448  << "' with definition '" << definition << "'.";
449 
450  return widget;
451 }
452 
453 } // namespace implementation
454 
455 // }------------ END --------------
456 
457 } // namespace gui2
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
child_itors child_range(config_key_type key)
Definition: config.cpp:272
Class for a combobox.
Definition: combobox.hpp:37
void signal_handler_mouse_enter(const event::ui_event, bool &)
Definition: combobox.cpp:316
void signal_handler_left_button_up(const event::ui_event event, bool &handled)
Definition: combobox.cpp:375
std::size_t max_input_length_
The maximum length of the text input.
Definition: combobox.hpp:126
void update_mouse_cursor()
Update the mouse cursor based on whether it is over button area or text area.
Definition: combobox.cpp:304
unsigned selected_
Definition: combobox.hpp:169
void delete_selection() override
Deletes the current selection.
Definition: combobox.cpp:179
void update_offsets()
Updates text_x_offset_ and text_y_offset_.
Definition: combobox.cpp:223
bool dragging_
Is the mouse in dragging mode, this affects selection in mouse move.
Definition: combobox.hpp:159
void signal_handler_mouse_motion(const event::ui_event event, bool &handled, const point &coordinate)
Definition: combobox.cpp:322
void handle_mouse_selection(point mouse, const bool start_selection)
Definition: combobox.cpp:199
virtual void update_canvas() override
Updates the canvas(ses).
Definition: combobox.cpp:89
unsigned const ICON_SIZE
Size of the dropdown icon TODO : Should be dynamically loaded from image.
Definition: combobox.hpp:131
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:165
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:272
unsigned text_y_offset_
The y offset in the widget where the text starts.
Definition: combobox.hpp:146
void signal_handler_left_button_down(const event::ui_event event, bool &handled)
Definition: combobox.cpp:337
void delete_char(const bool before_cursor) override
Deletes the character.
Definition: combobox.cpp:168
std::vector<::config > values_
Definition: combobox.hpp:167
void signal_handler_left_button_double_click(const event::ui_event event, bool &handled)
Definition: combobox.cpp:385
void set_selected(unsigned selected, bool fire_event=true)
Definition: combobox.cpp:287
unsigned text_x_offset_
The x offset in the widget where the text starts.
Definition: combobox.hpp:139
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:162
unsigned text_height_
The height of the text itself.
Definition: combobox.hpp:153
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:76
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_length() const
Wrapper function, returns length of the text in pango column offsets.
point get_column_line(const point &position) const
std::string get_value() const
std::size_t get_composition_start() const
size_t get_composition_length() const
Get length of composition text by IME.
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:1207
void mouse_capture(const bool capture=true)
Definition: window.cpp:1201
map_formula_callable & add(const std::string &key, const variant &value)
Definition: callable.hpp:253
#define LOG_HEADER
Definition: combobox.cpp:32
#define LOG_SCOPE_HEADER
Definition: combobox.cpp:31
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:176
void point(int x, int y)
Draw a single point.
Definition: draw.cpp:202
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:1142
std::string selected
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:158
Generic file dialog.
point get_mouse_position()
Returns the current mouse position.
Definition: helper.cpp:143
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:103
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
#define REGISTER_WIDGET(id)
Wrapper for REGISTER_WIDGET3.
This file contains the settings handling of the widget library.
combobox_definition(const config &cfg)
Definition: combobox.cpp:396
std::vector<::config > options_
Definition: combobox.hpp:258
virtual std::unique_ptr< widget > build() const override
Definition: combobox.cpp:433
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)