1 /*
2   Hotkeys.cpp: hotkeys (keyboard shortcuts)
3   Copyright 2013 by Vincent Fourmond
4 
5   This program is free software; you can redistribute it and/or modify
6   it under the terms of the GNU General Public License as published by
7   the Free Software Foundation; either version 2 of the License, or
8   (at your option) any later version.
9 
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14 
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 #include "Hotkeys.h"
20 
21 #include "../util/OptionsDB.h"
22 #include "../util/Logger.h"
23 
24 
25 /// A helper class that stores both a connection and the
26 /// conditions in which it should be on.
27 struct HotkeyManager::ConditionalConnection {
28     /// Block or unblocks the connection based on condition.
UpdateConnectionHotkeyManager::ConditionalConnection29     void UpdateConnection() {
30         if (connection.connected()) {
31             if (!condition || condition())
32                 blocker.unblock();
33             else
34                 blocker.block();
35         }
36     };
37 
ConditionalConnectionHotkeyManager::ConditionalConnection38     ConditionalConnection(const boost::signals2::connection& conn, std::function<bool()> cond) :
39         condition(cond),
40         connection(conn),
41         blocker(connection)
42     {
43         blocker.unblock();
44     }
45 
46     /// The condition. If null, always on.
47     std::function<bool()> condition;
48 
49     boost::signals2::connection connection;
50     boost::signals2::shared_connection_block blocker;
51 };
52 
53 /////////////////////////////////////////////////////////
54 // Hotkey
55 /////////////////////////////////////////////////////////
56 std::map<std::string, Hotkey>* Hotkey::s_hotkeys = nullptr;
57 
AddHotkey(const std::string & name,const std::string & description,GG::Key key,GG::Flags<GG::ModKey> mod)58 void Hotkey::AddHotkey(const std::string& name, const std::string& description, GG::Key key, GG::Flags<GG::ModKey> mod) {
59     if (!s_hotkeys)
60         s_hotkeys = new std::map<std::string, Hotkey>;
61     s_hotkeys->insert({name, Hotkey(name, description, key, mod)});
62 }
63 
HotkeyToString(GG::Key key,GG::Flags<GG::ModKey> mod)64 std::string Hotkey::HotkeyToString(GG::Key key, GG::Flags<GG::ModKey> mod) {
65     std::ostringstream s;
66     if (mod != GG::MOD_KEY_NONE) {
67         s << mod;
68         s << "+";
69     }
70     if (key > GG::GGK_NONE) {
71         s << key;
72     }
73     return s.str();
74 }
75 
DefinedHotkeys()76 std::set<std::string> Hotkey::DefinedHotkeys() {
77     std::set<std::string> retval;
78     if (s_hotkeys) {
79         for (const auto& entry : *s_hotkeys)
80         { retval.insert(entry.first); }
81     }
82     return retval;
83 }
84 
ToString() const85 std::string Hotkey::ToString() const
86 { return HotkeyToString(m_key, m_mod_keys); }
87 
HotkeyFromString(const std::string & str)88 std::pair<GG::Key, GG::Flags<GG::ModKey>> Hotkey::HotkeyFromString(const std::string& str) {
89     if (str.empty())
90         return {GG::GGK_NONE, GG::Flags<GG::ModKey>()};
91 
92     // Strip whitespace
93     std::string copy = str;
94     copy = std::string(copy.begin(), std::remove_if(copy.begin(), copy.end(), isspace));
95 
96     size_t plus = copy.find('+');
97     bool has_modifier = plus != std::string::npos;
98 
99     GG::Flags<GG::ModKey> mod = GG::MOD_KEY_NONE;
100     if (has_modifier) {
101         // We have a modifier. Things get a little complex, since we need
102         // to handle the |-separated flags:
103         std::string m = copy.substr(0, plus);
104 
105         size_t found = 0;
106         size_t prev = 0;
107 
108         try {
109             while (true) {
110                 found = m.find('|', prev);
111                 std::string sub = m.substr(prev, found-prev);
112                 GG::ModKey cm = GG::FlagSpec<GG::ModKey>::instance().FromString(sub);
113                 mod |= cm;
114                 if (found == std::string::npos)
115                     break;
116                 prev = found + 1;
117             }
118         } catch (...) {
119             ErrorLogger() << "Unable make flag from string: " << str;
120             return {GG::GGK_NONE, GG::Flags<GG::ModKey>()};
121         }
122     }
123 
124     std::string v = has_modifier ? copy.substr(plus+1) : copy;
125     std::istringstream s(v);
126     GG::Key key;
127     s >> key;
128     return std::pair<GG::Key, GG::Flags<GG::ModKey>>(key, mod);
129 }
130 
SetFromString(const std::string & str)131 void Hotkey::SetFromString(const std::string& str) {
132     std::pair<GG::Key, GG::Flags<GG::ModKey>> km = HotkeyFromString(str);
133     m_key = km.first;
134     m_mod_keys = km.second;
135 }
136 
AddOptions(OptionsDB & db)137 void Hotkey::AddOptions(OptionsDB& db) {
138     if (!s_hotkeys)
139         return;
140     for (const auto& entry : *s_hotkeys) {
141         const Hotkey& hotkey = entry.second;
142         std::string n = hotkey.m_name + ".hotkey";
143         db.Add(n, hotkey.GetDescription(), hotkey.ToString());
144     }
145 }
146 
ReplaceInString(std::string & str,const std::string & what,const std::string & replacement)147 static void ReplaceInString(std::string& str, const std::string& what,
148                             const std::string& replacement)
149 {
150     size_t lst = 0;
151     size_t l1 = what.size();
152     size_t l2 = replacement.size();
153     if (l1 == 0 && l2 == 0)
154         return;                 // Nothing to do
155     do {
156         size_t t = str.find(what, lst);
157         if(t == std::string::npos)
158             return;
159         str.replace(t, l1, replacement);
160         t += l2;
161     } while(true);
162 }
163 
PrettyPrint(GG::Key key,GG::Flags<GG::ModKey> mod)164 std::string Hotkey::PrettyPrint(GG::Key key, GG::Flags<GG::ModKey> mod) {
165     std::string retval;
166     if (mod & GG::MOD_KEY_CTRL)
167         retval += "CTRL+";
168     if (mod & GG::MOD_KEY_ALT)
169         retval += "ALT+";
170     if (mod & GG::MOD_KEY_SHIFT)
171         retval += "SHIFT+";
172     if (mod & GG::MOD_KEY_META)
173         retval += "META+";
174 
175     std::ostringstream key_stream;
176     key_stream << key;
177     std::string key_string = key_stream.str();
178     ReplaceInString(key_string, "GGK_", "");
179 
180     retval += key_string;
181     return retval;
182 }
183 
PrettyPrint() const184 std::string Hotkey::PrettyPrint() const
185 { return PrettyPrint(m_key, m_mod_keys); }
186 
ReadFromOptions(OptionsDB & db)187 void Hotkey::ReadFromOptions(OptionsDB& db) {
188     for (auto& entry : *s_hotkeys) {
189         Hotkey& hotkey = entry.second;
190 
191         std::string options_db_name = hotkey.m_name + ".hotkey";
192         if (!db.OptionExists(options_db_name)) {
193             ErrorLogger() << "Hotkey::ReadFromOptions : no option for " << options_db_name;
194             continue;
195         }
196         std::string option_string = db.Get<std::string>(options_db_name);
197 
198         std::pair<GG::Key, GG::Flags<GG::ModKey>> key_modkey_pair = {GG::GGK_NONE, GG::MOD_KEY_NONE};
199         try {
200             key_modkey_pair = HotkeyFromString(option_string);
201         } catch (...) {
202             ErrorLogger() << "Failed to read hotkey from string: " << option_string;
203             continue;
204         }
205 
206         if (key_modkey_pair.first == GG::GGK_NONE)
207             continue;
208 
209         if (!IsTypingSafe(key_modkey_pair.first, key_modkey_pair.second)) {
210             DebugLogger() << "Hotkey::ReadFromOptions : Typing-unsafe key spec: '"
211                           << option_string << "' for hotkey " << hotkey.m_name;
212         }
213 
214         hotkey.m_key = key_modkey_pair.first;
215         hotkey.m_mod_keys = key_modkey_pair.second;
216 
217         TraceLogger() << "Added hotkey '" << hotkey.m_key << "' with modifiers '"
218                       << hotkey.m_mod_keys << "' for hotkey '" << hotkey.m_name << "'";
219     }
220 }
221 
Hotkey(const std::string & name,const std::string & description,GG::Key key,GG::Flags<GG::ModKey> mod)222 Hotkey::Hotkey(const std::string& name, const std::string& description,
223                GG::Key key, GG::Flags<GG::ModKey> mod) :
224     m_name(name),
225     m_description(description),
226     m_key(key),
227     m_key_default(key),
228     m_mod_keys(mod),
229     m_mod_keys_default(mod)
230 {}
231 
NamedHotkey(const std::string & name)232 const Hotkey& Hotkey::NamedHotkey(const std::string& name)
233 { return PrivateNamedHotkey(name); }
234 
GetDescription() const235 std::string Hotkey::GetDescription() const
236 { return m_description; }
237 
PrivateNamedHotkey(const std::string & name)238 Hotkey& Hotkey::PrivateNamedHotkey(const std::string& name) {
239     std::string error_msg = "Hotkey::PrivateNamedHotkey error: no hotkey named: " + name;
240 
241     if (!s_hotkeys)
242         throw std::runtime_error("Hotkey::PrivateNamedHotkey error: couldn't get hotkeys container.");
243 
244     auto i = s_hotkeys->find(name);
245     if (i == s_hotkeys->end())
246         throw std::invalid_argument(error_msg.c_str());
247 
248     return i->second;
249 }
250 
IsTypingSafe(GG::Key key,GG::Flags<GG::ModKey> mod)251 bool Hotkey::IsTypingSafe(GG::Key key, GG::Flags<GG::ModKey> mod) {
252     if (GG::GGK_INSERT <= key && GG::GGK_PAGEUP >= key)
253         return false;
254     if (GG::GGK_END <= key && GG::GGK_UP >= key)
255         return false;
256     if (mod & (GG::MOD_KEY_CTRL | GG::MOD_KEY_ALT | GG::MOD_KEY_META))
257         return true;
258     if (key >= GG::GGK_F1 && key <= GG::GGK_F12)
259         return true;
260     if (key >= GG::GGK_F13 && key <= GG::GGK_F24)
261         return true;
262     if (key == GG::GGK_TAB || key == GG::GGK_ESCAPE || key == GG::GGK_NONE)
263         return true;
264     return false;
265 }
266 
IsTypingSafe() const267 bool Hotkey::IsTypingSafe() const
268 { return IsTypingSafe(m_key, m_mod_keys); }
269 
IsDefault() const270 bool Hotkey::IsDefault() const
271 { return m_key == m_key_default && m_mod_keys == m_mod_keys_default; }
272 
SetHotkey(const Hotkey & hotkey,GG::Key key,GG::Flags<GG::ModKey> mod)273 void Hotkey::SetHotkey(const Hotkey& hotkey, GG::Key key, GG::Flags<GG::ModKey> mod) {
274     Hotkey& hk = PrivateNamedHotkey(hotkey.m_name);
275     hk.m_key = key;
276     hk.m_mod_keys = GG::MassagedAccelModKeys(mod);
277 
278     GetOptionsDB().Set<std::string>(hk.m_name + ".hotkey", hk.ToString());
279 }
280 
ResetHotkey(const Hotkey & old_hotkey)281 void Hotkey::ResetHotkey(const Hotkey& old_hotkey) {
282     Hotkey& hk = PrivateNamedHotkey(old_hotkey.m_name);
283     hk.m_key = hk.m_key_default;
284     hk.m_mod_keys = hk.m_mod_keys_default;
285     GetOptionsDB().Set<std::string>(hk.m_name + ".hotkey", hk.ToString());
286 }
287 
ClearHotkey(const Hotkey & old_hotkey)288 void Hotkey::ClearHotkey(const Hotkey& old_hotkey)
289 { Hotkey::SetHotkey(old_hotkey, GG::GGK_NONE, GG::Flags<GG::ModKey>()); }
290 
291 //////////////////////////////////////////////////////////////////////
292 // InvisibleWindowCondition
293 //////////////////////////////////////////////////////////////////////
InvisibleWindowCondition(std::initializer_list<const GG::Wnd * > bl)294 InvisibleWindowCondition::InvisibleWindowCondition(std::initializer_list<const GG::Wnd*> bl) :
295     m_blacklist(bl)
296 {}
297 
operator ()() const298 bool InvisibleWindowCondition::operator()() const {
299     for (const auto& wnd : m_blacklist) {
300         if (wnd->Visible())
301             return false;
302     }
303     return true;
304 }
305 
306 
307 //////////////////////////////////////////////////////////////////////
308 // OrCondition
309 //////////////////////////////////////////////////////////////////////
OrCondition(std::initializer_list<std::function<bool ()>> conditions)310 OrCondition::OrCondition(std::initializer_list<std::function<bool()>> conditions) :
311     m_conditions(conditions)
312 {}
313 
operator ()() const314 bool OrCondition::operator()() const {
315     for (auto cond : m_conditions) {
316         if (cond())
317             return true;
318     }
319     return false;
320 }
321 
322 
323 //////////////////////////////////////////////////////////////////////
324 // AndCondition
325 //////////////////////////////////////////////////////////////////////
AndCondition(std::initializer_list<std::function<bool ()>> conditions)326 AndCondition::AndCondition(std::initializer_list<std::function<bool()>> conditions) :
327     m_conditions(conditions)
328 {}
329 
operator ()() const330 bool AndCondition::operator()() const {
331     for (auto cond : m_conditions) {
332         if (!cond())
333             return false;
334     }
335     return true;
336 }
337 
338 
339 //////////////////////////////////////////////////////////////////////
340 // HotkeyManager
341 //////////////////////////////////////////////////////////////////////
342 HotkeyManager* HotkeyManager::s_singleton = nullptr;
343 
HotkeyManager()344 HotkeyManager::HotkeyManager()
345 {}
346 
~HotkeyManager()347 HotkeyManager::~HotkeyManager()
348 {}
349 
GetManager()350 HotkeyManager* HotkeyManager::GetManager() {
351     if (!s_singleton)
352         s_singleton = new HotkeyManager;
353     return s_singleton;
354 }
355 
RebuildShortcuts()356 void HotkeyManager::RebuildShortcuts() {
357     for (const auto& con : m_internal_connections)
358     { con.disconnect(); }
359     m_internal_connections.clear();
360 
361     /// @todo Disable the shortcuts that we've enabled so far ? Is it
362     /// really necessary ? An unconnected signal should simply be
363     /// ignored.
364 
365     // Now, build up again all the shortcuts
366     GG::GUI* gui = GG::GUI::GetGUI();
367     for (auto& entry : m_connections) {
368         const Hotkey& hk = Hotkey::NamedHotkey(entry.first);
369 
370         gui->SetAccelerator(hk.m_key, hk.m_mod_keys);
371 
372         m_internal_connections.insert(gui->AcceleratorSignal(hk.m_key, hk.m_mod_keys).connect(
373             boost::bind(&HotkeyManager::ProcessNamedShortcut, this, hk.m_name, hk.m_key, hk.m_mod_keys)));
374     }
375 }
376 
AddConditionalConnection(const std::string & name,const boost::signals2::connection & conn,std::function<bool ()> cond)377 void HotkeyManager::AddConditionalConnection(const std::string& name,
378                                              const boost::signals2::connection& conn,
379                                              std::function<bool()> cond)
380 {
381     ConditionalConnectionList& list = m_connections[name];
382     list.push_back(ConditionalConnection(conn, cond));
383 }
384 
NamedSignal(const std::string & name)385 GG::GUI::AcceleratorSignalType& HotkeyManager::NamedSignal(const std::string& name) {
386     /// Unsure why GG::AcceleratorSignal implementation uses shared
387     /// pointers. Maybe I should, too ?
388     auto& sig = m_signals[name];
389     if (!sig)
390         sig = new GG::GUI::AcceleratorSignalType;
391     return *sig;
392 }
393 
ProcessNamedShortcut(const std::string & name,GG::Key key,GG::Flags<GG::ModKey> mod)394 bool HotkeyManager::ProcessNamedShortcut(const std::string& name, GG::Key key,
395                                          GG::Flags<GG::ModKey> mod)
396 {
397     // reject unsafe-for-typing key combinations while typing
398     if (GG::GUI::GetGUI()->FocusWndAcceptsTypingInput() && !Hotkey::IsTypingSafe(key, mod))
399         return false;
400 
401     // First update the connection state according to the current status.
402     ConditionalConnectionList& conds = m_connections[name];
403     for (auto i = conds.begin(); i != conds.end(); ++i) {
404         i->UpdateConnection();
405         if (!i->connection.connected())
406             i = conds.erase(i);
407     }
408 
409     // Then, return the value of the signal !
410     GG::GUI::AcceleratorSignalType* sig = m_signals[name];
411     return (*sig)();
412 }
413