The Battle for Wesnoth  1.19.5+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 "cursor.hpp"
21 #include "desktop/clipboard.hpp"
22 #include "desktop/open.hpp"
23 #include "gui/core/log.hpp"
25 #include "gui/dialogs/message.hpp"
26 #include "gui/widgets/window.hpp"
28 #include "font/text.hpp"
29 #include "wml_exception.hpp"
30 #include "gettext.hpp"
31 
32 #include <functional>
33 
34 #define LOG_SCOPE_HEADER get_control_type() + " [" + id() + "] " + __func__
35 #define LOG_HEADER LOG_SCOPE_HEADER + ':'
36 
37 namespace gui2
38 {
39 
40 // ------------ WIDGET -----------{
41 
42 REGISTER_WIDGET(multiline_text)
43 
44 multiline_text::multiline_text(const implementation::builder_multiline_text& builder)
45  : text_box_base(builder, type())
46  , history_()
47  , max_input_length_(0)
48  , text_x_offset_(0)
49  , text_y_offset_(0)
50  , text_height_(0)
51  , dragging_(false)
52  , link_aware_(builder.link_aware)
53 {
54  set_wants_mouse_left_double_click();
55 
56  connect_signal<event::MOUSE_MOTION>(std::bind(
57  &multiline_text::signal_handler_mouse_motion, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5));
58  connect_signal<event::LEFT_BUTTON_DOWN>(std::bind(
59  &multiline_text::signal_handler_left_button_down, this, std::placeholders::_2, std::placeholders::_3));
60  connect_signal<event::LEFT_BUTTON_UP>(std::bind(
61  &multiline_text::signal_handler_left_button_up, this, std::placeholders::_2, std::placeholders::_3));
62  connect_signal<event::LEFT_BUTTON_DOUBLE_CLICK>(std::bind(
63  &multiline_text::signal_handler_left_button_double_click, this, std::placeholders::_2, std::placeholders::_3));
64 
65  const auto conf = cast_config_to<multiline_text_definition>();
66  assert(conf);
67 
68  set_font_size(get_text_font_size());
69  set_font_style(conf->text_font_style);
70 
71  update_offsets();
72 }
73 
74 void multiline_text::set_link_aware(bool link_aware)
75 {
76  if(link_aware != link_aware_) {
77  link_aware_ = link_aware;
78  update_canvas();
79  queue_redraw();
80  }
81 }
82 
83 void multiline_text::place(const point& origin, const point& size)
84 {
85  // Inherited.
86  styled_widget::place(origin, size);
87 
90 
92 
94 }
95 
97 {
98  /***** Gather the info *****/
99 
100  // Set the cursor info.
101  const unsigned start = get_selection_start();
102  const int length = static_cast<int>(get_selection_length());
103 
104  // Set the composition info.
105  const unsigned edit_start = get_composition_start();
106  const int edit_length = get_composition_length();
107 
109 
110  unsigned comp_start_offset = 0;
111  unsigned comp_end_offset = 0;
112 
113  if(edit_length == 0) {
114  // Do nothing.
115  } else if(edit_length > 0) {
116  comp_start_offset = get_cursor_position(edit_start).x;
117  comp_end_offset = get_cursor_position(edit_start + edit_length).x;
118  } else {
119  comp_start_offset = get_cursor_position(edit_start + edit_length).x;
120  comp_end_offset = get_cursor_position(edit_start).x;
121  }
122 
123  // Set the selection info
124  unsigned start_offset = 0;
125  unsigned end_offset = 0;
126  if(length == 0) {
127  start_offset = start;
128  end_offset = start_offset;
129  } else if(length > 0) {
130  start_offset = start;
131  end_offset = start + length;
132  } else {
133  start_offset = start + length;
134  end_offset = start;
135  }
136 
137  /***** Set in all canvases *****/
138 
139  const int max_width = get_text_maximum_width();
140  const int max_height = get_text_maximum_height();
141  unsigned byte_pos = start + length;
142  if (get_use_markup() && (start + length > utf8::size(plain_text()) + 1)) {
143  byte_pos = utf8::size(plain_text());
144  }
145  const point cpos = get_cursor_pos_from_index(byte_pos);
146 
147  for(auto & tmp : get_canvases())
148  {
149 
150  tmp.set_variable("text", wfl::variant(get_value()));
151  tmp.set_variable("text_markup", wfl::variant(get_use_markup()));
152  tmp.set_variable("text_x_offset", wfl::variant(text_x_offset_));
153  tmp.set_variable("text_y_offset", wfl::variant(text_y_offset_));
154  tmp.set_variable("text_maximum_width", wfl::variant(max_width));
155  tmp.set_variable("text_maximum_height", wfl::variant(max_height));
156  tmp.set_variable("text_link_aware", wfl::variant(get_link_aware()));
157  tmp.set_variable("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
158 
159  tmp.set_variable("editable", wfl::variant(is_editable()));
160 
161  tmp.set_variable("highlight_start", wfl::variant(start_offset));
162  tmp.set_variable("highlight_end", wfl::variant(end_offset));
163 
164  tmp.set_variable("cursor_offset_x", wfl::variant(cpos.x));
165  tmp.set_variable("cursor_offset_y", wfl::variant(cpos.y));
166 
167  tmp.set_variable("composition_offset", wfl::variant(comp_start_offset));
168  tmp.set_variable("composition_width", wfl::variant(comp_end_offset - comp_start_offset));
169 
170  tmp.set_variable("hint_text", wfl::variant(hint_text_));
171  tmp.set_variable("hint_image", wfl::variant(hint_image_));
172  }
173 }
174 
175 void multiline_text::delete_char(const bool before_cursor)
176 {
177  if(!is_editable()) {
178  return;
179  }
180 
181  if(before_cursor) {
182  set_cursor(get_selection_start() - 1, false);
183  }
184 
186 
188 }
189 
191 {
192  if(get_selection_length() == 0 || (!is_editable()) ) {
193  return;
194  }
195 
196  // If we have a negative range change it to a positive range.
197  // This makes the rest of the algorithms easier.
198  int len = get_selection_length();
199  unsigned start = get_selection_start();
200  if(len < 0) {
201  len = -len;
202  start -= len;
203  }
204 
205  std::string tmp = get_value();
206  set_value(utf8::erase(tmp, start, len));
207  set_cursor(start, false);
208 
209  update_layout();
210 }
211 
212 void multiline_text::handle_mouse_selection(point mouse, const bool start_selection)
213 {
214  mouse -= get_origin();
215  point text_offset(text_x_offset_, text_y_offset_);
216  // FIXME we don't test for overflow in width
217  if(mouse < text_offset
218  || mouse.y >= static_cast<int>(text_y_offset_ + get_lines_count() * font::get_line_spacing_factor() * text_height_))
219  {
220  return;
221  }
222 
223  const auto& [offset, line] = get_column_line(mouse - text_offset);
224 
225  if(offset < 0) {
226  return;
227  }
228 
229  set_cursor(offset + get_line_start_offset(line), !start_selection);
230 
231  update_canvas();
232  queue_redraw();
233  dragging_ |= start_selection;
234 }
235 
236 unsigned multiline_text::get_line_end_offset(unsigned line_no) {
237  const auto line = get_line(line_no);
238  return (line->start_index + line->length);
239 }
240 
241 unsigned multiline_text::get_line_start_offset(unsigned line_no) {
242  return get_line(line_no)->start_index;
243 }
244 
246 {
247  const auto conf = cast_config_to<multiline_text_definition>();
248  assert(conf);
249 
251 
252  wfl::map_formula_callable variables;
253  variables.add("height", wfl::variant(get_height()));
254  variables.add("width", wfl::variant(get_width()));
255  variables.add("text_font_height", wfl::variant(text_height_));
256 
257  text_x_offset_ = conf->text_x_offset(variables);
258  text_y_offset_ = conf->text_y_offset(variables);
259 
260  // Since this variable doesn't change set it here instead of in
261  // update_canvas().
262  for(auto & tmp : get_canvases())
263  {
264  tmp.set_variable("text_font_height", wfl::variant(text_height_));
265  }
266 
267  // Force an update of the canvas since now text_font_height is known.
268  update_canvas();
269 }
270 
272 {
273  if(!history_.get_enabled()) {
274  return false;
275  }
276 
277  const std::string str = history_.up(get_value());
278  if(!str.empty()) {
279  set_value(str);
280  }
281  return true;
282 }
283 
285 {
286  if(!history_.get_enabled()) {
287  return false;
288  }
289 
290  const std::string str = history_.down(get_value());
291  if(!str.empty()) {
292  set_value(str);
293  }
294  return true;
295 }
296 
297 void multiline_text::handle_key_tab(SDL_Keymod modifier, bool& handled)
298 {
299  if(!is_editable())
300  {
301  return;
302  }
303 
304  if(modifier & KMOD_CTRL) {
305  if(!(modifier & KMOD_SHIFT)) {
306  handled = history_up();
307  } else {
308  handled = history_down();
309  }
310  } else {
311  handled = true;
312  insert_char("\t");
313  }
314 }
315 
316 void multiline_text::handle_key_enter(SDL_Keymod modifier, bool& handled)
317 {
318  if (is_editable() && !(modifier & (KMOD_CTRL | KMOD_ALT | KMOD_GUI))) {
319  insert_char("\n");
320  handled = true;
321  }
322 }
323 
324 
325 void multiline_text::handle_key_clear_line(SDL_Keymod /*modifier*/, bool& handled)
326 {
327  handled = true;
328 
329  set_value("");
330 }
331 
332 void multiline_text::handle_key_down_arrow(SDL_Keymod modifier, bool& handled)
333 {
335 
336  handled = true;
337 
338  unsigned offset = get_selection_start();
339  const unsigned line_num = get_line_number(offset);
340 
341  if (line_num == get_lines_count()-1) {
342  return;
343  }
344 
345  const unsigned line_start = get_line_start_offset(line_num);
346  const unsigned next_line_start = get_line_start_offset(line_num+1);
347  const unsigned next_line_end = get_line_end_offset(line_num+1);
348 
349  offset = std::min(offset - line_start + next_line_start, next_line_end) + get_selection_length();
350 
351  if (offset <= get_length()) {
352  set_cursor(offset, (modifier & KMOD_SHIFT) != 0);
353  }
354 
355  update_canvas();
356  queue_redraw();
357 }
358 
359 void multiline_text::handle_key_up_arrow(SDL_Keymod modifier, bool& handled)
360 {
362 
363  handled = true;
364 
365  unsigned offset = get_selection_start();
366  const unsigned line_num = get_line_number(offset);
367 
368  if (line_num == 0) {
369  return;
370  }
371 
372  const unsigned line_start = get_line_start_offset(line_num);
373  const unsigned prev_line_start = get_line_start_offset(line_num-1);
374  const unsigned prev_line_end = get_line_end_offset(line_num-1);
375 
376  offset = std::min(offset - line_start + prev_line_start, prev_line_end) + 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  } else {
396  if(!get_link_aware()) {
397  return; // without marking event as "handled"
398  }
399 
400  point mouse = coordinate - get_origin();
401  if (!get_label_link(mouse).empty()) {
403  } else {
405  }
406  }
407 
408  handled = true;
409 }
410 
412  bool& handled)
413 {
414  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
415 
416  get_window()->keyboard_capture(this);
418 
419  point mouse_pos = get_mouse_position();
420 
421  if (get_link_aware()) {
422  std::string link = get_label_link(mouse_pos - get_origin());
423  DBG_GUI_E << "Clicked Link:\"" << link << "\"";
424 
425  if (!link.empty()) {
427  if(show_message(_("Open link?"), link, dialogs::message::yes_no_buttons) == gui2::retval::OK) {
428  desktop::open_object(link);
429  }
430  } else {
432  show_message("", _("Opening links is not supported, contact your packager. Link URL has been copied to the clipboard."), dialogs::message::auto_close);
433  }
434  } else {
435  handle_mouse_selection(mouse_pos, true);
436  }
437  } else {
438  handle_mouse_selection(mouse_pos, true);
439  }
440 
441  handled = true;
442 }
443 
445  bool& handled)
446 {
447  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
448 
449  dragging_ = false;
450  handled = true;
451 }
452 
453 void
455  bool& handled)
456 {
457  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
458 
459  select_all();
460  handled = true;
461 }
462 
463 // }---------- DEFINITION ---------{
464 
467 {
468  DBG_GUI_P << "Parsing multiline_text " << id;
469 
470  load_resolutions<resolution>(cfg);
471 }
472 
474  : resolution_definition(cfg)
475  , text_x_offset(cfg["text_x_offset"])
476  , text_y_offset(cfg["text_y_offset"])
477 {
478  // Note the order should be the same as the enum state_t in multiline_text.hpp.
479  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_enabled", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_enabled")));
480  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_disabled", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_disabled")));
481  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_focused", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_focused")));
482  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_hovered", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_hovered")));
483 }
484 
485 // }---------- BUILDER -----------{
486 
487 namespace implementation
488 {
489 
490 builder_multiline_text::builder_multiline_text(const config& cfg)
491  : builder_styled_widget(cfg)
492  , history(cfg["history"])
493  , max_input_length(cfg["max_input_length"].to_size_t())
494  , hint_text(cfg["hint_text"].t_str())
495  , hint_image(cfg["hint_image"])
496  , editable(cfg["editable"].to_bool(true))
497  , wrap(cfg["wrap"].to_bool(true))
498  , link_aware(cfg["link_aware"].to_bool(false))
499 {
500 }
501 
502 std::unique_ptr<widget> builder_multiline_text::build() const
503 {
504  auto widget = std::make_unique<multiline_text>(*this);
505 
506  widget->set_editable(editable);
507  // A textbox doesn't have a label but a text
508  widget->set_value(label_string);
509 
510  if(!history.empty()) {
511  widget->set_history(history);
512  }
513 
514  widget->set_max_input_length(max_input_length);
515  widget->set_hint_data(hint_text, hint_image);
516 
517  DBG_GUI_G << "Window builder: placed text box '" << id
518  << "' with definition '" << definition << "'.";
519 
520  return widget;
521 }
522 
523 } // namespace implementation
524 
525 // }------------ END --------------
526 
527 } // namespace gui2
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
@ yes_no_buttons
Shows a yes and no button.
Definition: message.hpp:81
@ auto_close
Enables auto close.
Definition: message.hpp:71
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 link_aware_
Whether the text area is link aware, rendering links with special formatting and handling click event...
void set_link_aware(bool l)
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.
virtual bool get_link_aware() const override
See styled_widget::get_link_aware.
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.
std::string get_label_link(const point &position) const
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.
bool get_use_markup() const
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.
std::string plain_text()
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() const
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:55
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:464
point get_origin() const
Returns the screen origin of the widget.
Definition: widget.cpp:311
unsigned get_width() const
Definition: widget.cpp:336
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
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
static std::string _(const char *str)
Definition: gettext.hpp:93
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
@ IBEAM
Definition: cursor.hpp:28
@ HYPERLINK
Definition: cursor.hpp:28
void set(CURSOR_TYPE type)
Use the default parameter to reset cursors.
Definition: cursor.cpp:176
void copy_to_clipboard(const std::string &text)
Copies text to the clipboard.
Definition: clipboard.cpp:27
bool open_object([[maybe_unused]] const std::string &path_or_url)
Definition: open.cpp:46
constexpr bool open_object_is_supported()
Returns whether open_object() is supported/implemented for the current platform.
Definition: open.hpp:54
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:1142
constexpr float get_line_spacing_factor()
Definition: text.hpp:609
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
void show_message(const std::string &title, const std::string &msg, const std::string &button_caption, const bool auto_close, const bool message_use_markup, const bool title_use_markup)
Shows a message to the user.
Definition: message.cpp:148
@ OK
Dialog was closed with the OK button.
Definition: retval.hpp:35
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
Desktop environment interaction functions.
#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)