/* Hotkeys.cpp: hotkeys (keyboard shortcuts) Copyright 2013 by Vincent Fourmond This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "Hotkeys.h" #include "../util/OptionsDB.h" #include "../util/Logger.h" /// A helper class that stores both a connection and the /// conditions in which it should be on. struct HotkeyManager::ConditionalConnection { /// Block or unblocks the connection based on condition. void UpdateConnection() { if (connection.connected()) { if (!condition || condition()) blocker.unblock(); else blocker.block(); } }; ConditionalConnection(const boost::signals2::connection& conn, std::function cond) : condition(cond), connection(conn), blocker(connection) { blocker.unblock(); } /// The condition. If null, always on. std::function condition; boost::signals2::connection connection; boost::signals2::shared_connection_block blocker; }; ///////////////////////////////////////////////////////// // Hotkey ///////////////////////////////////////////////////////// std::map* Hotkey::s_hotkeys = nullptr; void Hotkey::AddHotkey(const std::string& name, const std::string& description, GG::Key key, GG::Flags mod) { if (!s_hotkeys) s_hotkeys = new std::map; s_hotkeys->insert({name, Hotkey(name, description, key, mod)}); } std::string Hotkey::HotkeyToString(GG::Key key, GG::Flags mod) { std::ostringstream s; if (mod != GG::MOD_KEY_NONE) { s << mod; s << "+"; } if (key > GG::GGK_NONE) { s << key; } return s.str(); } std::set Hotkey::DefinedHotkeys() { std::set retval; if (s_hotkeys) { for (const auto& entry : *s_hotkeys) { retval.insert(entry.first); } } return retval; } std::string Hotkey::ToString() const { return HotkeyToString(m_key, m_mod_keys); } std::pair> Hotkey::HotkeyFromString(const std::string& str) { if (str.empty()) return {GG::GGK_NONE, GG::Flags()}; // Strip whitespace std::string copy = str; copy = std::string(copy.begin(), std::remove_if(copy.begin(), copy.end(), isspace)); size_t plus = copy.find('+'); bool has_modifier = plus != std::string::npos; GG::Flags mod = GG::MOD_KEY_NONE; if (has_modifier) { // We have a modifier. Things get a little complex, since we need // to handle the |-separated flags: std::string m = copy.substr(0, plus); size_t found = 0; size_t prev = 0; try { while (true) { found = m.find('|', prev); std::string sub = m.substr(prev, found-prev); GG::ModKey cm = GG::FlagSpec::instance().FromString(sub); mod |= cm; if (found == std::string::npos) break; prev = found + 1; } } catch (...) { ErrorLogger() << "Unable make flag from string: " << str; return {GG::GGK_NONE, GG::Flags()}; } } std::string v = has_modifier ? copy.substr(plus+1) : copy; std::istringstream s(v); GG::Key key; s >> key; return std::pair>(key, mod); } void Hotkey::SetFromString(const std::string& str) { std::pair> km = HotkeyFromString(str); m_key = km.first; m_mod_keys = km.second; } void Hotkey::AddOptions(OptionsDB& db) { if (!s_hotkeys) return; for (const auto& entry : *s_hotkeys) { const Hotkey& hotkey = entry.second; std::string n = hotkey.m_name + ".hotkey"; db.Add(n, hotkey.GetDescription(), hotkey.ToString()); } } static void ReplaceInString(std::string& str, const std::string& what, const std::string& replacement) { size_t lst = 0; size_t l1 = what.size(); size_t l2 = replacement.size(); if (l1 == 0 && l2 == 0) return; // Nothing to do do { size_t t = str.find(what, lst); if(t == std::string::npos) return; str.replace(t, l1, replacement); t += l2; } while(true); } std::string Hotkey::PrettyPrint(GG::Key key, GG::Flags mod) { std::string retval; if (mod & GG::MOD_KEY_CTRL) retval += "CTRL+"; if (mod & GG::MOD_KEY_ALT) retval += "ALT+"; if (mod & GG::MOD_KEY_SHIFT) retval += "SHIFT+"; if (mod & GG::MOD_KEY_META) retval += "META+"; std::ostringstream key_stream; key_stream << key; std::string key_string = key_stream.str(); ReplaceInString(key_string, "GGK_", ""); retval += key_string; return retval; } std::string Hotkey::PrettyPrint() const { return PrettyPrint(m_key, m_mod_keys); } void Hotkey::ReadFromOptions(OptionsDB& db) { for (auto& entry : *s_hotkeys) { Hotkey& hotkey = entry.second; std::string options_db_name = hotkey.m_name + ".hotkey"; if (!db.OptionExists(options_db_name)) { ErrorLogger() << "Hotkey::ReadFromOptions : no option for " << options_db_name; continue; } std::string option_string = db.Get(options_db_name); std::pair> key_modkey_pair = {GG::GGK_NONE, GG::MOD_KEY_NONE}; try { key_modkey_pair = HotkeyFromString(option_string); } catch (...) { ErrorLogger() << "Failed to read hotkey from string: " << option_string; continue; } if (key_modkey_pair.first == GG::GGK_NONE) continue; if (!IsTypingSafe(key_modkey_pair.first, key_modkey_pair.second)) { DebugLogger() << "Hotkey::ReadFromOptions : Typing-unsafe key spec: '" << option_string << "' for hotkey " << hotkey.m_name; } hotkey.m_key = key_modkey_pair.first; hotkey.m_mod_keys = key_modkey_pair.second; TraceLogger() << "Added hotkey '" << hotkey.m_key << "' with modifiers '" << hotkey.m_mod_keys << "' for hotkey '" << hotkey.m_name << "'"; } } Hotkey::Hotkey(const std::string& name, const std::string& description, GG::Key key, GG::Flags mod) : m_name(name), m_description(description), m_key(key), m_key_default(key), m_mod_keys(mod), m_mod_keys_default(mod) {} const Hotkey& Hotkey::NamedHotkey(const std::string& name) { return PrivateNamedHotkey(name); } std::string Hotkey::GetDescription() const { return m_description; } Hotkey& Hotkey::PrivateNamedHotkey(const std::string& name) { std::string error_msg = "Hotkey::PrivateNamedHotkey error: no hotkey named: " + name; if (!s_hotkeys) throw std::runtime_error("Hotkey::PrivateNamedHotkey error: couldn't get hotkeys container."); auto i = s_hotkeys->find(name); if (i == s_hotkeys->end()) throw std::invalid_argument(error_msg.c_str()); return i->second; } bool Hotkey::IsTypingSafe(GG::Key key, GG::Flags mod) { if (GG::GGK_INSERT <= key && GG::GGK_PAGEUP >= key) return false; if (GG::GGK_END <= key && GG::GGK_UP >= key) return false; if (mod & (GG::MOD_KEY_CTRL | GG::MOD_KEY_ALT | GG::MOD_KEY_META)) return true; if (key >= GG::GGK_F1 && key <= GG::GGK_F12) return true; if (key >= GG::GGK_F13 && key <= GG::GGK_F24) return true; if (key == GG::GGK_TAB || key == GG::GGK_ESCAPE || key == GG::GGK_NONE) return true; return false; } bool Hotkey::IsTypingSafe() const { return IsTypingSafe(m_key, m_mod_keys); } bool Hotkey::IsDefault() const { return m_key == m_key_default && m_mod_keys == m_mod_keys_default; } void Hotkey::SetHotkey(const Hotkey& hotkey, GG::Key key, GG::Flags mod) { Hotkey& hk = PrivateNamedHotkey(hotkey.m_name); hk.m_key = key; hk.m_mod_keys = GG::MassagedAccelModKeys(mod); GetOptionsDB().Set(hk.m_name + ".hotkey", hk.ToString()); } void Hotkey::ResetHotkey(const Hotkey& old_hotkey) { Hotkey& hk = PrivateNamedHotkey(old_hotkey.m_name); hk.m_key = hk.m_key_default; hk.m_mod_keys = hk.m_mod_keys_default; GetOptionsDB().Set(hk.m_name + ".hotkey", hk.ToString()); } void Hotkey::ClearHotkey(const Hotkey& old_hotkey) { Hotkey::SetHotkey(old_hotkey, GG::GGK_NONE, GG::Flags()); } ////////////////////////////////////////////////////////////////////// // InvisibleWindowCondition ////////////////////////////////////////////////////////////////////// InvisibleWindowCondition::InvisibleWindowCondition(std::initializer_list bl) : m_blacklist(bl) {} bool InvisibleWindowCondition::operator()() const { for (const auto& wnd : m_blacklist) { if (wnd->Visible()) return false; } return true; } ////////////////////////////////////////////////////////////////////// // OrCondition ////////////////////////////////////////////////////////////////////// OrCondition::OrCondition(std::initializer_list> conditions) : m_conditions(conditions) {} bool OrCondition::operator()() const { for (auto cond : m_conditions) { if (cond()) return true; } return false; } ////////////////////////////////////////////////////////////////////// // AndCondition ////////////////////////////////////////////////////////////////////// AndCondition::AndCondition(std::initializer_list> conditions) : m_conditions(conditions) {} bool AndCondition::operator()() const { for (auto cond : m_conditions) { if (!cond()) return false; } return true; } ////////////////////////////////////////////////////////////////////// // HotkeyManager ////////////////////////////////////////////////////////////////////// HotkeyManager* HotkeyManager::s_singleton = nullptr; HotkeyManager::HotkeyManager() {} HotkeyManager::~HotkeyManager() {} HotkeyManager* HotkeyManager::GetManager() { if (!s_singleton) s_singleton = new HotkeyManager; return s_singleton; } void HotkeyManager::RebuildShortcuts() { for (const auto& con : m_internal_connections) { con.disconnect(); } m_internal_connections.clear(); /// @todo Disable the shortcuts that we've enabled so far ? Is it /// really necessary ? An unconnected signal should simply be /// ignored. // Now, build up again all the shortcuts GG::GUI* gui = GG::GUI::GetGUI(); for (auto& entry : m_connections) { const Hotkey& hk = Hotkey::NamedHotkey(entry.first); gui->SetAccelerator(hk.m_key, hk.m_mod_keys); m_internal_connections.insert(gui->AcceleratorSignal(hk.m_key, hk.m_mod_keys).connect( boost::bind(&HotkeyManager::ProcessNamedShortcut, this, hk.m_name, hk.m_key, hk.m_mod_keys))); } } void HotkeyManager::AddConditionalConnection(const std::string& name, const boost::signals2::connection& conn, std::function cond) { ConditionalConnectionList& list = m_connections[name]; list.push_back(ConditionalConnection(conn, cond)); } GG::GUI::AcceleratorSignalType& HotkeyManager::NamedSignal(const std::string& name) { /// Unsure why GG::AcceleratorSignal implementation uses shared /// pointers. Maybe I should, too ? auto& sig = m_signals[name]; if (!sig) sig = new GG::GUI::AcceleratorSignalType; return *sig; } bool HotkeyManager::ProcessNamedShortcut(const std::string& name, GG::Key key, GG::Flags mod) { // reject unsafe-for-typing key combinations while typing if (GG::GUI::GetGUI()->FocusWndAcceptsTypingInput() && !Hotkey::IsTypingSafe(key, mod)) return false; // First update the connection state according to the current status. ConditionalConnectionList& conds = m_connections[name]; for (auto i = conds.begin(); i != conds.end(); ++i) { i->UpdateConnection(); if (!i->connection.connected()) i = conds.erase(i); } // Then, return the value of the signal ! GG::GUI::AcceleratorSignalType* sig = m_signals[name]; return (*sig)(); }