The Battle for Wesnoth  1.17.17+dev
controller_base.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2023
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 #if defined(_WIN32) || defined(__APPLE__)
236  mh_base.mouse_wheel(-event.wheel.x, event.wheel.y, is_browsing());
237 #else
238  mh_base.mouse_wheel(event.wheel.x, event.wheel.y, is_browsing());
239 #endif
240  break;
241 
242  case TIMER_EVENT:
243  gui2::execute_timer(reinterpret_cast<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 =
278  || preferences::get("scroll_when_mouse_outside", true);
279 
281  double dx = 0.0, dy = 0.0;
282 
283  int scroll_threshold = preferences::mouse_scroll_enabled()
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  uint32_t tick_now = SDL_GetTicks();
295  // If we weren't previously scrolling, start small.
296  int dt = 1;
297  if (scrolling_) {
298  dt = tick_now - last_scroll_tick_;
299  }
300  // scroll_speed is in percent. Ticks are in milliseconds.
301  // Let's assume the maximum speed (100) moves 50 hexes per second,
302  // i.e. 3600 pixels per 1000 ticks.
303  double scroll_amount = double(dt) * 0.036 * double(scroll_speed);
304  last_scroll_tick_ = tick_now;
305 
306  // Apply keyboard scrolling
307  dy -= scroll_up_ * scroll_amount;
308  dy += scroll_down_ * scroll_amount;
309  dx -= scroll_left_ * scroll_amount;
310  dx += scroll_right_ * scroll_amount;
311 
312  // Scroll if mouse is placed near the edge of the screen
313  if(mouse_in_window) {
314  if(mousey < scroll_threshold) {
315  dy -= scroll_amount;
316  }
317 
318  if(mousey > video::game_canvas_size().y - scroll_threshold) {
319  dy += scroll_amount;
320  }
321 
322  if(mousex < scroll_threshold) {
323  dx -= scroll_amount;
324  }
325 
326  if(mousex > video::game_canvas_size().x - scroll_threshold) {
327  dx += scroll_amount;
328  }
329  }
330 
332 
333  // Scroll with middle-mouse if enabled
334  if((mouse_flags & SDL_BUTTON_MMASK) != 0 && preferences::middle_click_scrolls()) {
335  const SDL_Point original_loc = mh_base.get_scroll_start();
336 
337  if(mh_base.scroll_started()) {
338  if(get_display().map_outside_area().contains(mousex, mousey)
339  && mh_base.scroll_started())
340  {
341  // Scroll speed is proportional from the distance from the first
342  // middle click and scrolling speed preference.
343  const double speed = 0.01 * scroll_amount;
344  const double snap_dist = 16; // Snap to horizontal/vertical scrolling
345  const double x_diff = (mousex - original_loc.x);
346  const double y_diff = (mousey - original_loc.y);
347 
348  if(std::fabs(x_diff) > snap_dist || std::fabs(y_diff) <= snap_dist) {
349  dx += speed * x_diff;
350  }
351 
352  if(std::fabs(y_diff) > snap_dist || std::fabs(x_diff) <= snap_dist) {
353  dy += speed * y_diff;
354  }
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  int dx_int = int(dx);
372  int dy_int = int(dy);
373  scroll_carry_x_ = dx - double(dx_int);
374  scroll_carry_y_ = dy - double(dy_int);
375 
376  // Scroll the display
377  get_display().scroll(dx_int, dy_int);
378 
379  // Even if the integer parts are both zero, we are still scrolling.
380  // The subpixel amounts will add up.
381  return true;
382 }
383 
384 void controller_base::play_slice(bool is_delay_enabled)
385 {
386  CKey key;
387 
389  l->play_slice();
390  }
391 
392  events::pump();
394  events::draw();
395 
396  // Update sound sources before scrolling
398  l->update();
399  }
400 
401  const theme::menu* const m = get_display().menu_pressed();
402  if(m != nullptr) {
403  const rect& menu_loc = m->location(video::game_canvas());
404  show_menu(m->items(), menu_loc.x + 1, menu_loc.y + menu_loc.h + 1, false, get_display());
405 
406  return;
407  }
408 
409  const theme::action* const a = get_display().action_pressed();
410  if(a != nullptr) {
411  const rect& action_loc = a->location(video::game_canvas());
412  execute_action(a->items(), action_loc.x + 1, action_loc.y + action_loc.h + 1, false);
413 
414  return;
415  }
416 
417  auto str_vec = additional_actions_pressed();
418  if(!str_vec.empty()) {
419  execute_action(str_vec, 0, 0, false);
420  return;
421  }
422 
423  bool was_scrolling = scrolling_;
424 
425  int mousex, mousey;
426  uint8_t mouse_flags = sdl::get_mouse_state(&mousex, &mousey);
427 
428  scrolling_ = handle_scroll(mousex, mousey, mouse_flags);
429 
430  map_location highlighted_hex = get_display().mouseover_hex();
431 
432  // be nice when window is not visible // NOTE should be handled by display instead, to only disable drawing
433  if(is_delay_enabled && !video::window_is_visible()) {
434  SDL_Delay(200);
435  }
436 
437  // Scrolling ended, update the cursor and the brightened hex
438  if(!scrolling_ && was_scrolling) {
439  get_mouse_handler_base().mouse_update(is_browsing(), highlighted_hex);
440  }
441 }
442 
444  const std::vector<config>& items_arg, int xloc, int yloc, bool context_menu, display& disp)
445 {
447  if(!cmd_exec) {
448  return;
449  }
450 
451  std::vector<config> items;
452  for(const config& c : items_arg) {
453  const std::string& id = c["id"];
454  const hotkey::ui_command cmd = hotkey::ui_command(id);
455 
456  if(cmd_exec->can_execute_command(cmd) && (!context_menu || in_context_menu(cmd))) {
457  items.emplace_back(c);
458  }
459  }
460 
461  if(items.empty()) {
462  return;
463  }
464 
465  cmd_exec->show_menu(items, xloc, yloc, context_menu, disp);
466 }
467 
468 void controller_base::execute_action(const std::vector<std::string>& items_arg, int xloc, int yloc, bool context_menu)
469 {
471  if(!cmd_exec) {
472  return;
473  }
474 
475  std::vector<std::string> items;
476  for(const std::string& item : items_arg) {
478  if(cmd_exec->can_execute_command(cmd)) {
479  items.push_back(item);
480  }
481  }
482 
483  if(items.empty()) {
484  return;
485  }
486 
487  cmd_exec->execute_action(items, xloc, yloc, context_menu, get_display());
488 }
489 
491 {
492  return true;
493 }
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:161
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:87
bool scroll(int xmov, int ymov, bool force=false)
Scrolls the display by xmov,ymov pixels.
Definition: display.cpp:1753
const theme::action * action_pressed()
Definition: display.cpp:1573
theme & get_theme()
Definition: display.hpp:386
const theme::menu * menu_pressed()
Definition: display.cpp:1589
rect map_area() const
Returns the area used for the map.
Definition: display.cpp:519
const map_location & mouseover_hex() const
Definition: display.hpp:307
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:820
bool mouse_scroll_enabled()
Definition: general.cpp:810
bool middle_click_scrolls()
Definition: general.cpp:805
std::string get(const std::string &key)
Definition: general.cpp:213
int scroll_speed()
Definition: general.cpp:791
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:686
bool window_is_visible()
True iff the window is not hidden.
Definition: video.cpp:676
point game_canvas_size()
The size of the game canvas, in drawing coordinates / game pixels.
Definition: video.cpp:426
rect game_canvas()
The game canvas area, in drawing coordinates.
Definition: video.cpp:421
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