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 "map/map.hpp"
25 #include "mouse_handler_base.hpp"
26 #include "preferences/game.hpp"
28 #include "show_dialog.hpp" //gui::in_dialog
29 #include "gui/core/event/handler.hpp" // gui2::is_in_dialog
30 #include "soundsource.hpp"
31 #include "gui/core/timer.hpp"
32 #include "sdl/input.hpp" // get_mouse_state
33 #include "video.hpp"
34 
35 static lg::log_domain log_display("display");
36 #define ERR_DP LOG_STREAM(err, log_display)
37 
38 static const int long_touch_duration_ms = 800;
39 
41  : game_config_(game_config_manager::get()->game_config())
42  , key_()
43  , scrolling_(false)
44  , scroll_up_(false)
45  , scroll_down_(false)
46  , scroll_left_(false)
47  , scroll_right_(false)
48  , last_scroll_tick_(0)
49  , scroll_carry_x_(0.0)
50  , scroll_carry_y_(0.0)
51  , key_release_listener_(*this)
52  , last_mouse_is_touch_(false)
53  , long_touch_timer_(0)
54 {
55 }
56 
58 {
59  if(long_touch_timer_ != 0) {
62  }
63 }
64 
66 {
67  if(long_touch_timer_ != 0 && !get_mouse_handler_base().dragging_started()) {
68  int x_now;
69  int y_now;
70  uint32_t mouse_state = sdl::get_mouse_state(&x_now, &y_now);
71 
72 #ifdef MOUSE_TOUCH_EMULATION
73  if(mouse_state & SDL_BUTTON(SDL_BUTTON_RIGHT)) {
74  // Monkey-patch touch controls again to make them look like left button.
75  mouse_state = SDL_BUTTON(SDL_BUTTON_LEFT);
76  }
77 #endif
78 
79  // Workaround for double-menu b/c of slow events processing, or I don't know.
80  int dx = x - x_now;
81  int dy = y - y_now;
82  int threshold = get_mouse_handler_base().drag_threshold();
83  bool yes_actually_dragging = dx * dx + dy * dy >= threshold * threshold;
84 
85  if(!yes_actually_dragging
86  && (mouse_state & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0
87  && get_display().map_area().contains(x_now, y_now))
88  {
90  if(m != nullptr) {
91  show_menu(get_display().get_theme().context_menu()->items(), x_now, y_now, true, get_display());
92  }
93  }
94  }
95 
97 }
98 
99 void controller_base::handle_event(const SDL_Event& event)
100 {
101  if(gui::in_dialog()) {
102  return;
103  }
104 
106 
107  SDL_Event new_event = {};
108 
109  switch(event.type) {
110  case SDL_TEXTINPUT:
111  if(have_keyboard_focus()) {
113  }
114  break;
115 
116  case SDL_TEXTEDITING:
117  if(have_keyboard_focus()) {
118  SDL_Event evt = event;
119  evt.type = SDL_TEXTINPUT;
121  SDL_StopTextInput();
122  SDL_StartTextInput();
123  }
124  break;
125 
126  case SDL_KEYDOWN:
127  // Detect key press events, unless there something that has keyboard focus
128  // in which case the key press events should go only to it.
129  if(have_keyboard_focus()) {
130  if(event.key.keysym.sym == SDLK_ESCAPE) {
132  break;
133  }
134 
135  process_keydown_event(event);
137  process_keyup_event(event);
138  } else {
140  }
141  break;
142 
143  case SDL_KEYUP:
144  process_keyup_event(event);
146  break;
147 
148  case SDL_JOYBUTTONDOWN:
150  break;
151 
152  case SDL_JOYHATMOTION:
154  break;
155 
156  case SDL_MOUSEMOTION:
157  // Ignore old mouse motion events in the event queue
158  if(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_MOUSEMOTION, SDL_MOUSEMOTION) > 0) {
159  while(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_MOUSEMOTION, SDL_MOUSEMOTION) > 0) {
160  };
161  if(new_event.motion.which != SDL_TOUCH_MOUSEID) {
162  mh_base.mouse_motion_event(new_event.motion, is_browsing());
163  }
164  } else {
165  if(new_event.motion.which != SDL_TOUCH_MOUSEID) {
166  mh_base.mouse_motion_event(event.motion, is_browsing());
167  }
168  }
169  break;
170 
171  case SDL_FINGERMOTION:
172  if(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_FINGERMOTION, SDL_FINGERMOTION) > 0) {
173  while(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_FINGERMOTION, SDL_FINGERMOTION) > 0) {
174  };
175  mh_base.touch_motion_event(new_event.tfinger, is_browsing());
176  } else {
177  mh_base.touch_motion_event(event.tfinger, is_browsing());
178  }
179  break;
180 
181  case SDL_MOUSEBUTTONDOWN:
182  last_mouse_is_touch_ = event.button.which == SDL_TOUCH_MOUSEID;
183 
187  std::bind(&controller_base::long_touch_callback, this, event.button.x, event.button.y));
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  last_mouse_is_touch_ = event.button.which == SDL_TOUCH_MOUSEID;
205 
206  mh_base.mouse_press(event.button, is_browsing());
207  if(mh_base.get_show_menu()) {
208  show_menu(get_display().get_theme().context_menu()->items(), event.button.x, event.button.y, true,
209  get_display());
210  }
211  break;
212  case DOUBLE_CLICK_EVENT:
213  {
214  int x = static_cast<int>(reinterpret_cast<std::intptr_t>(event.user.data1));
215  int y = static_cast<int>(reinterpret_cast<std::intptr_t>(event.user.data2));
216  if(event.user.code == static_cast<int>(SDL_TOUCH_MOUSEID)
217  // TODO: Move to right_click_show_menu?
218  && get_display().map_area().contains(x, y)
219  // TODO: This chain repeats in several places, move to a method.
220  && get_display().get_theme().context_menu() != nullptr) {
221  show_menu(get_display().get_theme().context_menu()->items(),
222  x,
223  y,
224  true,
225  get_display());
226  }
227  }
228  break;
229 
230  case SDL_FINGERUP:
231  // handled by mouse case
232  break;
233 
234  case SDL_MOUSEWHEEL:
235  // Right and down are positive in Wesnoth's map.
236  // Right and up are positive in SDL_MouseWheelEvent on all platforms:
237  // https://wiki.libsdl.org/SDL2/SDL_MouseWheelEvent
238 #if defined(_WIN32) || defined(__APPLE__)
239  mh_base.mouse_wheel(event.wheel.x, -event.wheel.y, is_browsing());
240 #else
241  // Except right is wrongly negative on X11 in SDL < 2.0.18:
242  // https://github.com/libsdl-org/SDL/pull/4700
243  // https://github.com/libsdl-org/SDL/commit/515b7e9
244  // and on Wayland in SDL < 2.0.20:
245  // https://github.com/libsdl-org/SDL/commit/3e1b3bc
246  // Fixes issues #3362 and #7404, which are a regression caused by pull #2481 that fixed issue #2218.
247  {
248  static int xmul = 0;
249  if(xmul == 0) {
250  xmul = 1;
251  const char* video_driver = SDL_GetCurrentVideoDriver();
252  SDL_version ver;
253  SDL_GetVersion(&ver);
254  if(video_driver != nullptr && ver.major <= 2 && ver.minor <= 0) {
255  if(std::strcmp(video_driver, "x11") == 0 && ver.patch < 18) {
256  xmul = -1;
257  } else if(std::strcmp(video_driver, "wayland") == 0 && ver.patch < 20) {
258  xmul = -1;
259  }
260  }
261  }
262  mh_base.mouse_wheel(xmul * event.wheel.x, -event.wheel.y, is_browsing());
263  }
264 #endif
265  break;
266 
267  case TIMER_EVENT:
268  gui2::execute_timer(reinterpret_cast<size_t>(event.user.data1));
269  break;
270 
271  // TODO: Support finger specifically, like pan the map. For now, SDL's "shadow mouse" events will do.
272  case SDL_MULTIGESTURE:
273  default:
274  break;
275  }
276 }
277 
279 {
280  if(gui2::is_in_dialog()) {
281  return;
282  }
283 
285 }
286 
288 {
289  if(event.type == SDL_KEYUP) {
291  }
292 }
293 
295 {
296  return true;
297 }
298 
299 bool controller_base::handle_scroll(int mousex, int mousey, int mouse_flags)
300 {
301  const bool mouse_in_window =
303  || preferences::get("scroll_when_mouse_outside", true);
304 
306  double dx = 0.0, dy = 0.0;
307 
308  int scroll_threshold = preferences::mouse_scroll_enabled()
310  : 0;
311 
312  for(const theme::menu& m : get_display().get_theme().menus()) {
313  if(m.get_location().contains(mousex, mousey)) {
314  scroll_threshold = 0;
315  }
316  }
317 
318  // Scale scroll distance according to time passed
319  uint32_t tick_now = SDL_GetTicks();
320  // If we weren't previously scrolling, start small.
321  int dt = 1;
322  if (scrolling_) {
323  dt = tick_now - last_scroll_tick_;
324  }
325  // scroll_speed is in percent. Ticks are in milliseconds.
326  // Let's assume the maximum speed (100) moves 50 hexes per second,
327  // i.e. 3600 pixels per 1000 ticks.
328  double scroll_amount = double(dt) * 0.036 * double(scroll_speed);
329  last_scroll_tick_ = tick_now;
330 
331  // Apply keyboard scrolling
332  dy -= scroll_up_ * scroll_amount;
333  dy += scroll_down_ * scroll_amount;
334  dx -= scroll_left_ * scroll_amount;
335  dx += scroll_right_ * scroll_amount;
336 
337  // Scroll if mouse is placed near the edge of the screen
338  if(mouse_in_window) {
339  if(mousey < scroll_threshold) {
340  dy -= scroll_amount;
341  }
342 
343  if(mousey > video::game_canvas_size().y - scroll_threshold) {
344  dy += scroll_amount;
345  }
346 
347  if(mousex < scroll_threshold) {
348  dx -= scroll_amount;
349  }
350 
351  if(mousex > video::game_canvas_size().x - scroll_threshold) {
352  dx += scroll_amount;
353  }
354  }
355 
357 
358  // Scroll with middle-mouse if enabled
359  if((mouse_flags & SDL_BUTTON_MMASK) != 0 && preferences::middle_click_scrolls()) {
360  const SDL_Point original_loc = mh_base.get_scroll_start();
361 
362  if(mh_base.scroll_started()) {
363  if(get_display().map_outside_area().contains(mousex, mousey)
364  && mh_base.scroll_started())
365  {
366  // Scroll speed is proportional from the distance from the first
367  // middle click and scrolling speed preference.
368  const double speed = 0.01 * scroll_amount;
369  const double snap_dist = 16; // Snap to horizontal/vertical scrolling
370  const double x_diff = (mousex - original_loc.x);
371  const double y_diff = (mousey - original_loc.y);
372 
373  if(std::fabs(x_diff) > snap_dist || std::fabs(y_diff) <= snap_dist) {
374  dx += speed * x_diff;
375  }
376 
377  if(std::fabs(y_diff) > snap_dist || std::fabs(x_diff) <= snap_dist) {
378  dy += speed * y_diff;
379  }
380  }
381  } else { // Event may fire mouse down out of order with respect to initial click
382  mh_base.set_scroll_start(mousex, mousey);
383  }
384  }
385 
386  // If nothing is scrolling, just return.
387  if (!dx && !dy) {
388  return false;
389  }
390 
391  // If we are continuing a scroll, carry over any subpixel movement.
392  if (scrolling_) {
393  dx += scroll_carry_x_;
394  dy += scroll_carry_y_;
395  }
396  int dx_int = int(dx);
397  int dy_int = int(dy);
398  scroll_carry_x_ = dx - double(dx_int);
399  scroll_carry_y_ = dy - double(dy_int);
400 
401  // Scroll the display
402  get_display().scroll(dx_int, dy_int);
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 
409 void controller_base::play_slice(bool is_delay_enabled)
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  // be nice when window is not visible // NOTE should be handled by display instead, to only disable drawing
458  if(is_delay_enabled && !video::window_is_visible()) {
459  SDL_Delay(200);
460  }
461 
462  // Scrolling ended, update the cursor and the brightened hex
463  if(!scrolling_ && was_scrolling) {
464  get_mouse_handler_base().mouse_update(is_browsing(), highlighted_hex);
465  }
466 }
467 
469  const std::vector<config>& items_arg, int xloc, int yloc, bool context_menu, display& disp)
470 {
472  if(!cmd_exec) {
473  return;
474  }
475 
476  std::vector<config> items;
477  for(const config& c : items_arg) {
478  const std::string& id = c["id"];
479  const hotkey::ui_command cmd = hotkey::ui_command(id);
480 
481  if(cmd_exec->can_execute_command(cmd) && (!context_menu || in_context_menu(cmd))) {
482  items.emplace_back(c);
483  }
484  }
485 
486  if(items.empty()) {
487  return;
488  }
489 
490  cmd_exec->show_menu(items, xloc, yloc, context_menu, disp);
491 }
492 
493 void controller_base::execute_action(const std::vector<std::string>& items_arg, int xloc, int yloc, bool context_menu)
494 {
496  if(!cmd_exec) {
497  return;
498  }
499 
500  std::vector<std::string> items;
501  for(const std::string& item : items_arg) {
503  if(cmd_exec->can_execute_command(cmd)) {
504  items.push_back(item);
505  }
506  }
507 
508  if(items.empty()) {
509  return;
510  }
511 
512  cmd_exec->execute_action(items, xloc, yloc, context_menu, get_display());
513 }
514 
516 {
517  return true;
518 }
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:86
bool scroll(int xmov, int ymov, bool force=false)
Scrolls the display by xmov,ymov pixels.
Definition: display.cpp:1783
const theme::action * action_pressed()
Definition: display.cpp:1610
theme & get_theme()
Definition: display.hpp:385
const theme::menu * menu_pressed()
Definition: display.cpp:1626
rect map_area() const
Returns the area used for the map.
Definition: display.cpp:520
const map_location & mouseover_hex() const
Definition: display.hpp:306
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
virtual rect & location(const SDL_Rect &screen) const
Definition: theme.cpp:318
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 ...
#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:743
void raise_process_event()
Definition: events.cpp:748
void pump()
Process all events currently in the queue.
Definition: events.cpp:478
Game configuration data as global variables.
Definition: build_info.cpp:63
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:1088
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:60
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:414
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
int mouse_scroll_threshold()
Gets the threshold for when to scroll.
Definition: general.cpp:812
bool mouse_scroll_enabled()
Definition: general.cpp:802
bool middle_click_scrolls()
Definition: general.cpp:797
std::string get(const std::string &key)
Definition: general.cpp:213
int scroll_speed()
Definition: general.cpp:787
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:84
bool window_has_mouse_focus()
True iff the window has mouse focus.
Definition: video.cpp:703
bool window_is_visible()
True iff the window is not hidden.
Definition: video.cpp:693
point game_canvas_size()
The size of the game canvas, in drawing coordinates / game pixels.
Definition: video.cpp:438
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:433
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:54
mock_char c
Contains the gui2 timer routines.
#define a