You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

884 lines
40 KiB

local _,rematch = ...
local C = rematch.constants
local settings = rematch.settings
--[[
Wrapper for Blizzard's recently expanded ScrollBox control with support for expanding/collapsing headers,
different templates (can be different heights) in the same list, and searching.
autoScrollBox:Setup(definition) -- sets up an autoScrollBox from a definition table (see below)
autoScrollBox:Update() -- repopulates and updates the autoScrollBox
autoScrollBox:Refresh() -- just updates the already-displayed buttons for minor changes
autoScrollBox:SetCompactMode(true/false) -- changes from normal to compact mode (switches templates)
autoScrollBox:GetCompactMode() -- returns true/false if in compact mode
autoScrollBox:ToggleHeader(data) -- for expandable lists, expand/collapse header with data value
autoScrollBox:ToggleAllHeaders(data) -- for expandable lists, expand/collapse all headers
autoScrollBox:CollapseAllHeaders() -- for expandable lists, collapses all headers
autoScrollBox:CollapseAllButData(data) -- for expandable lists, collapses all headers except the one for data
autoScrollBox:IsHeaderExpanded(data) -- returns true if the header at data is expanded
autoScrollBox:IsAnyExpanded() -- returns true if any header is expanded
autoScrollBox:IsSearching() -- returns true if a search is happening (searchMask has a value)
autoScrollBox:Select(name,data) -- selects the listbutton that contains data with the named select (or unselects if data nil)
autoScrollBox:GetSelected(name) -- returns the currently selected data for the named select
autoScrollBox:ScrollToTop() -- scrolls to top of the list
autoScrollBox:ScrollDataToTop(data) -- scrolls data to the top of the list
autoScrollBox:ScrollHeaderIntoView(data) -- scrolls header(data) into view and as much of its contents if expanded
autoScrollBox:ScrollDataIntoView(data) -- scrolls data into view, expanding header it's within if needed
autoScrollBox:BlingData(data) -- flashes frame that contains data, scrolling it into view if needed
autoScrollBox:LockHeader() -- locks headers so they can't be expanded/collapsed
autoScrollBox:UnlockHeaders() -- unlocks headers so they can be expanded/collapsed
autoScrollBox:IsHeadersLocked() -- true if headers are locked
definition passed in Setup() defines the templates, callbacks and behavior of the autoScrollBox:
definition = {
allData = {}, -- (required) ordered list of data/id's that has meaning to the calling panel
normalTemplate = "", -- (required) list button template for normal stuff
normalFill = function(button,data) end, -- (required) fill function for normal stuff
normalHeight = 0, -- (required) pixel height of a normal list button
-- all the following are optional (unless their functionality needed)
isCompact = true/false, -- whether this starts as a compact-mode list
compactTemplate = "", -- list button template for compact stuff
compactFill = function(button,data) end, -- fill function for compact stuff
compactHeight = 0, -- pixel height of a compact list button
headerTemplate = "", -- header button template
headerFill = function(button,data) end, -- fill function for headers
headerCriteria = function(self,data) end, -- returns true if data is a header
headerHeight = 0, -- pixel height of a header list button
placeholderTemplate = "", -- placeholder button template
placeholderFill = function(button,data) end, -- fill function for placeholders
placeholderCriteria = function(self,data) end, -- returns true if data is a placeholder
placeholderHeight = 0, -- pixel height of a placeholder list button
allButton = <button>, -- button that inherits AllButtonTemplate (optional)
expandedHeaders = {}, -- unordered table of expanded headers {header1=true,header3=true,etc}
searchBox = <editbox>, -- EditBox where search text is entered
searchHit = function(self,mask,data) end, -- whether data should be listed (data matches mask)
onUpdate = function(self,percent,...) -- function called when list is updated
onScroll = function(self,percent,...) -- function called when list is scrolled
selects = { -- for use with autoScrollBox:Select(name,data)
name1 = {
color = {r,g,b,a}, -- vertex color/alpha of select texture (white if not defined)
alphaMode = "", -- blend mode ("ADD", "BLEND") of select texture ("BLEND" if not defined)
parentKey = "<parentKey>", -- which parentKey select texture should anchor to (eg "Back") (whole button if not defined)
padding = <number> or {left,right,top,bottom}, -- px space around edge (0 if not defined)
drawLayer = "", -- drawLayer of the select texture ("OVERLAY" if not defined)
textureSubLevel = <number> -- textureSubLevel for the drawLayer (0 if not defined)
tint = true/false, -- whether to use a solid color texture over whole button
},
name2 = { -- alternate selects supported (such as one select for summoned pet and another for pet card pet)
color = {r,g,b},
alphaMode = "",
parentKey = "<parentKey>",
padding = <number> or {left,right,top,bottom},
drawLayer = "",
textureSubLevel = <number>,
tint = false,
},
}
}
Notes:
- If allButton is defined, then its OnClick will be taken over (hook it after Setup(def) if needed)
- If searchBox is defined, then its OnTextChanged will be taken over (hook it after Setup(def) if needed)
- isCompact property should only be updated by SetCompactMode() since it needs to rebuild the view too
- The BlingData texture is BACKGROUND textureSubLevel 7
- The BlingData frame will anchor to parent.Back if it exists or whole parent if it doesn't
]]
local setView, populateList, handleSelect, setupSelect, getDataHeight, updateListSpeed -- local functions defined at end
local disableSearch = false -- set to true during a setView
local selectFrames = {} -- indexed by autoscrollbox(self), unordered {selectName=Frame,selectName=Frame,etc}
local allLists = {} -- lookup table of all AutoScrollBox frames that get set up (indexed by autoscrollbox)
RematchAutoScrollBoxMixin = {}
-- sets up the scrollbox definition, view, factories and data provider; this should be called only once
function RematchAutoScrollBoxMixin:Setup(definition)
-- some verification that necessities are defined
assert(type(definition)=="table","Invalid AutoScrollBox definition table")
assert(type(definition.allData)=="table","Missing allData for AutoScrollBox")
assert(type(definition.normalTemplate)=="string" and type(definition.normalFill)=="function","Invalid AutoScrollBox normal template")
assert(type(definition.normalHeight)=="number" and definition.normalHeight>0,"Invalid AutoScrollBox normal height")
-- if already setup, leave
if self.isSetup then
return
end
Mixin(self,definition) -- absorbing attributes of the definition to self
self.displayData = {} -- managed in this module, list of data to display (can be subset of allData)
if self.allButton then
self.allButton:SetScript("OnClick",function()
self:ToggleAllHeaders()
PlaySound(C.SOUND_HEADER_CLICK)
end)
end
-- and if searchBox defined, this AutoScrollBox is going to take over its OnTextChanged
if self.searchBox then
self.searchMask = ""
self.searchBox:SetScript("OnTextChanged",function(editBox) -- note editBox rather than self; keeping self reference to AutoScrollBox
local text = editBox:GetText()
local newMask
if text and text:match(rematch.constants.PET_ID_PATTERN) then
newMask = text -- special case if search is a petID (Battle-0-000000000000), don't desensitize it
else
newMask = rematch.utils:DesensitizeText(text)
end
if newMask~=self.searchMask then
self.searchMask = newMask
self:Update()
end
end)
end
-- if selects defined, this AutoScrollBox will have at least one selectFrames
if type(definition.selects)=="table" then
for name,selectDefinition in pairs(definition.selects) do
self:SetupSelect(name,selectDefinition)
end
end
if type(definition.onUpdate)=="function" then
self.ScrollBox:RegisterCallback("OnUpdate",function(...) definition.onUpdate(self,...) end)
end
if type(definition.onScroll)=="function" then
self.ScrollBox:RegisterCallback("OnScroll",function(...) definition.onScroll(self,...) end)
end
-- separate OnScroll callback to disable ScrollToTopButton/ScrollToBottomButton
self.ScrollBox:RegisterCallback("OnScroll",function(...)
local _,percent,_,extent = ...
if extent==0 then -- can't scroll, disable top and bottom
self.ScrollToTopButton:SetToDisable()
self.ScrollToBottomButton:SetToDisable()
elseif percent<0.00000001 then -- at top, disable top, enable bottom
self.ScrollToTopButton:SetToDisable()
self.ScrollToBottomButton:SetToEnable()
elseif percent>0.99999999 then -- at bottom, enable top, disable bottom
self.ScrollToTopButton:SetToEnable()
self.ScrollToBottomButton:SetToDisable()
else -- somewhere in middle of scrollable list, enable top and bottom
self.ScrollToTopButton:SetToEnable()
self.ScrollToBottomButton:SetToEnable()
end
end)
self.ScrollBox:RegisterCallback("OnUpdate",function(...)
updateListSpeed(self)
end)
setView(self) -- create view (local so it can't be called from outside here or SetCompactMode)
self.ScrollBox:SetPanExtent(self.isCompact and self.compactHeight or self.normalHeight)
allLists[self] = true
updateListSpeed(self)
self.isSetup = true
end
-- updates the scrollbox to reflect both the contents of allData and the header/search effect if any
function RematchAutoScrollBoxMixin:Update()
-- first clear the CaptureButton; we don't know how far the list extends yet
self.CaptureButton:ClearAllPoints()
self.CaptureButton:Hide()
-- update the displayData that will be fed into the data provider
populateList(self)
-- update the data provider from displayData
self.dataProvider = CreateDataProvider()
for _,data in ipairs(self.displayData) do
self.dataProvider:Insert(data)
end
self.ScrollBox:SetDataProvider(self.dataProvider,ScrollBoxConstants.RetainScrollPosition)
-- update allButton if one defined and headers used
if self.allButton and self.expandedHeaders then
self.allButton:SetEnabled(not self:IsSearching() and not self:IsHeadersLocked())
self.allButton:SetExpanded(self:IsAnyExpanded())
end
if self.searchBox then
self.searchBox:SetEnabled(not self:IsHeadersLocked())
end
-- if list doesn't take up enough space to be scrollable, there's empty space; fill with CaptureButton
if not self:IsScrollable() then
local lastListButton = self:GetLastListButton()
if lastListButton then -- there's at least one button, anchor topleft to it
self.CaptureButton:SetPoint("TOPLEFT",lastListButton,"BOTTOMLEFT")
else -- if there is no lastListButton, make capture extend whole autoscrollbox area
self.CaptureButton:SetPoint("TOPLEFT",self,"TOPLEFT",5,-4)
end
self.CaptureButton:SetPoint("BOTTOMRIGHT",self,"BOTTOMRIGHT",-29,4)
self.CaptureButton:Show()
end
end
-- if any list has a speed set, all lists are set to the same speed
function RematchAutoScrollBoxMixin:SetSpeed(speed)
if speed==C.MOUSE_SPEED_SLOW or speed==C.MOUSE_SPEED_NORMAL or speed==C.MOUSE_SPEED_MEDIUM or speed==C.MOUSE_SPEED_FAST then
settings.MousewheelSpeed = speed
else
settings.MousewheelSpeed = C.MOUSE_SPEED_NORMAL
end
for list in pairs(allLists) do
updateListSpeed(list)
end
end
-- returns true if the list has enough content that it's scrollable
function RematchAutoScrollBoxMixin:IsScrollable()
return self.ScrollBar:HasScrollableExtent()
end
-- for lists that aren't scrollable, get the last displayed listbutton (for drag/drop interactions with CaptureButton)
-- (when the list is full and scrollable this is the last defined button that's visible--but may be off edge of bottom)
function RematchAutoScrollBoxMixin:GetLastListButton()
local frames = self.ScrollBox:GetFrames()
for i=#frames,1,-1 do
if frames[i]:IsVisible() then
return frames[i]
end
end
end
-- refreshes the contents of the visible buttons; this is intended for select/unselecting and
-- other minor interactions with the scrollbox. if any data may change or scrolling happen, use
-- autoscrollbox:Update() instead
function RematchAutoScrollBoxMixin:Refresh()
for _,frame in ipairs(self.ScrollBox:GetFrames()) do
local data = frame.data
if not frame:IsVisible() or not data then
-- do nothing, possibly an empty list
elseif self.headerCriteria and self.headerCriteria(self,data) then
self.headerFill(frame,data)
handleSelect(self,frame)
elseif self.placeholderCriteria and self.placeholderCriteria(self,data) then
self.placeholderFill(frame,data)
handleSelect(self,frame)
elseif self.isCompact then
self.compactFill(frame,data)
handleSelect(self,frame)
else
self.normalFill(frame,data)
handleSelect(self,frame)
end
end
if self.allButton and self.expandedHeaders then
self.allButton:SetEnabled(not self:IsSearching() and not self:IsHeadersLocked())
end
if self.searchBox then
self.searchBox:SetEnabled(not self:IsHeadersLocked())
end
end
-- locks headers so they can't be expanded/collapsed
function RematchAutoScrollBoxMixin:LockHeaders()
self.headersLocked = true
self:Refresh()
end
-- unlocks headers so they can be expanded/collapsed
function RematchAutoScrollBoxMixin:UnlockHeaders()
self.headersLocked = false
self:Refresh()
end
-- returns current lock state of headers
function RematchAutoScrollBoxMixin:IsHeadersLocked()
return self.headersLocked
end
-- changes to/from normal and compact modes (rebuilds view too)
function RematchAutoScrollBoxMixin:SetCompactMode(isCompact)
local newCompact = isCompact and true or false
if self:GetCompactMode()~=newCompact then -- only need to change if value is different
self.isCompact = newCompact
setView(self)
self.ScrollBox:SetPanExtent(self.isCompact and self.compactHeight or self.normalHeight)
end
end
-- gets the current compact mode
function RematchAutoScrollBoxMixin:GetCompactMode()
return self.isCompact and true or false
end
-- this expands a header and scrolls it to the top, clearing search if any happening;
-- the calling function should also clear the searchbox if it has instructions/other stuff
-- collapseOthers will collapse all other headers
function RematchAutoScrollBoxMixin:ExpandHeader(data,collapseOthers)
if not self:IsHeadersLocked() and self.expandedHeaders and self.headerCriteria(self,data) then
-- clear search
self.searchMask = ""
self.searchBox:SetText("")
-- collapse all headers if chosen
if collapseOthers then
wipe(self.expandedHeaders)
end
-- ensure this header expanded
self.expandedHeaders[data] = true
-- update and scroll to top
self:Update()
if self.expandedHeaders[data] then
self:ScrollDataToTop(data)
end
end
end
-- toggles the given header and updates the list if expandedHeaders was defined
function RematchAutoScrollBoxMixin:ToggleHeader(data)
if self.expandedHeaders and not self:IsSearching() and not self:IsHeadersLocked() and self.headerCriteria(self,data) then
self.expandedHeaders[data] = not self.expandedHeaders[data] or nil -- toggle true/nil
self:Update()
if self.expandedHeaders[data] then -- if expanding a header, scroll the header and contents into view
self:ScrollHeaderIntoView(data)
end
end
end
-- collapses or expands all headers in self.expandedHeaders and updates the list
function RematchAutoScrollBoxMixin:ToggleAllHeaders()
if self.expandedHeaders and not self:IsSearching() and not self:IsHeadersLocked() then
if self:IsAnyExpanded() then
wipe(self.expandedHeaders)
else
for _,data in ipairs(self.allData) do
if self.headerCriteria(self,data) then
self.expandedHeaders[data] = true
end
end
end
self:Update()
end
end
-- collapses all headers
function RematchAutoScrollBoxMixin:CollapseAllHeaders(noUpdate)
if self.expandedHeaders and next(self.expandedHeaders) then
wipe(self.expandedHeaders)
if not noRefresh then
self:Update()
end
end
end
-- collapses all headers except for the one that contains data (expands if not expanded), and keeps data in view
function RematchAutoScrollBoxMixin:CollapseAllButData(data)
if self.expandedHeaders then
-- first verify if anything needs done
local expandedCount = 0
local expandedData
if rematch.utils:GetSize(self.expandedHeaders)==1 then -- if only one header expanded
for _,atData in ipairs(self.displayData) do
if atData==data then -- and data is within that expanded header
return -- then our work is done, leave
end
end
end
wipe(self.expandedHeaders) -- close all headers
-- find the one belonging to data (it may be data itself)
local atHeader
for _,atData in ipairs(self.allData) do
if self.headerCriteria(self,atData) then
atHeader = atData
end
if atData==data then
break
end
end
if atHeader then
self.expandedHeaders[atHeader] = true
end
end
-- update to get headers collapsed
self:Update()
-- then scroll data back into view if needed
self:ScrollDataIntoView(data)
end
-- returns true if the given data is an expanded header
function RematchAutoScrollBoxMixin:IsHeaderExpanded(data)
return self.expandedHeaders and self.expandedHeaders[data] or false
end
-- returns true if at least one header is expanded
function RematchAutoScrollBoxMixin:IsAnyExpanded()
return next(self.expandedHeaders) and true or false
end
-- returns true if a search is in progress (non-empty mask and there's a searchHit function)
function RematchAutoScrollBoxMixin:IsSearching()
return self.searchMask and self.searchHit and self.searchMask~="" and not disableSearch
end
-- puts the named select onto the button that contains data, if any (or clears if none or it's not in view)
-- when the list is going to be updated by the calling function already, noRefresh = true to skip the refresh
function RematchAutoScrollBoxMixin:Select(name,data,noRefresh)
local selectFrame = selectFrames[self] and selectFrames[self][name]
if selectFrame and selectFrame.data~=data then
selectFrame.data = data
if not noRefresh then
self:Refresh()
end
end
end
-- returns the data currently selected for the named select
function RematchAutoScrollBoxMixin:GetSelected(name)
local selectFrame = selectFrames[self] and selectFrames[self][name]
if selectFrame then
return selectFrame.data
end
end
-- creates or updates a selectFrame of the given properties, to use with autoscrollbox:Select(name,data)
function RematchAutoScrollBoxMixin:SetupSelect(name,def)
assert(type(name)=="string" and type(def)=="table","Invalid AutoScrollBox SetupSelect")
if not selectFrames[self] then
selectFrames[self] = {}
end
if name and not selectFrames[self][name] then
selectFrames[self][name] = CreateFrame("Frame",nil,self,def.tint and "RematchAutoScrollBoxTintTemplate" or "RematchAutoScrollBoxSelectTemplate")
end
local selectFrame = selectFrames[self][name]
-- color is {red,greem,blue[,alpha]}
if def.tint then
selectFrame.Texture:SetColorTexture(def.color[1] or 1,def.color[2] or 1,def.color[3] or 1,def.color[4] or 1)
elseif type(def.color)=="table" then
for _,texture in ipairs(selectFrame.Textures) do
texture:SetVertexColor(def.color[1] or 1,def.color[2] or 1,def.color[3] or 1,def.color[4] or 1)
end
end
-- if defined, parentKey is the parentKey of the listbutton the select frame is anchored to
selectFrame.parentKey = def.parentKey
if not def.tint then
-- if padding is a single number, nudge corners in by that amount
if type(def.padding)=="number" then
selectFrame.TopLeft:SetPoint("TOPLEFT",def.padding,-def.padding)
selectFrame.TopRight:SetPoint("TOPRIGHT",-def.padding,-def.padding)
selectFrame.BottomLeft:SetPoint("BOTTOMLEFT",def.padding,def.padding)
selectFrame.BottomRight:SetPoint("BOTTOMRIGHT",-def.padding,def.padding)
end
-- if def.padding is {left,right,top,bottom}, nudge corners in by those amounts
if type(def.padding)=="table" then
selectFrame.TopLeft:SetPoint("TOPLEFT",def.padding[1] or 0,-def.padding[3] or 0)
selectFrame.TopRight:SetPoint("TOPRIGHT",-def.padding[2] or 0,-def.padding[3] or 0)
selectFrame.BottomLeft:SetPoint("BOTTOMLEFT",def.padding[1] or 0,def.padding[4] or 0)
selectFrame.BottomRight:SetPoint("BOTTOMRIGHT",-def.padding[2] or 0,def.padding[4] or 0)
end
-- drawLayer can include an optional textureSubLevel
if type(def.drawLayer)=="string" then
for _,texture in ipairs(selectFrame.Textures) do
texture:SetDrawLayer(def.drawLayer,def.textureSubLevel)
end
end
-- alphaMode is commonly "ADD", "BLEND"
if type(def.alphaMode)=="string" then
for _,texture in ipairs(selectFrame.Textures) do
texture:SetBlendMode(def.alphaMode)
end
end
end
end
-- scrolls to the top of the list
function RematchAutoScrollBoxMixin:ScrollToTop()
self.ScrollBox:ScrollToOffset(0,floor(self:GetHeight()+0.5))
end
-- scrolls the list so that the given data is at top
function RematchAutoScrollBoxMixin:ScrollDataToTop(data)
local height = 0
for index,atData in ipairs(self.displayData) do
if atData==data then
self.ScrollBox:ScrollToOffset(height,floor(self:GetHeight()+0.5))
return
end
height = height + getDataHeight(self,atData)
end
end
-- if a header is not within the frame, or its contents extend beyond the bottom of the frame, scroll the header
-- up until contents are in view or the header is at the top of the frame
function RematchAutoScrollBoxMixin:ScrollHeaderIntoView(data)
if not data or not self.headerCriteria or not self.headerCriteria(self,data) then
return -- data isn't a header, get out of here
end
local height = 0 -- running total offset from top
local frameHeight = floor(self:GetHeight() + 0.5)
-- define top/bottom boundry
local topOffset = floor(self.ScrollBox:GetDerivedScrollOffset() + 0.5)
local bottomOffset = topOffset + frameHeight
-- find header offset and the height of the header and its contents
local headerOffset
local contentHeight
for index,atData in ipairs(self.displayData) do
if headerOffset and contentHeight then -- we have everything needed, stop looking
break
elseif atData==data then
headerOffset = height
elseif headerOffset and self.headerCriteria(self,atData) then -- found next header after one we're scrolling
contentHeight = height - headerOffset
end
height = height + getDataHeight(self,atData)
end
-- couldn't find header, leave with no change
if not headerOffset then
return
end
-- if reached end of list without running into another header to define contentHeight, then content is the rest
if not contentHeight then
contentHeight = height - headerOffset
end
-- if header and its content are within the top/bottom offsets of the frame, everything is good, leave
if headerOffset >= topOffset and headerOffset+contentHeight <= bottomOffset then
return
end
-- including half of next header in visible content so it's obvious that the current header's content has ended
contentHeight = contentHeight + self.headerHeight/2
-- if header and its contents span more than the height of the frame, scroll header to top
if contentHeight >= frameHeight then
self.ScrollBox:ScrollToOffset(headerOffset,frameHeight)
else -- if header and contents can fit but don't yet, scroll up the difference
local difference = headerOffset+contentHeight - bottomOffset + 8
if difference ~= 0 then -- was > 0 before when scrolling up to be in view (may revisit when scrolling down into view)
self.ScrollBox:ScrollToOffset(topOffset+difference,frameHeight)
end
end
end
-- if the data is not already in view, scroll it into view, potentially expanding the header it's in if so
function RematchAutoScrollBoxMixin:ScrollDataIntoView(data)
-- first if this is a header, use ScrollHeaderIntoView instead (so if expanded it will bring contents up too)
if self.headerCriteria and self.headerCriteria(self,data) then
self:ScrollHeaderIntoView(data)
return
end
-- confirm data exists, capturing which header it's in on the way
local found = false
local headerID -- watching these to keep track of which header it's in
for _,atData in ipairs(self.allData) do
if self.headerCriteria and self.headerCriteria(self,atData) then
headerID = atData
elseif data==atData then
found = true
break
end
end
if not found then
return -- didn't find the data, leave
end
-- if headers are used and data is within a header not expanded, it needs expanded
if headerID and not self.expandedHeaders[headerID] then
self:ToggleHeader(headerID) -- this will scroll header into view also
end
-- check if data is visible
if self:IsDataVisible(data) then
return -- data is visible, leave
end
-- here, data is in displayData but not visible, scroll to it
local height = 0
for index,atData in ipairs(self.displayData) do
if atData==data then
-- centering data: scrolling to height (offset of data) - half frame height + data height
local frameHeight = floor(self:GetHeight()+0.5)
self.ScrollBox:ScrollToOffset(height-frameHeight/2+getDataHeight(self,data),frameHeight)
return -- scrolled data to center, leave
end
height = height + getDataHeight(self,atData)
end
end
-- flashes a list button by moving the Bling frame
function RematchAutoScrollBoxMixin:BlingData(data)
if not self:IsDataVisible(data) then
self:ScrollDataIntoView(data) -- first scroll it into view if it's not visible
end
-- then loop over visible buttons to find the one to bling
for _,frame in pairs(self.ScrollBox:GetFrames()) do
if frame.data==data and frame:IsVisible() then
self.Bling:ClearAllPoints()
self.Bling:SetParent(frame)
local anchorTo = frame.Back
if not anchorTo or not anchorTo:IsVisible() then
anchorTo = frame
end
self.Bling:SetPoint("TOPLEFT",anchorTo,"TOPLEFT",1,-1)
self.Bling:SetPoint("BOTTOMRIGHT",anchorTo,"BOTTOMRIGHT",-1,1)
self.Bling:Show()
return
end
end
end
-- returns true if the data is in the visible list (one of the displayed list buttons contains data)
function RematchAutoScrollBoxMixin:IsDataVisible(data)
for _,frame in pairs(self.ScrollBox:GetFrames()) do
if frame.data==data and frame:IsVisible() then
return true -- data is visible
end
end
return false -- data is not visible
end
-- alternate form of IsDataInVisible that also returns data about where it is
function RematchAutoScrollBoxMixin:IsDataInView(data)
local height = 0 -- running total offset from top
local frameHeight = floor(self:GetHeight() + 0.5)
-- define top/bottom boundry
local topOffset = floor(self.ScrollBox:GetDerivedScrollOffset() + 0.5)
local bottomOffset = topOffset + frameHeight
for index,atData in ipairs(self.displayData) do
if atData==data then
break
end
height = height + getDataHeight(self,atData)
end
-- here, height is the top offset of data; return true if height is between topOffset and bottomOffset
if height >= topOffset and (height + getDataHeight(self,data)) <= bottomOffset then
return true, height, topOffset -- return true, offset of data, offset of top of frame
else
return false, height, topOffset
end
end
-- returns the frame data is in, if it's visible
function RematchAutoScrollBoxMixin:GetDataFrame(data)
for _,frame in pairs(self.ScrollBox:GetFrames()) do
if frame.data==data and frame:IsVisible() then
return frame
end
end
end
--[[ scroll to end button mixin ]]
RematchAutoScrollBoxScrollToEndMixin = {}
function RematchAutoScrollBoxScrollToEndMixin:OnMouseDown()
if not self.isDisabled then
self.Texture:SetTexCoord(0,1,0.5,1)
self.Highlight:SetTexCoord(0,1,0.5,1)
end
end
function RematchAutoScrollBoxScrollToEndMixin:OnMouseUp()
if not self.isDisabled then
self.Texture:SetTexCoord(0,1,0,0.5)
self.Highlight:SetTexCoord(0,1,0,0.5)
end
end
-- self.scrollMethod should be "ScrollToBegin" or "ScrollToEnd", the ScrollBox method to scroll to top/bottom
function RematchAutoScrollBoxScrollToEndMixin:OnClick(button)
local scrollBox = self:GetParent().ScrollBox
scrollBox[self.scrollMethod](scrollBox)
PlaySound(SOUNDKIT.IG_CHAT_BOTTOM)
end
-- to minimize work since this can be called many times while scrolling, this only does work if button needs enabled
function RematchAutoScrollBoxScrollToEndMixin:SetToEnable()
if self.isDisabled then
self.isDisabled = false
self:GetScript("OnMouseUp")(self)
self:SetAlpha(1)
self:GetParent().ScrollBar[self.stepButton]:SetAlpha(1) -- un-dims the scrollbar's built-in Back/Forward button
self.Highlight:SetAlpha(0.3)
end
end
-- to minimize work since this can be called many times while scrolling, this only does work if button needs disabled
function RematchAutoScrollBoxScrollToEndMixin:SetToDisable()
if not self.isDisabled then
self:GetScript("OnMouseUp")(self)
self:SetAlpha(0.5)
self:GetParent().ScrollBar[self.stepButton]:SetAlpha(0.5) -- dims the scrollbar's built-in Back/Forward button
self.Highlight:SetAlpha(0)
self.isDisabled = true
end
end
--[[ local stuff ]]
-- this fills displayData with data from allData. for simple lists with no headers or search, it's a direct copy;
-- when headers or searching is involved, displayData is likely a subset of allData
function populateList(self)
wipe(self.displayData)
local skip = false
local hasHeaders = self.expandedHeaders and self.headerTemplate and self.headerCriteria and true or false
local hasSearch = self.searchHit and self:IsSearching()
if hasHeaders and hasSearch then
wipe(self.expandedHeaders) -- searching collapses headers (otherwise things get weird)
end
for index,data in ipairs(self.allData) do
local add = false
if not hasHeaders and not hasSearch then
-- no headers no search, just list everything
add = true
elseif not hasHeaders and hasSearch then
-- no headers but search happening, list all search hits
add = self.searchHit(self,self.searchMask,data) and true or false
elseif hasHeaders and not hasSearch then
-- headers are used but no search, list all headers and expanded contents
if self.headerCriteria(self,data) then
add = true
skip = not self.expandedHeaders[data]
elseif not skip then
add = true
end
elseif hasHeaders and hasSearch then
-- both headers and search used; take a deep breath
-- 1. always add headers
-- 2. if adding a header right after another while searching, deleting previous header if it doesn't search hit
-- 3. if header search hit, add everything after until next header
-- 4. when done, if last item is a header, delete it if it's not a search hit
if self.headerCriteria(self,data) then
add = true -- always add headers initially (this may be removed)
if index>1 then
local lastData = self.displayData[#self.displayData]
if self.headerCriteria(self,lastData) and not self.searchHit(self,self.searchMask,lastData) then
tremove(self.displayData,#self.displayData) -- if we just added a header (that wasn't a search hit) and adding a new one, delete prior
end
end
skip = not self.searchHit(self,self.searchMask,data)
elseif not skip then -- header was a search hit, list everything under this header
add = true
elseif self.searchHit(self,self.searchMask,data) then -- non-header search hit
add = true
end
end
if add then
tinsert(self.displayData,data)
end
end
-- finally, if headers and search used, drop any trailing headers that were not a search hit
if hasHeaders and hasSearch then
local lastData = self.displayData[#self.displayData]
if self.headerCriteria(self,lastData) and not self.searchHit(self,self.searchMask,lastData) then
tremove(self.displayData,#self.displayData)
end
end
end
-- creates and sets the view, only called in initial Setup and SetCompactMode
function setView(self)
if self.view then
self.view:Flush() -- if old view exists, release its stuff
end
-- isCompact is a local value here because once a view is created, switching to/from compact mode without
-- rebuilding the view has undesirable effects (script ran too long lockup)
local isCompact = self.isCompact and type(self.compactFill)=="function" and type(self.compactTemplate)=="string"
self.view = CreateScrollBoxListLinearView()
self.view:SetElementFactory(function(factory,data)
if self.headerCriteria and self.headerFill and self.headerTemplate and (self.headerCriteria(self,data) or data=="PrimeHeader") then
factory(self.headerTemplate,function(button,data)
button.data = data
self.headerFill(button,data)
handleSelect(self,button)
end)
elseif self.placeholderCriteria and self.placeholderFill and self.placeholderTemplate and (self.placeholderCriteria(self,data) or data=="PrimePlaceholder") then
factory(self.placeholderTemplate,function(button,data)
button.data = data
self.placeholderFill(button,data)
handleSelect(self,button)
end)
elseif isCompact then
factory(self.compactTemplate,function(button,data)
button.data = data
self.compactFill(button,data)
handleSelect(self,button)
end)
else
factory(self.normalTemplate,function(button,data)
button.data = data
self.normalFill(button,data)
handleSelect(self,button)
end)
end
end)
ScrollUtil.InitScrollBoxListWithScrollBar(self.ScrollBox,self.ScrollBar,self.view)
-- when mixed templates are used, the client can freeze up when a different-sized one is brought in later,
-- such as 20 collapsed 24px-tall headers expanding to show 40px-tall normal list buttons amid the headers.
-- this will "prime the pump" of the header and non-header versions (compact and normal are mutually exclusive)
-- by creating and updating a list of 2 elements--a header and non header--which seems to fix the issue
if self.headerTemplate and self.headerCriteria then
disableSearch = true -- these two elements probably won't match a search and they need to be added
self.dataProvider = CreateDataProvider()
self.dataProvider:Insert("PrimeHeader")
self.dataProvider:Insert("PrimeNonHeader")
self.dataProvider:Insert("PrimePlaceholder")
self.ScrollBox:SetDataProvider(self.dataProvider,ScrollBoxConstants.RetainScrollPosition)
self.dataProvider:RemoveIndexRange(1,2) -- view should be safe now, removing two elements from data provider
disableSearch = false
end
end
-- called in fills to claim a selectFrame if the selectFrame's data matches the data being filled
function handleSelect(self,button)
if selectFrames[self] then
for _,selectFrame in pairs(selectFrames[self]) do
if button.data and button.data==selectFrame.data then -- if this button should be selected, claim the selectFrame
selectFrame:SetParent(button)
selectFrame:SetPoint("TOPLEFT",selectFrame.parentKey and button[selectFrame.parentKey] or button,"TOPLEFT")
selectFrame:SetPoint("BOTTOMRIGHT",selectFrame.parentKey and button[selectFrame.parentKey] or button,"BOTTOMRIGHT")
selectFrame:Show()
selectFrame.parent = button
elseif selectFrame.parent==button or not selectFrame.parent then -- otherwise if selectframe is attached to this button and shouldn't be, or it's not claimed, hide it
selectFrame:Hide()
selectFrame.parent = nil
end
end
end
end
-- returns the pixel height of the button that data would exist in
function getDataHeight(self,data)
if self.headerCriteria and self.headerCriteria(self,data) then
return self.headerHeight
elseif self.placeholderCriteria and self.placeholderCriteria(self,data) then
return self.placeholderHeight
elseif self.isCompact then
return self.compactHeight
else
return self.normalHeight
end
end
-- updates the list (autoscrollbox) wheelPanScalar to adjust scroll speed to settings.MousewheelSpeed
function updateListSpeed(list)
if allLists[list] then
local speed = settings.MousewheelSpeed
local scrollBox = list.ScrollBox
local wheelPanScalar
if speed==C.MOUSE_SPEED_SLOW then
wheelPanScalar = 1
elseif speed==C.MOUSE_SPEED_NORMAL then
wheelPanScalar = 2
else -- the other speeds are based on height of the list (autoscrollbox)
local panExtent = max(scrollBox:GetPanExtent() or 0,1)
local height = list:GetHeight()-panExtent
if speed==C.MOUSE_SPEED_MEDIUM then
wheelPanScalar = (height/2-(height/2)%panExtent)/panExtent
elseif speed==C.MOUSE_SPEED_FAST then
wheelPanScalar = (height-height%panExtent)/panExtent
end
end
scrollBox.wheelPanScalar = (wheelPanScalar and wheelPanScalar>0) and wheelPanScalar or 2
end
end