1 --[[-----------------------------------------------------------------------------
3 Container that uses a tree control to switch between groups.
4 -------------------------------------------------------------------------------]]
5 local Type, Version = "TreeGroup", 40
6 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
7 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end
10 local next, pairs, ipairs, assert, type = next, pairs, ipairs, assert, type
11 local math_min, math_max, floor = math.min, math.max, floor
12 local select, tremove, unpack, tconcat = select, table.remove, unpack, table.concat
15 local CreateFrame, UIParent = CreateFrame, UIParent
17 -- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
18 -- List them here for Mikk's FindGlobals script
19 -- GLOBALS: GameTooltip, FONT_COLOR_CODE_CLOSE
21 -- Recycling functions
24 local pool = setmetatable({},{__mode='k'})
42 local DEFAULT_TREE_WIDTH = 175
43 local DEFAULT_TREE_SIZABLE = true
45 --[[-----------------------------------------------------------------------------
47 -------------------------------------------------------------------------------]]
48 local function GetButtonUniqueValue(line)
49 local parent = line.parent
50 if parent and parent.value then
51 return GetButtonUniqueValue(parent).."\001"..line.value
57 local function UpdateButton(button, treeline, selected, canExpand, isExpanded)
58 local self = button.obj
59 local toggle = button.toggle
60 local frame = self.frame
61 local text = treeline.text or ""
62 local icon = treeline.icon
63 local iconCoords = treeline.iconCoords
64 local level = treeline.level
65 local value = treeline.value
66 local uniquevalue = treeline.uniquevalue
67 local disabled = treeline.disabled
69 button.treeline = treeline
71 button.uniquevalue = uniquevalue
73 button:LockHighlight()
74 button.selected = true
76 button:UnlockHighlight()
77 button.selected = false
79 local normalTexture = button:GetNormalTexture()
80 local line = button.line
82 if ( level == 1 ) then
83 button:SetNormalFontObject("GameFontNormal")
84 button:SetHighlightFontObject("GameFontHighlight")
85 button.text:SetPoint("LEFT", (icon and 16 or 0) + 8, 2)
87 button:SetNormalFontObject("GameFontHighlightSmall")
88 button:SetHighlightFontObject("GameFontHighlightSmall")
89 button.text:SetPoint("LEFT", (icon and 16 or 0) + 8 * level, 2)
93 button:EnableMouse(false)
94 button.text:SetText("|cff808080"..text..FONT_COLOR_CODE_CLOSE)
96 button.text:SetText(text)
97 button:EnableMouse(true)
101 button.icon:SetTexture(icon)
102 button.icon:SetPoint("LEFT", 8 * level, (level == 1) and 0 or 1)
104 button.icon:SetTexture(nil)
108 button.icon:SetTexCoord(unpack(iconCoords))
110 button.icon:SetTexCoord(0, 1, 0, 1)
114 if not isExpanded then
115 toggle:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-UP")
116 toggle:SetPushedTexture("Interface\\Buttons\\UI-PlusButton-DOWN")
118 toggle:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-UP")
119 toggle:SetPushedTexture("Interface\\Buttons\\UI-MinusButton-DOWN")
127 local function ShouldDisplayLevel(tree)
129 for k, v in ipairs(tree) do
130 if v.children == nil and v.visible ~= false then
132 elseif v.children then
133 result = result or ShouldDisplayLevel(v.children)
135 if result then return result end
140 local function addLine(self, v, tree, level, parent)
145 line.iconCoords = v.iconCoords
146 line.disabled = v.disabled
150 line.visible = v.visible
151 line.uniquevalue = GetButtonUniqueValue(line)
153 line.hasChildren = true
155 line.hasChildren = nil
157 self.lines[#self.lines+1] = line
161 --fire an update after one frame to catch the treeframes height
162 local function FirstFrameUpdate(frame)
163 local self = frame.obj
164 frame:SetScript("OnUpdate", nil)
168 local function BuildUniqueValue(...)
169 local n = select('#', ...)
173 return (...).."\001"..BuildUniqueValue(select(2,...))
177 --[[-----------------------------------------------------------------------------
179 -------------------------------------------------------------------------------]]
180 local function Expand_OnClick(frame)
181 local button = frame.button
182 local self = button.obj
183 local status = (self.status or self.localstatus).groups
184 status[button.uniquevalue] = not status[button.uniquevalue]
188 local function Button_OnClick(frame)
189 local self = frame.obj
190 self:Fire("OnClick", frame.uniquevalue, frame.selected)
191 if not frame.selected then
192 self:SetSelected(frame.uniquevalue)
193 frame.selected = true
194 frame:LockHighlight()
200 local function Button_OnDoubleClick(button)
201 local self = button.obj
202 local status = self.status or self.localstatus
203 local status = (self.status or self.localstatus).groups
204 status[button.uniquevalue] = not status[button.uniquevalue]
208 local function Button_OnEnter(frame)
209 local self = frame.obj
210 self:Fire("OnButtonEnter", frame.uniquevalue, frame)
212 if self.enabletooltips then
213 GameTooltip:SetOwner(frame, "ANCHOR_NONE")
214 GameTooltip:SetPoint("LEFT",frame,"RIGHT")
215 GameTooltip:SetText(frame.text:GetText() or "", 1, .82, 0, true)
221 local function Button_OnLeave(frame)
222 local self = frame.obj
223 self:Fire("OnButtonLeave", frame.uniquevalue, frame)
225 if self.enabletooltips then
230 local function OnScrollValueChanged(frame, value)
231 if frame.obj.noupdate then return end
232 local self = frame.obj
233 local status = self.status or self.localstatus
234 status.scrollvalue = floor(value + 0.5)
239 local function Tree_OnSizeChanged(frame)
240 frame.obj:RefreshTree()
243 local function Tree_OnMouseWheel(frame, delta)
244 local self = frame.obj
245 if self.showscroll then
246 local scrollbar = self.scrollbar
247 local min, max = scrollbar:GetMinMaxValues()
248 local value = scrollbar:GetValue()
249 local newvalue = math_min(max,math_max(min,value - delta))
250 if value ~= newvalue then
251 scrollbar:SetValue(newvalue)
256 local function Dragger_OnLeave(frame)
257 frame:SetBackdropColor(1, 1, 1, 0)
260 local function Dragger_OnEnter(frame)
261 frame:SetBackdropColor(1, 1, 1, 0.8)
264 local function Dragger_OnMouseDown(frame)
265 local treeframe = frame:GetParent()
266 treeframe:StartSizing("RIGHT")
269 local function Dragger_OnMouseUp(frame)
270 local treeframe = frame:GetParent()
271 local self = treeframe.obj
272 local frame = treeframe:GetParent()
273 treeframe:StopMovingOrSizing()
274 --treeframe:SetScript("OnUpdate", nil)
275 treeframe:SetUserPlaced(false)
276 --Without this :GetHeight will get stuck on the current height, causing the tree contents to not resize
277 treeframe:SetHeight(0)
278 treeframe:SetPoint("TOPLEFT", frame, "TOPLEFT",0,0)
279 treeframe:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT",0,0)
281 local status = self.status or self.localstatus
282 status.treewidth = treeframe:GetWidth()
284 treeframe.obj:Fire("OnTreeResize",treeframe:GetWidth())
285 -- recalculate the content width
286 treeframe.obj:OnWidthSet(status.fullwidth)
287 -- update the layout of the content
288 treeframe.obj:DoLayout()
291 --[[-----------------------------------------------------------------------------
293 -------------------------------------------------------------------------------]]
295 ["OnAcquire"] = function(self)
296 self:SetTreeWidth(DEFAULT_TREE_WIDTH, DEFAULT_TREE_SIZABLE)
297 self:EnableButtonTooltips(true)
298 self.frame:SetScript("OnUpdate", FirstFrameUpdate)
301 ["OnRelease"] = function(self)
303 for k, v in pairs(self.localstatus) do
304 if k == "groups" then
305 for k2 in pairs(v) do
309 self.localstatus[k] = nil
312 self.localstatus.scrollvalue = 0
313 self.localstatus.treewidth = DEFAULT_TREE_WIDTH
314 self.localstatus.treesizable = DEFAULT_TREE_SIZABLE
317 ["EnableButtonTooltips"] = function(self, enable)
318 self.enabletooltips = enable
321 ["CreateButton"] = function(self)
322 local num = AceGUI:GetNextWidgetNum("TreeGroupButton")
323 local button = CreateFrame("Button", ("AceGUI30TreeButton%d"):format(num), self.treeframe, "OptionsListButtonTemplate")
326 local icon = button:CreateTexture(nil, "OVERLAY")
331 button:SetScript("OnClick",Button_OnClick)
332 button:SetScript("OnDoubleClick", Button_OnDoubleClick)
333 button:SetScript("OnEnter",Button_OnEnter)
334 button:SetScript("OnLeave",Button_OnLeave)
336 button.toggle.button = button
337 button.toggle:SetScript("OnClick",Expand_OnClick)
339 button.text:SetHeight(14) -- Prevents text wrapping
344 ["SetStatusTable"] = function(self, status)
345 assert(type(status) == "table")
347 if not status.groups then
350 if not status.scrollvalue then
351 status.scrollvalue = 0
353 if not status.treewidth then
354 status.treewidth = DEFAULT_TREE_WIDTH
356 if status.treesizable == nil then
357 status.treesizable = DEFAULT_TREE_SIZABLE
359 self:SetTreeWidth(status.treewidth,status.treesizable)
363 --sets the tree to be displayed
364 ["SetTree"] = function(self, tree, filter)
367 assert(type(tree) == "table")
373 ["BuildLevel"] = function(self, tree, level, parent)
374 local groups = (self.status or self.localstatus).groups
375 local hasChildren = self.hasChildren
377 for i, v in ipairs(tree) do
379 if not self.filter or ShouldDisplayLevel(v.children) then
380 local line = addLine(self, v, tree, level, parent)
381 if groups[line.uniquevalue] then
382 self:BuildLevel(v.children, level+1, line)
385 elseif v.visible ~= false or not self.filter then
386 addLine(self, v, tree, level, parent)
391 ["RefreshTree"] = function(self,scrollToSelection)
392 local buttons = self.buttons
393 local lines = self.lines
395 for i, v in ipairs(buttons) do
399 local t = tremove(lines)
406 if not self.tree then return end
407 --Build the list of visible entries from the tree and status tables
408 local status = self.status or self.localstatus
409 local groupstatus = status.groups
410 local tree = self.tree
412 local treeframe = self.treeframe
414 status.scrollToSelection = status.scrollToSelection or scrollToSelection -- needs to be cached in case the control hasn't been drawn yet (code bails out below)
416 self:BuildLevel(tree, 1)
418 local numlines = #lines
420 local maxlines = (floor(((self.treeframe:GetHeight()or 0) - 20 ) / 18))
421 if maxlines <= 0 then return end
425 scrollToSelection = status.scrollToSelection
426 status.scrollToSelection = nil
428 if numlines <= maxlines then
429 --the whole tree fits in the frame
430 status.scrollvalue = 0
431 self:ShowScroll(false)
432 first, last = 1, numlines
434 self:ShowScroll(true)
435 --scrolling will be needed
437 self.scrollbar:SetMinMaxValues(0, numlines - maxlines)
438 --check if we are scrolled down too far
439 if numlines - status.scrollvalue < maxlines then
440 status.scrollvalue = numlines - maxlines
443 first, last = status.scrollvalue+1, status.scrollvalue + maxlines
445 if scrollToSelection and status.selected then
447 for i,line in ipairs(lines) do -- find the line number
448 if line.uniquevalue==status.selected then
453 -- selection was deleted or something?
454 elseif show>=first and show<=last then
459 status.scrollvalue = show-1
461 status.scrollvalue = show-maxlines
463 first, last = status.scrollvalue+1, status.scrollvalue + maxlines
466 if self.scrollbar:GetValue() ~= status.scrollvalue then
467 self.scrollbar:SetValue(status.scrollvalue)
472 for i = first, last do
473 local line = lines[i]
474 local button = buttons[buttonnum]
476 button = self:CreateButton()
478 buttons[buttonnum] = button
479 button:SetParent(treeframe)
480 button:SetFrameLevel(treeframe:GetFrameLevel()+1)
481 button:ClearAllPoints()
482 if buttonnum == 1 then
483 if self.showscroll then
484 button:SetPoint("TOPRIGHT", -22, -10)
485 button:SetPoint("TOPLEFT", 0, -10)
487 button:SetPoint("TOPRIGHT", 0, -10)
488 button:SetPoint("TOPLEFT", 0, -10)
491 button:SetPoint("TOPRIGHT", buttons[buttonnum-1], "BOTTOMRIGHT",0,0)
492 button:SetPoint("TOPLEFT", buttons[buttonnum-1], "BOTTOMLEFT",0,0)
496 UpdateButton(button, line, status.selected == line.uniquevalue, line.hasChildren, groupstatus[line.uniquevalue] )
498 buttonnum = buttonnum + 1
503 ["SetSelected"] = function(self, value)
504 local status = self.status or self.localstatus
505 if status.selected ~= value then
506 status.selected = value
507 self:Fire("OnGroupSelected", value)
511 ["Select"] = function(self, uniquevalue, ...)
513 local status = self.status or self.localstatus
514 local groups = status.groups
517 groups[tconcat(path, "\001", 1, i)] = true
519 status.selected = uniquevalue
520 self:RefreshTree(true)
521 self:Fire("OnGroupSelected", uniquevalue)
524 ["SelectByPath"] = function(self, ...)
525 self:Select(BuildUniqueValue(...), ...)
528 ["SelectByValue"] = function(self, uniquevalue)
529 self:Select(uniquevalue, ("\001"):split(uniquevalue))
532 ["ShowScroll"] = function(self, show)
533 self.showscroll = show
535 self.scrollbar:Show()
536 if self.buttons[1] then
537 self.buttons[1]:SetPoint("TOPRIGHT", self.treeframe,"TOPRIGHT",-22,-10)
540 self.scrollbar:Hide()
541 if self.buttons[1] then
542 self.buttons[1]:SetPoint("TOPRIGHT", self.treeframe,"TOPRIGHT",0,-10)
547 ["OnWidthSet"] = function(self, width)
548 local content = self.content
549 local treeframe = self.treeframe
550 local status = self.status or self.localstatus
551 status.fullwidth = width
553 local contentwidth = width - status.treewidth - 20
554 if contentwidth < 0 then
557 content:SetWidth(contentwidth)
558 content.width = contentwidth
560 local maxtreewidth = math_min(400, width - 50)
562 if maxtreewidth > 100 and status.treewidth > maxtreewidth then
563 self:SetTreeWidth(maxtreewidth, status.treesizable)
565 treeframe:SetMaxResize(maxtreewidth, 1600)
568 ["OnHeightSet"] = function(self, height)
569 local content = self.content
570 local contentheight = height - 20
571 if contentheight < 0 then
574 content:SetHeight(contentheight)
575 content.height = contentheight
578 ["SetTreeWidth"] = function(self, treewidth, resizable)
579 if not resizable then
580 if type(treewidth) == 'number' then
582 elseif type(treewidth) == 'boolean' then
583 resizable = treewidth
584 treewidth = DEFAULT_TREE_WIDTH
587 treewidth = DEFAULT_TREE_WIDTH
590 self.treeframe:SetWidth(treewidth)
591 self.dragger:EnableMouse(resizable)
593 local status = self.status or self.localstatus
594 status.treewidth = treewidth
595 status.treesizable = resizable
597 -- recalculate the content width
598 if status.fullwidth then
599 self:OnWidthSet(status.fullwidth)
603 ["GetTreeWidth"] = function(self)
604 local status = self.status or self.localstatus
605 return status.treewidth or DEFAULT_TREE_WIDTH
608 ["LayoutFinished"] = function(self, width, height)
609 if self.noAutoHeight then return end
610 self:SetHeight((height or 0) + 20)
614 --[[-----------------------------------------------------------------------------
616 -------------------------------------------------------------------------------]]
617 local PaneBackdrop = {
618 bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
619 edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
620 tile = true, tileSize = 16, edgeSize = 16,
621 insets = { left = 3, right = 3, top = 5, bottom = 3 }
624 local DraggerBackdrop = {
625 bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
627 tile = true, tileSize = 16, edgeSize = 0,
628 insets = { left = 3, right = 3, top = 7, bottom = 7 }
631 local function Constructor()
632 local num = AceGUI:GetNextWidgetNum(Type)
633 local frame = CreateFrame("Frame", nil, UIParent)
635 local treeframe = CreateFrame("Frame", nil, frame)
636 treeframe:SetPoint("TOPLEFT")
637 treeframe:SetPoint("BOTTOMLEFT")
638 treeframe:SetWidth(DEFAULT_TREE_WIDTH)
639 treeframe:EnableMouseWheel(true)
640 treeframe:SetBackdrop(PaneBackdrop)
641 treeframe:SetBackdropColor(0.1, 0.1, 0.1, 0.5)
642 treeframe:SetBackdropBorderColor(0.4, 0.4, 0.4)
643 treeframe:SetResizable(true)
644 treeframe:SetMinResize(100, 1)
645 treeframe:SetMaxResize(400, 1600)
646 treeframe:SetScript("OnUpdate", FirstFrameUpdate)
647 treeframe:SetScript("OnSizeChanged", Tree_OnSizeChanged)
648 treeframe:SetScript("OnMouseWheel", Tree_OnMouseWheel)
650 local dragger = CreateFrame("Frame", nil, treeframe)
652 dragger:SetPoint("TOP", treeframe, "TOPRIGHT")
653 dragger:SetPoint("BOTTOM", treeframe, "BOTTOMRIGHT")
654 dragger:SetBackdrop(DraggerBackdrop)
655 dragger:SetBackdropColor(1, 1, 1, 0)
656 dragger:SetScript("OnEnter", Dragger_OnEnter)
657 dragger:SetScript("OnLeave", Dragger_OnLeave)
658 dragger:SetScript("OnMouseDown", Dragger_OnMouseDown)
659 dragger:SetScript("OnMouseUp", Dragger_OnMouseUp)
661 local scrollbar = CreateFrame("Slider", ("AceConfigDialogTreeGroup%dScrollBar"):format(num), treeframe, "UIPanelScrollBarTemplate")
662 scrollbar:SetScript("OnValueChanged", nil)
663 scrollbar:SetPoint("TOPRIGHT", -10, -26)
664 scrollbar:SetPoint("BOTTOMRIGHT", -10, 26)
665 scrollbar:SetMinMaxValues(0,0)
666 scrollbar:SetValueStep(1)
667 scrollbar:SetValue(0)
668 scrollbar:SetWidth(16)
669 scrollbar:SetScript("OnValueChanged", OnScrollValueChanged)
671 local scrollbg = scrollbar:CreateTexture(nil, "BACKGROUND")
672 scrollbg:SetAllPoints(scrollbar)
673 scrollbg:SetColorTexture(0,0,0,0.4)
675 local border = CreateFrame("Frame",nil,frame)
676 border:SetPoint("TOPLEFT", treeframe, "TOPRIGHT")
677 border:SetPoint("BOTTOMRIGHT")
678 border:SetBackdrop(PaneBackdrop)
679 border:SetBackdropColor(0.1, 0.1, 0.1, 0.5)
680 border:SetBackdropBorderColor(0.4, 0.4, 0.4)
683 local content = CreateFrame("Frame", nil, border)
684 content:SetPoint("TOPLEFT", 10, -10)
685 content:SetPoint("BOTTOMRIGHT", -10, 10)
693 localstatus = { groups = {}, scrollvalue = 0 },
695 treeframe = treeframe,
697 scrollbar = scrollbar,
702 for method, func in pairs(methods) do
703 widget[method] = func
705 treeframe.obj, dragger.obj, scrollbar.obj = widget, widget, widget
707 return AceGUI:RegisterAsContainer(widget)
710 AceGUI:RegisterWidgetType(Type, Constructor, Version)