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"
22 #include "gui/widgets/settings.hpp"
24 #include "log.hpp"
25 #include "mouse_handler_base.hpp"
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 using namespace std::chrono_literals;
38 
40  : game_config_(game_config_manager::get()->game_config())
41  , scrolling_(false)
42  , scroll_up_(false)
43  , scroll_down_(false)
44  , scroll_left_(false)
45  , scroll_right_(false)
46  , last_scroll_tick_()
47  , scroll_carry_x_(0.0)
48  , scroll_carry_y_(0.0)
49  , key_release_listener_(*this)
50  , long_touch_timer_(0)
51 {
52 }
53 
55 {
56  if(long_touch_timer_ != 0) {
59  }
60 }
61 
63 {
64  if(long_touch_timer_ != 0 && !get_mouse_handler_base().dragging_started()) {
65  int x_now;
66  int y_now;
67  uint32_t mouse_state = sdl::get_mouse_state(&x_now, &y_now);
68 
69 #ifdef MOUSE_TOUCH_EMULATION
70  if(mouse_state & SDL_BUTTON(SDL_BUTTON_RIGHT)) {
71  // Monkey-patch touch controls again to make them look like left button.
72  mouse_state = SDL_BUTTON(SDL_BUTTON_LEFT);
73  }
74 #endif
75 
76  // Workaround for double-menu b/c of slow events processing, or I don't know.
77  int dx = x - x_now;
78  int dy = y - y_now;
79  int threshold = get_mouse_handler_base().drag_threshold();
80  bool yes_actually_dragging = dx * dx + dy * dy >= threshold * threshold;
81 
82  if(!yes_actually_dragging
83  && (mouse_state & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0)
84  {
85  show_menu(get_display().get_theme().context_menu(), { x_now, y_now }, true);
86  }
87  }
88 
90 }
91 
92 void controller_base::handle_event(const SDL_Event& event)
93 {
94  if(gui2::is_in_dialog()) {
95  return;
96  }
97 
99 
100  SDL_Event new_event = {};
101 
102  switch(event.type) {
103  case SDL_TEXTINPUT:
104  if(have_keyboard_focus()) {
106  }
107  break;
108 
109  case SDL_TEXTEDITING:
110  if(have_keyboard_focus()) {
111  SDL_Event evt = event;
112  evt.type = SDL_TEXTINPUT;
114  SDL_StopTextInput();
115  SDL_StartTextInput();
116  }
117  break;
118 
119  case SDL_KEYDOWN:
120  // Detect key press events, unless there something that has keyboard focus
121  // in which case the key press events should go only to it.
122  if(have_keyboard_focus()) {
123  if(event.key.keysym.sym == SDLK_ESCAPE) {
125  break;
126  }
127 
128  process_keydown_event(event);
130  process_keyup_event(event);
131  } else {
133  }
134  break;
135 
136  case SDL_KEYUP:
137  process_keyup_event(event);
139  break;
140 
141  case SDL_JOYBUTTONDOWN:
143  break;
144 
145  case SDL_JOYHATMOTION:
147  break;
148 
149  case SDL_MOUSEMOTION:
150  // Ignore old mouse motion events in the event queue
151  if(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_MOUSEMOTION, SDL_MOUSEMOTION) > 0) {
152  while(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_MOUSEMOTION, SDL_MOUSEMOTION) > 0) {};
153 
154  if(!events::is_touch(new_event.motion)) {
155  mh_base.mouse_motion_event(new_event.motion, is_browsing());
156  }
157  } else {
158  if(!events::is_touch(new_event.motion)) {
159  mh_base.mouse_motion_event(event.motion, is_browsing());
160  }
161  }
162  break;
163 
164  case SDL_FINGERMOTION:
165  if(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_FINGERMOTION, SDL_FINGERMOTION) > 0) {
166  while(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_FINGERMOTION, SDL_FINGERMOTION) > 0) {
167  };
168  mh_base.touch_motion_event(new_event.tfinger, is_browsing());
169  } else {
170  mh_base.touch_motion_event(event.tfinger, is_browsing());
171  }
172  break;
173 
174  case SDL_MOUSEBUTTONDOWN:
175  if(events::is_touch(event.button)) {
176  int x = event.button.x;
177  int y = event.button.y;
178 
179  if(long_touch_timer_ == 0) {
181  std::bind(&controller_base::long_touch_callback, this, x, y));
182  }
183  }
184 
185  mh_base.mouse_press(event.button, is_browsing());
187  break;
188 
189  case SDL_FINGERDOWN:
190  // handled by mouse case
191  break;
192 
193  case SDL_MOUSEBUTTONUP:
194  if(long_touch_timer_ != 0) {
196  long_touch_timer_ = 0;
197  }
198 
199  mh_base.mouse_press(event.button, is_browsing());
200  if(mh_base.get_show_menu()) {
201  show_menu(get_display().get_theme().context_menu(), { event.button.x, event.button.y }, true);
202  }
203  break;
204 
205  case SDL_FINGERUP:
206  // handled by mouse case
207  break;
208 
209  case SDL_MOUSEWHEEL:
210  // Right and down are positive in Wesnoth's map.
211  // Right and up are positive in SDL_MouseWheelEvent on all platforms:
212  // https://wiki.libsdl.org/SDL2/SDL_MouseWheelEvent
213 #if defined(_WIN32) || defined(__APPLE__)
214  mh_base.mouse_wheel(event.wheel.x, -event.wheel.y, is_browsing());
215 #else
216  // Except right is wrongly negative on X11 in SDL < 2.0.18:
217  // https://github.com/libsdl-org/SDL/pull/4700
218  // https://github.com/libsdl-org/SDL/commit/515b7e9
219  // and on Wayland in SDL < 2.0.20:
220  // https://github.com/libsdl-org/SDL/commit/3e1b3bc
221  // Fixes issues #3362 and #7404, which are a regression caused by pull #2481 that fixed issue #2218.
222  {
223  static int xmul = 0;
224  if(xmul == 0) {
225  xmul = 1;
226  const char* video_driver = SDL_GetCurrentVideoDriver();
227  SDL_version ver;
228  SDL_GetVersion(&ver);
229  if(video_driver != nullptr && ver.major <= 2 && ver.minor <= 0) {
230  if(std::strcmp(video_driver, "x11") == 0 && ver.patch < 18) {
231  xmul = -1;
232  } else if(std::strcmp(video_driver, "wayland") == 0 && ver.patch < 20) {
233  xmul = -1;
234  }
235  }
236  }
237  mh_base.mouse_wheel(xmul * event.wheel.x, -event.wheel.y, is_browsing());
238  }
239 #endif
240  break;
241 
242  case TIMER_EVENT:
243  gui2::execute_timer(reinterpret_cast<std::size_t>(event.user.data1));
244  break;
245 
246  // TODO: Support finger specifically, like pan the map. For now, SDL's "shadow mouse" events will do.
247  case SDL_MULTIGESTURE:
248  default:
249  break;
250  }
251 }
252 
254 {
255  if(gui2::is_in_dialog()) {
256  return;
257  }
258 
260 }
261 
263 {
264  if(event.type == SDL_KEYUP) {
266  }
267 }
268 
270 {
271  return true;
272 }
273 
274 bool controller_base::handle_scroll(int mousex, int mousey, int mouse_flags)
275 {
276  const bool mouse_in_window =
279 
280  int scroll_speed = prefs::get().scroll_speed();
281  double dx = 0.0, dy = 0.0;
282 
283  int scroll_threshold = prefs::get().mouse_scrolling()
285  : 0;
286 
287  for(const theme::menu& m : get_display().get_theme().menus()) {
288  if(m.get_location().contains(mousex, mousey)) {
289  scroll_threshold = 0;
290  }
291  }
292 
293  // Scale scroll distance according to time passed
294  auto tick_now = std::chrono::steady_clock::now();
295 
296  // If we weren't previously scrolling, start small.
297  auto dt = 1ms;
298  if (scrolling_) {
299  dt = std::chrono::duration_cast<std::chrono::milliseconds>(tick_now - last_scroll_tick_);
300  }
301 
302  // scroll_speed is in percent. Ticks are in milliseconds.
303  // Let's assume the maximum speed (100) moves 50 hexes per second,
304  // i.e. 3600 pixels per 1000 ticks.
305  double scroll_amount = dt.count() * 0.036 * double(scroll_speed);
306  last_scroll_tick_ = tick_now;
307 
308  // Apply keyboard scrolling
309  dy -= scroll_up_ * scroll_amount;
310  dy += scroll_down_ * scroll_amount;
311  dx -= scroll_left_ * scroll_amount;
312  dx += scroll_right_ * scroll_amount;
313 
314  // Scroll if mouse is placed near the edge of the screen
315  if(mouse_in_window) {
316  if(mousey < scroll_threshold) {
317  dy -= scroll_amount;
318  }
319 
320  if(mousey > video::game_canvas_size().y - scroll_threshold) {
321  dy += scroll_amount;
322  }
323 
324  if(mousex < scroll_threshold) {
325  dx -= scroll_amount;
326  }
327 
328  if(mousex > video::game_canvas_size().x - scroll_threshold) {
329  dx += scroll_amount;
330  }
331  }
332 
334 
335  // Scroll with middle-mouse if enabled
336  if((mouse_flags & SDL_BUTTON_MMASK) != 0 && prefs::get().middle_click_scrolls()) {
337  const point original_loc = mh_base.get_scroll_start();
338 
339  if(mh_base.scroll_started()
340  && get_display().map_outside_area().contains(mousex, mousey))
341  {
342  // Scroll speed is proportional from the distance from the first
343  // middle click and scrolling speed preference.
344  const double speed = 0.01 * scroll_amount;
345  const double snap_dist = 16; // Snap to horizontal/vertical scrolling
346  const double x_diff = (mousex - original_loc.x);
347  const double y_diff = (mousey - original_loc.y);
348 
349  if(std::fabs(x_diff) > snap_dist || std::fabs(y_diff) <= snap_dist) {
350  dx += speed * x_diff;
351  }
352 
353  if(std::fabs(y_diff) > snap_dist || std::fabs(x_diff) <= snap_dist) {
354  dy += speed * y_diff;
355  }
356  } else { // Event may fire mouse down out of order with respect to initial click
357  mh_base.set_scroll_start(mousex, mousey);
358  }
359  }
360 
361  // If nothing is scrolling, just return.
362  if (!dx && !dy) {
363  return false;
364  }
365 
366  // If we are continuing a scroll, carry over any subpixel movement.
367  if (scrolling_) {
368  dx += scroll_carry_x_;
369  dy += scroll_carry_y_;
370  }
371  point dist{int(dx), int(dy)};
372  scroll_carry_x_ = dx - double(dist.x);
373  scroll_carry_y_ = dy - double(dist.y);
374 
375  // Scroll the display
376  get_display().scroll(dist);
377 
378  // Even if the integer parts are both zero, we are still scrolling.
379  // The subpixel amounts will add up.
380  return true;
381 }
382 
384 {
386  l->play_slice();
387  }
388 
389  events::pump();
391  events::draw();
392 
393  // Update sound sources before scrolling
395  l->update();
396  }
397 
398  const theme::menu* const m = get_display().menu_pressed();
399  if(m != nullptr) {
400  const rect& menu_loc = m->location(video::game_canvas());
401  show_menu(m, { menu_loc.x + 1, menu_loc.y + menu_loc.h + 1 }, false);
402  return;
403  }
404 
405  const theme::action* const a = get_display().action_pressed();
406  if(a != nullptr) {
407  execute_action(a->items());
408  return;
409  }
410 
411  auto str_vec = additional_actions_pressed();
412  if(!str_vec.empty()) {
413  execute_action(str_vec);
414  return;
415  }
416 
417  bool was_scrolling = scrolling_;
418 
419  int mousex, mousey;
420  uint8_t mouse_flags = sdl::get_mouse_state(&mousex, &mousey);
421 
422  scrolling_ = handle_scroll(mousex, mousey, mouse_flags);
423 
424  map_location highlighted_hex = get_display().mouseover_hex();
425 
426  // Scrolling ended, update the cursor and the brightened hex
427  if(!scrolling_ && was_scrolling) {
428  get_mouse_handler_base().mouse_update(is_browsing(), highlighted_hex);
429  }
430 }
431 
432 bool controller_base::show_menu(const theme::menu* menu, const point& loc, bool context_menu)
433 {
435  if(!menu || !cmd_exec) {
436  return false;
437  }
438 
439  // context menus cannot appear outside map area,
440  // but main top-panel menus can.
441  if(context_menu && !get_display().map_area().contains(loc)) {
442  return false;
443  }
444 
445  cmd_exec->show_menu(menu->items(), loc, context_menu);
446  return true;
447 }
448 
449 void controller_base::execute_action(const std::vector<std::string>& items)
450 {
452  if(!cmd_exec) {
453  return;
454  }
455 
456  cmd_exec->execute_action(items);
457 }
map_location loc
Definition: move.cpp: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.
virtual soundsource::manager * get_soundsource_man()
Get (optionally) a soundsources manager a derived class uses.
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).
bool show_menu(const theme::menu *menu, const point &loc, bool context_menu)
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)
std::size_t long_touch_timer_
Context menu timer.
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
const theme::action * action_pressed()
Definition: display.cpp:1407
const theme::menu * menu_pressed()
Definition: display.cpp:1423
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.
void mouse_motion_event(const SDL_MouseMotionEvent &event, const bool browse)
virtual void mouse_wheel(int xscroll, int yscroll, bool browse)
Called when scrolling with the mouse wheel.
const point get_scroll_start() const
void set_scroll_start(int x, int y)
Called when the middle click scrolling.
virtual void show_menu(const std::vector< config > &items_arg, const point &menu_loc, bool context_menu)
void execute_action(const std::vector< std::string > &items_arg)
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:183
virtual rect & location(const rect &screen) const
Definition: theme.cpp:317
const std::vector< config > & items() const
Definition: theme.hpp:236
static lg::log_domain log_display("display")
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 TIMER_EVENT
Definition: events.hpp:24
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:719
void raise_process_event()
Definition: events.cpp:724
void pump()
Process all events currently in the queue.
Definition: events.cpp:483
bool is_touch(const SDL_MouseButtonEvent &event)
Check if this mouse button event is caused by a touch.
Definition: events.cpp:771
Game configuration data as global variables.
Definition: build_info.cpp:61
std::chrono::milliseconds popup_show_delay
Delay before a popup shows.
Definition: settings.cpp:38
bool is_in_dialog()
Is a dialog open?
Definition: handler.cpp:1121
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
This file contains the settings handling of the widget library.
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
Contains the gui2 timer routines.