From 849dfa5ec2097b03acf9639b42def262adb90af5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 14 Feb 2024 03:26:20 -0800 Subject: [PATCH 1/2] make gui/petitions resizable smaller --- gui/petitions.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/petitions.lua b/gui/petitions.lua index 20a5c3350a..cada0d2c3d 100644 --- a/gui/petitions.lua +++ b/gui/petitions.lua @@ -9,6 +9,7 @@ Petitions.ATTRS { frame_title='Petitions', frame={w=110, h=30}, resizable=true, + resize_min={w=70, h=20}, } function Petitions:init() From 717268a3a2207b1db856eec7eae2be8f0f60fd2b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 14 Feb 2024 03:27:08 -0800 Subject: [PATCH 2/2] initial version of notification panel --- docs/gui/notify.rst | 26 ++++ gui/notify.lua | 195 ++++++++++++++++++++++++++++++ internal/notify/notifications.lua | 185 ++++++++++++++++++++++++++++ 3 files changed, 406 insertions(+) create mode 100644 docs/gui/notify.rst create mode 100644 gui/notify.lua create mode 100644 internal/notify/notifications.lua diff --git a/docs/gui/notify.rst b/docs/gui/notify.rst new file mode 100644 index 0000000000..916f24749a --- /dev/null +++ b/docs/gui/notify.rst @@ -0,0 +1,26 @@ +gui/notify +========== + +.. dfhack-tool:: + :summary: Show notifications for important events. + :tags: fort interface + +This tool is the configuration interface for the provided overlay. It allows +you to select which notifications to enable for the overlay display. See the +descriptions in the `gui/notify` list for more details on what each +notification is for. + +Usage +----- + +:: + + gui/notify + +Overlay +------- + +This script provides an overlay that shows the currently enabled notifications +(when applicable). If you click on an active notification in the list, it will +zoom the map to the target. If there are multiple targets, each successive +click on the notification will zoom to the next target. diff --git a/gui/notify.lua b/gui/notify.lua new file mode 100644 index 0000000000..3110da4abb --- /dev/null +++ b/gui/notify.lua @@ -0,0 +1,195 @@ +--@module = true + +local gui = require('gui') +local notifications = reqscript('internal/notify/notifications') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +-- +-- NotifyOverlay +-- + +local LIST_MAX_HEIGHT = 5 + +NotifyOverlay = defclass(NotifyOverlay, overlay.OverlayWidget) +NotifyOverlay.ATTRS{ + desc='Shows list of active notifications.', + default_pos={x=1,y=-4}, + default_enabled=true, + viewscreens='dwarfmode/Default', + frame={w=30, h=LIST_MAX_HEIGHT+2}, +} + +function NotifyOverlay:init() + self:addviews{ + widgets.Panel{ + view_id='panel', + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + subviews={ + widgets.List{ + view_id='list', + frame={t=0, b=0, l=0, r=0}, + on_submit=function(_, choice) + choice.state = choice.data.on_click(choice.state) + end, + }, + }, + }, + widgets.ConfigureButton{ + frame={t=0, r=2}, + on_click=function() dfhack.run_script('gui/notify') end, + } + } +end + +function NotifyOverlay:overlay_onupdate() + local choices = {} + local max_width = 20 + for _, notification in ipairs(notifications.NOTIFICATIONS_BY_IDX) do + if notifications.config.data[notification.name].enabled then + local str = notification.fn() + if str then + max_width = math.max(max_width, #str) + table.insert(choices, { + text=str, + data=notification, + }) + end + end + end + -- +2 for the frame + self.frame.w = max_width + 2 + if #choices <= LIST_MAX_HEIGHT then + self.frame.h = #choices + 2 + else + self.frame.w = self.frame.w + 3 -- for the scrollbar + self.frame.h = LIST_MAX_HEIGHT + 2 + end + local list = self.subviews.list + local idx = 1 + local _, selected = list:getSelected() + if selected then + for i, v in ipairs(choices) do + if v.name == selected.name then + idx = i + break + end + end + end + list:setChoices(choices, idx) + self.visible = #choices > 0 +end + +OVERLAY_WIDGETS = { + panel=NotifyOverlay, +} + +-- +-- Notify +-- + +Notify = defclass(Notify, widgets.Window) +Notify.ATTRS{ + frame_title='Notification settings', + frame={w=40, h=17}, +} + +function Notify:init() + self:addviews{ + widgets.List{ + view_id='list', + frame={t=0, l=0, b=6}, + on_submit=self:callback('toggle'), + on_select=function(_, choice) + self.subviews.desc.text_to_wrap = choice.desc + if self.frame_parent_rect then + self:updateLayout() + end + end, + }, + widgets.WrappedLabel{ + view_id='desc', + frame={b=3, l=0, h=3}, + auto_height=false, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + label='Toggle', + key='SELECT', + auto_width=true, + on_activate=function() self:toggle(self.subviews.list:getSelected()) end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=15}, + label='Toggle all', + key='CUSTOM_CTRL_A', + auto_width=true, + on_activate=self:callback('toggle_all'), + }, + } + + self:refresh() +end + +function Notify:refresh() + local choices = {} + for name, conf in pairs(notifications.config.data) do + table.insert(choices, { + name=name, + desc=notifications.NOTIFICATIONS_BY_NAME[name].desc, + enabled=conf.enabled, + text={ + ('%20s: '):format(name), + { + text=conf.enabled and 'Enabled' or 'Disabled', + pen=conf.enabled and COLOR_GREEN or COLOR_RED, + } + } + }) + end + table.sort(choices, function(a, b) return a.name < b.name end) + local list = self.subviews.list + local selected = list:getSelected() + list:setChoices(choices) + list:setSelected(selected) +end + +function Notify:toggle(_, choice) + if not choice then return end + notifications.config.data[choice.name].enabled = not choice.enabled + self:refresh() +end + +function Notify:toggle_all() + local choice = self.subviews.list:getChoices()[1] + if not choice then return end + local target_state = not choice.enabled + for name in pairs(notifications.NOTIFICATIONS_BY_NAME) do + notifications.config.data[name].enabled = target_state + end + self:refresh() +end + +-- +-- NotifyScreen +-- + +NotifyScreen = defclass(NotifyScreen, gui.ZScreen) +NotifyScreen.ATTRS { + focus_path='notify', +} + +function NotifyScreen:init() + self:addviews{Notify{}} +end + +function NotifyScreen:onDismiss() + view = nil +end + +if dfhack_flags.module then + return +end + +view = view and view:raise() or NotifyScreen{}:show() diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua new file mode 100644 index 0000000000..261dac70fd --- /dev/null +++ b/internal/notify/notifications.lua @@ -0,0 +1,185 @@ +--@module = true + +local json = require('json') +local list_agreements = reqscript('list-agreements') + +local CONFIG_FILE = 'dfhack-config/notify.json' + +local caravans = df.global.plotinfo.caravans + +local function get_active_depot() + for _, bld in ipairs(df.global.world.buildings.other.TRADE_DEPOT) do + if bld:getBuildStage() == bld:getMaxBuildStage() and + (#bld.jobs == 0 or bld.jobs[0].job_type ~= df.job_type.DestroyBuilding) + then + return bld + end + end +end + +local function for_agitated_creature(fn) + for _, unit in ipairs(df.global.world.units.active) do + if not dfhack.units.isDead(unit) and + not unit.flags1.caged and + unit.flags4.agitated_wilderness_creature + then + if fn(unit) then return end + end + end +end + +local function for_invader(fn) + for _, unit in ipairs(df.global.world.units.active) do + if not dfhack.units.isDead(unit) and + not unit.flags1.caged and + dfhack.units.isInvader(unit) and + not dfhack.units.isHidden(unit) + then + if fn(unit) then return end + end + end +end + +local function count_units(for_fn, which) + local count = 0 + for_fn(function() count = count + 1 end) + if count > 0 then + return ('%d %s%s %s on the map'):format( + count, + which, + count == 1 and '' or 's', + count == 1 and 'is' or 'are' + ) + end +end + +local function zoom_to_next(for_fn, state) + local first_found, ret + for_fn(function(unit) + if not first_found then + first_found = unit + end + if not state then + dfhack.gui.revealInDwarfmodeMap( + xyz2pos(dfhack.units.getPosition(unit)), true, true) + ret = unit.id + return true + elseif unit.id == state then + state = nil + end + end) + if ret then return ret end + if first_found then + dfhack.gui.revealInDwarfmodeMap( + xyz2pos(dfhack.units.getPosition(first_found)), true, true) + return first_found.id + end +end + +NOTIFICATIONS_BY_IDX = { + { + name='traders_ready', + desc='Notifies when traders are ready to trade at the depot.', + fn=function() + if #caravans == 0 then return end + local bld = get_active_depot() + if not bld then return end + local trader_goods_in_depot = {} + for _, contained_item in ipairs(bld.contained_items) do + local item = contained_item.item + if item.flags.trader then + trader_goods_in_depot[item.id] = true + for _, binned_item in ipairs(dfhack.items.getContainedItems(item)) do + if binned_item.flags.trader then + trader_goods_in_depot[binned_item.id] = true + end + end + end + end + local num_ready = 0 + for _, car in ipairs(caravans) do + for _, item_id in ipairs(car.goods) do + local item = df.item.find(item_id) + if item and item.flags.trader and + not trader_goods_in_depot[item_id] + then + print('trader item not found: ', item_id) + goto skip + end + end + num_ready = num_ready + 1 + ::skip:: + end + if num_ready > 0 then + return ('%d trader%s %s ready to trade'):format( + num_ready, + num_ready == 1 and '' or 's', + num_ready == 1 and 'is' or 'are' + ) + end + end, + on_click=function() + local bld = get_active_depot() + if bld then + dfhack.gui.revealInDwarfmodeMap( + xyz2pos(bld.centerx, bld.centery, bld.z), true, true) + end + end, + }, + { + name='petitions_agreed', + desc='Notifies when you have agreed to build (but have not yet built) a guildhall or temple.', + fn=function() + local t_agr, g_agr = list_agreements.get_fort_agreements(true) + local sum = #t_agr + #g_agr + if sum > 0 then + return ('%d petition%s outstanding'):format( + sum, sum == 1 and '' or 's') + end + end, + on_click=function() dfhack.run_script('gui/petitions') end, + }, + { + name='agitated_count', + desc='Notifies when there are agitated animals on the map.', + fn=curry(count_units, for_agitated_creature, 'agitated animal'), + on_click=curry(zoom_to_next, for_agitated_creature), + }, + { + name='invader_count', + desc='Notifies when there are active invaders on the map.', + fn=curry(count_units, for_invader, 'invader'), + on_click=curry(zoom_to_next, for_invader), + }, +} + +NOTIFICATIONS_BY_NAME = {} +for _, v in ipairs(NOTIFICATIONS_BY_IDX) do + NOTIFICATIONS_BY_NAME[v.name] = v +end + +local function get_config() + local f = json.open(CONFIG_FILE) + local updated = false + if f.exists then + -- remove unknown or out of date entries from the loaded config + for k, v in pairs(f.data) do + if not NOTIFICATIONS_BY_NAME[k] or NOTIFICATIONS_BY_NAME[k].version ~= v.version then + updated = true + f.data[k] = nil + end + end + end + for k, v in pairs(NOTIFICATIONS_BY_NAME) do + if not f.data[k] or f.data[k].version ~= v.version then + f.data[k] = {enabled=true, version=v.version} + updated = true + end + end + if updated then + f:write() + end + return f +end + +config = get_config()