Last active
March 9, 2018 09:42
-
-
Save emptyrivers/99cbceb3fce5a2c6f8e3518789797820 to your computer and use it in GitHub Desktop.
GUI_layer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Abstraction layer for factorio GUI api, intended to extend the normal | |
functionality and simplify the process of handling GUI events. | |
How it works: | |
at the top of control.lua, write | |
local GUI = require "GUI" | |
edit the require path as necessary | |
call GUI:Init() during on_init, and GUI:Load during on_load | |
During on_player_created, call GUI:New(event.player_index) to create the | |
abstraction for that player (call GUI:Delete(event.player_index) on_player_removed). | |
You are then able to add more sophisticated gui elements. | |
to add gui elements, do like so: | |
--where `element` is any entry within our model | |
newElement = element:Add(widget) | |
widget should be a table, with the following data: | |
widget = { | |
-- prototype is what gets sent to the underlying factorio api. | |
-- Don't worry too much about the name attribute, since this will be handled separately. | |
-- Otherwise, follow the documentation http://lua-api.factorio.com/latest/LuaGuiElement.html#LuaGuiElement.add | |
prototype = <table>, | |
-- choose a unique string. GUI:Add() will modify the value you set here to ensure | |
-- that the element's name is truly unique for that player | |
-- note: unlike factorio, this abstraction **requires** that you give a name in order to function properly | |
name = <string>, | |
-- the following attributes are optional: | |
indestructible = <bool>, -- use to simulate top,left, and center's indestructibility | |
unique = <bool>, -- if true, then GUI:Add() assumes that you will not attempt to add a widget with the same name again. | |
-- supports OnAdd, OnDestroy, OnClear, which are called when the underlying element is | |
-- .add()ed, .destroy()ed, and .clear()ed, respectively | |
-- also supports event responses, keyed by event id. | |
-- note that these functions are saved, so they must not have any upvalues associated with them. | |
methods = <table>, | |
-- misc. attributes. It is recommended that you avoid name collisions. | |
-- If you set an attribute which collides with something in the abstraction *or* | |
-- the underlying element, then you will be unable to read or edit those values. | |
attributes = <table>, | |
} | |
Delete an element using | |
element:Destroy() | |
Clear it using: | |
element:Clear() | |
A shortcut for changing the visibility of an element is available | |
(equivalent to setting element.style.visible): | |
element:Hide() | |
element:Show() | |
element:Toggle() | |
All attributes from the underlying factorio api are available to you, as if | |
you were working with the base API. | |
If you use the OnClear/OnDestroy methods, note that the OnDestroy methods of | |
any descendants will be called as well, before the underlying element is actually destroyed. | |
]] | |
local GUI = {} | |
-- requires | |
local mod_gui = require "mod-gui" | |
require "util" | |
-- metatables | |
local elementMt = { | |
__index = function(self,key) | |
if type(key) == "number" then -- event ids are numbers | |
-- error("A gui event occured for a gui element with no response") | |
return -- uncomment the last line if you wish | |
end | |
return GUI[key] or self.__element[key] | |
end, | |
__newindex = function(self,key,value) | |
self.__element[key] = value | |
end, | |
} | |
local namePoolMt = { | |
__index = function(self,key) | |
self[key] = 1 | |
return 1 | |
end | |
} | |
-- Init/load/config scripts | |
function GUI:Init() | |
global.namePool = {} | |
global.models = {} | |
return global.models, global.namePool | |
end | |
function GUI:Load() | |
for _,model in pairs(global.models) do | |
self:setmetatable(model) | |
end | |
return global.models, global.namePool | |
end | |
function GUI:OnConfigurationChanged() | |
--dummy function, fill in with the edits to gui models which best suits your purposes | |
end | |
-- methods | |
function GUI:setmetatable(model) | |
for _, child in pairs(model.__flatmap) do | |
setmetatable(child, elementMt) | |
end | |
setmetatable(global.namePool[model.__element.player.index], namePoolMt) | |
return setmetatable(model, elementMt) | |
end | |
function GUI:New(playerID) | |
local player = game.players[playerID] | |
if not player then | |
error("Attempt to create a GUI model for a non-existant playerID: "..(playerID or 'nil')) | |
end | |
local model = { | |
__element = player.gui, | |
__flatmap = {}, | |
gui = model, | |
indestructible = true, | |
} | |
for id, method in pairs{top = 'get_button_flow',center = false,left = 'get_frame_flow'} do | |
model[id] = { | |
__element = method and mod_gui[method](player) or player.gui[id], | |
shown = true, | |
gui = model, | |
parent = model, | |
indestructible = true, | |
} | |
model.__flatmap[id] = model[id] | |
end | |
global.namePool[player.index] = {} | |
global.models[player.index] = model | |
return self:setmetatable(model) | |
end | |
function GUI:Delete(playerID) | |
local index = game.players[playerID].index -- there are more valid playerIDs then necessary, so get the uint version | |
global.namePool[index] = nil | |
global.models[index] = nil | |
end | |
-- helper function for Add | |
function AcquireName(name, playerID) | |
-- we could also release the name on GUI:PreDestroy(), i suppose. But that would require keeping a much larger data structure. | |
-- In practice, this will never cause collisions, since lua uses doubles for numbers, and it would overflow at 2^54 | |
local pool = global.namePool[game.players[playerID].index] | |
local id = pool[name] | |
pool[name] = pool[name] + 1 | |
return ("GUI_%s:%s:%s"):format(name,playerID,id),id | |
end | |
function GUI:Add(widget) | |
widget.prototype.name = not widget.unique and AcquireName(widget.name, self.player_index) or widget.name | |
local newElement = { | |
prototypeName = widget.name, | |
__element = self.add(widget.prototype), | |
gui = self.gui, | |
parent = self, | |
} | |
widget.prototype.name = nil | |
if widget.methods then | |
for eventID, method in pairs(widget.methods) do | |
newElement[eventID] = method | |
end | |
end | |
if widget.attributes then | |
local attributes = table.deepcopy(attributes) | |
for attributeID, attribute in pairs(attributes) do | |
newElement[attributeID] = attribute | |
end | |
end | |
--rawset because the parent of this new element already has a __newindex method, and it would try to write to the LuaGuiElement, causing an error | |
rawset(self,newElement.__element.name, setmetatable(newElement, elementMt)) | |
self.gui.__flatmap[newElement.name] = self[newElement.name] | |
if newElement.OnAdd then | |
newElement:OnAdd() | |
end | |
return newElement | |
end | |
function GUI:PreDestroy(...) | |
if self.OnDestroy then | |
self:OnDestroy(...) | |
end | |
self.gui.__flatmap[self.name] = nil | |
for _, child_name in pairs(self.children_names) do | |
self[child_name]:PreDestroy(...) | |
end | |
end | |
function GUI:Destroy(...) | |
if self.indestructible then | |
error("Attempt to destroy indestructible element: "..self.name) | |
end | |
self:PreDestroy(...) | |
self.parent[self.name] = nil | |
self.__element.destroy() | |
end | |
function GUI:Clear(...) | |
if self.OnClear then | |
self:OnClear(...) | |
end | |
for _,child_name in pairs(self.children_names) do | |
self[child_name]:PreDestroy(...) | |
self[child_name] = nil | |
end | |
self.__element.clear() | |
end | |
function GUI:Hide() | |
self.__element.style.visible = false | |
return true | |
end | |
function GUI:Show() | |
self.__element.style.visible = true | |
return true | |
end | |
function GUI:Toggle() | |
return self.__element.style.visible and self:Hide() or self:Show() | |
end | |
--script handler | |
script.on_event( | |
{ | |
defines.events.on_gui_checked_state_changed, | |
defines.events.on_gui_click, | |
defines.events.on_gui_elem_changed, | |
defines.events.on_gui_selection_state_changed, | |
defines.events.on_gui_text_changed, | |
}, | |
function(event) | |
local model = models[event.player_index] | |
if not model then | |
-- error("A gui event occured for a player with a non-existent model") | |
return | |
end | |
local element = model.__flatmap[event.element.name] | |
if element then | |
local response = element[event.name] | |
if response then | |
return response(element, event) | |
end | |
end | |
end | |
) | |
return GUI |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment