The Battle for Wesnoth  1.19.0+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 "show_dialog.hpp" //gui::in_dialog
28 #include "gui/core/event/handler.hpp" // gui2::is_in_dialog
29 #include "soundsource.hpp"
30 #include "gui/core/timer.hpp"
31 #include "sdl/input.hpp" // get_mouse_state
32 #include "video.hpp"
33 
34 static lg::log_domain log_display("display");
35 #define ERR_DP LOG_STREAM(err, log_display)
36 
37 static const int long_touch_duration_ms = 800;
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_(0)
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(gui::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_scroll_enabled()
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  uint32_t tick_now = SDL_GetTicks();
319  // If we weren't previously scrolling, start small.
320  int dt = 1;
321  if (scrolling_) {
322  dt = tick_now - last_scroll_tick_;
323  }
324  // scroll_speed is in percent. Ticks are in milliseconds.
325  // Let's assume the maximum speed (100) moves 50 hexes per second,
326  // i.e. 3600 pixels per 1000 ticks.
327  double scroll_amount = double(dt) * 0.036 * double(scroll_speed);
328  last_scroll_tick_ = tick_now;
329 
330  // Apply keyboard scrolling
331  dy -= scroll_up_ * scroll_amount;
332  dy += scroll_down_ * scroll_amount;
333  dx -= scroll_left_ * scroll_amount;
334  dx += scroll_right_ * scroll_amount;
335 
336  // Scroll if mouse is placed near the edge of the screen
337  if(mouse_in_window) {
338  if(mousey < scroll_threshold) {
339  dy -= scroll_amount;
340  }
341 
342  if(mousey > video::game_canvas_size().y - scroll_threshold) {
343  dy += scroll_amount;
344  }
345 
346  if(mousex < scroll_threshold) {
347  dx -= scroll_amount;
348  }
349 
350  if(mousex > video::game_canvas_size().x - scroll_threshold) {
351  dx += scroll_amount;
352  }
353  }
354 
356 
357  // Scroll with middle-mouse if enabled
358  if((mouse_flags & SDL_BUTTON_MMASK) != 0 && prefs::get().middle_click_scrolls()) {
359  const SDL_Point original_loc = mh_base.get_scroll_start();
360 
361  if(mh_base.scroll_started()) {
362  if(get_display().map_outside_area().contains(mousex, mousey)
363  && mh_base.scroll_started())
364  {
365  // Scroll speed is proportional from the distance from the first
366  // middle click and scrolling speed preference.
367  const double speed = 0.01 * scroll_amount;
368  const double snap_dist = 16; // Snap to horizontal/vertical scrolling
369  const double x_diff = (mousex - original_loc.x);
370  const double y_diff = (mousey - original_loc.y);
371 
372  if(std::fabs(x_diff) > snap_dist || std::fabs(y_diff) <= snap_dist) {
373  dx += speed * x_diff;
374  }
375 
376  if(std::fabs(y_diff) > snap_dist || std::fabs(x_diff) <= snap_dist) {
377  dy += speed * y_diff;
378  }
379  }
380  } else { // Event may fire mouse down out of order with respect to initial click
381  mh_base.set_scroll_start(mousex, mousey);
382  }
383  }
384 
385  // If nothing is scrolling, just return.
386  if (!dx && !dy) {
387  return false;
388  }
389 
390  // If we are continuing a scroll, carry over any subpixel movement.
391  if (scrolling_) {
392  dx += scroll_carry_x_;
393  dy += scroll_carry_y_;
394  }
395  int dx_int = int(dx);
396  int dy_int = int(dy);
397  scroll_carry_x_ = dx - double(dx_int);
398  scroll_carry_y_ = dy - double(dy_int);
399 
400  // Scroll the display
401  get_display().scroll(dx_int, dy_int);
402 
403  // Even if the integer parts are both zero, we are still scrolling.
404  // The subpixel amounts will add up.
405  return true;
406 }
407 
408 void controller_base::play_slice(bool is_delay_enabled)
409 {
410  CKey key;
411 
413  l->play_slice();
414  }
415 
416  events::pump();
418  events::draw();
419 
420  // Update sound sources before scrolling
422  l->update();
423  }
424 
425  const theme::menu* const m = get_display().menu_pressed();
426  if(m != nullptr) {
427  const rect& menu_loc = m->location(video::game_canvas());
428  show_menu(m->items(), menu_loc.x + 1, menu_loc.y + menu_loc.h + 1, false, get_display());
429 
430  return;
431  }
432 
433  const theme::action* const a = get_display().action_pressed();
434  if(a != nullptr) {
435  const rect& action_loc = a->location(video::game_canvas());
436  execute_action(a->items(), action_loc.x + 1, action_loc.y + action_loc.h + 1, false);
437 
438  return;
439  }
440 
441  auto str_vec = additional_actions_pressed();
442  if(!str_vec.empty()) {
443  execute_action(str_vec, 0, 0, false);
444  return;
445  }
446 
447  bool was_scrolling = scrolling_;
448 
449  int mousex, mousey;
450  uint8_t mouse_flags = sdl::get_mouse_state(&mousex, &mousey);
451 
452  scrolling_ = handle_scroll(mousex, mousey, mouse_flags);
453 
454  map_location highlighted_hex = get_display().mouseover_hex();
455 
456  // be nice when window is not visible // NOTE should be handled by display instead, to only disable drawing
457  if(is_delay_enabled && !video::window_is_visible()) {
458  SDL_Delay(200);
459  }
460 
461  // Scrolling ended, update the cursor and the brightened hex
462  if(!scrolling_ && was_scrolling) {
463  get_mouse_handler_base().mouse_update(is_browsing(), highlighted_hex);
464  }
465 }
466 
468  const std::vector<config>& items_arg, int xloc, int yloc, bool context_menu, display& disp)
469 {
471  if(!cmd_exec) {
472  return;
473  }
474 
475  std::vector<config> items;
476  for(const config& c : items_arg) {
477  const std::string& id = c["id"];
478  const hotkey::ui_command cmd = hotkey::ui_command(id);
479 
480  if(cmd_exec->can_execute_command(cmd) && (!context_menu || in_context_menu(cmd))) {
481  items.emplace_back(c);
482  }
483  }
484 
485  if(items.empty()) {
486  return;
487  }
488 
489  cmd_exec->show_menu(items, xloc, yloc, context_menu, disp);
490 }
491 
492 void controller_base::execute_action(const std::vector<std::string>& items_arg, int xloc, int yloc, bool context_menu)
493 {
495  if(!cmd_exec) {
496  return;
497  }
498 
499  std::vector<std::string> items;
500  for(const std::string& item : items_arg) {
502  if(cmd_exec->can_execute_command(cmd)) {
503  items.push_back(item);
504  }
505  }
506 
507  if(items.empty()) {
508  return;
509  }
510 
511  cmd_exec->execute_action(items, xloc, yloc, context_menu, get_display());
512 }
513 
515 {
516  return true;
517 }
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:159
void handle_event(const SDL_Event &event) override
uint32_t last_scroll_tick_
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
virtual void play_slice(bool is_delay_enabled=true)
void handle_event(const SDL_Event &event) override
Process mouse- and keypress-events from SDL.
virtual void process(events::pump_info &) override
virtual void process_keyup_event(const SDL_Event &)
Process keyup (always).
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 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:88
bool scroll(int xmov, int ymov, bool force=false)
Scrolls the display by xmov,ymov pixels.
Definition: display.cpp:1735
const theme::action * action_pressed()
Definition: display.cpp:1562
theme & get_theme()
Definition: display.hpp:387
const theme::menu * menu_pressed()
Definition: display.cpp:1578
rect map_area() const
Returns the area used for the map.
Definition: display.cpp:514
const map_location & mouseover_hex() const
Definition: display.hpp:308
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.
bool mouse_scroll_enabled()
virtual rect & location(const SDL_Rect &screen) const
Definition: theme.cpp:317
const std::vector< config > & items() const
Definition: theme.hpp:236
const menu * context_menu() const
Definition: theme.hpp:260
static lg::log_domain log_display("display")
static const int long_touch_duration_ms
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:751
void raise_process_event()
Definition: events.cpp:756
void pump()
Process all events currently in the queue.
Definition: events.cpp:479
Game configuration data as global variables.
Definition: build_info.cpp:61
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:127
bool is_in_dialog()
Is a dialog open?
Definition: handler.cpp:1085
bool remove_timer(const std::size_t id)
Removes a timer.
Definition: timer.cpp:168
bool execute_timer(const std::size_t id)
Executes a timer.
Definition: timer.cpp:201
bool in_dialog()
Definition: show_dialog.cpp:56
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:410
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)
const std::vector< std::string > items
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:83
bool window_has_mouse_focus()
True iff the window has mouse focus.
Definition: video.cpp:699
bool window_is_visible()
True iff the window is not hidden.
Definition: video.cpp:689
point game_canvas_size()
The size of the game canvas, in drawing coordinates / game pixels.
Definition: video.cpp:434
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:429
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:38
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:52
mock_char c
Contains the gui2 timer routines.
#define a