Merge pull request #2721 from robob27/enable-confirm

Enable Confirm
develop
Myk 2023-02-05 16:19:31 -08:00 committed by GitHub
commit a7b5ce418e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 679 additions and 762 deletions

@ -97,8 +97,8 @@ enable automelt
#enable automaterial
# Other interface improvement tools
#enable \
# confirm \
enable \
confirm
# dwarfmonitor \
# mousequery \
# autogems \

@ -950,10 +950,19 @@ Screens
Returns the topmost viewscreen. If ``skip_dismissed`` is *true*,
ignores screens already marked to be removed.
* ``dfhack.gui.getFocusString(viewscreen)``
* ``dfhack.gui.getFocusStrings(viewscreen)``
Returns a string representation of the current focus position
in the ui. The string has a "screen/foo/bar/baz..." format.
Returns a table of string representations of the current UI focuses.
The strings have a "screen/foo/bar/baz..." format e.g..::
[1] = "dwarfmode/Info/CREATURES/CITIZEN"
[2] = "dwardmode/Squads"
* ``dfhack.gui.matchFocusString(focus_string)``
Returns ``true`` if the given ``focus_string`` is found in the current
focus strings, or as a prefix to any of the focus strings, or ``false``
if no match is found. Matching is case insensitive.
* ``dfhack.gui.getCurFocus([skip_dismissed])``

@ -5,7 +5,7 @@ confirm
:summary: Adds confirmation dialogs for destructive actions.
:tags: untested fort interface
Now you can get the chance to avoid seizing goods from traders or deleting a
Now you can get the chance to avoid accidentally disbanding a squad or deleting a
hauling route in case you hit the key accidentally.
Usage

