Feature: Toolbar Menu (#268)

* Fix left flow not using uids

* Mock Toolbar menu

* Fix task list after core gui change

* Allow show/hide override

* Fix autofill permissions

* Copy style from toolbar on change

* Open and close automatically

* Removed hacky prevent default

* Fixed more core issues

* Add reset button

* Allow for custom draw order on join

* Add methods to reorder ui flows

* Impliment move buttons

* Add locale

* Add toolbar to player data

* Better player data layout

* Picked a suitable datastore id

* Update locale for readme

* Fix swaping left flow order

* Fix datastore updates

* Code cleanup

* Fix incorrect top flow hashing on load

* Fix loading of malformed data

* Fixed loading state of left flows

* Dont save default data

* Dont open menu on join

* Lint

* Remove incorrect new index metamethod

* Revert method used for move_toolbar_button

* Fixed missing toolbar button

* Fixed desync between visibilty and toggle state

* Fix bad gui element path

* Fixed enable state of toggle button

* Change order of operations

* Fix reset not showing top flow
This commit is contained in:
Cooldude2606
2024-01-16 00:01:00 +00:00
committed by GitHub
parent 96813816b9
commit 1c8a97a339
19 changed files with 912 additions and 168 deletions

View File

@@ -6,7 +6,7 @@
return { return {
--'example.file_not_loaded', --'example.file_not_loaded',
'modules.factorio-control', -- base factorio free play scenario 'modules.factorio-control', -- base factorio free play scenario
'expcore.player_data', 'expcore.player_data', -- must be loaded first to register event handlers
--- Game Commands --- Game Commands
'modules.commands.debug', 'modules.commands.debug',
@@ -97,6 +97,7 @@ return {
'modules.gui.playerdata', 'modules.gui.playerdata',
'modules.gui.surveillance', 'modules.gui.surveillance',
'modules.graftorio.require', -- graftorio 'modules.graftorio.require', -- graftorio
'modules.gui.toolbar', -- must be loaded last to register toolbar handlers
--- Config Files --- Config Files
'config.expcore.command_auth_admin', -- commands tagged with admin_only are blocked for non admins 'config.expcore.command_auth_admin', -- commands tagged with admin_only are blocked for non admins

View File

@@ -288,6 +288,7 @@ local default = Roles.new_role('Guest','')
'gui/readme', 'gui/readme',
'gui/vlayer', 'gui/vlayer',
'gui/research', 'gui/research',
'gui/autofill',
'gui/module' 'gui/module'
} }

View File

@@ -522,7 +522,7 @@ function Datastore:increment(key, delta)
return self:set(key, value + (delta or 1)) return self:set(key, value + (delta or 1))
end end
local function update_error(err) log('An error occurred in datastore update: '..trace(err)) end local function update_error(err) log('An error occurred in datastore update:\n\t'..trace(err)) end
--[[-- Use a function to update the value locally, will trigger on_update then on_save, save_to_disk and auto_save is required for on_save --[[-- Use a function to update the value locally, will trigger on_update then on_save, save_to_disk and auto_save is required for on_save
@tparam any key The key that you want to apply the update to, must be a string unless a serializer is set @tparam any key The key that you want to apply the update to, must be a string unless a serializer is set
@tparam function callback The function that will be used to update the value at this key @tparam function callback The function that will be used to update the value at this key
@@ -536,13 +536,16 @@ end)
]] ]]
function Datastore:update(key, callback) function Datastore:update(key, callback)
key = self:serialize(key) key = self:serialize(key)
local value = self:raw_get(key) local value = self:get(key)
local raw_value = self:raw_get(key)
local old_value = copy(self:raw_get(key)) local old_value = copy(self:raw_get(key))
local success, new_value = xpcall(callback, update_error, key, value) local success, new_value = xpcall(callback, update_error, key, value)
if not success then if not success then
self:raw_set(key, old_value) self:raw_set(key, old_value)
elseif new_value ~= nil then elseif new_value ~= nil then
self:set(key, new_value) self:set(key, new_value)
elseif raw_value == nil then
self:set(key, value)
else else
self:raise_event('on_update', key, value, old_value) self:raise_event('on_update', key, value, old_value)
if self.auto_save then self:save(key) end if self.auto_save then self:save(key) end
@@ -566,7 +569,7 @@ function Datastore:remove(key)
if self.parent and self.parent.auto_save then return self.parent:save(key) end if self.parent and self.parent.auto_save then return self.parent:save(key) end
end end
local function filter_error(err) log('An error ocurred in a datastore filter:'..trace(err)) end local function filter_error(err) log('An error ocurred in a datastore filter:\n\t'..trace(err)) end
--[[-- Internal, Used to filter elements from a table --[[-- Internal, Used to filter elements from a table
@tparam table tbl The table that will have the filter applied to it @tparam table tbl The table that will have the filter applied to it
@tparam[opt] function callback The function that will be used as a filter, if none giving then the provided table is returned @tparam[opt] function callback The function that will be used as a filter, if none giving then the provided table is returned
@@ -744,7 +747,7 @@ end
----- Events ----- Events
-- @section events -- @section events
local function event_error(err) log('An error ocurred in a datastore event handler: '..trace(err)) end local function event_error(err) log('An error ocurred in a datastore event handler:\n\t'..trace(err)) end
--[[-- Internal, Raise an event on this datastore --[[-- Internal, Raise an event on this datastore
@tparam string event_name The name of the event to raise for this datastore @tparam string event_name The name of the event to raise for this datastore
@tparam string key The key that this event is being raised for @tparam string key The key that this event is being raised for

View File

@@ -122,10 +122,10 @@ end)
]] ]]
local Gui = require 'expcore.gui.prototype' local Gui = require 'expcore.gui.prototype'
require 'expcore.gui.helper_functions'
require 'expcore.gui.core_defines' require 'expcore.gui.core_defines'
require 'expcore.gui.top_flow' require 'expcore.gui.top_flow'
require 'expcore.gui.left_flow' require 'expcore.gui.left_flow'
require 'expcore.gui.helper_functions'
require 'expcore.gui.defines' require 'expcore.gui.defines'
local Roles = _C.opt_require('expcore.roles') local Roles = _C.opt_require('expcore.roles')

View File

@@ -25,7 +25,7 @@ Gui.element{
height = 36 height = 36
} }
:on_click(function(player, _,_) :on_click(function(player, _,_)
Gui.toggle_top_flow(player) Gui.toggle_top_flow(player, false)
end) end)
Gui.core_defines.hide_top_flow = hide_top_flow Gui.core_defines.hide_top_flow = hide_top_flow
@@ -45,7 +45,7 @@ Gui.element{
height = 20 height = 20
} }
:on_click(function(player, _,_) :on_click(function(player, _,_)
Gui.toggle_top_flow(player) Gui.toggle_top_flow(player, true)
end) end)
Gui.core_defines.show_top_flow = show_top_flow Gui.core_defines.show_top_flow = show_top_flow

View File

@@ -38,8 +38,10 @@ example_flow_with_button:add_to_left_flow(true)
]] ]]
function Gui._prototype_element:add_to_left_flow(open_on_join) function Gui._prototype_element:add_to_left_flow(open_on_join)
_C.error_if_runtime()
if not self.name then error("Elements for the top flow must have a static name") end if not self.name then error("Elements for the top flow must have a static name") end
Gui.left_elements[self] = open_on_join or false self.open_on_join = open_on_join or false
table.insert(Gui.left_elements, self)
return self return self
end end
@@ -60,26 +62,38 @@ function Gui.left_toolbar_button(sprite, tooltip, element_define, authenticator)
local button = Gui.toolbar_button(sprite, tooltip, authenticator) local button = Gui.toolbar_button(sprite, tooltip, authenticator)
-- Add on_click handler to handle click events comming from the player -- Add on_click handler to handle click events comming from the player
button:on_click(function(player, _,_) button:on_click(function(player, _, _)
local top_flow = Gui.get_top_flow(player)
local element = top_flow[button.name]
local visibility_state = Gui.toggle_left_element(player, element_define)
-- Raise custom event that tells listening elements if the element has changed visibility by a player clicking -- Raise custom event that tells listening elements if the element has changed visibility by a player clicking
-- Used in warp gui to handle the keep open logic -- Used in warp gui to handle the keep open logic
button:raise_custom_event{ button:raise_event{
name = Gui.events.on_visibility_changed_by_click, name = Gui.events.on_visibility_changed_by_click,
element = element, element = Gui.get_top_element(player, button),
state = visibility_state state = Gui.toggle_left_element(player, element_define)
} }
end) end)
-- Add property to the left flow element with the name of the button -- Add property to the left flow element with the name of the button
-- This is for the ability to reverse lookup the button from the left flow element -- This is for the ability to reverse lookup the button from the left flow element
element_define.toolbar_button = button.name element_define.toolbar_button = button
button.left_flow_element = element_define
return button return button
end end
Gui._left_flow_order_src = "<default>"
--- Get the order of elements in the left flow, first argument is player but is unused in the default method
function Gui.get_left_flow_order(_)
return Gui.left_elements
end
--- Inject a custom left flow order provider, this should accept a player and return a list of elements definitions to draw
function Gui.inject_left_flow_order(provider)
Gui.get_left_flow_order = provider
local debug_info = debug.getinfo(2, "Sn")
local file_name = debug_info.source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
local func_name = debug_info.name or "<anonymous:"..debug_info.linedefined..">"
Gui._left_flow_order_src = file_name..":"..func_name
end
--[[-- Draw all the left elements onto the left flow, internal use only with on join --[[-- Draw all the left elements onto the left flow, internal use only with on join
@tparam LuaPlayer player the player that you want to draw the elements for @tparam LuaPlayer player the player that you want to draw the elements for
@@ -92,22 +106,33 @@ function Gui.draw_left_flow(player)
local hide_button = left_flow.gui_core_buttons[hide_left_flow] local hide_button = left_flow.gui_core_buttons[hide_left_flow]
local show_hide_button = false local show_hide_button = false
for element_define, open_on_join in pairs(Gui.left_elements) do -- Get the order to draw the elements in
local flow_order = Gui.get_left_flow_order(player)
if #flow_order ~= #Gui.left_elements then
error(string.format("Left flow order provider (%s) did not return the correct element count, expect %d got %d",
Gui._left_flow_order_src, #Gui.left_elements, #flow_order
))
end
for _, element_define in ipairs(flow_order) do
-- Draw the element to the left flow -- Draw the element to the left flow
local draw_success, left_element = xpcall(function() local draw_success, left_element = xpcall(function()
return element_define(left_flow) return element_define(left_flow)
end, debug.traceback) end, debug.traceback)
if not draw_success then if not draw_success then
error('There as been an error with an element draw function: '..element_define.defined_at..'\n\t'..left_element) log('There as been an error with an element draw function: '..element_define.defined_at..'\n\t'..left_element)
goto continue
end end
-- Check if it should be open by default -- Check if it should be open by default
local open_on_join = element_define.open_on_join
local visible = type(open_on_join) == 'boolean' and open_on_join or false local visible = type(open_on_join) == 'boolean' and open_on_join or false
if type(open_on_join) == 'function' then if type(open_on_join) == 'function' then
local success, err = pcall(open_on_join, player) local success, err = xpcall(open_on_join, debug.traceback, player)
if not success then if not success then
error('There as been an error with an open on join hander for a gui element:\n\t'..err) log('There as been an error with an open on join hander for a gui element:\n\t'..err)
goto continue
end end
visible = err visible = err
end end
@@ -116,23 +141,35 @@ function Gui.draw_left_flow(player)
left_element.visible = visible left_element.visible = visible
show_hide_button = show_hide_button or visible show_hide_button = show_hide_button or visible
-- Get the assosiated element define
local top_flow = Gui.get_top_flow(player)
-- Check if the the element has a button attached -- Check if the the element has a button attached
if element_define.toolbar_button then if element_define.toolbar_button then
-- Check if the topflow contains the button Gui.toggle_toolbar_button(player, element_define.toolbar_button, visible)
local button = top_flow[element_define.toolbar_button]
if button then
-- Style the button
Gui.toolbar_button_style(button, visible)
end
end end
::continue::
end end
hide_button.visible = show_hide_button hide_button.visible = show_hide_button
end end
--- Reorder the left flow elements to match that returned by the provider, uses a method equivalent to insert sort
function Gui.reorder_left_flow(player)
local left_flow = Gui.get_left_flow(player)
-- Get the order to draw the elements in
local flow_order = Gui.get_left_flow_order(player)
if #flow_order ~= #Gui.left_elements then
error(string.format("Left flow order provider (%s) did not return the correct element count, expect %d got %d",
Gui._left_flow_order_src, #Gui.left_elements, #flow_order
))
end
-- Reorder the elements, index 1 is the core ui buttons so +1 is required
for index, element_define in ipairs(flow_order) do
local element = left_flow[element_define.name]
left_flow.swap_children(index+1, element.get_index_in_parent())
end
end
--[[-- Update the visible state of the hide button, can be used to check if any frames are visible --[[-- Update the visible state of the hide button, can be used to check if any frames are visible
@tparam LuaPlayer player the player to update the left flow for @tparam LuaPlayer player the player to update the left flow for
@treturn boolean true if any left element is visible @treturn boolean true if any left element is visible
@@ -144,7 +181,7 @@ local visible = Gui.update_left_flow(player)
function Gui.update_left_flow(player) function Gui.update_left_flow(player)
local left_flow = Gui.get_left_flow(player) local left_flow = Gui.get_left_flow(player)
local hide_button = left_flow.gui_core_buttons[hide_left_flow] local hide_button = left_flow.gui_core_buttons[hide_left_flow]
for element_define, _ in pairs(Gui.left_elements) do for _, element_define in ipairs(Gui.left_elements) do
local left_element = left_flow[element_define.name] local left_element = left_flow[element_define.name]
if left_element.visible then if left_element.visible then
hide_button.visible = true hide_button.visible = true
@@ -169,20 +206,18 @@ function Gui.hide_left_flow(player)
-- Set the visible state of all elements in the flow -- Set the visible state of all elements in the flow
hide_button.visible = false hide_button.visible = false
for element_define, _ in pairs(Gui.left_elements) do for _, element_define in ipairs(Gui.left_elements) do
left_flow[element_define.name].visible = false left_flow[element_define.name].visible = false
-- Check if the the element has a toobar button attached -- Check if the the element has a toobar button attached
if element_define.toolbar_button then if element_define.toolbar_button then
-- Check if the topflow contains the button -- Check if the topflow contains the button
local button = top_flow[element_define.toolbar_button] local button = top_flow[element_define.toolbar_button.name]
if button then if button then
-- Style the button -- Style the button
Gui.toolbar_button_style(button, false) Gui.toggle_toolbar_button(player, element_define.toolbar_button, false)
-- Get the button define from the reverse lookup on the element
local button_define = Gui.defines[element_define.toolbar_button]
-- Raise the custom event if all of the top checks have passed -- Raise the custom event if all of the top checks have passed
button_define:raise_custom_event{ element_define.toolbar_button:raise_event{
name = Gui.events.on_visibility_changed_by_click, name = Gui.events.on_visibility_changed_by_click,
element = button, element = button,
state = false state = false
@@ -192,6 +227,12 @@ function Gui.hide_left_flow(player)
end end
end end
--- Checks if an element is loaded, used internally when the normal left gui assumptions may not hold
function Gui.left_flow_loaded(player, element_define)
local left_flow = Gui.get_left_flow(player)
return left_flow[element_define.name] ~= nil
end
--[[-- Get the element define that is in the left flow, use in events without an element refrence --[[-- Get the element define that is in the left flow, use in events without an element refrence
@tparam LuaPlayer player the player that you want to get the element for @tparam LuaPlayer player the player that you want to get the element for
@tparam table element_define the element that you want to get @tparam table element_define the element that you want to get
@@ -203,7 +244,7 @@ local frame = Gui.get_left_element(game.player, example_flow_with_button)
]] ]]
function Gui.get_left_element(player, element_define) function Gui.get_left_element(player, element_define)
local left_flow = Gui.get_left_flow(player) local left_flow = Gui.get_left_flow(player)
return left_flow[element_define.name] return assert(left_flow[element_define.name], "Left element failed to load")
end end
--[[-- Toggles the visible state of a left element for a given player, can be used to set the visible state --[[-- Toggles the visible state of a left element for a given player, can be used to set the visible state
@@ -220,23 +261,15 @@ Gui.toggle_top_flow(game.player, example_flow_with_button, true)
]] ]]
function Gui.toggle_left_element(player, element_define, state) function Gui.toggle_left_element(player, element_define, state)
local left_flow = Gui.get_left_flow(player)
local top_flow = Gui.get_top_flow(player)
-- Set the visible state -- Set the visible state
local element = left_flow[element_define.name] local element = Gui.get_left_element(player, element_define)
if state == nil then state = not element.visible end if state == nil then state = not element.visible end
element.visible = state element.visible = state
Gui.update_left_flow(player) Gui.update_left_flow(player)
-- Check if the the element has a button attached -- Check if the the element has a button attached
if element_define.toolbar_button then if element_define.toolbar_button then
-- Check if the topflow contains the button Gui.toggle_toolbar_button(player, element_define.toolbar_button, state)
local button = top_flow[element_define.toolbar_button]
if button then
-- Style the button
Gui.toolbar_button_style(button, state)
end
end end
return state return state
end end

View File

@@ -23,20 +23,46 @@ local Gui = {
--- The prototype used to store the functions of an element define --- The prototype used to store the functions of an element define
_prototype_element = {}, _prototype_element = {},
--- The prototype metatable applied to new element defines --- The prototype metatable applied to new element defines
_mt_element = { _mt_element = {}
__call = function(self, parent, ...)
local element = self._draw(self, parent, ...)
if self._style then self._style(element.style, element, ...) end
if self.name and self.name ~= element.name then
error("Static name \""..self.name.."\" expected but got: "..tostring(element.name))
end
return element and self:triggers_events(element)
end
}
} }
--- Allow access to the element prototype methods
Gui._mt_element.__index = Gui._prototype_element Gui._mt_element.__index = Gui._prototype_element
--- Allows the define to be called to draw the element
function Gui._mt_element.__call(self, parent, ...)
local element, no_events = self._draw(self, parent, ...)
if self._style then self._style(element.style, element, ...) end
-- Asserts to catch common errors
if element then
if self.name and self.name ~= element.name then
error("Static name \""..self.name.."\" expected but got: "..tostring(element.name))
end
local event_triggers = element.tags and element.tags.ExpGui_event_triggers
if event_triggers and table.array_contains(event_triggers, self.uid) then
error("Element::triggers_events should not be called on the value you return from the definition")
end
elseif self.name then
error("Static name \""..self.name.."\" expected but no element was returned from the definition")
end
-- Register events by default, but allow skipping them
if no_events == self.no_events then
return element
else
return element and self:triggers_events(element)
end
end
--- Get where a function was defined as a string
local function get_defined_at(level)
local debug_info = debug.getinfo(level, "Sn")
local file_name = debug_info.source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
local func_name = debug_info.name or "<anonymous:"..debug_info.linedefined..">"
return file_name..":"..func_name
end
--- Element Define. --- Element Define.
-- @section elementDefine -- @section elementDefine
@@ -97,20 +123,21 @@ function Gui.element(element_define)
if element_define.name == Gui.unique_static_name then if element_define.name == Gui.unique_static_name then
element_define.name = "ExpGui_"..tostring(uid) element_define.name = "ExpGui_"..tostring(uid)
end end
element.name = element_define.name for k, v in pairs(element_define) do
if element[k] == nil then
element[k] = v
end
end
element._draw = function(_, parent) element._draw = function(_, parent)
return parent.add(element_define) return parent.add(element_define)
end end
else else
Gui.debug_info[uid].draw = 'Function' Gui.debug_info[uid].draw = get_defined_at(element_define)
element._draw = element_define element._draw = element_define
end end
-- Add the define to the base module -- Add the define to the base module
local debug_info = debug.getinfo(2, "Sn") element.defined_at = get_defined_at(3)
local file_name = debug_info.source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
local func_name = debug_info.name or "<anonymous:"..debug_info.linedefined..">"
element.defined_at = file_name..":"..func_name
Gui.file_paths[uid] = element.defined_at Gui.file_paths[uid] = element.defined_at
Gui.defines[uid] = element Gui.defines[uid] = element
@@ -154,6 +181,7 @@ end)
]] ]]
function Gui._prototype_element:style(style_define) function Gui._prototype_element:style(style_define)
_C.error_if_runtime()
-- Add the definition function -- Add the definition function
if type(style_define) == 'table' then if type(style_define) == 'table' then
Gui.debug_info[self.uid].style = style_define Gui.debug_info[self.uid].style = style_define
@@ -163,7 +191,7 @@ function Gui._prototype_element:style(style_define)
end end
end end
else else
Gui.debug_info[self.uid].style = 'Function' Gui.debug_info[self.uid].style = get_defined_at(style_define)
self._style = style_define self._style = style_define
end end
@@ -176,6 +204,7 @@ end
@treturn table the element define is returned to allow for event handlers to be registered @treturn table the element define is returned to allow for event handlers to be registered
]] ]]
function Gui._prototype_element:static_name(name) function Gui._prototype_element:static_name(name)
_C.error_if_runtime()
if name == Gui.unique_static_name then if name == Gui.unique_static_name then
self.name = "ExpGui_"..tostring(self.uid) self.name = "ExpGui_"..tostring(self.uid)
else else
@@ -185,16 +214,20 @@ function Gui._prototype_element:static_name(name)
end end
--[[-- Used to link an element to an element define such that any event on the element will call the handlers on the element define --[[-- Used to link an element to an element define such that any event on the element will call the handlers on the element define
-- You should not call this on the element you return from your constructor because this is done automatically
@tparam LuaGuiElement element The element that will trigger calls to the event handlers @tparam LuaGuiElement element The element that will trigger calls to the event handlers
@treturn LuaGuiElement The element passed as the argument to allow for cleaner returns @treturn LuaGuiElement The element passed as the argument to allow for cleaner returns
]] ]]
function Gui._prototype_element:triggers_events(element) function Gui._prototype_element:triggers_events(element)
if not self._has_events then return element end
local tags = element.tags local tags = element.tags
if not tags then if not tags then
element.tags = { ExpGui_event_triggers = { self.uid } } element.tags = { ExpGui_event_triggers = { self.uid } }
return element return element
elseif not tags.ExpGui_event_triggers then elseif not tags.ExpGui_event_triggers then
tags.ExpGui_event_triggers = { self.uid } tags.ExpGui_event_triggers = { self.uid }
elseif table.array_contains(tags.ExpGui_event_triggers, self.uid) then
error("Element::triggers_events called multiple times on the same element with the same definition")
else else
table.insert(tags.ExpGui_event_triggers, self.uid) table.insert(tags.ExpGui_event_triggers, self.uid)
end end
@@ -203,21 +236,28 @@ function Gui._prototype_element:triggers_events(element)
return element return element
end end
--- Explicitly skip events on the element returned by your definition function
function Gui._prototype_element:no_events(element)
return element, self.no_events
end
--[[-- Set the handler which will be called for a custom event, only one handler can be used per event per element --[[-- Set the handler which will be called for a custom event, only one handler can be used per event per element
@tparam string event_name the name of the event you want to handler to be called on, often from Gui.events @tparam string event_name the name of the event you want to handler to be called on, often from Gui.events
@tparam function handler the handler that you want to be called when the event is raised @tparam function handler the handler that you want to be called when the event is raised
@treturn table the element define so more handleres can be registered @treturn table the element define so more handleres can be registered
@usage-- Register a handler to "my_custom_event" for this element @usage-- Register a handler to "my_custom_event" for this element
element_deinfe:on_custom_event('my_custom_event', function(event) element_deinfe:on_event('my_custom_event', function(event)
event.player.print(player.name) event.player.print(player.name)
end) end)
]] ]]
function Gui._prototype_element:on_custom_event(event_name, handler) function Gui._prototype_element:on_event(event_name, handler)
_C.error_if_runtime()
table.insert(Gui.debug_info[self.uid].events, event_name) table.insert(Gui.debug_info[self.uid].events, event_name)
Gui.events[event_name] = event_name Gui.events[event_name] = event_name
self[event_name] = handler self[event_name] = handler
self._has_events = true
return self return self
end end
@@ -226,13 +266,13 @@ end
@treturn table the element define so more events can be raised @treturn table the element define so more events can be raised
@usage Raising a custom event @usage Raising a custom event
element_define:raise_custom_event{ element_define:raise_event{
name = 'my_custom_event', name = 'my_custom_event',
element = element element = element
} }
]] ]]
function Gui._prototype_element:raise_custom_event(event) function Gui._prototype_element:raise_event(event)
-- Check the element is valid -- Check the element is valid
local element = event.element local element = event.element
if not element or not element.valid then if not element or not element.valid then
@@ -270,15 +310,14 @@ local function event_handler_factory(event_name)
for _, uid in pairs(event_triggers) do for _, uid in pairs(event_triggers) do
local element_define = Gui.defines[uid] local element_define = Gui.defines[uid]
if element_define then if element_define then
element_define:raise_custom_event(event) element_define:raise_event(event)
end end
end end
end) end)
Gui.events[event_name] = event_name
return function(self, handler) return function(self, handler)
table.insert(Gui.debug_info[self.uid].events, debug.getinfo(1, "n").name) return self:on_event(event_name, handler)
self[event_name] = handler
return self
end end
end end

