The Battle for Wesnoth  1.19.13+dev
controller_base.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
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);
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  }
209  break;
210  case DOUBLE_CLICK_EVENT:
211  {
212  int x = static_cast<int>(reinterpret_cast<std::intptr_t>(event.user.data1));
213  int y = static_cast<int>(reinterpret_cast<std::intptr_t>(event.user.data2));
214  if(event.user.code == static_cast<int>(SDL_TOUCH_MOUSEID)
215  // TODO: Move to right_click_show_menu?
216  && get_display().map_area().contains(x, y)
217  // TODO: This chain repeats in several places, move to a method.
218  && get_display().get_theme().context_menu() != nullptr) {
219  show_menu(get_display().get_theme().context_menu()->items(),
220  x,
221  y,
222  true);
223  }
224  }
225  break;
226 
227  case SDL_FINGERUP:
228  // handled by mouse case
229  break;
230 
231  case SDL_MOUSEWHEEL:
232  // Right and down are positive in Wesnoth's map.
233  // Right and up are positive in SDL_MouseWheelEvent on all platforms:
234  // https://wiki.libsdl.org/SDL2/SDL_MouseWheelEvent
235 #if defined(_WIN32) || defined(__APPLE__)
236  mh_base.mouse_wheel(event.wheel.x, -event.wheel.y, is_browsing());
237 #else
238  // Except right is wrongly negative on X11 in SDL < 2.0.18:
239  // https://github.com/libsdl-org/SDL/pull/4700
240  // https://github.com/libsdl-org/SDL/commit/515b7e9
241  // and on Wayland in SDL < 2.0.20:
242  // https://github.com/libsdl-org/SDL/commit/3e1b3bc
243  // Fixes issues #3362 and #7404, which are a regression caused by pull #2481 that fixed issue #2218.
244  {
245  static int xmul = 0;
246  if(xmul == 0) {
247  xmul = 1;
248  const char* video_driver = SDL_GetCurrentVideoDriver();
249  SDL_version ver;
250  SDL_GetVersion(&ver);
251  if(video_driver != nullptr && ver.major <= 2 && ver.minor <= 0) {
252  if(std::strcmp(video_driver, "x11") == 0 && ver.patch < 18) {
253  xmul = -1;
254  } else if(std::strcmp(video_driver, "wayland") == 0 && ver.patch < 20) {
255  xmul = -1;
256  }
257  }
258  }
259  mh_base.mouse_wheel(xmul * event.wheel.x, -event.wheel.y, is_browsing());
260  }
261 #endif
262  break;
263 
264  case TIMER_EVENT:
265  gui2::execute_timer(reinterpret_cast<size_t>(event.user.data1));
266  break;
267 
268  // TODO: Support finger specifically, like pan the map. For now, SDL's "shadow mouse" events will do.
269  case SDL_MULTIGESTURE:
270  default:
271  break;
272  }
273 }
274 
276 {
277  if(gui2::is_in_dialog()) {
278  return;
279  }
280 
282 }
283 
285 {
286  if(event.type == SDL_KEYUP) {
288  }
289 }
290 
292 {
293  return true;
294 }
295 
296 bool controller_base::handle_scroll(int mousex, int mousey, int mouse_flags)
297 {
298  const bool mouse_in_window =
301 
302  int scroll_speed = prefs::get().scroll_speed();
303  double dx = 0.0, dy = 0.0;
304 
305  int scroll_threshold = prefs::get().mouse_scrolling()
307  : 0;
308 
309  for(const theme::menu& m : get_display().get_theme().menus()) {
310  if(m.get_location().contains(mousex, mousey)) {
311  scroll_threshold = 0;
312  }
313  }
314 
315  // Scale scroll distance according to time passed
316  auto tick_now = std::chrono::steady_clock::now();
317 
318  // If we weren't previously scrolling, start small.
319  auto dt = 1ms;
320  if (scrolling_) {
321  dt = std::chrono::duration_cast<std::chrono::milliseconds>(tick_now - last_scroll_tick_);
322  }
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 = dt.count() * 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  point dist{int(dx), int(dy)};
396  scroll_carry_x_ = dx - double(dist.x);
397  scroll_carry_y_ = dy - double(dist.y);
398 
399  // Scroll the display
400  get_display().scroll(dist);
401 
402  // Even if the integer parts are both zero, we are still scrolling.
403  // The subpixel amounts will add up.
404  return true;
405 }
406 
408 {
409  CKey key;
410 
412  l->play_slice();
413  }
414 
415  events::pump();
417  events::draw();
418 
419  // Update sound sources before scrolling
421  l->update();
422  }
423 
424  const theme::menu* const m = get_display().menu_pressed();
425  if(m != nullptr) {
426  const rect& menu_loc = m->location(video::game_canvas());
427  show_menu(m->items(), menu_loc.x + 1, menu_loc.y + menu_loc.h + 1, false);
428 
429  return;
430  }
431 
432  const theme::action* const a = get_display().action_pressed();
433  if(a != nullptr) {
434  execute_action(a->items());
435  return;
436  }
437 
438  auto str_vec = additional_actions_pressed();
439  if(!str_vec.empty()) {
440  execute_action(str_vec);
441  return;
442  }
443 
444  bool was_scrolling = scrolling_;
445 
446  int mousex, mousey;
447  uint8_t mouse_flags = sdl::get_mouse_state(&mousex, &mousey);
448 
449  scrolling_ = handle_scroll(mousex, mousey, mouse_flags);
450 
451  map_location highlighted_hex = get_display().mouseover_hex();
452 
453  // Scrolling ended, update the cursor and the brightened hex
454  if(!scrolling_ && was_scrolling) {
455  get_mouse_handler_base().mouse_update(is_browsing(), highlighted_hex);
456  }
457 }
458 
459 void controller_base::show_menu(const std::vector<config>& items, int xloc, int yloc, bool context_menu)
460 {
462  if(!cmd_exec) {
463  return;
464  }
465 
466  cmd_exec->show_menu(items, xloc, yloc, context_menu);
467 }
468 
469 void controller_base::execute_action(const std::vector<std::string>& items)
470 {
472  if(!cmd_exec) {
473  return;
474  }
475 
476  cmd_exec->execute_action(items);
477 }
478 
480 {
481  return true;
482 }
Class that keeps track of all the keys on the keyboard.
Definition: key.hpp:29
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 execute_action(const std::vector< std::string > &items_arg)
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 bool is_browsing() const
virtual void show_menu(const std::vector< config > &items_arg, int xloc, int yloc, bool context_menu)
const theme::action * action_pressed()
Definition: display.cpp:1407
theme & get_theme()
Definition: display.hpp:381
const theme::menu * menu_pressed()
Definition: display.cpp:1423
rect map_area() const
Returns the area used for the map.
Definition: display.cpp:484
const map_location & mouseover_hex() const
Definition: display.hpp:301
bool scroll(const point &amount, bool force=false)
Scrolls the display by amount pixels.
Definition: display.cpp:1579
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)
virtual void show_menu(const std::vector< config > &items_arg, int xloc, int yloc, bool context_menu)
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.
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:218
void draw()
Trigger a draw cycle.
Definition: events.cpp:725
void raise_process_event()
Definition: events.cpp:730
void pump()
Process all events currently in the queue.
Definition: events.cpp:483
Game configuration data as global variables.
Definition: build_info.cpp:61
bool is_in_dialog()
Is a dialog open?
Definition: handler.cpp:1122
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:86
bool window_has_mouse_focus()
True iff the window has mouse focus.
Definition: video.cpp:728
point game_canvas_size()
The size of the game canvas, in drawing coordinates / game pixels.
Definition: video.cpp:446
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:441
Used as the main parameter for can_execute_command/do_execute_command These functions are used to exe...
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:49
bool contains(int x, int y) const
Whether the given point lies within the rectangle.
Definition: rect.cpp:54
Contains the gui2 timer routines.