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
function defclass(class,parent)
class = class or {}
rawset_default(class, { __index = class })
if parent then
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
DEFAULT_NIL = DEFAULT_NIL or {} -- Unique token
function defclass(...)
return require('class').defclass(...)
end
function mkinstance(class,table)
table = table or {}
setmetatable(table, class)
return table
function mkinstance(...)
return require('class').mkinstance(...)
end
-- Misc functions

@ -74,18 +74,23 @@ end
Painter = defclass(Painter, nil)
function Painter.new(rect, pen)
rect = rect or mkdims_wh(0,0,dscreen.getWindowSize())
local self = {
x1 = rect.x1, clip_x1 = rect.x1,
y1 = rect.y1, clip_y1 = rect.y1,
x2 = rect.x2, clip_x2 = rect.x2,
y2 = rect.y2, clip_y2 = rect.y2,
function Painter:init(args)
local rect = args.rect or mkdims_wh(0,0,dscreen.getWindowSize())
local crect = args.clip_rect or rect
self:assign{
x = rect.x1, y = rect.y1,
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,
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
function Painter:isValidPos()
@ -213,9 +218,8 @@ Screen = defclass(Screen)
Screen.text_input_mode = false
function Screen:init()
function Screen:postinit()
self:updateLayout()
return self
end
Screen.isDismissed = dscreen.isDismissed
@ -344,7 +348,12 @@ end
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)
if hint and hint > 0 then

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

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

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

@ -53,13 +53,7 @@ local permaflows = {
Toggle = defclass(Toggle)
function Toggle:init(items)
self:init_fields{
items = items,
selected = 1
}
return self
end
Toggle.ATTRS{ items = {}, selected = 1 }
function Toggle:get()
return self.items[self.selected]
@ -89,16 +83,14 @@ LiquidsUI = defclass(LiquidsUI, guidm.MenuOverlay)
LiquidsUI.focus_path = 'liquids'
function LiquidsUI:init()
self:init_fields{
brush = mkinstance(Toggle):init(brushes),
paint = mkinstance(Toggle):init(paints),
flow = mkinstance(Toggle):init(flowbits),
set = mkinstance(Toggle):init(setmode),
permaflow = mkinstance(Toggle):init(permaflows),
self:assign{
brush = Toggle{ items = brushes },
paint = Toggle{ items = paints },
flow = Toggle{ items = flowbits },
set = Toggle{ items = setmode },
permaflow = Toggle{ items = permaflows },
amount = 7,
}
guidm.MenuOverlay.init(self)
return self
end
function LiquidsUI:onDestroy()
@ -201,6 +193,7 @@ function LiquidsUI:onRenderBody(dc)
end
function ensure_blocks(cursor, size, cb)
size = size or xyz2pos(1,1,1)
local cx,cy,cz = pos2xyz(cursor)
local all = true
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")
end
local list = mkinstance(LiquidsUI):init()
local list = LiquidsUI()
list:show()

@ -43,13 +43,11 @@ MechanismList = defclass(MechanismList, guidm.MenuOverlay)
MechanismList.focus_path = 'mechanisms'
function MechanismList:init(building)
self:init_fields{
function MechanismList:init(info)
self:assign{
links = {}, selected = 1
}
guidm.MenuOverlay.init(self)
self:fillList(building)
return self
self:fillList(info.building)
end
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")
end
local list = mkinstance(MechanismList):init(df.global.world.selected_building)
local list = MechanismList{ building = df.global.world.selected_building }
list:show()
list:changeSelected(1)

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

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

@ -34,30 +34,29 @@ SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay)
SiegeEngine.focus_path = 'siege-engine'
function SiegeEngine:init(building)
self:init_fields{
building = building,
center = utils.getBuildingCenter(building),
SiegeEngine.ATTRS{ building = DEFAULT_NIL }
function SiegeEngine:init()
self:assign{
center = utils.getBuildingCenter(self.building),
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
function SiegeEngine:onShow()
guidm.MenuOverlay.onShow(self)
SiegeEngine.super.onShow(self)
self.old_cursor = guidm.getCursorPos()
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")
end
local list = mkinstance(SiegeEngine):init(df.global.world.selected_building)
local list = SiegeEngine{ building = building }
list:show()