From 023dc82564f9f896cffadfede284bb01d9fa7800 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Tue, 16 Oct 2012 18:33:00 +0400 Subject: [PATCH] Implement a material selection dialog. --- library/lua/class.lua | 14 +- library/lua/gui.lua | 4 +- library/lua/gui/materials.lua | 311 ++++++++++++++++++++++++++++++++++ library/lua/gui/script.lua | 12 ++ library/lua/gui/widgets.lua | 29 +++- 5 files changed, 361 insertions(+), 9 deletions(-) create mode 100644 library/lua/gui/materials.lua diff --git a/library/lua/class.lua b/library/lua/class.lua index 7b142e499..bcfff13e2 100644 --- a/library/lua/class.lua +++ b/library/lua/class.lua @@ -3,16 +3,16 @@ local _ENV = mkmodule('class') -- Metatable template for a class -class_obj = {} or class_obj +class_obj = class_obj or {} -- Methods shared by all classes -common_methods = {} or common_methods +common_methods = common_methods or {} -- Forbidden names for class fields and methods. reserved_names = { super = true, ATTRS = true } -- Attribute table metatable -attrs_meta = {} or attrs_meta +attrs_meta = attrs_meta or {} -- Create or updates a class; a class has metamethods and thus own metatable. function defclass(class,parent) @@ -133,6 +133,14 @@ function common_methods:callback(method, ...) return dfhack.curry(self[method], self, ...) end +function common_methods:cb_getfield(field) + return function() return self[field] end +end + +function common_methods:cb_setfield(field) + return function(val) self[field] = val end +end + function common_methods:assign(data) for k,v in pairs(data) do self[k] = v diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 125b59544..90eeb823a 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -377,7 +377,7 @@ function View:addviews(list) end for _,dir in ipairs(list) do - for id,obj in pairs(dir) do + for id,obj in pairs(dir.subviews) do if id and type(id) ~= 'number' and sv[id] == nil then sv[id] = obj end @@ -616,7 +616,7 @@ end function FramedScreen:computeFrame(parent_rect) local sw, sh = parent_rect.width, parent_rect.height - local fw, fh = self:getWantedFrameSize() + local fw, fh = self:getWantedFrameSize(parent_rect) return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1) end diff --git a/library/lua/gui/materials.lua b/library/lua/gui/materials.lua new file mode 100644 index 000000000..0cf3c21d9 --- /dev/null +++ b/library/lua/gui/materials.lua @@ -0,0 +1,311 @@ +-- Stock dialog for selecting materials + +local _ENV = mkmodule('gui.materials') + +local gui = require('gui') +local widgets = require('gui.widgets') +local utils = require('utils') + +ARROW = string.char(26) + +CREATURE_BASE = 19 +PLANT_BASE = 419 + +MaterialDialog = defclass(MaterialDialog, gui.FramedScreen) + +MaterialDialog.focus_path = 'MaterialDialog' + +MaterialDialog.ATTRS{ + prompt = 'Type or select a material from this list', + frame_style = gui.GREY_LINE_FRAME, + frame_inset = 1, + frame_title = 'Select Material', + -- new attrs + on_select = DEFAULT_NIL, + on_cancel = DEFAULT_NIL, + on_close = DEFAULT_NIL, +} + +function MaterialDialog:init(info) + self:addviews{ + widgets.Label{ + text = { + self.prompt, '\n\n', + 'Category: ', { text = self:cb_getfield('context_str'), pen = COLOR_CYAN } + }, + text_pen = COLOR_WHITE, + frame = { l = 0, t = 0 }, + }, + widgets.Label{ + view_id = 'back', + visible = false, + text = { { key = 'LEAVESCREEN', text = ': Back' } }, + frame = { r = 0, b = 0 }, + auto_width = true, + }, + widgets.List{ + view_id = 'list', + frame = { l = 0, r = 0, t = 6, b = 2 }, + on_submit = self:callback('onSubmitItem'), + }, + widgets.EditField{ + view_id = 'edit', + frame = { l = 2, t = 4 }, + on_change = self:callback('onFilterChoices'), + on_char = self:callback('onFilterChar'), + }, + widgets.Label{ + view_id = 'not_found', + text = 'No matching materials.', + text_pen = COLOR_LIGHTRED, + frame = { l = 2, r = 0, t = 6 }, + }, + widgets.Label{ + text = { { + key = 'SELECT', text = ': Select', + disabled = function() return self.subviews.not_found.visible end + } }, + frame = { l = 0, b = 0 }, + } + } + self:initBuiltinMode() +end + +function MaterialDialog:getWantedFrameSize(rect) + return math.max(40, #self.prompt), math.min(28, rect.height-8) +end + +function MaterialDialog:onDestroy() + if self.on_close then + self.on_close() + end +end + +function MaterialDialog:initBuiltinMode() + local choices = { + { text = ' none', mat_type = -1, mat_index = -1 }, + { text = ARROW..' inorganic', key = 'CUSTOM_SHIFT_I', + cb = self:callback('initInorganicMode') }, + { text = ARROW..' creature', key = 'CUSTOM_SHIFT_C', + cb = self:callback('initCreatureMode')}, + { text = ARROW..' plant', key = 'CUSTOM_SHIFT_P', + cb = self:callback('initPlantMode') }, + } + + self:addMaterials( + choices, + df.global.world.raws.mat_table.builtin, 0, -1, + df.builtin_mats._last_item + ) + + self:pushContext('Any material', choices) +end + +function MaterialDialog:initInorganicMode() + local choices = {} + + self:addMaterials( + choices, + df.global.world.raws.inorganics, 0, nil, + nil, 'material' + ) + + self:pushContext('Inorganic materials', choices) +end + +function MaterialDialog:initCreatureMode() + local choices = {} + + for i,v in ipairs(df.global.world.raws.creatures.all) do + self:addObjectChoice(choices, v, v.name[0], CREATURE_BASE, i) + end + + self:pushContext('Creature materials', choices) +end + +function MaterialDialog:initPlantMode() + local choices = {} + + for i,v in ipairs(df.global.world.raws.plants.all) do + self:addObjectChoice(choices, v, v.name, PLANT_BASE, i) + end + + self:pushContext('Plant materials', choices) +end + +function MaterialDialog:addObjectChoice(choices, obj, name, typ, index) + if #obj.material == 1 then + self:addMaterial(choices, obj.material[0], typ, index, true) + else + table.insert(choices, { + text = ARROW..' '..name, mat_type = typ, mat_index = index, + ctx = name, obj = obj, cb = self:callback('onSelectObj') + }) + end +end + +function MaterialDialog:onSelectObj(item) + local choices = {} + self:addMaterials(choices, item.obj.material, item.mat_type, item.mat_index) + self:pushContext(item.ctx, choices) +end + +function MaterialDialog:addMaterials(choices, vector, tid, index, maxid, field) + for i=0,(maxid or #vector-1) do + local mat = vector[i] + if mat and field then + mat = mat[field] + end + if mat then + local typ, idx + if index then + typ, idx = tid+i, index + else + typ, idx = tid, i + end + self:addMaterial(choices, mat, typ, idx) + end + end +end + +function MaterialDialog:addMaterial(choices, mat, typ, idx, pfix) + local state = 0 + if mat.heat.melting_point <= 10015 then + state = 1 + end + local name = mat.state_name[state] + name = string.gsub(name, '^frozen ','') + name = string.gsub(name, '^molten ','') + name = string.gsub(name, '^condensed ','') + local key + if pfix and mat.prefix ~= '' then + name = mat.prefix .. ' ' .. name + key = mat.prefix + end + table.insert(choices, { + text = ' '..name, + search_key = key, + material = mat, + mat_type = typ, mat_index = idx + }) +end + +function MaterialDialog:pushContext(name, choices) + if not self.back_stack then + self.back_stack = {} + self.subviews.back.visible = false + else + table.insert(self.back_stack, { + context_str = self.context_str, + all_choices = self.all_choices, + edit_text = self.subviews.edit.text, + choices = self.choices, + selected = self.subviews.list.selected, + }) + self.subviews.back.visible = true + end + + self.context_str = name + self.all_choices = choices + self.subviews.edit.text = '' + self:setChoices(choices, 1) +end + +function MaterialDialog:onGoBack() + local save = table.remove(self.back_stack) + self.subviews.back.visible = (#self.back_stack > 0) + + self.context_str = save.context_str + self.all_choices = save.all_choices + self.subviews.edit.text = save.edit_text + self:setChoices(save.choices, save.selected) +end + +function MaterialDialog:setChoices(choices, pos) + self.choices = choices + self.subviews.list:setChoices(self.choices, pos) + self.subviews.not_found.visible = (#self.choices == 0) +end + +function MaterialDialog:onFilterChoices(text) + local tokens = utils.split_string(text, ' ') + local choices = {} + + for i,v in ipairs(self.all_choices) do + local ok = true + local search_key = v.search_key or v.text + for _,key in ipairs(tokens) do + if key ~= '' and not string.match(search_key, '%f[^%s\x00]'..key) then + ok = false + break + end + end + if ok then + table.insert(choices, v) + end + end + + self:setChoices(choices) +end + +local bad_chars = { + ['%'] = true, ['.'] = true, ['+'] = true, ['*'] = true, + ['['] = true, [']'] = true, ['('] = true, [')'] = true, +} + +function MaterialDialog:onFilterChar(char, text) + if bad_chars[char] then + return false + end + if char == ' ' then + if #self.choices == 1 then + self.subviews.list:submit() + return false + end + return string.match(text, '%S$') + end + return true +end + +function MaterialDialog:submitMaterial(typ, index) + self:dismiss() + + if self.on_select then + local info = dfhack.matinfo.decode(typ, index) + self.on_select(info, typ, index) + end +end + +function MaterialDialog:onSubmitItem(idx, item) + if item.cb then + item:cb(idx) + else + self:submitMaterial(item.mat_type, item.mat_index) + end +end + +function MaterialDialog:onInput(keys) + if keys.LEAVESCREEN or keys.LEAVESCREEN_ALL then + if self.subviews.back.visible and not keys.LEAVESCREEN_ALL then + self:onGoBack() + else + self:dismiss() + if self.on_cancel then + self.on_cancel() + end + end + else + self:inputToSubviews(keys) + end +end + +function showMaterialPrompt(title, prompt, on_select, on_cancel) + MaterialDialog{ + frame_title = title, + prompt = prompt, + on_select = on_select, + on_cancel = on_cancel, + }:show() +end + +return _ENV diff --git a/library/lua/gui/script.lua b/library/lua/gui/script.lua index 021a4fa52..06079c426 100644 --- a/library/lua/gui/script.lua +++ b/library/lua/gui/script.lua @@ -148,4 +148,16 @@ function showListPrompt(title, text, tcolor, choices, min_width) return wait() end +function showMaterialPrompt(title, prompt) + require('gui.materials').MaterialDialog{ + frame_title = title, + prompt = prompt, + on_select = mkresume(true, + on_cancel = mkresume(false), + on_close = qresume(nil) + }:show() + + return wait() +end + return _ENV diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index f3796d0e3..eb2e8d05d 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -242,6 +242,8 @@ function render_text(obj,dc,x0,y0,pen,dpen) local keystr = gui.getKeyDisplay(token.key) local sep = token.key_sep or '' + x = x + #keystr + if sep == '()' then if dc then dc:string(text) @@ -285,6 +287,7 @@ Label.ATTRS{ text_pen = COLOR_WHITE, text_dpen = COLOR_DARKGREY, auto_height = true, + auto_width = false, } function Label:init(args) @@ -301,6 +304,13 @@ function Label:setText(text) end end +function Label:preUpdateLayout() + if self.auto_width then + self.frame = self.frame or {} + self.frame.w = self:getTextWidth() + end +end + function Label:itemById(id) if self.text_ids then return self.text_ids[id] @@ -404,6 +414,13 @@ end function List:moveCursor(delta, force_cb) local page = math.max(1, self.page_size) local cnt = #self.choices + + if cnt < 1 then + self.page_top = 1 + self.selected = 1 + return + end + local off = self.selected+delta-1 local ds = math.abs(delta) @@ -458,9 +475,15 @@ function List:onRenderBody(dc) end end +function List:submit() + if self.on_submit and #self.choices > 0 then + self.on_submit(self:getSelected()) + end +end + function List:onInput(keys) if self.on_submit and keys.SELECT then - self.on_submit(self:getSelected()) + self:submit() return true else for k,v in pairs(self.scroll_keys) do @@ -479,9 +502,7 @@ function List:onInput(keys) for i,v in ipairs(self.choices) do if v.key and keys[v.key] then self:setSelected(i) - if self.on_submit then - self.on_submit(self:getSelected()) - end + self:submit() return true end end