The Battle for Wesnoth  1.19.3+dev
multiline_text.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2023 - 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 
19 
20 #include "gui/core/log.hpp"
22 #include "gui/widgets/window.hpp"
24 #include "font/text.hpp"
25 #include "wml_exception.hpp"
26 #include "gettext.hpp"
27 
28 #include <functional>
29 
30 #define LOG_SCOPE_HEADER get_control_type() + " [" + id() + "] " + __func__
31 #define LOG_HEADER LOG_SCOPE_HEADER + ':'
32 
33 namespace gui2
34 {
35 
36 // ------------ WIDGET -----------{
37 
38 REGISTER_WIDGET(multiline_text)
39 
40 multiline_text::multiline_text(const implementation::builder_styled_widget& builder)
41  : text_box_base(builder, type())
42  , history_()
43  , max_input_length_(0)
44  , text_x_offset_(0)
45  , text_y_offset_(0)
46  , text_height_(0)
47  , dragging_(false)
48 {
49  set_wants_mouse_left_double_click();
50 
51  connect_signal<event::MOUSE_MOTION>(std::bind(
52  &multiline_text::signal_handler_mouse_motion, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5));
53  connect_signal<event::LEFT_BUTTON_DOWN>(std::bind(
54  &multiline_text::signal_handler_left_button_down, this, std::placeholders::_2, std::placeholders::_3));
55  connect_signal<event::LEFT_BUTTON_UP>(std::bind(
56  &multiline_text::signal_handler_left_button_up, this, std::placeholders::_2, std::placeholders::_3));
57  connect_signal<event::LEFT_BUTTON_DOUBLE_CLICK>(std::bind(
58  &multiline_text::signal_handler_left_button_double_click, this, std::placeholders::_2, std::placeholders::_3));
59 
60  const auto conf = cast_config_to<multiline_text_definition>();
61  assert(conf);
62 
63  set_font_size(get_text_font_size());
64  set_font_style(conf->text_font_style);
65 
66  update_offsets();
67 }
68 
69 void multiline_text::place(const point& origin, const point& size)
70 {
71  // Inherited.
72  styled_widget::place(origin, size);
73 
76 
78 
80 }
81 
83 {
84  /***** Gather the info *****/
85 
86  // Set the cursor info.
87  const unsigned start = get_selection_start();
88  const int length = static_cast<int>(get_selection_length());
89 
90  // Set the composition info.
91  const unsigned edit_start = get_composition_start();
92  const int edit_length = get_composition_length();
93 
95 
96  unsigned comp_start_offset = 0;
97  unsigned comp_end_offset = 0;
98 
99  if(edit_length == 0) {
100  // Do nothing.
101  } else if(edit_length > 0) {
102  comp_start_offset = get_cursor_position(edit_start).x;
103  comp_end_offset = get_cursor_position(edit_start + edit_length).x;
104  } else {
105  comp_start_offset = get_cursor_position(edit_start + edit_length).x;
106  comp_end_offset = get_cursor_position(edit_start).x;
107  }
108 
109  // Set the selection info
110  unsigned start_offset = 0;
111  unsigned end_offset = 0;
112  if(length == 0) {
113  start_offset = start;
114  end_offset = start_offset;
115  } else if(length > 0) {
116  start_offset = start;
117  end_offset = start + length;
118  } else {
119  start_offset = start + length;
120  end_offset = start;
121  }
122 
123  /***** Set in all canvases *****/
124 
125  const int max_width = get_text_maximum_width();
126  const int max_height = get_text_maximum_height();
127  const point cpos = get_cursor_pos_from_index(start + length);
128 
129  for(auto & tmp : get_canvases())
130  {
131 
132  tmp.set_variable("text", wfl::variant(get_value()));
133  tmp.set_variable("text_x_offset", wfl::variant(text_x_offset_));
134  tmp.set_variable("text_y_offset", wfl::variant(text_y_offset_));
135  tmp.set_variable("text_maximum_width", wfl::variant(max_width));
136  tmp.set_variable("text_maximum_height", wfl::variant(max_height));
137  tmp.set_variable("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
138 
139  tmp.set_variable("editable", wfl::variant(is_editable()));
140 
141  tmp.set_variable("highlight_start", wfl::variant(start_offset));
142  tmp.set_variable("highlight_end", wfl::variant(end_offset));
143 
144  tmp.set_variable("cursor_offset_x", wfl::variant(cpos.x));
145  tmp.set_variable("cursor_offset_y", wfl::variant(cpos.y));
146 
147  tmp.set_variable("composition_offset", wfl::variant(comp_start_offset));
148  tmp.set_variable("composition_width", wfl::variant(comp_end_offset - comp_start_offset));
149 
150  tmp.set_variable("hint_text", wfl::variant(hint_text_));
151  tmp.set_variable("hint_image", wfl::variant(hint_image_));
152  }
153 }
154 
155 void multiline_text::delete_char(const bool before_cursor)
156 {
157  if(!is_editable()) {
158  return;
159  }
160 
161  if(before_cursor) {
162  set_cursor(get_selection_start() - 1, false);
163  }
164 
166 
168 }
169 
171 {
172  if(get_selection_length() == 0 || (!is_editable()) ) {
173  return;
174  }
175 
176  // If we have a negative range change it to a positive range.
177  // This makes the rest of the algorithms easier.
178  int len = get_selection_length();
179  unsigned start = get_selection_start();
180  if(len < 0) {
181  len = -len;
182  start -= len;
183  }
184 
185  std::string tmp = get_value();
186  set_value(utf8::erase(tmp, start, len));
187  set_cursor(start, false);
188 
189  update_layout();
190 }
191 
192 void multiline_text::handle_mouse_selection(point mouse, const bool start_selection)
193 {
194  mouse.x -= get_x();
195  mouse.y -= get_y();
196  // FIXME we don't test for overflow in width
197  if(mouse.x < static_cast<int>(text_x_offset_)
198  || mouse.y < static_cast<int>(text_y_offset_)
199  || mouse.y >= static_cast<int>(text_y_offset_ + get_lines_count() * font::get_line_spacing_factor() * text_height_)) {
200  return;
201  }
202 
203  point cursor_pos = get_column_line(point(mouse.x - text_x_offset_, mouse.y - text_y_offset_));
204  int offset = cursor_pos.x;
205  int line = cursor_pos.y;
206 
207  if(offset < 0) {
208  return;
209  }
210 
211  offset += get_line_start_offset(line);
212 
213  set_cursor(offset, !start_selection);
214 
215  update_canvas();
216  queue_redraw();
217  dragging_ |= start_selection;
218 }
219 
220 unsigned multiline_text::get_line_end_offset(unsigned line_no) {
221  const auto line = get_line(line_no);
222  return (line->start_index + line->length);
223 }
224 
225 unsigned multiline_text::get_line_start_offset(unsigned line_no) {
226  return get_line(line_no)->start_index;
227 }
228 
230 {
231  const auto conf = cast_config_to<multiline_text_definition>();
232  assert(conf);
233 
235 
236  wfl::map_formula_callable variables;
237  variables.add("height", wfl::variant(get_height()));
238  variables.add("width", wfl::variant(get_width()));
239  variables.add("text_font_height", wfl::variant(text_height_));
240 
241  text_x_offset_ = conf->text_x_offset(variables);
242  text_y_offset_ = conf->text_y_offset(variables);
243 
244  // Since this variable doesn't change set it here instead of in
245  // update_canvas().
246  for(auto & tmp : get_canvases())
247  {
248  tmp.set_variable("text_font_height", wfl::variant(text_height_));
249  }
250 
251  // Force an update of the canvas since now text_font_height is known.
252  update_canvas();
253 }
254 
256 {
257  if(!history_.get_enabled()) {
258  return false;
259  }
260 
261  const std::string str = history_.up(get_value());
262  if(!str.empty()) {
263  set_value(str);
264  }
265  return true;
266 }
267 
269 {
270  if(!history_.get_enabled()) {
271  return false;
272  }
273 
274  const std::string str = history_.down(get_value());
275  if(!str.empty()) {
276  set_value(str);
277  }
278  return true;
279 }
280 
281 void multiline_text::handle_key_tab(SDL_Keymod modifier, bool& handled)
282 {
283  if(!is_editable())
284  {
285  return;
286  }
287 
288  if(modifier & KMOD_CTRL) {
289  if(!(modifier & KMOD_SHIFT)) {
290  handled = history_up();
291  } else {
292  handled = history_down();
293  }
294  } else {
295  handled = true;
296  insert_char("\t");
297  }
298 }
299 
300 void multiline_text::handle_key_enter(SDL_Keymod modifier, bool& handled)
301 {
302  if (is_editable() && !(modifier & (KMOD_CTRL | KMOD_ALT | KMOD_GUI))) {
303  insert_char("\n");
304  handled = true;
305  }
306 }
307 
308 
309 void multiline_text::handle_key_clear_line(SDL_Keymod /*modifier*/, bool& handled)
310 {
311  handled = true;
312 
313  set_value("");
314 }
315 
316 void multiline_text::handle_key_down_arrow(SDL_Keymod modifier, bool& handled)
317 {
319 
320  handled = true;
321 
322  size_t offset = get_selection_start();
323  const unsigned line_num = get_line_number(offset);
324 
325  if (line_num == get_lines_count()) {
326  return;
327  }
328 
329  const unsigned line_start = get_line_start_offset(line_num);
330  const unsigned next_line_start = get_line_start_offset(line_num+1);
331  const unsigned next_line_end = get_line_end_offset(line_num+1);
332 
333  if (line_num < get_lines_count()-1) {
334  offset = offset - line_start + next_line_start;
335 
336  if (offset > next_line_end) {
337  offset = next_line_end;
338  }
339  }
340 
341  offset += get_selection_length();
342 
343  if (offset <= get_length()) {
344  set_cursor(offset, (modifier & KMOD_SHIFT) != 0);
345  }
346 
347  update_canvas();
348  queue_redraw();
349 }
350 
351 void multiline_text::handle_key_up_arrow(SDL_Keymod modifier, bool& handled)
352 {
354 
355  handled = true;
356 
357  size_t offset = get_selection_start();
358  const unsigned line_num = get_line_number(offset);
359 
360  if (line_num == 0) {
361  return;
362  }
363 
364  const unsigned line_start = get_line_start_offset(line_num);
365  const unsigned prev_line_start = get_line_start_offset(line_num-1);
366  const unsigned prev_line_end = get_line_end_offset(line_num-1);
367 
368  if (line_num > 0) {
369  offset = offset - line_start + prev_line_start;
370 
371  if (offset > prev_line_end) {
372  offset = prev_line_end;
373  }
374  }
375 
376  offset += get_selection_length();
377 
378  /* offset is unsigned int */
379  if (offset <= get_length()) {
380  set_cursor(offset, (modifier & KMOD_SHIFT) != 0);
381  }
382 
383  update_canvas();
384  queue_redraw();
385 }
386 
388  bool& handled,
389  const point& coordinate)
390 {
391  DBG_GUI_E << get_control_type() << "[" << id() << "]: " << event << ".";
392 
393  if(dragging_) {
395  }
396 
397  handled = true;
398 }
399 
401  bool& handled)
402 {
403  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
404 
405  get_window()->keyboard_capture(this);
407 
409 
410  handled = true;
411 }
412 
414  bool& handled)
415 {
416  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
417 
418  dragging_ = false;
419  handled = true;
420 }
421 
422 void
424  bool& handled)
425 {
426  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
427 
428  select_all();
429  handled = true;
430 }
431 
432 // }---------- DEFINITION ---------{
433 
436 {
437  DBG_GUI_P << "Parsing multiline_text " << id;
438 
439  load_resolutions<resolution>(cfg);
440 }
441 
443  : resolution_definition(cfg)
444  , text_x_offset(cfg["text_x_offset"])
445  , text_y_offset(cfg["text_y_offset"])
446 {
447  // Note the order should be the same as the enum state_t in multiline_text.hpp.
448  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_enabled", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_enabled")));
449  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_disabled", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_disabled")));
450  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_focused", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_focused")));
451  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_hovered", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_hovered")));
452 }
453 
454 // }---------- BUILDER -----------{
455 
456 namespace implementation
457 {
458 
459 builder_multiline_text::builder_multiline_text(const config& cfg)
460  : builder_styled_widget(cfg)
461  , history(cfg["history"])
462  , max_input_length(cfg["max_input_length"])
463  , hint_text(cfg["hint_text"].t_str())
464  , hint_image(cfg["hint_image"])
465  , editable(cfg["editable"].to_bool(true))
466  , wrap(cfg["wrap"].to_bool(true))
467 {
468 }
469 
470 std::unique_ptr<widget> builder_multiline_text::build() const
471 {
472  auto widget = std::make_unique<multiline_text>(*this);
473 
474  widget->set_editable(editable);
475  // A textbox doesn't have a label but a text
476  widget->set_value(label_string);
477 
478  if(!history.empty()) {
479  widget->set_history(history);
480  }
481 
482  widget->set_max_input_length(max_input_length);
483  widget->set_hint_data(hint_text, hint_image);
484 
485  DBG_GUI_G << "Window builder: placed text box '" << id
486  << "' with definition '" << definition << "'.";
487 
488  return widget;
489 }
490 
491 } // namespace implementation
492 
493 // }------------ END --------------
494 
495 } // namespace gui2
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:159
void handle_key_down_arrow(SDL_Keymod, bool &handled) override
Inherited from text_box_base.
bool dragging_
Is the mouse in dragging mode, this affects selection in mouse move.
unsigned text_y_offset_
The y offset in the widget where the text starts.
unsigned text_x_offset_
The x offset in the widget where the text starts.
virtual void place(const point &origin, const point &size) override
See widget::place.
bool history_up()
Goes one item up in the history.
void delete_selection() override
Inherited from text_box_base.
unsigned text_height_
The height of the text itself.
void signal_handler_mouse_motion(const event::ui_event event, bool &handled, const point &coordinate)
void update_offsets()
Updates text_x_offset_ and text_y_offset_.
void handle_key_enter(SDL_Keymod modifier, bool &handled) override
Inherited from text_box_base.
virtual const std::string & get_control_type() const override
Inherited from styled_widget, implemented by REGISTER_WIDGET.
bool history_down()
Goes one item down in the history.
unsigned get_line_end_offset(unsigned line_no)
Utility function to calculate the offset of the end of the line.
void handle_mouse_selection(point mouse, const bool start_selection)
std::string hint_text_
Helper text to display (such as "Search") if the text box is empty.
void signal_handler_left_button_down(const event::ui_event event, bool &handled)
void delete_char(const bool before_cursor) override
Inherited from text_box_base.
virtual void update_canvas() override
See styled_widget::update_canvas.
std::string hint_image_
Image (such as a magnifying glass) that accompanies the help text.
void handle_key_tab(SDL_Keymod modifier, bool &handled) override
Inherited from text_box_base.
unsigned get_line_start_offset(unsigned line_no)
Utility function to calculate the offset of the end of the line.
void handle_key_clear_line(SDL_Keymod modifier, bool &handled) override
Inherited from text_box_base.
std::size_t max_input_length_
The maximum length of the text input.
void signal_handler_left_button_double_click(const event::ui_event event, bool &handled)
void update_layout()
Update layout.
void set_cursor(const std::size_t offset, const bool select) override
Inherited from text_box_base.
void handle_key_up_arrow(SDL_Keymod, bool &handled) override
Inherited from text_box_base.
void insert_char(const std::string &unicode) override
Inherited from text_box_base.
text_history history_
The history text for this widget.
void signal_handler_left_button_up(const event::ui_event event, bool &handled)
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
point get_cursor_pos_from_index(const unsigned offset) const
Wrapper function, return the cursor position given the byte index.
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
unsigned get_lines_count() const
Wrapper function, return number of lines.
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
PangoLayoutLine * get_line(int index)
Wrapper function, returns the line corresponding to index.
void set_maximum_height(const int height, const bool multiline)
font::family_class get_font_family()
void set_maximum_width(const int width)
bool is_editable()
Check whether text can be edited or not.
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)
int get_line_number(const unsigned offset)
Wrapper function, return the line number given the byte index.
void select_all()
Selects all text.
std::string down(const std::string &text="")
One step down in the history.
Definition: text_box.cpp:78
std::string up(const std::string &text="")
One step up in the history.
Definition: text_box.cpp:60
bool get_enabled() const
Definition: text_box.hpp:100
Base class for all widgets.
Definition: widget.hpp:53
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:454
int get_x() const
Definition: widget.cpp:316
unsigned get_width() const
Definition: widget.cpp:326
int get_y() const
Definition: widget.cpp:321
unsigned get_height() const
Definition: widget.cpp:331
const std::string & id() const
Definition: widget.cpp:110
window * get_window()
Get the parent window.
Definition: widget.cpp:117
void keyboard_capture(widget *widget)
Definition: window.cpp:1221
void mouse_capture(const bool capture=true)
Definition: window.cpp:1215
map_formula_callable & add(const std::string &key, const variant &value)
Definition: callable.hpp:253
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...
#define LOG_HEADER
#define LOG_SCOPE_HEADER
void point(int x, int y)
Draw a single point.
Definition: draw.cpp:202
void line(int from_x, int from_y, int to_x, int to_y)
Draw a line.
Definition: draw.cpp:180
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:1124
constexpr float get_line_spacing_factor()
Definition: text.hpp:557
ui_event
The event sent to the dispatcher.
Definition: handler.hpp:115
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.
virtual std::unique_ptr< widget > build() const override
std::string definition
Parameters for the styled_widget.
multiline_text_definition(const config &cfg)
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)