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  auto iter = std::find_if(hotkeys_.begin(), hotkeys_.end(),
373  [&item](const hotkey::hotkey_ptr& hk) { return hk->bindings_equal(item); });
374 
375  if(iter != hotkeys_.end()) {
376  iter->swap(item);
377  } else {
378  hotkeys_.push_back(std::move(item));
379  }
380  }
381 }
382 
383 void clear_hotkeys(const std::string& command)
384 {
385  for(hotkey::hotkey_ptr& item : hotkeys_) {
386  if(item->get_command() == command) {
387  if(item->is_default()) {
388  item->disable();
389  } else {
390  item->clear();
391  }
392  }
393  }
394 }
395 
397 {
398  hotkeys_.clear();
399 }
400 
401 const hotkey_ptr get_hotkey(const SDL_Event& event)
402 {
403  for(const hotkey_ptr& item : hotkeys_) {
404  if(item->matches(event)) {
405  return item;
406  }
407  }
408  return std::make_shared<hotkey_void>();
409 }
410 
412 {
413  hotkey_list new_hotkeys;
414  for(const config& hk : cfg.child_range(prefs_list::hotkey)) {
415  if(hotkey_ptr item = load_from_config(hk); !item->null()) {
416  new_hotkeys.push_back(std::move(item));
417  }
418  }
419 
420  default_hotkey_cfg_ = cfg;
421  hotkeys_.swap(new_hotkeys);
422 }
423 
425 {
426  for(const config& hk : cfg.child_range(prefs_list::hotkey)) {
427  if(hotkey_ptr item = load_from_config(hk); !item->null()) {
428  item->unset_default();
429  add_hotkey(item);
430  }
431  }
432 }
433 
435 {
436  hotkeys_.clear();
437 
438  if(!default_hotkey_cfg_.child_range(prefs_list::hotkey).empty()) {
439  load_default_hotkeys(default_hotkey_cfg_);
440  } else {
441  ERR_G << "no default hotkeys set yet; all hotkeys are now unassigned!";
442  }
443 }
444 
446 {
447  return hotkeys_;
448 }
449 
451 {
452  cfg.clear_children(prefs_list::hotkey);
453 
454  for(hotkey_ptr& item : hotkeys_) {
455  if((!item->is_default() && item->active()) || (item->is_default() && item->is_disabled())) {
456  item->save(cfg.add_child(prefs_list::hotkey));
457  }
458  }
459 }
460 
461 std::string get_names(const std::string& id)
462 {
463  // Names are used in places like the hot-key preferences menu
464  std::vector<std::string> names;
465  for(const hotkey::hotkey_ptr& item : hotkeys_) {
466  if(item->get_command() == id && !item->null() && !item->is_disabled()) {
467  names.push_back(item->get_name());
468  }
469  }
470 
471  // These are hard-coded, non-rebindable hotkeys
472  if(id == "quit") {
473  names.push_back("escape");
474  } else if(id == "quit-to-desktop") {
475 #ifdef __APPLE__
476  names.push_back("cmd+q");
477 #else
478  names.push_back("alt+F4");
479 #endif
480  }
481 
482  return boost::algorithm::join(names, ", ");
483 }
484 
485 bool is_hotkeyable_event(const SDL_Event& event)
486 {
487  if(event.type == SDL_EVENT_JOYSTICK_BUTTON_UP || event.type == SDL_EVENT_JOYSTICK_HAT_MOTION || event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
488  return true;
489  }
490 
491  unsigned mods = sdl::get_mods();
492 
493  if(mods & SDL_KMOD_CTRL || mods & SDL_KMOD_ALT || mods & SDL_KMOD_GUI) {
494  return event.type == SDL_EVENT_KEY_UP;
495  } else {
496  return event.type == SDL_EVENT_TEXT_INPUT || event.type == SDL_EVENT_KEY_UP;
497  }
498 }
499 
500 } // 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
auto * find_if(Container &container, const Predicate &predicate)
Convenience wrapper for using find_if on a container without needing to comare to end()
Definition: general.hpp:152
std::string join(const Range &v, const std::string &s=",")
Generates a new string joining container items in a list.
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.