Overhaul the concept of lua 'class' initialization yet again.

develop
Alexander Gavrilov 2012-09-18 20:30:25 +04:00
parent a7998f71a2
commit 57b72831ca
11 changed files with 285 additions and 150 deletions

@ -0,0 +1,150 @@
-- A trivial reloadable class system
local _ENV = mkmodule('class')
-- Metatable template for a class
class_obj = {} or class_obj
-- Methods shared by all classes
common_methods = {} or common_methods
-- Forbidden names for class fields and methods.
reserved_names = { super = true, ATTRS = true }
-- Attribute table metatable
attrs_meta = {} or attrs_meta
-- Create or updates a class; a class has metamethods and thus own metatable.
function defclass(class,parent)
class = class or {}
local meta = getmetatable(class)
if not meta then
meta = {}
setmetatable(class, meta)
end
for k,v in pairs(class_obj) do meta[k] = v end
meta.__index = parent or common_methods
local attrs = rawget(class, 'ATTRS') or {}
setmetatable(attrs, attrs_meta)
rawset(class, 'super', parent)
rawset(class, 'ATTRS', attrs)
rawset(class, '__index', rawget(class, '__index') or class)
return class
end
-- An instance uses the class as metatable
function mkinstance(class,table)
table = table or {}
setmetatable(table, class)
return table
end
-- Patch the stubs in the global environment
dfhack.BASE_G.defclass = _ENV.defclass
dfhack.BASE_G.mkinstance = _ENV.mkinstance
-- Just verify the name, and then set.
function class_obj:__newindex(name,val)
if reserved_names[name] or common_methods[name] then
error('Method name '..name..' is reserved.')
end
rawset(self, name, val)
end
function attrs_meta:__call(attrs)
for k,v in pairs(attrs) do
self[k] = v
end
end
local function apply_attrs(obj, attrs, init_table)
for k,v in pairs(attrs) do
if v == DEFAULT_NIL then
v = nil
end
obj[k] = init_table[k] or v
end
end
local function invoke_before_rec(self, class, method, ...)
local meta = getmetatable(class)
if meta then
local fun = rawget(class, method)
if fun then
fun(self, ...)
end
invoke_before_rec(self, meta.__index, method, ...)
end
end
local function invoke_after_rec(self, class, method, ...)
local meta = getmetatable(class)
if meta then
invoke_after_rec(self, meta.__index, method, ...)
local fun = rawget(class, method)
if fun then
fun(self, ...)
end
end
end
local function init_attrs_rec(obj, class, init_table)
local meta = getmetatable(class)
if meta then
init_attrs_rec(obj, meta.__index, init_table)
apply_attrs(obj, rawget(class, 'ATTRS'), init_table)
end
end
-- Call metamethod constructs the object
function class_obj:__call(init_table)
-- The table is assumed to be a scratch temporary.
-- If it is not, copy it yourself before calling.
init_table = init_table or {}
local obj = mkinstance(self)
-- This initialization sequence is broadly based on how the
-- Common Lisp initialize-instance generic function works.
-- preinit screens input arguments in subclass to superclass order
invoke_before_rec(obj, self, 'preinit', init_table)
-- initialize the instance table from init table
init_attrs_rec(obj, self, init_table)
-- init in superclass -> subclass
invoke_after_rec(obj, self, 'init', init_table)
-- postinit in superclass -> subclass
invoke_after_rec(obj, self, 'postinit', init_table)
return obj
end
-- Common methods for all instances:
function common_methods:callback(method, ...)
return dfhack.curry(self[method], self, ...)
end
function common_methods:assign(data)
for k,v in pairs(data) do
self[k] = v
end
end
function common_methods:invoke_before(method, ...)
return invoke_before_rec(self, getmetatable(self), method, ...)
end
function common_methods:invoke_after(method, ...)
return invoke_after_rec(self, getmetatable(self), method, ...)
end
return _ENV

@ -113,26 +113,14 @@ function rawset_default(target,source)
end end
end end
function defclass(class,parent) DEFAULT_NIL = DEFAULT_NIL or {} -- Unique token
class = class or {}
rawset_default(class, { __index = class }) function defclass(...)
if parent then return require('class').defclass(...)
setmetatable(class, parent)
else
rawset_default(class, {
init_fields = rawset_default,
callback = function(self, name, ...)
return dfhack.curry(self[name], self, ...)
end
})
end
return class
end end
function mkinstance(class,table) function mkinstance(...)
table = table or {} return require('class').mkinstance(...)
setmetatable(table, class)
return table
end end
-- Misc functions -- Misc functions

