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