Add support for modifying hotkeys while the program is running

Originally committed to SVN as r5793.
This commit is contained in:
Thomas Goyne 2011-10-28 20:40:32 +00:00
parent cba0b1edb8
commit 6c995e7780
6 changed files with 124 additions and 96 deletions

View File

@ -43,9 +43,9 @@
namespace agi { namespace agi {
namespace hotkey { namespace hotkey {
Hotkey *hotkey;
std::string Combo::Str() const { std::string Combo::Str() const {
if (key_map.empty()) return "";
std::string str(key_map[0]); std::string str(key_map[0]);
str.reserve(str.size() + (key_map.size() - 1) * 2); str.reserve(str.size() + (key_map.size() - 1) * 2);
for (unsigned int i=1; i < key_map.size(); i++) { for (unsigned int i=1; i < key_map.size(); i++) {
@ -63,10 +63,6 @@ void Hotkey::ComboInsert(Combo const& combo) {
cmd_map.insert(std::make_pair(combo.CmdName(), combo)); cmd_map.insert(std::make_pair(combo.CmdName(), combo));
} }
Hotkey::~Hotkey() {
Flush();
}
Hotkey::Hotkey(const std::string &file, const std::string &default_config) Hotkey::Hotkey(const std::string &file, const std::string &default_config)
: config_file(file) : config_file(file)
{ {
@ -75,26 +71,23 @@ Hotkey::Hotkey(const std::string &file, const std::string &default_config)
json::UnknownElement hotkey_root = agi::json_util::file(config_file, default_config); json::UnknownElement hotkey_root = agi::json_util::file(config_file, default_config);
json::Object object = hotkey_root; json::Object object = hotkey_root;
for (json::Object::const_iterator index(object.begin()); index != object.end(); index++) { for (json::Object::const_iterator index(object.begin()); index != object.end(); ++index)
BuildHotkey(index->first, index->second); BuildHotkey(index->first, index->second);
}
} }
void Hotkey::BuildHotkey(std::string const& context, const json::Object& object) { void Hotkey::BuildHotkey(std::string const& context, const json::Object& object) {
for (json::Object::const_iterator index(object.begin()); index != object.end(); index++) { for (json::Object::const_iterator index(object.begin()); index != object.end(); ++index) {
const json::Array& array = index->second; const json::Array& array = index->second;
for (json::Array::const_iterator arr_index(array.begin()); arr_index != array.end(); arr_index++) { for (json::Array::const_iterator arr_index(array.begin()); arr_index != array.end(); ++arr_index) {
Combo combo(context, index->first);
const json::Array& arr_mod = (*arr_index)["modifiers"]; const json::Array& arr_mod = (*arr_index)["modifiers"];
for (json::Array::const_iterator arr_mod_index(arr_mod.begin()); arr_mod_index != arr_mod.end(); arr_mod_index++) { std::vector<std::string> keys;
combo.KeyInsert(*arr_mod_index); keys.reserve(arr_mod.size() + 1);
} copy(arr_mod.begin(), arr_mod.end(), back_inserter(keys));
keys.push_back((*arr_index)["key"]);
combo.KeyInsert((*arr_index)["key"]); ComboInsert(Combo(context, index->first, keys));
ComboInsert(combo);
} }
} }
} }
@ -111,12 +104,10 @@ bool Hotkey::Scan(const std::string &context, const std::string &str, bool alway
LOG_D("agi/hotkey/found") << "Found: " << str << " Context (req/found): " << context << "/Always Command: " << cmd; LOG_D("agi/hotkey/found") << "Found: " << str << " Context (req/found): " << context << "/Always Command: " << cmd;
return true; return true;
} }
if (ctext == "Default") { if (ctext == "Default")
dfault = index->second.CmdName(); dfault = index->second.CmdName();
} else if (ctext == context)
else if (ctext == context) {
local = index->second.CmdName(); local = index->second.CmdName();
}
} }
if (!local.empty()) { if (!local.empty()) {
@ -139,9 +130,8 @@ std::vector<std::string> Hotkey::GetHotkeys(const std::string &context, const st
HotkeyMap::const_iterator it, end; HotkeyMap::const_iterator it, end;
for (std::tr1::tie(it, end) = cmd_map.equal_range(command); it != end; ++it) { for (std::tr1::tie(it, end) = cmd_map.equal_range(command); it != end; ++it) {
std::string ctext = it->second.Context(); std::string ctext = it->second.Context();
if (ctext == "Always" || ctext == "Default" || ctext == context) { if (ctext == "Always" || ctext == "Default" || ctext == context)
ret.push_back(it->second.StrMenu()); ret.push_back(it->second.StrMenu());
}
} }
sort(ret.begin(), ret.end()); sort(ret.begin(), ret.end());
@ -156,8 +146,10 @@ std::string Hotkey::GetHotkey(const std::string &context, const std::string &com
for (std::tr1::tie(it, end) = cmd_map.equal_range(command); it != end; ++it) { for (std::tr1::tie(it, end) = cmd_map.equal_range(command); it != end; ++it) {
std::string ctext = it->second.Context(); std::string ctext = it->second.Context();
if (ctext == context) return it->second.StrMenu(); if (ctext == context) return it->second.StrMenu();
if (ctext == "Default") ret = it->second.StrMenu(); if (ctext == "Default")
else if (ret.empty() && ctext == "Always") it->second.StrMenu(); ret = it->second.StrMenu();
else if (ret.empty() && ctext == "Always")
it->second.StrMenu();
} }
return ret; return ret;
} }
@ -166,12 +158,10 @@ void Hotkey::Flush() {
json::Object root; json::Object root;
for (HotkeyMap::iterator index = str_map.begin(); index != str_map.end(); ++index) { for (HotkeyMap::iterator index = str_map.begin(); index != str_map.end(); ++index) {
Combo::ComboMap combo_map(index->second.Get()); std::vector<std::string> const& combo_map(index->second.Get());
json::Array modifiers; json::Array modifiers;
for (int i = 0; i != combo_map.size()-1; i++) { copy(combo_map.begin(), combo_map.end() - 1, std::back_inserter(modifiers));
modifiers.push_back(json::String(combo_map[i]));
}
json::Object hotkey; json::Object hotkey;
hotkey["modifiers"] = modifiers; hotkey["modifiers"] = modifiers;
@ -185,5 +175,15 @@ void Hotkey::Flush() {
json::Writer::Write(root, file.Get()); json::Writer::Write(root, file.Get());
} }
void Hotkey::SetHotkeyMap(HotkeyMap const& new_map) {
cmd_map = new_map;
str_map.clear();
for (HotkeyMap::iterator it = cmd_map.begin(); it != cmd_map.end(); ++it)
str_map.insert(make_pair(it->second.Str(), it->second));
HotkeysChanged();
}
} // namespace toolbar } // namespace toolbar
} // namespace agi } // namespace agi

View File

@ -19,13 +19,14 @@
/// @ingroup hotkey menu event window /// @ingroup hotkey menu event window
#ifndef LAGI_PRE #ifndef LAGI_PRE
#include <cmath>
#include <map> #include <map>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#endif #endif
#include <libaegisub/signal.h>
namespace json { namespace json {
class UnknownElement; class UnknownElement;
typedef std::map<std::string, UnknownElement> Object; typedef std::map<std::string, UnknownElement> Object;
@ -34,27 +35,23 @@ namespace json {
namespace agi { namespace agi {
namespace hotkey { namespace hotkey {
class Hotkey;
/// Hotkey instance.
extern Hotkey *hotkey;
/// @class Combo /// @class Combo
/// A Combo represents a linear sequence of characters set in an std::vector. This makes up /// A Combo represents a linear sequence of characters set in an std::vector. This makes up
/// a single combination, or "Hotkey". /// a single combination, or "Hotkey".
class Combo { class Combo {
friend class Hotkey; std::vector<std::string> key_map;
std::string cmd_name;
std::string context;
public: public:
/// Linear key sequence that forms a combination or "Combo"
typedef std::vector<std::string> ComboMap;
/// Constructor /// Constructor
/// @param ctx Context /// @param ctx Context
/// @param cmd Command name /// @param cmd Command name
Combo(std::string const& ctx, std::string const& cmd): cmd_name(cmd), context(ctx) {} Combo(std::string const& ctx, std::string const& cmd, std::vector<std::string> const& keys)
: key_map(keys)
/// Destructor , cmd_name(cmd)
~Combo() {} , context(ctx)
{
}
/// String representation of the Combo /// String representation of the Combo
std::string Str() const; std::string Str() const;
@ -64,7 +61,7 @@ public:
/// Get the literal combo map. /// Get the literal combo map.
/// @return ComboMap (std::vector) of linear key sequence. /// @return ComboMap (std::vector) of linear key sequence.
const ComboMap& Get() const { return key_map; } const std::vector<std::string>& Get() const { return key_map; }
/// Command name triggered by the combination. /// Command name triggered by the combination.
/// @return Command name /// @return Command name
@ -72,30 +69,40 @@ public:
/// Context this Combo is triggered in. /// Context this Combo is triggered in.
const std::string& Context() const { return context; } const std::string& Context() const { return context; }
private:
ComboMap key_map; ///< Map.
const std::string cmd_name; ///< Command name.
const std::string context; ///< Context
/// Insert a key into the ComboMap.
/// @param key Key to insert.
void KeyInsert(std::string key) { key_map.push_back(key); }
}; };
/// @class Hotkey /// @class Hotkey
/// Holds the map of Combo instances and handles searching for matching key sequences. /// Holds the map of Combo instances and handles searching for matching key sequences.
class Hotkey { class Hotkey {
public:
/// Map to hold Combo instances
typedef std::multimap<std::string, Combo> HotkeyMap;
private:
HotkeyMap str_map; ///< String representation -> Combo
HotkeyMap cmd_map; ///< Command name -> Combo
const std::string config_file; ///< Default user config location.
/// Build hotkey map.
/// @param context Context being parsed.
/// @param object json::Object holding items for context being parsed.
void BuildHotkey(std::string const& context, const json::Object& object);
/// Insert Combo into HotkeyMap instance.
/// @param combo Combo to insert.
void ComboInsert(Combo const& combo);
/// Write active Hotkey configuration to disk.
void Flush();
/// Announce that the loaded hotkeys have been changed
agi::signal::Signal<> HotkeysChanged;
public: public:
/// Constructor /// Constructor
/// @param file Location of user config file. /// @param file Location of user config file.
/// @param default_config Default config. /// @param default_config Default config.
Hotkey(const std::string &file, const std::string &default_config); Hotkey(const std::string &file, const std::string &default_config);
/// Destructor.
~Hotkey();
/// Scan for a matching key. /// Scan for a matching key.
/// @param context Context requested. /// @param context Context requested.
/// @param str Hyphen separated key sequence. /// @param str Hyphen separated key sequence.
@ -115,23 +122,13 @@ public:
/// @return A hotkey for the given command or "" if there are none /// @return A hotkey for the given command or "" if there are none
std::string GetHotkey(const std::string &context, const std::string &command) const; std::string GetHotkey(const std::string &context, const std::string &command) const;
private: /// Get the raw command name -> combo map for all registered hotkeys
typedef std::multimap<std::string, Combo> HotkeyMap; ///< Map to hold Combo instances. HotkeyMap const& GetHotkeyMap() const { return cmd_map; }
HotkeyMap str_map; ///< String representation -> Combo
HotkeyMap cmd_map; ///< Command name -> Combo
const std::string config_file; ///< Default user config location.
/// Build hotkey map. /// Replace the loaded hotkeys with a new set
/// @param context Context being parsed. void SetHotkeyMap(HotkeyMap const& new_map);
/// @param object json::Object holding items for context being parsed.
void BuildHotkey(std::string const& context, const json::Object& object);
/// Insert Combo into HotkeyMap instance. DEFINE_SIGNAL_ADDERS(HotkeysChanged, AddHotkeyChangeListener)
/// @param combo Combo to insert.
void ComboInsert(Combo const& combo);
/// Write active Hotkey configuration to disk.
void Flush();
}; };
} // namespace hotkey } // namespace hotkey

View File

@ -186,9 +186,8 @@ bool AegisubApp::OnInit() {
// Init commands. // Init commands.
cmd::init_builtin_commands(); cmd::init_builtin_commands();
// Init hotkeys. // Init hotkeys
const std::string conf_user_hotkey(StandardPaths::DecodePath("?user/hotkey.json")); hotkey::init();
agi::hotkey::hotkey = new agi::hotkey::Hotkey(conf_user_hotkey, GET_DEFAULT_CONFIG(default_hotkey));
// Init icons. // Init icons.
icon::icon_init(); icon::icon_init();
@ -327,7 +326,7 @@ int AegisubApp::OnExit() {
delete plugins; delete plugins;
delete config::opt; delete config::opt;
delete config::mru; delete config::mru;
delete agi::hotkey::hotkey; hotkey::clear();
delete config::path; delete config::path;
cmd::clear(); cmd::clear();

View File

@ -32,6 +32,7 @@
#include "main.h" #include "main.h"
#include "standard_paths.h" #include "standard_paths.h"
#include <libaegisub/hotkey.h>
#include <libaegisub/json.h> #include <libaegisub/json.h>
#include <libaegisub/log.h> #include <libaegisub/log.h>
@ -133,6 +134,8 @@ struct menu_item_cmp {
class CommandManager { class CommandManager {
/// Menu items which need to do something on menu open /// Menu items which need to do something on menu open
std::deque<std::pair<cmd::Command*, wxMenuItem*> > dynamic_items; std::deque<std::pair<cmd::Command*, wxMenuItem*> > dynamic_items;
/// Menu items which need to be updated only when hotkeys change
std::deque<std::pair<cmd::Command*, wxMenuItem*> > static_items;
/// window id -> command map /// window id -> command map
std::vector<cmd::Command*> items; std::vector<cmd::Command*> items;
/// MRU menus which need to be updated on menu open /// MRU menus which need to be updated on menu open
@ -141,19 +144,30 @@ class CommandManager {
/// Project context /// Project context
agi::Context *context; agi::Context *context;
/// Connection for hotkey change signal
agi::signal::Connection hotkeys_changed;
/// Update a single dynamic menu item /// Update a single dynamic menu item
void UpdateItem(std::pair<cmd::Command*, wxMenuItem*> const& item) { void UpdateItem(std::pair<cmd::Command*, wxMenuItem*> const& item) {
int flags = item.first->Type(); int flags = item.first->Type();
if (flags & cmd::COMMAND_DYNAMIC_NAME) if (flags & cmd::COMMAND_DYNAMIC_NAME)
item.second->SetItemLabel(get_menu_text(item.first, context)); UpdateItemName(item);
if (flags & cmd::COMMAND_VALIDATE) if (flags & cmd::COMMAND_VALIDATE)
item.second->Enable(item.first->Validate(context)); item.second->Enable(item.first->Validate(context));
if (flags & cmd::COMMAND_RADIO || flags & cmd::COMMAND_TOGGLE) if (flags & cmd::COMMAND_RADIO || flags & cmd::COMMAND_TOGGLE)
item.second->Check(item.first->IsActive(context)); item.second->Check(item.first->IsActive(context));
} }
void UpdateItemName(std::pair<cmd::Command*, wxMenuItem*> const& item) {
item.second->SetItemLabel(get_menu_text(item.first, context));
}
public: public:
CommandManager(agi::Context *context) : context(context) { } CommandManager(agi::Context *context)
: context(context)
, hotkeys_changed(hotkey::inst->AddHotkeyChangeListener(&CommandManager::OnHotkeysChanged, this))
{
}
/// Append a command to a menu and register the needed handlers /// Append a command to a menu and register the needed handlers
int AddCommand(cmd::Command *co, wxMenu *parent, std::string const& text) { int AddCommand(cmd::Command *co, wxMenu *parent, std::string const& text) {
@ -177,6 +191,8 @@ public:
if (flags != cmd::COMMAND_NORMAL) if (flags != cmd::COMMAND_NORMAL)
dynamic_items.push_back(std::make_pair(co, item)); dynamic_items.push_back(std::make_pair(co, item));
else
static_items.push_back(std::make_pair(co, item));
return item->GetId(); return item->GetId();
} }
@ -209,6 +225,12 @@ public:
if (id < items.size()) if (id < items.size())
(*items[id])(context); (*items[id])(context);
} }
/// Update the hotkeys for all menu items
void OnHotkeysChanged() {
for_each(dynamic_items.begin(), dynamic_items.end(), bind(&CommandManager::UpdateItemName, this, _1));
for_each(static_items.begin(), static_items.end(), bind(&CommandManager::UpdateItemName, this, _1));
}
}; };
/// Wrapper for wxMenu to add a command manager /// Wrapper for wxMenu to add a command manager

View File

@ -35,6 +35,7 @@
#include <wx/toolbar.h> #include <wx/toolbar.h>
#endif #endif
#include <libaegisub/hotkey.h>
#include <libaegisub/json.h> #include <libaegisub/json.h>
#include <libaegisub/log.h> #include <libaegisub/log.h>
#include <libaegisub/signal.h> #include <libaegisub/signal.h>
@ -64,6 +65,9 @@ namespace {
/// Listener for icon size change signal /// Listener for icon size change signal
agi::signal::Connection icon_size_slot; agi::signal::Connection icon_size_slot;
/// Listener for hotkey change signal
agi::signal::Connection hotkeys_changed_slot;
/// Enable/disable the toolbar buttons /// Enable/disable the toolbar buttons
void OnIdle(wxIdleEvent &) { void OnIdle(wxIdleEvent &) {
for (size_t i = 0; i < commands.size(); ++i) { for (size_t i = 0; i < commands.size(); ++i) {
@ -81,8 +85,8 @@ namespace {
(*commands[evt.GetId() - TOOL_ID_BASE])(context); (*commands[evt.GetId() - TOOL_ID_BASE])(context);
} }
/// Clear the toolbar and recreate it with the new icon size /// Clear the toolbar and recreate it
void OnIconSizeChanged() { void RegenerateToolbar() {
Unbind(wxEVT_IDLE, &Toolbar::OnIdle, this); Unbind(wxEVT_IDLE, &Toolbar::OnIdle, this);
ClearTools(); ClearTools();
commands.clear(); commands.clear();
@ -130,20 +134,7 @@ namespace {
flags & cmd::COMMAND_TOGGLE ? wxITEM_CHECK : flags & cmd::COMMAND_TOGGLE ? wxITEM_CHECK :
wxITEM_NORMAL; wxITEM_NORMAL;
wxString tooltip = command->StrHelp(); AddTool(TOOL_ID_BASE + commands.size(), command->StrDisplay(context), icon, GetTooltip(command), kind);
std::vector<std::string> hotkeys = hotkey::get_hotkey_strs(ht_context, command->name());
for (size_t i = 0; i < hotkeys.size(); ++i) {
if (i == 0)
tooltip += " (";
else
tooltip += "/";
tooltip += hotkeys[i];
}
if (hotkeys.size()) tooltip += ")";
AddTool(TOOL_ID_BASE + commands.size(), command->StrDisplay(context), icon, tooltip, kind);
commands.push_back(command); commands.push_back(command);
needs_onidle = needs_onidle || flags != cmd::COMMAND_NORMAL; needs_onidle = needs_onidle || flags != cmd::COMMAND_NORMAL;
@ -157,14 +148,31 @@ namespace {
Realize(); Realize();
} }
wxString GetTooltip(cmd::Command *command) {
wxString ret = command->StrHelp();
std::vector<std::string> hotkeys = hotkey::get_hotkey_strs(ht_context, command->name());
for (size_t i = 0; i < hotkeys.size(); ++i) {
if (i == 0)
ret += " (";
else
ret += "/";
ret += hotkeys[i];
}
if (hotkeys.size()) ret += ")";
return ret;
}
public: public:
Toolbar(wxWindow *parent, std::string const& name, agi::Context *c, std::string const& ht_context) Toolbar(wxWindow *parent, std::string const& name, agi::Context *c, std::string const& ht_context)
: wxToolBar(parent, -1, wxDefaultPosition, wxDefaultSize, wxTB_FLAT | wxTB_HORIZONTAL) : wxToolBar(parent, -1, wxDefaultPosition, wxDefaultSize, wxTB_FLAT | wxTB_HORIZONTAL)
, name(name) , name(name)
, context(c) , context(c)
, ht_context(ht_context) , ht_context(ht_context)
, icon_size_slot(OPT_SUB("App/Toolbar Icon Size", &Toolbar::OnIconSizeChanged, this)) , icon_size_slot(OPT_SUB("App/Toolbar Icon Size", &Toolbar::RegenerateToolbar, this))
/// @todo bind to hotkey changed event when such a thing exists , hotkeys_changed_slot(hotkey::inst->AddHotkeyChangeListener(&Toolbar::RegenerateToolbar, this))
{ {
Populate(); Populate();
Bind(wxEVT_COMMAND_TOOL_CLICKED, &Toolbar::OnClick, this); Bind(wxEVT_COMMAND_TOOL_CLICKED, &Toolbar::OnClick, this);

View File

@ -40,6 +40,8 @@
#include "include/aegisub/hotkey.h" #include "include/aegisub/hotkey.h"
#include <libaegisub/hotkey.h>
struct ToolTipBinding { struct ToolTipBinding {
wxWindow *window; wxWindow *window;
wxString toolTip; wxString toolTip;
@ -54,10 +56,10 @@ ToolTipManager::~ToolTipManager() { }
void ToolTipManager::Bind(wxWindow *window, wxString tooltip, const char *context, const char *command) { void ToolTipManager::Bind(wxWindow *window, wxString tooltip, const char *context, const char *command) {
ToolTipBinding tip = { window, tooltip, command, context }; ToolTipBinding tip = { window, tooltip, command, context };
tip.Update(); tip.Update();
/// @todo bind to hotkey changed signal once such a thing exists
static ToolTipManager instance; static ToolTipManager instance;
instance.tips.push_back(tip); instance.tips.push_back(tip);
hotkey::inst->AddHotkeyChangeListener(&ToolTipBinding::Update, &instance.tips.back());
} }
void ToolTipBinding::Update() { void ToolTipBinding::Update() {