Implement context-sensitive keybinding support.

Allow defining commands with guard conditions, and binding
one or more commands to alphabetic and function keys. When
the relevant key is pressed, the first listed command with
successfully evaluated guard is chosen.

For consistency, the guard is also checked when the command
is invoked from the console; this requires suspending the
core inside PluginManager, before invoking plugin code.
develop
Alexander Gavrilov 2011-12-30 23:25:50 +04:00
parent 4aa77f5530
commit 2222757e77
6 changed files with 351 additions and 67 deletions

@ -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 <stdio.h>
#include <iomanip>
#include <stdlib.h>
#include <fstream>
#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"
@ -354,6 +359,40 @@ 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
{
con << "Usage:" << endl
<< " keybinding clear <key> <key>..." << endl
<< " keybinding set <key> \"cmdline\" \"cmdline\"..." << endl
<< " keybinding add <key> \"cmdline\" \"cmdline\"..." << endl
<< "Later adds, and earlier items within one command have priority." << endl;
}
}
else if(first == "fpause")
{
World * w = core->getWorld();
@ -482,12 +521,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 +733,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,37 +837,149 @@ 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)
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)
{
t_viewscreen * ws = g->GetCurrentScreen();
// FIXME: put hardcoded values into memory.xml
if(ws->getClassName() == "viewscreen_dwarfmodest" && *g->df_menu_state == 0x23)
hotkey_states[ke->ksym.sym] = false;
}
}
return orig_return;
else
// 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;
{
t_hotkey & hotkey = (*g->hotkeys)[idx];
setHotkeyCmd(hotkey.name);
tthread::lock_guard<tthread::mutex> lock(*HotkeyMutex);
// Check the internal keybindings
std::vector<KeyBinding> &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<df::viewscreen_dwarfmodest>(screen) ||
df::global::ui->main.mode != ui_sidebar_mode::Hotkeys)
{
cmd = df::global::ui->main.hotkeys[idx].name;
}
}
else if(ke->state == SDL::BTN_RELEASED)
{
hotkey_states[idx] = 0;
}
}
return orig_return;
// do stuff with the events...
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<tthread::mutex> lock(*HotkeyMutex);
std::vector<KeyBinding> &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<tthread::mutex> lock(*HotkeyMutex);
binding.cmdline = cmdline;
key_bindings[sym].push_back(binding);
return true;
}
bool DFHack::default_hotkey(Core *, df::viewscreen *top)
{
// Default hotkey guard function
for (;top ;top = top->parent)
if (strict_virtual_cast<df::viewscreen_dwarfmodest>(top))
return true;
return false;
}
////////////////

@ -27,6 +27,10 @@ distribution.
#include "dfhack/Process.h"
#include "dfhack/PluginManager.h"
#include "dfhack/Console.h"
#include "dfhack/DataDefs.h"
#include "dfhack/df/viewscreen.h"
using namespace DFHack;
#include <string>
@ -273,13 +277,69 @@ command_result Plugin::invoke( std::string & command, std::vector <std::string>
{
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.viewscreen_type && !cmd.viewscreen_type->is_instance(top))
|| !cmd.guard(&c, top))
{
c.con.printerr("Could not invoke %s: unsuitable UI state.\n", command.c_str());
cr = CR_FAILURE;
}
else
cr = commands[i].function(&c, parameters);
{
cr = cmd.function(&c, parameters);
}
}
else
{
cr = cmd.function(&c, parameters);
}
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.viewscreen_type || cmd.viewscreen_type->is_instance(top))
&& cmd.guard(&c, top);
}
else
{
cr = default_hotkey(&c, top);
}
break;
}
}
@ -363,19 +423,27 @@ Plugin *PluginManager::getPluginByName (const std::string & name)
return 0;
}
Plugin *PluginManager::getPluginByCommand(const std::string &command)
{
tthread::lock_guard<tthread::mutex> lock(*cmdlist_mutex);
map <string, Plugin *>::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 <std::string> & parameters, bool interactive)
{
command_result cr = CR_NOT_IMPLEMENTED;
Core * c = &Core::getInstance();
cmdlist_mutex->lock();
map <string, Plugin *>::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 )

@ -43,6 +43,11 @@ namespace tthread
class thread;
}
namespace df
{
struct viewscreen;
}
namespace DFHack
{
class Process;
@ -134,7 +139,11 @@ namespace DFHack
/// returns a named pointer.
void *GetData(std::string key);
bool ClearKeyBindings(std::string keyspec);
bool AddKeyBinding(std::string keyspec, std::string cmdline);
bool isWorldLoaded() { return (last_world_data_ptr != NULL); }
df::viewscreen *getTopViewscreen() { return top_viewscreen; }
DFHack::Process * p;
DFHack::VersionInfo * vinfo;
@ -180,13 +189,25 @@ namespace DFHack
} s_mods;
std::vector <Module *> allModules;
DFHack::PluginManager * plug_mgr;
// hotkey-related stuff
int hotkey_states[16];
struct KeyBinding {
int modifiers;
std::vector<std::string> command;
std::string cmdline;
};
std::map<int, std::vector<KeyBinding> > key_bindings;
std::map<int, bool> 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;

