diff --git a/library/Core.cpp b/library/Core.cpp index c6d204f03..013902d82 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -51,16 +51,20 @@ using namespace DFHack; #include "dfhack/SDL_fakes/events.h" +#include "dfhack/df/ui.h" #include "dfhack/df/world.h" #include "dfhack/df/world_data.h" +#include "dfhack/df/interface.h" +#include "dfhack/df/viewscreen_dwarfmodest.h" #include #include #include #include #include "tinythread.h" -using namespace tthread; +using namespace tthread; +using namespace df::enums; struct Core::Cond { @@ -204,6 +208,7 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue " cls - Clear the console.\n" " fpause - Force DF to pause.\n" " die - Force DF to close immediately\n" + " keybinding - Modify bindings of commands to keys\n" "Plugin management (useful for developers):\n" //" belongs COMMAND - Tell which plugin a command belongs to.\n" " plug [PLUGIN|v] - List plugin state and description.\n" @@ -212,6 +217,27 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue " reload PLUGIN|all - Reload a plugin or all loaded plugins.\n" ); } + else if (parts.size() == 1) + { + Plugin *plug = plug_mgr->getPluginByCommand(parts[0]); + if (plug) { + for (int j = 0; j < plug->size();j++) + { + const PluginCommand & pcmd = (plug->operator[](j)); + if (pcmd.name != parts[0]) + continue; + + if (pcmd.isHotkeyCommand()) + con.color(Console::COLOR_CYAN); + con.print("%s: %s\n",pcmd.name.c_str(), pcmd.description.c_str()); + con.reset_color(); + if (!pcmd.usage.empty()) + con << "Usage:\n" << pcmd.usage << flush; + return; + } + } + con.printerr("Unknown command: %s\n", parts[0].c_str()); + } else { con.printerr("not implemented yet\n"); @@ -311,7 +337,10 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue else for (int j = 0; j < plug->size();j++) { const PluginCommand & pcmd = (plug->operator[](j)); + if (pcmd.isHotkeyCommand()) + con.color(Console::COLOR_CYAN); con.print(" %-22s - %s\n",pcmd.name.c_str(), pcmd.description.c_str()); + con.reset_color(); } } else @@ -339,7 +368,10 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue for (int j = 0; j < plug->size();j++) { const PluginCommand & pcmd = (plug->operator[](j)); + if (pcmd.isHotkeyCommand()) + con.color(Console::COLOR_CYAN); con.print(" %-22s- %s\n",pcmd.name.c_str(), pcmd.description.c_str()); + con.reset_color(); } } } @@ -354,6 +386,49 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue con.print("%s\n", plug->getName().c_str()); } } + else if(first == "keybinding") + { + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) + { + std::string keystr = parts[1]; + if (parts[0] == "set") + core->ClearKeyBindings(keystr); + for (int i = parts.size()-1; i >= 2; i--) + { + if (!core->AddKeyBinding(keystr, parts[i])) { + con.printerr("Invalid key spec: %s\n", keystr.c_str()); + break; + } + } + } + else if (parts.size() >= 2 && parts[0] == "clear") + { + for (unsigned i = 1; i < parts.size(); i++) + { + if (!core->ClearKeyBindings(parts[i])) { + con.printerr("Invalid key spec: %s\n", parts[i].c_str()); + break; + } + } + } + else if (parts.size() == 2 && parts[0] == "list") + { + std::vector list = core->ListKeyBindings(parts[1]); + if (list.empty()) + con << "No bindings." << endl; + for (unsigned i = 0; i < list.size(); i++) + con << " " << list[i] << endl; + } + else + { + con << "Usage:" << endl + << " keybinding list " << endl + << " keybinding clear ..." << endl + << " keybinding set \"cmdline\" \"cmdline\"..." << endl + << " keybinding add \"cmdline\" \"cmdline\"..." << endl + << "Later adds, and earlier items within one command have priority." << endl; + } + } else if(first == "fpause") { World * w = core->getWorld(); @@ -482,12 +557,12 @@ Core::Core() StackMutex = 0; core_cond = 0; // set up hotkey capture - memset(hotkey_states,0,sizeof(hotkey_states)); hotkey_set = false; HotkeyMutex = 0; HotkeyCond = 0; misc_data_mutex=0; last_world_data_ptr = NULL; + top_viewscreen = NULL; }; void Core::fatal (std::string output, bool deactivate) @@ -694,6 +769,19 @@ int Core::Update() plug_mgr->OnStateChange(new_wdata ? SC_GAME_LOADED : SC_GAME_UNLOADED); } + // detect if the viewscreen changed + if (df::global::gview) + { + df::viewscreen *screen = &df::global::gview->view; + while (screen->child) + screen = screen->child; + if (screen != top_viewscreen) + { + top_viewscreen = screen; + plug_mgr->OnStateChange(SC_VIEWSCREEN_CHANGED); + } + } + // notify all the plugins that a game tick is finished plug_mgr->OnUpdate(); @@ -785,39 +873,168 @@ int Core::SDL_Event(SDL::Event* ev, int orig_return) if(ev && ev->type == SDL::ET_KEYDOWN || ev->type == SDL::ET_KEYUP) { SDL::KeyboardEvent * ke = (SDL::KeyboardEvent *)ev; - bool shift = ke->ksym.mod & SDL::KMOD_SHIFT; - // consuming F1 .. F8 - int idx = ke->ksym.sym - SDL::K_F1; - if(idx < 0 || idx > 7) - return orig_return; - idx += 8*shift; - // now we have the real index... - if(ke->state == SDL::BTN_PRESSED && !hotkey_states[idx]) + + if(ke->state == SDL::BTN_PRESSED && !hotkey_states[ke->ksym.sym]) { - hotkey_states[idx] = 1; - Gui * g = getGui(); - if(g->hotkeys && g->df_interface && g->df_menu_state) - { - t_viewscreen * ws = g->GetCurrentScreen(); - // FIXME: put hardcoded values into memory.xml - if(ws->getClassName() == "viewscreen_dwarfmodest" && *g->df_menu_state == 0x23) - return orig_return; - else - { - t_hotkey & hotkey = (*g->hotkeys)[idx]; - setHotkeyCmd(hotkey.name); - } - } + hotkey_states[ke->ksym.sym] = true; + + int mod = 0; + if (ke->ksym.mod & SDL::KMOD_SHIFT) mod |= 1; + if (ke->ksym.mod & SDL::KMOD_CTRL) mod |= 2; + if (ke->ksym.mod & SDL::KMOD_ALT) mod |= 4; + + SelectHotkey(ke->ksym.sym, mod); } else if(ke->state == SDL::BTN_RELEASED) { - hotkey_states[idx] = 0; + hotkey_states[ke->ksym.sym] = false; } } return orig_return; // do stuff with the events... } +bool Core::SelectHotkey(int sym, int modifiers) +{ + // Find the topmost viewscreen + if (!df::global::gview || !df::global::ui) + return false; + + df::viewscreen *screen = &df::global::gview->view; + while (screen->child) + screen = screen->child; + + std::string cmd; + + { + tthread::lock_guard lock(*HotkeyMutex); + + // Check the internal keybindings + std::vector &bindings = key_bindings[sym]; + for (int i = bindings.size()-1; i >= 0; --i) { + if (bindings[i].modifiers != modifiers) + continue; + if (!plug_mgr->CanInvokeHotkey(bindings[i].command[0], screen)) + continue; + cmd = bindings[i].cmdline; + break; + } + + if (cmd.empty()) { + // Check the hotkey keybindings + int idx = sym - SDL::K_F1; + if(idx >= 0 && idx < 8) + { + if (modifiers & 1) + idx += 8; + + if (!strict_virtual_cast(screen) || + df::global::ui->main.mode != ui_sidebar_mode::Hotkeys) + { + cmd = df::global::ui->main.hotkeys[idx].name; + } + } + } + } + + if (!cmd.empty()) { + setHotkeyCmd(cmd); + return true; + } + else + return false; +} + +static bool parseKeySpec(std::string keyspec, int *psym, int *pmod) +{ + *pmod = 0; + + // ugh, ugly + for (;;) { + if (keyspec.size() > 6 && keyspec.substr(0, 6) == "Shift-") { + *pmod |= 1; + keyspec = keyspec.substr(6); + } else if (keyspec.size() > 5 && keyspec.substr(0, 5) == "Ctrl-") { + *pmod |= 2; + keyspec = keyspec.substr(5); + } else if (keyspec.size() > 4 && keyspec.substr(0, 4) == "Alt-") { + *pmod |= 4; + keyspec = keyspec.substr(4); + } else + break; + } + + if (keyspec.size() == 1 && keyspec[0] >= 'A' && keyspec[0] <= 'Z') { + *psym = SDL::K_a + (keyspec[0]-'A'); + return true; + } else if (keyspec.size() == 2 && keyspec[0] == 'F' && keyspec[1] >= '1' && keyspec[1] <= '9') { + *psym = SDL::K_F1 + (keyspec[1]-'1'); + return true; + } else + return false; +} + +bool Core::ClearKeyBindings(std::string keyspec) +{ + int sym, mod; + if (!parseKeySpec(keyspec, &sym, &mod)) + return false; + + tthread::lock_guard lock(*HotkeyMutex); + + std::vector &bindings = key_bindings[sym]; + for (int i = bindings.size()-1; i >= 0; --i) { + if (bindings[i].modifiers == mod) + bindings.erase(bindings.begin()+i); + } + + return true; +} + +bool Core::AddKeyBinding(std::string keyspec, std::string cmdline) +{ + int sym; + KeyBinding binding; + if (!parseKeySpec(keyspec, &sym, &binding.modifiers)) + return false; + + cheap_tokenise(cmdline, binding.command); + if (binding.command.empty()) + return false; + + tthread::lock_guard lock(*HotkeyMutex); + + // Don't add duplicates + std::vector &bindings = key_bindings[sym]; + for (int i = bindings.size()-1; i >= 0; --i) { + if (bindings[i].modifiers == binding.modifiers && + bindings[i].cmdline == cmdline) + return true; + } + + binding.cmdline = cmdline; + bindings.push_back(binding); + return true; +} + +std::vector Core::ListKeyBindings(std::string keyspec) +{ + int sym, mod; + std::vector rv; + if (!parseKeySpec(keyspec, &sym, &mod)) + return rv; + + tthread::lock_guard lock(*HotkeyMutex); + + std::vector &bindings = key_bindings[sym]; + for (int i = bindings.size()-1; i >= 0; --i) { + if (bindings[i].modifiers == mod) + rv.push_back(bindings[i].cmdline); + } + + return rv; +} + //////////////// // ClassNamCheck //////////////// diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index 997b8e59f..f25c34d5c 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -27,6 +27,9 @@ distribution. #include "dfhack/Process.h" #include "dfhack/PluginManager.h" #include "dfhack/Console.h" + +#include "dfhack/DataDefs.h" + using namespace DFHack; #include @@ -273,13 +276,67 @@ command_result Plugin::invoke( std::string & command, std::vector { for (int i = 0; i < commands.size();i++) { - if(commands[i].name == command) + PluginCommand &cmd = commands[i]; + + if(cmd.name == command) { // running interactive things from some other source than the console would break it - if(!interactive_ && commands[i].interactive) + if(!interactive_ && cmd.interactive) cr = CR_WOULD_BREAK; + else if (cmd.guard) + { + // Execute hotkey commands in a way where they can + // expect their guard conditions to be matched, + // so as to avoid duplicating checks. + // This means suspending the core beforehand. + CoreSuspender suspend(&c); + df::viewscreen *top = c.getTopViewscreen(); + + if (!cmd.guard(&c, top)) + { + c.con.printerr("Could not invoke %s: unsuitable UI state.\n", command.c_str()); + cr = CR_WRONG_USAGE; + } + else + { + cr = cmd.function(&c, parameters); + } + } else - cr = commands[i].function(&c, parameters); + { + cr = cmd.function(&c, parameters); + } + + if (cr == CR_WRONG_USAGE && !cmd.usage.empty()) + c.con << "Usage:\n" << cmd.usage << flush; + + break; + } + } + } + access->lock_sub(); + return cr; +} + +bool Plugin::can_invoke_hotkey( std::string & command, df::viewscreen *top ) +{ + Core & c = Core::getInstance(); + bool cr = false; + access->lock_add(); + if(state == PS_LOADED) + { + for (int i = 0; i < commands.size();i++) + { + PluginCommand &cmd = commands[i]; + + if(cmd.name == command) + { + if (cmd.interactive) + cr = false; + else if (cmd.guard) + cr = cmd.guard(&c, top); + else + cr = default_hotkey(&c, top); break; } } @@ -363,19 +420,27 @@ Plugin *PluginManager::getPluginByName (const std::string & name) return 0; } +Plugin *PluginManager::getPluginByCommand(const std::string &command) +{ + tthread::lock_guard lock(*cmdlist_mutex); + map ::iterator iter = belongs.find(command); + if (iter != belongs.end()) + return iter->second; + else + return NULL; +} + // FIXME: handle name collisions... command_result PluginManager::InvokeCommand( std::string & command, std::vector & parameters, bool interactive) { - command_result cr = CR_NOT_IMPLEMENTED; - Core * c = &Core::getInstance(); - cmdlist_mutex->lock(); - map ::iterator iter = belongs.find(command); - if(iter != belongs.end()) - { - cr = iter->second->invoke(command, parameters, interactive); - } - cmdlist_mutex->unlock(); - return cr; + Plugin *plugin = getPluginByCommand(command); + return plugin ? plugin->invoke(command, parameters, interactive) : CR_NOT_IMPLEMENTED; +} + +bool PluginManager::CanInvokeHotkey(std::string &command, df::viewscreen *top) +{ + Plugin *plugin = getPluginByCommand(command); + return plugin ? plugin->can_invoke_hotkey(command, top) : false; } void PluginManager::OnUpdate( void ) diff --git a/library/include/dfhack/Core.h b/library/include/dfhack/Core.h index e91dae390..d075d0fae 100644 --- a/library/include/dfhack/Core.h +++ b/library/include/dfhack/Core.h @@ -43,6 +43,11 @@ namespace tthread class thread; } +namespace df +{ + struct viewscreen; +} + namespace DFHack { class Process; @@ -134,7 +139,12 @@ namespace DFHack /// returns a named pointer. void *GetData(std::string key); + bool ClearKeyBindings(std::string keyspec); + bool AddKeyBinding(std::string keyspec, std::string cmdline); + std::vector ListKeyBindings(std::string keyspec); + bool isWorldLoaded() { return (last_world_data_ptr != NULL); } + df::viewscreen *getTopViewscreen() { return top_viewscreen; } DFHack::Process * p; DFHack::VersionInfo * vinfo; @@ -180,13 +190,25 @@ namespace DFHack } s_mods; std::vector allModules; DFHack::PluginManager * plug_mgr; + // hotkey-related stuff - int hotkey_states[16]; + struct KeyBinding { + int modifiers; + std::vector command; + std::string cmdline; + }; + + std::map > key_bindings; + std::map hotkey_states; std::string hotkey_cmd; bool hotkey_set; tthread::mutex * HotkeyMutex; tthread::condition_variable * HotkeyCond; + + bool SelectHotkey(int key, int modifiers); + void *last_world_data_ptr; // for state change tracking + df::viewscreen *top_viewscreen; // Very important! bool started; diff --git a/library/include/dfhack/PluginManager.h b/library/include/dfhack/PluginManager.h index 0ac84f219..53d431c5c 100644 --- a/library/include/dfhack/PluginManager.h +++ b/library/include/dfhack/PluginManager.h @@ -35,49 +35,68 @@ namespace tthread class mutex; class condition_variable; } +namespace df +{ + struct viewscreen; +} namespace DFHack { class Core; class PluginManager; + enum command_result { CR_WOULD_BREAK = -2, CR_NOT_IMPLEMENTED = -1, CR_FAILURE = 0, - CR_OK = 1 + CR_OK = 1, + CR_WRONG_USAGE = 2 }; enum state_change_event { SC_GAME_LOADED, - SC_GAME_UNLOADED + SC_GAME_UNLOADED, + SC_VIEWSCREEN_CHANGED }; - struct PluginCommand + struct DFHACK_EXPORT PluginCommand { + typedef command_result (*command_function)(Core *, std::vector &); + typedef bool (*command_hotkey_guard)(Core *, df::viewscreen *); + /// create a command with a name, description, function pointer to its code /// and saying if it needs an interactive terminal /// Most commands shouldn't require an interactive terminal! PluginCommand(const char * _name, const char * _description, - command_result (*function_)(Core *, std::vector &), - bool interactive_ = false + command_function function_, + bool interactive_ = false, + const char * usage_ = "" ) + : name(_name), description(_description), + function(function_), interactive(interactive_), + guard(NULL), usage(usage_) { - name = _name; - description = _description; - function = function_; - interactive = interactive_; } - PluginCommand (const PluginCommand & rhs) + + PluginCommand(const char * _name, + const char * _description, + command_function function_, + command_hotkey_guard guard_, + const char * usage_ = "") + : name(_name), description(_description), + function(function_), interactive(false), + guard(guard_), usage(usage_) { - name = rhs.name; - description = rhs.description; - function = rhs.function; - interactive = rhs.interactive; } + + bool isHotkeyCommand() const { return guard != NULL; } + std::string name; std::string description; - command_result (*function)(Core *, std::vector &); + command_function function; bool interactive; + command_hotkey_guard guard; + std::string usage; }; class Plugin { @@ -98,6 +117,7 @@ namespace DFHack bool unload(); bool reload(); command_result invoke( std::string & command, std::vector & parameters, bool interactive ); + bool can_invoke_hotkey( std::string & command, df::viewscreen *top ); plugin_state getState () const; const PluginCommand& operator[] (std::size_t index) const { @@ -139,7 +159,9 @@ namespace DFHack // PUBLIC METHODS public: Plugin *getPluginByName (const std::string & name); + Plugin *getPluginByCommand (const std::string &command); command_result InvokeCommand( std::string & command, std::vector & parameters, bool interactive = true ); + bool CanInvokeHotkey(std::string &command, df::viewscreen *top); Plugin* operator[] (std::size_t index) { if(index >= all_plugins.size()) @@ -157,5 +179,10 @@ namespace DFHack std::vector all_plugins; std::string plugin_path; }; + + // Predefined hotkey guards + DFHACK_EXPORT bool default_hotkey(Core *, df::viewscreen *); + DFHACK_EXPORT bool dwarfmode_hotkey(Core *, df::viewscreen *); + DFHACK_EXPORT bool cursor_hotkey(Core *, df::viewscreen *); } diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 46f0ad2ea..d63849dce 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -37,8 +37,44 @@ using namespace std; #include "dfhack/Error.h" #include "ModuleFactory.h" #include "dfhack/Core.h" +#include "dfhack/PluginManager.h" using namespace DFHack; +#include "dfhack/DataDefs.h" +#include "dfhack/df/cursor.h" +#include "dfhack/df/viewscreen_dwarfmodest.h" + +// Predefined common guard functions + +bool DFHack::default_hotkey(Core *, df::viewscreen *top) +{ + // Default hotkey guard function + for (;top ;top = top->parent) + if (strict_virtual_cast(top)) + return true; + return false; +} + +bool DFHack::dwarfmode_hotkey(Core *, df::viewscreen *top) +{ + // Require the main dwarf mode screen + return !!strict_virtual_cast(top); +} + +bool DFHack::cursor_hotkey(Core *c, df::viewscreen *top) +{ + if (!dwarfmode_hotkey(c, top)) + return false; + + // Also require the cursor. + if (!df::global::cursor || df::global::cursor->x == -30000) + return false; + + return true; +} + +// + Module* DFHack::createGui() { return new Gui(); diff --git a/library/xml/df.viewscreen.xml b/library/xml/df.viewscreen.xml index cb76db09b..e5816a194 100644 --- a/library/xml/df.viewscreen.xml +++ b/library/xml/df.viewscreen.xml @@ -91,6 +91,11 @@ + + + todo + +