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