--- @type StdUi local StdUi = LibStub and LibStub('StdUi', true); if not StdUi then return end local module, version = 'ScrollTable', 7; if not StdUi:UpgradeNeeded(module, version) then return end local TableInsert = tinsert; local TableSort = table.sort; local padding = 2.5; --- Public methods of ScrollTable local methods = { ------------------------------------------------------------- --- Basic Methods ------------------------------------------------------------- SetAutoHeight = function(self) self:SetHeight((self.numberOfRows * self.rowHeight) + 10); self:Refresh(); end, SetAutoWidth = function(self) local width = 13; for _, col in pairs(self.columns) do width = width + col.width; end self:SetWidth(width + 20); self:Refresh(); end, ScrollToLine = function(self, line) line = Clamp(line, 1, #self.filtered - self.numberOfRows + 1); self:DoVerticalScroll( self.rowHeight * (line - 1), self.rowHeight, function(s) s:Refresh(); end ); end, ------------------------------------------------------------- --- Drawing Methods ------------------------------------------------------------- --- Set the column info for the scrolling table --- @usage st:SetColumns(columns) SetColumns = function(self, columns) local table = self; -- reference saved for closure self.columns = columns; local columnHeadFrame = self.head; if not columnHeadFrame then columnHeadFrame = CreateFrame('Frame', nil, self); columnHeadFrame:SetPoint('BOTTOMLEFT', self, 'TOPLEFT', 4, 0); columnHeadFrame:SetPoint('BOTTOMRIGHT', self, 'TOPRIGHT', -4, 0); columnHeadFrame:SetHeight(self.rowHeight); columnHeadFrame.columns = {}; self.head = columnHeadFrame; end for i = 1, #columns do local column = self.columns[i]; local columnFrame = columnHeadFrame.columns[i]; if not columnHeadFrame.columns[i] then columnFrame = self.stdUi:HighlightButton(columnHeadFrame); columnFrame:SetPushedTextOffset(0, 0); columnFrame.arrow = self.stdUi:Texture(columnFrame, 8, 8, [[Interface\Buttons\UI-SortArrow]]); columnFrame.arrow:Hide(); if self.headerEvents then for event, handler in pairs(self.headerEvents) do columnFrame:SetScript(event, function(cellFrame, ...) table:FireHeaderEvent(event, handler, columnFrame, columnHeadFrame, i, ...); end); end end columnHeadFrame.columns[i] = columnFrame; -- Add column head reference to it's column column.head = columnFrame; -- Create a table of empty column cell references column.cells = {}; end local align = columns[i].align or 'LEFT'; columnFrame.text:SetJustifyH(align); columnFrame.text:SetText(columns[i].name); if align == 'LEFT' then columnFrame.arrow:ClearAllPoints(); self.stdUi:GlueRight(columnFrame.arrow, columnFrame, 0, 0, true); else columnFrame.arrow:ClearAllPoints(); self.stdUi:GlueLeft(columnFrame.arrow, columnFrame, 5, 0, true); end if columns[i].sortable == false and columns[i].sortable ~= nil then else end if i > 1 then columnFrame:SetPoint('LEFT', columnHeadFrame.columns[i - 1], 'RIGHT', 0, 0); else columnFrame:SetPoint('LEFT', columnHeadFrame, 'LEFT', 2, 0); end columnFrame:SetHeight(self.rowHeight); columnFrame:SetWidth(columns[i].width); --- Set the width of a column --- @usage st.columns[i]:SetWidth(width) function column:SetWidth(width) -- Update the column's width value column.width = width; -- Set the width of the column's head column.head:SetWidth(width); -- Set the width of each cell in the column for j = 1, #column.cells do column.cells[j]:SetWidth(width) end end end self:SetDisplayRows(self.numberOfRows, self.rowHeight); self:SetAutoWidth(); end, --- Set the number and height of displayed rows --- @usage st:SetDisplayRows(10, 15) SetDisplayRows = function(self, numberOfRows, rowHeight) local table = self; -- reference saved for closure -- should always set columns first self.numberOfRows = numberOfRows; self.rowHeight = rowHeight; if not self.rows then self.rows = {}; end for i = 1, numberOfRows do local rowFrame = self.rows[i]; if not rowFrame then rowFrame = CreateFrame('Button', nil, self); self.rows[i] = rowFrame; if i > 1 then rowFrame:SetPoint('TOPLEFT', self.rows[i - 1], 'BOTTOMLEFT', 0, 0); rowFrame:SetPoint('TOPRIGHT', self.rows[i - 1], 'BOTTOMRIGHT', 0, 0); else rowFrame:SetPoint('TOPLEFT', self.scrollFrame, 'TOPLEFT', 1, -1); rowFrame:SetPoint('TOPRIGHT', self.scrollFrame, 'TOPRIGHT', -1, -1); end rowFrame:SetHeight(rowHeight); end if not rowFrame.columns then rowFrame.columns = {}; end for j = 1, #self.columns do local columnData = self.columns[j]; local cell = rowFrame.columns[j]; if not cell then cell = CreateFrame('Button', nil, rowFrame); cell.text = self.stdUi:FontString(cell, ''); rowFrame.columns[j] = cell; -- Add cell reference to column self.columns[j].cells[i] = cell; local align = columnData.align or 'LEFT'; cell.text:SetJustifyH(align); cell:EnableMouse(true); cell:RegisterForClicks('AnyUp'); if self.cellEvents then for event, handler in pairs(self.cellEvents) do cell:SetScript(event, function(cellFrame, ...) if table.offset then local rowIndex = table.filtered[i + table.offset]; local rowData = table:GetRow(rowIndex); table:FireCellEvent(event, handler, cellFrame, rowFrame, rowData, columnData, rowIndex, ...); end end); end end -- override a column based events if columnData.events then for event, handler in pairs(columnData.events) do cell:SetScript(event, function(cellFrame, ...) if table.offset then local rowIndex = table.filtered[i + table.offset]; local rowData = table:GetRow(rowIndex); table:FireCellEvent(event, handler, cellFrame, rowFrame, rowData, columnData, rowIndex, ...); end end); end end end if j > 1 then cell:SetPoint('LEFT', rowFrame.columns[j - 1], 'RIGHT', 0, 0); else cell:SetPoint('LEFT', rowFrame, 'LEFT', 2, 0); end cell:SetHeight(rowHeight); cell:SetWidth(self.columns[j].width); cell.text:SetPoint('TOP', cell, 'TOP', 0, 0); cell.text:SetPoint('BOTTOM', cell, 'BOTTOM', 0, 0); cell.text:SetWidth(self.columns[j].width - 2 * padding); end local j = #self.columns + 1; local col = rowFrame.columns[j]; while col do col:Hide(); j = j + 1; col = rowFrame.columns[j]; end end for i = numberOfRows + 1, #self.rows do self.rows[i]:Hide(); end self:SetAutoHeight(); end, --- Set the width of a column --- @usage st:SetColumnWidth(2, 65) SetColumnWidth = function(self, columnNumber, width) self.columns[columnNumber]:SetWidth(width); end, ------------------------------------------------------------- --- Sorting Methods ------------------------------------------------------------- --- Resorts the table using the rules specified in the table column info. --- @usage st:SortData() SortData = function(self, sortBy) -- sanity check if not (self.sortTable) or (#self.sortTable ~= #self.data) then self.sortTable = {}; end if #self.sortTable ~= #self.data then for i = 1, #self.data do self.sortTable[i] = i; end end -- go on sorting if not sortBy then local i = 1; while i <= #self.columns and not sortBy do if self.columns[i].sort then sortBy = i; end i = i + 1; end end if sortBy then TableSort(self.sortTable, function(rowA, rowB) local column = self.columns[sortBy]; if column.compareSort then return column.compareSort(self, rowA, rowB, sortBy); else return self:CompareSort(rowA, rowB, sortBy); end end); end self.filtered = self:DoFilter(); self:Refresh(); self:UpdateSortArrows(sortBy); end, --- CompareSort function used to determine how to sort column values. Can be overridden in column data or table data. --- @usage used internally. CompareSort = function(self, rowA, rowB, sortBy) local a = self:GetRow(rowA); local b = self:GetRow(rowB); local column = self.columns[sortBy]; local idx = column.index; local direction = column.sort or column.defaultSort or 'asc'; if direction:lower() == 'asc' then return a[idx] > b[idx]; else return a[idx] < b[idx]; end end, Filter = function(self, rowData) return true; end, --- Set a display filter for the table. --- @usage st:SetFilter( function (self, ...) return true end ) SetFilter = function(self, filter, noSort) self.Filter = filter; if not noSort then self:SortData(); end end, DoFilter = function(self) local result = {}; for row = 1, #self.data do local realRow = self.sortTable[row]; local rowData = self:GetRow(realRow); if self:Filter(rowData) then TableInsert(result, realRow); end end return result; end, ------------------------------------------------------------- --- Highlight Methods ------------------------------------------------------------- --- Set the row highlight color of a frame ( cell or row ) --- @usage st:SetHighLightColor(rowFrame, color) SetHighLightColor = function(self, frame, color) if not frame.highlight then frame.highlight = frame:CreateTexture(nil, 'OVERLAY'); frame.highlight:SetAllPoints(frame); end if not color then frame.highlight:SetColorTexture(0, 0, 0, 0); else frame.highlight:SetColorTexture(color.r, color.g, color.b, color.a); end end, ClearHighlightedRows = function(self) self.highlightedRows = {}; self:Refresh(); end, HighlightRows = function(self, rowIndexes) self.highlightedRows = rowIndexes; self:Refresh(); end, ------------------------------------------------------------- --- Selection Methods ------------------------------------------------------------- --- Turn on or off selection on a table according to flag. Will not refresh the table display. --- @usage st:EnableSelection(true) EnableSelection = function(self, flag) self.selectionEnabled = flag; end, --- Clear the currently selected row. You should not need to refresh the table. --- @usage st:ClearSelection() ClearSelection = function(self) self:SetSelection(nil); end, --- Sets the currently selected row to 'realRow'. RealRow is the unaltered index of the data row in your table. --- You should not need to refresh the table. --- @usage st:SetSelection(12) SetSelection = function(self, rowIndex) self.selected = rowIndex; self:Refresh(); end, --- Gets the currently selected row. --- Return will be the unaltered index of the data row that is selected. --- @usage st:GetSelection() GetSelection = function(self) return self.selected; end, --- Gets the currently selected row. --- Return will be the unaltered index of the data row that is selected. --- @usage st:GetSelection() GetSelectedItem = function(self) return self:GetRow(self.selected); end, ------------------------------------------------------------- --- Data Methods ------------------------------------------------------------- --- Sets the data for the scrolling table --- @usage st:SetData(datatable) SetData = function(self, data) self.data = data; self:SortData(); end, --- Returns the data row of the table from the given data row index --- @usage used internally. GetRow = function(self, rowIndex) return self.data[rowIndex]; end, --- Returns the cell data of the given row from the given row and column index --- @usage used internally. GetCell = function(self, row, col) local rowData = row; if type(row) == 'number' then rowData = self:GetRow(row); end return rowData[col]; end, --- Checks if a row is currently being shown --- @usage st:IsRowVisible(realrow) --- @thanks sapu94 IsRowVisible = function(self, rowIndex) return (rowIndex > self.offset and rowIndex <= (self.numberOfRows + self.offset)); end, ------------------------------------------------------------- --- Update Internal Methods ------------------------------------------------------------- --- Cell update function used to paint each cell. Can be overridden in column data or table data. --- @usage used internally. DoCellUpdate = function(table, shouldShow, rowFrame, cellFrame, value, columnData, rowData, rowIndex) if shouldShow then local format = columnData.format; if type(format) == 'function' then cellFrame.text:SetText(format(value, rowData, columnData)); elseif format == 'money' then value = table.stdUi.Util.formatMoney(value); cellFrame.text:SetText(value); elseif format == 'moneyShort' then value = table.stdUi.Util.formatMoney(value, true); cellFrame.text:SetText(value); elseif format == 'time' then value = table.stdUi.Util.formatTime(value); cellFrame.text:SetText(value); elseif format == 'number' then value = tostring(value); cellFrame.text:SetText(value); elseif format == 'icon' then if cellFrame.texture then cellFrame.texture:SetTexture(value); else local iconSize = columnData.iconSize or table.rowHeight; cellFrame.texture = table.stdUi:Texture(cellFrame, iconSize, iconSize, value); cellFrame.texture:SetPoint('CENTER', 0, 0); end elseif format == 'custom' then columnData.renderer(cellFrame, value, rowData, columnData); else cellFrame.text:SetText(value); end local color; if rowData.color then color = rowData.color; elseif columnData.color then color = columnData.color; end if type(color) == 'function' then color = color(table, value, rowData, columnData); end if color then cellFrame.text:SetTextColor(color.r, color.g, color.b, color.a); else table.stdUi:SetTextColor(cellFrame.text, 'normal'); end if table.selectionEnabled then if table.selected == rowIndex then table:SetHighLightColor(rowFrame, table.stdUi.config.highlight.color); else table:SetHighLightColor(rowFrame, nil); end else if tContains(table.highlightedRows, rowIndex) then table:SetHighLightColor(rowFrame, table.stdUi.config.highlight.color); else table:SetHighLightColor(rowFrame, nil); end end else cellFrame.text:SetText(''); end end, Refresh = function(self) self:Update(#self.filtered, self.numberOfRows, self.rowHeight); local o = self:GetOffset(); self.offset = o; for i = 1, self.numberOfRows do local row = i + o; if self.rows then local rowFrame = self.rows[i]; local rowIndex = self.filtered[row]; local rowData = self:GetRow(rowIndex); local shouldShow = true; for col = 1, #self.columns do local cellFrame = rowFrame.columns[col]; local columnData = self.columns[col]; local fnDoCellUpdate = self.DoCellUpdate; local value; if rowData then value = rowData[columnData.index]; self.rows[i]:Show(); if rowData.doCellUpdate then fnDoCellUpdate = rowData.doCellUpdate; elseif columnData.doCellUpdate then fnDoCellUpdate = columnData.doCellUpdate; end else self.rows[i]:Hide(); shouldShow = false; end fnDoCellUpdate(self, shouldShow, rowFrame, cellFrame, value, columnData, rowData, rowIndex); end end end end, ------------------------------------------------------------- --- Private Methods ------------------------------------------------------------- UpdateSortArrows = function(self, sortBy) if not self.head then return end for i = 1, #self.columns do local col = self.head.columns[i]; if col then if i == sortBy then local column = self.columns[sortBy]; local direction = column.sort or column.defaultSort or 'asc'; if direction == 'asc' then col.arrow:SetTexCoord(0, 0.5625, 0, 1); else col.arrow:SetTexCoord(0, 0.5625, 1, 0); end col.arrow:Show(); else col.arrow:Hide(); end end end end, FireCellEvent = function(self, event, handler, ...) if not handler(self, ...) then if self.cellEvents[event] then self.cellEvents[event](self, ...); end end end, FireHeaderEvent = function(self, event, handler, ...) if not handler(self, ...) then if self.headerEvents[event] then self.headerEvents[event](self, ...); end end end, --- Set the event handlers for various ui events for each cell. --- @usage st:RegisterEvents(events, true) RegisterEvents = function(self, cellEvents, headerEvents, removeOldEvents) local table = self; -- save for closure later if cellEvents then -- Register events for each cell for i, rowFrame in ipairs(self.rows) do for j, cell in ipairs(rowFrame.columns) do local columnData = self.columns[j]; -- unregister old events. if removeOldEvents and self.cellEvents then for event, handler in pairs(self.cellEvents) do cell:SetScript(event, nil); end end -- register new ones. for event, handler in pairs(cellEvents) do cell:SetScript(event, function(cellFrame, ...) local rowIndex = table.filtered[i + table.offset]; local rowData = table:GetRow(rowIndex); table:FireCellEvent(event, handler, cellFrame, rowFrame, rowData, columnData, rowIndex, ...); end); end -- override a column based events if columnData.events then for event, handler in pairs(self.columns[j].events) do cell:SetScript(event, function(cellFrame, ...) if table.offset then local rowIndex = table.filtered[i + table.offset]; local rowData = table:GetRow(rowIndex); table:FireCellEvent(event, handler, cellFrame, rowFrame, rowData, columnData, rowIndex, ...); end end); end end end end end if headerEvents then -- Register events on column headers for columnIndex, columnFrame in ipairs(self.head.columns) do -- unregister old events. if removeOldEvents and self.headerEvents then for event, _ in pairs(self.headerEvents) do columnFrame:SetScript(event, nil); end end -- register new ones. for event, handler in pairs(headerEvents) do columnFrame:SetScript(event, function(cellFrame, ...) table:FireHeaderEvent(event, handler, columnFrame, self.head, columnIndex, ...); end); end end end end, }; local cellEvents = { OnEnter = function(table, cellFrame, rowFrame, rowData, columnData, rowIndex) table:SetHighLightColor(rowFrame, table.stdUi.config.highlight.color); return true; end, OnLeave = function(table, cellFrame, rowFrame, rowData, columnData, rowIndex) if rowIndex ~= table.selected or not table.selectionEnabled then table:SetHighLightColor(rowFrame, nil); end return true; end, OnClick = function(table, cellFrame, rowFrame, rowData, columnData, rowIndex, button) if button == 'LeftButton' then if table:GetSelection() == rowIndex then table:ClearSelection(); else table:SetSelection(rowIndex); end return true; end end, }; local headerEvents = { OnClick = function(table, columnFrame, columnHeadFrame, columnIndex, button, ...) if button == 'LeftButton' then local columns = table.columns; local column = columns[columnIndex]; -- clear sort for other columns for i, _ in ipairs(columnHeadFrame.columns) do if i ~= columnIndex then columns[i].sort = nil; end end local sortOrder = 'asc'; if not column.sort and column.defaultSort then -- sort by columns default sort first; sortOrder = column.defaultSort; elseif column.sort and column.sort:lower() == 'asc' then sortOrder = 'dsc'; end column.sort = sortOrder; table:SortData(); return true; end end }; local ScrollTableUpdateFn = function(self) self:Refresh(); end local ScrollTableOnVerticalScroll = function(self, offset) local scrollTable = self.panel; -- LS: putting st:Refresh() in a function call passes the st as the 1st arg which lets you -- reference the st if you decide to hook the refresh scrollTable:DoVerticalScroll(offset, scrollTable.rowHeight, ScrollTableUpdateFn); end function StdUi:ScrollTable(parent, columns, numRows, rowHeight) local scrollTable = self:FauxScrollFrame(parent, 100, 100, rowHeight or 15); local scrollFrame = scrollTable.scrollFrame; scrollTable.stdUi = self; scrollTable.numberOfRows = numRows or 12; scrollTable.rowHeight = rowHeight or 15; scrollTable.columns = columns; scrollTable.data = {}; scrollTable.cellEvents = cellEvents; scrollTable.headerEvents = headerEvents; scrollTable.highlightedRows = {}; -- Add all methods for methodName, method in pairs(methods) do scrollTable[methodName] = method; end scrollFrame:SetScript('OnVerticalScroll', ScrollTableOnVerticalScroll); scrollTable:SortData(); scrollTable:SetColumns(scrollTable.columns); scrollTable:UpdateSortArrows(); scrollTable:RegisterEvents(scrollTable.cellEvents, scrollTable.headerEvents); -- no need to assign it once again and override all column events return scrollTable; end StdUi:RegisterModule(module, version);