1089 lines
31 KiB
Lua
1089 lines
31 KiB
Lua
-- Viewscreen implementation utility collection.
|
|
|
|
local _ENV = mkmodule('gui')
|
|
|
|
local utils = require('utils')
|
|
|
|
local dscreen = dfhack.screen
|
|
local getval = utils.getval
|
|
|
|
local to_pen = dfhack.pen.parse
|
|
|
|
CLEAR_PEN = to_pen{tile=dfhack.internal.getAddress('init') and df.global.init.texpos_border_interior, ch=32, fg=0, bg=0, write_to_lower=true}
|
|
TRANSPARENT_PEN = to_pen{tile=0, ch=0}
|
|
KEEP_LOWER_PEN = to_pen{ch=32, fg=0, bg=0, keep_lower=true}
|
|
|
|
local MOUSE_KEYS = {
|
|
_MOUSE_L = true,
|
|
_MOUSE_R = true,
|
|
_MOUSE_M = true,
|
|
_MOUSE_L_DOWN = true,
|
|
_MOUSE_R_DOWN = true,
|
|
_MOUSE_M_DOWN = true,
|
|
}
|
|
|
|
local FAKE_INPUT_KEYS = copyall(MOUSE_KEYS)
|
|
FAKE_INPUT_KEYS._STRING = true
|
|
|
|
function simulateInput(screen,...)
|
|
local keys = {}
|
|
local function push_key(arg)
|
|
local kv = arg
|
|
if type(arg) == 'string' then
|
|
kv = df.interface_key[arg]
|
|
if kv == nil and not FAKE_INPUT_KEYS[arg] then
|
|
error('Invalid keycode: '..arg)
|
|
end
|
|
end
|
|
if type(kv) == 'number' then
|
|
keys[#keys+1] = kv
|
|
end
|
|
end
|
|
for i = 1,select('#',...) do
|
|
local arg = select(i,...)
|
|
if arg ~= nil then
|
|
local t = type(arg)
|
|
if type(arg) == 'table' then
|
|
for k,v in pairs(arg) do
|
|
if v == true then
|
|
push_key(k)
|
|
else
|
|
push_key(v)
|
|
end
|
|
end
|
|
else
|
|
push_key(arg)
|
|
end
|
|
end
|
|
end
|
|
dscreen._doSimulateInput(screen, keys)
|
|
end
|
|
|
|
function mkdims_xy(x1,y1,x2,y2)
|
|
return { x1=x1, y1=y1, x2=x2, y2=y2, width=x2-x1+1, height=y2-y1+1 }
|
|
end
|
|
function mkdims_wh(x1,y1,w,h)
|
|
return { x1=x1, y1=y1, x2=x1+w-1, y2=y1+h-1, width=w, height=h }
|
|
end
|
|
function is_in_rect(rect,x,y)
|
|
return x and y and x >= rect.x1 and x <= rect.x2 and y >= rect.y1 and y <= rect.y2
|
|
end
|
|
|
|
local function align_coord(gap,align,lv,rv)
|
|
if gap <= 0 then
|
|
return 0
|
|
end
|
|
if not align then
|
|
if rv and not lv then
|
|
align = 1.0
|
|
elseif lv and not rv then
|
|
align = 0.0
|
|
else
|
|
align = 0.5
|
|
end
|
|
end
|
|
return math.floor(gap*align)
|
|
end
|
|
|
|
function compute_frame_rect(wavail,havail,spec,xgap,ygap)
|
|
if not spec then
|
|
return mkdims_wh(0,0,wavail,havail)
|
|
end
|
|
|
|
local sw = wavail - (spec.l or 0) - (spec.r or 0)
|
|
local sh = havail - (spec.t or 0) - (spec.b or 0)
|
|
local rqw = math.min(sw, (spec.w or sw)+xgap)
|
|
local rqh = math.min(sh, (spec.h or sh)+ygap)
|
|
local ax = align_coord(sw - rqw, spec.xalign, spec.l, spec.r)
|
|
local ay = align_coord(sh - rqh, spec.yalign, spec.t, spec.b)
|
|
|
|
local rect = mkdims_wh((spec.l or 0) + ax, (spec.t or 0) + ay, rqw, rqh)
|
|
rect.wgap = sw - rqw
|
|
rect.hgap = sh - rqh
|
|
return rect
|
|
end
|
|
|
|
function parse_inset(inset)
|
|
local l,r,t,b
|
|
if type(inset) == 'table' then
|
|
l,r = inset.l or inset.x or 0, inset.r or inset.x or 0
|
|
t,b = inset.t or inset.y or 0, inset.b or inset.y or 0
|
|
else
|
|
l = inset or 0
|
|
t,r,b = l,l,l
|
|
end
|
|
return l,t,r,b
|
|
end
|
|
|
|
function inset_frame(rect, inset, gap)
|
|
if not rect then return mkdims_wh(0, 0, 0, 0) end
|
|
gap = gap or 0
|
|
local l,t,r,b = parse_inset(inset)
|
|
return mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
|
|
end
|
|
|
|
function compute_frame_body(wavail, havail, spec, inset, gap, inner_frame)
|
|
gap = gap or 0
|
|
local l,t,r,b = parse_inset(inset)
|
|
local xgap,ygap = 0,0
|
|
if inner_frame then
|
|
xgap,ygap = gap*2+l+r, gap*2+t+b
|
|
end
|
|
local rect = compute_frame_rect(wavail, havail, spec, xgap, ygap)
|
|
local body = mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
|
|
return rect, body
|
|
end
|
|
|
|
function blink_visible(delay)
|
|
return math.floor(dfhack.getTickCount()/delay) % 2 == 0
|
|
end
|
|
|
|
function getKeyDisplay(code)
|
|
if type(code) == 'string' then
|
|
code = df.interface_key[code]
|
|
end
|
|
return dscreen.getKeyDisplay(code)
|
|
end
|
|
|
|
-----------------------------------
|
|
-- Clipped view rectangle object --
|
|
-----------------------------------
|
|
|
|
ViewRect = defclass(ViewRect, nil)
|
|
|
|
function ViewRect:init(args)
|
|
if args.view_rect then
|
|
self:assign(args.view_rect)
|
|
else
|
|
local rect = args.rect or mkdims_wh(0,0,dscreen.getWindowSize())
|
|
local crect = args.clip_rect or rect
|
|
self:assign{
|
|
x1 = rect.x1, clip_x1 = crect.x1,
|
|
y1 = rect.y1, clip_y1 = crect.y1,
|
|
x2 = rect.x2, clip_x2 = crect.x2,
|
|
y2 = rect.y2, clip_y2 = crect.y2,
|
|
width = rect.x2-rect.x1+1,
|
|
height = rect.y2-rect.y1+1,
|
|
}
|
|
end
|
|
if args.clip_view then
|
|
local cr = args.clip_view
|
|
self:assign{
|
|
clip_x1 = math.max(self.clip_x1, cr.clip_x1),
|
|
clip_y1 = math.max(self.clip_y1, cr.clip_y1),
|
|
clip_x2 = math.min(self.clip_x2, cr.clip_x2),
|
|
clip_y2 = math.min(self.clip_y2, cr.clip_y2),
|
|
}
|
|
end
|
|
end
|
|
|
|
function ViewRect:isDefunct()
|
|
return (self.clip_x1 > self.clip_x2 or self.clip_y1 > self.clip_y2)
|
|
end
|
|
|
|
function ViewRect:inClipGlobalXY(x,y)
|
|
return x >= self.clip_x1 and x <= self.clip_x2
|
|
and y >= self.clip_y1 and y <= self.clip_y2
|
|
end
|
|
|
|
function ViewRect:inClipLocalXY(x,y)
|
|
return (x+self.x1) >= self.clip_x1 and (x+self.x1) <= self.clip_x2
|
|
and (y+self.y1) >= self.clip_y1 and (y+self.y1) <= self.clip_y2
|
|
end
|
|
|
|
function ViewRect:localXY(x,y)
|
|
return x-self.x1, y-self.y1
|
|
end
|
|
|
|
function ViewRect:globalXY(x,y)
|
|
return x+self.x1, y+self.y1
|
|
end
|
|
|
|
function ViewRect:viewport(x,y,w,h)
|
|
if type(x) == 'table' then
|
|
x,y,w,h = x.x1, x.y1, x.width, x.height
|
|
end
|
|
local x1,y1 = self.x1+x, self.y1+y
|
|
local x2,y2 = x1+w-1, y1+h-1
|
|
local vp = {
|
|
-- Logical viewport
|
|
x1 = x1, y1 = y1, x2 = x2, y2 = y2,
|
|
width = w, height = h,
|
|
-- Actual clipping rect
|
|
clip_x1 = math.max(self.clip_x1, x1),
|
|
clip_y1 = math.max(self.clip_y1, y1),
|
|
clip_x2 = math.min(self.clip_x2, x2),
|
|
clip_y2 = math.min(self.clip_y2, y2),
|
|
}
|
|
return mkinstance(ViewRect, vp)
|
|
end
|
|
|
|
----------------------------
|
|
-- Clipped painter object --
|
|
----------------------------
|
|
|
|
Painter = defclass(Painter, ViewRect)
|
|
|
|
function Painter:init(args)
|
|
self.x = self.x1
|
|
self.y = self.y1
|
|
self.cur_pen = to_pen(args.pen or COLOR_GREY)
|
|
self.cur_key_pen = to_pen(args.key_pen or COLOR_LIGHTGREEN)
|
|
self.to_map = false
|
|
end
|
|
|
|
function Painter.new(rect, pen)
|
|
return Painter{ rect = rect, pen = pen }
|
|
end
|
|
|
|
function Painter.new_view(view_rect, pen)
|
|
return Painter{ view_rect = view_rect, pen = pen }
|
|
end
|
|
|
|
function Painter.new_xy(x1,y1,x2,y2,pen)
|
|
return Painter{ rect = mkdims_xy(x1,y1,x2,y2), pen = pen }
|
|
end
|
|
|
|
function Painter.new_wh(x,y,w,h,pen)
|
|
return Painter{ rect = mkdims_wh(x,y,w,h), pen = pen }
|
|
end
|
|
|
|
function Painter:isValidPos()
|
|
return self:inClipGlobalXY(self.x, self.y)
|
|
end
|
|
|
|
function Painter:viewport(x,y,w,h)
|
|
local vp = ViewRect.viewport(self,x,y,w,h)
|
|
vp.cur_pen = self.cur_pen
|
|
vp.cur_key_pen = self.cur_key_pen
|
|
return mkinstance(Painter, vp):seek(0,0)
|
|
end
|
|
|
|
function Painter:cursor()
|
|
return self.x - self.x1, self.y - self.y1
|
|
end
|
|
|
|
function Painter:cursorX()
|
|
return self.x - self.x1
|
|
end
|
|
|
|
function Painter:cursorY()
|
|
return self.y - self.y1
|
|
end
|
|
|
|
function Painter:seek(x,y)
|
|
if x then self.x = self.x1 + x end
|
|
if y then self.y = self.y1 + y end
|
|
return self
|
|
end
|
|
|
|
function Painter:advance(dx,dy)
|
|
if dx then self.x = self.x + dx end
|
|
if dy then self.y = self.y + dy end
|
|
return self
|
|
end
|
|
|
|
function Painter:newline(dx)
|
|
self.y = self.y + 1
|
|
self.x = self.x1 + (dx or 0)
|
|
return self
|
|
end
|
|
|
|
function Painter:pen(pen,...)
|
|
self.cur_pen = to_pen(self.cur_pen, pen, ...)
|
|
return self
|
|
end
|
|
|
|
function Painter:color(fg,bold,bg)
|
|
self.cur_pen = to_pen(self.cur_pen, fg, bg, bold)
|
|
return self
|
|
end
|
|
|
|
function Painter:key_pen(pen,...)
|
|
self.cur_key_pen = to_pen(self.cur_key_pen, pen, ...)
|
|
return self
|
|
end
|
|
|
|
function Painter:map(to_map)
|
|
self.to_map = to_map
|
|
return self
|
|
end
|
|
|
|
function Painter:clear()
|
|
dscreen.fillRect(CLEAR_PEN, self.clip_x1, self.clip_y1, self.clip_x2, self.clip_y2)
|
|
return self
|
|
end
|
|
|
|
function Painter:fill(x1,y1,x2,y2,pen,bg,bold)
|
|
if type(x1) == 'table' then
|
|
x1, y1, x2, y2, pen, bg, bold = x1.x1, x1.y1, x1.x2, x1.y2, y1, x2, y2
|
|
end
|
|
x1 = math.max(x1+self.x1,self.clip_x1)
|
|
y1 = math.max(y1+self.y1,self.clip_y1)
|
|
x2 = math.min(x2+self.x1,self.clip_x2)
|
|
y2 = math.min(y2+self.y1,self.clip_y2)
|
|
dscreen.fillRect(to_pen(self.cur_pen,pen,bg,bold),x1,y1,x2,y2,self.to_map)
|
|
return self
|
|
end
|
|
|
|
function Painter:char(char,pen,...)
|
|
if self:isValidPos() then
|
|
dscreen.paintTile(to_pen(self.cur_pen, pen, ...), self.x, self.y, char, nil, self.to_map)
|
|
end
|
|
return self:advance(1, nil)
|
|
end
|
|
|
|
function Painter:tile(char,tile,pen,...)
|
|
if self:isValidPos() then
|
|
dscreen.paintTile(to_pen(self.cur_pen, pen, ...), self.x, self.y, char, tile, self.to_map)
|
|
end
|
|
return self:advance(1, nil)
|
|
end
|
|
|
|
function Painter:string(text,pen,...)
|
|
if self.y >= self.clip_y1 and self.y <= self.clip_y2 then
|
|
local dx = 0
|
|
if self.x < self.clip_x1 then
|
|
dx = self.clip_x1 - self.x
|
|
end
|
|
local len = #text
|
|
if self.x + len - 1 > self.clip_x2 then
|
|
len = self.clip_x2 - self.x + 1
|
|
end
|
|
if len > dx then
|
|
dscreen.paintString(
|
|
to_pen(self.cur_pen, pen, ...),
|
|
self.x+dx, self.y,
|
|
string.sub(text,dx+1,len),
|
|
self.to_map
|
|
)
|
|
end
|
|
end
|
|
return self:advance(#text, nil)
|
|
end
|
|
|
|
function Painter:key(keycode,pen,...)
|
|
return self:string(
|
|
getKeyDisplay(keycode),
|
|
to_pen(self.cur_key_pen, pen, ...)
|
|
)
|
|
end
|
|
|
|
function Painter:key_string(keycode, text, ...)
|
|
return self:key(keycode):string(': '):string(text, ...)
|
|
end
|
|
|
|
--------------------------
|
|
-- Abstract view object --
|
|
--------------------------
|
|
|
|
View = defclass(View)
|
|
|
|
View.ATTRS {
|
|
active = true,
|
|
visible = true,
|
|
view_id = DEFAULT_NIL,
|
|
on_focus = DEFAULT_NIL,
|
|
on_unfocus = DEFAULT_NIL,
|
|
}
|
|
|
|
function View:init(args)
|
|
self.subviews = {}
|
|
self.focus_group = {self}
|
|
self.focus = false
|
|
end
|
|
|
|
local function inherit_focus_group(view, focus_group)
|
|
for _,child in ipairs(view.subviews) do
|
|
inherit_focus_group(child, focus_group)
|
|
end
|
|
view.focus_group = focus_group
|
|
end
|
|
|
|
function View:addviews(list)
|
|
if not list then return end
|
|
|
|
local sv = self.subviews
|
|
|
|
for _,obj in ipairs(list) do
|
|
-- absorb the focus groups of new children
|
|
for _,focus_obj in ipairs(obj.focus_group) do
|
|
table.insert(self.focus_group, focus_obj)
|
|
end
|
|
-- if the child's focus group has a focus owner, absorb it if we don't
|
|
-- already have one. otherwise keep the focus owner we have and clear
|
|
-- the focus of the child.
|
|
if obj.focus_group.cur then
|
|
if not self.focus_group.cur then
|
|
self.focus_group.cur = obj.focus_group.cur
|
|
else
|
|
obj.focus_group.cur:setFocus(false)
|
|
end
|
|
end
|
|
-- overwrite the child's focus_group hierarchy with ours
|
|
inherit_focus_group(obj, self.focus_group)
|
|
-- if we don't have a focus owner, give it to the new child if they want
|
|
if not self.focus_group.cur and obj:getPreferredFocusState() then
|
|
obj:setFocus(true)
|
|
end
|
|
|
|
-- set ourselves as the parent view of the new child
|
|
obj.parent_view = self
|
|
|
|
-- add the subview to our child list
|
|
table.insert(sv, obj)
|
|
|
|
local id = obj.view_id
|
|
if id and type(id) ~= 'number' and sv[id] == nil then
|
|
sv[id] = obj
|
|
end
|
|
end
|
|
|
|
for _,dir in ipairs(list) do
|
|
for id,obj in pairs(dir.subviews) do
|
|
if id and type(id) ~= 'number' and sv[id] == nil then
|
|
sv[id] = obj
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- should be overridden by widgets that care about capturing keyboard focus
|
|
-- (e.g. widgets.EditField)
|
|
function View:getPreferredFocusState()
|
|
return false
|
|
end
|
|
|
|
function View:setFocus(focus)
|
|
if focus then
|
|
if self.focus then return end -- nothing to do if we already have focus
|
|
if self.focus_group.cur then
|
|
-- steal focus from current owner
|
|
self.focus_group.cur:setFocus(false)
|
|
end
|
|
self.focus_group.cur = self
|
|
self.focus = true
|
|
if self.on_focus then
|
|
self.on_focus()
|
|
end
|
|
elseif self.focus then
|
|
self.focus = false
|
|
self.focus_group.cur = nil
|
|
if self.on_unfocus then
|
|
self.on_unfocus()
|
|
end
|
|
end
|
|
end
|
|
|
|
function View:getWindowSize()
|
|
local rect = self.frame_body
|
|
return rect.width, rect.height
|
|
end
|
|
|
|
function View:getMousePos(view_rect)
|
|
local rect = view_rect or self.frame_body
|
|
local x,y = dscreen.getMousePos()
|
|
if rect and x and rect:inClipGlobalXY(x,y) then
|
|
return rect:localXY(x,y)
|
|
end
|
|
end
|
|
|
|
function View:getMouseFramePos()
|
|
return self:getMousePos(ViewRect{rect=self.frame_rect})
|
|
end
|
|
|
|
function View:computeFrame(parent_rect)
|
|
return mkdims_wh(0,0,parent_rect.width,parent_rect.height)
|
|
end
|
|
|
|
function View:updateSubviewLayout(frame_body)
|
|
for _,child in ipairs(self.subviews) do
|
|
child:updateLayout(frame_body)
|
|
end
|
|
end
|
|
|
|
function View:updateLayout(parent_rect)
|
|
if not parent_rect then
|
|
parent_rect = self.frame_parent_rect
|
|
else
|
|
self.frame_parent_rect = parent_rect
|
|
end
|
|
|
|
self:invoke_before('preUpdateLayout', parent_rect)
|
|
|
|
local frame_rect,body_rect = self:computeFrame(parent_rect)
|
|
|
|
self.frame_rect = frame_rect
|
|
self.frame_body = parent_rect:viewport(body_rect or frame_rect)
|
|
|
|
self:invoke_after('postComputeFrame', self.frame_body)
|
|
|
|
self:updateSubviewLayout(self.frame_body)
|
|
|
|
self:invoke_after('postUpdateLayout', self.frame_body)
|
|
end
|
|
|
|
function View:renderSubviews(dc)
|
|
for _,child in ipairs(self.subviews) do
|
|
if getval(child.visible) then
|
|
child:render(dc)
|
|
end
|
|
end
|
|
end
|
|
|
|
function View:render(dc)
|
|
self:onRenderFrame(dc, self.frame_rect)
|
|
|
|
local sub_dc = Painter{
|
|
view_rect = self.frame_body,
|
|
clip_view = dc
|
|
}
|
|
|
|
self:onRenderBody(sub_dc)
|
|
|
|
self:renderSubviews(sub_dc)
|
|
end
|
|
|
|
function View:onRenderFrame(dc,rect)
|
|
end
|
|
|
|
function View:onRenderBody(dc)
|
|
end
|
|
|
|
-- Returns whether we should invoke the focus owner's onInput() function from
|
|
-- the given view's inputToSubviews() function. That is, returns true if:
|
|
-- - the view is not itself the focus owner since that would be an infinite loop
|
|
-- - the view is not a descendent of the focus owner (same as above)
|
|
-- - the focus owner and all of its ancestors are visible and active, since if
|
|
-- the focus owner is not (directly or transitively) visible or active, then
|
|
-- it shouldn't be getting input.
|
|
local function should_send_input_to_focus_owner(view, focus_owner)
|
|
local iter = view
|
|
while iter do
|
|
if iter == focus_owner then
|
|
return false
|
|
end
|
|
iter = iter.parent_view
|
|
end
|
|
iter = focus_owner
|
|
while iter do
|
|
if not getval(iter.visible) or not getval(iter.active) then
|
|
return false
|
|
end
|
|
iter = iter.parent_view
|
|
end
|
|
return true
|
|
end
|
|
|
|
function View:inputToSubviews(keys)
|
|
local children = self.subviews
|
|
|
|
-- give focus owner first dibs on the input
|
|
local focus_owner = self.focus_group.cur
|
|
if focus_owner and should_send_input_to_focus_owner(self, focus_owner) and
|
|
focus_owner:onInput(keys) then
|
|
return true
|
|
end
|
|
|
|
for i=#children,1,-1 do
|
|
local child = children[i]
|
|
if getval(child.visible) and getval(child.active)
|
|
and child ~= focus_owner and child:onInput(keys) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function View:onInput(keys)
|
|
return self:inputToSubviews(keys)
|
|
end
|
|
|
|
------------------------
|
|
-- Base screen object --
|
|
------------------------
|
|
|
|
Screen = defclass(Screen, View)
|
|
|
|
Screen.text_input_mode = false
|
|
Screen.request_full_screen_refresh = false
|
|
|
|
function Screen:postinit()
|
|
self:onResize(dscreen.getWindowSize())
|
|
end
|
|
|
|
Screen.isDismissed = dscreen.isDismissed
|
|
|
|
function Screen:isShown()
|
|
return self._native ~= nil
|
|
end
|
|
|
|
function Screen:isActive()
|
|
return self:isShown() and not self:isDismissed()
|
|
end
|
|
|
|
function Screen:invalidate()
|
|
dscreen.invalidate()
|
|
end
|
|
|
|
function Screen:renderParent()
|
|
if self._native and self._native.parent then
|
|
self._native.parent:render()
|
|
else
|
|
dscreen.clear()
|
|
end
|
|
if Screen.request_full_screen_refresh then
|
|
df.global.gps.force_full_display_count = 1
|
|
Screen.request_full_screen_refresh = false
|
|
end
|
|
end
|
|
|
|
function Screen:sendInputToParent(...)
|
|
if self._native and self._native.parent then
|
|
simulateInput(self._native.parent, ...)
|
|
end
|
|
end
|
|
|
|
function Screen:show(parent)
|
|
if self._native then
|
|
error("This screen is already on display")
|
|
end
|
|
self:onAboutToShow(parent or dfhack.gui.getCurViewscreen(true))
|
|
|
|
-- if we're autodetecting the parent, refresh the parent handle in case the
|
|
-- original parent has been dismissed by onAboutToShow()'s actions
|
|
parent = parent or dfhack.gui.getCurViewscreen(true)
|
|
if not dscreen.show(self, parent.child) then
|
|
error('Could not show screen')
|
|
end
|
|
return self
|
|
end
|
|
|
|
function Screen:onAboutToShow(parent)
|
|
end
|
|
|
|
function Screen:onShow()
|
|
self:onResize(dscreen.getWindowSize())
|
|
end
|
|
|
|
function Screen:dismiss()
|
|
if self._native then
|
|
dscreen.dismiss(self)
|
|
end
|
|
-- don't leave artifacts behind on the parent screen when we disappear
|
|
Screen.request_full_screen_refresh = true
|
|
end
|
|
|
|
function Screen:onDismiss()
|
|
end
|
|
|
|
function Screen:onDestroy()
|
|
end
|
|
|
|
function Screen:onResize(w,h)
|
|
self:updateLayout(ViewRect{ rect = mkdims_wh(0,0,w,h) })
|
|
end
|
|
|
|
function Screen:onRender()
|
|
self:render(Painter.new())
|
|
end
|
|
|
|
-----------------------------
|
|
-- Z-order swapping screen --
|
|
-----------------------------
|
|
|
|
DEFAULT_INITIAL_PAUSE = true
|
|
|
|
local zscreen_inhibit_mouse_l = false
|
|
|
|
-- ensure underlying DF screens don't also react to handled clicks
|
|
function markMouseClicksHandled(keys)
|
|
if keys._MOUSE_L_DOWN then
|
|
-- note we can't clear mouse_lbut here. otherwise we break dragging,
|
|
df.global.enabler.mouse_lbut_down = 0
|
|
zscreen_inhibit_mouse_l = true
|
|
end
|
|
if keys._MOUSE_R_DOWN then
|
|
df.global.enabler.mouse_rbut_down = 0
|
|
df.global.enabler.mouse_rbut = 0
|
|
end
|
|
end
|
|
|
|
ZScreen = defclass(ZScreen, Screen)
|
|
ZScreen.ATTRS{
|
|
defocusable=true,
|
|
initial_pause=DEFAULT_NIL,
|
|
force_pause=false,
|
|
pass_pause=true,
|
|
pass_movement_keys=false,
|
|
pass_mouse_clicks=true,
|
|
}
|
|
|
|
function ZScreen:preinit(args)
|
|
if self.ATTRS.initial_pause == nil then
|
|
args.initial_pause = DEFAULT_INITIAL_PAUSE or
|
|
(self.ATTRS.pass_mouse_clicks == false)
|
|
end
|
|
end
|
|
|
|
function ZScreen:init()
|
|
self.saved_pause_state = df.global.pause_state
|
|
if self.initial_pause and dfhack.isMapLoaded() then
|
|
df.global.pause_state = true
|
|
end
|
|
self.defocused = false
|
|
end
|
|
|
|
function ZScreen:dismiss()
|
|
ZScreen.super.dismiss(self)
|
|
if (self.force_pause or self.initial_pause) and dfhack.isMapLoaded() then
|
|
-- never go from unpaused to paused, just from paused to unpaused
|
|
df.global.pause_state = df.global.pause_state and self.saved_pause_state
|
|
end
|
|
end
|
|
|
|
local NO_LOGIC_SCREENS = {
|
|
'viewscreen_loadgamest',
|
|
'viewscreen_adopt_regionst',
|
|
'viewscreen_export_regionst',
|
|
'viewscreen_choose_game_typest',
|
|
'viewscreen_worldst',
|
|
}
|
|
for _,v in ipairs(NO_LOGIC_SCREENS) do
|
|
if not df[v] then
|
|
error('invalid class name: ' .. v)
|
|
end
|
|
NO_LOGIC_SCREENS[df[v]] = true
|
|
end
|
|
|
|
-- this is necessary for middle-click map scrolling to function
|
|
function ZScreen:onIdle()
|
|
if self.force_pause and dfhack.isMapLoaded() then
|
|
df.global.pause_state = true
|
|
end
|
|
if self._native and self._native.parent then
|
|
local vs_type = dfhack.gui.getDFViewscreen(true)._type
|
|
if NO_LOGIC_SCREENS[vs_type] then
|
|
self.force_pause = true
|
|
self.pass_movement_keys = false
|
|
self.pass_mouse_clicks = false
|
|
else
|
|
self._native.parent:logic()
|
|
end
|
|
end
|
|
end
|
|
|
|
function ZScreen:render(dc)
|
|
self:renderParent()
|
|
ZScreen.super.render(self, dc)
|
|
end
|
|
|
|
function ZScreen:hasFocus()
|
|
return not self.defocused
|
|
and dfhack.gui.getCurViewscreen(true) == self._native
|
|
end
|
|
|
|
function ZScreen:onInput(keys)
|
|
local has_mouse = self:isMouseOver()
|
|
if not self:hasFocus() then
|
|
if has_mouse and
|
|
(keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or
|
|
keys.CONTEXT_SCROLL_UP or keys.CONTEXT_SCROLL_DOWN or
|
|
keys.CONTEXT_SCROLL_PAGEUP or keys.CONTEXT_SCROLL_PAGEDOWN) then
|
|
self:raise()
|
|
else
|
|
self:sendInputToParent(keys)
|
|
return
|
|
end
|
|
end
|
|
|
|
if ZScreen.super.onInput(self, keys) then
|
|
markMouseClicksHandled(keys)
|
|
return
|
|
end
|
|
|
|
if self.pass_mouse_clicks and keys._MOUSE_L_DOWN and not has_mouse then
|
|
self.defocused = self.defocusable
|
|
self:sendInputToParent(keys)
|
|
return
|
|
elseif keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
|
|
self:dismiss()
|
|
markMouseClicksHandled(keys)
|
|
return
|
|
else
|
|
if zscreen_inhibit_mouse_l then
|
|
if keys._MOUSE_L then
|
|
return
|
|
else
|
|
zscreen_inhibit_mouse_l = false
|
|
end
|
|
end
|
|
local passit = self.pass_pause and keys.D_PAUSE
|
|
if not passit and self.pass_mouse_clicks then
|
|
if keys.CONTEXT_SCROLL_UP or keys.CONTEXT_SCROLL_DOWN or
|
|
keys.CONTEXT_SCROLL_PAGEUP or keys.CONTEXT_SCROLL_PAGEDOWN then
|
|
passit = true
|
|
else
|
|
for key in pairs(MOUSE_KEYS) do
|
|
if keys[key] then
|
|
passit = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if not passit and self.pass_movement_keys then
|
|
passit = require('gui.dwarfmode').getMapKey(keys)
|
|
end
|
|
if passit then
|
|
self:sendInputToParent(keys)
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
function ZScreen:raise()
|
|
if self:isDismissed() or self:hasFocus() then
|
|
return self
|
|
end
|
|
dscreen.raise(self)
|
|
self.defocused = false
|
|
return self
|
|
end
|
|
|
|
function ZScreen:isMouseOver()
|
|
for _,sv in ipairs(self.subviews) do
|
|
if sv:getMouseFramePos() then return true end
|
|
end
|
|
end
|
|
|
|
local function zscreen_get_any(scr, thing)
|
|
if not scr._native or not scr._native.parent then return nil end
|
|
return dfhack.gui['getAny'..thing](scr._native.parent)
|
|
end
|
|
function ZScreen:onGetSelectedUnit()
|
|
return zscreen_get_any(self, 'Unit')
|
|
end
|
|
function ZScreen:onGetSelectedItem()
|
|
return zscreen_get_any(self, 'Item')
|
|
end
|
|
function ZScreen:onGetSelectedBuilding()
|
|
return zscreen_get_any(self, 'Building')
|
|
end
|
|
function ZScreen:onGetSelectedPlant()
|
|
return zscreen_get_any(self, 'Plant')
|
|
end
|
|
|
|
-- convenience subclass for modal dialogs
|
|
ZScreenModal = defclass(ZScreenModal, ZScreen)
|
|
ZScreenModal.ATTRS{
|
|
defocusable = false,
|
|
force_pause = true,
|
|
pass_pause = false,
|
|
pass_movement_keys = false,
|
|
pass_mouse_clicks = false,
|
|
}
|
|
|
|
-- DFHack textures
|
|
--------------------------
|
|
|
|
-- Preloaded DFHack Assets
|
|
-- Use this handles if you need to get dfhack standard textures
|
|
local texpos_handles = {
|
|
green_pin = dfhack.textures.loadTileset('hack/data/art/green-pin.png', 8, 12),
|
|
red_pin = dfhack.textures.loadTileset('hack/data/art/red-pin.png', 8, 12),
|
|
icons = dfhack.textures.loadTileset('hack/data/art/icons.png', 8, 12),
|
|
on_off = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12),
|
|
control_panel = dfhack.textures.loadTileset('hack/data/art/control-panel.png', 8, 12),
|
|
border_thin = dfhack.textures.loadTileset('hack/data/art/border-thin.png', 8, 12),
|
|
border_medium = dfhack.textures.loadTileset('hack/data/art/border-medium.png', 8, 12),
|
|
border_bold = dfhack.textures.loadTileset('hack/data/art/border-bold.png', 8, 12),
|
|
border_panel = dfhack.textures.loadTileset('hack/data/art/border-panel.png', 8, 12),
|
|
border_window = dfhack.textures.loadTileset('hack/data/art/border-window.png', 8, 12),
|
|
}
|
|
|
|
-- Methods to obtain valid texposes by handles
|
|
function tp_green_pin(offset)
|
|
return dfhack.textures.getTexposByHandle(texpos_handles.green_pin[offset])
|
|
end
|
|
function tp_red_pin(offset)
|
|
return dfhack.textures.getTexposByHandle(texpos_handles.red_pin[offset])
|
|
end
|
|
function tp_icons(offset)
|
|
return dfhack.textures.getTexposByHandle(texpos_handles.icons[offset])
|
|
end
|
|
function tp_on_off(offset)
|
|
return dfhack.textures.getTexposByHandle(texpos_handles.on_off[offset])
|
|
end
|
|
function tp_control_panel(offset)
|
|
return dfhack.textures.getTexposByHandle(texpos_handles.control_panel[offset])
|
|
end
|
|
function tp_border_thin(offset)
|
|
return dfhack.textures.getTexposByHandle(texpos_handles.border_thin[offset])
|
|
end
|
|
function tp_border_medium(offset)
|
|
return dfhack.textures.getTexposByHandle(texpos_handles.border_medium[offset])
|
|
end
|
|
function tp_border_bold(offset)
|
|
return dfhack.textures.getTexposByHandle(texpos_handles.border_bold[offset])
|
|
end
|
|
function tp_border_panel(offset)
|
|
return dfhack.textures.getTexposByHandle(texpos_handles.border_panel[offset])
|
|
end
|
|
function tp_border_window(offset)
|
|
return dfhack.textures.getTexposByHandle(texpos_handles.border_window[offset])
|
|
end
|
|
|
|
-- Framed screen object
|
|
--------------------------
|
|
|
|
-- Plain grey-colored frame.
|
|
-- deprecated
|
|
GREY_FRAME = {
|
|
frame_pen = to_pen{ ch = ' ', fg = COLOR_BLACK, bg = COLOR_GREY },
|
|
title_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_WHITE },
|
|
signature_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
|
|
}
|
|
|
|
-- The boundary used by the pre-steam DF screens.
|
|
-- deprecated
|
|
BOUNDARY_FRAME = {
|
|
frame_pen = to_pen{ ch = 0xDB, fg = COLOR_GREY, bg = COLOR_BLACK },
|
|
title_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
|
|
signature_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
|
|
}
|
|
|
|
local BASE_FRAME = {
|
|
frame_pen = to_pen{ ch=206, fg=COLOR_GREY, bg=COLOR_BLACK },
|
|
title_pen = to_pen{ fg=COLOR_BLACK, bg=COLOR_GREY },
|
|
inactive_title_pen = to_pen{ fg=COLOR_GREY, bg=COLOR_BLACK },
|
|
signature_pen = to_pen{ fg=COLOR_GREY, bg=COLOR_BLACK },
|
|
paused_pen = to_pen{fg=COLOR_RED, bg=COLOR_BLACK},
|
|
}
|
|
|
|
|
|
local function make_frame(tp, double_line)
|
|
local frame = copyall(BASE_FRAME)
|
|
frame.t_frame_pen = to_pen{ tile=curry(tp, 2), ch=double_line and 205 or 196, fg=COLOR_GREY, bg=COLOR_BLACK }
|
|
frame.l_frame_pen = to_pen{ tile=curry(tp, 8), ch=double_line and 186 or 179, fg=COLOR_GREY, bg=COLOR_BLACK }
|
|
frame.b_frame_pen = to_pen{ tile=curry(tp, 16), ch=double_line and 205 or 196, fg=COLOR_GREY, bg=COLOR_BLACK }
|
|
frame.r_frame_pen = to_pen{ tile=curry(tp, 10), ch=double_line and 186 or 179, fg=COLOR_GREY, bg=COLOR_BLACK }
|
|
frame.lt_frame_pen = to_pen{ tile=curry(tp, 1), ch=double_line and 201 or 218, fg=COLOR_GREY, bg=COLOR_BLACK }
|
|
frame.lb_frame_pen = to_pen{ tile=curry(tp, 15), ch=double_line and 200 or 192, fg=COLOR_GREY, bg=COLOR_BLACK }
|
|
frame.rt_frame_pen = to_pen{ tile=curry(tp, 3), ch=double_line and 187 or 191, fg=COLOR_GREY, bg=COLOR_BLACK }
|
|
frame.rb_frame_pen = to_pen{ tile=curry(tp, 17), ch=double_line and 188 or 217, fg=COLOR_GREY, bg=COLOR_BLACK }
|
|
return frame
|
|
end
|
|
|
|
function FRAME_WINDOW(resizable)
|
|
local frame = make_frame(tp_border_window, true)
|
|
if not resizable then
|
|
frame.rb_frame_pen = to_pen{ tile=curry(tp_border_panel, 17), ch=double_line and 188 or 217, fg=COLOR_GREY, bg=COLOR_BLACK }
|
|
end
|
|
return frame
|
|
end
|
|
function FRAME_PANEL(resizable)
|
|
return make_frame(tp_border_panel, false)
|
|
end
|
|
function FRAME_MEDIUM(resizable)
|
|
return make_frame(tp_border_medium, false)
|
|
end
|
|
function FRAME_BOLD(resizable)
|
|
return make_frame(tp_border_bold, true)
|
|
end
|
|
function FRAME_INTERIOR(resizable)
|
|
local frame = make_frame(tp_border_thin, false)
|
|
frame.signature_pen = false
|
|
return frame
|
|
end
|
|
function FRAME_INTERIOR_MEDIUM(resizable)
|
|
local frame = make_frame(tp_border_medium, false)
|
|
frame.signature_pen = false
|
|
return frame
|
|
end
|
|
|
|
-- for compatibility with pre-steam code
|
|
GREY_LINE_FRAME = FRAME_PANEL
|
|
|
|
-- for compatibility with deprecated frame naming scheme
|
|
WINDOW_FRAME = FRAME_WINDOW
|
|
PANEL_FRAME = FRAME_PANEL
|
|
MEDIUM_FRAME = FRAME_MEDIUM
|
|
BOLD_FRAME = FRAME_BOLD
|
|
INTERIOR_FRAME = FRAME_INTERIOR
|
|
INTERIOR_MEDIUM_FRAME = FRAME_INTERIOR_MEDIUM
|
|
|
|
function paint_frame(dc, rect, style, title, inactive, pause_forced, resizable)
|
|
if type(style) == 'function' then
|
|
style = style(resizable)
|
|
end
|
|
local pen = style.frame_pen
|
|
local x1,y1,x2,y2 = dc.x1+rect.x1, dc.y1+rect.y1, dc.x1+rect.x2, dc.y1+rect.y2
|
|
dscreen.paintTile(style.lt_frame_pen or pen, x1, y1)
|
|
dscreen.paintTile(style.rt_frame_pen or pen, x2, y1)
|
|
dscreen.paintTile(style.lb_frame_pen or pen, x1, y2)
|
|
dscreen.paintTile(style.rb_frame_pen or pen, x2, y2)
|
|
dscreen.fillRect(style.t_frame_pen or style.h_frame_pen or pen,x1+1,y1,x2-1,y1)
|
|
dscreen.fillRect(style.b_frame_pen or style.h_frame_pen or pen,x1+1,y2,x2-1,y2)
|
|
dscreen.fillRect(style.l_frame_pen or style.v_frame_pen or pen,x1,y1+1,x1,y2-1)
|
|
dscreen.fillRect(style.r_frame_pen or style.v_frame_pen or pen,x2,y1+1,x2,y2-1)
|
|
if style.signature_pen ~= false then
|
|
dscreen.paintString(style.signature_pen or style.title_pen or pen,x2-7,y2,"DFHack")
|
|
end
|
|
|
|
if title then
|
|
local x = math.max(0,math.floor((x2-x1-3-#title)/2)) + x1
|
|
local tstr = ' '..title..' '
|
|
if #tstr > x2-x1-1 then
|
|
tstr = string.sub(tstr,1,x2-x1-1)
|
|
end
|
|
dscreen.paintString(inactive and style.inactive_title_pen or style.title_pen or pen,
|
|
x, y1, tstr)
|
|
end
|
|
|
|
if pause_forced then
|
|
dscreen.paintString(style.paused_pen or style.title_pen or pen,
|
|
x1+2, y2, ' PAUSE FORCED ')
|
|
end
|
|
end
|
|
|
|
FramedScreen = defclass(FramedScreen, Screen)
|
|
|
|
FramedScreen.ATTRS{
|
|
frame_style = BOUNDARY_FRAME,
|
|
frame_title = DEFAULT_NIL,
|
|
frame_width = DEFAULT_NIL,
|
|
frame_height = DEFAULT_NIL,
|
|
frame_inset = 0,
|
|
frame_background = CLEAR_PEN,
|
|
}
|
|
|
|
function FramedScreen:getWantedFrameSize()
|
|
return self.frame_width, self.frame_height
|
|
end
|
|
|
|
function FramedScreen:computeFrame(parent_rect)
|
|
local sw, sh = parent_rect.width, parent_rect.height
|
|
local fw, fh = self:getWantedFrameSize(parent_rect)
|
|
return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1, true)
|
|
end
|
|
|
|
function FramedScreen:onRenderFrame(dc, rect)
|
|
if rect.wgap <= 0 and rect.hgap <= 0 then
|
|
dc:clear()
|
|
else
|
|
self:renderParent()
|
|
dc:fill(rect, self.frame_background)
|
|
end
|
|
paint_frame(dc,rect,self.frame_style,self.frame_title)
|
|
end
|
|
|
|
-- Inverts the brightness of the color, optionally taking a "bold" parameter,
|
|
-- which you should include if you're reading the fg color of a pen.
|
|
function invert_color(color, bold)
|
|
color = bold and (color + 8) or color
|
|
return (color + 8) % 16
|
|
end
|
|
return _ENV
|