038931fcfb
Match the layout convention used by every other multi-addon-shape fork in Exiles/ (Bagnon/, Kui_Nameplates/, ShadowedUnitFrames/, etc.) — the addon's own files live in a subfolder named after the addon, with only the repo-level README files at the root. All moves are pure git renames (history preserved). Toc references are relative to the toc location so nothing inside the addon changes.
1021 lines
30 KiB
Lua
1021 lines
30 KiB
Lua
--- **AceGUI-3.0** provides access to numerous widgets which can be used to create GUIs.
|
|
-- AceGUI is used by AceConfigDialog to create the option GUIs, but you can use it by itself
|
|
-- to create any custom GUI. There are more extensive examples in the test suite in the Ace3
|
|
-- stand-alone distribution.
|
|
--
|
|
-- **Note**: When using AceGUI-3.0 directly, please do not modify the frames of the widgets directly,
|
|
-- as any "unknown" change to the widgets will cause addons that get your widget out of the widget pool
|
|
-- to misbehave. If you think some part of a widget should be modifiable, please open a ticket, and we"ll
|
|
-- implement a proper API to modify it.
|
|
-- @usage
|
|
-- local AceGUI = LibStub("AceGUI-3.0")
|
|
-- -- Create a container frame
|
|
-- local f = AceGUI:Create("Frame")
|
|
-- f:SetCallback("OnClose",function(widget) AceGUI:Release(widget) end)
|
|
-- f:SetTitle("AceGUI-3.0 Example")
|
|
-- f:SetStatusText("Status Bar")
|
|
-- f:SetLayout("Flow")
|
|
-- -- Create a button
|
|
-- local btn = AceGUI:Create("Button")
|
|
-- btn:SetWidth(170)
|
|
-- btn:SetText("Button !")
|
|
-- btn:SetCallback("OnClick", function() print("Click!") end)
|
|
-- -- Add the button to the container
|
|
-- f:AddChild(btn)
|
|
-- @class file
|
|
-- @name AceGUI-3.0
|
|
-- @release $Id$
|
|
local ACEGUI_MAJOR, ACEGUI_MINOR = "AceGUI-3.0", 41
|
|
local AceGUI, oldminor = LibStub:NewLibrary(ACEGUI_MAJOR, ACEGUI_MINOR)
|
|
|
|
if not AceGUI then return end -- No upgrade needed
|
|
|
|
-- Lua APIs
|
|
local tinsert, wipe = table.insert, table.wipe
|
|
local select, pairs, next, type = select, pairs, next, type
|
|
local error, assert = error, assert
|
|
local setmetatable, rawget = setmetatable, rawget
|
|
local math_max, math_min, math_ceil = math.max, math.min, math.ceil
|
|
|
|
-- WoW APIs
|
|
local UIParent = UIParent
|
|
|
|
AceGUI.WidgetRegistry = AceGUI.WidgetRegistry or {}
|
|
AceGUI.LayoutRegistry = AceGUI.LayoutRegistry or {}
|
|
AceGUI.WidgetBase = AceGUI.WidgetBase or {}
|
|
AceGUI.WidgetContainerBase = AceGUI.WidgetContainerBase or {}
|
|
AceGUI.WidgetVersions = AceGUI.WidgetVersions or {}
|
|
AceGUI.tooltip = AceGUI.tooltip or CreateFrame("GameTooltip", "AceGUITooltip", UIParent, "GameTooltipTemplate")
|
|
|
|
-- local upvalues
|
|
local WidgetRegistry = AceGUI.WidgetRegistry
|
|
local LayoutRegistry = AceGUI.LayoutRegistry
|
|
local WidgetVersions = AceGUI.WidgetVersions
|
|
|
|
--[[
|
|
xpcall safecall implementation
|
|
]]
|
|
local xpcall = xpcall
|
|
|
|
local function errorhandler(err)
|
|
return geterrorhandler()(err)
|
|
end
|
|
|
|
local function safecall(func, ...)
|
|
if func then
|
|
return xpcall(func, errorhandler, ...)
|
|
end
|
|
end
|
|
|
|
-- Recycling functions
|
|
local newWidget, delWidget
|
|
do
|
|
-- Version Upgrade in Minor 29
|
|
-- Internal Storage of the objects changed, from an array table
|
|
-- to a hash table, and additionally we introduced versioning on
|
|
-- the widgets which would discard all widgets from a pre-29 version
|
|
-- anyway, so we just clear the storage now, and don't try to
|
|
-- convert the storage tables to the new format.
|
|
-- This should generally not cause *many* widgets to end up in trash,
|
|
-- since once dialogs are opened, all addons should be loaded already
|
|
-- and AceGUI should be on the latest version available on the users
|
|
-- setup.
|
|
-- -- nevcairiel - Nov 2nd, 2009
|
|
if oldminor and oldminor < 29 and AceGUI.objPools then
|
|
AceGUI.objPools = nil
|
|
end
|
|
|
|
AceGUI.objPools = AceGUI.objPools or {}
|
|
local objPools = AceGUI.objPools
|
|
--Returns a new instance, if none are available either returns a new table or calls the given contructor
|
|
function newWidget(widgetType)
|
|
if not WidgetRegistry[widgetType] then
|
|
error("Attempt to instantiate unknown widget type", 2)
|
|
end
|
|
|
|
if not objPools[widgetType] then
|
|
objPools[widgetType] = {}
|
|
end
|
|
|
|
local newObj = next(objPools[widgetType])
|
|
if not newObj then
|
|
newObj = WidgetRegistry[widgetType]()
|
|
newObj.AceGUIWidgetVersion = WidgetVersions[widgetType]
|
|
else
|
|
objPools[widgetType][newObj] = nil
|
|
-- if the widget is older then the latest, don't even try to reuse it
|
|
-- just forget about it, and grab a new one.
|
|
if not newObj.AceGUIWidgetVersion or newObj.AceGUIWidgetVersion < WidgetVersions[widgetType] then
|
|
return newWidget(widgetType)
|
|
end
|
|
end
|
|
return newObj
|
|
end
|
|
-- Releases an instance to the Pool
|
|
function delWidget(obj,widgetType)
|
|
if not objPools[widgetType] then
|
|
objPools[widgetType] = {}
|
|
end
|
|
if objPools[widgetType][obj] then
|
|
error("Attempt to Release Widget that is already released", 2)
|
|
end
|
|
objPools[widgetType][obj] = true
|
|
end
|
|
end
|
|
|
|
|
|
-------------------
|
|
-- API Functions --
|
|
-------------------
|
|
|
|
-- Gets a widget Object
|
|
|
|
--- Create a new Widget of the given type.
|
|
-- This function will instantiate a new widget (or use one from the widget pool), and call the
|
|
-- OnAcquire function on it, before returning.
|
|
-- @param type The type of the widget.
|
|
-- @return The newly created widget.
|
|
function AceGUI:Create(widgetType)
|
|
if WidgetRegistry[widgetType] then
|
|
local widget = newWidget(widgetType)
|
|
|
|
if rawget(widget, "Acquire") then
|
|
widget.OnAcquire = widget.Acquire
|
|
widget.Acquire = nil
|
|
elseif rawget(widget, "Aquire") then
|
|
widget.OnAcquire = widget.Aquire
|
|
widget.Aquire = nil
|
|
end
|
|
|
|
if rawget(widget, "Release") then
|
|
widget.OnRelease = rawget(widget, "Release")
|
|
widget.Release = nil
|
|
end
|
|
|
|
if widget.OnAcquire then
|
|
widget:OnAcquire()
|
|
else
|
|
error(("Widget type %s doesn't supply an OnAcquire Function"):format(widgetType))
|
|
end
|
|
-- Set the default Layout ("List")
|
|
safecall(widget.SetLayout, widget, "List")
|
|
safecall(widget.ResumeLayout, widget)
|
|
return widget
|
|
end
|
|
end
|
|
|
|
--- Releases a widget Object.
|
|
-- This function calls OnRelease on the widget and places it back in the widget pool.
|
|
-- Any data on the widget is being erased, and the widget will be hidden.\\
|
|
-- If this widget is a Container-Widget, all of its Child-Widgets will be releases as well.
|
|
-- @param widget The widget to release
|
|
function AceGUI:Release(widget)
|
|
if widget.isQueuedForRelease then return end
|
|
widget.isQueuedForRelease = true
|
|
safecall(widget.PauseLayout, widget)
|
|
widget.frame:Hide()
|
|
widget:Fire("OnRelease")
|
|
safecall(widget.ReleaseChildren, widget)
|
|
|
|
if widget.OnRelease then
|
|
widget:OnRelease()
|
|
-- else
|
|
-- error(("Widget type %s doesn't supply an OnRelease Function"):format(widget.type))
|
|
end
|
|
for k in pairs(widget.userdata) do
|
|
widget.userdata[k] = nil
|
|
end
|
|
for k in pairs(widget.events) do
|
|
widget.events[k] = nil
|
|
end
|
|
widget.width = nil
|
|
widget.relWidth = nil
|
|
widget.height = nil
|
|
widget.relHeight = nil
|
|
widget.noAutoHeight = nil
|
|
widget.frame:ClearAllPoints()
|
|
widget.frame:Hide()
|
|
widget.frame:SetParent(UIParent)
|
|
widget.frame.width = nil
|
|
widget.frame.height = nil
|
|
if widget.content then
|
|
widget.content.width = nil
|
|
widget.content.height = nil
|
|
end
|
|
widget.isQueuedForRelease = nil
|
|
delWidget(widget, widget.type)
|
|
end
|
|
|
|
--- Check if a widget is currently in the process of being released
|
|
-- This function check if this widget, or any of its parents (in which case it'll be released shortly as well)
|
|
-- are currently being released. This allows addon to handle any callbacks accordingly.
|
|
-- @param widget The widget to check
|
|
function AceGUI:IsReleasing(widget)
|
|
if widget.isQueuedForRelease then
|
|
return true
|
|
end
|
|
|
|
if widget.parent and widget.parent.AceGUIWidgetVersion then
|
|
return AceGUI:IsReleasing(widget.parent)
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-----------
|
|
-- Focus --
|
|
-----------
|
|
|
|
|
|
--- Called when a widget has taken focus.
|
|
-- e.g. Dropdowns opening, Editboxes gaining kb focus
|
|
-- @param widget The widget that should be focused
|
|
function AceGUI:SetFocus(widget)
|
|
if self.FocusedWidget and self.FocusedWidget ~= widget then
|
|
safecall(self.FocusedWidget.ClearFocus, self.FocusedWidget)
|
|
end
|
|
self.FocusedWidget = widget
|
|
end
|
|
|
|
|
|
--- Called when something has happened that could cause widgets with focus to drop it
|
|
-- e.g. titlebar of a frame being clicked
|
|
function AceGUI:ClearFocus()
|
|
if self.FocusedWidget then
|
|
safecall(self.FocusedWidget.ClearFocus, self.FocusedWidget)
|
|
self.FocusedWidget = nil
|
|
end
|
|
end
|
|
|
|
-------------
|
|
-- Widgets --
|
|
-------------
|
|
--[[
|
|
Widgets must provide the following functions
|
|
OnAcquire() - Called when the object is acquired, should set everything to a default hidden state
|
|
|
|
And the following members
|
|
frame - the frame or derivitive object that will be treated as the widget for size and anchoring purposes
|
|
type - the type of the object, same as the name given to :RegisterWidget()
|
|
|
|
Widgets contain a table called userdata, this is a safe place to store data associated with the wigdet
|
|
It will be cleared automatically when a widget is released
|
|
Placing values directly into a widget object should be avoided
|
|
|
|
If the Widget can act as a container for other Widgets the following
|
|
content - frame or derivitive that children will be anchored to
|
|
|
|
The Widget can supply the following Optional Members
|
|
:OnRelease() - Called when the object is Released, should remove any additional anchors and clear any data
|
|
:OnWidthSet(width) - Called when the width of the widget is changed
|
|
:OnHeightSet(height) - Called when the height of the widget is changed
|
|
Widgets should not use the OnSizeChanged events of thier frame or content members, use these methods instead
|
|
AceGUI already sets a handler to the event
|
|
:LayoutFinished(width, height) - called after a layout has finished, the width and height will be the width and height of the
|
|
area used for controls. These can be nil if the layout used the existing size to layout the controls.
|
|
|
|
]]
|
|
|
|
--------------------------
|
|
-- Widget Base Template --
|
|
--------------------------
|
|
do
|
|
local WidgetBase = AceGUI.WidgetBase
|
|
|
|
WidgetBase.SetParent = function(self, parent)
|
|
local frame = self.frame
|
|
frame:SetParent(nil)
|
|
frame:SetParent(parent.content)
|
|
self.parent = parent
|
|
end
|
|
|
|
WidgetBase.SetCallback = function(self, name, func)
|
|
if type(func) == "function" then
|
|
self.events[name] = func
|
|
end
|
|
end
|
|
|
|
WidgetBase.Fire = function(self, name, ...)
|
|
if self.events[name] then
|
|
local success, ret = safecall(self.events[name], self, name, ...)
|
|
if success then
|
|
return ret
|
|
end
|
|
end
|
|
end
|
|
|
|
WidgetBase.SetWidth = function(self, width)
|
|
self.frame:SetWidth(width)
|
|
self.frame.width = width
|
|
if self.OnWidthSet then
|
|
self:OnWidthSet(width)
|
|
end
|
|
end
|
|
|
|
WidgetBase.SetRelativeWidth = function(self, width)
|
|
if width <= 0 or width > 1 then
|
|
error(":SetRelativeWidth(width): Invalid relative width.", 2)
|
|
end
|
|
self.relWidth = width
|
|
self.width = "relative"
|
|
end
|
|
|
|
WidgetBase.SetHeight = function(self, height)
|
|
self.frame:SetHeight(height)
|
|
self.frame.height = height
|
|
if self.OnHeightSet then
|
|
self:OnHeightSet(height)
|
|
end
|
|
end
|
|
|
|
--[[ WidgetBase.SetRelativeHeight = function(self, height)
|
|
if height <= 0 or height > 1 then
|
|
error(":SetRelativeHeight(height): Invalid relative height.", 2)
|
|
end
|
|
self.relHeight = height
|
|
self.height = "relative"
|
|
end ]]
|
|
|
|
WidgetBase.IsVisible = function(self)
|
|
return self.frame:IsVisible()
|
|
end
|
|
|
|
WidgetBase.IsShown= function(self)
|
|
return self.frame:IsShown()
|
|
end
|
|
|
|
WidgetBase.Release = function(self)
|
|
AceGUI:Release(self)
|
|
end
|
|
|
|
WidgetBase.IsReleasing = function(self)
|
|
return AceGUI:IsReleasing(self)
|
|
end
|
|
|
|
WidgetBase.SetPoint = function(self, ...)
|
|
return self.frame:SetPoint(...)
|
|
end
|
|
|
|
WidgetBase.ClearAllPoints = function(self)
|
|
return self.frame:ClearAllPoints()
|
|
end
|
|
|
|
WidgetBase.GetNumPoints = function(self)
|
|
return self.frame:GetNumPoints()
|
|
end
|
|
|
|
WidgetBase.GetPoint = function(self, ...)
|
|
return self.frame:GetPoint(...)
|
|
end
|
|
|
|
WidgetBase.GetUserDataTable = function(self)
|
|
return self.userdata
|
|
end
|
|
|
|
WidgetBase.SetUserData = function(self, key, value)
|
|
self.userdata[key] = value
|
|
end
|
|
|
|
WidgetBase.GetUserData = function(self, key)
|
|
return self.userdata[key]
|
|
end
|
|
|
|
WidgetBase.IsFullHeight = function(self)
|
|
return self.height == "fill"
|
|
end
|
|
|
|
WidgetBase.SetFullHeight = function(self, isFull)
|
|
if isFull then
|
|
self.height = "fill"
|
|
else
|
|
self.height = nil
|
|
end
|
|
end
|
|
|
|
WidgetBase.IsFullWidth = function(self)
|
|
return self.width == "fill"
|
|
end
|
|
|
|
WidgetBase.SetFullWidth = function(self, isFull)
|
|
if isFull then
|
|
self.width = "fill"
|
|
else
|
|
self.width = nil
|
|
end
|
|
end
|
|
|
|
-- local function LayoutOnUpdate(this)
|
|
-- this:SetScript("OnUpdate",nil)
|
|
-- this.obj:PerformLayout()
|
|
-- end
|
|
|
|
local WidgetContainerBase = AceGUI.WidgetContainerBase
|
|
|
|
WidgetContainerBase.PauseLayout = function(self)
|
|
self.LayoutPaused = true
|
|
end
|
|
|
|
WidgetContainerBase.ResumeLayout = function(self)
|
|
self.LayoutPaused = nil
|
|
end
|
|
|
|
WidgetContainerBase.PerformLayout = function(self)
|
|
if self.LayoutPaused then
|
|
return
|
|
end
|
|
safecall(self.LayoutFunc, self.content, self.children)
|
|
end
|
|
|
|
--call this function to layout, makes sure layed out objects get a frame to get sizes etc
|
|
WidgetContainerBase.DoLayout = function(self)
|
|
self:PerformLayout()
|
|
-- if not self.parent then
|
|
-- self.frame:SetScript("OnUpdate", LayoutOnUpdate)
|
|
-- end
|
|
end
|
|
|
|
WidgetContainerBase.AddChild = function(self, child, beforeWidget)
|
|
if beforeWidget then
|
|
local siblingIndex = 1
|
|
for _, widget in pairs(self.children) do
|
|
if widget == beforeWidget then
|
|
break
|
|
end
|
|
siblingIndex = siblingIndex + 1
|
|
end
|
|
tinsert(self.children, siblingIndex, child)
|
|
else
|
|
tinsert(self.children, child)
|
|
end
|
|
child:SetParent(self)
|
|
child.frame:Show()
|
|
self:DoLayout()
|
|
end
|
|
|
|
WidgetContainerBase.AddChildren = function(self, ...)
|
|
for i = 1, select("#", ...) do
|
|
local child = select(i, ...)
|
|
tinsert(self.children, child)
|
|
child:SetParent(self)
|
|
child.frame:Show()
|
|
end
|
|
self:DoLayout()
|
|
end
|
|
|
|
WidgetContainerBase.ReleaseChildren = function(self)
|
|
local children = self.children
|
|
for i = 1,#children do
|
|
AceGUI:Release(children[i])
|
|
children[i] = nil
|
|
end
|
|
end
|
|
|
|
WidgetContainerBase.SetLayout = function(self, Layout)
|
|
self.LayoutFunc = AceGUI:GetLayout(Layout)
|
|
end
|
|
|
|
WidgetContainerBase.SetAutoAdjustHeight = function(self, adjust)
|
|
if adjust then
|
|
self.noAutoHeight = nil
|
|
else
|
|
self.noAutoHeight = true
|
|
end
|
|
end
|
|
|
|
local function FrameResize(this)
|
|
local self = this.obj
|
|
if this:GetWidth() and this:GetHeight() then
|
|
if self.OnWidthSet then
|
|
self:OnWidthSet(this:GetWidth())
|
|
end
|
|
if self.OnHeightSet then
|
|
self:OnHeightSet(this:GetHeight())
|
|
end
|
|
end
|
|
end
|
|
|
|
local function ContentResize(this)
|
|
if this:GetWidth() and this:GetHeight() then
|
|
this.width = this:GetWidth()
|
|
this.height = this:GetHeight()
|
|
this.obj:DoLayout()
|
|
end
|
|
end
|
|
|
|
setmetatable(WidgetContainerBase, {__index=WidgetBase})
|
|
|
|
--One of these function should be called on each Widget Instance as part of its creation process
|
|
|
|
--- Register a widget-class as a container for newly created widgets.
|
|
-- @param widget The widget class
|
|
function AceGUI:RegisterAsContainer(widget)
|
|
widget.children = {}
|
|
widget.userdata = {}
|
|
widget.events = {}
|
|
widget.base = WidgetContainerBase
|
|
widget.content.obj = widget
|
|
widget.frame.obj = widget
|
|
widget.content:SetScript("OnSizeChanged", ContentResize)
|
|
widget.frame:SetScript("OnSizeChanged", FrameResize)
|
|
setmetatable(widget, {__index = WidgetContainerBase})
|
|
widget:SetLayout("List")
|
|
return widget
|
|
end
|
|
|
|
--- Register a widget-class as a widget.
|
|
-- @param widget The widget class
|
|
function AceGUI:RegisterAsWidget(widget)
|
|
widget.userdata = {}
|
|
widget.events = {}
|
|
widget.base = WidgetBase
|
|
widget.frame.obj = widget
|
|
widget.frame:SetScript("OnSizeChanged", FrameResize)
|
|
setmetatable(widget, {__index = WidgetBase})
|
|
return widget
|
|
end
|
|
end
|
|
|
|
|
|
|
|
|
|
------------------
|
|
-- Widget API --
|
|
------------------
|
|
|
|
--- Registers a widget Constructor, this function returns a new instance of the Widget
|
|
-- @param Name The name of the widget
|
|
-- @param Constructor The widget constructor function
|
|
-- @param Version The version of the widget
|
|
function AceGUI:RegisterWidgetType(Name, Constructor, Version)
|
|
assert(type(Constructor) == "function")
|
|
assert(type(Version) == "number")
|
|
|
|
local oldVersion = WidgetVersions[Name]
|
|
if oldVersion and oldVersion >= Version then return end
|
|
|
|
WidgetVersions[Name] = Version
|
|
WidgetRegistry[Name] = Constructor
|
|
end
|
|
|
|
--- Registers a Layout Function
|
|
-- @param Name The name of the layout
|
|
-- @param LayoutFunc Reference to the layout function
|
|
function AceGUI:RegisterLayout(Name, LayoutFunc)
|
|
assert(type(LayoutFunc) == "function")
|
|
if type(Name) == "string" then
|
|
Name = Name:upper()
|
|
end
|
|
LayoutRegistry[Name] = LayoutFunc
|
|
end
|
|
|
|
--- Get a Layout Function from the registry
|
|
-- @param Name The name of the layout
|
|
function AceGUI:GetLayout(Name)
|
|
if type(Name) == "string" then
|
|
Name = Name:upper()
|
|
end
|
|
return LayoutRegistry[Name]
|
|
end
|
|
|
|
AceGUI.counts = AceGUI.counts or {}
|
|
|
|
--- A type-based counter to count the number of widgets created.
|
|
-- This is used by widgets that require a named frame, e.g. when a Blizzard
|
|
-- Template requires it.
|
|
-- @param type The widget type
|
|
function AceGUI:GetNextWidgetNum(widgetType)
|
|
if not self.counts[widgetType] then
|
|
self.counts[widgetType] = 0
|
|
end
|
|
self.counts[widgetType] = self.counts[widgetType] + 1
|
|
return self.counts[widgetType]
|
|
end
|
|
|
|
--- Return the number of created widgets for this type.
|
|
-- In contrast to GetNextWidgetNum, the number is not incremented.
|
|
-- @param widgetType The widget type
|
|
function AceGUI:GetWidgetCount(widgetType)
|
|
return self.counts[widgetType] or 0
|
|
end
|
|
|
|
--- Return the version of the currently registered widget type.
|
|
-- @param widgetType The widget type
|
|
function AceGUI:GetWidgetVersion(widgetType)
|
|
return WidgetVersions[widgetType]
|
|
end
|
|
|
|
-------------
|
|
-- Layouts --
|
|
-------------
|
|
|
|
--[[
|
|
A Layout is a func that takes 2 parameters
|
|
content - the frame that widgets will be placed inside
|
|
children - a table containing the widgets to layout
|
|
]]
|
|
|
|
-- Very simple Layout, Children are stacked on top of each other down the left side
|
|
AceGUI:RegisterLayout("List",
|
|
function(content, children)
|
|
local height = 0
|
|
local width = content.width or content:GetWidth() or 0
|
|
for i = 1, #children do
|
|
local child = children[i]
|
|
|
|
local frame = child.frame
|
|
frame:ClearAllPoints()
|
|
frame:Show()
|
|
if i == 1 then
|
|
frame:SetPoint("TOPLEFT", content)
|
|
else
|
|
frame:SetPoint("TOPLEFT", children[i-1].frame, "BOTTOMLEFT")
|
|
end
|
|
|
|
if child.width == "fill" then
|
|
child:SetWidth(width)
|
|
frame:SetPoint("RIGHT", content)
|
|
|
|
if child.DoLayout then
|
|
child:DoLayout()
|
|
end
|
|
elseif child.width == "relative" then
|
|
child:SetWidth(width * child.relWidth)
|
|
|
|
if child.DoLayout then
|
|
child:DoLayout()
|
|
end
|
|
end
|
|
|
|
height = height + (frame.height or frame:GetHeight() or 0)
|
|
end
|
|
safecall(content.obj.LayoutFinished, content.obj, nil, height)
|
|
end)
|
|
|
|
-- A single control fills the whole content area
|
|
AceGUI:RegisterLayout("Fill",
|
|
function(content, children)
|
|
if children[1] then
|
|
children[1]:SetWidth(content:GetWidth() or 0)
|
|
children[1]:SetHeight(content:GetHeight() or 0)
|
|
children[1].frame:ClearAllPoints()
|
|
children[1].frame:SetAllPoints(content)
|
|
children[1].frame:Show()
|
|
safecall(content.obj.LayoutFinished, content.obj, nil, children[1].frame:GetHeight())
|
|
end
|
|
end)
|
|
|
|
local layoutrecursionblock = nil
|
|
local function safelayoutcall(object, func, ...)
|
|
layoutrecursionblock = true
|
|
object[func](object, ...)
|
|
layoutrecursionblock = nil
|
|
end
|
|
|
|
AceGUI:RegisterLayout("Flow",
|
|
function(content, children)
|
|
if layoutrecursionblock then return end
|
|
--used height so far
|
|
local height = 0
|
|
--width used in the current row
|
|
local usedwidth = 0
|
|
--height of the current row
|
|
local rowheight = 0
|
|
local rowoffset = 0
|
|
|
|
local width = content.width or content:GetWidth() or 0
|
|
|
|
--control at the start of the row
|
|
local rowstart
|
|
local rowstartoffset
|
|
local isfullheight
|
|
|
|
local frameoffset
|
|
local lastframeoffset
|
|
local oversize
|
|
for i = 1, #children do
|
|
local child = children[i]
|
|
oversize = nil
|
|
local frame = child.frame
|
|
local frameheight = frame.height or frame:GetHeight() or 0
|
|
local framewidth = frame.width or frame:GetWidth() or 0
|
|
lastframeoffset = frameoffset
|
|
-- HACK: Why did we set a frameoffset of (frameheight / 2) ?
|
|
-- That was moving all widgets half the widgets size down, is that intended?
|
|
-- Actually, it seems to be neccessary for many cases, we'll leave it in for now.
|
|
-- If widgets seem to anchor weirdly with this, provide a valid alignoffset for them.
|
|
-- TODO: Investigate moar!
|
|
frameoffset = child.alignoffset or (frameheight / 2)
|
|
|
|
if child.width == "relative" then
|
|
framewidth = width * child.relWidth
|
|
end
|
|
|
|
frame:Show()
|
|
frame:ClearAllPoints()
|
|
if i == 1 then
|
|
-- anchor the first control to the top left
|
|
frame:SetPoint("TOPLEFT", content)
|
|
rowheight = frameheight
|
|
rowoffset = frameoffset
|
|
rowstart = frame
|
|
rowstartoffset = frameoffset
|
|
usedwidth = framewidth
|
|
if usedwidth > width then
|
|
oversize = true
|
|
end
|
|
else
|
|
-- if there isn't available width for the control start a new row
|
|
-- if a control is "fill" it will be on a row of its own full width
|
|
if usedwidth == 0 or ((framewidth) + usedwidth > width) or child.width == "fill" then
|
|
if isfullheight then
|
|
-- a previous row has already filled the entire height, there's nothing we can usefully do anymore
|
|
-- (maybe error/warn about this?)
|
|
break
|
|
end
|
|
--anchor the previous row, we will now know its height and offset
|
|
rowstart:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(height + (rowoffset - rowstartoffset) + 3))
|
|
height = height + rowheight + 3
|
|
--save this as the rowstart so we can anchor it after the row is complete and we have the max height and offset of controls in it
|
|
rowstart = frame
|
|
rowstartoffset = frameoffset
|
|
rowheight = frameheight
|
|
rowoffset = frameoffset
|
|
usedwidth = framewidth
|
|
if usedwidth > width then
|
|
oversize = true
|
|
end
|
|
-- put the control on the current row, adding it to the width and checking if the height needs to be increased
|
|
else
|
|
--handles cases where the new height is higher than either control because of the offsets
|
|
--math.max(rowheight-rowoffset+frameoffset, frameheight-frameoffset+rowoffset)
|
|
|
|
--offset is always the larger of the two offsets
|
|
rowoffset = math_max(rowoffset, frameoffset)
|
|
rowheight = math_max(rowheight, rowoffset + (frameheight / 2))
|
|
|
|
frame:SetPoint("TOPLEFT", children[i-1].frame, "TOPRIGHT", 0, frameoffset - lastframeoffset)
|
|
usedwidth = framewidth + usedwidth
|
|
end
|
|
end
|
|
|
|
if child.width == "fill" then
|
|
safelayoutcall(child, "SetWidth", width)
|
|
frame:SetPoint("RIGHT", content)
|
|
|
|
usedwidth = 0
|
|
rowstart = frame
|
|
|
|
if child.DoLayout then
|
|
child:DoLayout()
|
|
end
|
|
rowheight = frame.height or frame:GetHeight() or 0
|
|
rowoffset = child.alignoffset or (rowheight / 2)
|
|
rowstartoffset = rowoffset
|
|
elseif child.width == "relative" then
|
|
safelayoutcall(child, "SetWidth", width * child.relWidth)
|
|
|
|
if child.DoLayout then
|
|
child:DoLayout()
|
|
end
|
|
elseif oversize then
|
|
if width > 1 then
|
|
frame:SetPoint("RIGHT", content)
|
|
end
|
|
end
|
|
|
|
if child.height == "fill" then
|
|
frame:SetPoint("BOTTOM", content)
|
|
isfullheight = true
|
|
end
|
|
end
|
|
|
|
--anchor the last row, if its full height needs a special case since its height has just been changed by the anchor
|
|
if isfullheight then
|
|
rowstart:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -height)
|
|
elseif rowstart then
|
|
rowstart:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(height + (rowoffset - rowstartoffset) + 3))
|
|
end
|
|
|
|
height = height + rowheight + 3
|
|
safecall(content.obj.LayoutFinished, content.obj, nil, height)
|
|
end)
|
|
|
|
-- Get alignment method and value. Possible alignment methods are a callback, a number, "start", "middle", "end", "fill" or "TOPLEFT", "BOTTOMRIGHT" etc.
|
|
local GetCellAlign = function (dir, tableObj, colObj, cellObj, cell, child)
|
|
local fn = cellObj and (cellObj["align" .. dir] or cellObj.align)
|
|
or colObj and (colObj["align" .. dir] or colObj.align)
|
|
or tableObj["align" .. dir] or tableObj.align
|
|
or "CENTERLEFT"
|
|
local val
|
|
child, cell = child or 0, cell or 0
|
|
|
|
if type(fn) == "string" then
|
|
fn = fn:lower()
|
|
fn = dir == "V" and (fn:sub(1, 3) == "top" and "start" or fn:sub(1, 6) == "bottom" and "end" or fn:sub(1, 6) == "center" and "middle")
|
|
or dir == "H" and (fn:sub(-4) == "left" and "start" or fn:sub(-5) == "right" and "end" or fn:sub(-6) == "center" and "middle")
|
|
or fn
|
|
val = (fn == "start" or fn == "fill") and 0 or fn == "end" and cell - child or (cell - child) / 2
|
|
elseif type(fn) == "function" then
|
|
val = fn(child or 0, cell, dir)
|
|
else
|
|
val = fn
|
|
end
|
|
|
|
return fn, math_max(0, math_min(val, cell))
|
|
end
|
|
|
|
-- Get width or height for multiple cells combined
|
|
local GetCellDimension = function (dir, laneDim, from, to, space)
|
|
local dim = 0
|
|
for cell=from,to do
|
|
dim = dim + (laneDim[cell] or 0)
|
|
end
|
|
return dim + math_max(0, to - from) * (space or 0)
|
|
end
|
|
|
|
--[[ Options
|
|
============
|
|
Container:
|
|
- columns ({col, col, ...}): Column settings. "col" can be a number (<= 0: content width, <1: rel. width, <10: weight, >=10: abs. width) or a table with column setting.
|
|
- space, spaceH, spaceV: Overall, horizontal and vertical spacing between cells.
|
|
- align, alignH, alignV: Overall, horizontal and vertical cell alignment. See GetCellAlign() for possible values.
|
|
Columns:
|
|
- width: Fixed column width (nil or <=0: content width, <1: rel. width, >=1: abs. width).
|
|
- min or 1: Min width for content based width
|
|
- max or 2: Max width for content based width
|
|
- weight: Flexible column width. The leftover width after accounting for fixed-width columns is distributed to weighted columns according to their weights.
|
|
- align, alignH, alignV: Overwrites the container setting for alignment.
|
|
Cell:
|
|
- colspan: Makes a cell span multiple columns.
|
|
- rowspan: Makes a cell span multiple rows.
|
|
- align, alignH, alignV: Overwrites the container and column setting for alignment.
|
|
]]
|
|
AceGUI:RegisterLayout("Table",
|
|
function (content, children)
|
|
local obj = content.obj
|
|
obj:PauseLayout()
|
|
|
|
local tableObj = obj:GetUserData("table")
|
|
local cols = tableObj.columns
|
|
local spaceH = tableObj.spaceH or tableObj.space or 0
|
|
local spaceV = tableObj.spaceV or tableObj.space or 0
|
|
local totalH = (content:GetWidth() or content.width or 0) - spaceH * (#cols - 1)
|
|
|
|
-- We need to reuse these because layout events can come in very frequently
|
|
local layoutCache = obj:GetUserData("layoutCache")
|
|
if not layoutCache then
|
|
layoutCache = {{}, {}, {}, {}, {}, {}}
|
|
obj:SetUserData("layoutCache", layoutCache)
|
|
end
|
|
local t, laneH, laneV, rowspans, rowStart, colStart = unpack(layoutCache)
|
|
|
|
-- Create the grid
|
|
local n, slotFound = 0
|
|
for i,child in ipairs(children) do
|
|
if child:IsShown() then
|
|
repeat
|
|
n = n + 1
|
|
local col = (n - 1) % #cols + 1
|
|
local row = math_ceil(n / #cols)
|
|
local rowspan = rowspans[col]
|
|
local cell = rowspan and rowspan.child or child
|
|
local cellObj = cell:GetUserData("cell")
|
|
slotFound = not rowspan
|
|
|
|
-- Rowspan
|
|
if not rowspan and cellObj and cellObj.rowspan then
|
|
rowspan = {child = child, from = row, to = row + cellObj.rowspan - 1}
|
|
rowspans[col] = rowspan
|
|
end
|
|
if rowspan and i == #children then
|
|
rowspan.to = row
|
|
end
|
|
|
|
-- Colspan
|
|
local colspan = math_max(0, math_min((cellObj and cellObj.colspan or 1) - 1, #cols - col))
|
|
n = n + colspan
|
|
|
|
-- Place the cell
|
|
if not rowspan or rowspan.to == row then
|
|
t[n] = cell
|
|
rowStart[cell] = rowspan and rowspan.from or row
|
|
colStart[cell] = col
|
|
|
|
if rowspan then
|
|
rowspans[col] = nil
|
|
end
|
|
end
|
|
until slotFound
|
|
end
|
|
end
|
|
|
|
local rows = math_ceil(n / #cols)
|
|
|
|
-- Determine fixed size cols and collect weights
|
|
local extantH, totalWeight = totalH, 0
|
|
for col,colObj in ipairs(cols) do
|
|
laneH[col] = 0
|
|
|
|
if type(colObj) == "number" then
|
|
colObj = {[colObj >= 1 and colObj < 10 and "weight" or "width"] = colObj}
|
|
cols[col] = colObj
|
|
end
|
|
|
|
if colObj.weight then
|
|
-- Weight
|
|
totalWeight = totalWeight + (colObj.weight or 1)
|
|
else
|
|
if not colObj.width or colObj.width <= 0 then
|
|
-- Content width
|
|
for row=1,rows do
|
|
local child = t[(row - 1) * #cols + col]
|
|
if child then
|
|
local f = child.frame
|
|
f:ClearAllPoints()
|
|
local childH = f:GetWidth() or 0
|
|
|
|
laneH[col] = math_max(laneH[col], childH - GetCellDimension("H", laneH, colStart[child], col - 1, spaceH))
|
|
end
|
|
end
|
|
|
|
laneH[col] = math_max(colObj.min or colObj[1] or 0, math_min(laneH[col], colObj.max or colObj[2] or laneH[col]))
|
|
else
|
|
-- Rel./Abs. width
|
|
laneH[col] = colObj.width < 1 and colObj.width * totalH or colObj.width
|
|
end
|
|
extantH = math_max(0, extantH - laneH[col])
|
|
end
|
|
end
|
|
|
|
-- Determine sizes based on weight
|
|
local scale = totalWeight > 0 and extantH / totalWeight or 0
|
|
for col,colObj in pairs(cols) do
|
|
if colObj.weight then
|
|
laneH[col] = scale * colObj.weight
|
|
end
|
|
end
|
|
|
|
-- Arrange children
|
|
for row=1,rows do
|
|
local rowV = 0
|
|
|
|
-- Horizontal placement and sizing
|
|
for col=1,#cols do
|
|
local child = t[(row - 1) * #cols + col]
|
|
if child then
|
|
local colObj = cols[colStart[child]]
|
|
local cellObj = child:GetUserData("cell")
|
|
local offsetH = GetCellDimension("H", laneH, 1, colStart[child] - 1, spaceH) + (colStart[child] == 1 and 0 or spaceH)
|
|
local cellH = GetCellDimension("H", laneH, colStart[child], col, spaceH)
|
|
|
|
local f = child.frame
|
|
f:ClearAllPoints()
|
|
local childH = f:GetWidth() or 0
|
|
|
|
local alignFn, align = GetCellAlign("H", tableObj, colObj, cellObj, cellH, childH)
|
|
f:SetPoint("LEFT", content, offsetH + align, 0)
|
|
if child:IsFullWidth() or alignFn == "fill" or childH > cellH then
|
|
f:SetPoint("RIGHT", content, "LEFT", offsetH + align + cellH, 0)
|
|
end
|
|
|
|
if child.DoLayout then
|
|
child:DoLayout()
|
|
end
|
|
|
|
rowV = math_max(rowV, (f:GetHeight() or 0) - GetCellDimension("V", laneV, rowStart[child], row - 1, spaceV))
|
|
end
|
|
end
|
|
|
|
laneV[row] = rowV
|
|
|
|
-- Vertical placement and sizing
|
|
for col=1,#cols do
|
|
local child = t[(row - 1) * #cols + col]
|
|
if child then
|
|
local colObj = cols[colStart[child]]
|
|
local cellObj = child:GetUserData("cell")
|
|
local offsetV = GetCellDimension("V", laneV, 1, rowStart[child] - 1, spaceV) + (rowStart[child] == 1 and 0 or spaceV)
|
|
local cellV = GetCellDimension("V", laneV, rowStart[child], row, spaceV)
|
|
|
|
local f = child.frame
|
|
local childV = f:GetHeight() or 0
|
|
|
|
local alignFn, align = GetCellAlign("V", tableObj, colObj, cellObj, cellV, childV)
|
|
if child:IsFullHeight() or alignFn == "fill" then
|
|
f:SetHeight(cellV)
|
|
end
|
|
f:SetPoint("TOP", content, 0, -(offsetV + align))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Calculate total height
|
|
local totalV = GetCellDimension("V", laneV, 1, #laneV, spaceV)
|
|
|
|
-- Cleanup
|
|
for _,v in pairs(layoutCache) do wipe(v) end
|
|
|
|
safecall(obj.LayoutFinished, obj, nil, totalV)
|
|
obj:ResumeLayout()
|
|
end)
|