The Battle for Wesnoth  1.19.28+dev
hotkey_item.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
3  by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
18 #include "hotkey/hotkey_item.hpp"
19 
20 #include "config.hpp"
21 #include "events.hpp"
22 #include "game_config_view.hpp"
24 #include "key.hpp"
25 #include "log.hpp"
27 #include "sdl/input.hpp" // for sdl::get_mods
29 #include "utils/general.hpp"
30 
31 #include <boost/algorithm/string.hpp>
32 
33 #include <functional>
34 
35 static lg::log_domain log_config("config");
36 #define ERR_G LOG_STREAM(err, lg::general())
37 #define LOG_G LOG_STREAM(info, lg::general())
38 #define DBG_G LOG_STREAM(debug, lg::general())
39 #define ERR_CF LOG_STREAM(err, log_config)
40 
41 namespace hotkey
42 {
43 namespace
44 {
45 hotkey_list hotkeys_;
46 game_config_view default_hotkey_cfg_;
47 
48 const int TOUCH_MOUSE_INDEX = 255;
49 }; // namespace
50 
51 std::string hotkey_base::get_name() const
52 {
53  std::string ret = "";
54 
55  if(mod_ & SDL_KMOD_CTRL) {
56  ret += "ctrl";
57  }
58 
59  ret += (!ret.empty() && !boost::algorithm::ends_with(ret, "+") ? "+" : "");
60  if(mod_ & SDL_KMOD_ALT) {
61 #ifdef __APPLE__
62  ret += "opt";
63 #else
64  ret += "alt";
65 #endif
66  }
67 
68  ret += (!ret.empty() && !boost::algorithm::ends_with(ret, "+") ? "+" : "");
69  if(mod_ & SDL_KMOD_SHIFT) {
70  ret += "shift";
71  }
72 
73  ret += (!ret.empty() && !boost::algorithm::ends_with(ret, "+") ? "+" : "");
74  if(mod_ & SDL_KMOD_GUI) {
75 #ifdef __APPLE__
76  ret += "cmd";
77 #else
78  ret += "win";
79 #endif
80  }
81 
82  ret += (!ret.empty() && !boost::algorithm::ends_with(ret, "+") ? "+" : "");
83  return ret += get_name_helper();
84 }
85 
87 {
88  if(other == nullptr) {
89  return false;
90  }
91 
92  const hk_scopes scopematch =
94  hotkey::get_hotkey_command(other->get_command()).scope;
95 
96  if(scopematch.none()) {
97  return false;
98  }
99 
100  return mod_ == other->mod_ && bindings_equal_helper(other);
101 }
102 
103 bool hotkey_base::matches(const SDL_Event& event) const
104 {
106  return false;
107  }
108 
109  return matches_helper(event);
110 }
111 
112 void hotkey_base::save(config& item) const
113 {
114  item["command"] = get_command();
115  item["disabled"] = is_disabled();
116 
117  item["shift"] = !!(mod_ & SDL_KMOD_SHIFT);
118  item["ctrl"] = !!(mod_ & SDL_KMOD_CTRL);
119  item["cmd"] = !!(mod_ & SDL_KMOD_GUI);
120  item["alt"] = !!(mod_ & SDL_KMOD_ALT);
121 
122  save_helper(item);
123 }
124 
125 hotkey_ptr create_hotkey(const std::string& id, const SDL_Event& event)
126 {
127  hotkey_ptr base = std::make_shared<hotkey_void>();
128  const hotkey_command& command = get_hotkey_command(id);
129  unsigned mods = sdl::get_mods();
130 
131  switch(event.type) {
132  case SDL_EVENT_KEY_UP: {
133  if(mods & SDL_KMOD_CTRL || mods & SDL_KMOD_ALT || mods & SDL_KMOD_GUI || CKey::is_uncomposable(event.key)
134  || command.toggle) {
135  auto keyboard = std::make_shared<hotkey_keyboard>();
136  base = std::dynamic_pointer_cast<hotkey_base>(keyboard);
137  SDL_Keycode code;
138  code = event.key.key;
139  keyboard->set_keycode(code);
140  keyboard->set_text(SDL_GetKeyName(event.key.key));
141  }
142  } break;
143 
144  case SDL_EVENT_TEXT_INPUT: {
145  if(command.toggle) {
146  return nullptr;
147  }
148  auto keyboard = std::make_shared<hotkey_keyboard>();
149  base = std::dynamic_pointer_cast<hotkey_base>(keyboard);
150  std::string text = std::string(event.text.text);
151  keyboard->set_text(text);
152  if(text == ":" || text == "`") {
153  mods = mods & ~SDL_KMOD_SHIFT;
154  }
155  } break;
156 
157  case SDL_EVENT_MOUSE_BUTTON_UP: {
158  auto mouse = std::make_shared<hotkey_mouse>();
159  base = std::dynamic_pointer_cast<hotkey_base>(mouse);
160  mouse->set_button(event.button.button);
161  mouse->set_clicks(event.button.clicks);
162  break;
163  }
164 
165  default:
166  ERR_G << "Trying to bind an unknown event type:" << event.type;
167  break;
168  }
169 
170  base->set_mods(mods);
171  base->set_command(id);
172  base->unset_default();
173 
174  return base;
175 }
176 
178 {
179  hotkey_ptr base = std::make_shared<hotkey_void>();
180 
181  const config::attribute_value& mouse_cfg = cfg["mouse"];
182  if(!mouse_cfg.empty()) {
183  auto mouse = std::make_shared<hotkey_mouse>();
184  base = std::dynamic_pointer_cast<hotkey_base>(mouse);
185 
186  if(mouse_cfg.to_int() == TOUCH_MOUSE_INDEX) {
187  mouse->set_button(TOUCH_MOUSE_INDEX);
188  } else {
189  mouse->set_button(cfg["button"].to_int());
190  }
191 
192  if(!cfg["click"].empty()) {
193  mouse->set_clicks(cfg["click"].to_int(1));
194  }
195  }
196 
197  const std::string& key_cfg = cfg["key"];
198  if(!key_cfg.empty()) {
199  auto keyboard = std::make_shared<hotkey_keyboard>();
200  base = std::dynamic_pointer_cast<hotkey_base>(keyboard);
201 
202  SDL_Keycode keycode = SDL_GetKeyFromName(key_cfg.c_str());
203  if(keycode == SDLK_UNKNOWN) {
204  ERR_G << "Unknown key: " << key_cfg;
205  }
206  keyboard->set_text(key_cfg);
207  keyboard->set_keycode(keycode);
208  }
209 
210  if(base == hotkey_ptr()) {
211  return base;
212  }
213 
214  unsigned int mods = 0;
215 
216  if(cfg["shift"].to_bool())
217  mods |= SDL_KMOD_SHIFT;
218  if(cfg["ctrl"].to_bool())
219  mods |= SDL_KMOD_CTRL;
220  if(cfg["cmd"].to_bool())
221  mods |= SDL_KMOD_GUI;
222  if(cfg["alt"].to_bool())
223  mods |= SDL_KMOD_ALT;
224 
225  base->set_mods(mods);
226  base->set_command(cfg["command"].str());
227 
228  cfg["disabled"].to_bool() ? base->disable() : base->enable();
229 
230  return base;
231 }
232 
233 bool hotkey_mouse::matches_helper(const SDL_Event& event) const
234 {
235  switch(event.type) {
236  case SDL_EVENT_MOUSE_BUTTON_UP:
237  case SDL_EVENT_MOUSE_BUTTON_DOWN:
238  case SDL_EVENT_FINGER_DOWN:
239  case SDL_EVENT_FINGER_UP:
240  break; // Continue with validation
241  default:
242  return false;
243  }
244 
245  unsigned mods = sdl::get_mods();
246  if((mods != mod_)) {
247  return false;
248  }
249 
250  if(events::is_touch(event.button)) {
251  return button_ == TOUCH_MOUSE_INDEX && clicks_ == event.button.clicks;
252  }
253 
254  return event.button.button == button_;
255 }
256 
257 const std::string hotkey_mouse::get_name_helper() const
258 {
259  std::stringstream hotkey_name;
260  switch(button_) {
261  case SDL_BUTTON_LEFT:
262  hotkey_name << "left mouse";
263  break;
264  case SDL_BUTTON_RIGHT:
265  hotkey_name << "right mouse";
266  break;
267  case SDL_BUTTON_MIDDLE:
268  hotkey_name << "middle mouse";
269  break;
270  case SDL_BUTTON_X1:
271  hotkey_name << "mouse back";
272  break;
273  case SDL_BUTTON_X2:
274  hotkey_name << "mouse forward";
275  break;
276  case TOUCH_MOUSE_INDEX:
277  hotkey_name << "touch";
278  break;
279  default:
280  hotkey_name << "mouse " + std::to_string(button_);
281  }
282 
283  if(clicks_ > 1) {
284  hotkey_name << " (clicks: " << clicks_ << ")";
285  }
286 
287  return hotkey_name.str();
288 }
289 
291 {
292  item["mouse"] = 0;
293  if(button_ != 0) {
294  item["button"] = button_;
295  }
296 }
297 
298 void hotkey_keyboard::set_text(const std::string& text)
299 {
300  text_ = text;
301  boost::algorithm::to_lower(text_);
302 }
303 
304 const std::string hotkey_keyboard::get_name_helper() const
305 {
306  return text_;
307 }
308 
309 bool hotkey_keyboard::matches_helper(const SDL_Event& event) const
310 {
311  unsigned mods = sdl::get_mods();
312  const hotkey_command& command = get_hotkey_command(get_command());
313 
314  if((event.type == SDL_EVENT_KEY_DOWN || event.type == SDL_EVENT_KEY_UP)
315  && (mods & SDL_KMOD_CTRL || mods & SDL_KMOD_ALT || mods & SDL_KMOD_GUI || command.toggle || CKey::is_uncomposable(event.key))
316  ) {
317  return event.key.key == keycode_ && mods == mod_;
318  }
319 
320  if(event.type == SDL_EVENT_TEXT_INPUT && !command.toggle) {
321  std::string text = std::string(event.text.text);
322  boost::algorithm::to_lower(text);
323  if(text == ":" || text == "`") {
324  mods = mods & ~SDL_KMOD_SHIFT;
325  }
326  return text_ == text && utf8::size(std::string(event.text.text)) == 1 && mods == mod_;
327  }
328 
329  return false;
330 }
331 
333 {
334  auto other_m = std::dynamic_pointer_cast<hotkey_mouse>(other);
335  if(other_m == nullptr) {
336  return false;
337  }
338 
339  return button_ == other_m->button_;
340 }
341 
343 {
344  if(!text_.empty()) {
345  item["key"] = text_;
346  }
347 }
348 
349 bool has_hotkey_item(const std::string& command)
350 {
351  for(const hotkey_ptr& item : hotkeys_) {
352  if(item->get_command() == command) {
353  return true;
354  }
355  }
356  return false;
357 }
358 
360 {
361  auto other_k = std::dynamic_pointer_cast<hotkey_keyboard>(other);
362  if(other_k == nullptr) {
363  return false;
364  }
365 
366  return text_ == other_k->text_;
367 }
368 
370 {
371  if(item) {
372  // More than one existing hot-key can match, since bindings_equal only checks for overlapping scopes.
373  // For example, a game+editor command bound to 's' matches both a game-only and an editor-only 's'.
374  utils::erase_if(hotkeys_, [&item](const hotkey::hotkey_ptr& hk) { return hk->bindings_equal(item); });
375  hotkeys_.push_back(std::move(item));
376  }
377 }
378 
379 void clear_hotkeys(const std::string& command)
380 {
381  for(hotkey::hotkey_ptr& item : hotkeys_) {
382  if(item->get_command() == command) {
383  if(item->is_default()) {
384  item->disable();
385  } else {
386  item->clear();
387  }
388  }
389  }
390 }
391 
393 {
394  hotkeys_.clear();
395 }
396 
397 const hotkey_ptr get_hotkey(const SDL_Event& event)
398 {
399  for(const hotkey_ptr& item : hotkeys_) {
400  if(item->matches(event)) {
401  return item;
402  }
403  }
404  return std::make_shared<hotkey_void>();
405 }
406 
408 {
409  hotkey_list new_hotkeys;
410  for(const config& hk : cfg.child_range(prefs_list::hotkey)) {
411  if(hotkey_ptr item = load_from_config(hk); !item->null()) {
412  new_hotkeys.push_back(std::move(item));
413  }
414  }
415 
416  default_hotkey_cfg_ = cfg;
417  hotkeys_.swap(new_hotkeys);
418 }
419 
421 {
422  for(const config& hk : cfg.child_range(prefs_list::hotkey)) {
423  if(hotkey_ptr item = load_from_config(hk); !item->null()) {
424  item->unset_default();
425  add_hotkey(item);
426  }
427  }
428 }
429 
431 {
432  hotkeys_.clear();
433 
434  if(!default_hotkey_cfg_.child_range(prefs_list::hotkey).empty()) {
435  load_default_hotkeys(default_hotkey_cfg_);
436  } else {
437  ERR_G << "no default hotkeys set yet; all hotkeys are now unassigned!";
438  }
439 }
440 
442 {
443  return hotkeys_;
444 }
445 
447 {
448  cfg.clear_children(prefs_list::hotkey);
449 
450  for(hotkey_ptr& item : hotkeys_) {
451  if((!item->is_default() && item->active()) || (item->is_default() && item->is_disabled())) {
452  item->save(cfg.add_child(prefs_list::hotkey));
453  }
454  }
455 }
456 
457 std::string get_names(const std::string& id)
458 {
459  // Names are used in places like the hot-key preferences menu
460  std::vector<std::string> names;
461  for(const hotkey::hotkey_ptr& item : hotkeys_) {
462  if(item->get_command() == id && !item->null() && !item->is_disabled()) {
463  names.push_back(item->get_name());
464  }
465  }
466 
467  // These are hard-coded, non-rebindable hotkeys
468  if(id == "quit") {
469  names.push_back("escape");
470  } else if(id == "quit-to-desktop") {
471 #ifdef __APPLE__
472  names.push_back("cmd+q");
473 #else
474  names.push_back("alt+F4");
475 #endif
476  }
477 
478  return boost::algorithm::join(names, ", ");
479 }
480 
481 bool is_hotkeyable_event(const SDL_Event& event)
482 {
483  if(event.type == SDL_EVENT_JOYSTICK_BUTTON_UP || event.type == SDL_EVENT_JOYSTICK_HAT_MOTION || event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
484  return true;
485  }
486 
487  unsigned mods = sdl::get_mods();
488 
489  if(mods & SDL_KMOD_CTRL || mods & SDL_KMOD_ALT || mods & SDL_KMOD_GUI) {
490  return event.type == SDL_EVENT_KEY_UP;
491  } else {
492  return event.type == SDL_EVENT_TEXT_INPUT || event.type == SDL_EVENT_KEY_UP;
493  }
494 }
495 
496 } // namespace hotkey
std::vector< std::string > names
Definition: build_info.cpp:74
static bool is_uncomposable(const SDL_KeyboardEvent &event)
Definition: key.cpp:28
Variant for storing WML attributes.
bool empty() const
Tests for an attribute that either was never set or was set to "".
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
config & add_child(std::string_view key)
Definition: config.cpp:436
child_itors child_range(std::string_view key)
Definition: config.cpp:268
void clear_children(T... keys)
Definition: config.hpp:601
void swap(config &cfg) noexcept
Definition: config.cpp:1280
A class grating read only view to a vector of config objects, viewed as one config with all children ...
bool is_disabled() const
Definition: hotkey_item.hpp:96
virtual void save_helper(config &cfg) const =0
const std::string & get_command() const
Returns the string name of the HOTKEY_COMMAND.
Definition: hotkey_item.hpp:63
std::string get_name() const
Return "name" of hotkey.
Definition: hotkey_item.cpp:51
virtual bool bindings_equal(const hotkey_ptr &other)
Checks whether the hotkey bindings and scope are equal.
Definition: hotkey_item.cpp:86
void save(config &cfg) const
Save the hotkey into the configuration object.
bool active() const
virtual bool bindings_equal_helper(hotkey_ptr other) const =0
This is invoked by hotkey_base::bindings_equal as a helper for the concrete classes.
bool matches(const SDL_Event &event) const
Used to evaluate whether:
virtual const std::string get_name_helper() const =0
This is invoked by hotkey_base::get_name and must be implemented by subclasses.
virtual bool matches_helper(const SDL_Event &event) const =0
This is invoked by hotkey_base::matches as a helper for the concrete classes.
void set_text(const std::string &text)
virtual void save_helper(config &cfg) const
virtual const std::string get_name_helper() const
This is invoked by hotkey_base::get_name and must be implemented by subclasses.
virtual bool bindings_equal_helper(hotkey_ptr other) const
This is invoked by hotkey_base::bindings_equal as a helper for the concrete classes.
virtual bool matches_helper(const SDL_Event &event) const
This is invoked by hotkey_base::matches as a helper for the concrete classes.
virtual const std::string get_name_helper() const
This is invoked by hotkey_base::get_name and must be implemented by subclasses.
virtual void save_helper(config &cfg) const
virtual bool matches_helper(const SDL_Event &event) const
This is invoked by hotkey_base::matches as a helper for the concrete classes.
virtual bool bindings_equal_helper(hotkey_ptr other) const
This is invoked by hotkey_base::bindings_equal as a helper for the concrete classes.
Definitions for the interface to Wesnoth Markup Language (WML).
const config * cfg
#define ERR_G
Definition: hotkey_item.cpp:36
static lg::log_domain log_config("config")
Contains functions for cleanly handling SDL input.
Standard logging facilities (interface).
bool is_touch(const SDL_MouseButtonEvent &event)
Check if this mouse button event is caused by a touch.
Definition: events.cpp:763
Keyboard shortcuts for game actions.
const hotkey_list & get_hotkeys()
Returns the list of hotkeys.
std::string get_names(const std::string &id)
Returns a comma-separated string of hotkey names.
void save_hotkeys(config &cfg)
Save the non-default hotkeys to the config.
std::shared_ptr< class hotkey_base > hotkey_ptr
Definition: hotkey_item.hpp:27
bool has_hotkey_item(const std::string &command)
const hotkey_ptr get_hotkey(const SDL_Event &event)
Iterate through the list of hotkeys and return a hotkey that matches the SDL_Event and the current ke...
void clear_hotkeys(const std::string &command)
Unset the command bindings for all hotkeys matching the command.
void reset_default_hotkeys()
Reset all hotkeys to the defaults.
hotkey_ptr load_from_config(const config &cfg)
Create and instantiate a hotkey from a config element.
hotkey_ptr create_hotkey(const std::string &id, const SDL_Event &event)
Create a new hotkey item for a command from an SDL_Event.
std::bitset< SCOPE_COUNT > hk_scopes
const hotkey_command & get_hotkey_command(std::string_view command)
Returns the hotkey_command with the given id.
std::vector< hotkey::hotkey_ptr > hotkey_list
Definition: hotkey_item.hpp:31
void load_custom_hotkeys(const game_config_view &cfg)
Registers all hotkeys present in this config, overwriting any matching default hotkeys.
bool is_scope_active(scope s)
scope
Available hotkey scopes.
void load_default_hotkeys(const game_config_view &cfg)
Registers all hotkeys present in this config.
void add_hotkey(hotkey_ptr item)
Add a hotkey to the list of hotkeys.
bool is_hotkeyable_event(const SDL_Event &event)
unsigned get_mods()
Returns a bitmask of active modifier keys (ctrl, shift, alt, gui).
Definition: input.cpp:57
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:81
std::string join(const Range &v, const std::string &s=",")
Generates a new string joining container items in a list.
void erase_if(Container &container, const Predicate &predicate)
Convenience wrapper for using std::remove_if on a container.
Definition: general.hpp:108
std::string to_string(const Range &range, const Func &op)
This file contains object "key", which is used to store information about keys while annotation parsi...
Stores all information related to functions that can be bound to hotkeys.
bool toggle
Toggle hotkeys have some restrictions on what can be bound to them.
hk_scopes scope
The visibility scope of the command.