From 1c8a97a33937ff273e96f1c474856ef89b25f166 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Tue, 16 Jan 2024 00:01:00 +0000 Subject: [PATCH] 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 --- config/_file_loader.lua | 3 +- config/expcore/roles.lua | 1 + expcore/datastore.lua | 11 +- expcore/gui/_require.lua | 2 +- expcore/gui/core_defines.lua | 4 +- expcore/gui/left_flow.lua | 115 +++++--- expcore/gui/prototype.lua | 89 ++++-- expcore/gui/top_flow.lua | 197 +++++++++++-- locale/en/data.cfg | 3 + locale/en/gui.cfg | 10 +- modules/addons/tree-decon.lua | 10 +- modules/control/spectate.lua | 14 +- modules/gui/autofill.lua | 16 +- modules/gui/player-list.lua | 5 +- modules/gui/readme.lua | 12 +- modules/gui/rocket-info.lua | 4 +- modules/gui/task-list.lua | 41 ++- modules/gui/toolbar.lua | 523 ++++++++++++++++++++++++++++++++++ modules/gui/warp-list.lua | 20 +- 19 files changed, 912 insertions(+), 168 deletions(-) create mode 100644 modules/gui/toolbar.lua diff --git a/config/_file_loader.lua b/config/_file_loader.lua index aa5d5ea6..d6234811 100644 --- a/config/_file_loader.lua +++ b/config/_file_loader.lua @@ -6,7 +6,7 @@ return { --'example.file_not_loaded', 'modules.factorio-control', -- base factorio free play scenario - 'expcore.player_data', + 'expcore.player_data', -- must be loaded first to register event handlers --- Game Commands 'modules.commands.debug', @@ -97,6 +97,7 @@ return { 'modules.gui.playerdata', 'modules.gui.surveillance', 'modules.graftorio.require', -- graftorio + 'modules.gui.toolbar', -- must be loaded last to register toolbar handlers --- Config Files 'config.expcore.command_auth_admin', -- commands tagged with admin_only are blocked for non admins diff --git a/config/expcore/roles.lua b/config/expcore/roles.lua index 6afe6299..ed4f769a 100644 --- a/config/expcore/roles.lua +++ b/config/expcore/roles.lua @@ -288,6 +288,7 @@ local default = Roles.new_role('Guest','') 'gui/readme', 'gui/vlayer', 'gui/research', + 'gui/autofill', 'gui/module' } diff --git a/expcore/datastore.lua b/expcore/datastore.lua index 241699cd..fcf88afd 100644 --- a/expcore/datastore.lua +++ b/expcore/datastore.lua @@ -522,7 +522,7 @@ function Datastore:increment(key, delta) return self:set(key, value + (delta or 1)) 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 @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 @@ -536,13 +536,16 @@ end) ]] function Datastore:update(key, callback) 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 success, new_value = xpcall(callback, update_error, key, value) if not success then self:raw_set(key, old_value) elseif new_value ~= nil then self:set(key, new_value) + elseif raw_value == nil then + self:set(key, value) else self:raise_event('on_update', key, value, old_value) 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 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 @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 @@ -744,7 +747,7 @@ end ----- 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 @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 diff --git a/expcore/gui/_require.lua b/expcore/gui/_require.lua index 318ab37b..5f795413 100644 --- a/expcore/gui/_require.lua +++ b/expcore/gui/_require.lua @@ -122,10 +122,10 @@ end) ]] local Gui = require 'expcore.gui.prototype' +require 'expcore.gui.helper_functions' require 'expcore.gui.core_defines' require 'expcore.gui.top_flow' require 'expcore.gui.left_flow' -require 'expcore.gui.helper_functions' require 'expcore.gui.defines' local Roles = _C.opt_require('expcore.roles') diff --git a/expcore/gui/core_defines.lua b/expcore/gui/core_defines.lua index 272adab7..ff39a20f 100644 --- a/expcore/gui/core_defines.lua +++ b/expcore/gui/core_defines.lua @@ -25,7 +25,7 @@ Gui.element{ height = 36 } :on_click(function(player, _,_) - Gui.toggle_top_flow(player) + Gui.toggle_top_flow(player, false) end) Gui.core_defines.hide_top_flow = hide_top_flow @@ -45,7 +45,7 @@ Gui.element{ height = 20 } :on_click(function(player, _,_) - Gui.toggle_top_flow(player) + Gui.toggle_top_flow(player, true) end) Gui.core_defines.show_top_flow = show_top_flow diff --git a/expcore/gui/left_flow.lua b/expcore/gui/left_flow.lua index 813b254f..6e601496 100644 --- a/expcore/gui/left_flow.lua +++ b/expcore/gui/left_flow.lua @@ -38,8 +38,10 @@ example_flow_with_button:add_to_left_flow(true) ]] 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 - 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 end @@ -60,26 +62,38 @@ function Gui.left_toolbar_button(sprite, tooltip, element_define, authenticator) local button = Gui.toolbar_button(sprite, tooltip, authenticator) -- Add on_click handler to handle click events comming from the 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) - + button:on_click(function(player, _, _) -- 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 - button:raise_custom_event{ + button:raise_event{ name = Gui.events.on_visibility_changed_by_click, - element = element, - state = visibility_state + element = Gui.get_top_element(player, button), + state = Gui.toggle_left_element(player, element_define) } end) -- 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 - element_define.toolbar_button = button.name + element_define.toolbar_button = button + button.left_flow_element = element_define return button end +Gui._left_flow_order_src = "" +--- 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 "" + 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 @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 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 local draw_success, left_element = xpcall(function() return element_define(left_flow) end, debug.traceback) 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 -- 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 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 - 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 visible = err end @@ -116,23 +141,35 @@ function Gui.draw_left_flow(player) left_element.visible = 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 if element_define.toolbar_button then - -- Check if the topflow contains the button - local button = top_flow[element_define.toolbar_button] - if button then - -- Style the button - Gui.toolbar_button_style(button, visible) - end + Gui.toggle_toolbar_button(player, element_define.toolbar_button, visible) end + ::continue:: end hide_button.visible = show_hide_button 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 @tparam LuaPlayer player the player to update the left flow for @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) local left_flow = Gui.get_left_flow(player) 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] if left_element.visible then hide_button.visible = true @@ -169,20 +206,18 @@ function Gui.hide_left_flow(player) -- Set the visible state of all elements in the flow 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 -- Check if the the element has a toobar button attached if element_define.toolbar_button then -- 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 -- Style the button - Gui.toolbar_button_style(button, false) - -- Get the button define from the reverse lookup on the element - local button_define = Gui.defines[element_define.toolbar_button] + Gui.toggle_toolbar_button(player, element_define.toolbar_button, false) -- 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, element = button, state = false @@ -192,6 +227,12 @@ function Gui.hide_left_flow(player) 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 @tparam LuaPlayer player the player that you want to get the element for @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) 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 --[[-- 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) - local left_flow = Gui.get_left_flow(player) - local top_flow = Gui.get_top_flow(player) - -- 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 element.visible = state Gui.update_left_flow(player) -- Check if the the element has a button attached if element_define.toolbar_button then - -- Check if the topflow contains the button - local button = top_flow[element_define.toolbar_button] - if button then - -- Style the button - Gui.toolbar_button_style(button, state) - end + Gui.toggle_toolbar_button(player, element_define.toolbar_button, state) end return state end \ No newline at end of file diff --git a/expcore/gui/prototype.lua b/expcore/gui/prototype.lua index d63591f4..b2758e07 100644 --- a/expcore/gui/prototype.lua +++ b/expcore/gui/prototype.lua @@ -23,20 +23,46 @@ local Gui = { --- The prototype used to store the functions of an element define _prototype_element = {}, --- The prototype metatable applied to new element defines - _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 - } + _mt_element = {} } +--- Allow access to the element prototype methods 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 "" + return file_name..":"..func_name +end + --- Element Define. -- @section elementDefine @@ -97,20 +123,21 @@ function Gui.element(element_define) if element_define.name == Gui.unique_static_name then element_define.name = "ExpGui_"..tostring(uid) 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) return parent.add(element_define) end else - Gui.debug_info[uid].draw = 'Function' + Gui.debug_info[uid].draw = get_defined_at(element_define) element._draw = element_define end -- Add the define to the base module - 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 "" - element.defined_at = file_name..":"..func_name + element.defined_at = get_defined_at(3) Gui.file_paths[uid] = element.defined_at Gui.defines[uid] = element @@ -154,6 +181,7 @@ end) ]] function Gui._prototype_element:style(style_define) + _C.error_if_runtime() -- Add the definition function if type(style_define) == 'table' then Gui.debug_info[self.uid].style = style_define @@ -163,7 +191,7 @@ function Gui._prototype_element:style(style_define) end end else - Gui.debug_info[self.uid].style = 'Function' + Gui.debug_info[self.uid].style = get_defined_at(style_define) self._style = style_define end @@ -176,6 +204,7 @@ end @treturn table the element define is returned to allow for event handlers to be registered ]] function Gui._prototype_element:static_name(name) + _C.error_if_runtime() if name == Gui.unique_static_name then self.name = "ExpGui_"..tostring(self.uid) else @@ -185,16 +214,20 @@ function Gui._prototype_element:static_name(name) 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 +-- 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 @treturn LuaGuiElement The element passed as the argument to allow for cleaner returns ]] function Gui._prototype_element:triggers_events(element) + if not self._has_events then return element end local tags = element.tags if not tags then element.tags = { ExpGui_event_triggers = { self.uid } } return element elseif not tags.ExpGui_event_triggers then 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 table.insert(tags.ExpGui_event_triggers, self.uid) end @@ -203,21 +236,28 @@ function Gui._prototype_element:triggers_events(element) return element 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 @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 @treturn table the element define so more handleres can be registered @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) 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) Gui.events[event_name] = event_name self[event_name] = handler + self._has_events = true return self end @@ -226,13 +266,13 @@ end @treturn table the element define so more events can be raised @usage Raising a custom event -element_define:raise_custom_event{ +element_define:raise_event{ name = 'my_custom_event', element = element } ]] -function Gui._prototype_element:raise_custom_event(event) +function Gui._prototype_element:raise_event(event) -- Check the element is valid local element = event.element 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 local element_define = Gui.defines[uid] if element_define then - element_define:raise_custom_event(event) + element_define:raise_event(event) end end end) + Gui.events[event_name] = event_name return function(self, handler) - table.insert(Gui.debug_info[self.uid].events, debug.getinfo(1, "n").name) - self[event_name] = handler - return self + return self:on_event(event_name, handler) end end diff --git a/expcore/gui/top_flow.lua b/expcore/gui/top_flow.lua index 8e38fa8f..6e860899 100644 --- a/expcore/gui/top_flow.lua +++ b/expcore/gui/top_flow.lua @@ -6,12 +6,16 @@ local Gui = require 'expcore.gui.prototype' 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 show_top_flow = Gui.core_defines.show_top_flow.name --- Top Flow. -- @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 -- @table top_elements Gui.top_elements = {} @@ -21,8 +25,30 @@ Gui.top_elements = {} 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 --- @field Gui.top_flow_button_visible_style -Gui.top_flow_button_visible_style = 'menu_button_continue' +-- @field Gui.top_flow_button_toggled_style +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 @function Gui.get_top_flow(player) @@ -48,11 +74,43 @@ end) ]] 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 - Gui.top_elements[self] = authenticator or true + self.authenticator = authenticator or true + table.insert(Gui.top_elements, self) return self 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 = "" +--- 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 "" + 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 @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) 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 - for element_define, authenticator in pairs(Gui.top_elements) do + for index, element_define in ipairs(flow_order) do -- Ensure the element exists local element = top_flow[element_define.name] if not element then element = element_define(top_flow) + else + top_flow.swap_children(index+1, element.get_index_in_parent()) end -- Set the visible state - local allowed = authenticator + local allowed = element_define.authenticator 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 @@ -119,7 +218,33 @@ local button = Gui.get_top_element(game.player, example_button) ]] function Gui.get_top_element(player, element_define) 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 --[[-- 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 } :style{ - minimal_width = 36, - height = 36, + minimal_width = toolbar_button_size, + height = toolbar_button_size, padding = -2 } :add_to_top_flow(authenticator) end ---[[-- 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 +--[[-- Creates a toggle button on the top flow with consistent styling +@tparam string sprite the sprite that you want to use on the button +@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 -Gui.toolbar_button_style(button, true) - -@usage-- Sets the button to the hidden style -Gui.toolbar_button_style(button, false) +@usage-- Add a button to the toolbar +local toolbar_button = +Gui.toolbar_toggle_button('entity/inserter', 'Nothing to see here', function(player) + return player.admin +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) - if state then - button.style = Gui.top_flow_button_visible_style - else - button.style = Gui.top_flow_button_style - end - button.style.minimal_width = 36 - button.style.height = 36 - button.style.padding = -2 +function Gui.toolbar_toggle_button(sprite, tooltip, authenticator) + local button = + Gui.element{ + type = 'sprite-button', + sprite = sprite, + tooltip = tooltip, + style = Gui.top_flow_button_style, + name = Gui.unique_static_name + } + :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 \ No newline at end of file diff --git a/locale/en/data.cfg b/locale/en/data.cfg index 8881c5c9..9d673acc 100644 --- a/locale/en/data.cfg +++ b/locale/en/data.cfg @@ -37,6 +37,9 @@ Bonus=Player Bonus Bonus-tooltip=The bonus given to your character Bonus-value-tooltip=Change by using /bonus 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] MapsPlayed=Maps Played diff --git a/locale/en/gui.cfg b/locale/en/gui.cfg index 88107e88..d1c05c69 100644 --- a/locale/en/gui.cfg +++ b/locale/en/gui.cfg @@ -101,7 +101,7 @@ inserted=Inserted __1__ __2__ into __3__ [warp-list] 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-current= - __1__ This is your current warp point; sub-tooltip-connected= - __1__ You can travel to this warp point; @@ -214,3 +214,11 @@ main-tooltip=Enable Vlayer GUI [module] 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 \ No newline at end of file diff --git a/modules/addons/tree-decon.lua b/modules/addons/tree-decon.lua index 6e7934d1..c33f1cdd 100644 --- a/modules/addons/tree-decon.lua +++ b/modules/addons/tree-decon.lua @@ -30,14 +30,12 @@ end local HasEnabledDecon = PlayerData.Settings:combine('HasEnabledDecon') 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") end) -:on_click(function(player, element) - local status = HasEnabledDecon:get(player) - HasEnabledDecon:set(player, not status) - 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'}}) +:on_event(Gui.events.on_toolbar_button_toggled, function(player, _, event) + HasEnabledDecon:set(player, event.state) + player.print{'tree-decon.toggle-msg', event.state and {'tree-decon.enabled'} or {'tree-decon.disabled'}} end) diff --git a/modules/control/spectate.lua b/modules/control/spectate.lua index 8a235e2e..bdafa03e 100644 --- a/modules/control/spectate.lua +++ b/modules/control/spectate.lua @@ -106,14 +106,12 @@ follow_label = Gui.element(function(definition, parent, target) Gui.destroy_if_valid(parent[definition.name]) - local label = definition:triggers_events( - parent.add{ - type = 'label', - style = 'heading_1_label', - caption = 'Following '..target.name..'.\nClick here or press esc to stop following.', - name = definition.name - } - ) + local label = parent.add{ + type = 'label', + style = 'heading_1_label', + caption = 'Following '..target.name..'.\nClick here or press esc to stop following.', + name = definition.name + } local player = Gui.get_player_from_element(parent) local res = player.display_resolution diff --git a/modules/gui/autofill.lua b/modules/gui/autofill.lua index 495e988d..7fd09ea8 100644 --- a/modules/gui/autofill.lua +++ b/modules/gui/autofill.lua @@ -6,6 +6,7 @@ local Game = require 'utils.game' -- @dep utils.game 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 config = require 'config.gui.autofill' -- @dep config.gui.autofill local Event = require 'utils.event' -- @dep utils.event @@ -54,13 +55,14 @@ end) --- Toggle enitity button, used for toggling autofill for the specific entity -- All entity autofill settings will be ignored if its disabled -- @element entity_toggle -local entity_toggle = Gui.element(function(definition, parent, entity_name) - return definition:triggers_events(parent.add{ +local entity_toggle = +Gui.element(function(_, parent, entity_name) + return parent.add{ type = 'sprite-button', sprite = 'utility/confirm_slot', tooltip = {'autofill.toggle-entity-tooltip', rich_img('item', entity_name)}, style = 'shortcut_bar_button_green' - }) + } end) :style(Gui.sprite_style(22)) :on_click(function(player, element, _) @@ -112,11 +114,11 @@ Gui.element(function(definition, parent, section_name, table_size) section_table.visible = false - return section_table + return definition:no_events(section_table) end) :on_click(function(_, element, event) event.element = element.parent.alignment[toggle_section.name] - toggle_section:raise_custom_event(event) + toggle_section:raise_event(event) end) --- 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 -- @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 Event.add(defines.events.on_player_created, function(event) diff --git a/modules/gui/player-list.lua b/modules/gui/player-list.lua index e04d2375..99a70cb7 100644 --- a/modules/gui/player-list.lua +++ b/modules/gui/player-list.lua @@ -86,7 +86,7 @@ end) --- Set of elements that are used to make up a row of the player table -- @element 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 local toggle_action_bar_flow = parent.add{ type = 'flow', name = player_data.name } 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.font_color = player_data.chat_color - definition:triggers_events(player_name) -- Add the time played label 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 - return time_label + return player_name end) :on_click(function(player, element, event) local selected_player_name = element.caption diff --git a/modules/gui/readme.lua b/modules/gui/readme.lua index af264d47..3aa1eb58 100644 --- a/modules/gui/readme.lua +++ b/modules/gui/readme.lua @@ -75,16 +75,16 @@ Gui.element{ --- Used to connect to servers in server list -- @element 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' if wrong_version then status = 'Version' end local flow = parent.add{ name = server_id, type = 'flow' } - local button = definition:triggers_events(flow.add{ + local button = flow.add{ type = 'sprite-button', 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 tooltip = {'readme.servers-connect-'..status, wrong_version} - }) + } if status == 'Offline' or status == 'Current' then button.enabled = false @@ -438,12 +438,10 @@ Gui.element(function(definition, parent) end) :static_name(Gui.unique_static_name) :on_open(function(player) - local toggle_button = Gui.get_top_element(player, readme_toggle) - Gui.toolbar_button_style(toggle_button, true) + Gui.toggle_toolbar_button(player, readme_toggle, true) end) :on_close(function(player, element) - local toggle_button = Gui.get_top_element(player, readme_toggle) - Gui.toolbar_button_style(toggle_button, false) + Gui.toggle_toolbar_button(player, readme_toggle, false) Gui.destroy_if_valid(element) end) diff --git a/modules/gui/rocket-info.lua b/modules/gui/rocket-info.lua index e0a38ceb..3b9d7b88 100644 --- a/modules/gui/rocket-info.lua +++ b/modules/gui/rocket-info.lua @@ -467,11 +467,11 @@ Gui.element(function(definition, parent, section_name, table_size) scroll_table.parent.visible = false -- Return the flow table - return scroll_table + return definition:no_events(scroll_table) end) :on_click(function(_, element, event) event.element = element.parent.alignment[toggle_section.name] - toggle_section:raise_custom_event(event) + toggle_section:raise_event(event) end) --- Main gui container for the left flow diff --git a/modules/gui/task-list.lua b/modules/gui/task-list.lua index e985ce75..f947bdb4 100644 --- a/modules/gui/task-list.lua +++ b/modules/gui/task-list.lua @@ -175,24 +175,26 @@ local subfooter_actions = local task_list_item = Gui.element( function(definition, parent, task) - local flow = - parent.add { + local flow = parent.add { type = "flow", name = "task-" .. task.task_id, caption = task.task_id } + flow.style.horizontally_stretchable = true - local button = - flow.add { + + local button = flow.add { name = definition.name, type = "button", 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_squashable = true - return flow + + return button end ):on_click( function(player, element, _) @@ -487,11 +489,7 @@ local repopulate_task_list = function(task_list_element) for _, task_id in ipairs(task_ids) do -- Add the task local task = Tasks.get_task(task_id) - local element = 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)} + task_list_item(task_list_element, task) end end @@ -560,19 +558,16 @@ local update_task = function(player, task_list_element, task_id) return end - local element - -- If task does not exist yet add it to the list - if not task_list_element["task-" .. task_id] then - element = task_list_item(task_list_element, task) + local flow = task_list_element["task-" .. task_id] + if not flow then + -- If task does not exist yet add it to the list + task_list_item(task_list_element, task) else - -- If the task exists update the caption - element = task_list_element["task-" .. task_id] - element[task_list_item.name].caption = task.title + -- If the task exists update the caption and tooltip + local button = flow[task_list_item.name] + button.caption = task.title + button.tooltip = {"task-list.last-edit", task.last_edit_name, format_time(task.last_edit_time)} 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 -- Update the footer task edit view diff --git a/modules/gui/toolbar.lua b/modules/gui/toolbar.lua new file mode 100644 index 00000000..cf4d42a3 --- /dev/null +++ b/modules/gui/toolbar.lua @@ -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) \ No newline at end of file diff --git a/modules/gui/warp-list.lua b/modules/gui/warp-list.lua index 2c3383db..4e489a83 100644 --- a/modules/gui/warp-list.lua +++ b/modules/gui/warp-list.lua @@ -159,13 +159,13 @@ Gui.element(function(definition, parent, warp) end -- Draw the element - return definition:triggers_events(parent.add{ + return parent.add{ type = 'sprite-button', sprite = sprite, name = definition.name, tooltip = {'warp-list.goto-tooltip', warp_position.x, warp_position.y}, style = 'slot_button' - }) + } end) :style(Styles.sprite32) :static_name(Gui.unique_static_name) @@ -186,13 +186,13 @@ end) -- @element warp_icon_editing local warp_icon_editing = Gui.element(function(definition, parent, warp) - return definition:triggers_events(parent.add{ + return parent.add{ name = definition.name, type = 'choose-elem-button', elem_type = 'signal', signal = {type = warp.icon.type, name = warp.icon.name}, tooltip = {'warp-list.goto-edit'} - }) + } end) :static_name(Gui.unique_static_name) :style(Styles.sprite32) @@ -204,12 +204,12 @@ Gui.element(function(definition, parent, warp) local last_edit_name = warp.last_edit_name local last_edit_time = warp.last_edit_time -- Draw the element - return definition:triggers_events(parent.add{ + return parent.add{ type = 'label', caption = warp.name, tooltip = {'warp-list.last-edit', last_edit_name, format_time(last_edit_time)}, name = definition.name - }) + } end) :style{ single_line = true, @@ -245,12 +245,12 @@ Gui.element{ local warp_textfield = Gui.element(function(definition, parent, warp) -- Draw the element - return definition:triggers_events(parent.add{ + return parent.add{ type = 'textfield', text = warp.name, clear_and_focus_on_right_click = true, name = definition.name - }) + } end) :style{ -- 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 -- @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') 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 keep_gui_open[player.name] = event.state end)