2021-05-17 16:49:54 +02:00
--[[-----------------------------------------------------------------------------
TreeGroup Container
Container that uses a tree control to switch between groups .
-------------------------------------------------------------------------------]]
2023-01-18 16:44:35 +01:00
local Type , Version = " TreeGroup " , 47
2021-05-17 16:49:54 +02:00
local AceGUI = LibStub and LibStub ( " AceGUI-3.0 " , true )
if not AceGUI or ( AceGUI : GetWidgetVersion ( Type ) or 0 ) >= Version then return end
-- Lua APIs
local next , pairs , ipairs , assert , type = next , pairs , ipairs , assert , type
2023-01-18 16:44:35 +01:00
local math_min , math_max , floor = math.min , math.max , math.floor
2021-05-17 16:49:54 +02:00
local select , tremove , unpack , tconcat = select , table.remove , unpack , table.concat
-- WoW APIs
local CreateFrame , UIParent = CreateFrame , UIParent
-- Recycling functions
local new , del
do
local pool = setmetatable ( { } , { __mode = ' k ' } )
function new ( )
local t = next ( pool )
if t then
pool [ t ] = nil
return t
else
return { }
end
end
function del ( t )
for k in pairs ( t ) do
t [ k ] = nil
end
pool [ t ] = true
end
end
local DEFAULT_TREE_WIDTH = 175
local DEFAULT_TREE_SIZABLE = true
--[[-----------------------------------------------------------------------------
Support functions
-------------------------------------------------------------------------------]]
local function GetButtonUniqueValue ( line )
local parent = line.parent
if parent and parent.value then
return GetButtonUniqueValue ( parent ) .. " \001 " .. line.value
else
return line.value
end
end
local function UpdateButton ( button , treeline , selected , canExpand , isExpanded )
local self = button.obj
local toggle = button.toggle
local text = treeline.text or " "
local icon = treeline.icon
local iconCoords = treeline.iconCoords
local level = treeline.level
local value = treeline.value
local uniquevalue = treeline.uniquevalue
local disabled = treeline.disabled
button.treeline = treeline
button.value = value
button.uniquevalue = uniquevalue
if selected then
button : LockHighlight ( )
button.selected = true
else
button : UnlockHighlight ( )
button.selected = false
end
button.level = level
if ( level == 1 ) then
button : SetNormalFontObject ( " GameFontNormal " )
button : SetHighlightFontObject ( " GameFontHighlight " )
button.text : SetPoint ( " LEFT " , ( icon and 16 or 0 ) + 8 , 2 )
else
button : SetNormalFontObject ( " GameFontHighlightSmall " )
button : SetHighlightFontObject ( " GameFontHighlightSmall " )
button.text : SetPoint ( " LEFT " , ( icon and 16 or 0 ) + 8 * level , 2 )
end
if disabled then
button : EnableMouse ( false )
button.text : SetText ( " |cff808080 " .. text .. FONT_COLOR_CODE_CLOSE )
else
button.text : SetText ( text )
button : EnableMouse ( true )
end
if icon then
button.icon : SetTexture ( icon )
button.icon : SetPoint ( " LEFT " , 8 * level , ( level == 1 ) and 0 or 1 )
else
button.icon : SetTexture ( nil )
end
if iconCoords then
button.icon : SetTexCoord ( unpack ( iconCoords ) )
else
button.icon : SetTexCoord ( 0 , 1 , 0 , 1 )
end
if canExpand then
if not isExpanded then
toggle : SetNormalTexture ( 130838 ) -- Interface\\Buttons\\UI-PlusButton-UP
toggle : SetPushedTexture ( 130836 ) -- Interface\\Buttons\\UI-PlusButton-DOWN
else
toggle : SetNormalTexture ( 130821 ) -- Interface\\Buttons\\UI-MinusButton-UP
toggle : SetPushedTexture ( 130820 ) -- Interface\\Buttons\\UI-MinusButton-DOWN
end
toggle : Show ( )
else
toggle : Hide ( )
end
end
local function ShouldDisplayLevel ( tree )
local result = false
for k , v in ipairs ( tree ) do
if v.children == nil and v.visible ~= false then
result = true
elseif v.children then
result = result or ShouldDisplayLevel ( v.children )
end
if result then return result end
end
return false
end
local function addLine ( self , v , tree , level , parent )
local line = new ( )
line.value = v.value
line.text = v.text
line.icon = v.icon
line.iconCoords = v.iconCoords
line.disabled = v.disabled
line.tree = tree
line.level = level
line.parent = parent
line.visible = v.visible
line.uniquevalue = GetButtonUniqueValue ( line )
if v.children then
line.hasChildren = true
else
line.hasChildren = nil
end
self.lines [ # self.lines + 1 ] = line
return line
end
--fire an update after one frame to catch the treeframes height
local function FirstFrameUpdate ( frame )
local self = frame.obj
frame : SetScript ( " OnUpdate " , nil )
self : RefreshTree ( nil , true )
end
local function BuildUniqueValue ( ... )
local n = select ( ' # ' , ... )
if n == 1 then
return ...
else
return ( ... ) .. " \001 " .. BuildUniqueValue ( select ( 2 , ... ) )
end
end
--[[-----------------------------------------------------------------------------
Scripts
-------------------------------------------------------------------------------]]
local function Expand_OnClick ( frame )
local button = frame.button
local self = button.obj
local status = ( self.status or self.localstatus ) . groups
status [ button.uniquevalue ] = not status [ button.uniquevalue ]
self : RefreshTree ( )
end
local function Button_OnClick ( frame )
local self = frame.obj
self : Fire ( " OnClick " , frame.uniquevalue , frame.selected )
if not frame.selected then
self : SetSelected ( frame.uniquevalue )
frame.selected = true
frame : LockHighlight ( )
self : RefreshTree ( )
end
AceGUI : ClearFocus ( )
end
local function Button_OnDoubleClick ( button )
local self = button.obj
local status = ( self.status or self.localstatus ) . groups
status [ button.uniquevalue ] = not status [ button.uniquevalue ]
self : RefreshTree ( )
end
local function Button_OnEnter ( frame )
local self = frame.obj
self : Fire ( " OnButtonEnter " , frame.uniquevalue , frame )
if self.enabletooltips then
local tooltip = AceGUI.tooltip
tooltip : SetOwner ( frame , " ANCHOR_NONE " )
tooltip : ClearAllPoints ( )
tooltip : SetPoint ( " LEFT " , frame , " RIGHT " )
tooltip : SetText ( frame.text : GetText ( ) or " " , 1 , .82 , 0 , true )
tooltip : Show ( )
end
end
local function Button_OnLeave ( frame )
local self = frame.obj
self : Fire ( " OnButtonLeave " , frame.uniquevalue , frame )
if self.enabletooltips then
AceGUI.tooltip : Hide ( )
end
end
local function OnScrollValueChanged ( frame , value )
if frame.obj . noupdate then return end
local self = frame.obj
local status = self.status or self.localstatus
status.scrollvalue = floor ( value + 0.5 )
self : RefreshTree ( )
AceGUI : ClearFocus ( )
end
local function Tree_OnSizeChanged ( frame )
frame.obj : RefreshTree ( )
end
local function Tree_OnMouseWheel ( frame , delta )
local self = frame.obj
if self.showscroll then
local scrollbar = self.scrollbar
local min , max = scrollbar : GetMinMaxValues ( )
local value = scrollbar : GetValue ( )
local newvalue = math_min ( max , math_max ( min , value - delta ) )
if value ~= newvalue then
scrollbar : SetValue ( newvalue )
end
end
end
local function Dragger_OnLeave ( frame )
frame : SetBackdropColor ( 1 , 1 , 1 , 0 )
end
local function Dragger_OnEnter ( frame )
frame : SetBackdropColor ( 1 , 1 , 1 , 0.8 )
end
local function Dragger_OnMouseDown ( frame )
local treeframe = frame : GetParent ( )
treeframe : StartSizing ( " RIGHT " )
end
local function Dragger_OnMouseUp ( frame )
local treeframe = frame : GetParent ( )
local self = treeframe.obj
local treeframeParent = treeframe : GetParent ( )
treeframe : StopMovingOrSizing ( )
--treeframe:SetScript("OnUpdate", nil)
treeframe : SetUserPlaced ( false )
--Without this :GetHeight will get stuck on the current height, causing the tree contents to not resize
treeframe : SetHeight ( 0 )
treeframe : ClearAllPoints ( )
treeframe : SetPoint ( " TOPLEFT " , treeframeParent , " TOPLEFT " , 0 , 0 )
treeframe : SetPoint ( " BOTTOMLEFT " , treeframeParent , " BOTTOMLEFT " , 0 , 0 )
local status = self.status or self.localstatus
status.treewidth = treeframe : GetWidth ( )
treeframe.obj : Fire ( " OnTreeResize " , treeframe : GetWidth ( ) )
-- recalculate the content width
treeframe.obj : OnWidthSet ( status.fullwidth )
-- update the layout of the content
treeframe.obj : DoLayout ( )
end
--[[-----------------------------------------------------------------------------
Methods
-------------------------------------------------------------------------------]]
local methods = {
[ " OnAcquire " ] = function ( self )
self : SetTreeWidth ( DEFAULT_TREE_WIDTH , DEFAULT_TREE_SIZABLE )
self : EnableButtonTooltips ( true )
self.frame : SetScript ( " OnUpdate " , FirstFrameUpdate )
end ,
[ " OnRelease " ] = function ( self )
self.status = nil
self.tree = nil
self.frame : SetScript ( " OnUpdate " , nil )
for k , v in pairs ( self.localstatus ) do
if k == " groups " then
for k2 in pairs ( v ) do
v [ k2 ] = nil
end
else
self.localstatus [ k ] = nil
end
end
self.localstatus . scrollvalue = 0
self.localstatus . treewidth = DEFAULT_TREE_WIDTH
self.localstatus . treesizable = DEFAULT_TREE_SIZABLE
end ,
[ " EnableButtonTooltips " ] = function ( self , enable )
self.enabletooltips = enable
end ,
[ " CreateButton " ] = function ( self )
local num = AceGUI : GetNextWidgetNum ( " TreeGroupButton " )
local button = CreateFrame ( " Button " , ( " AceGUI30TreeButton%d " ) : format ( num ) , self.treeframe , " OptionsListButtonTemplate " )
button.obj = self
local icon = button : CreateTexture ( nil , " OVERLAY " )
icon : SetWidth ( 14 )
icon : SetHeight ( 14 )
button.icon = icon
button : SetScript ( " OnClick " , Button_OnClick )
button : SetScript ( " OnDoubleClick " , Button_OnDoubleClick )
button : SetScript ( " OnEnter " , Button_OnEnter )
button : SetScript ( " OnLeave " , Button_OnLeave )
button.toggle . button = button
button.toggle : SetScript ( " OnClick " , Expand_OnClick )
button.text : SetHeight ( 14 ) -- Prevents text wrapping
return button
end ,
[ " SetStatusTable " ] = function ( self , status )
assert ( type ( status ) == " table " )
self.status = status
if not status.groups then
status.groups = { }
end
if not status.scrollvalue then
status.scrollvalue = 0
end
if not status.treewidth then
status.treewidth = DEFAULT_TREE_WIDTH
end
if status.treesizable == nil then
status.treesizable = DEFAULT_TREE_SIZABLE
end
self : SetTreeWidth ( status.treewidth , status.treesizable )
self : RefreshTree ( )
end ,
--sets the tree to be displayed
[ " SetTree " ] = function ( self , tree , filter )
self.filter = filter
if tree then
assert ( type ( tree ) == " table " )
end
self.tree = tree
self : RefreshTree ( )
end ,
[ " BuildLevel " ] = function ( self , tree , level , parent )
local groups = ( self.status or self.localstatus ) . groups
for i , v in ipairs ( tree ) do
if v.children then
if not self.filter or ShouldDisplayLevel ( v.children ) then
local line = addLine ( self , v , tree , level , parent )
if groups [ line.uniquevalue ] then
self : BuildLevel ( v.children , level + 1 , line )
end
end
elseif v.visible ~= false or not self.filter then
addLine ( self , v , tree , level , parent )
end
end
end ,
[ " RefreshTree " ] = function ( self , scrollToSelection , fromOnUpdate )
local buttons = self.buttons
local lines = self.lines
for i , v in ipairs ( buttons ) do
v : Hide ( )
end
while lines [ 1 ] do
local t = tremove ( lines )
for k in pairs ( t ) do
t [ k ] = nil
end
del ( t )
end
if not self.tree then return end
--Build the list of visible entries from the tree and status tables
local status = self.status or self.localstatus
local groupstatus = status.groups
local tree = self.tree
local treeframe = self.treeframe
status.scrollToSelection = status.scrollToSelection or scrollToSelection -- needs to be cached in case the control hasn't been drawn yet (code bails out below)
self : BuildLevel ( tree , 1 )
local numlines = # lines
local maxlines = ( floor ( ( ( self.treeframe : GetHeight ( ) or 0 ) - 20 ) / 18 ) )
if maxlines <= 0 then return end
if self.frame : GetParent ( ) == UIParent and not fromOnUpdate then
self.frame : SetScript ( " OnUpdate " , FirstFrameUpdate )
return
end
local first , last
scrollToSelection = status.scrollToSelection
status.scrollToSelection = nil
if numlines <= maxlines then
--the whole tree fits in the frame
status.scrollvalue = 0
self : ShowScroll ( false )
first , last = 1 , numlines
else
self : ShowScroll ( true )
--scrolling will be needed
self.noupdate = true
self.scrollbar : SetMinMaxValues ( 0 , numlines - maxlines )
--check if we are scrolled down too far
if numlines - status.scrollvalue < maxlines then
status.scrollvalue = numlines - maxlines
end
self.noupdate = nil
first , last = status.scrollvalue + 1 , status.scrollvalue + maxlines
--show selection?
if scrollToSelection and status.selected then
local show
for i , line in ipairs ( lines ) do -- find the line number
if line.uniquevalue == status.selected then
show = i
end
end
if not show then
-- selection was deleted or something?
elseif show >= first and show <= last then
-- all good
else
-- scrolling needed!
if show < first then
status.scrollvalue = show - 1
else
status.scrollvalue = show - maxlines
end
first , last = status.scrollvalue + 1 , status.scrollvalue + maxlines
end
end
if self.scrollbar : GetValue ( ) ~= status.scrollvalue then
self.scrollbar : SetValue ( status.scrollvalue )
end
end
local buttonnum = 1
for i = first , last do
local line = lines [ i ]
local button = buttons [ buttonnum ]
if not button then
button = self : CreateButton ( )
buttons [ buttonnum ] = button
button : SetParent ( treeframe )
button : SetFrameLevel ( treeframe : GetFrameLevel ( ) + 1 )
button : ClearAllPoints ( )
if buttonnum == 1 then
if self.showscroll then
button : SetPoint ( " TOPRIGHT " , - 22 , - 10 )
button : SetPoint ( " TOPLEFT " , 0 , - 10 )
else
button : SetPoint ( " TOPRIGHT " , 0 , - 10 )
button : SetPoint ( " TOPLEFT " , 0 , - 10 )
end
else
button : SetPoint ( " TOPRIGHT " , buttons [ buttonnum - 1 ] , " BOTTOMRIGHT " , 0 , 0 )
button : SetPoint ( " TOPLEFT " , buttons [ buttonnum - 1 ] , " BOTTOMLEFT " , 0 , 0 )
end
end
UpdateButton ( button , line , status.selected == line.uniquevalue , line.hasChildren , groupstatus [ line.uniquevalue ] )
button : Show ( )
buttonnum = buttonnum + 1
end
end ,
[ " SetSelected " ] = function ( self , value )
local status = self.status or self.localstatus
if status.selected ~= value then
status.selected = value
self : Fire ( " OnGroupSelected " , value )
end
end ,
[ " Select " ] = function ( self , uniquevalue , ... )
self.filter = false
local status = self.status or self.localstatus
local groups = status.groups
local path = { ... }
for i = 1 , # path do
groups [ tconcat ( path , " \001 " , 1 , i ) ] = true
end
status.selected = uniquevalue
self : RefreshTree ( true )
self : Fire ( " OnGroupSelected " , uniquevalue )
end ,
[ " SelectByPath " ] = function ( self , ... )
self : Select ( BuildUniqueValue ( ... ) , ... )
end ,
[ " SelectByValue " ] = function ( self , uniquevalue )
self : Select ( uniquevalue , ( " \001 " ) : split ( uniquevalue ) )
end ,
[ " ShowScroll " ] = function ( self , show )
self.showscroll = show
if show then
self.scrollbar : Show ( )
if self.buttons [ 1 ] then
self.buttons [ 1 ] : SetPoint ( " TOPRIGHT " , self.treeframe , " TOPRIGHT " , - 22 , - 10 )
end
else
self.scrollbar : Hide ( )
if self.buttons [ 1 ] then
self.buttons [ 1 ] : SetPoint ( " TOPRIGHT " , self.treeframe , " TOPRIGHT " , 0 , - 10 )
end
end
end ,
[ " OnWidthSet " ] = function ( self , width )
local content = self.content
local treeframe = self.treeframe
local status = self.status or self.localstatus
status.fullwidth = width
local contentwidth = width - status.treewidth - 20
if contentwidth < 0 then
contentwidth = 0
end
content : SetWidth ( contentwidth )
content.width = contentwidth
local maxtreewidth = math_min ( 400 , width - 50 )
if maxtreewidth > 100 and status.treewidth > maxtreewidth then
self : SetTreeWidth ( maxtreewidth , status.treesizable )
end
2023-01-18 16:44:35 +01:00
if treeframe.SetResizeBounds then
treeframe : SetResizeBounds ( 100 , 1 , maxtreewidth , 1600 )
else
treeframe : SetMaxResize ( maxtreewidth , 1600 )
end
2021-05-17 16:49:54 +02:00
end ,
[ " OnHeightSet " ] = function ( self , height )
local content = self.content
local contentheight = height - 20
if contentheight < 0 then
contentheight = 0
end
content : SetHeight ( contentheight )
content.height = contentheight
end ,
[ " SetTreeWidth " ] = function ( self , treewidth , resizable )
if not resizable then
if type ( treewidth ) == ' number ' then
resizable = false
elseif type ( treewidth ) == ' boolean ' then
resizable = treewidth
treewidth = DEFAULT_TREE_WIDTH
else
resizable = false
treewidth = DEFAULT_TREE_WIDTH
end
end
self.treeframe : SetWidth ( treewidth )
self.dragger : EnableMouse ( resizable )
local status = self.status or self.localstatus
status.treewidth = treewidth
status.treesizable = resizable
-- recalculate the content width
if status.fullwidth then
self : OnWidthSet ( status.fullwidth )
end
end ,
[ " GetTreeWidth " ] = function ( self )
local status = self.status or self.localstatus
return status.treewidth or DEFAULT_TREE_WIDTH
end ,
[ " LayoutFinished " ] = function ( self , width , height )
if self.noAutoHeight then return end
self : SetHeight ( ( height or 0 ) + 20 )
end
}
--[[-----------------------------------------------------------------------------
Constructor
-------------------------------------------------------------------------------]]
local PaneBackdrop = {
bgFile = " Interface \\ ChatFrame \\ ChatFrameBackground " ,
edgeFile = " Interface \\ Tooltips \\ UI-Tooltip-Border " ,
tile = true , tileSize = 16 , edgeSize = 16 ,
insets = { left = 3 , right = 3 , top = 5 , bottom = 3 }
}
local DraggerBackdrop = {
bgFile = " Interface \\ Tooltips \\ UI-Tooltip-Background " ,
edgeFile = nil ,
tile = true , tileSize = 16 , edgeSize = 1 ,
insets = { left = 3 , right = 3 , top = 7 , bottom = 7 }
}
local function Constructor ( )
local num = AceGUI : GetNextWidgetNum ( Type )
local frame = CreateFrame ( " Frame " , nil , UIParent )
2023-01-18 16:44:35 +01:00
local treeframe = CreateFrame ( " Frame " , nil , frame , " BackdropTemplate " )
2021-05-17 16:49:54 +02:00
treeframe : SetPoint ( " TOPLEFT " )
treeframe : SetPoint ( " BOTTOMLEFT " )
treeframe : SetWidth ( DEFAULT_TREE_WIDTH )
treeframe : EnableMouseWheel ( true )
treeframe : SetBackdrop ( PaneBackdrop )
treeframe : SetBackdropColor ( 0.1 , 0.1 , 0.1 , 0.5 )
treeframe : SetBackdropBorderColor ( 0.4 , 0.4 , 0.4 )
treeframe : SetResizable ( true )
2023-01-18 16:44:35 +01:00
if treeframe.SetResizeBounds then -- WoW 10.0
treeframe : SetResizeBounds ( 100 , 1 , 400 , 1600 )
else
treeframe : SetMinResize ( 100 , 1 )
treeframe : SetMaxResize ( 400 , 1600 )
end
2021-05-17 16:49:54 +02:00
treeframe : SetScript ( " OnUpdate " , FirstFrameUpdate )
treeframe : SetScript ( " OnSizeChanged " , Tree_OnSizeChanged )
treeframe : SetScript ( " OnMouseWheel " , Tree_OnMouseWheel )
2023-01-18 16:44:35 +01:00
local dragger = CreateFrame ( " Frame " , nil , treeframe , " BackdropTemplate " )
2021-05-17 16:49:54 +02:00
dragger : SetWidth ( 8 )
dragger : SetPoint ( " TOP " , treeframe , " TOPRIGHT " )
dragger : SetPoint ( " BOTTOM " , treeframe , " BOTTOMRIGHT " )
dragger : SetBackdrop ( DraggerBackdrop )
dragger : SetBackdropColor ( 1 , 1 , 1 , 0 )
dragger : SetScript ( " OnEnter " , Dragger_OnEnter )
dragger : SetScript ( " OnLeave " , Dragger_OnLeave )
dragger : SetScript ( " OnMouseDown " , Dragger_OnMouseDown )
dragger : SetScript ( " OnMouseUp " , Dragger_OnMouseUp )
local scrollbar = CreateFrame ( " Slider " , ( " AceConfigDialogTreeGroup%dScrollBar " ) : format ( num ) , treeframe , " UIPanelScrollBarTemplate " )
scrollbar : SetScript ( " OnValueChanged " , nil )
scrollbar : SetPoint ( " TOPRIGHT " , - 10 , - 26 )
scrollbar : SetPoint ( " BOTTOMRIGHT " , - 10 , 26 )
scrollbar : SetMinMaxValues ( 0 , 0 )
scrollbar : SetValueStep ( 1 )
scrollbar : SetValue ( 0 )
scrollbar : SetWidth ( 16 )
scrollbar : SetScript ( " OnValueChanged " , OnScrollValueChanged )
local scrollbg = scrollbar : CreateTexture ( nil , " BACKGROUND " )
scrollbg : SetAllPoints ( scrollbar )
scrollbg : SetColorTexture ( 0 , 0 , 0 , 0.4 )
2023-01-18 16:44:35 +01:00
local border = CreateFrame ( " Frame " , nil , frame , " BackdropTemplate " )
2021-05-17 16:49:54 +02:00
border : SetPoint ( " TOPLEFT " , treeframe , " TOPRIGHT " )
border : SetPoint ( " BOTTOMRIGHT " )
border : SetBackdrop ( PaneBackdrop )
border : SetBackdropColor ( 0.1 , 0.1 , 0.1 , 0.5 )
border : SetBackdropBorderColor ( 0.4 , 0.4 , 0.4 )
--Container Support
local content = CreateFrame ( " Frame " , nil , border )
content : SetPoint ( " TOPLEFT " , 10 , - 10 )
content : SetPoint ( " BOTTOMRIGHT " , - 10 , 10 )
local widget = {
frame = frame ,
lines = { } ,
levels = { } ,
buttons = { } ,
hasChildren = { } ,
localstatus = { groups = { } , scrollvalue = 0 } ,
filter = false ,
treeframe = treeframe ,
dragger = dragger ,
scrollbar = scrollbar ,
border = border ,
content = content ,
type = Type
}
for method , func in pairs ( methods ) do
widget [ method ] = func
end
treeframe.obj , dragger.obj , scrollbar.obj = widget , widget , widget
return AceGUI : RegisterAsContainer ( widget )
end
AceGUI : RegisterWidgetType ( Type , Constructor , Version )