@ -947,8 +947,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
<< "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << endl
<< "Context may be used to limit the scope of the binding, by" << endl
<< "requiring the current context to have a certain prefix." << endl
<< "Current UI context is: "
<< Gui::getFocusString(Core::getTopViewscreen()) << endl;
<< "Current UI context is: " << endl
<< join_strings("\n", Gui::getCurFocus(true)) << endl;
}
}
else if (first == "alias")
@ -2419,12 +2419,14 @@ bool Core::SelectHotkey(int sym, int modifiers)
binding.modifiers, modifiers);
continue;
}
string focusString = Gui::getFocusString(screen);
if (!binding.focus.empty() && !prefix_matches(binding.focus, focusString)) {
if (!binding.focus.empty()) {
if (!Gui::matchFocusString(binding.focus)) {
std::vector<std::string> focusStrings = Gui::getCurFocus(true);
DEBUG(keybinding).print("skipping keybinding due to focus string mismatch: '%s' !~ '%s'\n",
focusString.c_str(), binding.focus.c_str());
join_strings(", ", focusStrings).c_str(), binding.focus.c_str());
continue;
}
}
if (!plug_mgr->CanInvokeHotkey(binding.command[0], screen)) {
DEBUG(keybinding).print("skipping keybinding due to hotkey guard rejection (command: '%s')\n",
binding.command[0].c_str());

@ -1471,13 +1471,12 @@ static int gui_getMousePos(lua_State *L)
static const LuaWrapper::FunctionReg dfhack_gui_module[] = {
WRAPM(Gui, getCurViewscreen),
WRAPM(Gui, getDFViewscreen),
WRAPM(Gui, getFocusString),
WRAPM(Gui, getCurFocus),
WRAPM(Gui, getSelectedWorkshopJob),
WRAPM(Gui, getSelectedJob),
WRAPM(Gui, getSelectedUnit),
WRAPM(Gui, getSelectedItem),
WRAPM(Gui, getSelectedBuilding),
WRAPM(Gui, getSelectedStockpile),
WRAPM(Gui, getSelectedPlant),
WRAPM(Gui, getAnyUnit),
WRAPM(Gui, getAnyItem),
@ -1495,9 +1494,24 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = {
WRAPM(Gui, refreshSidebar),
WRAPM(Gui, inRenameBuilding),
WRAPM(Gui, getDepthAt),
WRAPM(Gui, matchFocusString),
{ NULL, NULL }
};
static int gui_getFocusStrings(lua_State *state) {
df::viewscreen *r = Lua::GetDFObject<df::viewscreen>(state, 1);
std::vector<std::string> focusStrings = Gui::getFocusStrings(r);
Lua::PushVector(state, focusStrings);
return 1;
}
static int gui_getCurFocus(lua_State *state) {
bool skip_dismissed = lua_toboolean(state, 1);
std::vector<std::string> cur_focus = Gui::getCurFocus(skip_dismissed);
Lua::PushVector(state, cur_focus);
return 1;
}
static int gui_autoDFAnnouncement(lua_State *state)
{
bool rv;
@ -1623,6 +1637,8 @@ static const luaL_Reg dfhack_gui_funcs[] = {
{ "pauseRecenter", gui_pauseRecenter },
{ "revealInDwarfmodeMap", gui_revealInDwarfmodeMap },
{ "getMousePos", gui_getMousePos },
{ "getFocusStrings", gui_getFocusStrings },
{ "getCurFocus", gui_getCurFocus },
{ NULL, NULL }
};

@ -36,6 +36,7 @@ distribution.
#include "df/plotinfost.h"
#include "df/announcement_type.h"
#include "df/announcement_flags.h"
#include "df/building_stockpilest.h"
#include "df/report_init.h"
#include "df/report_zoom_type.h"
#include "df/unit_report_type.h"
@ -65,7 +66,9 @@ namespace DFHack
*/
namespace Gui
{
DFHACK_EXPORT std::string getFocusString(df::viewscreen *top);
DFHACK_EXPORT std::vector<std::string> getFocusStrings(df::viewscreen *top);
DFHACK_EXPORT bool matchFocusString(std::string focusString, bool prefixMatch = true);
// Full-screen item details view
DFHACK_EXPORT bool item_details_hotkey(df::viewscreen *top);
@ -107,6 +110,10 @@ namespace DFHack
DFHACK_EXPORT df::building *getAnyBuilding(df::viewscreen *top);
DFHACK_EXPORT df::building *getSelectedBuilding(color_ostream &out, bool quiet = false);
DFHACK_EXPORT bool any_stockpile_hotkey(df::viewscreen* top);
DFHACK_EXPORT df::building_stockpilest *getAnyStockpile(df::viewscreen* top);
DFHACK_EXPORT df::building_stockpilest *getSelectedStockpile(color_ostream& out, bool quiet = false);
// A plant is selected, e.g. via 'k'
DFHACK_EXPORT bool any_plant_hotkey(df::viewscreen *top);
DFHACK_EXPORT df::plant *getAnyPlant(df::viewscreen *top);
@ -191,8 +198,8 @@ namespace DFHack
return strict_virtual_cast<T>(getViewscreenByIdentity(T::_identity, n));
}
inline std::string getCurFocus(bool skip_dismissed = false) {
return getFocusString(getCurViewscreen(skip_dismissed));
inline std::vector<std::string> getCurFocus(bool skip_dismissed = false) {
return getFocusStrings(getCurViewscreen(skip_dismissed));
}
/// get the size of the window buffer

@ -29,46 +29,6 @@ SIDEBAR_MODE_KEYS = {
[df.ui_sidebar_mode.ViewUnits]='D_VIEWUNIT',
}
-- Sends ESC keycodes until we get to dwarfmode/Default and then enters the
-- specified sidebar mode with the corresponding keycode. If we don't get to
-- Default after max_esc presses of ESC (default value is 10), we throw an
-- error. The target sidebar mode must be a member of SIDEBAR_MODE_KEYS
function enterSidebarMode(sidebar_mode, max_esc)
local navkey = SIDEBAR_MODE_KEYS[sidebar_mode]
if not navkey then
error(('Invalid or unsupported sidebar mode: %s (%s)')
:format(sidebar_mode, df.ui_sidebar_mode[sidebar_mode]))
end
local max_esc_num = tonumber(max_esc)
if max_esc and (not max_esc_num or max_esc_num <= 0) then
error(('max_esc must be a positive number: got %s')
:format(tostring(max_esc)))
end
local remaining_esc = max_esc_num or 10
local focus_string = ''
while remaining_esc > 0 do
local screen = dfhack.gui.getCurViewscreen(true)
focus_string = dfhack.gui.getFocusString(screen)
if df.global.plotinfo.main.mode == df.ui_sidebar_mode.Default and
focus_string == 'dwarfmode/Default' then
if #navkey > 0 then gui.simulateInput(screen, navkey) end
if navkey == 'D_DESIGNATE' then
-- if the z-level happens to be on the surface, the mode will be
-- set to DesignateChopTrees. we need an extra step to get to
-- DesignateMine
gui.simulateInput(dfhack.gui.getCurViewscreen(true),
'DESIGNATE_DIG')
end
return
end
gui.simulateInput(screen, 'LEAVESCREEN')
remaining_esc = remaining_esc - 1
end
error(('Unable to get into target sidebar mode (%s) from' ..
' current UI viewscreen (%s).'):format(
df.ui_sidebar_mode[sidebar_mode], focus_string))
end
function getPanelLayout()
local dims = dfhack.gui.getDwarfmodeViewDims()
return {

@ -130,479 +130,387 @@ static std::string getNameChunk(virtual_identity *id, int start, int end)
* Classifying focus context by means of a string path.
*/
typedef void (*getFocusStringHandler)(std::string &str, df::viewscreen *screen);
static std::map<virtual_identity*, getFocusStringHandler> getFocusStringHandlers;
typedef void (*getFocusStringsHandler)(std::string &str, std::vector<std::string> &strList, df::viewscreen *screen);
static std::map<virtual_identity*, getFocusStringsHandler> getFocusStringsHandlers;
#define VIEWSCREEN(name) df::viewscreen_##name##st
#define DEFINE_GET_FOCUS_STRING_HANDLER(screen_type) \
static void getFocusString_##screen_type(std::string &focus, VIEWSCREEN(screen_type) *screen);\
static void getFocusStrings_##screen_type(std::string &baseFocus, std::vector<std::string> &focusStrings, VIEWSCREEN(screen_type) *screen);\
DFHACK_STATIC_ADD_TO_MAP(\
&getFocusStringHandlers, &VIEWSCREEN(screen_type)::_identity, \
(getFocusStringHandler)getFocusString_##screen_type \
&getFocusStringsHandlers, &VIEWSCREEN(screen_type)::_identity, \
(getFocusStringsHandler)getFocusStrings_##screen_type \
); \
static void getFocusString_##screen_type(std::string &focus, VIEWSCREEN(screen_type) *screen)
static void getFocusStrings_##screen_type(std::string &baseFocus, std::vector<std::string> &focusStrings, VIEWSCREEN(screen_type) *screen)
DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
{
/* TODO: understand how this changes for v50
using namespace df::enums::ui_sidebar_mode;
std::string newFocusString;
using df::global::ui_workshop_in_add;
using df::global::ui_build_selector;
using df::global::ui_selected_unit;
using df::global::ui_look_list;
using df::global::ui_look_cursor;
using df::global::ui_building_item_cursor;
using df::global::ui_building_assign_type;
using df::global::ui_building_assign_is_marked;
using df::global::ui_building_assign_units;
using df::global::ui_building_assign_items;
using df::global::ui_building_in_assign;
focus += "/" + enum_item_key(plotinfo->main.mode);
switch (plotinfo->main.mode)
{
case QueryBuilding:
if (df::building *selected = world->selected_building)
{
if (!selected->jobs.empty() &&
selected->jobs[0]->job_type == job_type::DestroyBuilding)
{
focus += "/Destroying";
break;
if(game->main_interface.main_designation_selected != -1) {
newFocusString = baseFocus;
newFocusString += "/Designate/" + enum_item_key(game->main_interface.main_designation_selected);
focusStrings.push_back(newFocusString);
}
if (game->main_interface.info.open) {
newFocusString = baseFocus;
newFocusString += "/Info";
newFocusString += "/" + enum_item_key(game->main_interface.info.current_mode);
focus += "/Some";
virtual_identity *id = virtual_identity::get(selected);
bool jobs = false;
if (id == &df::building_workshopst::_identity ||
id == &df::building_furnacest::_identity)
{
focus += "/Workshop";
jobs = true;
}
else if (id == &df::building_trapst::_identity)
{
auto trap = (df::building_trapst*)selected;
focus += "/" + enum_item_key(trap->trap_type);
if (trap->trap_type == trap_type::Lever)
jobs = true;
}
else if (ui_building_in_assign && *ui_building_in_assign &&
ui_building_assign_type && ui_building_assign_units &&
ui_building_assign_type->size() == ui_building_assign_units->size())
{
focus += "/Assign";
if (ui_building_item_cursor)
{
auto unit = vector_get(*ui_building_assign_units, *ui_building_item_cursor);
focus += unit ? "/Unit" : "/None";
}
switch(game->main_interface.info.current_mode) {
case df::enums::info_interface_mode_type::CREATURES:
newFocusString += "/" + enum_item_key(game->main_interface.info.creatures.current_mode);
break;
case df::enums::info_interface_mode_type::BUILDINGS:
newFocusString += "/" + enum_item_key(game->main_interface.info.buildings.mode);
break;
case df::enums::info_interface_mode_type::LABOR:
newFocusString += "/" + enum_item_key(game->main_interface.info.labor.mode);
break;
case df::enums::info_interface_mode_type::ARTIFACTS:
newFocusString += "/" + enum_item_key(game->main_interface.info.artifacts.mode);
break;
case df::enums::info_interface_mode_type::JUSTICE:
newFocusString += "/" + enum_item_key(game->main_interface.info.justice.current_mode);
break;
default:
break;
}
else
focus += "/" + enum_item_key(selected->getType());
if (jobs)
{
if (ui_workshop_in_add && *ui_workshop_in_add)
focus += "/AddJob";
else if (!selected->jobs.empty())
focus += "/Job";
else
focus += "/Empty";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.view_sheets.open) {
newFocusString = baseFocus;
newFocusString += "/ViewSheets";
newFocusString += "/" + enum_item_key(game->main_interface.view_sheets.active_sheet);
focusStrings.push_back(newFocusString);
}
else
focus += "/None";
break;
case Build:
if (ui_build_selector)
{
// Not selecting, or no choices?
if (ui_build_selector->building_type < 0)
focus += "/Type";
else if (ui_build_selector->stage != 2)
{
if (ui_build_selector->stage != 1)
focus += "/NoMaterials";
else
focus += "/Position";
if(game->main_interface.bottom_mode_selected != -1) {
newFocusString = baseFocus;
focus += "/" + enum_item_key(ui_build_selector->building_type);
}
else
{
focus += "/Material";
if (ui_build_selector->is_grouped)
focus += "/Groups";
else
focus += "/Items";
}
switch(game->main_interface.bottom_mode_selected) {
case df::enums::main_bottom_mode_type::STOCKPILE:
if (game->main_interface.stockpile.cur_bld) {
newFocusString += "/Some";
}
newFocusString += "/Stockpile";
break;
case ViewUnits:
if (ui_selected_unit)
{
if (vector_get(world->units.active, *ui_selected_unit))
{
focus += "/Some";
using df::global::ui_unit_view_mode;
if (ui_unit_view_mode)
focus += "/" + enum_item_key(ui_unit_view_mode->value);
}
else
focus += "/None";
}
case df::enums::main_bottom_mode_type::STOCKPILE_PAINT:
newFocusString += "/Stockpile/Paint";
break;
case LookAround:
if (ui_look_list && ui_look_cursor)
{
auto item = vector_get(ui_look_list->items, *ui_look_cursor);
if (item)
focus += "/" + enum_item_key(item->type);
else
focus += "/None";
}
case df::enums::main_bottom_mode_type::HAULING:
newFocusString += "/Hauling";
break;
case BuildingItems:
if (VIRTUAL_CAST_VAR(selected, df::building_actual, world->selected_building))
{
if (selected->contained_items.empty())
focus += "/Some/Empty";
else
focus += "/Some/Item";
case df::enums::main_bottom_mode_type::ZONE:
newFocusString += "/Zone";
if (game->main_interface.civzone.cur_bld) {
newFocusString += "/Some";
newFocusString += "/" + enum_item_key(game->main_interface.civzone.cur_bld->type);
}
else
focus += "/None";
break;
case df::enums::main_bottom_mode_type::ZONE_PAINT:
newFocusString += "/Zone/Paint";
case ZonesPenInfo:
if (ui_building_assign_type && ui_building_assign_units &&
ui_building_assign_is_marked && ui_building_assign_items &&
ui_building_assign_type->size() == ui_building_assign_units->size())
{
focus += "/Assign";
if (ui_building_item_cursor)
{
if (vector_get(*ui_building_assign_units, *ui_building_item_cursor))
focus += "/Unit";
else if (vector_get(*ui_building_assign_items, *ui_building_item_cursor))
focus += "/Vermin";
else
focus += "/None";
}
}
// TODO: figure out why enum_item_key doesn't work on this?
switch(game->main_interface.civzone.adding_new_type) {
case df::enums::civzone_type::MeetingHall:
newFocusString += "/MeetingHall";
break;
case Burrows:
if (plotinfo->burrows.in_confirm_delete)
focus += "/ConfirmDelete";
else if (plotinfo->burrows.in_add_units_mode)
focus += "/AddUnits";
else if (plotinfo->burrows.in_edit_name_mode)
focus += "/EditName";
else if (plotinfo->burrows.in_define_mode)
focus += "/Define";
else
focus += "/List";
case df::enums::civzone_type::Bedroom:
newFocusString += "/Bedroom";
break;
case Hauling:
if (plotinfo->hauling.in_assign_vehicle)
{
auto vehicle = vector_get(plotinfo->hauling.vehicles, plotinfo->hauling.cursor_vehicle);
focus += "/AssignVehicle/" + std::string(vehicle ? "Some" : "None");
}
else
{
int idx = plotinfo->hauling.cursor_top;
auto route = vector_get(plotinfo->hauling.view_routes, idx);
auto stop = vector_get(plotinfo->hauling.view_stops, idx);
std::string tag = stop ? "Stop" : (route ? "Route" : "None");
if (plotinfo->hauling.in_name)
focus += "/Rename/" + tag;
else if (plotinfo->hauling.in_stop)
{
int sidx = plotinfo->hauling.cursor_stop;
auto cond = vector_get(plotinfo->hauling.stop_conditions, sidx);
auto link = vector_get(plotinfo->hauling.stop_links, sidx);
focus += "/DefineStop";
if (cond)
focus += "/Cond/" + enum_item_key(cond->mode);
else if (link)
{
focus += "/Link/";
if (link->mode.bits.give) focus += "Give";
if (link->mode.bits.take) focus += "Take";
}
else
focus += "/None";
}
else
focus += "/Select/" + tag;
}
case df::enums::civzone_type::DiningHall:
newFocusString += "/DiningHall";
break;
default:
case df::enums::civzone_type::Pen:
newFocusString += "/Pen";
break;
}
*/
}
/* TODO: understand how this changes for v50
DEFINE_GET_FOCUS_STRING_HANDLER(dungeonmode)
{
using df::global::adventure;
if (!adventure)
return;
focus += "/" + enum_item_key(adventure->menu);
}
DEFINE_GET_FOCUS_STRING_HANDLER(unitlist)
{
focus += "/" + enum_item_key(screen->page);
}
DEFINE_GET_FOCUS_STRING_HANDLER(layer_military)
{
auto list1 = getLayerList(screen, 0);
auto list2 = getLayerList(screen, 1);
auto list3 = getLayerList(screen, 2);
if (!list1 || !list2 || !list3) return;
focus += "/" + enum_item_key(screen->page);
int cur_list;
if (list1->active) cur_list = 0;
else if (list2->active) cur_list = 1;
else if (list3->active) cur_list = 2;
else return;
switch (screen->page)
{
case df::viewscreen_layer_militaryst::Positions:
{
static const char *lists[] = { "/Squads", "/Positions", "/Candidates" };
focus += lists[cur_list];
case df::enums::civzone_type::Pond:
newFocusString += "/Pond";
break;
}
case df::viewscreen_layer_militaryst::Equip:
{
focus += "/" + enum_item_key(screen->equip.mode);
switch (screen->equip.mode)
{
case df::viewscreen_layer_militaryst::T_equip::Customize:
{
if (screen->equip.edit_mode < 0)
focus += "/View";
else
focus += "/" + enum_item_key(screen->equip.edit_mode);
case df::enums::civzone_type::WaterSource:
newFocusString += "/WaterSource";
break;
}
case df::viewscreen_layer_militaryst::T_equip::Uniform:
case df::enums::civzone_type::Dungeon:
newFocusString += "/Dungeon";
break;
case df::viewscreen_layer_militaryst::T_equip::Priority:
{
if (screen->equip.prio_in_move >= 0)
focus += "/Move";
else
focus += "/View";
case df::enums::civzone_type::FishingArea:
newFocusString += "/FishingArea";
break;
}
}
static const char *lists[] = { "/Squads", "/Positions", "/Choices" };
focus += lists[cur_list];
case df::enums::civzone_type::SandCollection:
newFocusString += "/SandCollection";
break;
}
default:
case df::enums::civzone_type::Office:
newFocusString += "/Office";
break;
}
}
DEFINE_GET_FOCUS_STRING_HANDLER(workshop_profile)
{
typedef df::viewscreen_workshop_profilest::T_tab T_tab;
switch(screen->tab)
{
case T_tab::Workers:
focus += "/Unit";
case df::enums::civzone_type::Dormitory:
newFocusString += "/Dormitory";
break;
case T_tab::Orders:
focus += "/Orders";
case df::enums::civzone_type::Barracks:
newFocusString += "/Barracks";
break;
case T_tab::Restrictions:
focus += "/Restrictions";
case df::enums::civzone_type::ArcheryRange:
newFocusString += "/ArcheryRange";
break;
case df::enums::civzone_type::Dump:
newFocusString += "/Dump";
break;
case df::enums::civzone_type::AnimalTraining:
newFocusString += "/AnimalTraining";
break;
case df::enums::civzone_type::Tomb:
newFocusString += "/Tomb";
break;
case df::enums::civzone_type::PlantGathering:
newFocusString += "/PlantGathering";
break;
case df::enums::civzone_type::ClayCollection:
newFocusString += "/ClayCollection";
break;
}
}
DEFINE_GET_FOCUS_STRING_HANDLER(layer_noblelist)
{
auto list1 = getLayerList(screen, 0);
auto list2 = getLayerList(screen, 1);
if (!list1 || !list2) return;
focus += "/" + enum_item_key(screen->mode);
}
DEFINE_GET_FOCUS_STRING_HANDLER(pet)
{
focus += "/" + enum_item_key(screen->mode);
switch (screen->mode)
{
case df::viewscreen_petst::List:
focus += vector_get(screen->is_vermin, screen->cursor) ? "/Vermin" : "/Unit";
break;
case df::viewscreen_petst::SelectTrainer:
if (vector_get(screen->trainer_unit, screen->trainer_cursor))
focus += "/Unit";
case df::enums::main_bottom_mode_type::BURROW:
newFocusString += "/Burrow";
break;
case df::enums::main_bottom_mode_type::BURROW_PAINT:
newFocusString += "/Burrow/Paint";
break;
case df::enums::main_bottom_mode_type::BUILDING:
newFocusString += "/Building";
break;
case df::enums::main_bottom_mode_type::BUILDING_PLACEMENT:
newFocusString += "/Building/Placement";
break;
case df::enums::main_bottom_mode_type::BUILDING_PICK_MATERIALS:
newFocusString += "/Building/PickMaterials";
break;
default:
break;
}
}
DEFINE_GET_FOCUS_STRING_HANDLER(layer_overall_health)
{
auto list1 = getLayerList(screen, 0);
if (!list1) return;
focus += "/Units";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.trade.open) {
newFocusString = baseFocus;
newFocusString += "/Trade";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.job_details.open) {
newFocusString = baseFocus;
newFocusString += "/JobDetails";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.assign_trade.open) {
newFocusString = baseFocus;
newFocusString += "/AssignTrade";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.diplomacy.open) {
newFocusString = baseFocus;
newFocusString += "/Diplomacy";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.petitions.open) {
newFocusString = baseFocus;
newFocusString += "/Petitions";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.stocks.open) {
newFocusString = baseFocus;
newFocusString += "/Stocks";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.assign_display_item.open) {
newFocusString = baseFocus;
newFocusString += "/AssignDisplayItem";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.name_creator.open) {
newFocusString = baseFocus;
newFocusString += "/NameCreator";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.image_creator.open) {
newFocusString = baseFocus;
newFocusString += "/ImageCreator";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.unit_selector.open) {
newFocusString = baseFocus;
newFocusString += "/UnitSelector";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.announcement_alert.open) {
newFocusString = baseFocus;
newFocusString += "/AnnouncementAlert";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.custom_symbol.open) {
newFocusString = baseFocus;
newFocusString += "/CustomSymbol";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.patrol_routes.open) {
newFocusString = baseFocus;
newFocusString += "/PatrolRoutes";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.squad_schedule.open) {
newFocusString = baseFocus;
newFocusString += "/SquadSchedule";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.squad_selector.open) {
newFocusString = baseFocus;
newFocusString += "/SquadSelector";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.burrow_selector.open) {
newFocusString = baseFocus;
newFocusString += "/BurrowSelector";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.location_selector.open) {
newFocusString = baseFocus;
newFocusString += "/LocationSelector";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.location_details.open) {
newFocusString = baseFocus;
newFocusString += "/LocationDetails";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.hauling_stop_conditions.open) {
newFocusString = baseFocus;
newFocusString += "/HaulingStopConditions";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.assign_vehicle.open) {
newFocusString = baseFocus;
newFocusString += "/AssignVehicle";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.stockpile_link.open) {
newFocusString = baseFocus;
newFocusString += "/StockpileLink";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.stockpile_tools.open) {
newFocusString = baseFocus;
newFocusString += "/StockpileTools";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.custom_stockpile.open) {
newFocusString = baseFocus;
newFocusString += "/CustomStockpile";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.create_squad.open) {
newFocusString = baseFocus;
newFocusString += "/CreateSquad";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.squad_supplies.open) {
newFocusString = baseFocus;
newFocusString += "/SquadSupplies";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.assign_uniform.open) {
newFocusString = baseFocus;
newFocusString += "/AssignUniform";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.create_work_order.open) {
newFocusString = baseFocus;
newFocusString += "/CreateWorkOrder";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.hotkey.open) {
newFocusString = baseFocus;
newFocusString += "/Hotkey";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.options.open) {
newFocusString = baseFocus;
newFocusString += "/Options";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.help.open) {
newFocusString = baseFocus;
newFocusString += "/Help";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.settings.open) {
newFocusString = baseFocus;
newFocusString += "/Settings";
focusStrings.push_back(newFocusString);
}
if (game->main_interface.squad_equipment.open) {
newFocusString = baseFocus;
newFocusString += "/SquadEquipment";
focusStrings.push_back(newFocusString);
}
// squads should be last because it's the only one not exclusive with the others? or something?
if (game->main_interface.squads.open) {
newFocusString = baseFocus;
newFocusString += "/Squads";
focusStrings.push_back(newFocusString);
}
if (!newFocusString.size()) {
focusStrings.push_back(baseFocus);
}
DEFINE_GET_FOCUS_STRING_HANDLER(tradegoods)
{
if (!screen->has_traders || screen->is_unloading)
focus += "/NoTraders";
else if (screen->in_edit_count)
focus += "/EditCount";
else
focus += (screen->in_right_pane ? "/Items/Broker" : "/Items/Trader");
}
DEFINE_GET_FOCUS_STRING_HANDLER(layer_assigntrade)
{
auto list1 = getLayerList(screen, 0);
auto list2 = getLayerList(screen, 1);
if (!list1 || !list2) return;
int list_idx = vector_get(screen->visible_lists, list1->cursor, (int16_t)-1);
unsigned num_lists = sizeof(screen->lists)/sizeof(screen->lists[0]);
if (unsigned(list_idx) >= num_lists)
return;
if (list1->active)
focus += "/Groups";
else
focus += "/Items";
}
DEFINE_GET_FOCUS_STRING_HANDLER(stores)
{
if (!screen->in_right_list)
focus += "/Categories";
else if (screen->in_group_mode)
focus += "/Groups";
else
focus += "/Items";
}
DEFINE_GET_FOCUS_STRING_HANDLER(layer_stockpile)
/* TODO: understand how this changes for v50
DEFINE_GET_FOCUS_STRING_HANDLER(dungeonmode)
{
auto list1 = getLayerList(screen, 0);
auto list2 = getLayerList(screen, 1);
auto list3 = getLayerList(screen, 2);
if (!list1 || !list2 || !list3 || !screen->settings) return;
auto group = screen->cur_group;
if (group != vector_get(screen->group_ids, list1->cursor))
return;
focus += "/" + enum_item_key(group);
using df::global::adventure;
auto bits = vector_get(screen->group_bits, list1->cursor);
if (bits.whole && !(bits.whole & screen->settings->flags.whole))
{
focus += "/Off";
if (!adventure)
return;
}
focus += "/On";
if (list2->active || list3->active || screen->list_ids.empty()) {
focus += "/" + enum_item_key(screen->cur_list);
if (list3->active)
focus += (screen->item_names.empty() ? "/None" : "/Item");
}
focus += "/" + enum_item_key(adventure->menu);
}
*/
DEFINE_GET_FOCUS_STRING_HANDLER(locations)
{
focus += "/" + enum_item_key(screen->menu);
}
bool Gui::matchFocusString(std::string focusString, bool prefixMatch) {
focusString = toLower(focusString);
std::vector<std::string> currentFocusStrings = getFocusStrings(getCurViewscreen(true));
DEFINE_GET_FOCUS_STRING_HANDLER(jobmanagement)
{
focus += (screen->in_max_workshops ? "/MaxWorkshops" : "/Main");
return std::find_if(currentFocusStrings.begin(), currentFocusStrings.end(), [&focusString, &prefixMatch](std::string item) {
return prefixMatch ? prefix_matches(focusString, toLower(item)) : focusString == toLower(item);
}) != currentFocusStrings.end();
}
DEFINE_GET_FOCUS_STRING_HANDLER(workquota_condition)
std::vector<std::string> Gui::getFocusStrings(df::viewscreen* top)
{
focus += "/" + enum_item_key(screen->mode);
if (screen->item_count_edit)
focus += "/EditCount";
}
*/
std::vector<std::string> focusStrings;
std::string Gui::getFocusString(df::viewscreen *top)
{
if (!top)
return "";
return focusStrings;
if (dfhack_viewscreen::is_instance(top))
{
auto name = static_cast<dfhack_viewscreen*>(top)->getFocusString();
return name.empty() ? "dfhack" : "dfhack/"+name;
focusStrings.push_back(name.empty() ? "dfhack" : "dfhack/" + name);
}
else if (virtual_identity *id = virtual_identity::get(top))
{
std::string name = getNameChunk(id, 11, 2);
auto handler = map_find(getFocusStringHandlers, id);
auto handler = map_find(getFocusStringsHandlers, id);
if (handler)
handler(name, top);
return name;
handler(name, focusStrings, top);
}
else
{
Core &core = Core::getInstance();
std::string name = core.p->readClassName(*(void**)top);
return name.substr(11, name.size()-11-2);
focusStrings.push_back(name.substr(11, name.size()-11-2));
}
return focusStrings;
}
// Predefined common guard functions
@ -1269,6 +1177,28 @@ df::item *Gui::getSelectedItem(color_ostream &out, bool quiet)
return item;
}
bool Gui::any_stockpile_hotkey(df::viewscreen* top)
{
return getAnyStockpile(top) != NULL;
}
df::building_stockpilest* Gui::getAnyStockpile(df::viewscreen* top) {
if (matchFocusString("dwarfmode/Some/Stockpile")) {
return game->main_interface.stockpile.cur_bld;
}
return NULL;
}
df::building_stockpilest* Gui::getSelectedStockpile(color_ostream& out, bool quiet) {
df::building_stockpilest* stockpile = getAnyStockpile(Core::getTopViewscreen());
if (!stockpile && !quiet)
out.printerr("No stockpile is selected in the UI.\n");
return stockpile;
}
df::building *Gui::getAnyBuilding(df::viewscreen *top)
{
using df::global::game;

@ -97,7 +97,7 @@ add_subdirectory(channel-safely)
dfhack_plugin(cleanconst cleanconst.cpp)
dfhack_plugin(cleaners cleaners.cpp)
dfhack_plugin(cleanowned cleanowned.cpp)
#dfhack_plugin(confirm confirm.cpp LINK_LIBRARIES lua)
dfhack_plugin(confirm confirm.cpp LINK_LIBRARIES lua)
#dfhack_plugin(createitem createitem.cpp)
dfhack_plugin(cursecheck cursecheck.cpp)
dfhack_plugin(cxxrandom cxxrandom.cpp LINK_LIBRARIES lua)

@ -15,16 +15,11 @@
#include "modules/Gui.h"
#include "uicommon.h"
#include "df/building_tradedepotst.h"
#include "df/gamest.h"
#include "df/general_ref.h"
#include "df/general_ref_contained_in_itemst.h"
#include "df/interfacest.h"
#include "df/viewscreen_dwarfmodest.h"
#include "df/viewscreen_jobmanagementst.h"
#include "df/viewscreen_justicest.h"
#include "df/viewscreen_layer_militaryst.h"
#include "df/viewscreen_locationsst.h"
#include "df/viewscreen_tradegoodsst.h"
using namespace DFHack;
using namespace df::enums;
@ -35,8 +30,8 @@ using std::vector;
DFHACK_PLUGIN("confirm");
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(game);
REQUIRE_GLOBAL(gps);
REQUIRE_GLOBAL(plotinfo);
typedef std::set<df::interface_key> ikey_set;
command_result df_confirm (color_ostream &out, vector <string> & parameters);
@ -46,11 +41,6 @@ static map<string, conf_wrapper*> confirmations;
string active_id;
queue<string> cmds;
// true when confirm is paused
bool paused = false;
// if set, confirm will unpause when this screen is no longer on the stack
df::viewscreen *paused_screen = NULL;
namespace DFHack {
DBG_DECLARE(confirm,status);
}
@ -72,11 +62,14 @@ string char_replace (string s, char a, char b)
}
bool set_conf_state (string name, bool state);
bool set_conf_paused (string name, bool pause);
class confirmation_base {
public:
enum cstate { INACTIVE, ACTIVE, SELECTED };
virtual string get_id() = 0;
virtual string get_focus_string() = 0;
virtual bool match_prefix() = 0;
virtual bool set_state(cstate) = 0;
static bool set_state(string id, cstate state)
@ -96,10 +89,12 @@ confirmation_base *confirmation_base::active = nullptr;
struct conf_wrapper {
private:
bool enabled;
bool paused;
std::set<VMethodInterposeLinkBase*> hooks;
public:
conf_wrapper()
:enabled(false)
:enabled(false),
paused(false)
{}
void add_hook(VMethodInterposeLinkBase *hook)
{
@ -117,29 +112,35 @@ public:
enabled = state;
return true;
}
bool set_paused (bool pause) {
paused = pause;
return true;
}
inline bool is_enabled() { return enabled; }
inline bool is_paused() { return paused; }
};
namespace trade {
static bool goods_selected (const std::vector<char> &selected)
static bool goods_selected (std::vector<uint8_t> &selected)
{
for (char c : selected)
if (c)
if(!game->main_interface.trade.open)
return false;
for (uint8_t sel : selected)
if (sel == 1)
return true;
return false;
}
inline bool trader_goods_selected (df::viewscreen_tradegoodsst *screen)
inline bool trader_goods_selected ()
{
CHECK_NULL_POINTER(screen);
return goods_selected(screen->trader_selected);
return goods_selected(game->main_interface.trade.goodflag[0]);
}
inline bool broker_goods_selected (df::viewscreen_tradegoodsst *screen)
inline bool broker_goods_selected ()
{
CHECK_NULL_POINTER(screen);
return goods_selected(screen->broker_selected);
return goods_selected(game->main_interface.trade.goodflag[1]);
}
static bool goods_all_selected(const std::vector<char> &selected, const std::vector<df::item*> &items) \
/*static bool goods_all_selected(const std::vector<char>& selected, const std::vector<df::item*>& items) \
{
for (size_t i = 0; i < selected.size(); ++i)
{
@ -162,16 +163,14 @@ namespace trade {
}
return true;
}
inline bool trader_goods_all_selected(df::viewscreen_tradegoodsst *screen)
inline bool trader_goods_all_selected()
{
CHECK_NULL_POINTER(screen);
return goods_all_selected(screen->trader_selected, screen->trader_items);
}
inline bool broker_goods_all_selected(df::viewscreen_tradegoodsst *screen)
inline bool broker_goods_all_selected()
{
CHECK_NULL_POINTER(screen);
return goods_all_selected(screen->broker_selected, screen->broker_items);
}
}*/
}
namespace conf_lua {
@ -228,6 +227,7 @@ namespace conf_lua {
lua_newtable(L);
Lua::TableInsert(L, "id", item.first);
Lua::TableInsert(L, "enabled", item.second->is_enabled());
Lua::TableInsert(L, "paused", item.second->is_paused());
lua_settable(L, -3);
}
return 1;
@ -240,28 +240,17 @@ namespace conf_lua {
lua_pushnil(L);
return 1;
}
int unpause(lua_State *)
{
DEBUG(status).print("unpausing\n");
paused = false;
paused_screen = NULL;
return 0;
}
int get_paused (lua_State *L)
{
Lua::Push(L, paused);
return 1;
}
}
}
#define CONF_LUA_FUNC(ns, name) {#name, df::wrap_function(ns::name, true)}
DFHACK_PLUGIN_LUA_FUNCTIONS {
CONF_LUA_FUNC( , set_conf_state),
CONF_LUA_FUNC( , set_conf_paused),
CONF_LUA_FUNC(trade, broker_goods_selected),
CONF_LUA_FUNC(trade, broker_goods_all_selected),
//CONF_LUA_FUNC(trade, broker_goods_all_selected),
CONF_LUA_FUNC(trade, trader_goods_selected),
CONF_LUA_FUNC(trade, trader_goods_all_selected),
//CONF_LUA_FUNC(trade, trader_goods_all_selected),
DFHACK_LUA_END
};
@ -270,14 +259,12 @@ DFHACK_PLUGIN_LUA_COMMANDS {
CONF_LUA_CMD(get_ids),
CONF_LUA_CMD(get_conf_data),
CONF_LUA_CMD(get_active_id),
CONF_LUA_CMD(unpause),
CONF_LUA_CMD(get_paused),
DFHACK_LUA_END
};
void show_options()
{
cmds.push("gui/confirm-opts");
cmds.push("gui/confirm");
}
template <class T>
@ -306,45 +293,70 @@ public:
return true;
}
bool feed (ikey_set *input) {
if (paused)
{
// we can only detect that we've left the screen by intercepting the
// ESC key
if (!paused_screen && input->count(df::interface_key::LEAVESCREEN))
conf_lua::api::unpause(NULL);
bool mouseExit = false;
if(df::global::enabler->mouse_rbut) {
mouseExit = true;
}
bool mouseSelect = false;
if(df::global::enabler->mouse_lbut) {
mouseSelect = true;
}
conf_wrapper *wrapper = confirmations[this->get_id()];
if(wrapper->is_paused()) {
std::string concernedFocus = this->get_focus_string();
if(!Gui::matchFocusString(this->get_focus_string(), this->match_prefix()))
wrapper->set_paused(false);
return false;
} else if (state == INACTIVE)
{
if(mouseExit) {
if(intercept_key("MOUSE_RIGHT") && set_state(ACTIVE)) {
df::global::enabler->mouse_rbut = 0;
df::global::enabler->mouse_rbut_down = 0;
mouse_pos = df::coord2d(df::global::gps->mouse_x, df::global::gps->mouse_y);
last_key_is_right_click = true;
return true;
}
else if (state == INACTIVE)
{
} else
last_key_is_right_click = false;
if(mouseSelect) {
if(intercept_key("MOUSE_LEFT") && set_state(ACTIVE)) {
df::global::enabler->mouse_lbut = 0;
df::global::enabler->mouse_lbut_down = 0;
mouse_pos = df::coord2d(df::global::gps->mouse_x, df::global::gps->mouse_y);
last_key_is_left_click = true;
return true;
}
} else
last_key_is_left_click = false;
for (df::interface_key key : *input)
{
if (intercept_key(key))
{
if (set_state(ACTIVE))
if (intercept_key(key) && set_state(ACTIVE))
{
last_key = key;
return true;
}
}
}
return false;
}
else if (state == ACTIVE)
{
if (input->count(df::interface_key::LEAVESCREEN))
if (input->count(df::interface_key::LEAVESCREEN) || mouseExit) {
if(mouseExit) {
df::global::enabler->mouse_rbut = 0;
df::global::enabler->mouse_rbut_down = 0;
}
set_state(INACTIVE);
else if (input->count(df::interface_key::SELECT))
} else if (input->count(df::interface_key::SELECT))
set_state(SELECTED);
else if (input->count(df::interface_key::CUSTOM_P))
{
DEBUG(status).print("pausing\n");
paused = true;
// only record the screen when we're not at the top viewscreen
// since this screen will *always* be on the stack. for
// dwarfmode screens, use ESC detection to discover when to
// unpause
if (!df::viewscreen_dwarfmodest::_identity.is_instance(screen))
paused_screen = screen;
wrapper->set_paused(true);
set_state(INACTIVE);
}
else if (input->count(df::interface_key::CUSTOM_S))
@ -429,12 +441,35 @@ public:
else if (state == SELECTED)
{
ikey_set tmp;
if(last_key_is_left_click) {
long prevx = df::global::gps->mouse_x;
long prevy = df::global::gps->mouse_y;
df::global::gps->mouse_x = mouse_pos.x;
df::global::gps->mouse_y = mouse_pos.y;
df::global::enabler->mouse_lbut = 1;
df::global::enabler->mouse_lbut_down = 1;
screen->feed(&tmp);
df::global::enabler->mouse_lbut = 0;
df::global::enabler->mouse_lbut_down = 0;
df::global::gps->mouse_x = prevx;
df::global::gps->mouse_y = prevy;
}
else if(last_key_is_right_click) {
tmp.insert(df::interface_key::LEAVESCREEN);
screen->feed(&tmp);
}
else {
tmp.insert(last_key);
screen->feed(&tmp);
}
set_state(INACTIVE);
}
// clean up any artifacts
df::global::gps->force_full_display_count = 1;
}
virtual string get_id() override = 0;
string get_id() override = 0;
string get_focus_string() override = 0;
bool match_prefix() override = 0;
#define CONF_LUA_START using namespace conf_lua; Lua::StackUnwinder unwind(l_state); push(screen); push(get_id());
bool intercept_key (df::interface_key key)
{
@ -445,6 +480,15 @@ public:
else
return false;
};
bool intercept_key (std::string mouse_button = "MOUSE_LEFT")
{
CONF_LUA_START;
push(mouse_button);
if (call("intercept_key", 3, 1))
return lua_toboolean(l_state, -1);
else
return false;
};
string get_title()
{
CONF_LUA_START;
@ -473,6 +517,9 @@ public:
protected:
cstate state;
df::interface_key last_key;
bool last_key_is_left_click;
bool last_key_is_right_click;
df::coord2d mouse_pos;
};
template<typename T>
@ -501,23 +548,19 @@ struct cls##_hooks : cls::screen_type { \
INTERPOSE_NEXT(render)(); \
cls##_instance.render(); \
} \
DEFINE_VMETHOD_INTERPOSE(bool, key_conflict, (df::interface_key key)) \
{ \
return cls##_instance.key_conflict(key) || INTERPOSE_NEXT(key_conflict)(key); \
} \
}; \
IMPLEMENT_VMETHOD_INTERPOSE_PRIO(cls##_hooks, feed, prio); \
IMPLEMENT_VMETHOD_INTERPOSE_PRIO(cls##_hooks, render, prio); \
IMPLEMENT_VMETHOD_INTERPOSE_PRIO(cls##_hooks, key_conflict, prio); \
static int conf_register_##cls = conf_register(&cls##_instance, {\
&INTERPOSE_HOOK(cls##_hooks, feed), \
&INTERPOSE_HOOK(cls##_hooks, render), \
&INTERPOSE_HOOK(cls##_hooks, key_conflict), \
});
#define DEFINE_CONFIRMATION(cls, screen) \
#define DEFINE_CONFIRMATION(cls, screen, focusString) \
class confirmation_##cls : public confirmation<df::screen> { \
virtual string get_id() { static string id = char_replace(#cls, '_', '-'); return id; } \
virtual string get_focus_string() { return focusString; } \
virtual bool match_prefix() { return focusString[strlen(focusString) - 1] == '*'; } \
}; \
IMPLEMENT_CONFIRMATION_HOOKS(confirmation_##cls, 0);
@ -525,21 +568,39 @@ static int conf_register_##cls = conf_register(&cls##_instance, {\
implemented in plugins/lua/confirm.lua.
IDs (used in the "confirm enable/disable" command, by Lua, and in the docs)
are obtained by replacing '_' with '-' in the first argument to DEFINE_CONFIRMATION
The second argument to DEFINE_CONFIRMATION determines the viewscreen that any
intercepted input will be fed to.
The third argument to DEFINE_CONFIRMATION determines the focus string that will
be used to determine if the confirmation should be unpaused. If a confirmation is paused
and the focus string is no longer found in the current focus, the confirmation will be
unpaused. Focus strings ending in "*" will use prefix matching e.g. "dwarfmode/Info*" would
match "dwarfmode/Info/Foo", "dwarfmode/Info/Bar" and so on. All matching is case insensitive.
*/
DEFINE_CONFIRMATION(trade, viewscreen_tradegoodsst);
DEFINE_CONFIRMATION(trade_cancel, viewscreen_tradegoodsst);
DEFINE_CONFIRMATION(trade_seize, viewscreen_tradegoodsst);
DEFINE_CONFIRMATION(trade_offer, viewscreen_tradegoodsst);
DEFINE_CONFIRMATION(trade_select_all, viewscreen_tradegoodsst);
DEFINE_CONFIRMATION(haul_delete, viewscreen_dwarfmodest);
DEFINE_CONFIRMATION(depot_remove, viewscreen_dwarfmodest);
DEFINE_CONFIRMATION(squad_disband, viewscreen_layer_militaryst);
DEFINE_CONFIRMATION(uniform_delete, viewscreen_layer_militaryst);
DEFINE_CONFIRMATION(note_delete, viewscreen_dwarfmodest);
DEFINE_CONFIRMATION(route_delete, viewscreen_dwarfmodest);
DEFINE_CONFIRMATION(location_retire, viewscreen_locationsst);
DEFINE_CONFIRMATION(convict, viewscreen_justicest);
DEFINE_CONFIRMATION(order_remove, viewscreen_jobmanagementst);
DEFINE_CONFIRMATION(trade_cancel, viewscreen_dwarfmodest, "dwarfmode/Trade");
DEFINE_CONFIRMATION(haul_delete_route, viewscreen_dwarfmodest, "dwarfmode/Hauling");
DEFINE_CONFIRMATION(haul_delete_stop, viewscreen_dwarfmodest, "dwarfmode/Hauling");
DEFINE_CONFIRMATION(depot_remove, viewscreen_dwarfmodest, "dwarfmode/ViewSheets/BUILDING");
DEFINE_CONFIRMATION(squad_disband, viewscreen_dwarfmodest, "dwarfmode/Squads");
DEFINE_CONFIRMATION(order_remove, viewscreen_dwarfmodest, "dwarfmode/Info/WORK_ORDERS");
DEFINE_CONFIRMATION(zone_remove, viewscreen_dwarfmodest, "dwarfmode/Zone*");
DEFINE_CONFIRMATION(burrow_remove, viewscreen_dwarfmodest, "dwarfmode/Burrow");
DEFINE_CONFIRMATION(stockpile_remove, viewscreen_dwarfmodest, "dwarfmode/Some/Stockpile");
// these are more complex to implement
//DEFINE_CONFIRMATION(convict, viewscreen_dwarfmodest);
//DEFINE_CONFIRMATION(trade, viewscreen_dwarfmodest);
//DEFINE_CONFIRMATION(trade_seize, viewscreen_dwarfmodest);
//DEFINE_CONFIRMATION(trade_offer, viewscreen_dwarfmodest);
//DEFINE_CONFIRMATION(trade_select_all, viewscreen_dwarfmodest);
//DEFINE_CONFIRMATION(uniform_delete, viewscreen_dwarfmodest);
//DEFINE_CONFIRMATION(note_delete, viewscreen_dwarfmodest);
//DEFINE_CONFIRMATION(route_delete, viewscreen_dwarfmodest);
// locations can't be retired currently
//DEFINE_CONFIRMATION(location_retire, viewscreen_locationsst);
DFhackCExport command_result plugin_init (color_ostream &out, vector <PluginCommand> &commands)
{
@ -585,22 +646,6 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out)
return CR_OK;
}
static bool screen_found(df::viewscreen *target_screen)
{
if (!df::global::gview)
return false;
df::viewscreen *screen = &df::global::gview->view;
while (screen)
{
if (screen == target_screen)
return true;
screen = screen->child;
}
return false;
}
DFhackCExport command_result plugin_onupdate (color_ostream &out)
{
while (!cmds.empty())
@ -609,10 +654,6 @@ DFhackCExport command_result plugin_onupdate (color_ostream &out)
cmds.pop();
}
// if the screen that we paused on is no longer on the stack, unpause
if (paused_screen && !screen_found(paused_screen))
conf_lua::api::unpause(NULL);
return CR_OK;
}
@ -637,6 +678,27 @@ bool set_conf_state (string name, bool state)
return found;
}
bool set_conf_paused (string name, bool pause)
{
bool found = false;
for (auto it : confirmations)
{
if (it.first == name)
{
found = true;
it.second->set_paused(pause);
}
}
if (pause == true)
{
// dismiss the confirmation too
confirmation_base::set_state(name, confirmation_base::INACTIVE);
}
return found;
}
void enable_conf (color_ostream &out, string name, bool state)
{
if (!set_conf_state(name, state))

@ -26,7 +26,6 @@ static const string INVOKE_HOTKEYS_COMMAND = "hotkeys";
static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu";
static bool valid = false; // whether the following two vars contain valid data
static string current_focus;
static map<string, string> current_bindings;
static vector<string> sorted_keys;
@ -38,14 +37,13 @@ static bool can_invoke(const string &cmdline, df::viewscreen *screen) {
}
static int cleanupHotkeys(lua_State *) {
DEBUG(log).print("cleaning up old stub keybindings for %s\n", current_focus.c_str());
DEBUG(log).print("cleaning up old stub keybindings for: %s\n", join_strings(", ", Gui::getCurFocus(true)).c_str());
std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) {
string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING;
DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str());
Core::getInstance().ClearKeyBindings(keyspec);
});
valid = false;
current_focus = "";
sorted_keys.clear();
current_bindings.clear();
return 0;
@ -90,7 +88,6 @@ static void find_active_keybindings(df::viewscreen *screen, bool filtermenu) {
valid_keys.push_back("`");
current_focus = Gui::getFocusString(screen);
for (int shifted = 0; shifted < 2; shifted++) {
for (int alt = 0; alt < 2; alt++) {
for (int ctrl = 0; ctrl < 2; ctrl++) {
@ -112,7 +109,7 @@ static void find_active_keybindings(df::viewscreen *screen, bool filtermenu) {
vector<string> tokens;
split_string(&tokens, *invoke_cmd, ":");
string focus = tokens[0].substr(1);
if (prefix_matches(focus, current_focus)) {
if(Gui::matchFocusString(focus)) {
auto cmdline = trim(tokens[1]);
add_binding_if_valid(sym, cmdline, screen, filtermenu);
}
@ -145,8 +142,8 @@ static void list(color_ostream &out) {
if (!valid)
find_active_keybindings(Gui::getCurViewscreen(true), false);
out.print("Valid keybindings for the current screen (%s)\n",
current_focus.c_str());
out.print("Valid keybindings for the current focus:\n %s\n",
join_strings("\n", Gui::getCurFocus(true)).c_str());
std::for_each(sorted_keys.begin(), sorted_keys.end(), [&](const string &sym) {
out.print("%s: %s\n", sym.c_str(), current_bindings[sym].c_str());
});
@ -158,7 +155,7 @@ static void list(color_ostream &out) {
static bool invoke_command(color_ostream &out, const size_t index) {
auto screen = Core::getTopViewscreen();
if (sorted_keys.size() <= index ||
Gui::getFocusString(screen) != MENU_SCREEN_FOCUS_STRING)
!Gui::matchFocusString(MENU_SCREEN_FOCUS_STRING))
return false;
auto cmd = current_bindings[sorted_keys[index]];

@ -1,7 +1,5 @@
local _ENV = mkmodule('plugins.confirm')
local ui = df.global.plotinfo
local confs = {}
-- Wraps df.interface_key[foo] functionality but fails with invalid keys
keys = {}
@ -11,6 +9,9 @@ setmetatable(keys, {
end,
__newindex = function() error('Table is read-only') end
})
-- Mouse keys will be sent as a string instead of interface_key
local MOUSE_LEFT = "MOUSE_LEFT"
local MOUSE_RIGHT = "MOUSE_RIGHT"
--[[ The screen where a confirmation has been triggered
Note that this is *not* necessarily the topmost viewscreen, so do not use
gui.getCurViewscreen() or related functions. ]]
@ -55,20 +56,95 @@ is equivalent to:
]]
trade_cancel = defconf('trade-cancel')
function trade_cancel.intercept_key(key)
return dfhack.gui.matchFocusString("dwarfmode/Trade") and
(key == keys.LEAVESCREEN or key == MOUSE_RIGHT) and
(trader_goods_selected() or broker_goods_selected())
end
trade_cancel.title = "Cancel trade"
trade_cancel.message = "Are you sure you want leave this screen?\nSelected items will not be saved."
haul_delete_route = defconf('haul-delete-route')
function haul_delete_route.intercept_key(key)
return df.global.game.main_interface.current_hover == 180 and key == MOUSE_LEFT
end
haul_delete_route.title = "Confirm deletion"
haul_delete_route.message = "Are you sure you want to delete this route?"
haul_delete_stop = defconf('haul-delete-stop')
function haul_delete_stop.intercept_key(key)
return df.global.game.main_interface.current_hover == 185 and key == MOUSE_LEFT
end
haul_delete_stop.title = "Confirm deletion"
haul_delete_stop.message = "Are you sure you want to delete this stop?"
depot_remove = defconf('depot-remove')
function depot_remove.intercept_key(key)
if df.global.game.main_interface.current_hover == 299 and
key == MOUSE_LEFT and
df.building_tradedepotst:is_instance(dfhack.gui.getSelectedBuilding(true)) then
for _, caravan in pairs(df.global.plotinfo.caravans) do
if caravan.time_remaining > 0 then
return true
end
end
end
end
depot_remove.title = "Confirm depot removal"
depot_remove.message = "Are you sure you want to remove this depot?\n" ..
"Merchants are present and will lose profits."
squad_disband = defconf('squad-disband')
function squad_disband.intercept_key(key)
return key == MOUSE_LEFT and df.global.game.main_interface.current_hover == 341
end
squad_disband.title = "Disband squad"
squad_disband.message = "Are you sure you want to disband this squad?"
order_remove = defconf('order-remove')
function order_remove.intercept_key(key)
return key == MOUSE_LEFT and df.global.game.main_interface.current_hover == 222
end
order_remove.title = "Remove manager order"
order_remove.message = "Are you sure you want to remove this order?"
zone_remove = defconf('zone-remove')
function zone_remove.intercept_key(key)
return key == MOUSE_LEFT and df.global.game.main_interface.current_hover == 130
end
zone_remove.title = "Remove zone"
zone_remove.message = "Are you sure you want to remove this zone?"
burrow_remove = defconf('burrow-remove')
function burrow_remove.intercept_key(key)
return key == MOUSE_LEFT and df.global.game.main_interface.current_hover == 171
end
burrow_remove.title = "Remove burrow"
burrow_remove.message = "Are you sure you want to remove this burrow?"
stockpile_remove = defconf('stockpile-remove')
function stockpile_remove.intercept_key(key)
return key == MOUSE_LEFT and df.global.game.main_interface.current_hover == 118
end
stockpile_remove.title = "Remove stockpile"
stockpile_remove.message = "Are you sure you want to remove this stockpile?"
-- these confirmations have more complex button detection requirements
--[[
trade = defconf('trade')
function trade.intercept_key(key)
return screen.in_edit_count == 0 and
key == keys.TRADE_TRADE
dfhack.gui.matchFocusString("dwarfmode/Trade") and key == MOUSE_LEFT and hovering over trade button?
end
trade.title = "Confirm trade"
function trade.get_message()
if trader_goods_selected(screen) and broker_goods_selected(screen) then
if trader_goods_selected() and broker_goods_selected() then
return "Are you sure you want to trade the selected goods?"
elseif trader_goods_selected(screen) then
elseif trader_goods_selected() then
return "You are not giving any items. This is likely\n" ..
"to irritate the merchants.\n" ..
"Attempt to trade anyway?"
elseif broker_goods_selected(screen) then
elseif broker_goods_selected() then
return "You are not receiving any items. You may want to\n" ..
"offer these items instead or choose items to receive.\n" ..
"Attempt to trade anyway?"
@ -79,19 +155,10 @@ function trade.get_message()
end
end
trade_cancel = defconf('trade-cancel')
function trade_cancel.intercept_key(key)
return screen.in_edit_count == 0 and
key == keys.LEAVESCREEN and
(trader_goods_selected(screen) or broker_goods_selected(screen))
end
trade_cancel.title = "Cancel trade"
trade_cancel.message = "Are you sure you want leave this screen?\nSelected items will not be saved."
trade_seize = defconf('trade-seize')
function trade_seize.intercept_key(key)
return screen.in_edit_count == 0 and
trader_goods_selected(screen) and
trader_goods_selected() and
key == keys.TRADE_SEIZE
end
trade_seize.title = "Confirm seize"
@ -100,7 +167,7 @@ trade_seize.message = "Are you sure you want to seize these goods?"
trade_offer = defconf('trade-offer')
function trade_offer.intercept_key(key)
return screen.in_edit_count == 0 and
broker_goods_selected(screen) and
broker_goods_selected() and
key == keys.TRADE_OFFER
end
trade_offer.title = "Confirm offer"
@ -109,9 +176,9 @@ trade_offer.message = "Are you sure you want to offer these goods?\nYou will rec
trade_select_all = defconf('trade-select-all')
function trade_select_all.intercept_key(key)
if screen.in_edit_count == 0 and key == keys.SEC_SELECT then
if screen.in_right_pane and broker_goods_selected(screen) and not broker_goods_all_selected(screen) then
if screen.in_right_pane and broker_goods_selected() and not broker_goods_all_selected() then
return true
elseif not screen.in_right_pane and trader_goods_selected(screen) and not trader_goods_all_selected(screen) then
elseif not screen.in_right_pane and trader_goods_selected() and not trader_goods_all_selected() then
return true
end
end
@ -121,49 +188,6 @@ trade_select_all.title = "Confirm selection"
trade_select_all.message = "Selecting all goods will overwrite your current selection\n" ..
"and cannot be undone. Continue?"
haul_delete = defconf('haul-delete')
function haul_delete.intercept_key(key)
if ui.main.mode == df.ui_sidebar_mode.Hauling and
#ui.hauling.view_routes > 0 and
not ui.hauling.in_name and
not ui.hauling.in_stop and
not ui.hauling.in_assign_vehicle then
return key == keys.D_HAULING_REMOVE
end
return false
end
haul_delete.title = "Confirm deletion"
function haul_delete.get_message()
local t = ui.hauling.view_stops[ui.hauling.cursor_top] and "stop" or "route"
return "Are you sure you want to delete this " ..
(ui.hauling.view_stops[ui.hauling.cursor_top] and "stop" or "route") .. "?"
end
depot_remove = defconf('depot-remove')
function depot_remove.intercept_key(key)
if df.building_tradedepotst:is_instance(dfhack.gui.getSelectedBuilding(true)) and
key == keys.DESTROYBUILDING then
for _, caravan in pairs(ui.caravans) do
if caravan.time_remaining > 0 then
return true
end
end
end
end
depot_remove.title = "Confirm depot removal"
depot_remove.message = "Are you sure you want to remove this depot?\n" ..
"Merchants are present and will lose profits."
squad_disband = defconf('squad-disband')
function squad_disband.intercept_key(key)
return key == keys.D_MILITARY_DISBAND_SQUAD and
screen.page == screen._type.T_page.Positions and
screen.num_squads > 0 and
not screen.in_rename_alert
end
squad_disband.title = "Disband squad"
squad_disband.message = "Are you sure you want to disband this squad?"
uniform_delete = defconf('uniform-delete')
function uniform_delete.intercept_key(key)
return key == keys.D_MILITARY_DELETE_UNIFORM and
@ -193,17 +217,6 @@ end
route_delete.title = "Delete route"
route_delete.message = "Are you sure you want to delete this route?"
location_retire = defconf('location-retire')
function location_retire.intercept_key(key)
return key == keys.LOCATION_RETIRE and
(screen.menu == df.viewscreen_locationsst.T_menu.Locations or
screen.menu == df.viewscreen_locationsst.T_menu.Occupations) and
screen.in_edit == df.viewscreen_locationsst.T_in_edit.None and
screen.locations[screen.location_idx]
end
location_retire.title = "Retire location"
location_retire.message = "Are you sure you want to retire this location?"
convict = defconf('convict')
convict.title = "Confirm conviction"
function convict.intercept_key(key)
@ -218,14 +231,21 @@ function convict.get_message()
return "Are you sure you want to convict " .. name .. "?\n" ..
"This action is irreversible."
end
]]--
order_remove = defconf('order-remove')
function order_remove.intercept_key(key)
return key == keys.MANAGER_REMOVE and
not screen.in_max_workshops
-- locations cannot be retired currently
--[[
location_retire = defconf('location-retire')
function location_retire.intercept_key(key)
return key == keys.LOCATION_RETIRE and
(screen.menu == df.viewscreen_locationsst.T_menu.Locations or
screen.menu == df.viewscreen_locationsst.T_menu.Occupations) and
screen.in_edit == df.viewscreen_locationsst.T_in_edit.None and
screen.locations[screen.location_idx]
end
order_remove.title = "Remove manager order"
order_remove.message = "Are you sure you want to remove this order?"
location_retire.title = "Retire location"
location_retire.message = "Are you sure you want to retire this location?"
]]--
-- End of confirmation definitions

@ -196,17 +196,6 @@ static inline char get_string_input(const std::set<df::interface_key> *input)
* Utility Functions
*/
static inline df::building_stockpilest *get_selected_stockpile()
{
if (!Gui::dwarfmode_hotkey(Core::getTopViewscreen()) ||
df::global::plotinfo->main.mode != ui_sidebar_mode::QueryBuilding)
{
return nullptr;
}
return virtual_cast<df::building_stockpilest>(df::global::world->selected_building);
}
static inline bool can_trade()
{
if (df::global::plotinfo->caravans.size() == 0)

@ -1,75 +0,0 @@
config = {
mode = 'fortress',
}
local gui = require('gui')
local guidm = require('gui.dwarfmode')
function test.enterSidebarMode()
expect.error_match('Invalid or unsupported sidebar mode',
function() guidm.enterSidebarMode('badmode') end)
expect.error_match('Invalid or unsupported sidebar mode',
function() guidm.enterSidebarMode(
df.ui_sidebar_mode.OrdersRefuse) end)
expect.error_match('must be a positive number',
function() guidm.enterSidebarMode(0, 'gg') end)
expect.error_match('must be a positive number',
function() guidm.enterSidebarMode(0, 0) end)
expect.error_match('must be a positive number',
function() guidm.enterSidebarMode(0, '0') end)
expect.error_match('must be a positive number',
function() guidm.enterSidebarMode(0, -1) end)
expect.error_match('must be a positive number',
function() guidm.enterSidebarMode(0, '-1') end)
-- Simulate not being able to get to default from a screen via mocks. This
-- failure can actually happen in-game in some situations, such as when
-- naming a building with ctrl-N (no way to cancel changes).
mock.patch({{dfhack.gui, 'getFocusString', mock.func()},
{gui, 'simulateInput', mock.func()}},
function()
expect.error_match('Unable to get into target sidebar mode',
function()
guidm.enterSidebarMode(df.ui_sidebar_mode.Default)
end)
end)
-- verify expected starting state
expect.eq(df.ui_sidebar_mode.Default, df.global.plotinfo.main.mode)
expect.eq('dwarfmode/Default', dfhack.gui.getCurFocus(true))
-- get into the orders screen
gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'D_JOBLIST')
gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'UNITJOB_MANAGER')
expect.eq(df.ui_sidebar_mode.Default, df.global.plotinfo.main.mode)
expect.eq('jobmanagement/Main', dfhack.gui.getCurFocus(true))
-- get back into default from some deep screen
guidm.enterSidebarMode(df.ui_sidebar_mode.Default)
expect.eq(df.ui_sidebar_mode.Default, df.global.plotinfo.main.mode)
expect.eq('dwarfmode/Default', dfhack.gui.getCurFocus(true))
-- move from default to some other mode
guidm.enterSidebarMode(df.ui_sidebar_mode.QueryBuilding)
expect.eq(df.ui_sidebar_mode.QueryBuilding, df.global.plotinfo.main.mode)
expect.str_find('^dwarfmode/QueryBuilding', dfhack.gui.getCurFocus(true))
-- move between non-default modes
guidm.enterSidebarMode(df.ui_sidebar_mode.LookAround)
expect.eq(df.ui_sidebar_mode.LookAround, df.global.plotinfo.main.mode)
expect.str_find('^dwarfmode/LookAround', dfhack.gui.getCurFocus(true))
-- get back into default from a supported mode
guidm.enterSidebarMode(df.ui_sidebar_mode.Default)
expect.eq(df.ui_sidebar_mode.Default, df.global.plotinfo.main.mode)
expect.eq('dwarfmode/Default', dfhack.gui.getCurFocus(true))
-- verify that all supported modes lead where we say they'll go
for k,v in pairs(guidm.SIDEBAR_MODE_KEYS) do
guidm.enterSidebarMode(k)
expect.eq(k, df.global.plotinfo.main.mode, df.ui_sidebar_mode[k])
end
-- end test back in default so the test harness doesn't have to autocorrect
guidm.enterSidebarMode(df.ui_sidebar_mode.Default)
end