@ -74,18 +74,23 @@ end
Painter = defclass(Painter, nil) Painter = defclass(Painter, nil)
function Painter.new(rect, pen) function Painter:init(args)
rect = rect or mkdims_wh(0,0,dscreen.getWindowSize()) local rect = args.rect or mkdims_wh(0,0,dscreen.getWindowSize())
local self = { local crect = args.clip_rect or rect
x1 = rect.x1, clip_x1 = rect.x1, self:assign{
y1 = rect.y1, clip_y1 = rect.y1, x = rect.x1, y = rect.y1,
x2 = rect.x2, clip_x2 = rect.x2, x1 = rect.x1, clip_x1 = crect.x1,
y2 = rect.y2, clip_y2 = rect.y2, 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, width = rect.x2-rect.x1+1,
height = rect.y2-rect.y1+1, height = rect.y2-rect.y1+1,
cur_pen = to_pen(nil, pen or COLOR_GREY) cur_pen = to_pen(nil, args.pen or COLOR_GREY)
} }
return mkinstance(Painter, self):seek(0,0) end
function Painter.new(rect, pen)
return Painter{ rect = rect, pen = pen }
end end
function Painter:isValidPos() function Painter:isValidPos()
@ -213,9 +218,8 @@ Screen = defclass(Screen)
Screen.text_input_mode = false Screen.text_input_mode = false
function Screen:init() function Screen:postinit()
self:updateLayout() self:updateLayout()
return self
end end
Screen.isDismissed = dscreen.isDismissed Screen.isDismissed = dscreen.isDismissed
@ -344,7 +348,12 @@ end
FramedScreen = defclass(FramedScreen, Screen) FramedScreen = defclass(FramedScreen, Screen)
FramedScreen.frame_style = BOUNDARY_FRAME FramedScreen.ATTRS{
frame_style = BOUNDARY_FRAME,
frame_title = DEFAULT_NIL,
frame_width = DEFAULT_NIL,
frame_height = DEFAULT_NIL,
}
local function hint_coord(gap,hint) local function hint_coord(gap,hint)
if hint and hint > 0 then if hint and hint > 0 then

