The Battle for Wesnoth  1.19.24+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  float x_now;
66  float 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_MASK(SDL_BUTTON_LEFT)) != 0)
84  {
85  show_menu(get_display().get_theme().context_menu(), { static_cast<int>(x_now), static_cast<int>(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_EVENT_TEXT_INPUT:
104  if(have_keyboard_focus()) {
106  }
107  break;
108 
109  case SDL_EVENT_TEXT_EDITING:
110  if(have_keyboard_focus()) {
111  SDL_Event evt = event;
112  evt.type = SDL_EVENT_TEXT_INPUT;
114  SDL_StopTextInput(video::get_window());
115  SDL_StartTextInput(video::get_window());
116  }
117  break;
118 
119  case SDL_EVENT_KEY_DOWN:
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.key == SDLK_ESCAPE
124  || event.key.key == SDLK_AC_BACK)
125  {
127  break;
128  }
129 
130  process_keydown_event(event);
132  process_keyup_event(event);
133  } else {
135  }
136  break;
137 
138  case SDL_EVENT_KEY_UP:
139  process_keyup_event(event);
141  break;
142 
143  case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
145  break;
146 
147  case SDL_EVENT_JOYSTICK_HAT_MOTION:
149  break;
150 
151  case SDL_EVENT_MOUSE_MOTION:
152  // Ignore old mouse motion events in the event queue
153  if(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_EVENT_MOUSE_MOTION, SDL_EVENT_MOUSE_MOTION) > 0) {
154  while(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_EVENT_MOUSE_MOTION, SDL_EVENT_MOUSE_MOTION) > 0) {};
155 
156  if(!events::is_touch(new_event.motion)) {
157  mh_base.mouse_motion_event(new_event.motion, is_browsing());
158  }
159  } else {
160  if(!events::is_touch(new_event.motion)) {
161  mh_base.mouse_motion_event(event.motion, is_browsing());
162  }
163  }
164  break;
165 
166  case SDL_EVENT_FINGER_MOTION:
167  if(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_EVENT_FINGER_MOTION, SDL_EVENT_FINGER_MOTION) > 0) {
168  while(SDL_PeepEvents(&new_event, 1, SDL_GETEVENT, SDL_EVENT_FINGER_MOTION, SDL_EVENT_FINGER_MOTION) > 0) {
169  };
170  mh_base.touch_motion_event(new_event.tfinger, is_browsing());
171  } else {
172  mh_base.touch_motion_event(event.tfinger, is_browsing());
173  }
174  break;
175 
176  case SDL_EVENT_MOUSE_BUTTON_DOWN:
177  if(events::is_touch(event.button)) {
178  int x = event.button.x;
179  int y = event.button.y;
180 
181  if(long_touch_timer_ == 0) {
183  std::bind(&controller_base::long_touch_callback, this, x, y));
184  }
185  }
186 
187  mh_base.mouse_press(event.button, is_browsing());
189  break;
190 
191  case SDL_EVENT_FINGER_DOWN:
192  // handled by mouse case
193  break;
194 
195  case SDL_EVENT_MOUSE_BUTTON_UP:
196  if(long_touch_timer_ != 0) {
198  long_touch_timer_ = 0;
199  }
200 
201  mh_base.mouse_press(event.button, is_browsing());
202  if(mh_base.get_show_menu()) {
203  show_menu(get_display().get_theme().context_menu(), { static_cast<int>(event.button.x), static_cast<int>(event.button.y) }, true);
204  }
205  break;
206 
207  case SDL_EVENT_FINGER_UP:
208  // handled by mouse case
209  break;
210 
211  case SDL_EVENT_MOUSE_WHEEL:
212  // Right and down are positive in Wesnoth's map.
213  // Right and up are positive in SDL_MouseWheelEvent on all platforms:
214  // https://wiki.libsdl.org/SDL2/SDL_MouseWheelEvent
215 #if defined(_WIN32) || defined(__APPLE__)
216  mh_base.mouse_wheel(event.wheel.x, -event.wheel.y, is_browsing());
217 #else
218  {
219  mh_base.mouse_wheel(event.wheel.x, -event.wheel.y, is_browsing());
220  }
221 #endif
222  break;
223 
224  case TIMER_EVENT:
225  gui2::execute_timer(reinterpret_cast<std::size_t>(event.user.data1));
226  break;
227 
228  default:
229  break;
230  }
231 }
232 
234 {
235  if(gui2::is_in_dialog()) {
236  return;
237  }
238 
240 }
241 
243 {
244  if(event.type == SDL_EVENT_KEY_UP) {
246  }
247 }
248 
250 {
251  return true;
252 }
253 
254 bool controller_base::handle_scroll(int mousex, int mousey, int mouse_flags)
255 {
256  const bool mouse_in_window =
259 
260  int scroll_speed = prefs::get().scroll_speed();
261  double dx = 0.0, dy = 0.0;
262 
263  int scroll_threshold = prefs::get().mouse_scrolling()
265  : 0;
266 
267  for(const theme::menu& m : get_display().get_theme().menus()) {
268  if(m.get_location().contains(mousex, mousey)) {
269  scroll_threshold = 0;
270  }
271  }
272 
273  // Scale scroll distance according to time passed
274  auto tick_now = std::chrono::steady_clock::now();
275 
276  // If we weren't previously scrolling, start small.
277  auto dt = 1ms;
278  if (scrolling_) {
279  dt = std::chrono::duration_cast<std::chrono::milliseconds>(tick_now - last_scroll_tick_);
280  }
281 
282  // scroll_speed is in percent. Ticks are in milliseconds.
283  // Let's assume the maximum speed (100) moves 50 hexes per second,
284  // i.e. 3600 pixels per 1000 ticks.
285  double scroll_amount = dt.count() * 0.036 * double(scroll_speed);
286  last_scroll_tick_ = tick_now;
287 
288  // Apply keyboard scrolling
289  dy -= scroll_up_ * scroll_amount;
290  dy += scroll_down_ * scroll_amount;
291  dx -= scroll_left_ * scroll_amount;
292  dx += scroll_right_ * scroll_amount;
293 
294  // Scroll if mouse is placed near the edge of the screen
295  if(mouse_in_window) {
296  if(mousey < scroll_threshold) {
297  dy -= scroll_amount;
298  }
299 
300  if(mousey > video::game_canvas_size().y - scroll_threshold) {
301  dy += scroll_amount;
302  }
303 
304  if(mousex < scroll_threshold) {
305  dx -= scroll_amount;
306  }
307 
308  if(mousex > video::game_canvas_size().x - scroll_threshold) {
309  dx += scroll_amount;
310  }
311  }
312 
314 
315  // Scroll with middle-mouse if enabled
316  if((mouse_flags & SDL_BUTTON_MMASK) != 0 && prefs::get().middle_click_scrolls()) {
317  const point original_loc = mh_base.get_scroll_start();
318 
319  if(mh_base.scroll_started()
320  && get_display().map_outside_area().contains(mousex, mousey))
321  {
322  // Scroll speed is proportional from the distance from the first
323  // middle click and scrolling speed preference.
324  const double speed = 0.01 * scroll_amount;
325  const double snap_dist = 16; // Snap to horizontal/vertical scrolling
326  const double x_diff = (mousex - original_loc.x);
327  const double y_diff = (mousey - original_loc.y);
328 
329  if(std::fabs(x_diff) > snap_dist || std::fabs(y_diff) <= snap_dist) {
330  dx += speed * x_diff;
331  }
332 
333  if(std::fabs(y_diff) > snap_dist || std::fabs(x_diff) <= snap_dist) {
334  dy += speed * y_diff;
335  }
336  } else { // Event may fire mouse down out of order with respect to initial click
337  mh_base.set_scroll_start(mousex, mousey);
338  }
339  }
340 
341  // If nothing is scrolling, just return.
342  if (!dx && !dy) {
343  return false;
344  }
345 
346  // If we are continuing a scroll, carry over any subpixel movement.
347  if (scrolling_) {
348  dx += scroll_carry_x_;
349  dy += scroll_carry_y_;
350  }
351  point dist{int(dx), int(dy)};
352  scroll_carry_x_ = dx - double(dist.x);
353  scroll_carry_y_ = dy - double(dist.y);
354 
355  // Scroll the display
356  get_display().scroll(dist);
357 
358  // Even if the integer parts are both zero, we are still scrolling.
359  // The subpixel amounts will add up.
360  return true;
361 }
362 
364 {
366  l->play_slice();
367  }
368 
369  events::pump();
371  events::draw();
372 
373  // Update sound sources before scrolling
375  l->update();
376  }
377 
378  const theme::menu* const m = get_display().menu_pressed();
379  if(m != nullptr) {
380  const rect& menu_loc = m->location(video::game_canvas());
381  show_menu(m, { menu_loc.x + 1, menu_loc.y + menu_loc.h + 1 }, false);
382  return;
383  }
384 
385  const theme::action* const a = get_display().action_pressed();
386  if(a != nullptr) {
387  execute_action(a->items());
388  return;
389  }
390 
391  auto str_vec = additional_actions_pressed();
392  if(!str_vec.empty()) {
393  execute_action(str_vec);
394  return;
395  }
396 
397  bool was_scrolling = scrolling_;
398 
399  float mousex, mousey;
400  uint8_t mouse_flags = sdl::get_mouse_state(&mousex, &mousey);
401 
402  scrolling_ = handle_scroll(mousex, mousey, mouse_flags);
403 
404  map_location highlighted_hex = get_display().mouseover_hex();
405 
406  // Scrolling ended, update the cursor and the brightened hex
407  if(!scrolling_ && was_scrolling) {
408  get_mouse_handler_base().mouse_update(is_browsing(), highlighted_hex);
409  }
410 }
411 
412 bool controller_base::show_menu(const theme::menu* menu, const point& loc, bool context_menu)
413 {
415  if(!menu || !cmd_exec) {
416  return false;
417  }
418 
419  // context menus cannot appear outside map area,
420  // but main top-panel menus can.
421  if(context_menu && !get_display().map_area().contains(loc)) {
422  return false;
423  }
424 
425  cmd_exec->show_menu(menu->items(), loc, context_menu);
426  return true;
427 }
428 
429 void controller_base::execute_action(const std::vector<std::string>& items)
430 {
432  if(!cmd_exec) {
433  return;
434  }
435 
436  cmd_exec->execute_action(items);
437 }
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:1410
const theme::menu * menu_pressed()
Definition: display.cpp:1426
const map_location & mouseover_hex() const
Definition: display.hpp:299
bool scroll(const point &amount, bool force=false)
Scrolls the display by amount pixels.
Definition: display.cpp:1582
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.
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:333
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:29
Contains functions for cleanly handling SDL input.
Standard logging facilities (interface).
CURSOR_TYPE get()
Definition: cursor.cpp:206
void draw()
Trigger a draw cycle.
Definition: events.cpp:712
void raise_process_event()
Definition: events.cpp:717
void pump()
Process all events currently in the queue.
Definition: events.cpp:480
bool is_touch(const SDL_MouseButtonEvent &event)
Check if this mouse button event is caused by a touch.
Definition: events.cpp:763
Game configuration data as global variables.
Definition: build_info.cpp:68
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:1090
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(float *x, float *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:87
bool window_has_mouse_focus()
True iff the window has mouse focus.
Definition: video.cpp:745
point game_canvas_size()
The size of the game canvas, in drawing coordinates / game pixels.
Definition: video.cpp:438
SDL_Window * get_window()
Definition: video.cpp:697
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:433
This file contains the settings handling of the widget library.
Encapsulates the map of the game.
Definition: location.hpp:46
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.