@ -35,10 +35,16 @@ namespace tthread
class mutex;
class condition_variable;
}
namespace df
{
struct viewscreen;
}
namespace DFHack
{
class Core;
class PluginManager;
struct virtual_identity;
enum command_result
{
CR_WOULD_BREAK = -2,
@ -49,35 +55,45 @@ namespace DFHack
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 <std::string> &);
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 <std::string> &),
command_function function_,
bool interactive_ = false
)
: name(_name), description(_description),
function(function_), interactive(interactive_),
guard(NULL), viewscreen_type(NULL)
{
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_,
virtual_identity *viewscreen_type_ = NULL)
: name(_name), description(_description),
function(function_), interactive(false),
guard(guard_), viewscreen_type(viewscreen_type_)
{
name = rhs.name;
description = rhs.description;
function = rhs.function;
interactive = rhs.interactive;
}
std::string name;
std::string description;
command_result (*function)(Core *, std::vector <std::string> &);
command_function function;
bool interactive;
command_hotkey_guard guard;
virtual_identity *viewscreen_type;
};
class Plugin
{
@ -98,6 +114,7 @@ namespace DFHack
bool unload();
bool reload();
command_result invoke( std::string & command, std::vector <std::string> & 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 +156,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 <std::string> & 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 +176,7 @@ namespace DFHack
std::vector <Plugin *> all_plugins;
std::string plugin_path;
};
DFHACK_EXPORT bool default_hotkey(Core *, df::viewscreen *);
}

@ -91,6 +91,11 @@
<stl-string name="str_visit"/>
<stl-string name="str_site"/>
</class-type>
<class-type type-name='viewscreen_dwarfmodest' inherits-from='viewscreen'>
todo
</class-type>
</data-definition>
<!--

@ -8,6 +8,7 @@
#include <dfhack/df/ui.h>
#include <dfhack/df/building_stockpilest.h>
#include <dfhack/df/selection_rect.h>
#include <dfhack/df/viewscreen_dwarfmodest.h>
using std::vector;
using std::string;
@ -21,7 +22,8 @@ using df::global::selection_rect;
using df::building_stockpilest;
DFhackCExport command_result copystock(Core * c, vector <string> & parameters);
static command_result copystock(Core *c, vector <string> & parameters);
static bool copystock_guard(Core *c, df::viewscreen *top);
DFhackCExport const char * plugin_name ( void )
{
@ -32,7 +34,12 @@ DFhackCExport command_result plugin_init (Core *c, std::vector <PluginCommand> &
{
commands.clear();
if (world && ui) {
commands.push_back(PluginCommand("copystock", "Copy stockpile under cursor.", copystock));
commands.push_back(
PluginCommand(
"copystock", "Copy stockpile under cursor.", copystock,
copystock_guard, &df::viewscreen_dwarfmodest::_identity
)
);
}
std::cerr << "world: " << sizeof(df::world) << " ui: " << sizeof(df::ui)
<< " b_stock: " << sizeof(building_stockpilest) << endl;
@ -44,21 +51,24 @@ DFhackCExport command_result plugin_shutdown ( Core * c )
return CR_OK;
}
bool inSelectMode() {
static bool copystock_guard(Core *c, df::viewscreen *)
{
using namespace ui_sidebar_mode;
switch (ui->main.mode) {
case Stockpiles:
return true;
case BuildingItems:
case QueryBuilding:
return true;
return !!virtual_cast<building_stockpilest>(world->selected_building);
default:
return false;
}
}
DFhackCExport command_result copystock(Core * c, vector <string> & parameters)
static command_result copystock(Core * c, vector <string> & parameters)
{
CoreSuspender suspend(c);
/* HOTKEY COMMAND: CORE ALREADY SUSPENDED */
// For convenience: when used in the stockpiles mode, switch to 'q'
if (ui->main.mode == ui_sidebar_mode::Stockpiles) {
@ -70,15 +80,10 @@ DFhackCExport command_result copystock(Core * c, vector <string> & parameters)
return CR_OK;
}
if (!inSelectMode()) {
c->con << "Cannot copy stockpile in mode " << ENUM_KEY_STR(ui_sidebar_mode, ui->main.mode) << endl;
return CR_OK;
}
building_stockpilest *sp = virtual_cast<building_stockpilest>(world->selected_building);
if (!sp) {
c->con << "Selected building isn't a stockpile." << endl;
return CR_OK;
c->con.printerr("Selected building isn't a stockpile.\n");
return CR_FAILURE;
}
ui->stockpile.custom_settings = sp->settings;