local detailsFramework = DetailsFramework if (not detailsFramework or not DetailsFrameworkCanLoad) then return end local unpack = unpack local CreateFrame = CreateFrame local geterrorhandler = geterrorhandler local wipe = wipe --definitions ---@class df_headercolumndata : table ---@field key string? ---@field name string? ---@field icon string? ---@field texcoord table? ---@field text string? ---@field canSort boolean? ---@field selected boolean? ---@field width number? ---@field height number? ---@field align string? ---@field offset number? ---@class df_headerchild : uiobject ---@field FramesToAlign table ---@class df_headerframe : frame, df_headermixin, df_optionsmixin ---@field columnHeadersCreated df_headercolumnframe[] ---@field options table ---@field HeaderTable df_headercolumndata[] ---@field columnSelected number ---@class df_headermixin : table ---@field NextHeader number ---@field HeaderWidth number ---@field HeaderHeight number ---@field OnColumnSettingChangeCallback function ---@field GetColumnWidth fun(self: df_headerframe, columnId: number) : number ---@field SetHeaderTable fun(self: df_headerframe, table) ---@field GetSelectedColumn fun(self: df_headerframe) : number, string, string, string ---@field Refresh fun(self: df_headerframe) ---@field UpdateSortArrow fun(self: df_headerframe, columnHeader: df_headercolumnframe, defaultShown: boolean|nil, defaultOrder: string|nil) ---@field UpdateColumnHeader fun(self: df_headerframe, columnHeader: df_headercolumnframe, headerIndex) ---@field ResetColumnHeaderBackdrop fun(self: df_headerframe, columnHeader: df_headercolumnframe) ---@field SetBackdropColorForSelectedColumnHeader fun(self: df_headerframe, columnHeader: df_headercolumnframe) ---@field ClearColumnHeader fun(self: df_headerframe, columnHeader: df_headercolumnframe) ---@field GetNextHeader fun(self: df_headerframe) : df_headercolumnframe ---@field SetColumnSettingChangedCallback fun(self: df_headerframe, func: function) : boolean ---@field ResetFramesToHeaderAlignment fun(self: df_headerframe) ---@field GetFramesFromHeaderAlignment fun(self: df_headerframe) : table ---@class df_headercolumnframe : button ---@field Icon texture ---@field Text fontstring ---@field Arrow texture ---@field Separator texture ---@field resizerButton df_headerresizer ---@field bIsRezising boolean ---@field bInUse boolean ---@field columnData table ---@field order string ---@field columnIndex number ---@field columnAlign string ---@field XPosition number ---@field columnOffset number ---@field key string used to sort the values ---@class df_headerresizer : button ---@field texture texture --mixed functions ---@class df_headerfunctions : table detailsFramework.HeaderFunctions = { ---comment ---@param self df_headerchild ---@param frame uiobject AddFrameToHeaderAlignment = function(self, frame) self.FramesToAlign = self.FramesToAlign or {} table.insert(self.FramesToAlign, frame) end, ---comment ---@param self df_headerchild ResetFramesToHeaderAlignment = function(self) wipe(self.FramesToAlign) end, SetFramesToHeaderAlignment = function(self, ...) ---@cast self df_headerchild wipe(self.FramesToAlign) self.FramesToAlign = {...} end, GetFramesFromHeaderAlignment = function(self) return self.FramesToAlign or {} end, ---@param self uiobject ---@param headerFrame df_headerframe ---@param anchor string AlignWithHeader = function(self, headerFrame, anchor) local columnHeaderFrames = headerFrame.columnHeadersCreated anchor = anchor or "topleft" ---@cast self df_headerchild for i = 1, #self.FramesToAlign do ---@type uiobject local uiObject = self.FramesToAlign[i] uiObject:ClearAllPoints() ---@type df_headercolumnframe local columnHeader = columnHeaderFrames[i] if (columnHeader) then local offset = 0 if (columnHeader.columnAlign == "right") then offset = columnHeader:GetWidth() end if (uiObject:GetObjectType() == "FontString") then ---@cast uiObject fontstring if (columnHeader.columnAlign == "right") then uiObject:SetJustifyH("right") elseif (columnHeader.columnAlign == "left") then uiObject:SetJustifyH("left") elseif (columnHeader.columnAlign == "center") then uiObject:SetJustifyH("center") end end uiObject:SetPoint(columnHeader.columnAlign, self, anchor, columnHeader.XPosition + columnHeader.columnOffset + offset, 0) end end end, ---comment ---@param columnHeader df_headercolumnframe ---@param buttonClicked string OnClick = function(columnHeader, buttonClicked) --get the header main frame local headerFrame = columnHeader:GetParent() ---@cast headerFrame df_headerframe --if this header does not have a clickable header, just ignore if (not headerFrame.columnSelected) then return end --check if this column has 'canSort' key, otherwise ignore the click if (not columnHeader.columnData.canSort) then return end --get the latest column header selected ---@type df_headercolumnframe local previousColumnHeader = headerFrame.columnHeadersCreated[headerFrame.columnSelected] previousColumnHeader.Arrow:Hide() headerFrame:ResetColumnHeaderBackdrop(previousColumnHeader) headerFrame:SetBackdropColorForSelectedColumnHeader(columnHeader) if (headerFrame.columnSelected == columnHeader.columnIndex) then columnHeader.order = columnHeader.order ~= "ASC" and "ASC" or "DESC" end headerFrame.columnOrder = columnHeader.order --set the new column header selected headerFrame.columnSelected = columnHeader.columnIndex headerFrame:UpdateSortArrow(columnHeader) if (headerFrame.options.header_click_callback) then --callback with the main header frame, column header, column index and column order as payload local okay, errortext = pcall(headerFrame.options.header_click_callback, headerFrame, columnHeader, columnHeader.columnIndex, columnHeader.order) if (not okay) then print("DF: Header onClick callback error:", errortext) end end end, ---comment ---@param self button ---@param buttonClicked string OnMouseDown = function(self, buttonClicked) if (buttonClicked == "LeftButton") then end end, ---comment ---@param self button ---@param buttonClicked string OnMouseUp = function(self, buttonClicked) if (buttonClicked == "LeftButton") then end end, } ---@class df_headermixin : table detailsFramework.HeaderMixin = { ---@param self df_headerframe ---@param columnId number ---@return number GetColumnWidth = function(self, columnId) return self.HeaderTable[columnId].width end, ---@param self df_headerframe ---@param newTable table SetHeaderTable = function(self, newTable) self.columnHeadersCreated = self.columnHeadersCreated or {} self.HeaderTable = newTable self.NextHeader = 1 self.HeaderWidth = 0 self.HeaderHeight = 0 self:Refresh() end, ---@param self df_headerframe ---@param func function ---@return boolean SetColumnSettingChangedCallback = function(self, func) if (type(func) ~= "function") then self.OnColumnSettingChangeCallback = nil return false end self.OnColumnSettingChangeCallback = func return true end, --return which header is current selected and the the order ASC DESC ---@param self df_headerframe ---@return number, string, string, string GetSelectedColumn = function(self) ---@type number local columnSelected = self.columnSelected ---@type df_headercolumnframe local columnHeader = self.columnHeadersCreated[columnSelected or 1] return columnSelected, columnHeader.order, columnHeader.key, columnHeader.columnData.name end, --clean up and rebuild the header following the header options --@self: main header frame ---@param self df_headerframe Refresh = function(self) --refresh background frame self:SetBackdrop(self.options.backdrop) self:SetBackdropColor(unpack(self.options.backdrop_color)) self:SetBackdropBorderColor(unpack(self.options.backdrop_border_color)) --reset all header frames for i = 1, #self.columnHeadersCreated do local columnHeader = self.columnHeadersCreated[i] columnHeader.bInUse = false columnHeader:Hide() end local previousColumnHeader local growDirection = string.lower(self.options.grow_direction) --amount of headers to be updated local headerSize = #self.HeaderTable --update header frames for i = 1, headerSize do --get the header button, a new one is created if it doesn't exists yet local columnHeader = self:GetNextHeader() self:UpdateColumnHeader(columnHeader, i) --grow direction if (not previousColumnHeader) then columnHeader:SetPoint("topleft", self, "topleft", 0, 0) if (growDirection == "right") then if (self.options.use_line_separators) then columnHeader.Separator:Show() columnHeader.Separator:SetWidth(self.options.line_separator_width) columnHeader.Separator:SetColorTexture(unpack(self.options.line_separator_color)) columnHeader.Separator:ClearAllPoints() if (self.options.line_separator_gap_align) then columnHeader.Separator:SetPoint("topleft", columnHeader, "topright", 0, 0) else columnHeader.Separator:SetPoint("topright", columnHeader, "topright", 0, 0) end columnHeader.Separator:SetHeight(self.options.line_separator_height) end end else if (growDirection == "right") then columnHeader:SetPoint("topleft", previousColumnHeader, "topright", self.options.padding, 0) if (self.options.use_line_separators) then columnHeader.Separator:Show() columnHeader.Separator:SetWidth(self.options.line_separator_width) columnHeader.Separator:SetColorTexture(unpack(self.options.line_separator_color)) columnHeader.Separator:ClearAllPoints() if (self.options.line_separator_gap_align) then columnHeader.Separator:SetPoint("topleft", columnHeader, "topright", 0, 0) else columnHeader.Separator:SetPoint("topleft", columnHeader, "topright", 0, 0) end columnHeader.Separator:SetHeight(self.options.line_separator_height) if (headerSize == i) then columnHeader.Separator:Hide() end end elseif (growDirection == "left") then columnHeader:SetPoint("topright", previousColumnHeader, "topleft", -self.options.padding, 0) elseif (growDirection == "bottom") then columnHeader:SetPoint("topleft", previousColumnHeader, "bottomleft", 0, -self.options.padding) elseif (growDirection == "top") then columnHeader:SetPoint("bottomleft", previousColumnHeader, "topleft", 0, self.options.padding) end end previousColumnHeader = columnHeader end self:SetSize(self.HeaderWidth, self.HeaderHeight) end, ---@param self df_headerframe ---@param columnHeader df_headercolumnframe ---@param defaultShown boolean ---@param defaultOrder string UpdateSortArrow = function(self, columnHeader, defaultShown, defaultOrder) local options = self.options local order = defaultOrder or columnHeader.order local arrowIcon = columnHeader.Arrow if (type(defaultShown) ~= "boolean") then arrowIcon:Show() else arrowIcon:SetShown(defaultShown) if (defaultShown) then self:SetBackdropColorForSelectedColumnHeader(columnHeader) end end arrowIcon:SetAlpha(options.arrow_alpha) if (order == "ASC") then arrowIcon:SetTexture(options.arrow_up_texture) arrowIcon:SetTexCoord(unpack(options.arrow_up_texture_coords)) arrowIcon:SetSize(unpack(options.arrow_up_size)) elseif (order == "DESC") then arrowIcon:SetTexture(options.arrow_down_texture) arrowIcon:SetTexCoord(unpack(options.arrow_down_texture_coords)) arrowIcon:SetSize(unpack(options.arrow_down_size)) end end, ---@param self df_headerframe ---@param columnHeader df_headercolumnframe ---@param headerIndex number UpdateColumnHeader = function(self, columnHeader, headerIndex) --this is the data to update the columnHeader local columnData = self.HeaderTable[headerIndex] columnHeader.key = columnData.key or "total" if (columnData.icon) then columnHeader.Icon:SetTexture(columnData.icon) if (columnData.texcoord) then columnHeader.Icon:SetTexCoord(unpack(columnData.texcoord)) else columnHeader.Icon:SetTexCoord(0, 1, 0, 1) end columnHeader.Icon:SetPoint("left", columnHeader, "left", self.options.padding, 0) columnHeader.Icon:Show() end if (columnData.text) then columnHeader.Text:SetText(columnData.text) --text options detailsFramework:SetFontColor(columnHeader.Text, self.options.text_color) detailsFramework:SetFontSize(columnHeader.Text, self.options.text_size) detailsFramework:SetFontOutline(columnHeader.Text, self.options.text_shadow) --point if (not columnData.icon) then columnHeader.Text:SetPoint("left", columnHeader, "left", self.options.padding, 0) else columnHeader.Text:SetPoint("left", columnHeader.Icon, "right", self.options.padding, 0) end columnHeader.Text:Show() end --column header index columnHeader.columnIndex = headerIndex if (columnData.canSort) then columnHeader.order = "DESC" columnHeader.Arrow:SetTexture(self.options.arrow_up_texture) else columnHeader.Arrow:Hide() end if (columnData.selected) then columnHeader.Arrow:Show() columnHeader.Arrow:SetAlpha(.843) self:UpdateSortArrow(columnHeader, true, columnHeader.order) self.columnSelected = headerIndex else if (columnData.canSort) then self:UpdateSortArrow(columnHeader, false, columnHeader.order) end end --size if (columnData.width) then columnHeader:SetWidth(columnData.width) end if (columnData.height) then columnHeader:SetHeight(columnData.height) end columnHeader.XPosition = self.HeaderWidth -- + self.options.padding columnHeader.YPosition = self.HeaderHeight -- + self.options.padding columnHeader.columnAlign = columnData.align or "left" columnHeader.columnOffset = columnData.offset or 0 --add the header piece size to the total header size local growDirection = string.lower(self.options.grow_direction) if (growDirection == "right" or growDirection == "left") then self.HeaderWidth = self.HeaderWidth + columnHeader:GetWidth() + self.options.padding self.HeaderHeight = math.max(self.HeaderHeight, columnHeader:GetHeight()) elseif (growDirection == "top" or growDirection == "bottom") then self.HeaderWidth = math.max(self.HeaderWidth, columnHeader:GetWidth()) self.HeaderHeight = self.HeaderHeight + columnHeader:GetHeight() + self.options.padding end local bShowColumnHeaderReziser = self.options.reziser_shown if (bShowColumnHeaderReziser) then local resizerButton = columnHeader.resizerButton resizerButton:Show() resizerButton.texture:SetVertexColor(unpack(self.options.reziser_color)) resizerButton:SetWidth(self.options.reziser_width) resizerButton:SetHeight(columnHeader:GetHeight()) else columnHeader.resizerButton:Hide() end columnHeader:Show() columnHeader.bInUse = true columnHeader.columnData = columnData end, ---reset column header backdrop ---@param self df_headerframe ---@param columnHeader df_headercolumnframe ResetColumnHeaderBackdrop = function(self, columnHeader) columnHeader:SetBackdrop(self.options.header_backdrop) columnHeader:SetBackdropColor(unpack(self.options.header_backdrop_color)) columnHeader:SetBackdropBorderColor(unpack(self.options.header_backdrop_border_color)) end, ---@param self df_headerframe ---@param columnHeader df_headercolumnframe SetBackdropColorForSelectedColumnHeader = function(self, columnHeader) columnHeader:SetBackdropColor(unpack(self.options.header_backdrop_color_selected)) end, ---clear the column header ---@param self df_headerframe ---@param columnHeader df_headercolumnframe ClearColumnHeader = function(self, columnHeader) columnHeader:SetSize(self.options.header_width, self.options.header_height) self:ResetColumnHeaderBackdrop(columnHeader) columnHeader:ClearAllPoints() columnHeader.Icon:SetTexture("") columnHeader.Icon:Hide() columnHeader.Text:SetText("") columnHeader.Text:Hide() end, ---get the next column header, create one if doesn't exists ---@param self df_headerframe GetNextHeader = function(self) local nextHeader = self.NextHeader local columnHeader = self.columnHeadersCreated[nextHeader] if (not columnHeader) then --create a new column header ---@type df_headercolumnframe columnHeader = CreateFrame("button", "$parentHeaderIndex" .. nextHeader, self, "BackdropTemplate") columnHeader:SetScript("OnClick", detailsFramework.HeaderFunctions.OnClick) columnHeader:SetMovable(true) columnHeader:SetResizable(true) --header icon detailsFramework:CreateImage(columnHeader, "", self.options.header_height, self.options.header_height, "ARTWORK", nil, "Icon", "$parentIcon") --header separator detailsFramework:CreateImage(columnHeader, "", 1, 1, "ARTWORK", nil, "Separator", "$parentSeparator") --header name text detailsFramework:CreateLabel(columnHeader, "", self.options.text_size, self.options.text_color, "GameFontNormal", "Text", "$parentText", "ARTWORK") --header selected and order icon detailsFramework:CreateImage(columnHeader, self.options.arrow_up_texture, 12, 12, "ARTWORK", nil, "Arrow", "$parentArrow") ---rezise button ---@type df_headerresizer local resizerButton = CreateFrame("button", "$parentResizer", columnHeader) resizerButton:SetWidth(4) resizerButton:SetFrameLevel(columnHeader:GetFrameLevel()+2) resizerButton:SetPoint("topright", columnHeader, "topright", -1, -1) resizerButton:SetPoint("bottomright", columnHeader, "bottomright", -1, 1) resizerButton:EnableMouse(true) resizerButton:RegisterForClicks("LeftButtonDown", "LeftButtonUp") columnHeader.resizerButton = resizerButton resizerButton:SetScript("OnEnter", function() resizerButton.texture:SetVertexColor(1, 1, 1, 0.9) end) resizerButton:SetScript("OnLeave", function() resizerButton.texture:SetVertexColor(unpack(self.options.reziser_color)) end) resizerButton:SetScript("OnMouseDown", function() --move this to a single function if (not columnHeader.bIsRezising) then --get the string length to know the min size local textLength = columnHeader.Text:GetStringWidth() + 6 columnHeader:SetResizeBounds(math.max(textLength, self.options.reziser_min_width), columnHeader:GetHeight(), self.options.reziser_max_width, columnHeader:GetHeight()) columnHeader.bIsRezising = true columnHeader:StartSizing("right") end end) resizerButton:SetScript("OnMouseUp", function() if (columnHeader.bIsRezising) then columnHeader.bIsRezising = false columnHeader:StopMovingOrSizing() --callback or modify into a passed by table? if (self.OnColumnSettingChangeCallback) then --need to get the header name local columnName = columnHeader.columnData.name xpcall(self.OnColumnSettingChangeCallback, geterrorhandler(), self, "width", columnName, columnHeader:GetWidth()) end end end) resizerButton:SetScript("OnHide", function() if (columnHeader.bIsRezising) then columnHeader:StopMovingOrSizing() columnHeader.bIsRezising = false end end) resizerButton.texture = resizerButton:CreateTexture(nil, "overlay") resizerButton.texture:SetAllPoints() resizerButton.texture:SetColorTexture(1, 1, 1, 1) local xOffset = self.options.reziser_shown and -5 or -1 columnHeader.Arrow:SetPoint("right", columnHeader, "right", xOffset, 0) columnHeader.Separator:Hide() columnHeader.Arrow:Hide() self:UpdateSortArrow(columnHeader, false, "DESC") table.insert(self.columnHeadersCreated, columnHeader) columnHeader = columnHeader end self:ClearColumnHeader(columnHeader) self.NextHeader = self.NextHeader + 1 return columnHeader end, ---return a header button by passing its name (.name on the column table) ---@param self df_headerframe ---@param columnName string ---@return df_headercolumnframe|nil GetHeaderColumnByName = function(self, columnName) for _, headerColumnFrame in ipairs(self.columnHeadersCreated) do if (headerColumnFrame.columnData.name == columnName) then return headerColumnFrame end end end, NextHeader = 1, HeaderWidth = 0, HeaderHeight = 0, } --default options local default_header_options = { backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true}, backdrop_color = {0, 0, 0, 0.2}, backdrop_border_color = {0.1, 0.1, 0.1, .2}, text_color = {1, 1, 1, 1}, text_size = 10, text_shadow = false, grow_direction = "RIGHT", padding = 2, reziser_shown = false, --make sure to set the callback function with: header:SetOnColumnResizeScript(callbackFunction) reziser_width = 2, reziser_color = {1, 0.6, 0, 0.6}, reziser_min_width = 16, reziser_max_width = 200, --each piece of the header header_backdrop = {bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true}, header_backdrop_color = {0, 0, 0, 0.5}, header_backdrop_color_selected = {0.3, 0.3, 0.3, 0.5}, header_backdrop_border_color = {0, 0, 0, 0}, header_width = 120, header_height = 20, arrow_up_texture = [[Interface\Buttons\Arrow-Up-Down]], arrow_up_texture_coords = {0, 1, 6/16, 1}, arrow_up_size = {12, 11}, arrow_down_texture = [[Interface\Buttons\Arrow-Down-Down]], arrow_down_texture_coords = {0, 1, 0, 11/16}, arrow_down_size = {12, 11}, arrow_alpha = 0.659, use_line_separators = false, line_separator_color = {.1, .1, .1, .6}, line_separator_width = 1, line_separator_height = 200, line_separator_gap_align = false, } ---create a df_headerframe, alias 'header'. ---a header is a frame that can hold multiple columns which are also frames, each column is a df_headercolumnframe, these columns are arranged in horizontal form. ---a header is used to organize columns giving them a name/title, a way to sort and align them. ---each column is placed on the right side of the previous column. ---@param parent frame ---@param headerTable df_headercolumndata[] ---@param options table? ---@param frameName string? ---@return df_headerframe function detailsFramework:CreateHeader(parent, headerTable, options, frameName) ---create the header frame which is returned by this function ---@type df_headerframe local newHeader = CreateFrame("frame", frameName or ("$parentHeaderLine" .. math.random(100000000)), parent, "BackdropTemplate") detailsFramework:Mixin(newHeader, detailsFramework.OptionsFunctions) detailsFramework:Mixin(newHeader, detailsFramework.HeaderMixin) options = options or {} newHeader:BuildOptionsTable(default_header_options, options) --set the backdrop and backdrop color following the values in the options table newHeader:SetBackdrop(newHeader.options.backdrop) newHeader:SetBackdropColor(unpack(newHeader.options.backdrop_color)) newHeader:SetBackdropBorderColor(unpack(newHeader.options.backdrop_border_color)) newHeader:SetHeaderTable(headerTable) return newHeader end --[=[example: C_Timer.After(1, function() local parent = UIParent --declare the columns the headerFrame will have ---@type df_headercolumndata[] local headerTable = { {name = "playername", text = "Player Name", width = 120, align = "left", canSort = true}, {name = "damage", text = "Damage Done", width = 80, align = "right", canSort = true}, {name = "points", text = "Total Points", width = 80, align = "right", canSort = false}, } local frameName = "MyAddOnOptionsFrame" local options = {} local headerFrame = DetailsFramework:CreateHeader(parent, headerTable, options, frameName) headerFrame:SetPoint("center", parent, "center", 10, -10) end) --]=]