#include #include #include #include #include //#include "df/viewscreen_petst.h" #include "df/viewscreen_storesst.h" #include "df/viewscreen_layer_stockpilest.h" #include "df/viewscreen_tradegoodsst.h" #include "df/viewscreen_unitlistst.h" #include "df/interface_key.h" #include "df/interfacest.h" #include "df/layer_object_listst.h" using std::set; using std::vector; using std::string; using namespace DFHack; using namespace df::enums; using df::global::gps; using df::global::gview; /* Search Plugin A plugin that adds a "Search" hotkey to some screens (Units, Trade and Stocks) that allows filtering of the list items by a typed query. Works by manipulating the vector(s) that the list based viewscreens use to store their items. When a search is started the plugin saves the original vectors and with each keystroke creates a new filtered vector off the saves for the screen to use. */ void OutputString(int8_t color, int &x, int y, const std::string &text) { Screen::paintString(Screen::Pen(' ', color, 0), x, y, text); x += text.length(); } static bool is_live_screen(const df::viewscreen *screen) { for (df::viewscreen *cur = &gview->view; cur; cur = cur->child) if (cur == screen) return true; return false; } // // START: Base Search functionality // // Parent class that does most of the work template class search_parent { public: // Called each time you enter or leave a searchable screen. Resets everything. void reset_all() { reset_search(); valid = false; sort_list1 = NULL; sort_list2 = NULL; viewscreen = NULL; select_key = 's'; track_secondary_values = false; } bool reset_on_change() { if (valid && is_live_screen(viewscreen)) return false; reset_all(); return true; } bool is_valid() { return valid; } // A new keystroke is received in a searchable screen virtual bool process_input(set *input) { // If the page has two search options (Trade screen), only allow one to operate // at a time if (lock != NULL && lock != this) return false; // Allows custom preprocessing for each screen if (!should_check_input(input)) return false; bool key_processed = true; if (entry_mode) { // Query typing mode df::interface_key last_token = *input->rbegin(); if (last_token >= interface_key::STRING_A032 && last_token <= interface_key::STRING_A126) { // Standard character search_string += last_token - ascii_to_enum_offset; do_search(); } else if (last_token == interface_key::STRING_A000) { // Backspace if (search_string.length() > 0) { search_string.erase(search_string.length()-1); do_search(); } } else if (input->count(interface_key::SELECT) || input->count(interface_key::LEAVESCREEN)) { // ENTER or ESC: leave typing mode end_entry_mode(); } else if (input->count(interface_key::CURSOR_UP) || input->count(interface_key::CURSOR_DOWN) || input->count(interface_key::CURSOR_LEFT) || input->count(interface_key::CURSOR_RIGHT)) { // Arrow key pressed. Leave entry mode and allow screen to process key end_entry_mode(); key_processed = false; } } // Not in query typing mode else if (input->count(select_token)) { // Hotkey pressed, enter typing mode start_entry_mode(); } else if (input->count((df::interface_key) (select_token + shift_offset))) { // Shift + Hotkey pressed, clear query clear_search(); } else { // Not a key for us, pass it on to the screen key_processed = false; } return key_processed || entry_mode; // Only pass unrecognized keys down if not in typing mode } // Called if the search should be redone after the screen processes the keystroke. // Used by the stocks screen where changing categories should redo the search on // the new category. virtual void do_post_update_check() { if (redo_search) { do_search(); redo_search = false; } } static search_parent *lock; protected: const S *viewscreen; vector saved_list1, reference_list; vector saved_list2; vector *sort_list2; vector saved_indexes; bool redo_search; bool track_secondary_values; string search_string; search_parent() : ascii_to_enum_offset(interface_key::STRING_A048 - '0'), shift_offset('A' - 'a') { reset_all(); } virtual void init(int *cursor_pos, vector *sort_list1, vector *sort_list2 = NULL, char select_key = 's') { this->cursor_pos = cursor_pos; this->sort_list1 = sort_list1; this->sort_list2 = sort_list2; this->select_key = select_key; select_token = (df::interface_key) (ascii_to_enum_offset + select_key); track_secondary_values = false; valid = true; } bool is_entry_mode() { return entry_mode; } void start_entry_mode() { entry_mode = true; lock = this; } void end_entry_mode() { entry_mode = false; lock = NULL; } void reset_search() { end_entry_mode(); search_string = ""; saved_list1.clear(); saved_list2.clear(); reference_list.clear(); saved_indexes.clear(); } // If the second vector is editable (i.e. Trade screen vector used for marking). then it may // have been edited while the list was filtered. We have to update the original unfiltered // list with these values. Uses a stored reference vector to determine if the list has been // reordered after filtering, in which case indexes must be remapped. void update_secondary_values() { if (sort_list2 != NULL && track_secondary_values) { bool list_has_been_sorted = (sort_list1->size() == reference_list.size() && *sort_list1 != reference_list); for (size_t i = 0; i < saved_indexes.size(); i++) { int adjusted_item_index = i; if (list_has_been_sorted) { for (size_t j = 0; j < sort_list1->size(); j++) { if ((*sort_list1)[j] == reference_list[i]) { adjusted_item_index = j; break; } } } update_saved_secondary_list_item(saved_indexes[i], adjusted_item_index); } saved_indexes.clear(); } } virtual void update_saved_secondary_list_item(size_t i, size_t j) { saved_list2[i] = (*sort_list2)[j]; } // Store a copy of filtered list, used later to work out if filtered list has been sorted after filtering void store_reference_values() { if (track_secondary_values) reference_list = *sort_list1; } // Shortcut to clear the search immediately virtual void clear_search() { if (saved_list1.size() > 0) { *sort_list1 = saved_list1; if (sort_list2 != NULL) { update_secondary_values(); *sort_list2 = saved_list2; } saved_list1.clear(); saved_list2.clear(); } store_reference_values(); search_string = ""; } // The actual sort virtual void do_search() { if (search_string.length() == 0) { clear_search(); return; } if (saved_list1.size() == 0) { // On first run, save the original list saved_list1 = *sort_list1; if (sort_list2 != NULL) saved_list2 = *sort_list2; } else update_secondary_values(); // Update original list with any modified values // Clear viewscreen vectors sort_list1->clear(); if (sort_list2 != NULL) { sort_list2->clear(); saved_indexes.clear(); } string search_string_l = toLower(search_string); for (size_t i = 0; i < saved_list1.size(); i++ ) { T element = saved_list1[i]; string desc = toLower(get_element_description(element)); if (desc.find(search_string_l) != string::npos) { sort_list1->push_back(element); if (sort_list2 != NULL) { sort_list2->push_back(saved_list2[i]); if (track_secondary_values) saved_indexes.push_back(i); // Used to map filtered indexes back to original, if needed } } } store_reference_values(); //Keep a copy, in case user sorts new list if (cursor_pos) *cursor_pos = 0; } virtual bool should_check_input(set *input) { return true; } // Display hotkey message void print_search_option(int x, int y = -1) const { auto dim = Screen::getWindowSize(); if (y == -1) y = dim.y - 2; OutputString((entry_mode) ? 4 : 12, x, y, string(1, select_key)); OutputString((entry_mode) ? 10 : 15, x, y, ": Search"); if (search_string.length() > 0 || entry_mode) OutputString(15, x, y, ": " + search_string); if (entry_mode) OutputString(10, x, y, "_"); } virtual string get_element_description(T element) const = 0; virtual void render () const = 0; private: vector *sort_list1; int *cursor_pos; char select_key; bool valid; bool entry_mode; df::interface_key select_token; const int ascii_to_enum_offset; const int shift_offset; }; template search_parent *search_parent ::lock = NULL; // Parent struct for the hooks, use optional param D to generate multiple classes with same T & V // but different static modules template struct search_hook : T { typedef T interpose_base; static V module; DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) { if (!module.init(this)) { INTERPOSE_NEXT(feed)(input); return; } if (!module.process_input(input)) { INTERPOSE_NEXT(feed)(input); module.do_post_update_check(); } } DEFINE_VMETHOD_INTERPOSE(void, render, ()) { bool ok = module.init(this); INTERPOSE_NEXT(render)(); if (ok) module.render(); } }; template V search_hook ::module; // // END: Base Search functionality // // // START: Stocks screen search // class stocks_search : public search_parent { public: virtual void render() const { if (!viewscreen->in_group_mode) print_search_option(2); else { auto dim = Screen::getWindowSize(); int x = 2, y = dim.y - 2; OutputString(15, x, y, "Tab to enable Search"); } } virtual void do_post_update_check() { if (viewscreen->in_group_mode) { // Disable search if item lists are grouped clear_search(); reset_search(); } else search_parent::do_post_update_check(); } bool init(df::viewscreen_storesst *screen) { if (screen != viewscreen && !reset_on_change()) return false; if (!is_valid()) { viewscreen = screen; search_parent::init(&screen->item_cursor, &screen->items); } return true; } private: virtual string get_element_description(df::item *element) const { return Items::getDescription(element, 0, true); } virtual bool should_check_input(set *input) { if (viewscreen->in_group_mode) return false; if ((input->count(interface_key::CURSOR_UP) || input->count(interface_key::CURSOR_DOWN)) && !viewscreen->in_right_list) { // Redo search if category changes saved_list1.clear(); end_entry_mode(); if (search_string.length() > 0) redo_search = true; return false; } return true; } }; typedef search_hook stocks_search_hook; template<> IMPLEMENT_VMETHOD_INTERPOSE(stocks_search_hook, feed); template<> IMPLEMENT_VMETHOD_INTERPOSE(stocks_search_hook, render); // // END: Stocks screen search // // // START: Unit screen search // class unitlist_search : public search_parent { public: virtual void render() const { print_search_option(28); } bool init(df::viewscreen_unitlistst *screen) { if (screen != viewscreen && !reset_on_change()) return false; if (!is_valid()) { viewscreen = screen; search_parent::init(&screen->cursor_pos[viewscreen->page], &screen->units[viewscreen->page], &screen->jobs[viewscreen->page]); } return true; } private: virtual string get_element_description(df::unit *element) const { string desc = Translation::TranslateName(Units::getVisibleName(element), false); desc += ", " + Units::getProfessionName(element); // Check animal type too return desc; } virtual bool should_check_input(set *input) { if (input->count(interface_key::CURSOR_LEFT) || input->count(interface_key::CURSOR_RIGHT) || (!is_entry_mode() && input->count(interface_key::UNITVIEW_PRF_PROF))) { if (!is_entry_mode()) { // Changing screens, reset search clear_search(); reset_all(); } else input->clear(); // Ignore cursor keys when typing return false; } return true; } }; typedef search_hook unitlist_search_hook; template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(unitlist_search_hook, feed, 100); template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(unitlist_search_hook, render, 100); // // END: Unit screen search // // // TODO: Animals screen search // // // END: Animals screen search // // // START: Trade screen search // class trade_search_base : public search_parent { private: virtual string get_element_description(df::item *element) const { return Items::getDescription(element, 0, true); } virtual bool should_check_input(set *input) { if (is_entry_mode()) return true; if (input->count(interface_key::TRADE_TRADE) || input->count(interface_key::TRADE_OFFER) || input->count(interface_key::TRADE_SEIZE)) { // Block the keys if were searching if (!search_string.empty()) input->clear(); // Trying to trade, reset search clear_search(); reset_all(); return false; } return true; } }; class trade_search_merc : public trade_search_base { public: virtual void render() const { print_search_option(2, 26); } bool init(df::viewscreen_tradegoodsst *screen) { if (screen != viewscreen && !reset_on_change()) return false; if (!is_valid()) { viewscreen = screen; search_parent::init(&screen->trader_cursor, &screen->trader_items, &screen->trader_selected, 'q'); track_secondary_values = true; } return true; } }; typedef search_hook trade_search_merc_hook; template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_merc_hook, feed); template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_merc_hook, render); class trade_search_fort : public trade_search_base { public: virtual void render() const { print_search_option(42, 26); } bool init(df::viewscreen_tradegoodsst *screen) { if (screen != viewscreen && !reset_on_change()) return false; if (!is_valid()) { viewscreen = screen; search_parent::init(&screen->broker_cursor, &screen->broker_items, &screen->broker_selected, 'w'); track_secondary_values = true; } return true; } }; typedef search_hook trade_search_fort_hook; template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_fort_hook, feed); template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_fort_hook, render); // // END: Trade screen search // // // START: Stockpile screen search // class stockpile_search : public search_parent { public: void update_saved_secondary_list_item(size_t i, size_t j) { *saved_list2[i] = *(*sort_list2)[j]; } string get_element_description(string *element) const { return *element; } void render() const { print_search_option(2, 25); } static df::layer_object_listst *getLayerList(const df::viewscreen_layer *layer, int idx) { return virtual_cast(vector_get(layer->layer_objects,idx)); } bool init(df::viewscreen_layer_stockpilest *screen) { if (screen != viewscreen && !reset_on_change()) return false; auto list3 = getLayerList(screen, 2); if (!list3->active) { if (is_valid()) { clear_search(); reset_all(); } return false; } if (!is_valid()) { viewscreen = screen; search_parent::init(&list3->cursor, &screen->item_names, &screen->item_status); track_secondary_values = true; } return true; } void do_search() { search_parent::do_search(); auto list3 = getLayerList(viewscreen, 2); list3->num_entries = viewscreen->item_names.size(); } void clear_search() { search_parent::clear_search(); auto list3 = getLayerList(viewscreen, 2); list3->num_entries = viewscreen->item_names.size(); } }; typedef search_hook stockpile_search_hook; template<> IMPLEMENT_VMETHOD_INTERPOSE(stockpile_search_hook, feed); template<> IMPLEMENT_VMETHOD_INTERPOSE(stockpile_search_hook, render); // // END: Stockpile screen search // DFHACK_PLUGIN("search"); DFhackCExport command_result plugin_init ( color_ostream &out, vector &commands) { if (!gps || !gview || !INTERPOSE_HOOK(unitlist_search_hook, feed).apply() || !INTERPOSE_HOOK(unitlist_search_hook, render).apply() || !INTERPOSE_HOOK(trade_search_merc_hook, feed).apply() || !INTERPOSE_HOOK(trade_search_merc_hook, render).apply() || !INTERPOSE_HOOK(trade_search_fort_hook, feed).apply() || !INTERPOSE_HOOK(trade_search_fort_hook, render).apply() || !INTERPOSE_HOOK(stocks_search_hook, feed).apply() || !INTERPOSE_HOOK(stocks_search_hook, render).apply() || !INTERPOSE_HOOK(stockpile_search_hook, feed).apply() || !INTERPOSE_HOOK(stockpile_search_hook, render).apply()) out.printerr("Could not insert Search hooks!\n"); return CR_OK; } DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { INTERPOSE_HOOK(unitlist_search_hook, feed).remove(); INTERPOSE_HOOK(unitlist_search_hook, render).remove(); INTERPOSE_HOOK(trade_search_merc_hook, feed).remove(); INTERPOSE_HOOK(trade_search_merc_hook, render).remove(); INTERPOSE_HOOK(trade_search_fort_hook, feed).remove(); INTERPOSE_HOOK(trade_search_fort_hook, render).remove(); INTERPOSE_HOOK(stocks_search_hook, feed).remove(); INTERPOSE_HOOK(stocks_search_hook, render).remove(); INTERPOSE_HOOK(stockpile_search_hook, feed).remove(); INTERPOSE_HOOK(stockpile_search_hook, render).remove(); return CR_OK; } DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_change_event event ) { switch (event) { case SC_VIEWSCREEN_CHANGED: unitlist_search_hook::module.reset_on_change(); trade_search_merc_hook::module.reset_on_change(); trade_search_fort_hook::module.reset_on_change(); stocks_search_hook::module.reset_on_change(); stockpile_search_hook::module.reset_on_change(); break; default: break; } return CR_OK; }