View File

@@ -6,12 +6,16 @@
local Gui = require 'expcore.gui.prototype' local Gui = require 'expcore.gui.prototype'
local mod_gui = require 'mod-gui' --- @dep mod-gui local mod_gui = require 'mod-gui' --- @dep mod-gui
local toolbar_button_size = 36
local hide_top_flow = Gui.core_defines.hide_top_flow.name local hide_top_flow = Gui.core_defines.hide_top_flow.name
local show_top_flow = Gui.core_defines.show_top_flow.name local show_top_flow = Gui.core_defines.show_top_flow.name
--- Top Flow. --- Top Flow.
-- @section topFlow -- @section topFlow
-- Triggered when a user changed the visibility of a left flow element by clicking a button
Gui.events.on_toolbar_button_toggled = 'on_toolbar_button_toggled'
--- Contains the uids of the elements that will shown on the top flow and their auth functions --- Contains the uids of the elements that will shown on the top flow and their auth functions
-- @table top_elements -- @table top_elements
Gui.top_elements = {} Gui.top_elements = {}
@@ -21,8 +25,30 @@ Gui.top_elements = {}
Gui.top_flow_button_style = mod_gui.button_style Gui.top_flow_button_style = mod_gui.button_style
--- The style that should be used for buttons on the top flow when their flow is visible --- The style that should be used for buttons on the top flow when their flow is visible
-- @field Gui.top_flow_button_visible_style -- @field Gui.top_flow_button_toggled_style
Gui.top_flow_button_visible_style = 'menu_button_continue' Gui.top_flow_button_toggled_style = 'menu_button_continue'
--[[-- Styles a top flow button depending on the state given
@tparam LuaGuiElement button the button element to style
@tparam boolean state The state the button is in
@usage-- Sets the button to the visible style
Gui.toolbar_button_style(button, true)
@usage-- Sets the button to the hidden style
Gui.toolbar_button_style(button, false)
]]
function Gui.toolbar_button_style(button, state, size)
if state then
button.style = Gui.top_flow_button_toggled_style
else
button.style = Gui.top_flow_button_style
end
button.style.minimal_width = size or toolbar_button_size
button.style.height = size or toolbar_button_size
button.style.padding = -2
end
--[[-- Gets the flow refered to as the top flow, each player has one top flow --[[-- Gets the flow refered to as the top flow, each player has one top flow
@function Gui.get_top_flow(player) @function Gui.get_top_flow(player)
@@ -48,11 +74,43 @@ end)
]] ]]
function Gui._prototype_element:add_to_top_flow(authenticator) function Gui._prototype_element:add_to_top_flow(authenticator)
_C.error_if_runtime()
if not self.name then error("Elements for the top flow must have a static name") end if not self.name then error("Elements for the top flow must have a static name") end
Gui.top_elements[self] = authenticator or true self.authenticator = authenticator or true
table.insert(Gui.top_elements, self)
return self return self
end end
--- Returns true if the top flow has visible elements
function Gui.top_flow_has_visible_elements(player)
local top_flow = Gui.get_top_flow(player)
for _, child in pairs(top_flow.children) do
if child.name ~= hide_top_flow then
if child.visible then
return true
end
end
end
return false
end
Gui._top_flow_order_src = "<default>"
--- Get the order of elements in the top flow, first argument is player but is unused in the default method
function Gui.get_top_flow_order(_)
return Gui.top_elements
end
--- Inject a custom top flow order provider, this should accept a player and return a list of elements definitions to draw
function Gui.inject_top_flow_order(provider)
Gui.get_top_flow_order = provider
local debug_info = debug.getinfo(2, "Sn")
local file_name = debug_info.source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
local func_name = debug_info.name or "<anonymous:"..debug_info.linedefined..">"
Gui._top_flow_order_src = file_name..":"..func_name
end
--[[-- Updates the visible state of all the elements on the players top flow, uses authenticator --[[-- Updates the visible state of all the elements on the players top flow, uses authenticator
@tparam LuaPlayer player the player that you want to update the top flow for @tparam LuaPlayer player the player that you want to update the top flow for
@@ -62,21 +120,62 @@ Gui.update_top_flow(game.player)
]] ]]
function Gui.update_top_flow(player) function Gui.update_top_flow(player)
local top_flow = Gui.get_top_flow(player) local top_flow = Gui.get_top_flow(player)
local hide_button = top_flow[hide_top_flow]
local is_visible = hide_button.visible -- Get the order to draw the elements in
local flow_order = Gui.get_top_flow_order(player)
if #flow_order ~= #Gui.top_elements then
error(string.format("Top flow order provider (%s) did not return the correct element count, expect %d got %d",
Gui._top_flow_order_src, #Gui.top_elements, #flow_order
))
end
-- Set the visible state of all elements in the flow -- Set the visible state of all elements in the flow
for element_define, authenticator in pairs(Gui.top_elements) do for index, element_define in ipairs(flow_order) do
-- Ensure the element exists -- Ensure the element exists
local element = top_flow[element_define.name] local element = top_flow[element_define.name]
if not element then if not element then
element = element_define(top_flow) element = element_define(top_flow)
else
top_flow.swap_children(index+1, element.get_index_in_parent())
end end
-- Set the visible state -- Set the visible state
local allowed = authenticator local allowed = element_define.authenticator
if type(allowed) == 'function' then allowed = allowed(player) end if type(allowed) == 'function' then allowed = allowed(player) end
element.visible = is_visible and allowed or false element.visible = allowed or false
-- If its not visible and there is a left element, then hide it
if element_define.left_flow_element and not element.visible and Gui.left_flow_loaded(player, element_define.left_flow_element) then
Gui.toggle_left_element(player, element_define.left_flow_element, false)
end
end
-- Check if there are any visible elements in the top flow
if not Gui.top_flow_has_visible_elements(player) then
-- None are visible so hide the top_flow and its show button
Gui.toggle_top_flow(player, false)
local left_flow = Gui.get_left_flow(player)
local show_button = left_flow.gui_core_buttons[show_top_flow]
show_button.visible = false
end
end
--- Reorder the top flow elements to match that returned by the provider, uses a method equivalent to insert sort
function Gui.reorder_top_flow(player)
local top_flow = Gui.get_top_flow(player)
-- Get the order to draw the elements in
local flow_order = Gui.get_top_flow_order(player)
if #flow_order ~= #Gui.top_elements then
error(string.format("Top flow order provider (%s) did not return the correct element count, expect %d got %d",
Gui._top_flow_order_src, #Gui.top_elements, #flow_order
))
end
-- Reorder the elements, index 1 is the core ui buttons so +1 is required
for index, element_define in ipairs(flow_order) do
local element = top_flow[element_define.name]
top_flow.swap_children(index+1, element.get_index_in_parent())
end end
end end
@@ -119,7 +218,33 @@ local button = Gui.get_top_element(game.player, example_button)
]] ]]
function Gui.get_top_element(player, element_define) function Gui.get_top_element(player, element_define)
local top_flow = Gui.get_top_flow(player) local top_flow = Gui.get_top_flow(player)
return top_flow[element_define.name] return assert(top_flow[element_define.name], "Top element failed to load")
end
--[[-- Toggles the state of a toolbar button for a given player, can be used to set the visual state
@tparam LuaPlayer player the player that you want to toggle the element for
@tparam table element_define the element that you want to toggle
@tparam[opt] boolean state with given will set the state, else state will be toggled
@treturn boolean the new visible state of the element
@usage-- Toggle your example button
Gui.toggle_toolbar_button(game.player, toolbar_button)
@usage-- Show your example button
Gui.toggle_toolbar_button(game.player, toolbar_button, true)
]]
function Gui.toggle_toolbar_button(player, element_define, state)
local toolbar_button = Gui.get_top_element(player, element_define)
if state == nil then state = toolbar_button.style.name ~= Gui.top_flow_button_toggled_style end
Gui.toolbar_button_style(toolbar_button, state, toolbar_button.style.minimal_width)
element_define:raise_event{
name = Gui.events.on_toolbar_button_toggled,
element = toolbar_button,
player = player,
state = state
}
return state
end end
--[[-- Creates a button on the top flow with consistent styling --[[-- Creates a button on the top flow with consistent styling
@@ -143,31 +268,47 @@ function Gui.toolbar_button(sprite, tooltip, authenticator)
name = Gui.unique_static_name name = Gui.unique_static_name
} }
:style{ :style{
minimal_width = 36, minimal_width = toolbar_button_size,
height = 36, height = toolbar_button_size,
padding = -2 padding = -2
} }
:add_to_top_flow(authenticator) :add_to_top_flow(authenticator)
end end
--[[-- Styles a top flow button depending on the state given --[[-- Creates a toggle button on the top flow with consistent styling
@tparam LuaGuiElement button the button element to style @tparam string sprite the sprite that you want to use on the button
@tparam boolean state The state the button is in @tparam ?string|Concepts.LocalizedString tooltip the tooltip that you want the button to have
@tparam[opt] function authenticator used to decide if the button should be visible to a player
@usage-- Sets the button to the visible style @usage-- Add a button to the toolbar
Gui.toolbar_button_style(button, true) local toolbar_button =
Gui.toolbar_toggle_button('entity/inserter', 'Nothing to see here', function(player)
@usage-- Sets the button to the hidden style return player.admin
Gui.toolbar_button_style(button, false) end)
:on_event(Gui.events.on_toolbar_button_toggled, function(player, element, event)
game.print(table.inspect(event))
end)
]] ]]
function Gui.toolbar_button_style(button, state) function Gui.toolbar_toggle_button(sprite, tooltip, authenticator)
if state then local button =
button.style = Gui.top_flow_button_visible_style Gui.element{
else type = 'sprite-button',
button.style = Gui.top_flow_button_style sprite = sprite,
end tooltip = tooltip,
button.style.minimal_width = 36 style = Gui.top_flow_button_style,
button.style.height = 36 name = Gui.unique_static_name
button.style.padding = -2 }
:style{
minimal_width = toolbar_button_size,
height = toolbar_button_size,
padding = -2
}
:add_to_top_flow(authenticator)
button:on_click(function(player, _, _)
Gui.toggle_toolbar_button(player, button)
end)
return button
end end

View File

@@ -37,6 +37,9 @@ Bonus=Player Bonus
Bonus-tooltip=The bonus given to your character Bonus-tooltip=The bonus given to your character
Bonus-value-tooltip=Change by using /bonus Bonus-value-tooltip=Change by using /bonus
HasEnabledDecon=Quick Tree Decon HasEnabledDecon=Quick Tree Decon
ToolbarState=Toolbox
ToolbarState-tooltip=The order and favourites in your toolbox
ToolbarState-value-tooltip=This value is calculated automatically when you leave the game
[exp-statistics] [exp-statistics]
MapsPlayed=Maps Played MapsPlayed=Maps Played

View File

@@ -101,7 +101,7 @@ inserted=Inserted __1__ __2__ into __3__
[warp-list] [warp-list]
main-caption=Warp List [img=info] main-caption=Warp List [img=info]
main-tooltip=Warp List; Must be within __1__ tiles to use main-tooltip=Warp List
sub-tooltip=Warps can only be used every __1__ seconds and when within __2__ tiles\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__ sub-tooltip=Warps can only be used every __1__ seconds and when within __2__ tiles\n__3__\n__4__\n__5__\n__6__\n__7__\n__8__
sub-tooltip-current= - __1__ This is your current warp point; sub-tooltip-current= - __1__ This is your current warp point;
sub-tooltip-connected= - __1__ You can travel to this warp point; sub-tooltip-connected= - __1__ You can travel to this warp point;
@@ -214,3 +214,11 @@ main-tooltip=Enable Vlayer GUI
[module] [module]
main-tooltip=Enable Module GUI main-tooltip=Enable Module GUI
[toolbar]
main-caption=Toolbox
main-tooltip=Toolbox Settings\nUse the checkboxs to select facourites
reset=Reset All
toggle=Toggle Favourites
move-up=Move Up
move-down=Move Down

View File

@@ -30,14 +30,12 @@ end
local HasEnabledDecon = PlayerData.Settings:combine('HasEnabledDecon') local HasEnabledDecon = PlayerData.Settings:combine('HasEnabledDecon')
HasEnabledDecon:set_default(false) HasEnabledDecon:set_default(false)
Gui.toolbar_button("entity/tree-01", {'tree-decon.main-tooltip'}, function (player) Gui.toolbar_toggle_button("entity/tree-01", {'tree-decon.main-tooltip'}, function (player)
return Roles.player_allowed(player, "fast-tree-decon") return Roles.player_allowed(player, "fast-tree-decon")
end) end)
:on_click(function(player, element) :on_event(Gui.events.on_toolbar_button_toggled, function(player, _, event)
local status = HasEnabledDecon:get(player) HasEnabledDecon:set(player, event.state)
HasEnabledDecon:set(player, not status) player.print{'tree-decon.toggle-msg', event.state and {'tree-decon.enabled'} or {'tree-decon.disabled'}}
Gui.toolbar_button_style(element, not status)
player.print(status and {'tree-decon.toggle-msg', {'tree-decon.disabled'}} or {'tree-decon.toggle-msg', {'tree-decon.enabled'}})
end) end)

View File

@@ -106,14 +106,12 @@ follow_label =
Gui.element(function(definition, parent, target) Gui.element(function(definition, parent, target)
Gui.destroy_if_valid(parent[definition.name]) Gui.destroy_if_valid(parent[definition.name])
local label = definition:triggers_events( local label = parent.add{
parent.add{ type = 'label',
type = 'label', style = 'heading_1_label',
style = 'heading_1_label', caption = 'Following '..target.name..'.\nClick here or press esc to stop following.',
caption = 'Following '..target.name..'.\nClick here or press esc to stop following.', name = definition.name
name = definition.name }
}
)
local player = Gui.get_player_from_element(parent) local player = Gui.get_player_from_element(parent)
local res = player.display_resolution local res = player.display_resolution

View File

@@ -6,6 +6,7 @@
local Game = require 'utils.game' -- @dep utils.game local Game = require 'utils.game' -- @dep utils.game
local Gui = require 'expcore.gui' -- @dep expcore.gui local Gui = require 'expcore.gui' -- @dep expcore.gui
local Roles = require 'expcore.roles' -- @dep expcore.gui
local Global = require 'utils.global' -- @dep utils.global local Global = require 'utils.global' -- @dep utils.global
local config = require 'config.gui.autofill' -- @dep config.gui.autofill local config = require 'config.gui.autofill' -- @dep config.gui.autofill
local Event = require 'utils.event' -- @dep utils.event local Event = require 'utils.event' -- @dep utils.event
@@ -54,13 +55,14 @@ end)
--- Toggle enitity button, used for toggling autofill for the specific entity --- Toggle enitity button, used for toggling autofill for the specific entity
-- All entity autofill settings will be ignored if its disabled -- All entity autofill settings will be ignored if its disabled
-- @element entity_toggle -- @element entity_toggle
local entity_toggle = Gui.element(function(definition, parent, entity_name) local entity_toggle =
return definition:triggers_events(parent.add{ Gui.element(function(_, parent, entity_name)
return parent.add{
type = 'sprite-button', type = 'sprite-button',
sprite = 'utility/confirm_slot', sprite = 'utility/confirm_slot',
tooltip = {'autofill.toggle-entity-tooltip', rich_img('item', entity_name)}, tooltip = {'autofill.toggle-entity-tooltip', rich_img('item', entity_name)},
style = 'shortcut_bar_button_green' style = 'shortcut_bar_button_green'
}) }
end) end)
:style(Gui.sprite_style(22)) :style(Gui.sprite_style(22))
:on_click(function(player, element, _) :on_click(function(player, element, _)
@@ -112,11 +114,11 @@ Gui.element(function(definition, parent, section_name, table_size)
section_table.visible = false section_table.visible = false
return section_table return definition:no_events(section_table)
end) end)
:on_click(function(_, element, event) :on_click(function(_, element, event)
event.element = element.parent.alignment[toggle_section.name] event.element = element.parent.alignment[toggle_section.name]
toggle_section:raise_custom_event(event) toggle_section:raise_event(event)
end) end)
--- Toggle item button, used for toggling autofill for the specific item --- Toggle item button, used for toggling autofill for the specific item
@@ -294,7 +296,9 @@ end)
--- Button on the top flow used to toggle autofill container --- Button on the top flow used to toggle autofill container
-- @element autofill_toggle -- @element autofill_toggle
Gui.left_toolbar_button(config.icon, {'autofill.main-tooltip'}, autofill_container) Gui.left_toolbar_button(config.icon, {'autofill.main-tooltip'}, autofill_container, function(player)
return Roles.player_allowed(player, 'gui/autofill')
end)
--- When a player is created make sure they have the default autofill settings --- When a player is created make sure they have the default autofill settings
Event.add(defines.events.on_player_created, function(event) Event.add(defines.events.on_player_created, function(event)

View File

@@ -86,7 +86,7 @@ end)
--- Set of elements that are used to make up a row of the player table --- Set of elements that are used to make up a row of the player table
-- @element add_player_base -- @element add_player_base
local add_player_base = local add_player_base =
Gui.element(function(definition, parent, player_data) Gui.element(function(_, parent, player_data)
-- Add the button to open the action bar -- Add the button to open the action bar
local toggle_action_bar_flow = parent.add{ type = 'flow', name = player_data.name } local toggle_action_bar_flow = parent.add{ type = 'flow', name = player_data.name }
open_action_bar(toggle_action_bar_flow) open_action_bar(toggle_action_bar_flow)
@@ -100,7 +100,6 @@ Gui.element(function(definition, parent, player_data)
} }
player_name.style.padding = {0, 2,0, 0} player_name.style.padding = {0, 2,0, 0}
player_name.style.font_color = player_data.chat_color player_name.style.font_color = player_data.chat_color
definition:triggers_events(player_name)
-- Add the time played label -- Add the time played label
local alignment = Gui.alignment(parent, 'player-time-'..player_data.index) local alignment = Gui.alignment(parent, 'player-time-'..player_data.index)
@@ -112,7 +111,7 @@ Gui.element(function(definition, parent, player_data)
} }
time_label.style.padding = 0 time_label.style.padding = 0
return time_label return player_name
end) end)
:on_click(function(player, element, event) :on_click(function(player, element, event)
local selected_player_name = element.caption local selected_player_name = element.caption

View File

@@ -75,16 +75,16 @@ Gui.element{
--- Used to connect to servers in server list --- Used to connect to servers in server list
-- @element join_server -- @element join_server
local join_server = local join_server =
Gui.element(function(definition, parent, server_id, wrong_version) Gui.element(function(_, parent, server_id, wrong_version)
local status = External.get_server_status(server_id) or 'Offline' local status = External.get_server_status(server_id) or 'Offline'
if wrong_version then status = 'Version' end if wrong_version then status = 'Version' end
local flow = parent.add{ name = server_id, type = 'flow' } local flow = parent.add{ name = server_id, type = 'flow' }
local button = definition:triggers_events(flow.add{ local button = flow.add{
type = 'sprite-button', type = 'sprite-button',
sprite = 'utility/circuit_network_panel_white', --- network panel white, warning white, download white sprite = 'utility/circuit_network_panel_white', --- network panel white, warning white, download white
hovered_sprite = 'utility/circuit_network_panel_black', --- network panel black, warning black, download black hovered_sprite = 'utility/circuit_network_panel_black', --- network panel black, warning black, download black
tooltip = {'readme.servers-connect-'..status, wrong_version} tooltip = {'readme.servers-connect-'..status, wrong_version}
}) }
if status == 'Offline' or status == 'Current' then if status == 'Offline' or status == 'Current' then
button.enabled = false button.enabled = false
@@ -438,12 +438,10 @@ Gui.element(function(definition, parent)
end) end)
:static_name(Gui.unique_static_name) :static_name(Gui.unique_static_name)
:on_open(function(player) :on_open(function(player)
local toggle_button = Gui.get_top_element(player, readme_toggle) Gui.toggle_toolbar_button(player, readme_toggle, true)
Gui.toolbar_button_style(toggle_button, true)
end) end)
:on_close(function(player, element) :on_close(function(player, element)
local toggle_button = Gui.get_top_element(player, readme_toggle) Gui.toggle_toolbar_button(player, readme_toggle, false)
Gui.toolbar_button_style(toggle_button, false)
Gui.destroy_if_valid(element) Gui.destroy_if_valid(element)
end) end)

View File

@@ -467,11 +467,11 @@ Gui.element(function(definition, parent, section_name, table_size)
scroll_table.parent.visible = false scroll_table.parent.visible = false
-- Return the flow table -- Return the flow table
return scroll_table return definition:no_events(scroll_table)
end) end)
:on_click(function(_, element, event) :on_click(function(_, element, event)
event.element = element.parent.alignment[toggle_section.name] event.element = element.parent.alignment[toggle_section.name]
toggle_section:raise_custom_event(event) toggle_section:raise_event(event)
end) end)
--- Main gui container for the left flow --- Main gui container for the left flow

View File

@@ -175,24 +175,26 @@ local subfooter_actions =
local task_list_item = local task_list_item =
Gui.element( Gui.element(
function(definition, parent, task) function(definition, parent, task)
local flow = local flow = parent.add {
parent.add {
type = "flow", type = "flow",
name = "task-" .. task.task_id, name = "task-" .. task.task_id,
caption = task.task_id caption = task.task_id
} }
flow.style.horizontally_stretchable = true flow.style.horizontally_stretchable = true
local button =
flow.add { local button = flow.add {
name = definition.name, name = definition.name,
type = "button", type = "button",
style = "list_box_item", style = "list_box_item",
caption = task.title caption = task.title,
tooltip = { "task-list.last-edit", task.last_edit_name, format_time(task.last_edit_time) }
} }
definition:triggers_events(button)
button.style.horizontally_stretchable = true button.style.horizontally_stretchable = true
button.style.horizontally_squashable = true button.style.horizontally_squashable = true
return flow
return button
end end
):on_click( ):on_click(
function(player, element, _) function(player, element, _)
@@ -487,11 +489,7 @@ local repopulate_task_list = function(task_list_element)
for _, task_id in ipairs(task_ids) do for _, task_id in ipairs(task_ids) do
-- Add the task -- Add the task
local task = Tasks.get_task(task_id) local task = Tasks.get_task(task_id)
local element = task_list_item(task_list_element, task) task_list_item(task_list_element, task)
-- Set tooltip
local last_edit_name = task.last_edit_name
local last_edit_time = task.last_edit_time
element[task_list_item.name].tooltip = {"task-list.last-edit", last_edit_name, format_time(last_edit_time)}
end end
end end
@@ -560,19 +558,16 @@ local update_task = function(player, task_list_element, task_id)
return return
end end
local element local flow = task_list_element["task-" .. task_id]
-- If task does not exist yet add it to the list if not flow then
if not task_list_element["task-" .. task_id] then -- If task does not exist yet add it to the list
element = task_list_item(task_list_element, task) task_list_item(task_list_element, task)
else else
-- If the task exists update the caption -- If the task exists update the caption and tooltip
element = task_list_element["task-" .. task_id] local button = flow[task_list_item.name]
element[task_list_item.name].caption = task.title button.caption = task.title
button.tooltip = {"task-list.last-edit", task.last_edit_name, format_time(task.last_edit_time)}
end end
-- Set tooltip
local last_edit_name = task.last_edit_name
local last_edit_time = task.last_edit_time
element[task_list_item.name].tooltip = {"task-list.last-edit", last_edit_name, format_time(last_edit_time)}
end end
-- Update the footer task edit view -- Update the footer task edit view

523
modules/gui/toolbar.lua Normal file
View File

@@ -0,0 +1,523 @@
local Gui = require "expcore.gui" --- @dep expcore.gui
local PlayerData = require 'expcore.player_data' --- @dep expcore.player_data
-- Used to store the state of the toolbar when a player leaves
local ToolbarState = PlayerData.Settings:combine('ToolbarState')
ToolbarState:set_metadata{
stringify = function(value)
local buttons, favourites = 0, 0
for _, state in ipairs(value) do
buttons = buttons + 1
if state.favourite then
favourites = favourites + 1
end
end
return string.format("Buttons: %d, Favourites: %d", buttons, favourites)
end
}
-- Styles used for sprite buttons
local button_size = 20
local Styles = {
header = Gui.sprite_style(22),
item = Gui.sprite_style(button_size)
}
--- Set the style of the fake toolbar element
local function copy_style(src, dst)
dst.style = src.style.name
dst.style.height = button_size
dst.style.width = button_size
dst.style.padding = -2
end
local toolbar_container, move_up, move_down, toggle_toolbar
--- Reorder the buttons relative to each other, this will update the datastore
local function move_toolbar_button(player, item, offset)
local old_index = item.get_index_in_parent()
local new_index = old_index + offset
-- Ideally the following would all happen in on_update but this had too much latency
-- Swap the position in the list
local list = item.parent
local other_item = list.children[new_index]
list.swap_children(old_index, new_index)
-- Swap the position in the top flow, offset by 1 because of settings button
local top_flow = Gui.get_top_flow(player)
top_flow.swap_children(old_index+1, new_index+1)
-- Check if the element has a left element to move
local element_define = Gui.defines[item.tags.top_element_uid]
local other_define = Gui.defines[other_item.tags.top_element_uid]
if element_define.left_flow_element and other_define.left_flow_element then
local left_element = Gui.get_left_element(player, element_define.left_flow_element)
local other_left_element = Gui.get_left_element(player, other_define.left_flow_element)
local left_index = left_element.get_index_in_parent()
local other_index = other_left_element.get_index_in_parent()
left_element.parent.swap_children(left_index, other_index)
end
-- If we are moving in/out of first/last place we need to update the move buttons
local last_index = #list.children
if old_index == 1 then -- Moving out of index 1
other_item.move[move_up.name].enabled = false
item.move[move_up.name].enabled = true
elseif new_index == 1 then -- Moving into index 1
other_item.move[move_up.name].enabled = true
item.move[move_up.name].enabled = false
elseif old_index == last_index then -- Moving out of the last index
other_item.move[move_down.name].enabled = false
item.move[move_down.name].enabled = true
elseif new_index == last_index then -- Moving into the last index
other_item.move[move_down.name].enabled = true
item.move[move_down.name].enabled = false
end
-- Update the datastore state
ToolbarState:update(player, function(_, order)
local tmp = order[old_index]
order[old_index] = order[new_index]
order[new_index] = tmp
end)
end
--- Reorder the toolbar buttons
local function reorder_toolbar_menu(player)
local frame = Gui.get_left_element(player, toolbar_container)
local list = frame.container.scroll.list
local order = ToolbarState:get(player)
local last_index = #order
-- Reorder the buttons
for index, state in ipairs(order) do
local element_define = Gui.defines[state.element_uid]
-- Switch item order
local item = list[element_define.name]
list.swap_children(index, item.get_index_in_parent())
-- Check if the player is allowed to see the button
local allowed = element_define.authenticator
if type(allowed) == 'function' then allowed = allowed(player) end
-- Update the checkbox state and item visibility
local toolbar_button = Gui.get_top_element(player, element_define)
toolbar_button.visible = allowed and state.favourite or false
item.checkbox.state = state.favourite
-- Update the state if the move buttons
item.move[move_up.name].enabled = index ~= 1
item.move[move_down.name].enabled = index ~= last_index
end
-- Update the state of the toggle button
local button = frame.container.header.alignment[toggle_toolbar.name]
button.enabled = Gui.top_flow_has_visible_elements(player)
button.toggled = Gui.get_top_flow(player).parent.visible
end
--- Resets the toolbar to its default state when pressed
-- @element reset_toolbar
local reset_toolbar =
Gui.element {
type = "sprite-button",
sprite = "utility/reset",
style = "shortcut_bar_button_red",
tooltip = {"toolbar.reset"},
name = Gui.unique_static_name
}
:style(Gui.sprite_style(Styles.header.width, -1))
:on_click(function(player)
ToolbarState:set(player, nil)
Gui.toggle_top_flow(player, true)
reorder_toolbar_menu(player)
end)
--- Replaces the default method for opening and closing the toolbar
-- @element toggle_toolbar
toggle_toolbar =
Gui.element {
type = "sprite-button",
sprite = "utility/bookmark",
tooltip = {"toolbar.toggle"},
style = "tool_button",
auto_toggle = true,
name = Gui.unique_static_name
}
:style(Styles.header)
:on_click(function(player, element)
Gui.toggle_top_flow(player, element.toggled)
end)
--- Move an element up the list
-- @element move_up
move_up =
Gui.element {
type = "sprite-button",
sprite = "utility/speed_up",
tooltip = {"toolbar.move-up"},
name = Gui.unique_static_name
}
:style(Styles.item)
:on_click(function(player, element)
local item = element.parent.parent
move_toolbar_button(player, item, -1)
end)
--- Move an element down the list
-- @element move_down
move_down =
Gui.element {
type = "sprite-button",
sprite = "utility/speed_down",
tooltip = {"toolbar.move-down"},
name = Gui.unique_static_name
}
:style(Styles.item)
:on_click(function(player, element)
local item = element.parent.parent
move_toolbar_button(player, item, 1)
end)
--- A flow which represents one item in the toolbar list
-- @element toolbar_list_item
local toolbar_list_item =
Gui.element(function(definition, parent, element_define)
local flow = parent.add {
type = "frame",
style = "shortcut_selection_row",
name = element_define.name,
tags = {
top_element_uid = element_define.uid
}
}
flow.style.horizontally_stretchable = true
flow.style.vertical_align = "center"
-- Add the button and the icon edit button
local element = element_define(flow)
local player = Gui.get_player_from_element(parent)
local top_element = Gui.get_top_element(player, element_define)
copy_style(top_element, element)
-- Add the checkbox that can toggle the visibility
local checkbox = flow.add{
type = "checkbox",
name = "checkbox",
caption = element_define.tooltip or element_define.caption or "None",
state = top_element.visible or false,
tags = {
top_element_name = element_define.name
}
}
definition:triggers_events(checkbox)
checkbox.style.width = 180
-- Add the buttons used to move the flow up and down
local move_flow = flow.add{ type = "flow", name = "move" }
move_flow.style.horizontal_spacing = 0
move_up(move_flow)
move_down(move_flow)
return definition:no_events(flow)
end)
:on_checked_changed(function(player, element)
local top_flow = Gui.get_top_flow(player)
local top_element = top_flow[element.tags.top_element_name]
local had_visible = Gui.top_flow_has_visible_elements(player)
top_element.visible = element.state
-- Check if we are on the edge case between 0 and 1 visible elements
if element.state and not had_visible then
Gui.toggle_top_flow(player, true)
local container = element.parent.parent.parent.parent
local button = container.header.alignment[toggle_toolbar.name]
button.toggled = true
button.enabled = true
elseif not element.state and not Gui.top_flow_has_visible_elements(player) then
Gui.toggle_top_flow(player, false)
local container = element.parent.parent.parent.parent
local button = container.header.alignment[toggle_toolbar.name]
button.toggled = false
button.enabled = false
end
-- Update the datastore state
ToolbarState:update(player, function(_, order)
local index = element.parent.get_index_in_parent()
order[index].favourite = element.state
end)
end)
--- Scrollable list of all toolbar buttons
-- @element toolbar_list
local toolbar_list =
Gui.element(function(_, parent)
-- This is a scroll pane for the list
local scroll_pane = parent.add {
name = "scroll",
type = "scroll-pane",
direction = "vertical",
horizontal_scroll_policy = "never",
vertical_scroll_policy = "auto",
style = "scroll_pane_under_subheader"
}
scroll_pane.style.horizontally_stretchable = true
scroll_pane.style.padding = 0
scroll_pane.style.maximal_height = 224
-- This flow is the list, we need a linear list because of get_index_in_parent
local flow = scroll_pane.add {
name = "list",
type = "flow",
direction = "vertical"
}
flow.style.vertical_spacing = 0
flow.style.horizontally_stretchable = true
return flow
end)
--- Main toolbar container for the left flow
-- @element toolbar_container
toolbar_container =
Gui.element(function(definition, parent)
-- Draw the internal container
local container = Gui.container(parent, definition.name, 268)
container.style.maximal_width = 268
container.style.minimal_width = 268
-- Draw the header
local player = Gui.get_player_from_element(parent)
local header = Gui.header(container, {"toolbar.main-caption"}, {"toolbar.main-tooltip"}, true)
-- Draw the toolbar control buttons
local toggle_element = toggle_toolbar(header)
toggle_element.toggled = Gui.get_top_flow(player).visible
reset_toolbar(header)
-- Draw toolbar list element
local list_element = toolbar_list(container)
local flow_order = Gui.get_top_flow_order(player)
for _, element_define in ipairs(flow_order) do
-- Ensure the element exists
local element = list_element[element_define.name]
if not element then
element = toolbar_list_item(list_element, element_define)
end
-- Set the visible state
local allowed = element_define.authenticator
if type(allowed) == 'function' then allowed = allowed(player) end
element.visible = allowed or false
end
-- Set the state of the move buttons for the first and last element
local children = list_element.children
children[1].move[move_up.name].enabled = false
children[#children].move[move_down.name].enabled = false
-- Return the external container
return container.parent
end)
:static_name(Gui.unique_static_name)
:add_to_left_flow(false)
--- Set the default value for the datastore
local datastore_id_map = {}
local toolbar_default_state = {}
ToolbarState:set_default(toolbar_default_state)
--- Get the datastore id for this element define, to best of ability it should be unique between versions
local function to_datastore_id(element_define)
-- First try to use the tooltip locale string
local tooltip = element_define.tooltip
if type(tooltip) == "table" then
return tooltip[1]:gsub("%.(.+)", "")
end
-- Then try to use the caption or sprite
return element_define.caption or element_define.sprite
end
--- For all top element, register an on click which will copy their style
for index, element_define in ipairs(Gui.top_elements) do
-- This is a bit hacky, the gui system cant have multiple handlers registered
local prev_handler = element_define[Gui.events.on_toolbar_button_toggled]
-- Add the handler for when the button is toggled
element_define:on_event(Gui.events.on_toolbar_button_toggled, function(player, element, event)
if prev_handler then prev_handler(player, element, event) end -- Kind of hacky but works
local frame = Gui.get_left_element(player, toolbar_container)
if not frame then return end -- Gui might not be loaded yet
local button = frame.container.scroll.list[element_define.name][element_define.name]
local toolbar_button = Gui.get_top_element(player, element_define)
copy_style(toolbar_button, button)
end)
-- Insert the element into the id map
local id = to_datastore_id(element_define)
if datastore_id_map[id] then
error(string.format("All toolbar elements need a unique id to be saved correctly, %d (%s) and %d (%s) share the id %s",
datastore_id_map[id].uid, datastore_id_map[id].defined_at, element_define.uid, element_define.defined_at, id
))
end
datastore_id_map[id] = element_define
-- Add the element to the default state
table.insert(toolbar_default_state, {
element_uid = element_define.uid,
favourite = true,
})
end
--- Get the top order based on the players settings
Gui.inject_top_flow_order(function(player)
local order = ToolbarState:get(player)
local elements = {}
for index, state in ipairs(order) do
elements[index] = Gui.defines[state.element_uid]
end
return elements
end)
--- Get the left order based on the player settings, with toolbar menu first, and all remaining after
Gui.inject_left_flow_order(function(player)
local order = Gui.get_top_flow_order(player)
local elements, element_map = { toolbar_container }, { [toolbar_container] = true }
-- Add the flows that have a top element
for _, element_define in ipairs(order) do
if element_define.left_flow_element then
table.insert(elements, element_define.left_flow_element)
element_map[element_define.left_flow_element] = true
end
end
-- Add the flows that dont have a top element
for _, element_define in ipairs(Gui.left_elements) do
if not element_map[element_define] then
table.insert(elements, element_define)
end
end
return elements
end)
--- Overwrite the default toggle behaviour and instead toggle this menu
Gui.core_defines.hide_top_flow:on_click(function(player, _, _)
Gui.toggle_left_element(player, toolbar_container)
end)
--- Overwrite the default toggle behaviour and instead toggle this menu
Gui.core_defines.show_top_flow:on_click(function(player, _, _)
Gui.toggle_left_element(player, toolbar_container)
end)
--- Overwrite the default update top flow
local _update_top_flow = Gui.update_top_flow
function Gui.update_top_flow(player)
_update_top_flow(player) -- Call the original
local order = ToolbarState:get(player)
for index, state in ipairs(order) do
local element_define = Gui.defines[state.element_uid]
local top_element = Gui.get_top_element(player, element_define)
top_element.visible = top_element.visible and state.favourite or false
end
end
--- Uncompress the data to be more useable
ToolbarState:on_load(function(player_name, value)
-- If there is no value, do nothing
if value == nil then return end
-- Create a hash map of the favourites
local favourites = {}
for _, id in ipairs(value[2]) do
favourites[id] = true
end
-- Read the order from the value
local elements = {}
local element_hash = {}
for index, id in ipairs(value[1]) do
local element = datastore_id_map[id]
if element and not element_hash[element.uid] then
element_hash[element.uid] = true
elements[index] = {
element_uid = element.uid,
favourite = favourites[id] or false,
}
end
end
-- Add any in the default state that are missing
for _, state in ipairs(toolbar_default_state) do
if not element_hash[state.element_uid] then
table.insert(elements, table.deep_copy(state))
end
end
-- Create a hash map of the open left flows
local left_flows = {}
for _, id in ipairs(value[3]) do
local element = datastore_id_map[id]
if element.left_flow_element then
left_flows[element.left_flow_element] = true
end
end
-- Set the visible state of all left flows
local player = game.get_player(player_name)
for _, left_element in ipairs(Gui.left_elements) do
Gui.toggle_left_element(player, left_element, left_flows[left_element] or false)
end
-- Set the toolbar visible state
Gui.toggle_top_flow(player, value[4])
-- Set the data now and update now, ideally this would be on_update but that had too large of a latency
ToolbarState:raw_set(player_name, elements)
Gui.reorder_top_flow(player)
Gui.reorder_left_flow(player)
reorder_toolbar_menu(player)
return elements
end)
--- Save the current state of the players toolbar menu
ToolbarState:on_save(function(player_name, value)
if value == nil then return nil end -- Don't save default
local order, favourites, left_flows = {}, {}, {}
local player = game.get_player(player_name)
local top_flow_open = Gui.get_top_flow(player).parent.visible
for index, state in ipairs(value) do
-- Add the element to the order array
local element_define = Gui.defines[state.element_uid]
local id = to_datastore_id(element_define)
order[index] = id
-- If its a favourite then insert it
if state.favourite then
table.insert(favourites, id)
end
-- If it has a left flow and its open then insert it
if element_define.left_flow_element then
local left_element = Gui.get_left_element(player, element_define.left_flow_element)
if left_element.visible then
table.insert(left_flows, id)
end
end
end
return { order, favourites, left_flows, top_flow_open }
end)

View File

@@ -159,13 +159,13 @@ Gui.element(function(definition, parent, warp)
end end
-- Draw the element -- Draw the element
return definition:triggers_events(parent.add{ return parent.add{
type = 'sprite-button', type = 'sprite-button',
sprite = sprite, sprite = sprite,
name = definition.name, name = definition.name,
tooltip = {'warp-list.goto-tooltip', warp_position.x, warp_position.y}, tooltip = {'warp-list.goto-tooltip', warp_position.x, warp_position.y},
style = 'slot_button' style = 'slot_button'
}) }
end) end)
:style(Styles.sprite32) :style(Styles.sprite32)
:static_name(Gui.unique_static_name) :static_name(Gui.unique_static_name)
@@ -186,13 +186,13 @@ end)
-- @element warp_icon_editing -- @element warp_icon_editing
local warp_icon_editing = local warp_icon_editing =
Gui.element(function(definition, parent, warp) Gui.element(function(definition, parent, warp)
return definition:triggers_events(parent.add{ return parent.add{
name = definition.name, name = definition.name,
type = 'choose-elem-button', type = 'choose-elem-button',
elem_type = 'signal', elem_type = 'signal',
signal = {type = warp.icon.type, name = warp.icon.name}, signal = {type = warp.icon.type, name = warp.icon.name},
tooltip = {'warp-list.goto-edit'} tooltip = {'warp-list.goto-edit'}
}) }
end) end)
:static_name(Gui.unique_static_name) :static_name(Gui.unique_static_name)
:style(Styles.sprite32) :style(Styles.sprite32)
@@ -204,12 +204,12 @@ Gui.element(function(definition, parent, warp)
local last_edit_name = warp.last_edit_name local last_edit_name = warp.last_edit_name
local last_edit_time = warp.last_edit_time local last_edit_time = warp.last_edit_time
-- Draw the element -- Draw the element
return definition:triggers_events(parent.add{ return parent.add{
type = 'label', type = 'label',
caption = warp.name, caption = warp.name,
tooltip = {'warp-list.last-edit', last_edit_name, format_time(last_edit_time)}, tooltip = {'warp-list.last-edit', last_edit_name, format_time(last_edit_time)},
name = definition.name name = definition.name
}) }
end) end)
:style{ :style{
single_line = true, single_line = true,
@@ -245,12 +245,12 @@ Gui.element{
local warp_textfield = local warp_textfield =
Gui.element(function(definition, parent, warp) Gui.element(function(definition, parent, warp)
-- Draw the element -- Draw the element
return definition:triggers_events(parent.add{ return parent.add{
type = 'textfield', type = 'textfield',
text = warp.name, text = warp.name,
clear_and_focus_on_right_click = true, clear_and_focus_on_right_click = true,
name = definition.name name = definition.name
}) }
end) end)
:style{ :style{
-- Required fields to make it squashable and strechable. -- Required fields to make it squashable and strechable.
@@ -697,10 +697,10 @@ end)
--- Button on the top flow used to toggle the warp list container --- Button on the top flow used to toggle the warp list container
-- @element toggle_warp_list -- @element toggle_warp_list
Gui.left_toolbar_button(config.default_icon.type ..'/'..config.default_icon.name, {'warp-list.main-tooltip', config.standard_proximity_radius}, warp_list_container, function(player) Gui.left_toolbar_button(config.default_icon.type ..'/'..config.default_icon.name, {'warp-list.main-tooltip'}, warp_list_container, function(player)
return Roles.player_allowed(player, 'gui/warp-list') return Roles.player_allowed(player, 'gui/warp-list')
end) end)
:on_custom_event(Gui.events.on_visibility_changed_by_click, function(player, _,event) :on_event(Gui.events.on_visibility_changed_by_click, function(player, _,event)
-- Set gui keep open state for player that clicked the button: true if visible, false if invisible -- Set gui keep open state for player that clicked the button: true if visible, false if invisible
keep_gui_open[player.name] = event.state keep_gui_open[player.name] = event.state
end) end)