The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
multiline_text.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2023 - 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
19
20#include "cursor.hpp"
21#include "desktop/clipboard.hpp"
22#include "desktop/open.hpp"
23#include "gui/core/log.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
37namespace gui2
38{
39
40// ------------ WIDGET -----------{
41
42REGISTER_WIDGET(multiline_text)
43
44multiline_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
76void multiline_text::set_link_aware(bool link_aware)
77{
78 if(link_aware != link_aware_) {
79 link_aware_ = link_aware;
82 }
83}
84
85void 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
177void 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
212}
213
214void 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
234 queue_redraw();
235 dragging_ |= start_selection;
236}
237
238unsigned 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
243unsigned 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
252 text_height_ = font::get_max_height(get_text_font_size(), conf->text_font_family);
253
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.
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
299void multiline_text::handle_key_tab(SDL_Keymod modifier, bool& handled)
300{
301 if(!is_editable())
302 {
303 return;
304 }
305
306 if(modifier & SDL_KMOD_CTRL) {
307 if(!(modifier & SDL_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
318void multiline_text::handle_key_enter(SDL_Keymod modifier, bool& handled)
319{
320 if (is_editable() && !(modifier & (SDL_KMOD_CTRL | SDL_KMOD_ALT | SDL_KMOD_GUI))) {
321 insert_char("\n");
322 handled = true;
323 }
324}
325
326
327void multiline_text::handle_key_clear_line(SDL_Keymod /*modifier*/, bool& handled)
328{
329 handled = true;
330
331 set_value("");
332}
333
334void 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 & SDL_KMOD_SHIFT) != 0);
355 }
356
358 queue_redraw();
359}
360
361void 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 & SDL_KMOD_SHIFT) != 0);
383 }
384
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_) {
396 handle_mouse_selection(coordinate, false);
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
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()) {
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
455void
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
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
489namespace implementation
490{
491
492builder_multiline_text::builder_multiline_text(const config& 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
504std::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:157
@ 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 bool get_link_aware() const override
See styled_widget::get_link_aware.
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)
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.
std::vector< canvas > & get_canvases()
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_composition_length() const
Get length of composition text by IME.
std::size_t get_length() const
Wrapper function, see font::pango_text::get_length.
PangoLayoutLine * get_line(int index)
Wrapper function, see font::pango_text::get_line.
point get_column_line(const point &position) const
point get_cursor_pos_from_index(const unsigned offset) const
Wrapper function, see font::pango_text::get_cursor_pos_from_index.
std::string get_value() const
std::size_t get_composition_start() const
std::string plain_text()
point get_cursor_position(const unsigned column, const unsigned line=0) const
unsigned get_lines_count() const
Wrapper function, see font::pango_text::get_lines_count.
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_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, see font::pango_text::index_to_line_x.
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 capture_and_show_keyboard(widget *widget)
Definition window.cpp:1205
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:249
#define LOG_HEADER
const config * cfg
static std::string _(const char *str)
Definition gettext.hpp:100
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
#define LOG_SCOPE_HEADER
Definition button.cpp:34
This file contains the window object, this object is a top level container which has the event manage...
@ 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:161
void copy_to_clipboard(const std::string &text)
Copies text to the clipboard.
Definition clipboard.cpp:24
constexpr bool open_object_is_supported()
Returns whether open_object() is supported/implemented for the current platform.
Definition open.hpp:54
bool open_object(const std::string &path_or_url)
Opens the specified object with the default application configured for its type.
Definition open.cpp:50
constexpr float get_line_spacing_factor()
Definition text.hpp:568
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:958
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:167
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.
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
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)