From 4afe74efa6af69a9c6979722a9696ae1d204ba60 Mon Sep 17 00:00:00 2001 From: Anuradha Dissanayake Date: Sat, 16 Feb 2013 12:10:47 +1300 Subject: [PATCH] Add DwarfMonitor plugin to monitor activities and happiness levels in a fort. --- plugins/CMakeLists.txt | 1 + plugins/buildingplan.cpp | 487 +------------------ plugins/dwarfmonitor.cpp | 987 +++++++++++++++++++++++++++++++++++++++ plugins/uicommon.h | 531 +++++++++++++++++++++ 4 files changed, 1528 insertions(+), 478 deletions(-) create mode 100644 plugins/dwarfmonitor.cpp create mode 100644 plugins/uicommon.h diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index e1410116b..cdc0ef53d 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -131,6 +131,7 @@ if (BUILD_SUPPORTED) DFHACK_PLUGIN(misery misery.cpp) DFHACK_PLUGIN(buildingplan buildingplan.cpp) DFHACK_PLUGIN(resume resume.cpp) + DFHACK_PLUGIN(dwarfmonitor dwarfmonitor.cpp) endif() diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp index c3a1f3867..ad00490b4 100644 --- a/plugins/buildingplan.cpp +++ b/plugins/buildingplan.cpp @@ -1,14 +1,6 @@ -#include -#include -#include -#include - -#include "Core.h" -#include -#include -#include -#include +#include "uicommon.h" +#include // DF data structure definition headers #include "DataDefs.h" @@ -24,11 +16,9 @@ #include "df/job.h" #include "df/world.h" #include "df/building_constructionst.h" -#include "df/enabler.h" #include "df/building_design.h" #include "modules/Gui.h" -#include "modules/Screen.h" #include "modules/Buildings.h" #include "modules/Maps.h" #include "modules/Items.h" @@ -41,21 +31,12 @@ #include "df/building_design.h" #include "df/buildings_other_id.h" #include "modules/World.h" -#include #include "df/building.h" -using std::map; -using std::string; -using std::vector; - -using namespace DFHack; -using namespace df::enums; - using df::global::gps; using df::global::ui; using df::global::ui_build_selector; using df::global::world; -using df::global::enabler; DFHACK_PLUGIN("buildingplan"); #define PLUGIN_VERSION 0.5 @@ -82,41 +63,6 @@ struct MaterialDescriptor } }; -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) -{ - return CR_OK; -} - -void OutputString(int8_t color, int &x, int &y, const std::string &text, bool newline = false, int left_margin = 0) -{ - Screen::paintString(Screen::Pen(' ', color, 0), x, y, text); - if (newline) - { - ++y; - x = left_margin; - } - else - x += text.length(); -} - -void OutputHotkeyString(int &x, int &y, const char *text, const char *hotkey, bool newline = false, int left_margin = 0, int8_t color = COLOR_WHITE) -{ - OutputString(10, x, y, hotkey); - string display(": "); - display.append(text); - OutputString(color, x, y, display, newline, left_margin); -} - -void OutputToggleString(int &x, int &y, const char *text, const char *hotkey, bool state, bool newline = true, int left_margin = 0, int8_t color = COLOR_WHITE) -{ - OutputHotkeyString(x, y, text, hotkey); - OutputString(COLOR_WHITE, x, y, ": "); - if (state) - OutputString(COLOR_GREEN, x, y, "Enabled", newline, left_margin); - else - OutputString(COLOR_GREY, x, y, "Disabled", newline, left_margin); -} - struct coord32_t { int32_t x, y, z; @@ -127,429 +73,14 @@ struct coord32_t } }; -typedef int8_t UIColor; - -const int ascii_to_enum_offset = interface_key::STRING_A048 - '0'; - -inline string int_to_string(const int n) -{ - return static_cast( &(ostringstream() << n) )->str(); -} - -static void set_to_limit(int &value, const int maximum, const int min = 0) -{ - if (value < min) - value = min; - else if (value > maximum) - value = maximum; -} - -inline void paint_text(const UIColor color, const int &x, const int &y, const std::string &text, const UIColor background = 0) -{ - Screen::paintString(Screen::Pen(' ', color, background), x, y, text); -} - -static string pad_string(string text, const int size, const bool front = true, const bool trim = false) -{ - if (text.length() > size) - { - if (trim && size > 10) - { - text = text.substr(0, size-3); - text.append("..."); - } - return text; - } - - string aligned(size - text.length(), ' '); - if (front) - { - aligned.append(text); - return aligned; - } - else - { - text.append(aligned); - return text; - } -} - -template -static void for_each_(vector &v, Fn func) -{ - for_each(v.begin(), v.end(), func); -} - -template -static void for_each_(map &v, Fn func) -{ - for_each(v.begin(), v.end(), func); -} - -template -static void transform_(vector &src, vector &dst, Fn func) +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { - transform(src.begin(), src.end(), back_inserter(dst), func); + return CR_OK; } - #define MAX_MASK 10 #define MAX_MATERIAL 21 - #define SIDEBAR_WIDTH 30 -#define COLOR_TITLE COLOR_BLUE -#define COLOR_UNSELECTED COLOR_GREY -#define COLOR_SELECTED COLOR_WHITE -#define COLOR_HIGHLIGHTED COLOR_GREEN - - -/* - * List classes - */ -template -class ListEntry -{ -public: - T elem; - string text; - bool selected; - - ListEntry(string text, T elem) - { - this->text = text; - this->elem = elem; - selected = false; - } -}; - -template -class ListColumn -{ -public: - string title; - int highlighted_index; - int display_start_offset; - int32_t bottom_margin, search_margin, left_margin; - bool multiselect; - bool allow_null; - bool auto_select; - bool force_sort; - bool allow_search; - - ListColumn() - { - clear(); - left_margin = 2; - bottom_margin = 3; - search_margin = 63; - highlighted_index = 0; - multiselect = false; - allow_null = true; - auto_select = false; - force_sort = false; - allow_search = true; - } - - void clear() - { - list.clear(); - display_list.clear(); - display_start_offset = 0; - max_item_width = 0; - resize(); - } - - void resize() - { - display_max_rows = gps->dimy - 4 - bottom_margin; - } - - void add(ListEntry &entry) - { - list.push_back(entry); - if (entry.text.length() > max_item_width) - max_item_width = entry.text.length(); - } - - void add(const string &text, T &elem) - { - list.push_back(ListEntry(text, elem)); - if (text.length() > max_item_width) - max_item_width = text.length(); - } - - virtual void display_extras(const T &elem, int32_t &x, int32_t &y) const {} - - void display(const bool is_selected_column) const - { - int32_t y = 2; - paint_text(COLOR_TITLE, left_margin, y, title); - - int last_index_able_to_display = display_start_offset + display_max_rows; - for (int i = display_start_offset; i < display_list.size() && i < last_index_able_to_display; i++) - { - ++y; - UIColor fg_color = (display_list[i]->selected) ? COLOR_SELECTED : COLOR_UNSELECTED; - UIColor bg_color = (is_selected_column && i == highlighted_index) ? COLOR_HIGHLIGHTED : COLOR_BLACK; - paint_text(fg_color, left_margin, y, display_list[i]->text, bg_color); - int x = left_margin + display_list[i]->text.length() + 1; - display_extras(display_list[i]->elem, x, y); - } - - if (is_selected_column && allow_search) - { - y = gps->dimy - bottom_margin; - int32_t x = search_margin; - OutputHotkeyString(x, y, "Search" ,"S"); - OutputString(COLOR_WHITE, x, y, ": "); - OutputString(COLOR_WHITE, x, y, search_string); - OutputString(COLOR_LIGHTGREEN, x, y, "_"); - } - } - - void filterDisplay() - { - ListEntry *prev_selected = (getDisplayListSize() > 0) ? display_list[highlighted_index] : NULL; - display_list.clear(); - for (size_t i = 0; i < list.size(); i++) - { - if (search_string.empty() || list[i].text.find(search_string) != string::npos) - { - ListEntry *entry = &list[i]; - display_list.push_back(entry); - if (entry == prev_selected) - highlighted_index = display_list.size() - 1; - } - } - changeHighlight(0); - } - - void selectDefaultEntry() - { - for (size_t i = 0; i < display_list.size(); i++) - { - if (display_list[i]->selected) - { - highlighted_index = i; - break; - } - } - } - - void validateHighlight() - { - set_to_limit(highlighted_index, display_list.size() - 1); - - if (highlighted_index < display_start_offset) - display_start_offset = highlighted_index; - else if (highlighted_index >= display_start_offset + display_max_rows) - display_start_offset = highlighted_index - display_max_rows + 1; - - if (auto_select || (!allow_null && list.size() == 1)) - display_list[highlighted_index]->selected = true; - } - - void changeHighlight(const int highlight_change, const int offset_shift = 0) - { - if (!initHighlightChange()) - return; - - highlighted_index += highlight_change + offset_shift * display_max_rows; - - display_start_offset += offset_shift * display_max_rows; - set_to_limit(display_start_offset, max(0, (int)(display_list.size())-display_max_rows)); - validateHighlight(); - } - - void setHighlight(const int index) - { - if (!initHighlightChange()) - return; - - highlighted_index = index; - validateHighlight(); - } - - bool initHighlightChange() - { - if (display_list.size() == 0) - return false; - - if (auto_select && !multiselect) - { - for (typename vector< ListEntry >::iterator it = list.begin(); it != list.end(); it++) - { - it->selected = false; - } - } - - return true; - } - - void toggleHighlighted() - { - if (auto_select) - return; - - ListEntry *entry = display_list[highlighted_index]; - if (!multiselect || !allow_null) - { - int selected_count = 0; - for (size_t i = 0; i < list.size(); i++) - { - if (!multiselect && !entry->selected) - list[i].selected = false; - if (!allow_null && list[i].selected) - selected_count++; - } - - if (!allow_null && entry->selected && selected_count == 1) - return; - } - - entry->selected = !entry->selected; - } - - vector getSelectedElems(bool only_one = false) - { - vector results; - for (auto it = list.begin(); it != list.end(); it++) - { - if ((*it).selected) - { - results.push_back(&(it->elem)); - if (only_one) - break; - } - } - - return results; - } - - void clear_selection() - { - for_each_(list, [] (ListEntry &e) { e.selected = false; }); - } - - T* getFirstSelectedElem() - { - vector results = getSelectedElems(true); - if (results.size() == 0) - return NULL; - else - return results[0]; - } - - size_t getDisplayListSize() - { - return display_list.size(); - } - - size_t getBaseListSize() - { - return list.size(); - } - - bool feed(set *input) - { - if (input->count(interface_key::CURSOR_UP)) - { - changeHighlight(-1); - } - else if (input->count(interface_key::CURSOR_DOWN)) - { - changeHighlight(1); - } - else if (input->count(interface_key::STANDARDSCROLL_PAGEUP)) - { - changeHighlight(0, -1); - } - else if (input->count(interface_key::STANDARDSCROLL_PAGEDOWN)) - { - changeHighlight(0, 1); - } - else if (input->count(interface_key::SELECT) && !auto_select) - { - toggleHighlighted(); - } - else if (input->count(interface_key::CUSTOM_SHIFT_S)) - { - search_string.clear(); - filterDisplay(); - } - else if (enabler->tracking_on && gps->mouse_x != -1 && gps->mouse_y != -1 && enabler->mouse_lbut) - { - return setHighlightByMouse(); - } - else if (allow_search) - { - // Search query typing mode always on - - df::interface_key last_token = *input->rbegin(); - if (last_token >= interface_key::STRING_A096 && last_token <= interface_key::STRING_A123) - { - // Standard character - search_string += last_token - ascii_to_enum_offset; - filterDisplay(); - } - else if (last_token == interface_key::STRING_A000) - { - // Backspace - if (search_string.length() > 0) - { - search_string.erase(search_string.length()-1); - filterDisplay(); - } - } - else - { - return false; - } - - return true; - } - else - { - return false; - } - - return true; - } - - bool setHighlightByMouse() - { - if (gps->mouse_y >= 3 && gps->mouse_y < display_max_rows + 3 && - gps->mouse_x >= left_margin && gps->mouse_x < left_margin + max_item_width) - { - int new_index = display_start_offset + gps->mouse_y - 3; - if (new_index < display_list.size()) - setHighlight(new_index); - - enabler->mouse_lbut = enabler->mouse_rbut = 0; - - return true; - } - - return false; - } - - void sort() - { - if (force_sort || list.size() < 100) - std::sort(list.begin(), list.end(), - [] (ListEntry const& a, ListEntry const& b) { return a.text.compare(b.text) < 0; }); - - filterDisplay(); - } - - - private: - vector> list; - vector*> display_list; - string search_string; - int display_max_rows; - int max_item_width; -}; /* @@ -688,12 +219,12 @@ public: ViewscreenChooseMaterial(ItemFilter *filter) { selected_column = 0; - masks_column.title = "Type"; + masks_column.setTitle("Type"); masks_column.multiselect = true; masks_column.allow_search = false; masks_column.left_margin = 2; materials_column.left_margin = MAX_MASK + 3; - materials_column.title = "Material"; + materials_column.setTitle("Material"); materials_column.multiselect = true; this->filter = filter; @@ -709,7 +240,7 @@ public: void feed(set *input) { - bool key_processed; + bool key_processed = false; switch (selected_column) { case 0: @@ -734,8 +265,8 @@ public: if (input->count(interface_key::CUSTOM_SHIFT_C)) { filter->clear(); - masks_column.clear_selection(); - materials_column.clear_selection(); + masks_column.clearSelection(); + materials_column.clearSelection(); populateMaterials(); } else if (input->count(interface_key::SEC_SELECT)) diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp new file mode 100644 index 000000000..95b576828 --- /dev/null +++ b/plugins/dwarfmonitor.cpp @@ -0,0 +1,987 @@ +#include "uicommon.h" + +#include "DataDefs.h" +#include "MiscUtils.h" + +#include "df/job.h" +#include "df/ui.h" +#include "df/unit.h" +#include "df/viewscreen_dwarfmodest.h" +#include "df/world.h" +#include "df/misc_trait_type.h" +#include "df/unit_misc_trait.h" + +#include "modules/Gui.h" +#include "modules/Units.h" +#include "modules/Translation.h" +#include "modules/World.h" +#include "modules/Maps.h" + +using std::deque; + +using df::global::gps; +using df::global::world; +using df::global::ui; + +typedef int16_t activity_type; + +#define PLUGIN_VERSION 0.2 +#define DAY_TICKS 1200 +#define DELTA_TICKS 100 + +int max_history_days = 7; + +template +struct less_second { + typedef pair type; + bool operator ()(type const& a, type const& b) const { + return a.second > b.second; + } +}; + +static bool monitor_jobs = false; +static bool monitor_misery = false; +static map> work_history; + +static int misery[] = { 0, 0, 0, 0, 0, 0, 0 }; +static bool misery_upto_date = false; + +static int get_max_history() +{ + return (DAY_TICKS / DELTA_TICKS) * max_history_days; +} + +static int getPercentage(const int n, const int d) +{ + return static_cast( + static_cast(n) / static_cast(d) * 100.0); +} + +static string getUnitName(df::unit * unit) +{ + string label = ""; + auto name = Units::getVisibleName(unit); + if (name->has_name) + label = Translation::TranslateName(name, false); + + return label; +} + +static void send_key(const df::interface_key &key) +{ + set< df::interface_key > keys; + keys.insert(key); + Gui::getCurViewscreen(true)->feed(&keys); +} + +static void move_cursor(df::coord &pos) +{ + Gui::setCursorCoords(pos.x, pos.y, pos.z); + send_key(interface_key::CURSOR_DOWN_Z); + send_key(interface_key::CURSOR_UP_Z); +} + +static void open_stats_srceen(); + +#define JOB_UNKNOWN -2 +#define JOB_IDLE -1 +#define JOB_MILITARY -3 +#define JOB_LEISURE -4 +#define JOB_OTHER -5 +#define JOB_DESIG_SLOPE -6 +#define JOB_STORE_ITEM -7 +#define JOB_CONSTRUCT_FURNITURE -8 +#define JOB_DETAILING -9 +#define JOB_HUNTING -10 +#define JOB_CLOTING -11 +#define JOB_ARMOUR -12 +#define JOB_WEAPONS -13 +#define JOB_MEDICAL -14 + +static string getActivityLabel(const activity_type & activity) +{ + string label; + switch (activity) + { + case JOB_IDLE: + label = "Idle"; + break; + case JOB_MILITARY: + label = "Military Duty"; + break; + case JOB_LEISURE: + label = "Leisure"; + break; + case JOB_OTHER: + label = "Other"; + break; + case JOB_DESIG_SLOPE: + label = "Designate Stair/Slope"; + break; + case JOB_STORE_ITEM: + label = "Store Item"; + break; + case JOB_CONSTRUCT_FURNITURE: + label = "Construct Furniture"; + break; + case JOB_DETAILING: + label = "Detailing"; + break; + case JOB_HUNTING: + label = "Hunting"; + break; + case JOB_CLOTING: + label = "Make Clothing"; + break; + case JOB_ARMOUR: + label = "Make Armor"; + break; + case JOB_WEAPONS: + label = "Make Weapons"; + break; + case JOB_MEDICAL: + label = "Medical"; + break; + + default: + string raw_label = enum_item_key_str(static_cast(activity)); + for (auto c = raw_label.begin(); c != raw_label.end(); c++) + { + if (label.length() > 0 && *c >= 'A' && *c <= 'Z') + label += ' '; + + label += *c; + } + + break; + } + + return label; +} + +class ViewscreenDwarfStats : public dfhack_viewscreen +{ +public: + ViewscreenDwarfStats(df::unit *starting_selection) : selected_column(0) + { + dwarves_column.multiselect = false; + dwarves_column.auto_select = true; + dwarves_column.setTitle("Dwarves"); + + dwarf_activity_column.multiselect = false; + dwarf_activity_column.auto_select = true; + dwarf_activity_column.setTitle("Dwarf Activity"); + + for (auto it = work_history.begin(); it != work_history.end();) + { + auto unit = it->first; + if (Units::isDead(unit)) + { + work_history.erase(it++); + continue; + } + + deque *work_list = &it->second; + ++it; + + size_t dwarf_total = 0; + dwarf_activity_values[unit] = map(); + for (auto entry = work_list->begin(); entry != work_list->end(); entry++) + { + if (*entry == JOB_UNKNOWN || *entry == job_type::DrinkBlood) + continue; + + ++dwarf_total; + addDwarfActivity(unit, *entry); + } + + for_each_(dwarf_activity_values[unit], + [&] (const pair &x) + { dwarf_activity_values[unit][x.first] = getPercentage(x.second, dwarf_total); } ); + + dwarves_column.add(getUnitName(unit), *unit); + } + + dwarf_activity_column.left_margin = dwarves_column.fixWidth() + 2; + dwarves_column.filterDisplay(); + dwarves_column.selectItem(starting_selection); + populateActivityColumn(); + } + + void addDwarfActivity(df::unit *unit, const activity_type &activity) + { + if (dwarf_activity_values[unit].find(activity) == dwarf_activity_values[unit].end()) + dwarf_activity_values[unit][activity] = 0; + + dwarf_activity_values[unit][activity]++; + } + + void populateActivityColumn() + { + dwarf_activity_column.clear(); + if (dwarves_column.getDisplayedListSize() == 0) + return; + + auto unit = dwarves_column.getFirstSelectedElem(); + if (dwarf_activity_values.find(unit) == dwarf_activity_values.end()) + return; + + auto dwarf_activities = &dwarf_activity_values[unit]; + if (dwarf_activities) + { + vector> rev_vec(dwarf_activities->begin(), dwarf_activities->end()); + sort(rev_vec.begin(), rev_vec.end(), less_second()); + + for_each_(rev_vec, + [&] (pair x) + { dwarf_activity_column.add(getActivityItem(x.first, x.second), x.first); }); + } + + dwarf_activity_column.fixWidth(); + dwarf_activity_column.clearSearch(); + dwarf_activity_column.setHighlight(0); + } + + string getActivityItem(activity_type activity, size_t value) + { + return pad_string(int_to_string(value), 3) + " " + getActivityLabel(activity); + } + + void feed(set *input) + { + bool key_processed = false; + switch (selected_column) + { + case 0: + key_processed = dwarves_column.feed(input); + break; + case 1: + key_processed = dwarf_activity_column.feed(input); + break; + } + + if (key_processed) + { + if (selected_column == 0 && dwarves_column.feed_changed_highlight) + populateActivityColumn(); + + return; + } + + if (input->count(interface_key::LEAVESCREEN)) + { + input->clear(); + Screen::dismiss(this); + return; + } + else if (input->count(interface_key::CUSTOM_SHIFT_D)) + { + Screen::dismiss(this); + open_stats_srceen(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_Z)) + { + df::unit *selected_unit = (selected_column == 0) ? dwarves_column.getFirstSelectedElem() : nullptr; + if (selected_unit) + { + input->clear(); + Screen::dismiss(this); + Gui::resetDwarfmodeView(true); + send_key(interface_key::D_VIEWUNIT); + move_cursor(selected_unit->pos); + } + } + else if (input->count(interface_key::CURSOR_LEFT)) + { + --selected_column; + validateColumn(); + } + else if (input->count(interface_key::CURSOR_RIGHT)) + { + ++selected_column; + validateColumn(); + } + else if (enabler->tracking_on && enabler->mouse_lbut) + { + if (dwarves_column.setHighlightByMouse()) + { + selected_column = 0; + populateActivityColumn(); + } + else if (dwarf_activity_column.setHighlightByMouse()) + selected_column = 1; + + enabler->mouse_lbut = enabler->mouse_rbut = 0; + } + } + + void render() + { + if (Screen::isDismissed(this)) + return; + + dfhack_viewscreen::render(); + + Screen::clear(); + Screen::drawBorder(" Dwarf Activity "); + + dwarves_column.display(selected_column == 0); + dwarf_activity_column.display(selected_column == 1); + + int32_t y = gps->dimy - 3; + int32_t x = 2; + OutputHotkeyString(x, y, "Leave", "Esc"); + x += 3; + OutputHotkeyString(x, y, "Fort Stats", "Shift-D"); + x += 3; + OutputHotkeyString(x, y, "Zoom Unit", "Shift-Z"); + } + + std::string getFocusString() { return "dwarfmonitor_dwarfstats"; } + +private: + ListColumn dwarves_column; + ListColumn dwarf_activity_column; + int selected_column; + + map> dwarf_activity_values; + + void validateColumn() + { + set_to_limit(selected_column, 1); + } + + void resize(int32_t x, int32_t y) + { + dfhack_viewscreen::resize(x, y); + dwarves_column.resize(); + dwarf_activity_column.resize(); + } +}; + + +class ViewscreenFortStats : public dfhack_viewscreen +{ +public: + ViewscreenFortStats() : selected_column(0), fort_activity_count(0) + { + fort_activity_column.multiselect = false; + fort_activity_column.auto_select = true; + fort_activity_column.setTitle("Fort Activities"); + + dwarf_activity_column.multiselect = false; + dwarf_activity_column.auto_select = true; + dwarf_activity_column.setTitle("Units on Activity"); + + for (auto it = work_history.begin(); it != work_history.end();) + { + auto unit = it->first; + if (Units::isDead(unit)) + { + work_history.erase(it++); + continue; + } + + deque *work_list = &it->second; + ++it; + + for (auto entry = work_list->begin(); entry != work_list->end(); entry++) + { + if (*entry == JOB_UNKNOWN) + continue; + + ++fort_activity_count; + + auto real_activity = *entry; + if (real_activity < 0) + { + addFortActivity(real_activity); + } + else + { + auto activity = static_cast(real_activity); + + switch (activity) + { + case job_type::Eat: + case job_type::Drink: + case job_type::Drink2: + case job_type::Sleep: + case job_type::AttendParty: + case job_type::Rest: + case job_type::CleanSelf: + case job_type::DrinkBlood: + real_activity = JOB_LEISURE; + break; + + case job_type::Kidnap: + case job_type::StartingFistFight: + case job_type::SeekInfant: + case job_type::SeekArtifact: + case job_type::GoShopping: + case job_type::GoShopping2: + case job_type::RecoverPet: + case job_type::CauseTrouble: + case job_type::ReportCrime: + case job_type::BeatCriminal: + real_activity = JOB_OTHER; + break; + + case job_type::CarveUpwardStaircase: + case job_type::CarveDownwardStaircase: + case job_type::CarveUpDownStaircase: + case job_type::CarveRamp: + case job_type::DigChannel: + real_activity = JOB_DESIG_SLOPE; + break; + + case job_type::StoreOwnedItem: + case job_type::PlaceItemInTomb: + case job_type::StoreItemInStockpile: + case job_type::StoreItemInBag: + case job_type::StoreItemInHospital: + case job_type::StoreItemInChest: + case job_type::StoreItemInCabinet: + case job_type::StoreWeapon: + case job_type::StoreArmor: + case job_type::StoreItemInBarrel: + case job_type::StoreItemInBin: + real_activity = JOB_STORE_ITEM; + break; + + case job_type::ConstructDoor: + case job_type::ConstructFloodgate: + case job_type::ConstructBed: + case job_type::ConstructThrone: + case job_type::ConstructCoffin: + case job_type::ConstructTable: + case job_type::ConstructChest: + case job_type::ConstructBin: + case job_type::ConstructArmorStand: + case job_type::ConstructWeaponRack: + case job_type::ConstructCabinet: + case job_type::ConstructStatue: + real_activity = JOB_CONSTRUCT_FURNITURE; + break; + + case job_type::DetailFloor: + case job_type::DetailWall: + real_activity = JOB_DETAILING; + break; + + case job_type::Hunt: + case job_type::ReturnKill: + case job_type::HuntVermin: + real_activity = JOB_HUNTING; + break; + + default: + break; + } + + addFortActivity(real_activity); + } + + if (dwarf_activity_values.find(real_activity) == dwarf_activity_values.end()) + dwarf_activity_values[real_activity] = map(); + + map &activity_for_dwarf = dwarf_activity_values[real_activity]; + if (activity_for_dwarf.find(unit) == activity_for_dwarf.end()) + activity_for_dwarf[unit] = 0; + + ++activity_for_dwarf[unit]; + } + } + + vector> rev_vec(fort_activity_totals.begin(), fort_activity_totals.end()); + sort(rev_vec.begin(), rev_vec.end(), less_second()); + + for (auto rev_it = rev_vec.begin(); rev_it != rev_vec.end(); rev_it++) + { + auto activity = rev_it->first; + addToFortAverageColumn(activity); + + for (auto it = dwarf_activity_values[activity].begin(); it != dwarf_activity_values[activity].end(); it++) + { + auto avg = getPercentage(it->second, getFortActivityCount(activity)); + dwarf_activity_values[activity][it->first] = avg; + } + } + + dwarf_activity_column.left_margin = fort_activity_column.fixWidth() + 2; + fort_activity_column.filterDisplay(); + populateDwarfColumn(); + } + + void addToFortAverageColumn(activity_type type) + { + if (getFortActivityCount(type)) + fort_activity_column.add(getFortAverage(type), dwarf_activity_values[type]); + } + + string getFortAverage(const activity_type &activity) + { + auto average = getPercentage(getFortActivityCount(activity), fort_activity_count); + auto label = getActivityLabel(activity); + auto result = pad_string(int_to_string(average), 3) + " " + label; + + return result; + } + + string getDwarfAverage(df::unit *unit, const size_t value) + { + auto label = getUnitName(unit); + auto result = pad_string(int_to_string(value), 3) + " " + label; + + return result; + } + + size_t getFortActivityCount(const activity_type &activity) + { + if (fort_activity_totals.find(activity) == fort_activity_totals.end()) + return 0; + + return fort_activity_totals[activity]; + } + + void addFortActivity(const activity_type &activity) + { + if (fort_activity_totals.find(activity) == fort_activity_totals.end()) + fort_activity_totals[activity] = 0; + + fort_activity_totals[activity]++; + } + + void populateDwarfColumn() + { + dwarf_activity_column.clear(); + if (fort_activity_column.getDisplayListSize() == 0) + return; + + auto dwarf_activities = fort_activity_column.getFirstSelectedElem(); + if (dwarf_activities) + { + vector> rev_vec(dwarf_activities->begin(), dwarf_activities->end()); + sort(rev_vec.begin(), rev_vec.end(), less_second()); + + for_each_(rev_vec, + [&] (pair x) + { dwarf_activity_column.add(getDwarfAverage(x.first, x.second), *x.first); }); + } + + dwarf_activity_column.fixWidth(); + dwarf_activity_column.clearSearch(); + dwarf_activity_column.setHighlight(0); + } + + void feed(set *input) + { + bool key_processed = false; + switch (selected_column) + { + case 0: + key_processed = fort_activity_column.feed(input); + break; + case 1: + key_processed = dwarf_activity_column.feed(input); + break; + } + + if (key_processed) + { + if (selected_column == 0 && fort_activity_column.feed_changed_highlight) + populateDwarfColumn(); + + return; + } + + if (input->count(interface_key::LEAVESCREEN)) + { + input->clear(); + Screen::dismiss(this); + return; + } + else if (input->count(interface_key::CUSTOM_SHIFT_D)) + { + df::unit *selected_unit = (selected_column == 1) ? dwarf_activity_column.getFirstSelectedElem() : nullptr; + Screen::dismiss(this); + Screen::show(new ViewscreenDwarfStats(selected_unit)); + } + else if (input->count(interface_key::CUSTOM_SHIFT_Z)) + { + df::unit *selected_unit = (selected_column == 1) ? dwarf_activity_column.getFirstSelectedElem() : nullptr; + if (selected_unit) + { + input->clear(); + Screen::dismiss(this); + Gui::resetDwarfmodeView(true); + send_key(interface_key::D_VIEWUNIT); + move_cursor(selected_unit->pos); + } + } + else if (input->count(interface_key::CURSOR_LEFT)) + { + --selected_column; + validateColumn(); + } + else if (input->count(interface_key::CURSOR_RIGHT)) + { + ++selected_column; + validateColumn(); + } + else if (enabler->tracking_on && enabler->mouse_lbut) + { + if (fort_activity_column.setHighlightByMouse()) + { + selected_column = 0; + populateDwarfColumn(); + } + else if (dwarf_activity_column.setHighlightByMouse()) + selected_column = 1; + + enabler->mouse_lbut = enabler->mouse_rbut = 0; + } + } + + void render() + { + if (Screen::isDismissed(this)) + return; + + dfhack_viewscreen::render(); + + Screen::clear(); + Screen::drawBorder(" Fortress Efficiency "); + + fort_activity_column.display(selected_column == 0); + dwarf_activity_column.display(selected_column == 1); + + int32_t y = gps->dimy - 3; + int32_t x = 2; + OutputHotkeyString(x, y, "Leave", "Esc"); + x += 3; + OutputHotkeyString(x, y, "Dwarf Stats", "Shift-D"); + x += 3; + OutputHotkeyString(x, y, "Zoom Unit", "Shift-Z"); + } + + std::string getFocusString() { return "dwarfmonitor_fortstats"; } + +private: + ListColumn> fort_activity_column; + ListColumn dwarf_activity_column; + int selected_column; + + map fort_activity_totals; + map> dwarf_activity_values; + size_t fort_activity_count; + + void validateColumn() + { + set_to_limit(selected_column, 1); + } + + void resize(int32_t x, int32_t y) + { + dfhack_viewscreen::resize(x, y); + fort_activity_column.resize(); + dwarf_activity_column.resize(); + } +}; + +static void open_stats_srceen() +{ + Screen::show(new ViewscreenFortStats()); +} + +static void add_work_history(df::unit *unit, activity_type type) +{ + if (work_history.find(unit) == work_history.end()) + { + auto max_history = get_max_history(); + for (int i = 0; i < max_history; i++) + work_history[unit].push_back(JOB_UNKNOWN); + } + + work_history[unit].push_back(type); + work_history[unit].pop_front(); +} + +static bool is_at_leisure(df::unit *unit) +{ + for (auto p = unit->status.misc_traits.begin(); p < unit->status.misc_traits.end(); p++) + { + if ((*p)->id == misc_trait_type::Migrant || (*p)->id == misc_trait_type::OnBreak) + return true; + } + + return false; +} + +static void reset() +{ + work_history.clear(); + + for (int i = 0; i < 7; i++) + misery[i] = 0; + + misery_upto_date = false; +} + +static void update_dwarf_stats(bool is_paused) +{ + if (monitor_misery) + { + for (int i = 0; i < 7; i++) + misery[i] = 0; + } + + for (auto iter = world->units.active.begin(); iter != world->units.active.end(); iter++) + { + df::unit* unit = *iter; + if (!Units::isCitizen(unit)) + continue; + + if (unit->profession == profession::BABY || + unit->profession == profession::CHILD || + unit->profession == profession::DRUNK) + { + continue; + } + + using namespace DFHack::Units; + if (!isSane(unit) || isDead(unit)) + { + auto it = work_history.find(unit); + if (it != work_history.end()) + work_history.erase(it); + + continue; + } + + if (monitor_misery) + { + int happy = unit->status.happiness; + if (happy == 0) // miserable + misery[0]++; + else if (happy <= 25) // very unhappy + misery[1]++; + else if (happy <= 50) // unhappy + misery[2]++; + else if (happy <= 75) // fine + misery[3]++; + else if (happy <= 125) // quite content + misery[4]++; + else if (happy <= 150) // happy + misery[5]++; + else // ecstatic + misery[6]++; + } + + if (!monitor_jobs || is_paused) + continue; + + if (ENUM_ATTR(profession, military, unit->profession)) + add_work_history(unit, JOB_MILITARY); + + if (!unit->job.current_job) + { + add_work_history(unit, JOB_IDLE); + continue; + } + + if (is_at_leisure(unit)) + { + add_work_history(unit, JOB_LEISURE); + continue; + } + + add_work_history(unit, unit->job.current_job->job_type); + } +} + +DFhackCExport command_result plugin_onupdate (color_ostream &out) +{ + if (!monitor_jobs && !monitor_misery) + return CR_OK; + + if(!Maps::IsValid()) + return CR_OK; + + static decltype(world->frame_counter) last_frame_count = 0; + + bool is_paused = Core::getInstance().getWorld()->ReadPauseState(); + if (is_paused) + { + if (monitor_misery && !misery_upto_date) + misery_upto_date = true; + else + return CR_OK; + } + else + { + if (world->frame_counter - last_frame_count < DELTA_TICKS) + return CR_OK; + + last_frame_count = world->frame_counter; + } + + update_dwarf_stats(is_paused); + + return CR_OK; +} + +struct dwarf_monitor_hook : public df::viewscreen_dwarfmodest +{ + typedef df::viewscreen_dwarfmodest interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + INTERPOSE_NEXT(feed)(input); + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + + if (monitor_misery && Maps::IsValid()) + { + int x = 1; + int y = gps->dimy - 1; + OutputString(COLOR_WHITE, x, y, "H:"); + OutputString(COLOR_LIGHTRED, x, y, int_to_string(misery[0])); + OutputString(COLOR_WHITE, x, y, "/"); + OutputString(COLOR_RED, x, y, int_to_string(misery[1])); + OutputString(COLOR_WHITE, x, y, "/"); + OutputString(COLOR_YELLOW, x, y, int_to_string(misery[2])); + OutputString(COLOR_WHITE, x, y, "/"); + OutputString(COLOR_WHITE, x, y, int_to_string(misery[3])); + OutputString(COLOR_WHITE, x, y, "/"); + OutputString(COLOR_CYAN, x, y, int_to_string(misery[4])); + OutputString(COLOR_WHITE, x, y, "/"); + OutputString(COLOR_LIGHTBLUE, x, y, int_to_string(misery[5])); + OutputString(COLOR_WHITE, x, y, "/"); + OutputString(COLOR_LIGHTGREEN, x, y, int_to_string(misery[6])); + } + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(dwarf_monitor_hook, feed); +IMPLEMENT_VMETHOD_INTERPOSE(dwarf_monitor_hook, render); + +DFHACK_PLUGIN("dwarfmonitor"); + +static bool set_monitoring_mode(const string &mode, const bool &state) +{ + bool mode_recognized = false; + + if (mode == "work" || mode == "all") + { + mode_recognized = true; + monitor_jobs = state; + if (!monitor_jobs) + reset(); + } + + if (mode == "misery" || mode == "all") + { + mode_recognized = true; + monitor_misery = state; + } + + return mode_recognized; +} + +static command_result dwarfmonitor_cmd(color_ostream &out, vector & parameters) +{ + bool show_help = false; + if (parameters.empty()) + { + show_help = true; + } + else + { + auto cmd = parameters[0][0]; + string mode; + + if (parameters.size() > 1) + mode = toLower(parameters[1]); + + if (cmd == 'v' || cmd == 'V') + { + out << "DwarfMonitor" << endl << "Version: " << PLUGIN_VERSION << endl; + } + else if ((cmd == 'e' || cmd == 'E') && !mode.empty()) + { + if (set_monitoring_mode(mode, true)) + { + out << "Monitoring enabled: " << mode << endl; + if (parameters.size() > 2) + { + int new_window = atoi(parameters[2].c_str()); + if (new_window > 0) + max_history_days = new_window; + } + } + else + { + show_help = true; + } + } + else if ((cmd == 'd' || cmd == 'D') && !mode.empty()) + { + if (set_monitoring_mode(mode, false)) + out << "Monitoring disabled: " << mode << endl; + else + show_help = true; + } + else if (cmd == 's' || cmd == 'S') + { + if(Maps::IsValid()) + Screen::show(new ViewscreenFortStats()); + } + else + { + show_help = true; + } + } + + if (show_help) + return CR_WRONG_USAGE; + + return CR_OK; +} + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) +{ + if (!gps || !INTERPOSE_HOOK(dwarf_monitor_hook, feed).apply() || !INTERPOSE_HOOK(dwarf_monitor_hook, render).apply()) + out.printerr("Could not insert dwarfmonitor hooks!\n"); + + commands.push_back( + PluginCommand( + "dwarfmonitor", "Records dwarf activity to measure fort efficiency", + dwarfmonitor_cmd, false, + "dwarfmonitor enable \n" + " Start monitoring [days]\n" + " can be \"work\", \"misery\", or \"all\"\n" + " [days] number of days to track (default is 7)\n\n" + "dwarfmonitor disable \n" + " as above\n\n" + "dwarfmonitor stats\n" + " Show statistics summary\n\n" + )); + + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) { + case SC_MAP_LOADED: + reset(); + break; + default: + break; + } + return CR_OK; +} diff --git a/plugins/uicommon.h b/plugins/uicommon.h new file mode 100644 index 000000000..d4d52df4a --- /dev/null +++ b/plugins/uicommon.h @@ -0,0 +1,531 @@ +#include +#include +#include +#include + +#include "Core.h" +#include +#include +#include +#include + +#include "modules/Screen.h" + +#include "df/enabler.h" + + +using std::string; +using std::vector; +using std::map; +using std::ostringstream; +using std::set; + +using namespace DFHack; +using namespace df::enums; + +using df::global::enabler; + + +#ifndef HAVE_NULLPTR +#define nullptr 0L +#endif + +#define COLOR_TITLE COLOR_BLUE +#define COLOR_UNSELECTED COLOR_GREY +#define COLOR_SELECTED COLOR_WHITE +#define COLOR_HIGHLIGHTED COLOR_GREEN + + +template +static void for_each_(vector &v, Fn func) +{ + for_each(v.begin(), v.end(), func); +} + +template +static void for_each_(map &v, Fn func) +{ + for_each(v.begin(), v.end(), func); +} + +template +static void transform_(vector &src, vector &dst, Fn func) +{ + transform(src.begin(), src.end(), back_inserter(dst), func); +} + +typedef int8_t UIColor; + +void OutputString(UIColor color, int &x, int &y, const std::string &text, + bool newline = false, int left_margin = 0, const UIColor bg_color = 0) +{ + Screen::paintString(Screen::Pen(' ', color, bg_color), x, y, text); + if (newline) + { + ++y; + x = left_margin; + } + else + x += text.length(); +} + +void OutputHotkeyString(int &x, int &y, const char *text, const char *hotkey, bool newline = false, int left_margin = 0, int8_t color = COLOR_WHITE) +{ + OutputString(10, x, y, hotkey); + string display(": "); + display.append(text); + OutputString(color, x, y, display, newline, left_margin); +} + +void OutputToggleString(int &x, int &y, const char *text, const char *hotkey, bool state, bool newline = true, int left_margin = 0, int8_t color = COLOR_WHITE) +{ + OutputHotkeyString(x, y, text, hotkey); + OutputString(COLOR_WHITE, x, y, ": "); + if (state) + OutputString(COLOR_GREEN, x, y, "Enabled", newline, left_margin); + else + OutputString(COLOR_GREY, x, y, "Disabled", newline, left_margin); +} + +const int ascii_to_enum_offset = interface_key::STRING_A048 - '0'; + +inline string int_to_string(const int n) +{ + return static_cast( &(ostringstream() << n) )->str(); +} + +static void set_to_limit(int &value, const int maximum, const int min = 0) +{ + if (value < min) + value = min; + else if (value > maximum) + value = maximum; +} + + +inline void paint_text(const UIColor color, const int &x, const int &y, const std::string &text, const UIColor background = 0) +{ + Screen::paintString(Screen::Pen(' ', color, background), x, y, text); +} + +static string pad_string(string text, const int size, const bool front = true, const bool trim = false) +{ + if (text.length() > size) + { + if (trim && size > 10) + { + text = text.substr(0, size-3); + text.append("..."); + } + return text; + } + + string aligned(size - text.length(), ' '); + if (front) + { + aligned.append(text); + return aligned; + } + else + { + text.append(aligned); + return text; + } +} + + +/* + * List classes + */ +template +class ListEntry +{ +public: + T *elem; + string text; + bool selected; + + ListEntry(string text, T &elem) : elem(&elem), text(text), selected(false) + { + } +}; + +template +class ListColumn +{ +public: + int highlighted_index; + int display_start_offset; + int32_t bottom_margin, search_margin, left_margin; + bool multiselect; + bool allow_null; + bool auto_select; + bool force_sort; + bool allow_search; + bool feed_changed_highlight; + + ListColumn() + { + bottom_margin = 3; + clear(); + left_margin = 2; + search_margin = 63; + highlighted_index = 0; + multiselect = false; + allow_null = true; + auto_select = false; + force_sort = false; + allow_search = true; + feed_changed_highlight = false; + } + + void clear() + { + list.clear(); + display_list.clear(); + display_start_offset = 0; + max_item_width = 0; + resize(); + } + + void resize() + { + display_max_rows = gps->dimy - 4 - bottom_margin; + } + + void add(ListEntry &entry) + { + list.push_back(entry); + if (entry.text.length() > max_item_width) + max_item_width = entry.text.length(); + } + + void add(const string &text, T &elem) + { + list.push_back(ListEntry(text, elem)); + if (text.length() > max_item_width) + max_item_width = text.length(); + } + + int fixWidth() + { + for (auto it = list.begin(); it != list.end(); it++) + { + it->text = pad_string(it->text, max_item_width, false); + } + + return left_margin + max_item_width; + } + + virtual void display_extras(const T &elem, int32_t &x, int32_t &y) const {} + + void display(const bool is_selected_column) const + { + int32_t y = 2; + paint_text(COLOR_TITLE, left_margin, y, title); + + int last_index_able_to_display = display_start_offset + display_max_rows; + for (int i = display_start_offset; i < display_list.size() && i < last_index_able_to_display; i++) + { + ++y; + UIColor fg_color = (display_list[i]->selected) ? COLOR_SELECTED : COLOR_UNSELECTED; + UIColor bg_color = (is_selected_column && i == highlighted_index) ? COLOR_HIGHLIGHTED : COLOR_BLACK; + paint_text(fg_color, left_margin, y, display_list[i]->text, bg_color); + int x = left_margin + display_list[i]->text.length() + 1; + display_extras(*display_list[i]->elem, x, y); + } + + if (is_selected_column && allow_search) + { + y = gps->dimy - bottom_margin; + int32_t x = search_margin; + OutputHotkeyString(x, y, "Search" ,"S"); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString(COLOR_WHITE, x, y, search_string); + OutputString(COLOR_LIGHTGREEN, x, y, "_"); + } + } + + void filterDisplay() + { + ListEntry *prev_selected = (getDisplayListSize() > 0) ? display_list[highlighted_index] : NULL; + display_list.clear(); + search_string = toLower(search_string); + for (size_t i = 0; i < list.size(); i++) + { + if (search_string.empty() || toLower(list[i].text).find(search_string) != string::npos) + { + ListEntry *entry = &list[i]; + display_list.push_back(entry); + if (entry == prev_selected) + highlighted_index = display_list.size() - 1; + } + } + changeHighlight(0); + feed_changed_highlight = true; + } + + void selectDefaultEntry() + { + for (size_t i = 0; i < display_list.size(); i++) + { + if (display_list[i]->selected) + { + highlighted_index = i; + break; + } + } + } + + void validateHighlight() + { + set_to_limit(highlighted_index, display_list.size() - 1); + + if (highlighted_index < display_start_offset) + display_start_offset = highlighted_index; + else if (highlighted_index >= display_start_offset + display_max_rows) + display_start_offset = highlighted_index - display_max_rows + 1; + + if (auto_select || (!allow_null && list.size() == 1)) + display_list[highlighted_index]->selected = true; + + feed_changed_highlight = true; + } + + void changeHighlight(const int highlight_change, const int offset_shift = 0) + { + if (!initHighlightChange()) + return; + + highlighted_index += highlight_change + offset_shift * display_max_rows; + + display_start_offset += offset_shift * display_max_rows; + set_to_limit(display_start_offset, max(0, (int)(display_list.size())-display_max_rows)); + validateHighlight(); + } + + void setHighlight(const int index) + { + if (!initHighlightChange()) + return; + + highlighted_index = index; + validateHighlight(); + } + + bool initHighlightChange() + { + if (display_list.size() == 0) + return false; + + if (auto_select && !multiselect) + { + for (auto it = list.begin(); it != list.end(); it++) + { + it->selected = false; + } + } + + return true; + } + + void toggleHighlighted() + { + if (auto_select) + return; + + ListEntry *entry = display_list[highlighted_index]; + if (!multiselect || !allow_null) + { + int selected_count = 0; + for (size_t i = 0; i < list.size(); i++) + { + if (!multiselect && !entry->selected) + list[i].selected = false; + if (!allow_null && list[i].selected) + selected_count++; + } + + if (!allow_null && entry->selected && selected_count == 1) + return; + } + + entry->selected = !entry->selected; + } + + vector getSelectedElems(bool only_one = false) + { + vector results; + for (auto it = list.begin(); it != list.end(); it++) + { + if ((*it).selected) + { + results.push_back(it->elem); + if (only_one) + break; + } + } + + return results; + } + + void clearSelection() + { + for_each_(list, [] (ListEntry &e) { e.selected = false; }); + } + + void selectItem(const T* elem) + { + if (!elem) + return; + + //clearSelection(); + int i = 0; + for (; i < display_list.size(); i++) + { + if (display_list[i]->elem == elem) + { + setHighlight(i); + break; + } + } + } + + void clearSearch() + { + search_string.clear(); + filterDisplay(); + } + + T* getFirstSelectedElem() + { + vector results = getSelectedElems(true); + if (results.size() == 0) + return nullptr; + else + return results[0]; + } + + size_t getDisplayListSize() + { + return display_list.size(); + } + + size_t getBaseListSize() + { + return list.size(); + } + + bool feed(set *input) + { + feed_changed_highlight = false; + if (input->count(interface_key::CURSOR_UP)) + { + changeHighlight(-1); + } + else if (input->count(interface_key::CURSOR_DOWN)) + { + changeHighlight(1); + } + else if (input->count(interface_key::STANDARDSCROLL_PAGEUP)) + { + changeHighlight(0, -1); + } + else if (input->count(interface_key::STANDARDSCROLL_PAGEDOWN)) + { + changeHighlight(0, 1); + } + else if (input->count(interface_key::SELECT) && !auto_select) + { + toggleHighlighted(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_S)) + { + clearSearch(); + } + else if (enabler->tracking_on && gps->mouse_x != -1 && gps->mouse_y != -1 && enabler->mouse_lbut) + { + return setHighlightByMouse(); + } + else if (allow_search) + { + // Search query typing mode always on + df::interface_key last_token = *input->rbegin(); + if (last_token >= interface_key::STRING_A096 && last_token <= interface_key::STRING_A123) + { + // Standard character + search_string += last_token - ascii_to_enum_offset; + filterDisplay(); + } + else if (last_token == interface_key::STRING_A000) + { + // Backspace + if (search_string.length() > 0) + { + search_string.erase(search_string.length()-1); + filterDisplay(); + } + } + else + { + return false; + } + + return true; + } + else + { + return false; + } + + return true; + } + + bool setHighlightByMouse() + { + if (gps->mouse_y >= 3 && gps->mouse_y < display_max_rows + 3 && + gps->mouse_x >= left_margin && gps->mouse_x < left_margin + max_item_width) + { + int new_index = display_start_offset + gps->mouse_y - 3; + if (new_index < display_list.size()) + setHighlight(new_index); + + enabler->mouse_lbut = enabler->mouse_rbut = 0; + + return true; + } + + return false; + } + + void sort() + { + if (force_sort || list.size() < 100) + std::sort(list.begin(), list.end(), + [] (ListEntry const& a, ListEntry const& b) { return a.text.compare(b.text) < 0; }); + + filterDisplay(); + } + + void setTitle(const string t) + { + title = t; + if (title.length() > max_item_width) + max_item_width = title.length(); + } + + size_t getDisplayedListSize() + { + return display_list.size(); + } + +private: + vector> list; + vector*> display_list; + string search_string; + string title; + int display_max_rows; + int max_item_width; +}; + +