@ -10,24 +10,21 @@ local dscreen = dfhack.screen
MessageBox = defclass(MessageBox, gui.FramedScreen) MessageBox = defclass(MessageBox, gui.FramedScreen)
MessageBox.focus_path = 'MessageBox' MessageBox.focus_path = 'MessageBox'
MessageBox.frame_style = gui.GREY_LINE_FRAME
MessageBox.ATTRS{
function MessageBox:init(info) frame_style = gui.GREY_LINE_FRAME,
info = info or {} -- new attrs
self:init_fields{ text = {},
text = info.text or {}, on_accept = DEFAULT_NIL,
frame_title = info.title, on_cancel = DEFAULT_NIL,
frame_width = info.frame_width, on_close = DEFAULT_NIL,
on_accept = info.on_accept, text_pen = DEFAULT_NIL,
on_cancel = info.on_cancel, }
on_close = info.on_close,
text_pen = info.text_pen function MessageBox:preinit(info)
} if type(info.text) == 'string' then
if type(self.text) == 'string' then info.text = utils.split_string(info.text, "\n")
self.text = utils.split_string(self.text, "\n")
end end
gui.FramedScreen.init(self, info)
return self
end end
function MessageBox:getWantedFrameSize() function MessageBox:getWantedFrameSize()
@ -82,9 +79,8 @@ function MessageBox:onInput(keys)
end end
function showMessage(title, text, tcolor, on_close) function showMessage(title, text, tcolor, on_close)
mkinstance(MessageBox):init{ MessageBox{
text = text, frame_title = title,
title = title,
text = text, text = text,
text_pen = tcolor, text_pen = tcolor,
on_close = on_close on_close = on_close
@ -92,8 +88,8 @@ function showMessage(title, text, tcolor, on_close)
end end
function showYesNoPrompt(title, text, tcolor, on_accept, on_cancel) function showYesNoPrompt(title, text, tcolor, on_accept, on_cancel)
mkinstance(MessageBox):init{ MessageBox{
title = title, frame_title = title,
text = text, text = text,
text_pen = tcolor, text_pen = tcolor,
on_accept = on_accept, on_accept = on_accept,
@ -105,25 +101,23 @@ InputBox = defclass(InputBox, MessageBox)
InputBox.focus_path = 'InputBox' InputBox.focus_path = 'InputBox'
function InputBox:init(info) InputBox.ATTRS{
info = info or {} input = '',
self:init_fields{ input_pen = DEFAULT_NIL,
input = info.input or '', on_input = DEFAULT_NIL,
input_pen = info.input_pen, }
on_input = info.on_input,
} function InputBox:preinit(info)
MessageBox.init(self, info) info.on_accept = nil
self.on_accept = nil
return self
end end
function InputBox:getWantedFrameSize() function InputBox:getWantedFrameSize()
local mw, mh = MessageBox.getWantedFrameSize(self) local mw, mh = InputBox.super.getWantedFrameSize(self)
return mw, mh+2 return mw, mh+2
end end
function InputBox:onRenderBody(dc) function InputBox:onRenderBody(dc)
MessageBox.onRenderBody(self, dc) InputBox.super.onRenderBody(self, dc)
dc:newline(1) dc:newline(1)
dc:pen(self.input_pen or COLOR_LIGHTCYAN) dc:pen(self.input_pen or COLOR_LIGHTCYAN)
@ -161,8 +155,8 @@ function InputBox:onInput(keys)
end end
function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_width) function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_width)
mkinstance(InputBox):init{ InputBox{
title = title, frame_title = title,
text = text, text = text,
text_pen = tcolor, text_pen = tcolor,
input = input, input = input,
@ -176,27 +170,28 @@ ListBox = defclass(ListBox, MessageBox)
ListBox.focus_path = 'ListBox' ListBox.focus_path = 'ListBox'
ListBox.ATTRS{
selection = 0,
choices = {},
select_pen = DEFAULT_NIL,
on_input = DEFAULT_NIL
}
function InputBox:preinit(info)
info.on_accept = nil
end
function ListBox:init(info) function ListBox:init(info)
info = info or {} self.page_top = 0
self:init_fields{
selection = info.selection or 0,
choices = info.choices or {},
select_pen = info.select_pen,
on_input = info.on_input,
page_top = 0
}
MessageBox.init(self, info)
self.on_accept = nil
return self
end end
function ListBox:getWantedFrameSize() function ListBox:getWantedFrameSize()
local mw, mh = MessageBox.getWantedFrameSize(self) local mw, mh = ListBox.super.getWantedFrameSize(self)
return mw, mh+#self.choices return mw, mh+#self.choices
end end
function ListBox:onRenderBody(dc) function ListBox:onRenderBody(dc)
MessageBox.onRenderBody(self, dc) ListBox.super.onRenderBody(self, dc)
dc:newline(1) dc:newline(1)
@ -220,6 +215,7 @@ function ListBox:onRenderBody(dc)
end end
end end
end end
function ListBox:moveCursor(delta) function ListBox:moveCursor(delta)
local newsel=self.selection+delta local newsel=self.selection+delta
if #self.choices ~=0 then if #self.choices ~=0 then
@ -229,6 +225,7 @@ function ListBox:moveCursor(delta)
end end
self.selection=newsel self.selection=newsel
end end
function ListBox:onInput(keys) function ListBox:onInput(keys)
if keys.SELECT then if keys.SELECT then
self:dismiss() self:dismiss()
@ -257,8 +254,8 @@ function ListBox:onInput(keys)
end end
function showListPrompt(title, text, tcolor, choices, on_input, on_cancel, min_width) function showListPrompt(title, text, tcolor, choices, on_input, on_cancel, min_width)
mkinstance(ListBox):init{ ListBox{
title = title, frame_title = title,
text = text, text = text,
text_pen = tcolor, text_pen = tcolor,
choices = choices, choices = choices,

@ -353,7 +353,7 @@ end
MenuOverlay = defclass(MenuOverlay, DwarfOverlay) MenuOverlay = defclass(MenuOverlay, DwarfOverlay)
function MenuOverlay:updateLayout() function MenuOverlay:updateLayout()
DwarfOverlay.updateLayout(self) MenuOverlay.super.updateLayout(self)
self.frame_rect = self.df_layout.menu self.frame_rect = self.df_layout.menu
end end
@ -361,7 +361,7 @@ MenuOverlay.getWindowSize = gui.FramedScreen.getWindowSize
MenuOverlay.getMousePos = gui.FramedScreen.getMousePos MenuOverlay.getMousePos = gui.FramedScreen.getMousePos
function MenuOverlay:onAboutToShow(below) function MenuOverlay:onAboutToShow(below)
DwarfOverlay.onAboutToShow(self,below) MenuOverlay.super.onAboutToShow(self,below)
self:updateLayout() self:updateLayout()
if not self.df_layout.menu then if not self.df_layout.menu then

@ -4,19 +4,21 @@ local gui = require 'gui'
local text = 'Woohoo, lua viewscreen :)' local text = 'Woohoo, lua viewscreen :)'
local screen = mkinstance(gui.FramedScreen, { local screen = gui.FramedScreen{
frame_style = gui.GREY_LINE_FRAME, frame_style = gui.GREY_LINE_FRAME,
frame_title = 'Hello World', frame_title = 'Hello World',
frame_width = #text+6, frame_width = #text+6,
frame_height = 3, frame_height = 3,
onRenderBody = function(self, dc) }
dc:seek(3,1):string(text, COLOR_LIGHTGREEN)
end, function screen:onRenderBody(dc)
onInput = function(self,keys) dc:seek(3,1):string(text, COLOR_LIGHTGREEN)
if keys.LEAVESCREEN or keys.SELECT then end
self:dismiss()
end function screen:onInput(keys)
if keys.LEAVESCREEN or keys.SELECT then
self:dismiss()
end end
}):init() end
screen:show() screen:show()

@ -53,13 +53,7 @@ local permaflows = {
Toggle = defclass(Toggle) Toggle = defclass(Toggle)
function Toggle:init(items) Toggle.ATTRS{ items = {}, selected = 1 }
self:init_fields{
items = items,
selected = 1
}
return self
end
function Toggle:get() function Toggle:get()
return self.items[self.selected] return self.items[self.selected]
@ -89,16 +83,14 @@ LiquidsUI = defclass(LiquidsUI, guidm.MenuOverlay)
LiquidsUI.focus_path = 'liquids' LiquidsUI.focus_path = 'liquids'
function LiquidsUI:init() function LiquidsUI:init()
self:init_fields{ self:assign{
brush = mkinstance(Toggle):init(brushes), brush = Toggle{ items = brushes },
paint = mkinstance(Toggle):init(paints), paint = Toggle{ items = paints },
flow = mkinstance(Toggle):init(flowbits), flow = Toggle{ items = flowbits },
set = mkinstance(Toggle):init(setmode), set = Toggle{ items = setmode },
permaflow = mkinstance(Toggle):init(permaflows), permaflow = Toggle{ items = permaflows },
amount = 7, amount = 7,
} }
guidm.MenuOverlay.init(self)
return self
end end
function LiquidsUI:onDestroy() function LiquidsUI:onDestroy()
@ -201,6 +193,7 @@ function LiquidsUI:onRenderBody(dc)
end end
function ensure_blocks(cursor, size, cb) function ensure_blocks(cursor, size, cb)
size = size or xyz2pos(1,1,1)
local cx,cy,cz = pos2xyz(cursor) local cx,cy,cz = pos2xyz(cursor)
local all = true local all = true
for x=1,size.x or 1,16 do for x=1,size.x or 1,16 do
@ -298,5 +291,5 @@ if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/LookAround') then
qerror("This script requires the main dwarfmode view in 'k' mode") qerror("This script requires the main dwarfmode view in 'k' mode")
end end
local list = mkinstance(LiquidsUI):init() local list = LiquidsUI()
list:show() list:show()

@ -43,13 +43,11 @@ MechanismList = defclass(MechanismList, guidm.MenuOverlay)
MechanismList.focus_path = 'mechanisms' MechanismList.focus_path = 'mechanisms'
function MechanismList:init(building) function MechanismList:init(info)
self:init_fields{ self:assign{
links = {}, selected = 1 links = {}, selected = 1
} }
guidm.MenuOverlay.init(self) self:fillList(info.building)
self:fillList(building)
return self
end end
function MechanismList:fillList(building) function MechanismList:fillList(building)
@ -126,6 +124,6 @@ if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') t
qerror("This script requires the main dwarfmode view in 'q' mode") qerror("This script requires the main dwarfmode view in 'q' mode")
end end
local list = mkinstance(MechanismList):init(df.global.world.selected_building) local list = MechanismList{ building = df.global.world.selected_building }
list:show() list:show()
list:changeSelected(1) list:changeSelected(1)

@ -13,15 +13,13 @@ PowerMeter = defclass(PowerMeter, guidm.MenuOverlay)
PowerMeter.focus_path = 'power-meter' PowerMeter.focus_path = 'power-meter'
function PowerMeter:init() function PowerMeter:init()
self:init_fields{ self:assign{
min_power = 0, max_power = -1, invert = false, min_power = 0, max_power = -1, invert = false,
} }
guidm.MenuOverlay.init(self)
return self
end end
function PowerMeter:onShow() function PowerMeter:onShow()
guidm.MenuOverlay.onShow(self) PowerMeter.super.onShow(self)
-- Send an event to update the errors -- Send an event to update the errors
bselector.plate_info.flags.whole = 0 bselector.plate_info.flags.whole = 0
@ -112,5 +110,5 @@ then
qerror("This script requires the main dwarfmode view in build pressure plate mode") qerror("This script requires the main dwarfmode view in build pressure plate mode")
end end
local list = mkinstance(PowerMeter):init() local list = PowerMeter()
list:show() list:show()

@ -78,15 +78,17 @@ RoomList = defclass(RoomList, guidm.MenuOverlay)
RoomList.focus_path = 'room-list' RoomList.focus_path = 'room-list'
function RoomList:init(unit) RoomList.ATTRS{ unit = DEFAULT_NIL }
function RoomList:init(info)
local unit = info.unit
local base_bld = df.global.world.selected_building local base_bld = df.global.world.selected_building
self:init_fields{ self:assign{
unit = unit, base_building = base_bld, base_building = base_bld,
items = {}, selected = 1, items = {}, selected = 1,
own_rooms = {}, spouse_rooms = {} own_rooms = {}, spouse_rooms = {}
} }
guidm.MenuOverlay.init(self)
self.old_viewport = self:getViewport() self.old_viewport = self:getViewport()
self.old_cursor = guidm.getCursorPos() self.old_cursor = guidm.getCursorPos()
@ -115,8 +117,6 @@ function RoomList:init(unit)
self.items = concat_lists({self.base_item}, self.items) self.items = concat_lists({self.base_item}, self.items)
::found:: ::found::
end end
return self
end end
local sex_char = { [0] = 12, [1] = 11 } local sex_char = { [0] = 12, [1] = 11 }
@ -235,12 +235,13 @@ function RoomList:onInput(keys)
end end
local focus = dfhack.gui.getCurFocus() local focus = dfhack.gui.getCurFocus()
if focus == 'dwarfmode/QueryBuilding/Some' then
local base = df.global.world.selected_building if focus == 'dwarfmode/QueryBuilding/Some/Assign/Unit' then
mkinstance(RoomList):init(base.owner):show()
elseif focus == 'dwarfmode/QueryBuilding/Some/Assign/Unit' then
local unit = df.global.ui_building_assign_units[df.global.ui_building_item_cursor] local unit = df.global.ui_building_assign_units[df.global.ui_building_item_cursor]
mkinstance(RoomList):init(unit):show() RoomList{ unit = unit }:show()
elseif string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') then
local base = df.global.world.selected_building
RoomList{ unit = base.owner }:show()
else else
qerror("This script requires the main dwarfmode view in 'q' mode") qerror("This script requires the main dwarfmode view in 'q' mode")
end end

@ -34,30 +34,29 @@ SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay)
SiegeEngine.focus_path = 'siege-engine' SiegeEngine.focus_path = 'siege-engine'
function SiegeEngine:init(building) SiegeEngine.ATTRS{ building = DEFAULT_NIL }
self:init_fields{
building = building, function SiegeEngine:init()
center = utils.getBuildingCenter(building), self:assign{
center = utils.getBuildingCenter(self.building),
selected_pile = 1, selected_pile = 1,
mode_main = {
render = self:callback 'onRenderBody_main',
input = self:callback 'onInput_main',
},
mode_aim = {
render = self:callback 'onRenderBody_aim',
input = self:callback 'onInput_aim',
},
mode_pile = {
render = self:callback 'onRenderBody_pile',
input = self:callback 'onInput_pile',
}
} }
guidm.MenuOverlay.init(self)
self.mode_main = {
render = self:callback 'onRenderBody_main',
input = self:callback 'onInput_main',
}
self.mode_aim = {
render = self:callback 'onRenderBody_aim',
input = self:callback 'onInput_aim',
}
self.mode_pile = {
render = self:callback 'onRenderBody_pile',
input = self:callback 'onInput_pile',
}
return self
end end
function SiegeEngine:onShow() function SiegeEngine:onShow()
guidm.MenuOverlay.onShow(self) SiegeEngine.super.onShow(self)
self.old_cursor = guidm.getCursorPos() self.old_cursor = guidm.getCursorPos()
self.old_viewport = self:getViewport() self.old_viewport = self:getViewport()
@ -487,5 +486,5 @@ if not df.building_siegeenginest:is_instance(building) then
qerror("A siege engine must be selected") qerror("A siege engine must be selected")
end end
local list = mkinstance(SiegeEngine):init(df.global.world.selected_building) local list = SiegeEngine{ building = building }
list:show() list:show()