From 57b72831ca700ab556566a85f2245e014ca96c30 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Tue, 18 Sep 2012 20:30:25 +0400 Subject: [PATCH] Overhaul the concept of lua 'class' initialization yet again. --- library/lua/class.lua | 150 ++++++++++++++++++++++++++++++++++ library/lua/dfhack.lua | 24 ++---- library/lua/gui.lua | 33 +++++--- library/lua/gui/dialogs.lua | 99 +++++++++++----------- library/lua/gui/dwarfmode.lua | 4 +- scripts/gui/hello-world.lua | 20 +++-- scripts/gui/liquids.lua | 25 ++---- scripts/gui/mechanisms.lua | 10 +-- scripts/gui/power-meter.lua | 8 +- scripts/gui/room-list.lua | 23 +++--- scripts/gui/siege-engine.lua | 39 +++++---- 11 files changed, 285 insertions(+), 150 deletions(-) create mode 100644 library/lua/class.lua diff --git a/library/lua/class.lua b/library/lua/class.lua new file mode 100644 index 000000000..7b142e499 --- /dev/null +++ b/library/lua/class.lua @@ -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 diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index baf0d42e0..ce3be5a87 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -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 diff --git a/library/lua/gui.lua b/library/lua/gui.lua index f9b6ab6d2..6eaa98606 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -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 diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index eb883465f..b1a96a558 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -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, diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index 661e15591..ba3cfbe6c 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -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 diff --git a/scripts/gui/hello-world.lua b/scripts/gui/hello-world.lua index 80986bbf6..c8cd3bd01 100644 --- a/scripts/gui/hello-world.lua +++ b/scripts/gui/hello-world.lua @@ -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() diff --git a/scripts/gui/liquids.lua b/scripts/gui/liquids.lua index 89f08b7cf..cddb9f01d 100644 --- a/scripts/gui/liquids.lua +++ b/scripts/gui/liquids.lua @@ -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() diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua index c14bfcbe9..d1e8ec803 100644 --- a/scripts/gui/mechanisms.lua +++ b/scripts/gui/mechanisms.lua @@ -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) diff --git a/scripts/gui/power-meter.lua b/scripts/gui/power-meter.lua index 8baf43e7e..6c2f699ac 100644 --- a/scripts/gui/power-meter.lua +++ b/scripts/gui/power-meter.lua @@ -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() diff --git a/scripts/gui/room-list.lua b/scripts/gui/room-list.lua index a4507466f..0de82db5f 100644 --- a/scripts/gui/room-list.lua +++ b/scripts/gui/room-list.lua @@ -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 diff --git a/scripts/gui/siege-engine.lua b/scripts/gui/siege-engine.lua index 7a76d7673..c98cb1676 100644 --- a/scripts/gui/siege-engine.lua +++ b/scripts/gui/siege-engine.lua @@ -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()