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.
bastion/libs/StdUi/widgets/ScrollTable.lua

778 lines
21 KiB

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