diff --git a/Readme.rst b/Readme.rst index 7491c4a0c..904314722 100644 --- a/Readme.rst +++ b/Readme.rst @@ -2621,6 +2621,19 @@ materials, it returns you back to this screen. If you use this along with severa enabled materials, you should be able to place complex constructions more conveniently. +Hotkeys +======= + +Opens an in-game screen showing DFHack keybindings that are valid in the current mode. + +.. image:: images/hotkeys.png + +Type ``hotkeys`` into the DFHack console to open the screen, or bind the command to a +globally active hotkey in dfhack.init, e.g.: + + ``keybinding add Ctrl-F1 hotkeys`` + + gui/liquids =========== diff --git a/images/hotkeys.PNG b/images/hotkeys.PNG new file mode 100644 index 000000000..bdf9c76b8 Binary files /dev/null and b/images/hotkeys.PNG differ diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 650fe1e98..193168c71 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -159,6 +159,7 @@ if (BUILD_SUPPORTED) DFHACK_PLUGIN(mousequery mousequery.cpp) DFHACK_PLUGIN(autotrade autotrade.cpp) DFHACK_PLUGIN(stocks stocks.cpp) + DFHACK_PLUGIN(hotkeys hotkeys.cpp) DFHACK_PLUGIN(treefarm treefarm.cpp) DFHACK_PLUGIN(cleanconst cleanconst.cpp) DFHACK_PLUGIN(3dveins 3dveins.cpp) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp new file mode 100644 index 000000000..52c2e2eac --- /dev/null +++ b/plugins/hotkeys.cpp @@ -0,0 +1,378 @@ +#include "uicommon.h" + +#include "df/viewscreen_dwarfmodest.h" +#include "df/ui.h" + +#include "modules/Maps.h" +#include "modules/World.h" +#include "modules/Gui.h" + +#include "PluginManager.h" + +DFHACK_PLUGIN("hotkeys"); +#define PLUGIN_VERSION 0.1 + +static map current_bindings; +static vector sorted_keys; +static bool show_usage = false; + +static void send_key(const df::interface_key &key) +{ + set< df::interface_key > keys; + keys.insert(key); + Gui::getCurViewscreen(true)->feed(&keys); +} + +static bool can_invoke(string cmdline, df::viewscreen *screen) +{ + vector cmd_parts; + split_string(&cmd_parts, cmdline, " "); + if (toLower(cmd_parts[0]) == "hotkeys") + return false; + + return Core::getInstance().getPluginManager()->CanInvokeHotkey(cmd_parts[0], screen); +} + +static void add_binding_if_valid(string sym, string cmdline, df::viewscreen *screen) +{ + if (!can_invoke(cmdline, screen)) + return; + + current_bindings[sym] = cmdline; + sorted_keys.push_back(sym); + string keyspec = sym + "@dfhack/viewscreen_hotkeys"; + Core::getInstance().AddKeyBinding(keyspec, "hotkeys invoke " + int_to_string(sorted_keys.size() - 1)); +} + +static void find_active_keybindings(df::viewscreen *screen) +{ + current_bindings.clear(); + sorted_keys.clear(); + + vector valid_keys; + for (char c = 'A'; c <= 'Z'; c++) + { + valid_keys.push_back(string(&c, 1)); + } + + for (int i = 1; i < 10; i++) + { + valid_keys.push_back("F" + int_to_string(i)); + } + + auto current_focus = Gui::getFocusString(screen); + for (int shifted = 0; shifted < 2; shifted++) + { + for (int ctrl = 0; ctrl < 2; ctrl++) + { + for (int alt = 0; alt < 2; alt++) + { + for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) + { + string sym; + if (shifted) sym += "Shift-"; + if (ctrl) sym += "Ctrl-"; + if (alt) sym += "Alt-"; + sym += *it; + + auto list = Core::getInstance().ListKeyBindings(sym); + for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) + { + bool add_temp_binding = false; + if (invoke_cmd->find(":") == string::npos) + { + add_binding_if_valid(sym, *invoke_cmd, screen); + } + else + { + vector tokens; + split_string(&tokens, *invoke_cmd, ":"); + string focus = tokens[0].substr(1); + if (prefix_matches(focus, current_focus)) + { + auto cmdline = trim(tokens[1]); + add_binding_if_valid(sym, cmdline, screen); + } + } + } + } + } + } + } +} + +static bool close_hotkeys_screen() +{ + auto screen = Core::getTopViewscreen(); + if (Gui::getFocusString(screen) != "dfhack/viewscreen_hotkeys") + return false; + + Screen::dismiss(Core::getTopViewscreen()); + for_each_(sorted_keys, [] (const string &sym) + { Core::getInstance().ClearKeyBindings(sym + "@dfhack/viewscreen_hotkeys"); }); + sorted_keys.clear(); + return true; +} + + +static void invoke_command(const int index) +{ + if (sorted_keys.size() <= index) + return; + + auto cmd = current_bindings[sorted_keys[index]]; + if (close_hotkeys_screen()) + { + Core::getInstance().setHotkeyCmd(cmd); + } +} + +class ViewscreenHotkeys : public dfhack_viewscreen +{ +public: + ViewscreenHotkeys(df::viewscreen *top_screen) : top_screen(top_screen) + { + hotkeys_column.multiselect = false; + hotkeys_column.auto_select = true; + hotkeys_column.setTitle("Key Binding"); + hotkeys_column.bottom_margin = 4; + hotkeys_column.allow_search = false; + + focus = Gui::getFocusString(top_screen); + populateColumns(); + } + + void populateColumns() + { + hotkeys_column.clear(); + + int max_key_length = 0; + for_each_(sorted_keys, [&] (const string &sym) + { if (sym.length() > max_key_length) { max_key_length = sym.length(); } }); + int padding = max_key_length + 2; + + for (int i = 0; i < sorted_keys.size(); i++) + { + string text = pad_string(sorted_keys[i], padding, false); + text += current_bindings[sorted_keys[i]]; + hotkeys_column.add(text, i+1); + + } + + help_start = hotkeys_column.fixWidth() + 2; + hotkeys_column.filterDisplay(); + } + + void feed(set *input) + { + if (hotkeys_column.feed(input)) + return; + + if (input->count(interface_key::LEAVESCREEN)) + { + close_hotkeys_screen(); + } + else if (input->count(interface_key::SELECT)) + { + invoke_command(hotkeys_column.highlighted_index); + } + else if (input->count(interface_key::CUSTOM_U)) + { + show_usage = !show_usage; + } + } + + void render() + { + if (Screen::isDismissed(this)) + return; + + dfhack_viewscreen::render(); + + Screen::clear(); + Screen::drawBorder(" Hotkeys "); + + hotkeys_column.display(true); + + int32_t y = gps->dimy - 3; + int32_t x = 2; + OutputHotkeyString(x, y, "Leave", "Esc"); + + x += 3; + OutputHotkeyString(x, y, "Invoke", "Enter or Hotkey"); + + x += 3; + OutputToggleString(x, y, "Show Usage", "u", show_usage); + + y = gps->dimy - 4; + x = 2; + OutputHotkeyString(x, y, focus.c_str(), "Context", false, help_start, COLOR_WHITE, COLOR_BROWN); + + if (sorted_keys.size() == 0) + return; + + y = 2; + x = help_start; + + auto width = gps->dimx - help_start - 2; + vector parts; + Core::cheap_tokenise(current_bindings[sorted_keys[hotkeys_column.highlighted_index]], parts); + if(parts.size() == 0) + return; + + string first = parts[0]; + parts.erase(parts.begin()); + + if (first[0] == '#') + return; + + Plugin *plugin = Core::getInstance().getPluginManager()->getPluginByCommand(first); + if (plugin) + { + for (auto i = 0; i < plugin->size(); i++) + { + auto pc = plugin->operator[](i); + if (pc.name == first) + { + OutputString(COLOR_BROWN, x, y, "Help", true, help_start); + vector lines; + string help_text = pc.description; + if (show_usage) + help_text += "\n\n" + pc.usage; + + split_string(&lines, help_text, "\n"); + for (auto it = lines.begin(); it != lines.end() && y < gps->dimy - 4; it++) + { + auto wrapped_lines = wrapString(*it, width); + for (auto wit = wrapped_lines.begin(); wit != wrapped_lines.end() && y < gps->dimy - 4; wit++) + { + OutputString(COLOR_WHITE, x, y, *wit, true, help_start); + } + } + break; + } + } + } + } + + virtual std::string getFocusString() + { + return "viewscreen_hotkeys"; + } + +private: + ListColumn hotkeys_column; + df::viewscreen *top_screen; + string focus; + + int32_t help_start; + + void resize(int32_t x, int32_t y) + { + dfhack_viewscreen::resize(x, y); + hotkeys_column.resize(); + } + + static vector wrapString(string str, int width) + { + vector result; + string excess; + if (str.length() > width) + { + auto cut_space = str.rfind(' ', width-1); + int excess_start; + if (cut_space == string::npos) + { + cut_space = width-1; + excess_start = cut_space; + } + else + { + excess_start = cut_space + 1; + } + + string line = str.substr(0, cut_space); + excess = str.substr(excess_start); + result.push_back(line); + auto excess_lines = wrapString(excess, width); + result.insert(result.end(), excess_lines.begin(), excess_lines.end()); + } + else + { + result.push_back(str); + } + + return result; + } +}; + + +static command_result hotkeys_cmd(color_ostream &out, vector & parameters) +{ + bool show_help = false; + if (parameters.empty()) + { + if (Maps::IsValid()) + { + auto top_screen = Core::getTopViewscreen(); + if (Gui::getFocusString(top_screen) != "dfhack/viewscreen_hotkeys") + { + find_active_keybindings(top_screen); + Screen::show(new ViewscreenHotkeys(top_screen)); + } + } + } + else + { + auto cmd = parameters[0][0]; + if (cmd == 'v') + { + out << "Hotkeys" << endl << "Version: " << PLUGIN_VERSION << endl; + } + else if (cmd == 'i') + { + int index; + stringstream index_raw(parameters[1]); + index_raw >> index; + invoke_command(index); + } + else + { + return CR_WRONG_USAGE; + } + } + + return CR_OK; +} + + +DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +{ + if (!gps) + out.printerr("Could not insert hotkeys hooks!\n"); + + commands.push_back( + PluginCommand( + "hotkeys", "Shows ingame viewscreen with all dfhack keybindings active in current mode.", + hotkeys_cmd, false, "")); + + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) { + case SC_MAP_LOADED: + sorted_keys.clear(); + break; + default: + break; + } + + return CR_OK; +}