The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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
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
35static 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
41namespace hotkey
42{
43namespace
44{
45hotkey_list hotkeys_;
46game_config_view default_hotkey_cfg_;
47
48const int TOUCH_MOUSE_INDEX = 255;
49}; // namespace
50
51std::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
103bool hotkey_base::matches(const SDL_Event& event) const
104{
106 return false;
107 }
108
109 return matches_helper(event);
110}
111
112void 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
125hotkey_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 = 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 = 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 = 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 = 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 = 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
233bool 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
257const 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
298void hotkey_keyboard::set_text(const std::string& text)
299{
300 text_ = text;
301 boost::algorithm::to_lower(text_);
302}
303
304const std::string hotkey_keyboard::get_name_helper() const
305{
306 return text_;
307}
308
309bool hotkey_keyboard::matches_helper(const SDL_Event& event) const
310{
311 unsigned mods = sdl::get_mods();
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
349bool 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
379void 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
397const 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
457std::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
481bool 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
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 ...
const std::string & get_command() const
Returns the string name of the HOTKEY_COMMAND.
bool is_disabled() const
virtual void save_helper(config &cfg) const =0
std::string get_name() const
Return "name" of hotkey.
virtual bool bindings_equal(const hotkey_ptr &other)
Checks whether the hotkey bindings and scope are equal.
void save(config &cfg) const
Save the hotkey into the configuration object.
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 gettext.cpp:39
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.
void clear_hotkeys()
Unset the bindings for all 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
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 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
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
void erase_if(Container &container, const Predicate &predicate)
Convenience wrapper for using std::remove_if on a container.
Definition general.hpp:108
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.