view Libs/AceGUI-3.0/widgets/AceGUIWidget-TreeGroup.lua @ 0:169f5211fc7f

First public revision. At this point ItemAuditor watches mail for auctions sold or purchased, watches for buy/sell (money and 1 item type change) and conversions/tradeskills. Milling isn't working yet because there is too much time between the first event and the last event.
author Asa Ayers <Asa.Ayers@Gmail.com>
date Thu, 20 May 2010 19:22:19 -0700
parents
children
line wrap: on
line source
local AceGUI = LibStub("AceGUI-3.0")

-- Lua APIs
local next, pairs, ipairs, assert, type = next, pairs, ipairs, assert, type
local math_min, math_max, floor = math.min, math.max, floor
local select, tremove, unpack = select, table.remove, unpack

-- WoW APIs
local CreateFrame, UIParent = CreateFrame, UIParent

-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
-- List them here for Mikk's FindGlobals script
-- GLOBALS: GameTooltip, FONT_COLOR_CODE_CLOSE

-- 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

--------------
-- TreeView --
--------------

do
	local Type = "TreeGroup"
	local Version = 23
	
	local DEFAULT_TREE_WIDTH = 175
	local DEFAULT_TREE_SIZABLE = true

	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 = 0,
		insets = { left = 3, right = 3, top = 7, bottom = 7 }
	}
    
	local function OnAcquire(self)
		self:SetTreeWidth(DEFAULT_TREE_WIDTH,DEFAULT_TREE_SIZABLE)
		self:EnableButtonTooltips(true)
	end
	
	local function OnRelease(self)
        
		self.frame:ClearAllPoints()
		self.frame:Hide()
		self.status = 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
	
	local function GetButtonParents(line)
		local parent = line.parent
		if parent and parent.value then
			return parent.value, GetButtonParents(parent)
		end
	end
	
	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 ButtonOnClick(this)
		local self = this.obj
		self:Fire("OnClick",this.uniquevalue, this.selected)
		if not this.selected then
			self:SetSelected(this.uniquevalue)
			this.selected = true
			this:LockHighlight()
			self:RefreshTree()
		end
		AceGUI:ClearFocus()
	end
	
	local function ExpandOnClick(this)
		local button = this.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 ButtonOnDoubleClick(button)
		local self = button.obj
		local status = self.status or self.localstatus
		local status = (self.status or self.localstatus).groups
		status[button.uniquevalue] = not status[button.uniquevalue]
		self:RefreshTree()
	end

	local function EnableButtonTooltips(self, enable)
		self.enabletooltips = enable
	end

	local function Button_OnEnter(this)
		local self = this.obj
		self:Fire("OnButtonEnter", this.uniquevalue, this)

		if self.enabletooltips then
			GameTooltip:SetOwner(this, "ANCHOR_NONE")
			GameTooltip:SetPoint("LEFT",this,"RIGHT")
			GameTooltip:SetText(this.text:GetText() or "", 1, .82, 0, 1)

			GameTooltip:Show()
		end
	end
	
	local function Button_OnLeave(this)
		local self = this.obj
		self:Fire("OnButtonLeave", this.uniquevalue, this)
		
		if self.enabletooltips then
			GameTooltip:Hide()
		end
	end
    
    
	local buttoncount = 1
	local function CreateButton(self)
		local button = CreateFrame("Button",("AceGUI30TreeButton%d"):format(buttoncount),self.treeframe, "OptionsListButtonTemplate")
		buttoncount = buttoncount + 1
		button.obj = self
		
		local icon = button:CreateTexture(nil, "OVERLAY")
		icon:SetWidth(14)
		icon:SetHeight(14)
		button.icon = icon
		
		button:SetScript("OnClick",ButtonOnClick)
		button:SetScript("OnDoubleClick", ButtonOnDoubleClick)
		button:SetScript("OnEnter",Button_OnEnter)
		button:SetScript("OnLeave",Button_OnLeave)

		button.toggle.button = button
		button.toggle:SetScript("OnClick",ExpandOnClick)
		
		return button
	end

	local function UpdateButton(button, treeline, selected, canExpand, isExpanded)
		local self = button.obj
		local toggle = button.toggle
		local frame = self.frame
		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
		local normalTexture = button:GetNormalTexture()
		local line = button.line
		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", button, "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("Interface\\Buttons\\UI-PlusButton-UP")
				toggle:SetPushedTexture("Interface\\Buttons\\UI-PlusButton-DOWN")
			else
				toggle:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-UP")
				toggle:SetPushedTexture("Interface\\Buttons\\UI-MinusButton-DOWN")
			end
			toggle:Show()
		else
			toggle:Hide()
		end
	end

	
	local function OnScrollValueChanged(this, value)
		if this.obj.noupdate then return end
		local self = this.obj
		local status = self.status or self.localstatus
		status.scrollvalue = value
		self:RefreshTree()
		AceGUI:ClearFocus()
	end
	
	-- called to set an external table to store status in
	local function SetStatusTable(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 not status.treesizable then
			status.treesizable = DEFAULT_TREE_SIZABLE
		end
		self:SetTreeWidth(status.treewidth,status.treesizable)
		self:RefreshTree()
	end

	--sets the tree to be displayed
	--[[
		example tree
		
		Alpha
		Bravo
		  -Charlie
		  -Delta
			-Echo
		Foxtrot
		
		tree = { 
			{ 
				value = "A",
				text = "Alpha"
			},
			{
				value = "B",
				text = "Bravo",
				children = {
					{ 
						value = "C", 
						text = "Charlie"
					},
					{
						value = "D",	
						text = "Delta"
						children = { 
							{ 
								value = "E",
								text = "Echo"
							} 
						} 
					}
				}
			},
			{ 
				value = "F", 
				text = "Foxtrot" 
			},
		}
	]]
	local function SetTree(self, tree, filter)
		self.filter = filter
		if tree then 
			assert(type(tree) == "table") 
		end
		self.tree = tree
		self:RefreshTree()
	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
	
	local function BuildLevel(self, tree, level, parent)
		local groups = (self.status or self.localstatus).groups
		local hasChildren = self.hasChildren
		
		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
	
	--fire an update after one frame to catch the treeframes height
	local function FirstFrameUpdate(this)
		local self = this.obj
		this:SetScript("OnUpdate",nil)
		self:RefreshTree()
	end
	
	local function ResizeUpdate(this)
		this.obj:RefreshTree()
	end
	
	local function RefreshTree(self)
		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

		self:BuildLevel(tree, 1)
		
		local numlines = #lines
		
		local maxlines = (floor(((self.treeframe:GetHeight()or 0) - 20 ) / 18))
		
		local first, last
		
		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
				self.scrollbar:SetValue(status.scrollvalue)
			end
			self.noupdate = nil
			first, last = status.scrollvalue+1, status.scrollvalue + maxlines
		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 i == 1 then
					if self.showscroll then
						button:SetPoint("TOPRIGHT", self.treeframe,"TOPRIGHT",-22,-10)
						button:SetPoint("TOPLEFT", self.treeframe, "TOPLEFT", 0, -10)
					else
						button:SetPoint("TOPRIGHT", self.treeframe,"TOPRIGHT",0,-10)
						button:SetPoint("TOPLEFT", self.treeframe, "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
	
	local function SetSelected(self, value)
		local status = self.status or self.localstatus
		if status.selected ~= value then
			status.selected = value
			self:Fire("OnGroupSelected", value)
		end
	end
	
	local function BuildUniqueValue(...)
		local n = select('#', ...)
		if n == 1 then
			return ...
		else
			return (...).."\001"..BuildUniqueValue(select(2,...))
		end
	end
	
	local function Select(self, uniquevalue, ...)
		self.filter = false
		local status = self.status or self.localstatus
		local groups = status.groups
		for i = 1, select('#', ...) do
			groups[BuildUniqueValue(select(i, ...))] = true
		end
		status.selected = uniquevalue
		self:RefreshTree()
		self:Fire("OnGroupSelected", uniquevalue)
	end
	
	local function SelectByPath(self, ...)
		 self:Select(BuildUniqueValue(...), ...)
	end
	
	--Selects a tree node by UniqueValue
	local function SelectByValue(self, uniquevalue)
		self:Select(uniquevalue, ("\001"):split(uniquevalue))
	end
	

	local function ShowScroll(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
	
	local function OnWidthSet(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
		treeframe:SetMaxResize(maxtreewidth,1600)
	end
	
	
	local function OnHeightSet(self, height)
		local content = self.content
		local contentheight = height - 20
		if contentheight < 0 then
			contentheight = 0
		end
		content:SetHeight(contentheight)
		content.height = contentheight
	end
	

	local function TreeOnMouseWheel(this, delta)
		local self = this.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 SetTreeWidth(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
	
	local function draggerLeave(this)
		this:SetBackdropColor(1, 1, 1, 0)
	end
	
	local function draggerEnter(this)
		this:SetBackdropColor(1, 1, 1, 0.8)
	end
	
	local function draggerDown(this)
		local treeframe = this:GetParent()
		treeframe:StartSizing("RIGHT")
	end
	
	local function draggerUp(this)
		local treeframe = this:GetParent()
		local self = treeframe.obj
		local frame = 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:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
		treeframe:SetPoint("BOTTOMLEFT",frame,"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
	
	local function LayoutFinished(self, width, height)
		if self.noAutoHeight then return end
		self:SetHeight((height or 0) + 20)
	end

	local createdcount = 0
	local function Constructor()
		local frame = CreateFrame("Frame",nil,UIParent)
		local self = {}
		self.type = Type
		self.lines = {}
		self.levels = {}
		self.buttons = {}
		self.hasChildren = {}
		self.localstatus = {}
		self.localstatus.groups = {}
		self.filter = false
		
		local treeframe = CreateFrame("Frame",nil,frame)
		treeframe.obj = self
		treeframe:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
		treeframe:SetPoint("BOTTOMLEFT",frame,"BOTTOMLEFT",0,0)
		treeframe:SetWidth(DEFAULT_TREE_WIDTH)
		treeframe:SetScript("OnUpdate",FirstFrameUpdate)
		treeframe:SetScript("OnSizeChanged",ResizeUpdate)
		
		treeframe:EnableMouseWheel(true)
		treeframe:SetScript("OnMouseWheel", TreeOnMouseWheel)
		treeframe:SetBackdrop(PaneBackdrop)
		treeframe:SetBackdropColor(0.1,0.1,0.1,0.5)
		treeframe:SetBackdropBorderColor(0.4,0.4,0.4)
		
		treeframe:SetResizable(true)
		treeframe:SetMinResize(100, 1)
		treeframe:SetMaxResize(400,1600)
		local dragger = CreateFrame("Frame", nil, treeframe)
		dragger:SetWidth(8)
		dragger:SetPoint("TOP", treeframe, "TOPRIGHT")
		dragger:SetPoint("BOTTOM", treeframe, "BOTTOMRIGHT")
		dragger:SetBackdrop(DraggerBackdrop)
		dragger:SetBackdropColor(1, 1, 1, 0)
		dragger:SetScript("OnMouseDown", draggerDown)
		dragger:SetScript("OnMouseUp", draggerUp)
		dragger:SetScript("OnEnter", draggerEnter)
		dragger:SetScript("OnLeave", draggerLeave)
		
		self.dragger = dragger
		self.treeframe = treeframe
		self.OnRelease = OnRelease
		self.OnAcquire = OnAcquire
		
		self.SetTree = SetTree
		self.SetTreeWidth = SetTreeWidth
		self.RefreshTree = RefreshTree
		self.SetStatusTable = SetStatusTable
		self.BuildLevel = BuildLevel
		self.CreateButton = CreateButton
		self.SetSelected = SetSelected
		self.ShowScroll = ShowScroll
		self.SetStatusTable = SetStatusTable
		self.Select = Select
		self.SelectByValue = SelectByValue
		self.SelectByPath = SelectByPath
		self.OnWidthSet = OnWidthSet
		self.OnHeightSet = OnHeightSet
		self.EnableButtonTooltips = EnableButtonTooltips
		--self.Filter = Filter
		self.LayoutFinished = LayoutFinished
		
		self.frame = frame
		frame.obj = self

		createdcount = createdcount + 1
		local scrollbar = CreateFrame("Slider",("AceConfigDialogTreeGroup%dScrollBar"):format(createdcount),treeframe,"UIPanelScrollBarTemplate")
		self.scrollbar = scrollbar
		local scrollbg = scrollbar:CreateTexture(nil,"BACKGROUND")
		scrollbg:SetAllPoints(scrollbar)
		scrollbg:SetTexture(0,0,0,0.4)
		scrollbar.obj = self
		self.noupdate = true
		scrollbar:SetPoint("TOPRIGHT",treeframe,"TOPRIGHT",-10,-26)
		scrollbar:SetPoint("BOTTOMRIGHT",treeframe,"BOTTOMRIGHT",-10,26)
		scrollbar:SetScript("OnValueChanged", OnScrollValueChanged)
		scrollbar:SetMinMaxValues(0,0)
		self.localstatus.scrollvalue = 0
		scrollbar:SetValueStep(1)
		scrollbar:SetValue(0)
		scrollbar:SetWidth(16)
		self.noupdate = nil

		local border = CreateFrame("Frame",nil,frame)
		self.border = border
		border:SetPoint("TOPLEFT",treeframe,"TOPRIGHT", 0,0)
		border:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",0,0)
		
		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)
		self.content = content
		content.obj = self
		content:SetPoint("TOPLEFT",border,"TOPLEFT",10,-10)
		content:SetPoint("BOTTOMRIGHT",border,"BOTTOMRIGHT",-10,10)
		
		AceGUI:RegisterAsContainer(self)
		--AceGUI:RegisterAsWidget(self)
		return self
	end
	
	AceGUI:RegisterWidgetType(Type,Constructor,Version)
end