The Battle for Wesnoth  1.15.0+dev
text_box_base.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2018 by Mark de Wever <koraq@xs4all.nl>
3  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
15 #define GETTEXT_DOMAIN "wesnoth-lib"
16 
18 
19 #include "desktop/clipboard.hpp"
20 #include "gui/core/log.hpp"
21 #include "gui/core/timer.hpp"
23 
24 #include "utils/functional.hpp"
25 
26 #include <limits>
27 
28 #define LOG_SCOPE_HEADER get_control_type() + " [" + id() + "] " + __func__
29 #define LOG_HEADER LOG_SCOPE_HEADER + ':'
30 
31 namespace gui2
32 {
33 
34 text_box_base::text_box_base(const implementation::builder_styled_widget& builder, const std::string& control_type)
35  : styled_widget(builder, control_type)
36  , state_(ENABLED)
37  , text_()
38  , selection_start_(0)
39  , selection_length_(0)
40  , ime_composing_(false)
41  , ime_start_point_(0)
42  , cursor_timer_(0)
43  , cursor_alpha_(0)
44  , cursor_blink_rate_ms_(750)
45  , text_changed_callback_()
46 {
47 #ifdef __unix__
48  // pastes on UNIX systems.
49  connect_signal<event::MIDDLE_BUTTON_CLICK>(std::bind(
51 
52 #endif
53 
54  connect_signal<event::SDL_KEY_DOWN>(std::bind(
55  &text_box_base::signal_handler_sdl_key_down, this, _2, _3, _5, _6));
56  connect_signal<event::SDL_TEXT_INPUT>(std::bind(&text_box_base::handle_commit, this, _3, _5));
57  connect_signal<event::SDL_TEXT_EDITING>(std::bind(&text_box_base::handle_editing, this, _3, _5, _6, _7));
58 
59  connect_signal<event::RECEIVE_KEYBOARD_FOCUS>(std::bind(
61  connect_signal<event::LOSE_KEYBOARD_FOCUS>(
63 
64  toggle_cursor_timer(true);
65 }
66 
68 {
69  toggle_cursor_timer(false);
70 }
71 
72 void text_box_base::set_active(const bool active)
73 {
74  if(get_active() != active) {
75  set_state(active ? ENABLED : DISABLED);
76  }
77 }
78 
80 {
81  return state_ != DISABLED;
82 }
83 
84 unsigned text_box_base::get_state() const
85 {
86  return state_;
87 }
88 
89 void text_box_base::set_maximum_length(const std::size_t maximum_length)
90 {
91  if(maximum_length == 0) {
92  return;
93  }
94 
95  const bool need_update = text_.get_length() > maximum_length;
96 
97  text_.set_maximum_length(maximum_length);
98 
99  if(need_update) {
100  if(selection_start_ > maximum_length) {
101  selection_start_ = maximum_length;
102  selection_length_ = 0;
103  } else if(selection_start_ + selection_length_ > maximum_length) {
104  selection_length_ = maximum_length - selection_start_;
105  }
106  update_canvas();
107  set_is_dirty(true);
108  }
109 }
110 
111 void text_box_base::set_value(const std::string& text)
112 {
113  if(text != text_.text()) {
114  text_.set_text(text, false);
115 
116  // default to put the cursor at the end of the buffer.
118  selection_length_ = 0;
119  update_canvas();
120  set_is_dirty(true);
121  }
122 }
123 
124 void text_box_base::set_cursor(const std::size_t offset, const bool select)
125 {
127 
128  if(select) {
129 
130  if(selection_start_ == offset) {
131  selection_length_ = 0;
132  } else {
133  selection_length_ = -static_cast<int>(selection_start_ - offset);
134  }
135 
136 #ifdef __unix__
137  // selecting copies on UNIX systems.
138  copy_selection(true);
139 #endif
140  update_canvas();
141  set_is_dirty(true);
142 
143  } else {
144  assert(offset <= text_.get_length());
145  selection_start_ = offset;
146  selection_length_ = 0;
147 
148  update_canvas();
149  set_is_dirty(true);
150  }
151 }
152 
153 void text_box_base::insert_char(const std::string& unicode)
154 {
156 
157  if(text_.insert_text(selection_start_, unicode)) {
158 
159  // Update status
160  set_cursor(selection_start_ + utf8::size(unicode), false);
161  update_canvas();
162  set_is_dirty(true);
163  }
164 }
165 
167 {
168  if(!is_composing()) {
169  return 0;
170  }
171 
172  size_t text_length = utf8::size(text_.text());
173  size_t text_cached_length = utf8::size(text_cached_);
174  if(text_length < text_cached_length) {
175  return 0;
176  }
177 
179 }
180 
182 {
183  ime_composing_ = false;
184  // We need to inform the IME that text input is no longer in progress.
185  SDL_StopTextInput();
186  SDL_StartTextInput();
187 }
188 
189 void text_box_base::copy_selection(const bool mouse)
190 {
191  if(selection_length_ == 0) {
192  return;
193  }
194 
195  unsigned end, start = selection_start_;
196  const std::string txt = text_.text();
197 
198  if(selection_length_ > 0) {
199  end = utf8::index(txt, start + selection_length_);
200  start = utf8::index(txt, start);
201  } else {
202  // inverse selection: selection_start_ is in fact the end
203  end = utf8::index(txt, start);
204  start = utf8::index(txt, start + selection_length_);
205  }
206  desktop::clipboard::copy_to_clipboard(txt.substr(start, end - start), mouse);
207 }
208 
209 void text_box_base::paste_selection(const bool mouse)
210 {
211  const std::string& text = desktop::clipboard::copy_from_clipboard(mouse);
212  if(text.empty()) {
213  return;
214  }
215 
217 
219 
220  update_canvas();
221  set_is_dirty(true);
222  fire(event::NOTIFY_MODIFIED, *this, nullptr);
223 }
224 
225 void text_box_base::set_selection_start(const std::size_t selection_start)
226 {
227  if(selection_start != selection_start_) {
228  selection_start_ = selection_start;
229  set_is_dirty(true);
230  }
231 }
232 
233 void text_box_base::set_selection_length(const int selection_length)
234 {
235  if(selection_length != selection_length_) {
236  selection_length_ = selection_length;
237  set_is_dirty(true);
238  }
239 }
240 
241 void text_box_base::set_selection(std::size_t start, int length)
242 {
243  const std::size_t text_size = text_.get_length();
244 
245  if(start >= text_size) {
246  start = text_size;
247  }
248 
249  if(length == 0) {
250  set_cursor(start, false);
251  return;
252  }
253 
254  // The text pos/size type differs in both signedness and size with the
255  // selection length. Such is life.
256  const int sel_start = std::min<std::size_t>(start, std::numeric_limits<int>::max());
257  const int sel_max_length = std::min<std::size_t>(text_size - start, std::numeric_limits<int>::max());
258 
259  const bool backwards = length < 0;
260 
261  if(backwards && -length > sel_start) {
262  length = -sel_start;
263  } else if(!backwards && length > sel_max_length) {
264  length = sel_max_length;
265  }
266 
267  set_selection_start(start);
268  set_selection_length(length);
269 
270  update_canvas();
271 }
272 
274 {
275  if(state != state_) {
276  state_ = state;
277  set_is_dirty(true);
278  }
279 }
280 
282 {
283  if(!cursor_blink_rate_ms_) {
284  return;
285  }
286 
287  if(cursor_timer_) {
289  }
290 
291  cursor_timer_ = enable
293  : 0;
294 }
295 
297 {
298  unsigned was_alpha = cursor_alpha_;
299  switch(state_) {
300  case DISABLED:
301  cursor_alpha_ = 0;
302  return;
303  case ENABLED:
304  cursor_alpha_ = 255;
305  return;
306  default:
307  if(get_window() != open_window_stack.back()) {
308  cursor_alpha_ = 0;
309  } else {
310  cursor_alpha_ = (~cursor_alpha_) & 0xFF;
311  }
312  }
313 
314  if(was_alpha == cursor_alpha_) {
315  return;
316  }
317 
318  for(auto& tmp : get_canvases()) {
319  tmp.set_variable("cursor_alpha", wfl::variant(cursor_alpha_));
320  }
321 
322  set_is_dirty(true);
323 }
324 
326 {
327  if(!cursor_blink_rate_ms_) {
328  return;
329  }
330 
331  cursor_alpha_ = 255;
332 
333  for(auto& tmp : get_canvases()) {
334  tmp.set_variable("cursor_alpha", wfl::variant(cursor_alpha_));
335  }
336 
337  // Restart the blink timer.
338  toggle_cursor_timer(true);
339 }
340 
341 void text_box_base::handle_key_left_arrow(SDL_Keymod modifier, bool& handled)
342 {
343  /** @todo implement the ctrl key. */
344  DBG_GUI_E << LOG_SCOPE_HEADER << '\n';
345 
346  handled = true;
347  const int offset = selection_start_ - 1 + selection_length_;
348  if(offset >= 0) {
349  set_cursor(offset, (modifier & KMOD_SHIFT) != 0);
350  }
351 }
352 
353 void text_box_base::handle_key_right_arrow(SDL_Keymod modifier, bool& handled)
354 {
355  /** @todo implement the ctrl key. */
356  DBG_GUI_E << LOG_SCOPE_HEADER << '\n';
357 
358  handled = true;
359  const std::size_t offset = selection_start_ + 1 + selection_length_;
360  if(offset <= text_.get_length()) {
361  set_cursor(offset, (modifier & KMOD_SHIFT) != 0);
362  }
363 }
364 
365 void text_box_base::handle_key_home(SDL_Keymod modifier, bool& handled)
366 {
367  DBG_GUI_E << LOG_SCOPE_HEADER << '\n';
368 
369  handled = true;
370  if(modifier & KMOD_CTRL) {
371  goto_start_of_data((modifier & KMOD_SHIFT) != 0);
372  } else {
373  goto_start_of_line((modifier & KMOD_SHIFT) != 0);
374  }
375 }
376 
377 void text_box_base::handle_key_end(SDL_Keymod modifier, bool& handled)
378 {
379  DBG_GUI_E << LOG_SCOPE_HEADER << '\n';
380 
381  handled = true;
382  if(modifier & KMOD_CTRL) {
383  goto_end_of_data((modifier & KMOD_SHIFT) != 0);
384  } else {
385  goto_end_of_line((modifier & KMOD_SHIFT) != 0);
386  }
387 }
388 
389 void text_box_base::handle_key_backspace(SDL_Keymod /*modifier*/, bool& handled)
390 {
391  DBG_GUI_E << LOG_SCOPE_HEADER << '\n';
392 
393  handled = true;
394  if(selection_length_ != 0) {
396  } else if(selection_start_) {
397  delete_char(true);
398  if(is_composing()) {
399  if(get_composition_length() == 0) {
400  ime_composing_ = false;
401  }
402  }
403  }
404  fire(event::NOTIFY_MODIFIED, *this, nullptr);
405 }
406 
407 void text_box_base::handle_key_delete(SDL_Keymod /*modifier*/, bool& handled)
408 {
409  DBG_GUI_E << LOG_SCOPE_HEADER << '\n';
410 
411  handled = true;
412  if(selection_length_ != 0) {
414  } else if(selection_start_ < text_.get_length()) {
415  delete_char(false);
416  if(is_composing()) {
417  if(get_composition_length() == 0) {
418  ime_composing_ = false;
419  }
420  }
421  }
422  fire(event::NOTIFY_MODIFIED, *this, nullptr);
423 }
424 
425 void text_box_base::handle_commit(bool& handled, const std::string& unicode)
426 {
427  DBG_GUI_E << LOG_SCOPE_HEADER << '\n';
428 
429  if(unicode.size() > 1 || unicode[0] != 0) {
430  handled = true;
431  if(is_composing()) {
433  ime_composing_ = false;
434  }
435  insert_char(unicode);
436  fire(event::NOTIFY_MODIFIED, *this, nullptr);
437 
439  text_changed_callback_(this, this->text());
440  }
441  }
442 }
443 
444 /**
445  * SDL_TEXTEDITING handler. See example at https://wiki.libsdl.org/Tutorials/TextInput
446  */
447 void text_box_base::handle_editing(bool& handled, const std::string& unicode, int32_t start, int32_t len)
448 {
449  if(unicode.size() > 1 || unicode[0] != 0) {
450  handled = true;
451  std::size_t new_len = utf8::size(unicode);
452  if(!is_composing()) {
453  ime_composing_ = true;
456  text_cached_ = text_.text();
457  SDL_Rect rect = get_rectangle();
458  if(new_len > 0) {
460  rect.w = get_cursor_position(ime_start_point_ + new_len).x - rect.x;
461  } else {
462  rect.x += get_cursor_position(ime_start_point_ + new_len).x;
463  rect.w = get_cursor_position(ime_start_point_).x - rect.x;
464  }
465  SDL_SetTextInputRect(&rect);
466  }
467 
468 #ifdef __unix__
469  // In SDL_TextEditingEvent, size of editing_text is limited
470  // If length of composition text is more than the limit,
471  // Linux (ibus) implementation of SDL separates it into multiple
472  // SDL_TextEditingEvent.
473  // start is start position of the separated event in entire composition text
474  if(start == 0) {
475  text_.set_text(text_cached_, false);
476  }
477  text_.insert_text(ime_start_point_ + start, unicode);
478 #else
479  std::string new_text(text_cached_);
480  utf8::insert(new_text, ime_start_point_, unicode);
481  text_.set_text(new_text, false);
482 
483 #endif
484  int maximum_length = text_.get_length();
485 
486  // Update status
487  set_cursor(std::min(maximum_length, ime_start_point_ + start), false);
488  if(len > 0) {
489  set_cursor(std::min(maximum_length, ime_start_point_ + start + len), true);
490  }
491  update_canvas();
492  set_is_dirty(true);
493  }
494 }
495 
497  bool& handled)
498 {
499  DBG_GUI_E << LOG_HEADER << ' ' << event << ".\n";
500 
501  paste_selection(true);
502 
503  handled = true;
504 }
505 
507  bool& handled,
508  const SDL_Keycode key,
509  SDL_Keymod modifier)
510 {
511 
512  DBG_GUI_E << LOG_HEADER << ' ' << event << ".\n";
513 
514 /*
515  * For copy, cut and paste we use a different key on the MAC. Even for 'select
516  * all', contradicting the comment in widgets/textbox.cpp:495.
517  *
518  * The reason for that is, by coupling 'select all' to the behavior for copy,
519  * cut and paste, the text box behavior as a whole gets consistent with default
520  * macOS hotkey idioms.
521  */
522 #ifdef __APPLE__
523  // Idiomatic modifier key in macOS computers.
524  const SDL_Keycode modifier_key = KMOD_GUI;
525 #else
526  // Idiomatic modifier key in Microsoft desktop environments. Common in
527  // GNU/Linux as well, to some extent.
528  const SDL_Keycode modifier_key = KMOD_CTRL;
529 #endif
530 
531  switch(key) {
532 
533  case SDLK_LEFT:
534  handle_key_left_arrow(modifier, handled);
535  break;
536 
537  case SDLK_RIGHT:
538  handle_key_right_arrow(modifier, handled);
539  break;
540 
541  case SDLK_UP:
542  handle_key_up_arrow(modifier, handled);
543  break;
544 
545  case SDLK_DOWN:
546  handle_key_down_arrow(modifier, handled);
547  break;
548 
549  case SDLK_PAGEUP:
550  handle_key_page_up(modifier, handled);
551  break;
552 
553  case SDLK_PAGEDOWN:
554  handle_key_page_down(modifier, handled);
555  break;
556 
557  case SDLK_a:
558  if(!(modifier & modifier_key)) {
559  return;
560  }
561 
562  select_all();
563  break;
564 
565  case SDLK_HOME:
566  handle_key_home(modifier, handled);
567  break;
568 
569  case SDLK_END:
570  handle_key_end(modifier, handled);
571  break;
572 
573  case SDLK_BACKSPACE:
574  handle_key_backspace(modifier, handled);
575  break;
576 
577  case SDLK_u:
578  if(!(modifier & KMOD_CTRL)) {
579  return;
580  }
581  handle_key_clear_line(modifier, handled);
582  break;
583 
584  case SDLK_DELETE:
585  handle_key_delete(modifier, handled);
586  break;
587 
588  case SDLK_c:
589  if(!(modifier & modifier_key)) {
590  return;
591  }
592 
593  // atm we don't care whether there is something to copy or paste
594  // if nothing is there we still don't want to be chained.
595  copy_selection(false);
596  handled = true;
597  break;
598 
599  case SDLK_x:
600  if(!(modifier & modifier_key)) {
601  return;
602  }
603 
604  copy_selection(false);
606  handled = true;
607  break;
608 
609  case SDLK_v:
610  if(!(modifier & modifier_key)) {
611  return;
612  }
613 
614  paste_selection(false);
615  handled = true;
616  break;
617 
618  case SDLK_RETURN:
619  case SDLK_KP_ENTER:
620  if(!is_composing() || (modifier & (KMOD_CTRL | KMOD_ALT | KMOD_GUI | KMOD_SHIFT))) {
621  return;
622  }
623  // The IME will handle it, we just need to make sure nothing else handles it too.
624  handled = true;
625  break;
626 
627  case SDLK_ESCAPE:
628  if(!is_composing() || (modifier & (KMOD_CTRL | KMOD_ALT | KMOD_GUI | KMOD_SHIFT))) {
629  return;
630  }
632  handled = true;
633  break;
634 
635  default:
636  // Don't call the text changed callback if nothing happened.
637  return;
638  }
639 
641  text_changed_callback_(this, this->text());
642  }
643 }
644 
646 {
647  DBG_GUI_E << LOG_HEADER << ' ' << event << ".\n";
648 
650 }
651 
653 {
654  DBG_GUI_E << LOG_HEADER << ' ' << event << ".\n";
655 
657 }
658 
659 } // namespace gui2
Define the common log macros for the gui toolkit.
bool is_composing() const
virtual void handle_key_left_arrow(SDL_Keymod modifier, bool &handled)
Left arrow key pressed.
virtual unsigned get_state() const override
See styled_widget::get_state.
virtual void goto_start_of_line(const bool select=false)=0
Moves the cursor to the beginning of the line.
state_t
Note the order of the states must be the same as defined in settings.hpp.
text_box_base(const implementation::builder_styled_widget &builder, const std::string &control_type)
void select_all()
Selects all text.
virtual void handle_editing(bool &handled, const std::string &unicode, int32_t start, int32_t length)
SDL_TEXTEDITING handler.
virtual void goto_end_of_line(const bool select=false)=0
Moves the cursor to the end of the line.
virtual void paste_selection(const bool mouse)
Pastes the current selection.
void set_selection(std::size_t start, int length)
Sets or clears the text selection.
std::size_t cursor_timer_
std::string copy_from_clipboard(const bool)
Copies text from the clipboard.
Definition: clipboard.cpp:40
std::size_t get_length() const
Gets the length of the text in bytes.
Definition: text.hpp:205
virtual void handle_commit(bool &handled, const std::string &unicode)
bool set_text(const std::string &text, const bool markedup)
Sets the text to render.
Definition: text.cpp:283
virtual bool get_active() const override
See styled_widget::get_active.
virtual void handle_key_end(SDL_Keymod modifier, bool &handled)
End key pressed.
void set_maximum_length(const std::size_t maximum_length)
virtual void handle_key_backspace(SDL_Keymod modifier, bool &handled)
Backspace key pressed.
void set_selection_length(const int selection_length)
SDL_Rect get_rectangle() const
Gets the bounding rectangle of the widget on the screen.
Definition: widget.cpp:307
void goto_start_of_data(const bool select=false)
Moves the cursor to the beginning of the data.
virtual void copy_selection(const bool mouse)
Copies the current selection.
virtual void toggle_cursor_timer(bool enable)
void signal_handler_sdl_key_down(const event::ui_event event, bool &handled, const SDL_Keycode key, SDL_Keymod modifier)
pango_text & set_maximum_length(const std::size_t maximum_length)
Definition: text.cpp:435
void signal_handler_middle_button_click(const event::ui_event event, bool &handled)
virtual void handle_key_up_arrow(SDL_Keymod modifier, bool &handled)=0
Every key can have several behaviors.
size_t get_composition_length() const
Get length of composition text by IME.
unsigned short cursor_blink_rate_ms_
virtual void handle_key_right_arrow(SDL_Keymod modifier, bool &handled)
Right arrow key pressed.
virtual void handle_key_clear_line(SDL_Keymod modifier, bool &handled)=0
Clears the current line.
int x
x coordinate.
Definition: point.hpp:44
Generic file dialog.
Definition: field-fwd.hpp:22
Sent by a widget to notify others its contents or state are modified.
Definition: handler.hpp:96
void goto_end_of_data(const bool select=false)
Moves the cursor to the end of all text.
void set_selection_start(const std::size_t selection_start)
virtual void handle_key_down_arrow(SDL_Keymod modifier, bool &handled)=0
Down arrow key pressed.
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:86
std::size_t selection_start_
Start of the selected text.
virtual void handle_key_delete(SDL_Keymod modifier, bool &handled)
Delete key pressed.
unsigned short cursor_alpha_
std::string text_cached_
Cached version of the text without any pending IME modifications.
void set_is_dirty(const bool is_dirty)
Definition: widget.cpp:463
std::function< void(text_box_base *textbox, const std::string text)> text_changed_callback_
Text changed callback.
virtual void update_canvas()
Updates the canvas(ses).
std::vector< canvas > & get_canvases()
int selection_length_
Length of the selected text.
virtual void handle_key_page_down(SDL_Keymod, bool &)
Page down key.
point get_cursor_position(const unsigned column, const unsigned line=0) const
unsigned insert_text(const unsigned offset, const std::string &text)
Inserts UTF-8 text.
Definition: text.cpp:121
void signal_handler_receive_keyboard_focus(const event::ui_event event)
virtual void handle_key_page_up(SDL_Keymod, bool &)
Page up key.
void signal_handler_lose_keyboard_focus(const event::ui_event event)
virtual void insert_char(const std::string &unicode)
Inserts a character at the cursor.
#define DBG_GUI_E
Definition: log.hpp:34
virtual void delete_char(const bool before_cursor)=0
Deletes the character.
window * get_window()
Get the parent window.
Definition: widget.cpp:114
#define LOG_SCOPE_HEADER
Contains the gui2 timer routines.
std::size_t add_timer(const uint32_t interval, const std::function< void(std::size_t id)> &callback, const bool repeat)
Adds a new timer.
Definition: timer.cpp:126
std::string & insert(std::string &str, const std::size_t pos, const std::string &insert)
Insert a UTF-8 string at the specified position.
Definition: unicode.cpp:99
font::pango_text text_
The text entered in the widget.
std::size_t index(const std::string &str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:71
Base class for all visible items.
const std::string & text() const
Definition: text.hpp:223
virtual void set_value(const std::string &text)
The set_value is virtual for the password_box class.
void copy_to_clipboard(const std::string &text, const bool)
Copies text to the clipboard.
Definition: clipboard.cpp:35
state_t state_
Current state of the widget.
const std::string & text() const
virtual void cursor_timer_callback()
Implements blinking cursor functionality.
bool fire(const ui_event event, widget &target)
Fires an event which has no extra parameters.
Definition: dispatcher.cpp:130
EXIT_STATUS start(const std::string &filename, bool take_screenshot, const std::string &screenshot_filename)
Main interface for launching the editor from the title screen.
Definition: editor_main.cpp:28
void set_state(const state_t state)
void set_cursor(const std::size_t offset, const bool select)
Moves the cursor at the wanted position.
virtual void reset_cursor_state()
virtual void delete_selection()=0
Deletes the current selection.
#define LOG_HEADER
ui_event
The event send to the dispatcher.
Definition: handler.hpp:55
virtual void set_active(const bool active) override
See styled_widget::set_active.
virtual void handle_key_home(SDL_Keymod modifier, bool &handled)
Home key pressed.
std::vector< window * > open_window_stack
Keeps track of any open windows of any type (modal, non-modal, or tooltip) in the order in which they...
Definition: handler.cpp:1106
bool remove_timer(const std::size_t id)
Removes a timer.
Definition: timer.cpp:167