Merge pull request #2367 from myk002/myk_overlay_v2
[overlay] implement overlay v2 frameworkdevelop
commit
63410d63c7
@ -0,0 +1,302 @@
|
||||
.. _overlay-dev-guide:
|
||||
|
||||
DFHack overlay dev guide
|
||||
=========================
|
||||
|
||||
.. highlight:: lua
|
||||
|
||||
This guide walks you through how to build overlay widgets and register them with
|
||||
the `overlay` framework for injection into Dwarf Fortress viewscreens.
|
||||
|
||||
Why would I want to create an overlay widget?
|
||||
---------------------------------------------
|
||||
|
||||
There are both C++ and Lua APIs for creating viewscreens and drawing to the
|
||||
screen. If you need very specific low-level control, those APIs might be the
|
||||
right choice for you. However, here are some reasons you might want to implement
|
||||
an overlay widget instead:
|
||||
|
||||
1. You can draw directly to an existing viewscreen instead of creating an
|
||||
entirely new screen on the viewscreen stack. This allows the original
|
||||
viewscreen to continue processing uninterrupted and keybindings bound to
|
||||
that viewscreen will continue to function. This was previously only
|
||||
achievable by C++ plugins.
|
||||
|
||||
1. You'll get a free UI for enabling/disabling your widget and repositioning it
|
||||
on the screen. Widget state is saved for you and is automatically restored
|
||||
when the game is restarted.
|
||||
|
||||
1. You don't have to manage the C++ interposing logic yourself and can focus on
|
||||
the business logic, writing purely in Lua if desired.
|
||||
|
||||
In general, if you are writing a plugin or script and have anything you'd like
|
||||
to add to an existing screen (including live updates of map tiles while the game
|
||||
is unpaused), an overlay widget is probably your easiest path to get it done. If
|
||||
your plugin or script doesn't otherwise need to be enabled to function, using
|
||||
the overlay allows you to avoid writing any of the enable management code that
|
||||
would normally be required for you to show info in the UI.
|
||||
|
||||
Overlay widget API
|
||||
------------------
|
||||
|
||||
Overlay widgets are Lua classes that inherit from ``overlay.OverlayWidget``
|
||||
(which itself inherits from `widgets.Widget <widget>`). The regular
|
||||
``onInput(keys)``, ``onRenderFrame(dc, frame_rect)``, and ``onRenderBody(dc)``
|
||||
functions work as normal, and they are called when the viewscreen that the
|
||||
widget is associated with does its usual input and render processing. The widget
|
||||
gets first dibs on input processing. If a widget returns ``true`` from its
|
||||
``onInput()`` function, the viewscreen will not receive the input.
|
||||
|
||||
Overlay widgets can contain other Widgets and be as simple or complex as you
|
||||
need them to be, just like you're building a regular UI element.
|
||||
|
||||
There are a few extra capabilities that overlay widgets have that take them
|
||||
beyond your everyday ``Widget``:
|
||||
|
||||
- If an ``overlay_onupdate(viewscreen)`` function is defined, it will be called
|
||||
just after the associated viewscreen's ``logic()`` function is called (i.e.
|
||||
a "tick" or a (non-graphical) "frame"). For hotspot widgets, this function
|
||||
will also get called after the top viewscreen's ``logic()`` function is
|
||||
called, regardless of whether the widget is associated with that viewscreen.
|
||||
If this function returns ``true``, then the widget's ``overlay_trigger()``
|
||||
function is immediately called. Note that the ``viewscreen`` parameter will
|
||||
be ``nil`` for hotspot widgets that are not also associated with the current
|
||||
viewscreen.
|
||||
- If an ``overlay_trigger()`` function is defined, will be called when the
|
||||
widget's ``overlay_onupdate`` callback returns true or when the player uses
|
||||
the CLI (or a keybinding calling the CLI) to trigger the widget. The
|
||||
function must return either ``nil`` or the ``gui.Screen`` object that the
|
||||
widget code has allocated, shown, and now owns. Hotspot widgets will receive
|
||||
no callbacks from unassociated viewscreens until the returned screen is
|
||||
dismissed. Unbound hotspot widgets **must** allocate a Screen with this
|
||||
function if they want to react to the ``onInput()`` feed or be rendered. The
|
||||
widgets owned by the overlay framework must not be attached to that new
|
||||
screen, but the returned screen can instantiate and configure any new views
|
||||
that it wants to.
|
||||
|
||||
If the widget can take up a variable amount of space on the screen, and you want
|
||||
the widget to adjust its position according to the size of its contents, you can
|
||||
modify ``self.frame.w`` and ``self.frame.h`` at any time -- in ``init()`` or in
|
||||
any of the callbacks -- to indicate a new size. The overlay framework will
|
||||
detect the size change and adjust the widget position and layout.
|
||||
|
||||
If you don't need to dynamically resize, just set ``self.frame.w`` and
|
||||
``self.frame.h`` once in ``init()``.
|
||||
|
||||
Widget attributes
|
||||
*****************
|
||||
|
||||
The ``overlay.OverlayWidget`` superclass defines the following class attributes:
|
||||
|
||||
- ``name``
|
||||
This will be filled in with the display name of your widget, in case you
|
||||
have multiple widgets with the same implementation but different
|
||||
configurations.
|
||||
- ``default_pos`` (default: ``{x=-2, y=-2}``)
|
||||
Override this attribute with your desired default widget position. See
|
||||
the `overlay` docs for information on what positive and negative numbers
|
||||
mean for the position. Players can change the widget position at any time
|
||||
via the `overlay position <overlay>` command, so don't assume that your
|
||||
widget will always be at the default position.
|
||||
- ``viewscreens`` (default: ``{}``)
|
||||
The list of viewscreens that this widget should be associated with. When
|
||||
one of these viewscreens is on top of the viewscreen stack, your widget's
|
||||
callback functions for update, input, and render will be interposed into the
|
||||
viewscreen's call path. The name of the viewscreen is the name of the DFHack
|
||||
class that represents the viewscreen, minus the ``viewscreen_`` prefix and
|
||||
``st`` suffix. For example, the fort mode main map viewscreen would be
|
||||
``dwarfmode`` and the adventure mode map viewscreen would be
|
||||
``dungeonmode``. If there is only one viewscreen that this widget is
|
||||
associated with, it can be specified as a string instead of a list of
|
||||
strings with a single element.
|
||||
- ``hotspot`` (default: ``false``)
|
||||
If set to ``true``, your widget's ``overlay_onupdate`` function will be
|
||||
called whenever the `overlay` plugin's ``plugin_onupdate()`` function is
|
||||
called (which corresponds to one call per call to the current top
|
||||
viewscreen's ``logic()`` function). This call to ``overlay_onupdate`` is in
|
||||
addition to any calls initiated from associated interposed viewscreens and
|
||||
will come after calls from associated viewscreens.
|
||||
- ``overlay_onupdate_max_freq_seconds`` (default: ``5``)
|
||||
This throttles how often a widget's ``overlay_onupdate`` function can be
|
||||
called (from any source). Set this to the largest amount of time (in
|
||||
seconds) that your widget can take to react to changes in information and
|
||||
not annoy the player. Set to 0 to be called at the maximum rate. Be aware
|
||||
that running more often than you really need to will impact game FPS,
|
||||
especially if your widget can run while the game is unpaused.
|
||||
|
||||
Registering a widget with the overlay framework
|
||||
***********************************************
|
||||
|
||||
Anywhere in your code after the widget classes are declared, define a table
|
||||
named ``OVERLAY_WIDGETS``. The keys are the display names for your widgets and
|
||||
the values are the widget classes. For example, the `dwarfmonitor` widgets are
|
||||
declared like this::
|
||||
|
||||
OVERLAY_WIDGETS = {
|
||||
cursor=CursorWidget,
|
||||
date=DateWidget,
|
||||
misery=MiseryWidget,
|
||||
weather=WeatherWidget,
|
||||
}
|
||||
|
||||
When the `overlay` plugin is enabled, it scans all plugins and scripts for
|
||||
this table and registers the widgets on your behalf. The widget is enabled if it
|
||||
was enabled the last time the `overlay` plugin was loaded and the widget's
|
||||
position is restored according to the state saved in the
|
||||
:file:`dfhack-config/overlay.json` file.
|
||||
|
||||
The overlay framework will instantiate widgets from the named classes and own
|
||||
the resulting objects. The instantiated widgets must not be added as subviews to
|
||||
any other View, including the Screen views that can be returned from the
|
||||
``overlay_trigger()`` function.
|
||||
|
||||
Widget example 1: adding text to a DF screen
|
||||
--------------------------------------------
|
||||
|
||||
This is a simple widget that displays a message at its position. The message
|
||||
text is retrieved from the host script or plugin every ~20 seconds or when
|
||||
the :kbd:`Alt`:kbd:`Z` hotkey is hit::
|
||||
|
||||
local overlay = require('plugins.overlay')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
MessageWidget = defclass(MessageWidget, overlay.OverlayWidget)
|
||||
MessageWidget.ATTRS{
|
||||
default_pos={x=5,y=-2},
|
||||
viewscreens={'dwarfmode', 'dungeonmode'},
|
||||
overlay_onupdate_max_freq_seconds=20,
|
||||
}
|
||||
|
||||
function MessageWidget:init()
|
||||
self.label = widgets.Label{text=''}
|
||||
self:addviews{self.label}
|
||||
end
|
||||
|
||||
function MessageWidget:overlay_onupdate()
|
||||
local text = getImportantMessage() -- defined in the host script/plugin
|
||||
self.label:setText(text)
|
||||
self.frame.w = #text
|
||||
end
|
||||
|
||||
function MessageWidget:onInput(keys)
|
||||
if keys.CUSTOM_ALT_Z then
|
||||
self:overlay_onupdate()
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
OVERLAY_WIDGETS = {message=MessageWidget}
|
||||
|
||||
Widget example 2: highlighting artifacts on the live game map
|
||||
-------------------------------------------------------------
|
||||
|
||||
This widget is not rendered at its "position" at all, but instead monitors the
|
||||
map and overlays information about where artifacts are located. Scanning for
|
||||
which artifacts are visible on the map can slow, so that is only done every 10
|
||||
seconds to avoid slowing down the entire game on every frame.
|
||||
|
||||
::
|
||||
|
||||
local overlay = require('plugins.overlay')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
ArtifactRadarWidget = defclass(ArtifactRadarWidget, overlay.OverlayWidget)
|
||||
ArtifactRadarWidget.ATTRS{
|
||||
viewscreens={'dwarfmode', 'dungeonmode'},
|
||||
overlay_onupdate_max_freq_seconds=10,
|
||||
}
|
||||
|
||||
function ArtifactRadarWidget:overlay_onupdate()
|
||||
self.visible_artifacts_coords = getVisibleArtifactCoords()
|
||||
end
|
||||
|
||||
function ArtifactRadarWidget:onRenderFrame()
|
||||
for _,pos in ipairs(self.visible_artifacts_coords) do
|
||||
-- highlight tile at given coordinates
|
||||
end
|
||||
end
|
||||
|
||||
OVERLAY_WIDGETS = {radar=ArtifactRadarWidget}
|
||||
|
||||
Widget example 3: corner hotspot
|
||||
--------------------------------
|
||||
|
||||
This hotspot reacts to mouseover events and launches a screen that can react to
|
||||
input events. The hotspot area is a 2x2 block near the lower right corner of the
|
||||
screen (by default, but the player can move it wherever).
|
||||
|
||||
::
|
||||
|
||||
local overlay = require('plugins.overlay')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget)
|
||||
HotspotMenuWidget.ATTRS{
|
||||
default_pos={x=-3,y=-3},
|
||||
frame={w=2, h=2},
|
||||
hotspot=true,
|
||||
viewscreens='dwarfmode',
|
||||
overlay_onupdate_max_freq_seconds=0, -- check for mouseover every tick
|
||||
}
|
||||
|
||||
function HotspotMenuWidget:init()
|
||||
-- note this label only gets rendered on the associated viewscreen
|
||||
-- (dwarfmode), but the hotspot is active on all screens
|
||||
self:addviews{widgets.Label{text={'!!', NEWLINE, '!!'}}}
|
||||
self.mouseover = false
|
||||
end
|
||||
|
||||
function HotspotMenuWidget:overlay_onupdate()
|
||||
local hasMouse = self:getMousePos()
|
||||
if hasMouse and not self.mouseover then -- only trigger on mouse entry
|
||||
self.mouseover = true
|
||||
return true
|
||||
end
|
||||
self.mouseover = hasMouse
|
||||
end
|
||||
|
||||
function HotspotMenuWidget:overlay_trigger()
|
||||
return MenuScreen{hotspot_frame=self.frame}:show()
|
||||
end
|
||||
|
||||
OVERLAY_WIDGETS = {menu=HotspotMenuWidget}
|
||||
|
||||
MenuScreen = defclass(MenuScreen, gui.Screen)
|
||||
MenuScreen.ATTRS{
|
||||
focus_path='hotspot/menu',
|
||||
hotspot_frame=DEFAULT_NIL,
|
||||
}
|
||||
|
||||
function MenuScreen:init()
|
||||
self.mouseover = false
|
||||
|
||||
-- derrive the menu frame from the hotspot frame so it
|
||||
-- can appear in a nearby location
|
||||
local frame = copyall(self.hotspot_frame)
|
||||
-- ...
|
||||
|
||||
self:addviews{
|
||||
widgets.ResizingPanel{
|
||||
autoarrange_subviews=true,
|
||||
frame=frame,
|
||||
frame_style=gui.GREY_LINE_FRAME,
|
||||
frame_background=gui.CLEAR_PEN,
|
||||
subviews={
|
||||
-- ...
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function MenuScreen:onInput(keys)
|
||||
if keys.LEAVESCREEN then
|
||||
self:dismiss()
|
||||
return true
|
||||
end
|
||||
return self:inputToSubviews(keys)
|
||||
end
|
||||
|
||||
function MenuScreen:onRenderFrame(dc, rect)
|
||||
self:renderParent()
|
||||
end
|
@ -0,0 +1,485 @@
|
||||
local _ENV = mkmodule('plugins.overlay')
|
||||
|
||||
local gui = require('gui')
|
||||
local json = require('json')
|
||||
local utils = require('utils')
|
||||
local widgets = require('gui.widgets')
|
||||
|
||||
local OVERLAY_CONFIG_FILE = 'dfhack-config/overlay.json'
|
||||
local OVERLAY_WIDGETS_VAR = 'OVERLAY_WIDGETS'
|
||||
|
||||
local DEFAULT_X_POS, DEFAULT_Y_POS = -2, -2
|
||||
|
||||
-- ---------------- --
|
||||
-- state and config --
|
||||
-- ---------------- --
|
||||
|
||||
local active_triggered_widget = nil
|
||||
local active_triggered_screen = nil -- if non-nil, hotspots will not get updates
|
||||
local widget_db = {} -- map of widget name to ephermeral state
|
||||
local widget_index = {} -- ordered list of widget names
|
||||
local overlay_config = {} -- map of widget name to persisted state
|
||||
local active_hotspot_widgets = {} -- map of widget names to the db entry
|
||||
local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db
|
||||
|
||||
local function reset()
|
||||
if active_triggered_screen then
|
||||
active_triggered_screen:dismiss()
|
||||
end
|
||||
active_triggered_widget = nil
|
||||
active_triggered_screen = nil
|
||||
|
||||
widget_db = {}
|
||||
widget_index = {}
|
||||
|
||||
local ok, config = pcall(json.decode_file, OVERLAY_CONFIG_FILE)
|
||||
overlay_config = ok and config or {}
|
||||
|
||||
active_hotspot_widgets = {}
|
||||
active_viewscreen_widgets = {}
|
||||
end
|
||||
|
||||
local function save_config()
|
||||
if not safecall(json.encode_file, overlay_config, OVERLAY_CONFIG_FILE) then
|
||||
dfhack.printerr(('failed to save overlay config file: "%s"')
|
||||
:format(path))
|
||||
end
|
||||
end
|
||||
|
||||
local function triggered_screen_has_lock()
|
||||
if not active_triggered_screen then return false end
|
||||
if active_triggered_screen:isActive() then return true end
|
||||
active_triggered_widget = nil
|
||||
active_triggered_screen = nil
|
||||
return false
|
||||
end
|
||||
|
||||
-- ----------- --
|
||||
-- utility fns --
|
||||
-- ----------- --
|
||||
|
||||
local function normalize_list(element_or_list)
|
||||
if type(element_or_list) == 'table' then return element_or_list end
|
||||
return {element_or_list}
|
||||
end
|
||||
|
||||
-- normalize "short form" viewscreen names to "long form"
|
||||
local function normalize_viewscreen_name(vs_name)
|
||||
if vs_name:match('viewscreen_.*st') then return vs_name end
|
||||
return 'viewscreen_' .. vs_name .. 'st'
|
||||
end
|
||||
|
||||
-- reduce "long form" viewscreen names to "short form"
|
||||
local function simplify_viewscreen_name(vs_name)
|
||||
_,_,short_name = vs_name:find('^viewscreen_(.*)st$')
|
||||
if short_name then return short_name end
|
||||
return vs_name
|
||||
end
|
||||
|
||||
local function is_empty(tbl)
|
||||
for _ in pairs(tbl) do
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function sanitize_pos(pos)
|
||||
local x = math.floor(tonumber(pos.x) or DEFAULT_X_POS)
|
||||
local y = math.floor(tonumber(pos.y) or DEFAULT_Y_POS)
|
||||
-- if someone accidentally uses 0-based instead of 1-based indexing, fix it
|
||||
if x == 0 then x = 1 end
|
||||
if y == 0 then y = 1 end
|
||||
return {x=x, y=y}
|
||||
end
|
||||
|
||||
local function make_frame(pos, old_frame)
|
||||
old_frame = old_frame or {}
|
||||
local frame = {w=old_frame.w, h=old_frame.h}
|
||||
if pos.x < 0 then frame.r = math.abs(pos.x) - 1 else frame.l = pos.x - 1 end
|
||||
if pos.y < 0 then frame.b = math.abs(pos.y) - 1 else frame.t = pos.y - 1 end
|
||||
return frame
|
||||
end
|
||||
|
||||
local function get_screen_rect()
|
||||
local w, h = dfhack.screen.getWindowSize()
|
||||
return gui.ViewRect{rect=gui.mkdims_wh(0, 0, w, h)}
|
||||
end
|
||||
|
||||
-- ------------- --
|
||||
-- CLI functions --
|
||||
-- ------------- --
|
||||
|
||||
local function get_name(name_or_number)
|
||||
local num = tonumber(name_or_number)
|
||||
if num and widget_index[num] then
|
||||
return widget_index[num]
|
||||
end
|
||||
return tostring(name_or_number)
|
||||
end
|
||||
|
||||
local function do_by_names_or_numbers(args, fn)
|
||||
local arglist = normalize_list(args)
|
||||
if #arglist == 0 then
|
||||
dfhack.printerr('please specify a widget name or list number')
|
||||
return
|
||||
end
|
||||
for _,name_or_number in ipairs(arglist) do
|
||||
local name = get_name(name_or_number)
|
||||
local db_entry = widget_db[name]
|
||||
if not db_entry then
|
||||
dfhack.printerr(('widget not found: "%s"'):format(name))
|
||||
else
|
||||
fn(name, db_entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function do_enable(args, quiet, skip_save)
|
||||
local enable_fn = function(name, db_entry)
|
||||
overlay_config[name].enabled = true
|
||||
if db_entry.widget.hotspot then
|
||||
active_hotspot_widgets[name] = db_entry
|
||||
end
|
||||
for _,vs_name in ipairs(normalize_list(db_entry.widget.viewscreens)) do
|
||||
vs_name = normalize_viewscreen_name(vs_name)
|
||||
ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry
|
||||
end
|
||||
if not quiet then
|
||||
print(('enabled widget %s'):format(name))
|
||||
end
|
||||
end
|
||||
if args[1] == 'all' then
|
||||
for name,db_entry in pairs(widget_db) do
|
||||
if not overlay_config[name].enabled then
|
||||
enable_fn(name, db_entry)
|
||||
end
|
||||
end
|
||||
else
|
||||
do_by_names_or_numbers(args, enable_fn)
|
||||
end
|
||||
if not skip_save then
|
||||
save_config()
|
||||
end
|
||||
end
|
||||
|
||||
local function do_disable(args)
|
||||
local disable_fn = function(name, db_entry)
|
||||
overlay_config[name].enabled = false
|
||||
if db_entry.widget.hotspot then
|
||||
active_hotspot_widgets[name] = nil
|
||||
end
|
||||
for _,vs_name in ipairs(normalize_list(db_entry.widget.viewscreens)) do
|
||||
vs_name = normalize_viewscreen_name(vs_name)
|
||||
ensure_key(active_viewscreen_widgets, vs_name)[name] = nil
|
||||
if is_empty(active_viewscreen_widgets[vs_name]) then
|
||||
active_viewscreen_widgets[vs_name] = nil
|
||||
end
|
||||
end
|
||||
print(('disabled widget %s'):format(name))
|
||||
end
|
||||
if args[1] == 'all' then
|
||||
for name,db_entry in pairs(widget_db) do
|
||||
if overlay_config[name].enabled then
|
||||
disable_fn(name, db_entry)
|
||||
end
|
||||
end
|
||||
else
|
||||
do_by_names_or_numbers(args, disable_fn)
|
||||
end
|
||||
save_config()
|
||||
end
|
||||
|
||||
local function do_list(args)
|
||||
local filter = args and #args > 0
|
||||
local num_filtered = 0
|
||||
for i,name in ipairs(widget_index) do
|
||||
if filter then
|
||||
local passes = false
|
||||
for _,str in ipairs(args) do
|
||||
if name:find(str) then
|
||||
passes = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not passes then
|
||||
num_filtered = num_filtered + 1
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
local db_entry = widget_db[name]
|
||||
local enabled = overlay_config[name].enabled
|
||||
dfhack.color(enabled and COLOR_LIGHTGREEN or COLOR_YELLOW)
|
||||
dfhack.print(enabled and '[enabled] ' or '[disabled]')
|
||||
dfhack.color()
|
||||
print((' %d) %s'):format(i, name))
|
||||
::continue::
|
||||
end
|
||||
if num_filtered > 0 then
|
||||
print(('(%d widgets filtered out)'):format(num_filtered))
|
||||
end
|
||||
end
|
||||
|
||||
local function load_widget(name, widget_class)
|
||||
local widget = widget_class{name=name}
|
||||
widget_db[name] = {
|
||||
widget=widget,
|
||||
next_update_ms=widget.overlay_onupdate and 0 or math.huge,
|
||||
}
|
||||
if not overlay_config[name] then overlay_config[name] = {} end
|
||||
local config = overlay_config[name]
|
||||
config.pos = sanitize_pos(config.pos or widget.default_pos)
|
||||
widget.frame = make_frame(config.pos, widget.frame)
|
||||
if config.enabled then
|
||||
do_enable(name, true, true)
|
||||
else
|
||||
config.enabled = false
|
||||
end
|
||||
end
|
||||
|
||||
local function load_widgets(env_prefix, provider, env_fn)
|
||||
local env_name = env_prefix .. provider
|
||||
local ok, provider_env = pcall(env_fn, env_name)
|
||||
if not ok or not provider_env[OVERLAY_WIDGETS_VAR] then return end
|
||||
local overlay_widgets = provider_env[OVERLAY_WIDGETS_VAR]
|
||||
if type(overlay_widgets) ~= 'table' then
|
||||
dfhack.printerr(
|
||||
('error loading overlay widgets from "%s": %s map is malformed')
|
||||
:format(env_name, OVERLAY_WIDGETS_VAR))
|
||||
return
|
||||
end
|
||||
for widget_name,widget_class in pairs(overlay_widgets) do
|
||||
local name = provider .. '.' .. widget_name
|
||||
if not safecall(load_widget, name, widget_class) then
|
||||
dfhack.printerr(('error loading overlay widget "%s"'):format(name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- called directly from cpp on plugin enable
|
||||
function reload()
|
||||
reset()
|
||||
|
||||
for _,plugin in ipairs(dfhack.internal.listPlugins()) do
|
||||
load_widgets('plugins.', plugin, require)
|
||||
end
|
||||
for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do
|
||||
local files = dfhack.filesystem.listdir_recursive(
|
||||
script_path, nil, false)
|
||||
if not files then goto skip_path end
|
||||
for _,f in ipairs(files) do
|
||||
if not f.isdir and
|
||||
f.path:endswith('.lua') and
|
||||
not f.path:startswith('test/') and
|
||||
not f.path:startswith('internal/') then
|
||||
local script_name = f.path:sub(1, #f.path - 4) -- remove '.lua'
|
||||
load_widgets('', script_name, reqscript)
|
||||
end
|
||||
end
|
||||
::skip_path::
|
||||
end
|
||||
|
||||
for name in pairs(widget_db) do
|
||||
table.insert(widget_index, name)
|
||||
end
|
||||
table.sort(widget_index)
|
||||
|
||||
reposition_widgets()
|
||||
end
|
||||
|
||||
local function dump_widget_config(name, widget)
|
||||
local pos = overlay_config[name].pos
|
||||
print(('widget %s is positioned at x=%d, y=%d'):format(name, pos.x, pos.y))
|
||||
local viewscreens = normalize_list(widget.viewscreens)
|
||||
if #viewscreens > 0 then
|
||||
print(' it will be attached to the following viewscreens:')
|
||||
for _,vs in ipairs(viewscreens) do
|
||||
print((' %s'):format(simplify_viewscreen_name(vs)))
|
||||
end
|
||||
end
|
||||
if widget.hotspot then
|
||||
print(' it will act as a hotspot on all screens')
|
||||
end
|
||||
end
|
||||
|
||||
local function do_position(args)
|
||||
local name_or_number, x, y = table.unpack(args)
|
||||
local name = get_name(name_or_number)
|
||||
if not widget_db[name] then
|
||||
if not name_or_number then
|
||||
dfhack.printerr('please specify a widget name or list number')
|
||||
else
|
||||
dfhack.printerr(('widget not found: "%s"'):format(name))
|
||||
end
|
||||
return
|
||||
end
|
||||
local widget = widget_db[name].widget
|
||||
local pos
|
||||
if x == 'default' then
|
||||
pos = sanitize_pos(widget.default_pos)
|
||||
else
|
||||
x, y = tonumber(x), tonumber(y)
|
||||
if not x or not y then
|
||||
dump_widget_config(name, widget)
|
||||
return
|
||||
end
|
||||
pos = sanitize_pos{x=x, y=y}
|
||||
end
|
||||
overlay_config[name].pos = pos
|
||||
widget.frame = make_frame(pos, widget.frame)
|
||||
widget:updateLayout(get_screen_rect())
|
||||
save_config()
|
||||
print(('repositioned widget %s to x=%d, y=%d'):format(name, pos.x, pos.y))
|
||||
end
|
||||
|
||||
-- note that the widget does not have to be enabled to be triggered
|
||||
local function do_trigger(args)
|
||||
if triggered_screen_has_lock() then
|
||||
dfhack.printerr(('cannot trigger widget; widget "%s" is already active')
|
||||
:format(active_triggered_widget))
|
||||
return
|
||||
end
|
||||
do_by_names_or_numbers(args[1], function(name, db_entry)
|
||||
local widget = db_entry.widget
|
||||
if widget.overlay_trigger then
|
||||
active_triggered_screen = widget:overlay_trigger()
|
||||
if active_triggered_screen then
|
||||
active_triggered_widget = name
|
||||
end
|
||||
print(('triggered widget %s'):format(name))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local command_fns = {
|
||||
enable=do_enable,
|
||||
disable=do_disable,
|
||||
list=do_list,
|
||||
position=do_position,
|
||||
trigger=do_trigger,
|
||||
}
|
||||
|
||||
local HELP_ARGS = utils.invert{'help', '--help', '-h'}
|
||||
|
||||
function overlay_command(args)
|
||||
local command = table.remove(args, 1) or 'help'
|
||||
if HELP_ARGS[command] or not command_fns[command] then return false end
|
||||
command_fns[command](args)
|
||||
return true
|
||||
end
|
||||
|
||||
-- ---------------- --
|
||||
-- event management --
|
||||
-- ---------------- --
|
||||
|
||||
local function detect_frame_change(widget, fn)
|
||||
local frame = widget.frame
|
||||
local w, h = frame.w, frame.h
|
||||
local ret = fn()
|
||||
if w ~= frame.w or h ~= frame.h then
|
||||
widget:updateLayout()
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local function get_next_onupdate_timestamp(now_ms, widget)
|
||||
local freq_s = widget.overlay_onupdate_max_freq_seconds
|
||||
if freq_s == 0 then
|
||||
return now_ms
|
||||
end
|
||||
local freq_ms = math.floor(freq_s * 1000)
|
||||
local jitter = math.random(0, freq_ms // 8) -- up to ~12% jitter
|
||||
return now_ms + freq_ms - jitter
|
||||
end
|
||||
|
||||
-- reduces the next call by a small random amount to introduce jitter into the
|
||||
-- widget processing timings
|
||||
local function do_update(name, db_entry, now_ms, vs)
|
||||
if db_entry.next_update_ms > now_ms then return end
|
||||
local w = db_entry.widget
|
||||
db_entry.next_update_ms = get_next_onupdate_timestamp(now_ms, w)
|
||||
if detect_frame_change(w, function() return w:overlay_onupdate(vs) end) then
|
||||
active_triggered_screen = w:overlay_trigger()
|
||||
if active_triggered_screen then
|
||||
active_triggered_widget = name
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function update_hotspot_widgets()
|
||||
if triggered_screen_has_lock() then return end
|
||||
local now_ms = dfhack.getTickCount()
|
||||
for name,db_entry in pairs(active_hotspot_widgets) do
|
||||
if do_update(name, db_entry, now_ms) then return end
|
||||
end
|
||||
end
|
||||
|
||||
-- not subject to trigger lock since these widgets are already filtered by
|
||||
-- viewscreen
|
||||
function update_viewscreen_widgets(vs_name, vs)
|
||||
local vs_widgets = active_viewscreen_widgets[vs_name]
|
||||
if not vs_widgets then return end
|
||||
local now_ms = dfhack.getTickCount()
|
||||
for name,db_entry in pairs(vs_widgets) do
|
||||
if do_update(name, db_entry, now_ms, vs) then return end
|
||||
end
|
||||
end
|
||||
|
||||
function feed_viewscreen_widgets(vs_name, keys)
|
||||
local vs_widgets = active_viewscreen_widgets[vs_name]
|
||||
if not vs_widgets then return false end
|
||||
for _,db_entry in pairs(vs_widgets) do
|
||||
local w = db_entry.widget
|
||||
if detect_frame_change(w, function() return w:onInput(keys) end) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function render_viewscreen_widgets(vs_name)
|
||||
local vs_widgets = active_viewscreen_widgets[vs_name]
|
||||
if not vs_widgets then return false end
|
||||
local dc = gui.Painter.new()
|
||||
for _,db_entry in pairs(vs_widgets) do
|
||||
local w = db_entry.widget
|
||||
detect_frame_change(w, function() w:render(dc) end)
|
||||
end
|
||||
end
|
||||
|
||||
-- called when the DF window is resized
|
||||
function reposition_widgets()
|
||||
local sr = get_screen_rect()
|
||||
for _,db_entry in pairs(widget_db) do
|
||||
db_entry.widget:updateLayout(sr)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------- --
|
||||
-- OverlayWidget (base class of all overlay widgets) --
|
||||
-- ------------------------------------------------- --
|
||||
|
||||
OverlayWidget = defclass(OverlayWidget, widgets.Widget)
|
||||
OverlayWidget.ATTRS{
|
||||
name=DEFAULT_NIL, -- this is set by the framework to the widget name
|
||||
default_pos={x=DEFAULT_X_POS, y=DEFAULT_Y_POS}, -- 1-based widget screen pos
|
||||
hotspot=false, -- whether to call overlay_onupdate on all screens
|
||||
viewscreens={}, -- override with associated viewscreen or list of viewscrens
|
||||
overlay_onupdate_max_freq_seconds=5, -- throttle calls to overlay_onupdate
|
||||
}
|
||||
|
||||
function OverlayWidget:init()
|
||||
if self.overlay_onupdate_max_freq_seconds < 0 then
|
||||
error(('overlay_onupdate_max_freq_seconds must be >= 0: %s')
|
||||
:format(tostring(self.overlay_onupdate_max_freq_seconds)))
|
||||
end
|
||||
|
||||
-- set defaults for frame. the widget is expected to keep these up to date
|
||||
-- when display contents change so the widget position can shift if the
|
||||
-- frame is relative to the right or bottom edges.
|
||||
self.frame = self.frame or {}
|
||||
self.frame.w = self.frame.w or 5
|
||||
self.frame.h = self.frame.h or 1
|
||||
end
|
||||
|
||||
return _ENV
|
Loading…
Reference in New Issue