From c3984569c06dc3c2890516e95adc38dcab9ec89a Mon Sep 17 00:00:00 2001 From: sapier Date: Fri, 18 Apr 2014 15:39:15 +0200 Subject: Add formspec toolkit and refactor mainmenu to use it Fix crash on using cursor keys in client menu without selected server Add support for non fixed size tabviews --- builtin/fstk/buttonbar.lua | 209 ++++++++++++++++++++++++++++++++++ builtin/fstk/dialog.lua | 69 ++++++++++++ builtin/fstk/tabview.lua | 273 +++++++++++++++++++++++++++++++++++++++++++++ builtin/fstk/ui.lua | 172 ++++++++++++++++++++++++++++ 4 files changed, 723 insertions(+) create mode 100644 builtin/fstk/buttonbar.lua create mode 100644 builtin/fstk/dialog.lua create mode 100644 builtin/fstk/tabview.lua create mode 100644 builtin/fstk/ui.lua (limited to 'builtin/fstk') diff --git a/builtin/fstk/buttonbar.lua b/builtin/fstk/buttonbar.lua new file mode 100644 index 000000000..f5ac8905e --- /dev/null +++ b/builtin/fstk/buttonbar.lua @@ -0,0 +1,209 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--self program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--self program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with self program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +local function buttonbar_formspec(self) + + if self.hidden then + return "" + end + + local formspec = string.format("box[%f,%f;%f,%f;%s]", + self.pos.x,self.pos.y ,self.size.x,self.size.y,self.bgcolor) + + for i=self.startbutton,#self.buttons,1 do + local btn_name = self.buttons[i].name + local btn_pos = {} + + if self.orientation == "horizontal" then + btn_pos.x = self.pos.x + --base pos + (i - self.startbutton) * self.btn_size + --button offset + self.btn_initial_offset + else + btn_pos.x = self.pos.x + (self.btn_size * 0.05) + end + + if self.orientation == "vertical" then + btn_pos.y = self.pos.y + --base pos + (i - self.startbutton) * self.btn_size + --button offset + self.btn_initial_offset + else + btn_pos.y = self.pos.y + (self.btn_size * 0.05) + end + + if (self.orientation == "vertical" and + (btn_pos.y + self.btn_size <= self.pos.y + self.size.y)) or + (self.orientation == "horizontal" and + (btn_pos.x + self.btn_size <= self.pos.x + self.size.x)) then + + local borders="true" + + if self.buttons[i].image ~= nil then + borders="false" + end + + formspec = formspec .. + string.format("image_button[%f,%f;%f,%f;%s;%s;%s;true;%s]", + btn_pos.x, btn_pos.y, self.btn_size, self.btn_size, + self.buttons[i].image, btn_name, self.buttons[i].caption, + borders) + else + --print("end of displayable buttons: orientation: " .. self.orientation) + --print( "button_end: " .. (btn_pos.y + self.btn_size - (self.btn_size * 0.05))) + --print( "bar_end: " .. (self.pos.x + self.size.x)) + break + end + end + + if (self.have_move_buttons) then + local btn_dec_pos = {} + btn_dec_pos.x = self.pos.x + (self.btn_size * 0.05) + btn_dec_pos.y = self.pos.y + (self.btn_size * 0.05) + local btn_inc_pos = {} + local btn_size = {} + + if self.orientation == "horizontal" then + btn_size.x = 0.5 + btn_size.y = self.btn_size + btn_inc_pos.x = self.pos.x + self.size.x - 0.5 + btn_inc_pos.y = self.pos.y + (self.btn_size * 0.05) + else + btn_size.x = self.btn_size + btn_size.y = 0.5 + btn_inc_pos.x = self.pos.x + (self.btn_size * 0.05) + btn_inc_pos.y = self.pos.y + self.size.y - 0.5 + end + + local text_dec = "<" + local text_inc = ">" + if self.orientation == "vertical" then + text_dec = "^" + text_inc = "v" + end + + formspec = formspec .. + string.format("image_button[%f,%f;%f,%f;;btnbar_dec_%s;%s;true;true]", + btn_dec_pos.x, btn_dec_pos.y, btn_size.x, btn_size.y, + self.name, text_dec) + + formspec = formspec .. + string.format("image_button[%f,%f;%f,%f;;btnbar_dec_%s;%s;true;true]", + btn_inc_pos.x, btn_inc_pos.y, btn_size.x, btn_size.y, + self.name, text_inc) + end + + return formspec +end + +local function buttonbar_buttonhandler(self, fields) + + if fields["btnbar_inc_" .. self.name] ~= nil and + self.startbutton < #self.buttons then + + self.startbutton = self.startbutton + 1 + return true + end + + if fields["btnbar_dec_" .. self.name] ~= nil and self.startbutton > 1 then + self.startbutton = self.startbutton - 1 + return true + end + + for i=1,#self.buttons,1 do + if fields[self.buttons[i].name] ~= nil then + return self.userbuttonhandler(fields) + end + end +end + +local buttonbar_metatable = { + handle_buttons = buttonbar_buttonhandler, + handle_events = function(self, event) end, + get_formspec = buttonbar_formspec, + + hide = function(self) self.hidden = true end, + show = function(self) self.hidden = false end, + + delete = function(self) ui.delete(self) end, + + add_button = function(self, name, caption, image) + if caption == nil then caption = "" end + if image == nil then image = "" end + + table.insert(self.buttons,{ name=name, caption=caption, image=image}) + if self.orientation == "horizontal" then + if ( (self.btn_size * #self.buttons) + (self.btn_size * 0.05 *2) + > self.size.x ) then + + self.btn_initial_offset = self.btn_size * 0.05 + 0.5 + self.have_move_buttons = true + end + else + if ((self.btn_size * #self.buttons) + (self.btn_size * 0.05 *2) + > self.size.y ) then + + self.btn_initial_offset = self.btn_size * 0.05 + 0.5 + self.have_move_buttons = true + end + end + end, + + set_bgparams = function(self, bgcolor) + if (type(bgcolor) == "string") then + self.bgcolor = bgcolor + end + end, +} + +buttonbar_metatable.__index = buttonbar_metatable + +function buttonbar_create(name, cbf_buttonhandler, pos, orientation, size) + assert(name ~= nil) + assert(cbf_buttonhandler ~= nil) + assert(orientation == "vertical" or orientation == "horizontal") + assert(pos ~= nil and type(pos) == "table") + assert(size ~= nil and type(size) == "table") + + local self = {} + self.name = name + self.type = "addon" + self.bgcolor = "#000000" + self.pos = pos + self.size = size + self.orientation = orientation + self.startbutton = 1 + self.have_move_buttons = false + self.hidden = false + + if self.orientation == "horizontal" then + self.btn_size = self.size.y + else + self.btn_size = self.size.x + end + + if (self.btn_initial_offset == nil) then + self.btn_initial_offset = self.btn_size * 0.05 + end + + self.userbuttonhandler = cbf_buttonhandler + self.buttons = {} + + setmetatable(self,buttonbar_metatable) + + ui.add(self) + return self +end diff --git a/builtin/fstk/dialog.lua b/builtin/fstk/dialog.lua new file mode 100644 index 000000000..214b0388f --- /dev/null +++ b/builtin/fstk/dialog.lua @@ -0,0 +1,69 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--self program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--self program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with self program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +local function dialog_event_handler(self,event) + if self.user_eventhandler == nil or + self.user_eventhandler(event) == false then + + --close dialog on esc + if event == "MenuQuit" then + self:delete() + return true + end + end +end + +local dialog_metatable = { + eventhandler = dialog_event_handler, + get_formspec = function(self) + if not self.hidden then return self.formspec(self.data) end + end, + handle_buttons = function(self,fields) + if not self.hidden then return self.buttonhandler(self,fields) end + end, + handle_events = function(self,event) + if not self.hidden then return self.eventhandler(self,event) end + end, + hide = function(self) self.hidden = true end, + show = function(self) self.hidden = false end, + delete = function(self) + if self.parent ~= nil then + self.parent:show() + end + ui.delete(self) + end, + set_parent = function(self,parent) self.parent = parent end +} +dialog_metatable.__index = dialog_metatable + +function dialog_create(name,get_formspec,buttonhandler,eventhandler) + local self = {} + + self.name = name + self.type = "toplevel" + self.hidden = true + self.data = {} + + self.formspec = get_formspec + self.buttonhandler = buttonhandler + self.user_eventhandler = eventhandler + + setmetatable(self,dialog_metatable) + + ui.add(self) + return self +end diff --git a/builtin/fstk/tabview.lua b/builtin/fstk/tabview.lua new file mode 100644 index 000000000..47603fb1b --- /dev/null +++ b/builtin/fstk/tabview.lua @@ -0,0 +1,273 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--self program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--self program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with self program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +-------------------------------------------------------------------------------- +-- A tabview implementation -- +-- Usage: -- +-- tabview.create: returns initialized tabview raw element -- +-- element.add(tab): add a tab declaration -- +-- element.handle_buttons() -- +-- element.handle_events() -- +-- element.getFormspec() returns formspec of tabview -- +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +local function add_tab(self,tab) + assert(tab.size == nil or (type(tab.size) == table and + tab.size.x ~= nil and tab.size.y ~= nil)) + assert(tab.cbf_formspec ~= nil and type(tab.cbf_formspec) == "function") + assert(tab.cbf_button_handler == nil or + type(tab.cbf_button_handler) == "function") + assert(tab.cbf_events == nil or type(tab.cbf_events) == "function") + + local newtab = { + name = tab.name, + caption = tab.caption, + button_handler = tab.cbf_button_handler, + event_handler = tab.cbf_events, + get_formspec = tab.cbf_formspec, + tabsize = tab.tabsize, + on_change = tab.on_change, + tabdata = {}, + } + + table.insert(self.tablist,newtab) + + if self.last_tab_index == #self.tablist then + self.current_tab = tab.name + if tab.on_activate ~= nil then + tab.on_activate(nil,tab.name) + end + end +end + +-------------------------------------------------------------------------------- +local function get_formspec(self) + local formspec = "" + + if not self.hidden and (self.parent == nil or not self.parent.hidden) then + + if self.parent == nil then + local tsize = self.tablist[self.last_tab_index].tabsize or + {width=self.width, height=self.height} + formspec = formspec .. + string.format("size[%f,%f,%s]",tsize.width,tsize.height, + dump(self.fixed_size)) + end + formspec = formspec .. self:tab_header() + formspec = formspec .. + self.tablist[self.last_tab_index].get_formspec( + self, + self.tablist[self.last_tab_index].name, + self.tablist[self.last_tab_index].tabdata, + self.tablist[self.last_tab_index].tabsize + ) + end + return formspec +end + +-------------------------------------------------------------------------------- +local function handle_buttons(self,fields) + + if self.hidden then + return false + end + + if self:handle_tab_buttons(fields) then + return true + end + + if self.glb_btn_handler ~= nil and + self.glb_btn_handler(self,fields) then + return true + end + + if self.tablist[self.last_tab_index].button_handler ~= nil then + return + self.tablist[self.last_tab_index].button_handler( + self, + fields, + self.tablist[self.last_tab_index].name, + self.tablist[self.last_tab_index].tabdata + ) + end + + return false +end + +-------------------------------------------------------------------------------- +local function handle_events(self,event) + + if self.hidden then + return false + end + + if self.glb_evt_handler ~= nil and + self.glb_evt_handler(self,event) then + return true + end + + if self.tablist[self.last_tab_index].evt_handler ~= nil then + return + self.tablist[self.last_tab_index].evt_handler( + self, + event, + self.tablist[self.last_tab_index].name, + self.tablist[self.last_tab_index].tabdata + ) + end + + return false +end + + +-------------------------------------------------------------------------------- +local function tab_header(self) + + local toadd = "" + + for i=1,#self.tablist,1 do + + if toadd ~= "" then + toadd = toadd .. "," + end + + toadd = toadd .. self.tablist[i].caption + end + return string.format("tabheader[%f,%f;%s;%s;%i;true;false]", + self.header_x, self.header_y, self.name, toadd, self.last_tab_index); +end + +-------------------------------------------------------------------------------- +local function switch_to_tab(self, index) + --first call on_change for tab to leave + if self.tablist[self.last_tab_index].on_change ~= nil then + self.tablist[self.last_tab_index].on_change("LEAVE", + self.current_tab, self.tablist[index].name) + end + + --update tabview data + self.last_tab_index = index + local old_tab = self.current_tab + self.current_tab = self.tablist[index].name + + if (self.autosave_tab) then + core.setting_set(self.name .. "_LAST",self.current_tab) + end + + -- call for tab to enter + if self.tablist[index].on_change ~= nil then + self.tablist[index].on_change("ENTER", + old_tab,self.current_tab) + end +end + +-------------------------------------------------------------------------------- +local function handle_tab_buttons(self,fields) + --save tab selection to config file + if fields[self.name] then + local index = tonumber(fields[self.name]) + switch_to_tab(self, index) + return true + end + + return false +end + +-------------------------------------------------------------------------------- +local function set_tab_by_name(self, name) + for i=1,#self.tablist,1 do + if self.tablist[i].name == name then + switch_to_tab(self, i) + return true + end + end + + return false +end + +-------------------------------------------------------------------------------- +local function hide_tabview(self) + self.hidden=true + + --call on_change as we're not gonna show self tab any longer + if self.tablist[self.last_tab_index].on_change ~= nil then + self.tablist[self.last_tab_index].on_change("LEAVE", + self.current_tab, nil) + end +end + +-------------------------------------------------------------------------------- +local function show_tabview(self) + self.hidden=false + + -- call for tab to enter + if self.tablist[self.last_tab_index].on_change ~= nil then + self.tablist[self.last_tab_index].on_change("ENTER", + nil,self.current_tab) + end +end + +local tabview_metatable = { + add = add_tab, + handle_buttons = handle_buttons, + handle_events = handle_events, + get_formspec = get_formspec, + show = show_tabview, + hide = hide_tabview, + delete = function(self) ui.delete(self) end, + set_parent = function(self,parent) self.parent = parent end, + set_autosave_tab = + function(self,value) self.autosave_tab = value end, + set_tab = set_tab_by_name, + set_global_button_handler = + function(self,handler) self.glb_btn_handler = handler end, + set_global_event_handler = + function(self,handler) self.glb_evt_handler = handler end, + set_fixed_size = + function(self,state) self.fixed_size = state end, + tab_header = tab_header, + handle_tab_buttons = handle_tab_buttons +} + +tabview_metatable.__index = tabview_metatable + +-------------------------------------------------------------------------------- +function tabview_create(name, size, tabheaderpos) + local self = {} + + self.name = name + self.type = "toplevel" + self.width = size.x + self.height = size.y + self.header_x = tabheaderpos.x + self.header_y = tabheaderpos.y + + setmetatable(self, tabview_metatable) + + self.fixed_size = true + self.hidden = true + self.current_tab = nil + self.last_tab_index = 1 + self.tablist = {} + + self.autosave_tab = false + + ui.add(self) + return self +end diff --git a/builtin/fstk/ui.lua b/builtin/fstk/ui.lua new file mode 100644 index 000000000..e0438247c --- /dev/null +++ b/builtin/fstk/ui.lua @@ -0,0 +1,172 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--self program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--self program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with self program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +ui = {} +ui.childlist = {} +ui.default = nil + +-------------------------------------------------------------------------------- +function ui.add(child) + --TODO check child + ui.childlist[child.name] = child + + return child.name +end + +-------------------------------------------------------------------------------- +function ui.delete(child) + + if ui.childlist[child.name] == nil then + return false + end + + ui.childlist[child.name] = nil + return true +end + +-------------------------------------------------------------------------------- +function ui.set_default(name) + ui.default = name +end + +-------------------------------------------------------------------------------- +function ui.find_by_name(name) + return ui.childlist[name] +end + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +-- Internal functions not to be called from user +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +function ui.update() + local formspec = "" + + -- handle errors + if gamedata ~= nil and gamedata.errormessage ~= nil then + formspec = "size[12,3.2]" .. + "textarea[1,1;10,2;;ERROR: " .. + core.formspec_escape(gamedata.errormessage) .. + ";]".. + "button[4.5,2.5;3,0.5;btn_error_confirm;" .. fgettext("Ok") .. "]" + else + local active_toplevel_ui_elements = 0 + for key,value in pairs(ui.childlist) do + if (value.type == "toplevel") then + local retval = value:get_formspec() + + if retval ~= nil and retval ~= "" then + active_toplevel_ui_elements = active_toplevel_ui_elements +1 + formspec = formspec .. retval + end + end + end + + -- no need to show addons if there ain't a toplevel element + if (active_toplevel_ui_elements > 0) then + for key,value in pairs(ui.childlist) do + if (value.type == "addon") then + local retval = value:get_formspec() + + if retval ~= nil and retval ~= "" then + formspec = formspec .. retval + end + end + end + end + + if (active_toplevel_ui_elements > 1) then + print("WARNING: ui manager detected more then one active ui element, self most likely isn't intended") + end + + if (active_toplevel_ui_elements == 0) then + print("WARNING: not a single toplevel ui element active switching to default") + ui.childlist[ui.default]:show() + formspec = ui.childlist[ui.default]:get_formspec() + end + end + core.update_formspec(formspec) +end + +-------------------------------------------------------------------------------- +function ui.handle_buttons(fields) + + if fields["btn_error_confirm"] then + gamedata.errormessage = nil + update_menu() + return + end + + for key,value in pairs(ui.childlist) do + + local retval = value:handle_buttons(fields) + + if retval then + ui.update() + return + end + end +end + + +-------------------------------------------------------------------------------- +function ui.handle_events(event) + + for key,value in pairs(ui.childlist) do + + if value.handle_events ~= nil then + local retval = value:handle_events(event) + + if retval then + print("event handled by: " .. key) + return retval + end + end + end +end + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +-- initialize callbacks +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +core.button_handler = function(fields) + if fields["btn_error_confirm"] then + gamedata.errormessage = nil + ui.update() + return + end + + if ui.handle_buttons(fields) then + ui.update() + end +end + +-------------------------------------------------------------------------------- +core.event_handler = function(event) + if ui.handle_events(event) then + ui.update() + return + end + + if event == "Refresh" then + ui.update() + return + end +end -- cgit v1.2.3