The Battle for Wesnoth  1.19.7+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_(builder.max_input_length)
48  , text_x_offset_(0)
49  , text_y_offset_(0)
50  , text_height_(0)
51  , dragging_(false)
52  , link_aware_(builder.link_aware)
53  , hint_text_(builder.hint_text)
54  , hint_image_(builder.hint_image)
55 {
56  set_wants_mouse_left_double_click();
57 
58  connect_signal<event::MOUSE_MOTION>(std::bind(
59  &multiline_text::signal_handler_mouse_motion, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5));
60  connect_signal<event::LEFT_BUTTON_DOWN>(std::bind(
61  &multiline_text::signal_handler_left_button_down, this, std::placeholders::_2, std::placeholders::_3));
62  connect_signal<event::LEFT_BUTTON_UP>(std::bind(
63  &multiline_text::signal_handler_left_button_up, this, std::placeholders::_2, std::placeholders::_3));
64  connect_signal<event::LEFT_BUTTON_DOUBLE_CLICK>(std::bind(
65  &multiline_text::signal_handler_left_button_double_click, this, std::placeholders::_2, std::placeholders::_3));
66 
67  const auto conf = cast_config_to<multiline_text_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 multiline_text::set_link_aware(bool link_aware)
77 {
78  if(link_aware != link_aware_) {
79  link_aware_ = link_aware;
80  update_canvas();
81  queue_redraw();
82  }
83 }
84 
85 void multiline_text::place(const point& origin, const point& size)
86 {
87  // Inherited.
88  styled_widget::place(origin, size);
89 
92 
94 
96 }
97 
99 {
100  /***** Gather the info *****/
101 
102  // Set the cursor info.
103  const unsigned start = get_selection_start();
104  const int length = static_cast<int>(get_selection_length());
105 
106  // Set the composition info.
107  const unsigned edit_start = get_composition_start();
108  const int edit_length = get_composition_length();
109 
111 
112  unsigned comp_start_offset = 0;
113  unsigned comp_end_offset = 0;
114 
115  if(edit_length == 0) {
116  // Do nothing.
117  } else if(edit_length > 0) {
118  comp_start_offset = get_cursor_position(edit_start).x;
119  comp_end_offset = get_cursor_position(edit_start + edit_length).x;
120  } else {
121  comp_start_offset = get_cursor_position(edit_start + edit_length).x;
122  comp_end_offset = get_cursor_position(edit_start).x;
123  }
124 
125  // Set the selection info
126  unsigned start_offset = 0;
127  unsigned end_offset = 0;
128  if(length == 0) {
129  start_offset = start;
130  end_offset = start_offset;
131  } else if(length > 0) {
132  start_offset = start;
133  end_offset = start + length;
134  } else {
135  start_offset = start + length;
136  end_offset = start;
137  }
138 
139  /***** Set in all canvases *****/
140 
141  const int max_width = get_text_maximum_width();
142  const int max_height = get_text_maximum_height();
143  unsigned byte_pos = start + length;
144  if (get_use_markup() && (start + length > utf8::size(plain_text()) + 1)) {
145  byte_pos = utf8::size(plain_text());
146  }
147  const point cpos = get_cursor_pos_from_index(byte_pos);
148 
149  for(auto & tmp : get_canvases())
150  {
151 
152  tmp.set_variable("text", wfl::variant(get_value()));
153  tmp.set_variable("text_markup", wfl::variant(get_use_markup()));
154  tmp.set_variable("text_x_offset", wfl::variant(text_x_offset_));
155  tmp.set_variable("text_y_offset", wfl::variant(text_y_offset_));
156  tmp.set_variable("text_maximum_width", wfl::variant(max_width));
157  tmp.set_variable("text_maximum_height", wfl::variant(max_height));
158  tmp.set_variable("text_link_aware", wfl::variant(get_link_aware()));
159  tmp.set_variable("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
160 
161  tmp.set_variable("editable", wfl::variant(is_editable()));
162 
163  tmp.set_variable("highlight_start", wfl::variant(start_offset));
164  tmp.set_variable("highlight_end", wfl::variant(end_offset));
165 
166  tmp.set_variable("cursor_offset_x", wfl::variant(cpos.x));
167  tmp.set_variable("cursor_offset_y", wfl::variant(cpos.y));
168 
169  tmp.set_variable("composition_offset", wfl::variant(comp_start_offset));
170  tmp.set_variable("composition_width", wfl::variant(comp_end_offset - comp_start_offset));
171 
172  tmp.set_variable("hint_text", wfl::variant(hint_text_));
173  tmp.set_variable("hint_image", wfl::variant(hint_image_));
174  }
175 }
176 
177 void multiline_text::delete_char(const bool before_cursor)
178 {
179  if(!is_editable()) {
180  return;
181  }
182 
183  if(before_cursor) {
184  set_cursor(get_selection_start() - 1, false);
185  }
186 
188 
190 }
191 
193 {
194  if(get_selection_length() == 0 || (!is_editable()) ) {
195  return;
196  }
197 
198  // If we have a negative range change it to a positive range.
199  // This makes the rest of the algorithms easier.
200  int len = get_selection_length();
201  unsigned start = get_selection_start();
202  if(len < 0) {
203  len = -len;
204  start -= len;
205  }
206 
207  std::string tmp = get_value();
208  set_value(utf8::erase(tmp, start, len));
209  set_cursor(start, false);
210 
211  update_layout();
212 }
213 
214 void multiline_text::handle_mouse_selection(point mouse, const bool start_selection)
215 {
216  mouse -= get_origin();
217  point text_offset(text_x_offset_, text_y_offset_);
218  // FIXME we don't test for overflow in width
219  if(mouse < text_offset
220  || mouse.y >= static_cast<int>(text_y_offset_ + get_lines_count() * font::get_line_spacing_factor() * text_height_))
221  {
222  return;
223  }
224 
225  const auto& [offset, line] = get_column_line(mouse - text_offset);
226 
227  if(offset < 0) {
228  return;
229  }
230 
231  set_cursor(offset + get_line_start_offset(line), !start_selection);
232 
233  update_canvas();
234  queue_redraw();
235  dragging_ |= start_selection;
236 }
237 
238 unsigned multiline_text::get_line_end_offset(unsigned line_no) {
239  const auto line = get_line(line_no);
240  return (line->start_index + line->length);
241 }
242 
243 unsigned multiline_text::get_line_start_offset(unsigned line_no) {
244  return get_line(line_no)->start_index;
245 }
246 
248 {
249  const auto conf = cast_config_to<multiline_text_definition>();
250  assert(conf);
251 
253 
254  wfl::map_formula_callable variables;
255  variables.add("height", wfl::variant(get_height()));
256  variables.add("width", wfl::variant(get_width()));
257  variables.add("text_font_height", wfl::variant(text_height_));
258 
259  text_x_offset_ = conf->text_x_offset(variables);
260  text_y_offset_ = conf->text_y_offset(variables);
261 
262  // Since this variable doesn't change set it here instead of in
263  // update_canvas().
264  for(auto & tmp : get_canvases())
265  {
266  tmp.set_variable("text_font_height", wfl::variant(text_height_));
267  }
268 
269  // Force an update of the canvas since now text_font_height is known.
270  update_canvas();
271 }
272 
274 {
275  if(!history_.get_enabled()) {
276  return false;
277  }
278 
279  const std::string str = history_.up(get_value());
280  if(!str.empty()) {
281  set_value(str);
282  }
283  return true;
284 }
285 
287 {
288  if(!history_.get_enabled()) {
289  return false;
290  }
291 
292  const std::string str = history_.down(get_value());
293  if(!str.empty()) {
294  set_value(str);
295  }
296  return true;
297 }
298 
299 void multiline_text::handle_key_tab(SDL_Keymod modifier, bool& handled)
300 {
301  if(!is_editable())
302  {
303  return;
304  }
305 
306  if(modifier & KMOD_CTRL) {
307  if(!(modifier & KMOD_SHIFT)) {
308  handled = history_up();
309  } else {
310  handled = history_down();
311  }
312  } else {
313  handled = true;
314  insert_char("\t");
315  }
316 }
317 
318 void multiline_text::handle_key_enter(SDL_Keymod modifier, bool& handled)
319 {
320  if (is_editable() && !(modifier & (KMOD_CTRL | KMOD_ALT | KMOD_GUI))) {
321  insert_char("\n");
322  handled = true;
323  }
324 }
325 
326 
327 void multiline_text::handle_key_clear_line(SDL_Keymod /*modifier*/, bool& handled)
328 {
329  handled = true;
330 
331  set_value("");
332 }
333 
334 void multiline_text::handle_key_down_arrow(SDL_Keymod modifier, bool& handled)
335 {
337 
338  handled = true;
339 
340  unsigned offset = get_selection_start();
341  const unsigned line_num = get_line_number(offset);
342 
343  if (line_num == get_lines_count()-1) {
344  return;
345  }
346 
347  const unsigned line_start = get_line_start_offset(line_num);
348  const unsigned next_line_start = get_line_start_offset(line_num+1);
349  const unsigned next_line_end = get_line_end_offset(line_num+1);
350 
351  offset = std::min(offset - line_start + next_line_start, next_line_end) + get_selection_length();
352 
353  if (offset <= get_length()) {
354  set_cursor(offset, (modifier & KMOD_SHIFT) != 0);
355  }
356 
357  update_canvas();
358  queue_redraw();
359 }
360 
361 void multiline_text::handle_key_up_arrow(SDL_Keymod modifier, bool& handled)
362 {
364 
365  handled = true;
366 
367  unsigned offset = get_selection_start();
368  const unsigned line_num = get_line_number(offset);
369 
370  if (line_num == 0) {
371  return;
372  }
373 
374  const unsigned line_start = get_line_start_offset(line_num);
375  const unsigned prev_line_start = get_line_start_offset(line_num-1);
376  const unsigned prev_line_end = get_line_end_offset(line_num-1);
377 
378  offset = std::min(offset - line_start + prev_line_start, prev_line_end) + get_selection_length();
379 
380  /* offset is unsigned int */
381  if (offset <= get_length()) {
382  set_cursor(offset, (modifier & KMOD_SHIFT) != 0);
383  }
384 
385  update_canvas();
386  queue_redraw();
387 }
388 
390  bool& handled,
391  const point& coordinate)
392 {
393  DBG_GUI_E << get_control_type() << "[" << id() << "]: " << event << ".";
394 
395  if(dragging_) {
397  } else {
398  if(!get_link_aware()) {
399  return; // without marking event as "handled"
400  }
401 
402  point mouse = coordinate - get_origin();
403  if (!get_label_link(mouse).empty()) {
405  } else {
407  }
408  }
409 
410  handled = true;
411 }
412 
414  bool& handled)
415 {
416  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
417 
418  get_window()->keyboard_capture(this);
420 
421  point mouse_pos = get_mouse_position();
422 
423  if (get_link_aware()) {
424  std::string link = get_label_link(mouse_pos - get_origin());
425  DBG_GUI_E << "Clicked Link:\"" << link << "\"";
426 
427  if (!link.empty()) {
429  if(show_message(_("Open link?"), link, dialogs::message::yes_no_buttons) == gui2::retval::OK) {
430  desktop::open_object(link);
431  }
432  } else {
434  show_message("", _("Opening links is not supported, contact your packager. Link URL has been copied to the clipboard."), dialogs::message::auto_close);
435  }
436  } else {
437  handle_mouse_selection(mouse_pos, true);
438  }
439  } else {
440  handle_mouse_selection(mouse_pos, true);
441  }
442 
443  handled = true;
444 }
445 
447  bool& handled)
448 {
449  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
450 
451  dragging_ = false;
452  handled = true;
453 }
454 
455 void
457  bool& handled)
458 {
459  DBG_GUI_E << LOG_HEADER << ' ' << event << ".";
460 
461  select_all();
462  handled = true;
463 }
464 
465 // }---------- DEFINITION ---------{
466 
469 {
470  DBG_GUI_P << "Parsing multiline_text " << id;
471 
472  load_resolutions<resolution>(cfg);
473 }
474 
476  : resolution_definition(cfg)
477  , text_x_offset(cfg["text_x_offset"])
478  , text_y_offset(cfg["text_y_offset"])
479 {
480  // Note the order should be the same as the enum state_t in multiline_text.hpp.
481  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_enabled", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_enabled")));
482  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_disabled", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_disabled")));
483  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_focused", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_focused")));
484  state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_hovered", missing_mandatory_wml_tag("multiline_text_definition][resolution", "state_hovered")));
485 }
486 
487 // }---------- BUILDER -----------{
488 
489 namespace implementation
490 {
491 
492 builder_multiline_text::builder_multiline_text(const config& cfg)
493  : builder_styled_widget(cfg)
494  , history(cfg["history"])
495  , max_input_length(cfg["max_input_length"].to_size_t())
496  , hint_text(cfg["hint_text"].t_str())
497  , hint_image(cfg["hint_image"])
498  , editable(cfg["editable"].to_bool(true))
499  , wrap(cfg["wrap"].to_bool(true))
500  , link_aware(cfg["link_aware"].to_bool(false))
501 {
502 }
503 
504 std::unique_ptr<widget> builder_multiline_text::build() const
505 {
506  auto widget = std::make_unique<multiline_text>(*this);
507 
508  widget->set_editable(editable);
509  // A textbox doesn't have a label but a text
510  widget->set_value(label_string);
511 
512  if(!history.empty()) {
513  widget->set_history(history);
514  }
515 
516  DBG_GUI_G << "Window builder: placed text box '" << id
517  << "' with definition '" << definition << "'.";
518 
519  return widget;
520 }
521 
522 } // namespace implementation
523 
524 // }------------ END --------------
525 
526 } // 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:173
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(std::string_view 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)