The Battle for Wesnoth  1.19.5+dev
controller_base.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2024
3  by Joerg Hinrichs <joerg.hinrichs@alice-dsl.de>
4  Copyright (C) 2003 by David White <dave@whitevine.net>
5  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
6 
7  This program is free software; you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation; either version 2 of the License, or
10  (at your option) any later version.
11  This program is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY.
13 
14  See the COPYING file for more details.
15 */
16 
17 #include "controller_base.hpp"
18 
19 #include "display.hpp"
20 #include "events.hpp"
21 #include "game_config_manager.hpp"
23 #include "log.hpp"
24 #include "mouse_handler_base.hpp"
27 #include "gui/core/event/handler.hpp" // gui2::is_in_dialog
28 #include "soundsource.hpp"
29 #include "gui/core/timer.hpp"
30 #include "sdl/input.hpp" // get_mouse_state
31 #include "video.hpp"
32 
33 static lg::log_domain log_display("display");
34 #define ERR_DP LOG_STREAM(err, log_display)
35 
36 using namespace std::chrono_literals;
37 static constexpr auto long_touch_duration = 800ms;
38 
40  : game_config_(game_config_manager::get()->game_config())
41  , key_()
42  , scrolling_(false)
43  , scroll_up_(false)
44  , scroll_down_(false)
45  , scroll_left_(false)
46  , scroll_right_(false)
47  , last_scroll_tick_()
48  , scroll_carry_x_(0.0)
49  , scroll_carry_y_(0.0)
50  , key_release_listener_(*this)
51  , last_mouse_is_touch_(false)
52  , long_touch_timer_(0)
53 {
54 }
55 
57 {
58  if(long_touch_timer_ != 0) {
61  }
62 }
63 
65 {
66  if(long_touch_timer_ != 0 && !get_mouse_handler_base().dragging_started()) {
67  int x_now;
68  int y_now;
69  uint32_t mouse_state = sdl::get_mouse_state(&x_now, &y_now);
70 
71 #ifdef MOUSE_TOUCH_EMULATION
72  if(mouse_state & SDL_BUTTON(SDL_BUTTON_RIGHT)) {
73  // Monkey-patch touch controls again to make them look like left button.
74  mouse_state = SDL_BUTTON(SDL_BUTTON_LEFT);
75  }
76 #endif
77 
78  // Workaround for double-menu b/c of slow events processing, or I don't know.
79  int dx = x - x_now;
80  int dy = y - y_now;
81  int threshold = get_mouse_handler_base().drag_threshold();
82  bool yes_actually_dragging = dx * dx + dy * dy >= threshold * threshold;
83 
84  if(!yes_actually_dragging
85  && (mouse_state & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0
86  && get_display().map_area().contains(x_now, y_now))
87  {
89  if(m != nullptr) {
90  show_menu(get_display().get_theme().context_menu()->items(), x_now, y_now, true, get_display());
91  }
92  }
93  }
94 
96 }
97 
98 void controller_base::handle_event(const SDL_Event& event)
99 {
100  if(gui2::is_in_dialog()) {
101  return;
102  }
103 
105 
106  SDL_Event new_event = {};
107 
108  switch(event.type) {
109  case SDL_TEXTINPUT:
110  if(have_keyboard_focus()) {
112  }
113  break;
114 
115  case SDL_TEXTEDITING:
116  if(have_keyboard_focus()) {
117  SDL_Event evt = event;
118  evt.type = SDL_TEXTINPUT;
120  SDL_StopTextInput();
121  SDL_StartTextInput();
122  }
123  break;
124 
125  case SDL_KEYDOWN:
126  // Detect key press events, unless there something that has keyboard focus
127  // in which case the key press events should go only to it.
128  if(have_keyboard_focus()) {
129  if(event.key.keysym.sym == SDLK_ESCAPE) {
131  break;
132  }
133 
134  process_keydown_event(event);
136  process_keyup_event(event);
137  } else {
139  }
140  break;
141 
142  case SDL_KEYUP:
143  process_keyup_event(event);
145  break;
146 
147  case SDL_JOYBUTTONDOWN:
149  break;
150 
151  case SDL_JOYHATMOTION:
153  break;
154 
155  case SDL_MOUSEMOTION:
156  // Ignore old mouse motion events in the event queue
157  if(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_MOUSEMOTION, SDL_MOUSEMOTION) > 0) {
158  while(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_MOUSEMOTION, SDL_MOUSEMOTION) > 0) {
159  };
160  if(new_event.motion.which != SDL_TOUCH_MOUSEID) {
161  mh_base.mouse_motion_event(new_event.motion, is_browsing());
162  }
163  } else {
164  if(new_event.motion.which != SDL_TOUCH_MOUSEID) {
165  mh_base.mouse_motion_event(event.motion, is_browsing());
166  }
167  }
168  break;
169 
170  case SDL_FINGERMOTION:
171  if(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_FINGERMOTION, SDL_FINGERMOTION) > 0) {
172  while(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_FINGERMOTION, SDL_FINGERMOTION) > 0) {
173  };
174  mh_base.touch_motion_event(new_event.tfinger, is_browsing());
175  } else {
176  mh_base.touch_motion_event(event.tfinger, is_browsing());
177  }
178  break;
179 
180  case SDL_MOUSEBUTTONDOWN:
181  last_mouse_is_touch_ = event.button.which == SDL_TOUCH_MOUSEID;
182 
186  std::bind(&controller_base::long_touch_callback, this, event.button.x, event.button.y));
187  }
188 
189  mh_base.mouse_press(event.button, is_browsing());
191  break;
192 
193  case SDL_FINGERDOWN:
194  // handled by mouse case
195  break;
196 
197  case SDL_MOUSEBUTTONUP:
198  if(long_touch_timer_ != 0) {
200  long_touch_timer_ = 0;
201  }
202 
203  last_mouse_is_touch_ = event.button.which == SDL_TOUCH_MOUSEID;
204 
205  mh_base.mouse_press(event.button, is_browsing());
206  if(mh_base.get_show_menu()) {
207  show_menu(get_display().get_theme().context_menu()->items(), event.button.x, event.button.y, true,
208  get_display());
209  }
210  break;
211  case DOUBLE_CLICK_EVENT:
212  {
213  int x = static_cast<int>(reinterpret_cast<std::intptr_t>(event.user.data1));
214  int y = static_cast<int>(reinterpret_cast<std::intptr_t>(event.user.data2));
215  if(event.user.code == static_cast<int>(SDL_TOUCH_MOUSEID)
216  // TODO: Move to right_click_show_menu?
217  && get_display().map_area().contains(x, y)
218  // TODO: This chain repeats in several places, move to a method.
219  && get_display().get_theme().context_menu() != nullptr) {
220  show_menu(get_display().get_theme().context_menu()->items(),
221  x,
222  y,
223  true,
224  get_display());
225  }
226  }
227  break;
228 
229  case SDL_FINGERUP:
230  // handled by mouse case
231  break;
232 
233  case SDL_MOUSEWHEEL:
234  // Right and down are positive in Wesnoth's map.
235  // Right and up are positive in SDL_MouseWheelEvent on all platforms:
236  // https://wiki.libsdl.org/SDL2/SDL_MouseWheelEvent
237 #if defined(_WIN32) || defined(__APPLE__)
238  mh_base.mouse_wheel(event.wheel.x, -event.wheel.y, is_browsing());
239 #else
240  // Except right is wrongly negative on X11 in SDL < 2.0.18:
241  // https://github.com/libsdl-org/SDL/pull/4700
242  // https://github.com/libsdl-org/SDL/commit/515b7e9
243  // and on Wayland in SDL < 2.0.20:
244  // https://github.com/libsdl-org/SDL/commit/3e1b3bc
245  // Fixes issues #3362 and #7404, which are a regression caused by pull #2481 that fixed issue #2218.
246  {
247  static int xmul = 0;
248  if(xmul == 0) {
249  xmul = 1;
250  const char* video_driver = SDL_GetCurrentVideoDriver();
251  SDL_version ver;
252  SDL_GetVersion(&ver);
253  if(video_driver != nullptr && ver.major <= 2 && ver.minor <= 0) {
254  if(std::strcmp(video_driver, "x11") == 0 && ver.patch < 18) {
255  xmul = -1;
256  } else if(std::strcmp(video_driver, "wayland") == 0 && ver.patch < 20) {
257  xmul = -1;
258  }
259  }
260  }
261  mh_base.mouse_wheel(xmul * event.wheel.x, -event.wheel.y, is_browsing());
262  }
263 #endif
264  break;
265 
266  case TIMER_EVENT:
267  gui2::execute_timer(reinterpret_cast<size_t>(event.user.data1));
268  break;
269 
270  // TODO: Support finger specifically, like pan the map. For now, SDL's "shadow mouse" events will do.
271  case SDL_MULTIGESTURE:
272  default:
273  break;
274  }
275 }
276 
278 {
279  if(gui2::is_in_dialog()) {
280  return;
281  }
282 
284 }
285 
287 {
288  if(event.type == SDL_KEYUP) {
290  }
291 }
292 
294 {
295  return true;
296 }
297 
298 bool controller_base::handle_scroll(int mousex, int mousey, int mouse_flags)
299 {
300  const bool mouse_in_window =
303 
304  int scroll_speed = prefs::get().scroll_speed();
305  double dx = 0.0, dy = 0.0;
306 
307  int scroll_threshold = prefs::get().mouse_scrolling()
309  : 0;
310 
311  for(const theme::menu& m : get_display().get_theme().menus()) {
312  if(m.get_location().contains(mousex, mousey)) {
313  scroll_threshold = 0;
314  }
315  }
316 
317  // Scale scroll distance according to time passed
318  auto tick_now = std::chrono::steady_clock::now();
319 
320  // If we weren't previously scrolling, start small.
321  auto dt = 1ms;
322  if (scrolling_) {
323  dt = std::chrono::duration_cast<std::chrono::milliseconds>(tick_now - last_scroll_tick_);
324  }
325 
326  // scroll_speed is in percent. Ticks are in milliseconds.
327  // Let's assume the maximum speed (100) moves 50 hexes per second,
328  // i.e. 3600 pixels per 1000 ticks.
329  double scroll_amount = dt.count() * 0.036 * double(scroll_speed);
330  last_scroll_tick_ = tick_now;
331 
332  // Apply keyboard scrolling
333  dy -= scroll_up_ * scroll_amount;
334  dy += scroll_down_ * scroll_amount;
335  dx -= scroll_left_ * scroll_amount;
336  dx += scroll_right_ * scroll_amount;
337 
338  // Scroll if mouse is placed near the edge of the screen
339  if(mouse_in_window) {
340  if(mousey < scroll_threshold) {
341  dy -= scroll_amount;
342  }
343 
344  if(mousey > video::game_canvas_size().y - scroll_threshold) {
345  dy += scroll_amount;
346  }
347 
348  if(mousex < scroll_threshold) {
349  dx -= scroll_amount;
350  }
351 
352  if(mousex > video::game_canvas_size().x - scroll_threshold) {
353  dx += scroll_amount;
354  }
355  }
356 
358 
359  // Scroll with middle-mouse if enabled
360  if((mouse_flags & SDL_BUTTON_MMASK) != 0 && prefs::get().middle_click_scrolls()) {
361  const SDL_Point original_loc = mh_base.get_scroll_start();
362 
363  if(mh_base.scroll_started()) {
364  if(get_display().map_outside_area().contains(mousex, mousey)
365  && mh_base.scroll_started())
366  {
367  // Scroll speed is proportional from the distance from the first
368  // middle click and scrolling speed preference.
369  const double speed = 0.01 * scroll_amount;
370  const double snap_dist = 16; // Snap to horizontal/vertical scrolling
371  const double x_diff = (mousex - original_loc.x);
372  const double y_diff = (mousey - original_loc.y);
373 
374  if(std::fabs(x_diff) > snap_dist || std::fabs(y_diff) <= snap_dist) {
375  dx += speed * x_diff;
376  }
377 
378  if(std::fabs(y_diff) > snap_dist || std::fabs(x_diff) <= snap_dist) {
379  dy += speed * y_diff;
380  }
381  }
382  } else { // Event may fire mouse down out of order with respect to initial click
383  mh_base.set_scroll_start(mousex, mousey);
384  }
385  }
386 
387  // If nothing is scrolling, just return.
388  if (!dx && !dy) {
389  return false;
390  }
391 
392  // If we are continuing a scroll, carry over any subpixel movement.
393  if (scrolling_) {
394  dx += scroll_carry_x_;
395  dy += scroll_carry_y_;
396  }
397  point dist{int(dx), int(dy)};
398  scroll_carry_x_ = dx - double(dist.x);
399  scroll_carry_y_ = dy - double(dist.y);
400 
401  // Scroll the display
402  get_display().scroll(dist);
403 
404  // Even if the integer parts are both zero, we are still scrolling.
405  // The subpixel amounts will add up.
406  return true;
407 }
408 
410 {
411  CKey key;
412 
414  l->play_slice();
415  }
416 
417  events::pump();
419  events::draw();
420 
421  // Update sound sources before scrolling
423  l->update();
424  }
425 
426  const theme::menu* const m = get_display().menu_pressed();
427  if(m != nullptr) {
428  const rect& menu_loc = m->location(video::game_canvas());
429  show_menu(m->items(), menu_loc.x + 1, menu_loc.y + menu_loc.h + 1, false, get_display());
430 
431  return;
432  }
433 
434  const theme::action* const a = get_display().action_pressed();
435  if(a != nullptr) {
436  const rect& action_loc = a->location(video::game_canvas());
437  execute_action(a->items(), action_loc.x + 1, action_loc.y + action_loc.h + 1, false);
438 
439  return;
440  }
441 
442  auto str_vec = additional_actions_pressed();
443  if(!str_vec.empty()) {
444  execute_action(str_vec, 0, 0, false);
445  return;
446  }
447 
448  bool was_scrolling = scrolling_;
449 
450  int mousex, mousey;
451  uint8_t mouse_flags = sdl::get_mouse_state(&mousex, &mousey);
452 
453  scrolling_ = handle_scroll(mousex, mousey, mouse_flags);
454 
455  map_location highlighted_hex = get_display().mouseover_hex();
456 
457  // Scrolling ended, update the cursor and the brightened hex
458  if(!scrolling_ && was_scrolling) {
459  get_mouse_handler_base().mouse_update(is_browsing(), highlighted_hex);
460  }
461 }
462 
464  const std::vector<config>& items_arg, int xloc, int yloc, bool context_menu, display& disp)
465 {
467  if(!cmd_exec) {
468  return;
469  }
470 
471  std::vector<config> items;
472  for(const config& c : items_arg) {
473  const std::string& id = c["id"];
474  const hotkey::ui_command cmd = hotkey::ui_command(id);
475 
476  if(cmd_exec->can_execute_command(cmd) && (!context_menu || in_context_menu(cmd))) {
477  items.emplace_back(c);
478  }
479  }
480 
481  if(items.empty()) {
482  return;
483  }
484 
485  cmd_exec->show_menu(items, xloc, yloc, context_menu, disp);
486 }
487 
488 void controller_base::execute_action(const std::vector<std::string>& items_arg, int xloc, int yloc, bool context_menu)
489 {
491  if(!cmd_exec) {
492  return;
493  }
494 
495  std::vector<std::string> items;
496  for(const std::string& item : items_arg) {
498  if(cmd_exec->can_execute_command(cmd)) {
499  items.push_back(item);
500  }
501  }
502 
503  if(items.empty()) {
504  return;
505  }
506 
507  cmd_exec->execute_action(items, xloc, yloc, context_menu, get_display());
508 }
509 
511 {
512  return true;
513 }
Class that keeps track of all the keys on the keyboard.
Definition: key.hpp:29
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
void handle_event(const SDL_Event &event) override
bool handle_scroll(int mousex, int mousey, int mouse_flags)
Handle scrolling by keyboard, joystick and moving mouse near map edges.
virtual events::mouse_handler_base & get_mouse_handler_base()=0
Get a reference to a mouse handler member a derived class uses.
virtual ~controller_base()
virtual plugins_context * get_plugins_context()
Get (optionally) a plugins context a derived class uses.
size_t long_touch_timer_
Context menu timer.
virtual soundsource::manager * get_soundsource_man()
Get (optionally) a soundsources manager a derived class uses.
virtual bool in_context_menu(const hotkey::ui_command &cmd) const
void handle_event(const SDL_Event &event) override
Process mouse- and keypress-events from SDL.
virtual void process_keyup_event(const SDL_Event &)
Process keyup (always).
virtual void process() override
virtual void process_focus_keydown_event(const SDL_Event &)
Process keydown (only when the general map display does not have focus).
virtual bool have_keyboard_focus()
Derived classes should override this to return false when arrow keys should not scroll the map,...
virtual void process_keydown_event(const SDL_Event &)
Process keydown (always).
virtual display & get_display()=0
Get a reference to a display member a derived class uses.
void long_touch_callback(int x, int y)
virtual std::vector< std::string > additional_actions_pressed()
virtual hotkey::command_executor * get_hotkey_command_executor()
Optionally get a command executor to handle context menu events.
virtual void play_slice()
std::chrono::steady_clock::time_point last_scroll_tick_
virtual void show_menu(const std::vector< config > &items_arg, int xloc, int yloc, bool context_menu, display &disp)
virtual bool is_browsing() const
virtual void execute_action(const std::vector< std::string > &items_arg, int xloc, int yloc, bool context_menu)
Sort-of-Singleton that many classes, both GUI and non-GUI, use to access the game data.
Definition: display.hpp:97
const theme::action * action_pressed()
Definition: display.cpp:1528
theme & get_theme()
Definition: display.hpp:390
const theme::menu * menu_pressed()
Definition: display.cpp:1544
rect map_area() const
Returns the area used for the map.
Definition: display.cpp:495
const map_location & mouseover_hex() const
Definition: display.hpp:310
bool scroll(const point &amount, bool force=false)
Scrolls the display by amount pixels.
Definition: display.cpp:1700
virtual int drag_threshold() const
Minimum dragging distance to fire the drag&drop.
void touch_motion_event(const SDL_TouchFingerEvent &event, const bool browse)
virtual void mouse_press(const SDL_MouseButtonEvent &event, const bool browse)
void mouse_update(const bool browse, map_location loc)
Update the mouse with a fake mouse motion.
virtual display & gui()=0
Reference to the used display objects.
void mouse_motion_event(const SDL_MouseMotionEvent &event, const bool browse)
const SDL_Point get_scroll_start() const
virtual void mouse_wheel(int xscroll, int yscroll, bool browse)
Called when scrolling with the mouse wheel.
void set_scroll_start(int x, int y)
Called when the middle click scrolling.
void execute_action(const std::vector< std::string > &items_arg, int xloc, int yloc, bool context_menu, display &gui)
virtual void show_menu(const std::vector< config > &items_arg, int xloc, int yloc, bool context_menu, display &gui)
virtual bool can_execute_command(const hotkey::ui_command &command) const =0
static prefs & get()
int scroll_speed()
bool get_scroll_when_mouse_outside(bool def)
int mouse_scroll_threshold()
Gets the threshold for when to scroll.
virtual rect & location(const SDL_Rect &screen) const
Definition: theme.cpp:317
const std::vector< std::string > & items() const
Definition: theme.hpp:184
virtual rect & location(const SDL_Rect &screen) const
Definition: theme.cpp:317
const std::vector< config > & items() const
Definition: theme.hpp:237
const menu * context_menu() const
Definition: theme.hpp:261
static lg::log_domain log_display("display")
static constexpr auto long_touch_duration
controller_base framework: controller_base is roughly analogous to a "dialog" class in a GUI toolkit ...
map_display and display: classes which take care of displaying the map and game-data on the screen.
#define DOUBLE_CLICK_EVENT
Definition: events.hpp:24
#define TIMER_EVENT
Definition: events.hpp:25
Contains functions for cleanly handling SDL input.
Standard logging facilities (interface).
CURSOR_TYPE get()
Definition: cursor.cpp:216
void draw()
Trigger a draw cycle.
Definition: events.cpp:721
void raise_process_event()
Definition: events.cpp:726
void pump()
Process all events currently in the queue.
Definition: events.cpp:479
Game configuration data as global variables.
Definition: build_info.cpp:61
bool is_in_dialog()
Is a dialog open?
Definition: handler.cpp:1084
std::size_t add_timer(const std::chrono::milliseconds &interval, const std::function< void(std::size_t id)> &callback, const bool repeat)
Adds a new timer.
Definition: timer.cpp:123
bool remove_timer(const std::size_t id)
Removes a timer.
Definition: timer.cpp:164
bool execute_timer(const std::size_t id)
Executes a timer.
Definition: timer.cpp:197
void jhat_event(const SDL_Event &event, command_executor *executor)
void key_event(const SDL_Event &event, command_executor *executor)
void mbutton_event(const SDL_Event &event, command_executor *executor)
void run_events(command_executor *executor)
void jbutton_event(const SDL_Event &event, command_executor *executor)
void keyup_event(const SDL_Event &, command_executor *executor)
uint32_t get_mouse_state(int *x, int *y)
A wrapper for SDL_GetMouseState that gives coordinates in draw space.
Definition: input.cpp:27
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition: general.hpp:80
bool window_has_mouse_focus()
True iff the window has mouse focus.
Definition: video.cpp:701
point game_canvas_size()
The size of the game canvas, in drawing coordinates / game pixels.
Definition: video.cpp:432
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:427
Used as the main paramneter for can_execute_command/do_execute_command These functions are used to ex...
Encapsulates the map of the game.
Definition: location.hpp:45
Holds a 2D point.
Definition: point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:47
bool contains(int x, int y) const
Whether the given point lies within the rectangle.
Definition: rect.cpp:53
mock_char c
Contains the gui2 timer routines.