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.

732 lines
24 KiB

---@type detailsframework
local detailsFramework = _G["DetailsFramework"]
if (not detailsFramework or not DetailsFrameworkCanLoad) then
return
end
local CreateFrame = CreateFrame
local GetSpellInfo = GetSpellInfo
local GameTooltip = GameTooltip
local unpack = unpack
---mixin to use with DetailsFramework:Mixin(table, detailsFramework.SortFunctions)
---add methods to be used on scrollframes
---@class df_scrollboxmixin
detailsFramework.ScrollBoxFunctions = {
--set a function to run right before the refresh function (scroll:Refresh())
--this function receives the same parameters as the refresh function
SetPreRefreshFunction = function(self, func)
self.pre_refresh_func = func
end,
---refresh the scrollbox by resetting all lines created with :CreateLine(), then calling the refresh_func which was set at :CreateScrollBox()
---@param self table
---@return table
Refresh = function(self)
--hide all frames and tag as not in use
self._LinesInUse = 0
for index, frame in ipairs(self.Frames) do
if (not self.DontHideChildrenOnPreRefresh) then
frame:Hide()
end
frame._InUse = nil
end
local offset = 0
if (self.IsFauxScroll) then
self:UpdateFaux(#self.data, self.LineAmount, self.LineHeight)
offset = self:GetOffsetFaux()
end
if (self.pre_refresh_func) then
detailsFramework:Dispatch(self.pre_refresh_func, self, self.data, offset, self.LineAmount)
end
--call the refresh function
detailsFramework:Dispatch(self.refresh_func, self, self.data, offset, self.LineAmount)
--hide all frames that are not in use
for index, frame in ipairs(self.Frames) do
--the member _InUse is true when the line is used by the refresh function
--this member is set to true when the code calls scrollBox:GetLine(index)
if (not frame._InUse) then
frame:Hide()
else
frame:Show()
end
end
self:Show()
local frameName = self:GetName()
if (frameName) then
if (self.HideScrollBar) then
local scrollBar = _G[frameName .. "ScrollBar"]
if (scrollBar) then
scrollBar:Hide()
end
else
--[=[ --maybe in the future I visit this again
local scrollBar = _G[frameName .. "ScrollBar"]
local height = self:GetHeight()
local totalLinesRequired = #self.data
local linesShown = self._LinesInUse
local percent = linesShown / totalLinesRequired
local thumbHeight = height * percent
scrollBar.ThumbTexture:SetSize(12, thumbHeight)
print("thumbHeight:", thumbHeight)
--]=]
end
end
return self.Frames
end,
OnVerticalScroll = function(self, offset)
self:OnVerticalScrollFaux(offset, self.LineHeight, self.Refresh)
return true
end,
---create a line within the scrollbox
---@param self table is the scrollbox
---@param func function|nil function to create the line object, this function will receive the line index as argument and return a table with the line object
---@return table line object (table)
CreateLine = function(self, func)
if (not func) then
func = self.CreateLineFunc
end
local okay, newLine = xpcall(func, geterrorhandler(), self, #self.Frames+1)
if (okay) then
if (not newLine) then
error("ScrollFrame:CreateLine() function did not returned a line, use: 'return line'")
end
table.insert(self.Frames, newLine)
newLine.Index = #self.Frames
return newLine
end
return newLine
end,
CreateLines = function(self, callback, lineAmount)
for i = 1, lineAmount do
self:CreateLine(callback)
end
end,
GetLine = function(self, lineIndex)
local line = self.Frames[lineIndex]
if (line) then
line._InUse = true
end
self._LinesInUse = self._LinesInUse + 1
return line
end,
SetData = function(self, data)
self.data = data
if (self.OnSetData) then
detailsFramework:CoreDispatch((self:GetName() or "ScrollBox") .. ":OnSetData()", self.OnSetData, self, self.data)
end
end,
GetData = function(self)
return self.data
end,
GetFrames = function(self)
return self.Frames
end,
GetLines = function(self) --alias of GetFrames
return self.Frames
end,
GetNumFramesCreated = function(self)
return #self.Frames
end,
GetNumFramesShown = function(self)
return self.LineAmount
end,
SetNumFramesShown = function(self, newAmount)
--hide frames which won't be used
if (newAmount < #self.Frames) then
for i = newAmount+1, #self.Frames do
self.Frames[i]:Hide()
end
end
--set the new amount
self.LineAmount = newAmount
end,
SetFramesHeight = function(self, height)
self.LineHeight = height
self:OnSizeChanged()
self:Refresh()
end,
OnSizeChanged = function(self)
if (self.ReajustNumFrames) then
--how many lines the scroll can show
local amountOfFramesToShow = math.floor(self:GetHeight() / self.LineHeight)
--how many lines the scroll already have
local totalFramesCreated = self:GetNumFramesCreated()
--how many lines are current shown
local totalFramesShown = self:GetNumFramesShown()
--the amount of frames increased
if (amountOfFramesToShow > totalFramesShown) then
for i = totalFramesShown+1, amountOfFramesToShow do
--check if need to create a new line
if (i > totalFramesCreated) then
self:CreateLine(self.CreateLineFunc)
end
end
--the amount of frames decreased
elseif (amountOfFramesToShow < totalFramesShown) then
--hide all frames above the new amount to show
for i = totalFramesCreated, amountOfFramesToShow, -1 do
if (self.Frames[i]) then
self.Frames[i]:Hide()
end
end
end
--set the new amount of frames
self:SetNumFramesShown(amountOfFramesToShow)
--refresh lines
self:Refresh()
end
end,
--moved functions from blizzard faux scroll that are called from insecure code environment
--this reduces the amount of taints while using the faux scroll frame
GetOffsetFaux = function(self)
return self.offset or 0
end,
OnVerticalScrollFaux = function(self, value, lineHeight, updateFunction)
local scrollbar = self:GetChildFramesFaux()
scrollbar:SetValue(value)
self.offset = math.floor((value / lineHeight) + 0.5)
if (updateFunction) then
updateFunction(self)
end
end,
GetChildFramesFaux = function(frame)
local frameName = frame:GetName();
if frameName then
return _G[ frameName.."ScrollBar" ], _G[ frameName.."ScrollChildFrame" ], _G[ frameName.."ScrollBarScrollUpButton" ], _G[ frameName.."ScrollBarScrollDownButton" ];
else
return frame.ScrollBar, frame.ScrollChildFrame, frame.ScrollBar.ScrollUpButton, frame.ScrollBar.ScrollDownButton;
end
end,
UpdateFaux = function(frame, numItems, numToDisplay, buttonHeight, button, smallWidth, bigWidth, highlightFrame, smallHighlightWidth, bigHighlightWidth, alwaysShowScrollBar)
local scrollBar, scrollChildFrame, scrollUpButton, scrollDownButton = frame:GetChildFramesFaux();
-- If more than one screen full of items then show the scrollbar
local showScrollBar;
if ( numItems > numToDisplay or alwaysShowScrollBar ) then
frame:Show();
showScrollBar = 1;
else
scrollBar:SetValue(0);
frame:Hide();
end
if ( frame:IsShown() ) then
local scrollFrameHeight = 0;
local scrollChildHeight = 0;
if ( numItems > 0 ) then
scrollFrameHeight = (numItems - numToDisplay) * buttonHeight;
scrollChildHeight = numItems * buttonHeight;
if ( scrollFrameHeight < 0 ) then
scrollFrameHeight = 0;
end
scrollChildFrame:Show();
else
scrollChildFrame:Hide();
end
local maxRange = (numItems - numToDisplay) * buttonHeight;
if (maxRange < 0) then
maxRange = 0;
end
scrollBar:SetMinMaxValues(0, maxRange);
scrollBar:SetValueStep(buttonHeight);
scrollBar:SetStepsPerPage(numToDisplay-1);
scrollChildFrame:SetHeight(scrollChildHeight);
-- Arrow button handling
if ( scrollBar:GetValue() == 0 ) then
scrollUpButton:Disable();
else
scrollUpButton:Enable();
end
if ((scrollBar:GetValue() - scrollFrameHeight) == 0) then
scrollDownButton:Disable();
else
scrollDownButton:Enable();
end
-- Shrink because scrollbar is shown
if ( highlightFrame ) then
highlightFrame:SetWidth(smallHighlightWidth);
end
if ( button ) then
for i=1, numToDisplay do
_G[button..i]:SetWidth(smallWidth);
end
end
else
-- Widen because scrollbar is hidden
if ( highlightFrame ) then
highlightFrame:SetWidth(bigHighlightWidth);
end
if ( button ) then
for i=1, numToDisplay do
_G[button..i]:SetWidth(bigWidth);
end
end
end
return showScrollBar;
end,
}
---@class df_gridscrollbox_options : table
---@field width number?
---@field height number?
---@field line_amount number?
---@field line_height number?
---@field columns_per_line number?
---@field auto_amount boolean?
---@field no_scroll boolean?
---@field vertical_padding number?
---@field no_backdrop boolean?
---@type df_gridscrollbox_options
local grid_scrollbox_options = {
width = 600,
height = 400,
line_amount = 10,
line_height = 30,
columns_per_line = 4,
no_scroll = false,
vertical_padding = 1,
no_backdrop = false,
}
---@class df_gridscrollbox : df_scrollbox
---create a scrollbox with a grid layout
---@param parent frame
---@param name string
---@param refreshFunc function
---@param data table
---@param createColumnFrameFunc function
---@param options df_gridscrollbox_options?
---@return unknown
function detailsFramework:CreateGridScrollBox(parent, name, refreshFunc, data, createColumnFrameFunc, options)
options = options or {}
--check values passed, get defaults and cast values due to the scrollbox require some values to be numbers
local width = type(options.width) == "number" and options.width or grid_scrollbox_options.width
---@cast width number
local height = type(options.height) == "number" and options.height or grid_scrollbox_options.height
---@cast height number
local lineAmount = type(options.line_amount) == "number" and options.line_amount or grid_scrollbox_options.line_amount
---@cast lineAmount number
local lineHeight = type(options.line_height) == "number" and options.line_height or grid_scrollbox_options.line_height
---@cast lineHeight number
local columnsPerLine = options.columns_per_line or grid_scrollbox_options.columns_per_line
local autoAmount = options.auto_amount
local noScroll = options.no_scroll
local noBackdrop = options.no_backdrop
local verticalPadding = options.vertical_padding or grid_scrollbox_options.vertical_padding
local createLineFunc = function(scrollBox, lineIndex)
local line = CreateFrame("frame", "$parentLine" .. lineIndex, scrollBox)
line:SetSize(width, lineHeight)
line:SetPoint("top", scrollBox, "top", 0, -((lineIndex-1) * (lineHeight + verticalPadding)))
line.optionFrames = {}
for columnIndex = 1, columnsPerLine do
--dispatch payload: line, lineIndex, columnIndex
local optionFrame = createColumnFrameFunc(line, lineIndex, columnIndex)
line.optionFrames[columnIndex] = optionFrame
optionFrame:SetPoint("left", line, "left", (columnIndex-1) * (width/columnsPerLine), 0)
end
return line
end
local onSetData = function(self, data)
local newData = {}
for i = 1, #data, columnsPerLine do
local thisColumnData = {}
for o = 1, columnsPerLine do
local index = i + (o-1)
local thisData = data[index]
if (thisData) then
thisColumnData[#thisColumnData+1] = thisData
end
end
newData[#newData+1] = thisColumnData
end
self.data = newData
end
local refreshGrid = function(scrollBox, thisData, offset, totalLines)
for i = 1, totalLines do
local index = i + offset
local lineData = thisData[index]
if (lineData) then
local line = scrollBox:GetLine(i)
for o = 1, columnsPerLine do
local optionFrame = line.optionFrames[o]
local data = lineData[o]
if (data) then
detailsFramework:Dispatch(refreshFunc, optionFrame, data)
optionFrame:Show()
line:Show()
else
optionFrame:Hide()
end
end
end
end
end
if (not name) then
name = "DetailsFrameworkAuraScrollBox" .. math.random(1, 9999999)
end
local scrollBox = detailsFramework:CreateScrollBox(parent, name, refreshGrid, data, width, height, lineAmount, lineHeight, createLineFunc, autoAmount, noScroll, noBackdrop)
scrollBox:CreateLines(createLineFunc, lineAmount)
detailsFramework:ReskinSlider(scrollBox)
scrollBox.OnSetData = onSetData
onSetData(scrollBox, data)
return scrollBox
end
--Need to test this and check the "same_name_spells_add(value)" on the OnEnter function
--also need to make sure this can work with any data (global, class, spec) and aura type (buff, debuff)
--aura scroll box
---@class df_aurascrollbox_options : table
---@field line_height number?
---@field line_amount number?
---@field width number?
---@field height number?
---@field vertical_padding number?
---@field show_spell_tooltip boolean
---@field remove_icon_border boolean
---@field no_scroll boolean
---@field no_backdrop boolean
---@field backdrop_onenter number[]?
---@field backdrop_onleave number[]?
---@field font_size number?
---@field title_text string?
local auraScrollDefaultSettings = {
line_height = 18,
line_amount = 18,
width = 300,
height = 500,
vertical_padding = 1,
show_spell_tooltip = false,
remove_icon_border = true,
no_scroll = false,
no_backdrop = false,
backdrop_onenter = {.8, .8, .8, 0.4},
backdrop_onleave = {.8, .8, .8, 0.2},
font_size = 12,
title_text = "",
}
---@param parent frame
---@param name string?
---@param data table? --can be set later with :SetData()
---@param onAuraRemoveCallback function?
---@param options df_aurascrollbox_options?
function detailsFramework:CreateAuraScrollBox(parent, name, data, onAuraRemoveCallback, options)
--hack the construction of the options table here, as the scrollbox is created much later
options = options or {}
local scrollOptions = {}
detailsFramework.OptionsFunctions.BuildOptionsTable(scrollOptions, auraScrollDefaultSettings, options)
options = scrollOptions.options
local refreshAuraLines = function(self, data, offset, totalLines)
for i = 1, totalLines do
local index = i + offset
local auraTable = data[index]
if (auraTable) then
local line = self:GetLine(i)
local spellId, spellName, spellIcon, lowerSpellName, bAddedBySpellName = unpack(auraTable)
line.SpellID = spellId
line.SpellName = spellName
line.SpellNameLower = lowerSpellName
line.SpellIcon = spellIcon
line.Flag = bAddedBySpellName
if (bAddedBySpellName) then
line.name:SetText(spellName)
else
line.name:SetText(spellName .. " (" .. spellId .. ")")
end
line.icon:SetTexture(spellIcon)
if (options.remove_icon_border) then
line.icon:SetTexCoord(.1, .9, .1, .9)
else
line.icon:SetTexCoord(0, 1, 0, 1)
end
end
end
end
local onLeaveAuraLine = function(self)
self:SetBackdropColor(unpack(options.backdrop_onleave))
GameTooltip:Hide()
GameCooltip:Hide()
end
local onEnterAuraLine = function(line)
if (options.show_spell_tooltip and line.SpellID and GetSpellInfo(line.SpellID)) then
GameTooltip:SetOwner(line, "ANCHOR_CURSOR")
GameTooltip:SetSpellByID(line.SpellID)
GameTooltip:AddLine(" ")
GameTooltip:Show()
end
line:SetBackdropColor(unpack(options.backdrop_onenter))
local bTrackByName = line.Flag --the user entered the spell name to track the spell (and not a spellId)
local spellId = line.SpellID
if (bTrackByName) then --the user entered the spell name to track the spell
local spellsHashMap, spellsIndexTable, spellsWithSameName = detailsFramework:GetSpellCaches()
if (spellsWithSameName) then
local spellName, _, spellIcon = GetSpellInfo(spellId)
if (spellName) then
local spellNameLower = spellName:lower()
local sameNameSpells = spellsWithSameName[spellNameLower]
if (sameNameSpells) then
GameCooltip:Preset(2)
GameCooltip:SetOwner(line, "left", "right", 2, 0)
GameCooltip:SetOption("TextSize", 10)
for i, thisSpellId in ipairs(sameNameSpells) do
GameCooltip:AddLine(spellName .. " (" .. thisSpellId .. ")")
GameCooltip:AddIcon(spellIcon, 1, 1, 14, 14, .1, .9, .1, .9)
end
GameCooltip:Show()
end
end
end
else --the user entered the spellId to track the spell
GameCooltip:Preset(2)
GameCooltip:SetOwner(line, "left", "right", 2, 0)
GameCooltip:SetOption("TextSize", 10)
local spellName, _, spellIcon = GetSpellInfo(spellId)
if (spellName) then
GameCooltip:AddLine(spellName .. " (" .. spellId .. ")")
GameCooltip:AddIcon(spellIcon, 1, 1, 14, 14, .1, .9, .1, .9)
end
GameCooltip:Show()
end
end
local onClickAuraRemoveButton = function(self)
local spellId = tonumber(self:GetParent().SpellID)
if (spellId and type(spellId) == "number") then
--button > line > scrollbox
local scrollBox = self:GetParent():GetParent()
scrollBox.data_original[spellId] = nil
scrollBox.data_original["" .. (spellId or "")] = nil -- cleanup...
scrollBox:TransformAuraData()
scrollBox:Refresh()
if (onAuraRemoveCallback) then --upvalue
detailsFramework:QuickDispatch(onAuraRemoveCallback, spellId)
end
end
end
local createLineFunc = function(self, index)
local line = CreateFrame("button", "$parentLine" .. index, self, "BackdropTemplate")
local scrollBoxWidth = options.width
local lineHeight = options.line_height
local verticalPadding = options.vertical_padding
line:SetPoint("topleft", self, "topleft", 1, -((index-1) * (lineHeight + verticalPadding)) - 1)
line:SetSize(scrollBoxWidth - 2, lineHeight)
line:SetScript("OnEnter", onEnterAuraLine)
line:SetScript("OnLeave", onLeaveAuraLine)
line:SetBackdrop({bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true})
line:SetBackdropColor(unpack(options.backdrop_onleave))
local iconTexture = line:CreateTexture("$parentIcon", "overlay")
iconTexture:SetSize(lineHeight - 2, lineHeight - 2)
local spellNameFontString = line:CreateFontString("$parentName", "overlay", "GameFontNormal")
detailsFramework:SetFontSize(spellNameFontString, options.font_size)
local removeButton = CreateFrame("button", "$parentRemoveButton", line, "UIPanelCloseButton")
removeButton:SetSize(16, 16)
removeButton:SetScript("OnClick", onClickAuraRemoveButton)
removeButton:SetPoint("topright", line, "topright", 0, 0)
removeButton:GetNormalTexture():SetDesaturated(true)
iconTexture:SetPoint("left", line, "left", 2, 0)
spellNameFontString:SetPoint("left", iconTexture, "right", 3, 0)
line.icon = iconTexture
line.name = spellNameFontString
line.removebutton = removeButton
return line
end
---@class df_aurascrollbox : df_scrollbox
---@field data_original table
---@field refresh_original function
---@field TitleLabel fontstring
---@field TransformAuraData fun(self:df_aurascrollbox)
---@field GetTitleFontString fun(self:df_aurascrollbox): fontstring
data = data or {}
if (not name) then
name = "DetailsFrameworkAuraScrollBox" .. math.random(1, 9999999)
end
local auraScrollBox = detailsFramework:CreateScrollBox(parent, name, refreshAuraLines, data, options.width, options.height, options.line_amount, options.line_height)
detailsFramework:ReskinSlider(auraScrollBox)
---@cast auraScrollBox df_aurascrollbox
auraScrollBox.data_original = data
local titleLabel = auraScrollBox:CreateFontString("$parentTitleLabel", "overlay", "GameFontNormal")
titleLabel:SetPoint("bottomleft", auraScrollBox, "topleft", 0, 2)
detailsFramework:SetFontColor(titleLabel, "silver")
detailsFramework:SetFontSize(titleLabel, 10)
auraScrollBox.TitleLabel = titleLabel
function auraScrollBox:GetTitleFontString()
return self.TitleLabel
end
for i = 1, options.line_amount do
auraScrollBox:CreateLine(createLineFunc)
end
function auraScrollBox:TransformAuraData()
local newData = {}
local added = {}
for spellId, bAddedBySpellName in pairs(self.data_original) do
local spellName, _, spellIcon = GetSpellInfo(spellId)
if (spellName and not added[tonumber(spellId) or 0]) then
local lowerSpellName = spellName:lower()
table.insert(newData, {spellId, spellName, spellIcon, lowerSpellName, bAddedBySpellName})
added[tonumber(spellId) or 0] = true
end
end
table.sort(newData, function(t1, t2) return t1[4] < t2[4] end)
self.data = newData
end
auraScrollBox.SetData = function(self, data)
self.data_original = data
self.data = data
auraScrollBox:TransformAuraData()
end
auraScrollBox.GetData = function(self)
return self.data_original
end
auraScrollBox.refresh_original = auraScrollBox.Refresh
auraScrollBox.Refresh = function()
auraScrollBox:TransformAuraData()
auraScrollBox:refresh_original()
end
auraScrollBox:SetData(data)
return auraScrollBox
end
detailsFramework.CanvasScrollBoxMixin = {
}
local canvasScrollBoxDefaultOptions = {
width = 600,
height = 400,
reskin_slider = true,
}
---@class df_canvasscrollbox : scrollframe, df_optionsmixin
---@field child frame
---@param parent frame
---@param child frame?
---@param name string?
---@param options table?
---@return df_canvasscrollbox
function detailsFramework:CreateCanvasScrollBox(parent, child, name, options)
---@type df_canvasscrollbox
local canvasScrollBox = CreateFrame("scrollframe", name or ("DetailsFrameworkCanvasScroll" .. math.random(50000, 10000000)), parent, "BackdropTemplate, UIPanelScrollFrameTemplate")
detailsFramework:Mixin(canvasScrollBox, detailsFramework.CanvasScrollBoxMixin)
detailsFramework:Mixin(canvasScrollBox, detailsFramework.OptionsFunctions)
options = options or {}
canvasScrollBox:BuildOptionsTable(canvasScrollBoxDefaultOptions, options)
canvasScrollBox:SetSize(canvasScrollBox.options.width, canvasScrollBox.options.height)
if (not child) then
child = CreateFrame("frame", "$parentChild", canvasScrollBox)
end
canvasScrollBox:SetScrollChild(child)
canvasScrollBox:EnableMouseWheel(true)
canvasScrollBox.child = child
if (canvasScrollBox.options.reskin_slider) then
detailsFramework:ReskinSlider(canvasScrollBox)
end
return canvasScrollBox
end