TableControl = class(Turbine.UI.Control);
-- Add SetRowVisible().
-- SetColumnVisible(), add right-click menu to select columns
-- Add horizontal / vertical orientation?
-- MinimumSize (horizontal) style
function TableControl:Constructor(style, options)
Turbine.UI.Control.Constructor(self);
self:SetMouseVisible(false);
if (not options) then
options = {};
options.columnLockingEnabled = true;
end
if (not style) then
style = {}
style.horizontalCellSpacing = -1;
style.verticalCellSpacing = -1;
style.minimumColumnWidth = 10;
style.heading = {
-- Built-in styles (Turbine.UI.Control)
BackColor = Turbine.UI.Color(1, 0, 0.38, .67);
Enabled = true;
Font = Turbine.UI.Lotro.Font.Verdana14;
ForeColor = Turbine.UI.Color.White;
Height = 24;
Multiline = false;
TextAlignment = Turbine.UI.ContentAlignment.MiddleCenter;
ReadOnly = true;
Selectable = false;
MarkupEnabled = true; -- so we can underline the text when sorting
-- Special styles (TableControl)
Object = Turbine.UI.Lotro.TextBox; -- what kind of object to create for each heading
HighlightColor = Turbine.UI.Color(1, 0, 0.57, 1);
};
style.column = {
SortMethod = function(a, b)
-- Takes two cells as arguments, returns -1, 0, or 1
local aText, bText = a:GetText(), b:GetText();
if (aText < bText) then
return -1;
elseif (aText > bText) then
return 1;
else
return 0;
end
end
};
style.cell = {
-- Built-in styles (Turbine.UI.Control)
BackColor = Turbine.UI.Color(1, 0, 0.19, 0.33);
Enabled = true;
Font = Turbine.UI.Lotro.Font.Verdana14;
ForeColor = Turbine.UI.Color.Silver;
Height = 24;
Multiline = false;
TextAlignment = Turbine.UI.ContentAlignment.MiddleCenter;
ReadOnly = true;
-- Special styles (TableControl)
Object = Turbine.UI.Lotro.TextBox; -- what kind of object to create for each cell
HighlightColor = Turbine.UI.Color(1, 0, 0.57, 1);
};
style.toolTip = {
-- Built-in styles (Turbine.UI.Label)
BackColor = Turbine.UI.Color(0.9, 0, 0, 0);
ForeColor = Turbine.UI.Color.White;
Font = Turbine.UI.Lotro.Font.Verdana14;
FontStyle = Turbine.UI.FontStyle.Outline;
OutlineColor = Turbine.UI.Color(0.5, 0, 0, 0);
Height = 14;
};
style.edgeMover = {
-- Built-in styles (Turbine.UI.Control)
Width = 5;
BackColor = Turbine.UI.Color(0.75, 0, .19, .33);
BackColorBlendMode = Turbine.UI.BlendMode.Overlay;
};
style.marquee = {
Color = Turbine.UI.Color(1, 0, 0.57, 1);
AntsEnabled = true;
Speed = 20; -- updates per second
};
end
self.rows = Turbine.UI.ListBox();
self.rowsByName = {};
self.rows:SetParent(self);
-- The cell container must be in front of the headings, in case they overlap
-- due to verticalCellSpacing < 0.
self.rows:SetZOrder(1);
self.rows.MouseClick = function()
self:Deselect();
end
self.rows.SelectedIndexChanged = function()
self:_SelectRow(self.rows:GetSelectedItem());
end
self.rowWidth = 0;
self.vScrollBar = Turbine.UI.Lotro.ScrollBar();
self.vScrollBar:SetOrientation(Turbine.UI.Orientation.Vertical);
self.vScrollBar:SetParent(self);
self.vScrollBar:SetWidth(10);
self.rows:SetVerticalScrollBar(self.vScrollBar);
self.vScrollBar.VisibleChanged = function()
self:SetWidth(self:GetWidth());
end
self.columns = {};
self.columnsByName = {};
self.totalWeight = 0;
self.edgeMovers = {};
self:SetStyle(style);
self:SetOptions(options);
end
function TableControl:SetStyle(style)
self.style = style;
self.rows:SetTop(self.style.heading.Height + self.style.verticalCellSpacing);
if (self.marquee) then
self:_ApplyStyles(self.marquee, style.marquee);
end
-- to do: update other styles
end
function TableControl:GetStyle()
return self.style;
end
function TableControl:SetOptions(options)
self.options = options;
-- to do: apply updated options
end
function TableControl:GetOptions()
return self.options;
end
function TableControl:GetDimensions()
return #self.rows:GetItemCount(), #self.columns;
end
function TableControl:AddColumn(name, text, weight, sortMethod, locked)
if (weight == nil) then
weight = 1.0;
end
if (sortMethod == nil) then
sortMethod = self.style.column.SortMethod;
end
if (locked == nil) then
locked = false;
end
-- Create and initialize new column object
local column = {
name = name;
heading = nil;
weight = 0; -- this gets set below
locked = false; -- this gets set below
sortMethod = sortMethod;
};
table.insert(self.columns, column);
local col = #self.columns;
self.columnsByName[name] = col;
-- Add a cell to each row and a heading
column.heading = self:_CreateHeading(col, text);
for row = 1, self.rows:GetItemCount() do
self:SetCell(row, col);
end
-- Add lock icon
column.lockIcon = Turbine.UI.Control();
column.lockIcon:SetParent(column.heading);
column.lockIcon:SetVisible(false);
column.lockIcon:SetBackground(0x41007E30);
column.lockIcon:SetBlendMode(Turbine.UI.BlendMode.AlphaBlend);
column.lockIcon:SetZOrder(1);
Thurallor.UI.Tooltip(L:GetText("/TableControl/HeadingMenu/UnlockColumn")):Attach(column.lockIcon);
column.lockIcon.MouseClick = function(_, args)
if (args.Button == Turbine.UI.MouseButton.Left) then
self:SetColumnLocked(name, false);
end
end
--column.lockIcon:SetBackColor(Turbine.UI.Color(0.5, 1, 0, 0));
-- Add lock mask control
column.lockMask = Turbine.UI.Control();
column.lockMask:SetZOrder(3); -- in front of cells
--column.lockMask:SetBackColor(Turbine.UI.Color(0.5, 1, 0, 0));
self:SetColumnWeight(col, weight); -- also calls _UpdateColumnWidths()
self:SetColumnLocked(name, locked);
return column;
end
function TableControl:_CreateHeading(col, text)
local heading = self.style.heading.Object();
self:_ApplyStyles(heading, self.style.heading);
heading:SetParent(self);
heading:SetText(text);
heading.col = col;
AddCallback(heading, "MouseEnter", function()
heading.mouseInside = true;
heading:SetBackColor(self.style.heading.HighlightColor);
end);
AddCallback(heading, "MouseLeave", function()
heading.mouseInside = false;
if (not heading.mouseDown) then
heading:SetBackColor(self.style.heading.BackColor);
end
end);
AddCallback(heading, "MouseDown", function(_, args)
if (args.Button == Turbine.UI.MouseButton.Left) then
heading.mouseDown = true;
heading.mouseStartX = Turbine.UI.Display.GetMouseX();
heading.startLeft = heading:GetLeft();
end
end);
AddCallback(heading, "MouseClick", function(_, args)
if (args.Button == Turbine.UI.MouseButton.Left) then
if (heading.mouseMoved) then
heading.mouseMoved = false;
else
self:_SortColumn(heading.col);
end
elseif (args.Button == Turbine.UI.MouseButton.Right) then
self:_ShowHeadingMenu(heading.col);
end
end);
AddCallback(heading, "MouseMove", function(_, args)
if (heading.mouseDown) then
heading.mouseMoved = true;
heading:SetZOrder(1);
local deltaX = Turbine.UI.Display.GetMouseX() - heading.mouseStartX;
local left = math.min(math.max(0, heading.startLeft + deltaX), self.rowWidth - heading:GetWidth());
heading:SetLeft(left);
deltaX = left - heading.startLeft;
self:_ColumnDragged(heading.col, deltaX);
end
end);
AddCallback(heading, "MouseUp", function(_, args)
if (args.Button == Turbine.UI.MouseButton.Left) then
heading.mouseDown = false;
if (heading.mouseMoved) then
heading:SetZOrder(0);
self:_UpdateColumnWidths();
end
if (not heading.mouseInside) then
heading:SetBackColor(self.style.heading.BackColor);
end
end
end);
if (self.columns[col].sortMethod) then
Thurallor.UI.Tooltip(function() return L:GetText("/TableControl/HeadingMenu/SortColumn") end):Attach(heading);
end
return heading;
end
function TableControl:_ColumnDragged(col, deltaX)
local heading = self.columns[col].heading;
local leftEdge = heading:GetLeft();
local rightEdge = leftEdge + heading:GetWidth();
local swapped = false;
if (deltaX < 0) then
-- Find visible column to the left, if any
for c = col - 1, 1, -1 do
local other = self.columns[c];
if (other.weight > 0) then
local center = other.heading:GetLeft() + math.floor(0.5 + (other.heading:GetWidth() / 2));
if (leftEdge < center) then
--Puts("deltaX = " .. deltaX .. "; halfWidth = " .. halfWidth .. "; hw = " .. hw);
--Puts("swapping left: " .. col .. ", " .. c);
self:_SwapColumns(col, c);
swapped = true;
break;
end
end
end
else
-- Find visible column to the right, if any
for c = col + 1, #self.columns do
local other = self.columns[c];
if (other.weight > 0) then
local center = other.heading:GetLeft() + math.floor(0.5 + (other.heading:GetWidth() / 2));
if (rightEdge > center) then
--Puts("deltaX = " .. deltaX .. "; halfWidth = " .. halfWidth .. "; hw = " .. hw);
--Puts("swapping right: " .. col .. ", " .. c);
self:_SwapColumns(col, c);
swapped = true;
break;
end
end
end
end
--Puts("before: " .. heading:GetLeft());
heading.startLeft = heading:GetLeft();
heading:SetLeft(leftEdge);
deltaX = leftEdge - heading.startLeft;
heading.mouseStartX = Turbine.UI.Display:GetMouseX() - deltaX;
--Puts("after: " .. heading:GetLeft());
end
function TableControl:_SwapColumns(a, b)
local temp = self.columns[a];
self.columns[a] = self.columns[b];
self.columns[b] = temp;
self.columns[a].heading.col = a;
self.columns[b].heading.col = b;
self.columnsByName[self.columns[a].name] = a;
self.columnsByName[self.columns[b].name] = b;
for row = 1, self.rows:GetItemCount() do
local cells = self.rows:GetItem(row).cells;
temp = cells[a];
cells[a] = cells[b];
cells[b] = temp;
end
-- If one of the swapped columns is sorted, update the lastSort data structure
if (self.lastSort) then
if (self.lastSort.col == a) then
self.lastSort.col = b;
self.lastSort.compare = self:_GetCompareFunction(b);
elseif (self.lastSort.col == b) then
self.lastSort.col = a;
self.lastSort.compare = self:_GetCompareFunction(a);
end
end
self:_UpdateColumnWidths();
self:_DoCallbacks("PresentationChanged", { Desc = "Columns swapped positions" });
end
function TableControl:_ShowHeadingMenu(col)
local column = self.columns[col];
local contextMenu = Turbine.UI.ContextMenu();
local menuItems = contextMenu:GetItems();
local prevContext = L:SetContext("/TableControl/HeadingMenu");
if (self.totalWeight > column.weight) then
local hideItem = Turbine.UI.MenuItem(L:GetText("HideColumn"));
hideItem.Click = function()
self:SetColumnWeight(col, 0);
end
menuItems:Add(hideItem);
end
if (#self.columns > 1) then
local showItem = Turbine.UI.MenuItem(L:GetText("ShowColumns"));
local colItems = showItem:GetItems();
for c = 1, #self.columns do
local other = self.columns[c];
local name = other.heading:GetText();
local item = Turbine.UI.MenuItem(name, true, true);
if (other.weight > 0) then
item.Click = function()
self:SetColumnWeight(c, 0);
end
else
item:SetChecked(false);
item.Click = function()
self:SetColumnWeight(c, 1);
end
end
colItems:Add(item);
end
menuItems:Add(showItem);
end
if (column.sortMethod) then
local sortItem = Turbine.UI.MenuItem(L:GetText("SortColumn"));
sortItem.Click = function()
self:_SortColumn(col);
end
menuItems:Add(sortItem);
end
if (self.options.columnLockingEnabled) then
local lockItem = Turbine.UI.MenuItem(L:GetText("LockColumn"), true, column.locked);
lockItem.Click = function()
self:SetColumnLocked(column.name, not column.locked);
end
menuItems:Add(lockItem);
end
contextMenu:ShowMenu();
L:SetContext(prevContext);
end
function TableControl:SortColumn(name, dir)
local col = self.columnsByName[name];
local column = self.columns[col];
column.sortDirection = not dir;
self:_SortColumn(col);
end
function TableControl:_SortColumn(col)
local column = self.columns[col];
if (not column.sortMethod) then
return;
end
-- Show sort indication icon
if (not self.sortIcon) then
self.sortIcon = Turbine.UI.Control();
self.sortIcon:SetMouseVisible(false);
self.sortIcon:SetSize(10, 10);
self.sortIcon:SetBlendMode(Turbine.UI.BlendMode.Overlay);
end;
self.sortIcon:SetParent(column.heading);
self.sortIcon:SetZOrder(1);
local inset = math.floor(0.5 + (column.heading:GetHeight() - 10) / 2);
self.sortIcon:SetPosition(inset, inset);
if (column.sortDirection) then
self.sortIcon:SetBackground(0x4100028E); -- â¼
else
self.sortIcon:SetBackground(0x4100028C); -- â²
end
-- Toggle between ascending/descending when creating the sort function
column.sortDirection = not column.sortDirection;
local compare = self:_GetCompareFunction(col);
self.lastSort = { col = col; compare = compare };
self:_ReSort();
self:_DoCallbacks("PresentationChanged", { Desc = "Column sort changed" });
end
function TableControl:_GetCompareFunction(col)
local column = self.columns[col];
if (column.sortDirection) then
return function(a, b)
-- Takes rowContainers as args, returns true or false
local sortResult = column.sortMethod(a.cells[col], b.cells[col]);
if (sortResult == 0) then
-- Make the sorting "stable" to allow sorting by multiple columns in sequence
return a.idx > b.idx;
else
return (sortResult > 0);
end
end
else
return function(a, b)
-- Takes rowContainers as args, returns true or false
local sortResult = column.sortMethod(b.cells[col], a.cells[col]);
if (sortResult == 0) then
-- Make the sorting "stable" to allow sorting by multiple columns in sequence
return a.idx > b.idx;
else
return (sortResult > 0);
end
end
end
end
function TableControl:_CreateCell(row, col)
local rowContainer = self.rows:GetItem(row);
local cell = self.style.cell.Object();
self:_ApplyStyles(cell, self.style.cell);
return cell;
end
function TableControl:_CreateEdgeMovers()
-- Destroy previous objects
for edge = 1, #self.edgeMovers do
self.edgeMovers[edge]:SetParent(nil);
end
self.edgeMovers = {};
-- Only show an EdgeMover between two visible (weight > 0) columns.
local prev = nil;
for col = 1, #self.columns do
if (self.columns[col].weight > 0) then
if (prev) then
-- Found a pair of visible columns.
local edgeMover = Thurallor.UI.EdgeMover(Turbine.UI.Orientation.Vertical);
edgeMover.leftColumn = prev;
edgeMover.rightColumn = col;
edgeMover:SetParent(self);
edgeMover:SetZOrder(1); -- in front of rows
self:_ApplyStyles(edgeMover, self.style.edgeMover);
AddCallback(edgeMover, "EdgeMoved", function()
self:_EdgeMoved(edgeMover);
end);
AddCallback(edgeMover, "MouseUp", function()
-- EdgeMover may set a column weight to zero; if so, disable the column's EdgeMover.
self:_CreateEdgeMovers();
end);
table.insert(self.edgeMovers, edgeMover);
end
prev = col;
end
end
self:_PlaceEdgeMovers();
self:_DoCallbacks("PresentationChanged", { Desc = "Column weight(s) changed" });
end
function TableControl:_EdgeMoved(edgeMover)
local x, y = self:GetMousePosition();
local minWidth = self.style.minimumColumnWidth;
local leftCol = self.columns[edgeMover.leftColumn];
local leftHeading = leftCol.heading;
x = math.max(x, leftHeading:GetLeft() + minWidth);
local rightCol = self.columns[edgeMover.rightColumn];
local rightHeading = rightCol.heading;
x = math.min(x, rightHeading:GetLeft() + rightHeading:GetWidth() - minWidth);
local leftWidth = x - leftHeading:GetLeft();
local rightWidth = rightHeading:GetLeft() + rightHeading:GetWidth() - x;
local bothWidth = leftWidth + rightWidth;
local bothWeight = leftCol.weight + rightCol.weight;
leftCol.weight = bothWeight * (leftWidth / bothWidth);
rightCol.weight = bothWeight * (rightWidth / bothWidth);
self:_UpdateColumnWidths();
end
function TableControl:_DoCallbacks(event, args)
if (not self.callbacksDisabled) then
DoCallbacks(self, event, args);
end
end
function TableControl:GetColumnWeight(colName)
local col = self.columnsByName[colName];
if (not col) then
error("no such column: " .. colName, 2);
end
local column = self.columns[col];
return column.weight;
end
function TableControl:SetColumnWeight(col, weight)
local column = self.columns[col];
local prevWeight = column.weight;
self.totalWeight = self.totalWeight - prevWeight + weight;
column.weight = weight;
if ((weight == 0) and (prevWeight > 0)) then
column.heading:SetParent(nil);
for row = 1, self.rows:GetItemCount() do
local rowContainer = self.rows:GetItem(row);
rowContainer.cells[col]:SetParent(nil);
end
self:_CreateEdgeMovers();
elseif ((weight > 0) and (prevWeight == 0)) then
column.heading:SetParent(self);
for row = 1, self.rows:GetItemCount() do
local rowContainer = self.rows:GetItem(row);
rowContainer.cells[col]:SetParent(rowContainer);
end
self:_CreateEdgeMovers();
end
self:_UpdateColumnWidths();
column.prevWeight = prevWeight;
end
-- "Presentation" includes (so far)
-- - the user's choices of column weights, visibility and ordering
-- - the sorted column and direction
function TableControl:GetPresentation()
local columns = {};
for col = 1, #self.columns do
local column = self.columns[col];
table.insert(columns, { column.name, column.weight, column.locked });
end
local presentation = {};
presentation.columns = columns;
if (self.lastSort) then
local column = self.columns[self.lastSort.col];
presentation.sort = { column = column.name; dir = column.sortDirection };
end
return presentation;
end
function TableControl:SetPresentation(presentation)
-- Make a local copy of presentation, so we can modify it. Remove any columns
-- from it that don't exist in the table.
local temp = {};
for n, colInfo in ipairs(presentation.columns) do
local name, weight, locked = unpack(colInfo);
if (self.columnsByName[name]) then
table.insert(temp, { name, weight, locked });
end
end
presentation.columns = temp;
-- Get column weights from presentation
local foundColumns = {};
for col = 1, #presentation.columns do
local name, weight = unpack(presentation.columns[col]);
foundColumns[name] = weight;
end
-- Handle columns that aren't specified in the presentation
-- Set their weight to 0.
for name, col in pairs(self.columnsByName) do
if (foundColumns[name] == nil) then
foundColumns[name] = true;
table.insert(presentation.columns, { name, 0 });
end
end
-- Create a new cell list in each rowContainer
for row = 1, self.rows:GetItemCount() do
self.rows:GetItem(row).newCells = {};
end
-- Apply the new ordering and weights (and locks) to the columns
local oldColumns = self.columns;
self.columns = {};
self.totalWeight = 0;
for col = 1, #presentation.columns do
local name, weight, locked = unpack(presentation.columns[col]);
local oldCol = self.columnsByName[name];
local column = oldColumns[oldCol];
self.totalWeight = self.totalWeight + weight;
column.weight = weight;
if (locked ~= nil) then
column.locked = locked;
end
column.heading.col = col;
column.heading:SetParent((weight > 0) and self or nil);
self.columnsByName[name] = col;
table.insert(self.columns, column);
for row = 1, self.rows:GetItemCount() do
local rowContainer = self.rows:GetItem(row);
local cell = rowContainer.cells[oldCol];
rowContainer.newCells[col] = cell;
cell:SetParent((weight > 0) and rowContainer or nil);
end
end
-- Start using the new cell list in each rowContainer
for row = 1, self.rows:GetItemCount() do
local rowContainer = self.rows:GetItem(row);
rowContainer.cells = rowContainer.newCells;
rowContainer.newCells = {};
end
-- Move the cells into their new positions
self:_CreateEdgeMovers();
self:_UpdateColumnWidths();
-- Apply the sort (if any)
if (presentation.sort) then
local col = self.columnsByName[presentation.sort.column];
local column = self.columns[col];
if (column) then
column.sortDirection = not presentation.sort.dir;
self:_SortColumn(col);
end
end
-- Apply the locks
for col = 1, #self.columns do
local column = self.columns[col];
self:SetColumnLocked(column.name, column.locked);
end
end
function TableControl:GetRow(name)
return self.rowsByName[name];
end
function TableControl:_GetToolTip(rowContainer)
local function AddLabel(parent, top, prevMaxWidth, text, align)
local label = Turbine.UI.Label();
label:SetTextAlignment(align);
self:_ApplyStyles(label, self.style.toolTip);
label:SetBackColor(nil);
label:SetText(text);
label:AutoSize();
label:SetParent(parent);
label:SetTop(top);
AddCallback(parent, "SizeChanged", function() label:SetWidth(parent:GetWidth()) end);
return top + label:GetHeight(), math.max(prevMaxWidth, label:GetWidth());
end
local toolTip = Turbine.UI.Control();
local colNames = Turbine.UI.Control();
local cellValues = Turbine.UI.Control();
colNames:SetParent(toolTip);
cellValues:SetParent(toolTip);
local colNamesWidth = 0;
local cellValuesWidth = 0;
local top = 0;
for c = 1, #self.columns do
local column = self.columns[c];
local colName = column.heading:GetText() .. ": ";
local cell = rowContainer.cells[c];
local cellValue = (cell.GetText and cell:GetText() and (cell:GetText() ~= "") and tostring(cell:GetText()));
if (cellValue) then
_, colNamesWidth = AddLabel(colNames, top, colNamesWidth, colName, Turbine.UI.ContentAlignment.MiddleRight);
top, cellValuesWidth = AddLabel(cellValues, top, cellValuesWidth, cellValue, Turbine.UI.ContentAlignment.MiddleLeft);
end
end
colNames:SetSize(colNamesWidth, top);
colNames:SetPosition(0, 3);
cellValues:SetSize(cellValuesWidth, top);
cellValues:SetPosition(colNamesWidth, 3);
self:_ApplyStyles(toolTip, self.style.toolTip);
toolTip:SetSize(colNamesWidth + cellValuesWidth, top + 6);
return toolTip;
end
function TableControl:AddRow(name, moreRows)
local rowContainer = Turbine.UI.Control();
rowContainer.idx = math.huge;
rowContainer:SetWidth(self.rowWidth);
rowContainer.name = name;
rowContainer.cells = {};
self.rows:AddItem(rowContainer);
self.rowsByName[name] = rowContainer;
local row = self.rows:GetItemCount();
-- Add new cells
self:SetRowVisible(row, false);
for col = 1, #self.columns do
self:SetCell(row, col);
end
-- Add mask overlay. We want the user to have to select the row before clicking again
-- on a particular cell to edit it.
local mask = Turbine.UI.Control();
mask:SetParent(rowContainer);
mask:SetSize(rowContainer:GetSize());
--mask:SetBackColor(Turbine.UI.Color(0.25, 1, 0, 0));
--mask:SetBackColorBlendMode(Turbine.UI.BlendMode.Overlay);
mask:SetZOrder(3); -- in front of cells
rowContainer.mask = mask;
-- Add event behaviors
rowContainer.PositionChanged = function(ctl)
if ((self.selectedRow == ctl) and self.marquee) then
self.marquee:SetPosition(ctl:GetPosition());
end
end
rowContainer.SizeChanged = function(ctl)
ctl.mask:SetSize(ctl:GetSize());
if ((self.selectedRow == ctl) and self.marquee) then
self.marquee:SetSize(ctl:GetSize());
end
end
mask.MouseEnter = function()
for c = 1, #self.columns do
rowContainer.cells[c]:SetBackColor(self.style.cell.HighlightColor);
end
end
mask.MouseLeave = function()
for c = 1, #self.columns do
rowContainer.cells[c]:SetBackColor(self.style.cell.BackColor);
end
end
mask.MouseClick = function(_, args)
if (args.Button == Turbine.UI.MouseButton.Right) then
DoCallbacks(rowContainer, "ContextMenu");
elseif (args.Button == Turbine.UI.MouseButton.Left) then
-- The ListBox control didn't steal the left-mouse click, because the
-- user clicked the already-selected item. *facepalm*
DoCallbacks(self.rows, "SelectedIndexChanged");
end
end
Thurallor.UI.Tooltip(function() return self:_GetToolTip(rowContainer) end):Attach(mask);
self:_UpdateLastRow(); -- also calls SetRowVisible(row, true)
-- Table needs to be resorted, if enabled
if ((not moreRows) and self.lastSort) then
self:ReSort();
-- if not auto-sorting, then hide the sort icon
end
-- Return a table of cells indexed by column name
local cells = {};
for c = 1, #self.columns do
local colName = self.columns[c].name;
cells[colName] = rowContainer.cells[c];
end
return rowContainer, cells, row;
end
function TableControl:ReSort(now)
if (self.lastSort) then
self.lastSort.sorted = false;
if (now) then
self:_ReSort();
else
self:SetWantsUpdates(true);
end
end
end
function TableControl:Update()
self:_ReSort();
self:SetWantsUpdates(false);
end
-- Reapply the most recent sort
function TableControl:_ReSort()
if (self.lastSort) then
local rows = self.rows;
rows:Sort(self.lastSort.compare);
for idx = 1, rows:GetItemCount() do
local row = rows:GetItem(idx);
row.idx = idx;
end
self.lastSort.sorted = true;
self:_UpdateLastRow();
end
end
-- Can specify colName or nil; returns column name of sorted column or nil
function TableControl:IsSorted(colName)
if ((self.lastSort) and (self.lastSort.sorted)) then
local sortedCol = self.columns[self.lastSort.col];
if (not colName) then
return sortedCol.name;
else
if (self.lastSort.col == self.columnsByName[colName]) then
return colName;
end
end
end
end
function TableControl:IsColumnLocked(colName)
if (colName) then
local col = self.columnsByName[colName];
if (col) then
return self.columns[col].locked;
end
end
end
function TableControl:SetColumnLocked(colName, locked)
if (colName and self.options.columnLockingEnabled) then
local col = self.columnsByName[colName];
if (col) then
local column = self.columns[col];
local changed = (column.locked ~= locked);
column.locked = locked;
column.lockIcon:SetVisible(locked);
self:_PlaceColumnLockMasks();
if (changed) then
DoCallbacks(self, "PresentationChanged", { Desc = "Column " .. (locked and "locked" or "unlocked") });
end
end
end
end
function TableControl:SelectRow(rowName)
if (rowName) then
self:_SelectRow(self.rowsByName[rowName]);
else
self:_SelectRow(nil);
end
end
function TableControl:_SelectRow(rowContainer)
local oldSelectedRow = self.selectedRow;
self.selectedRow = rowContainer;
-- Re-enable mask on previous row
if (oldSelectedRow) then
oldSelectedRow.mask:SetMouseVisible(true);
end
-- Remove old marquee, if any
if (self.marquee) then
self.marquee:SetParent(nil);
self.marquee = nil;
end
if (rowContainer) then
rowContainer:Focus();
-- Create new marquee and attach it
self.marquee = Thurallor.UI.Marquee();
self:_ApplyStyles(self.marquee, self.style.marquee);
local height = 0;
for _, cell in pairs(rowContainer.cells) do
height = math.max(height, cell:GetHeight());
end
self.marquee:SetPosition(rowContainer:GetPosition());
self.marquee:SetSize(rowContainer:GetWidth(), height);
self.marquee:SetParent(self.rows);
-- Disable mask to allow mouse clicks to reach the cells
rowContainer.mask:SetMouseVisible(false);
-- Unless they're locked; then we still need to intercept mouse clicks
self:_PlaceColumnLockMasks();
else
self.rows:Focus();
end
local oldName = (oldSelectedRow and oldSelectedRow.name) or nil;
local newName = (rowContainer and rowContainer.name) or nil;
self:_DoCallbacks("SelectionChanged", { New = newName; Old = oldName });
end
function TableControl:Deselect()
self:_SelectRow(nil);
end
function TableControl:InsertRows(beforeRow, numNewRows)
end
function TableControl:RowIsVisible(row)
return (self.rows:GetItem(row):GetHeight() > 0);
end
function TableControl:EnsureVisible(rowName)
if (rowName) then
-- local left, top = self.rows:PointToScreen(0, 0);
local rowContainer = self.rowsByName[rowName];
if (rowContainer) then
-- local x, y = rowContainer:PointToScreen(0, 0);
self.rows:EnsureVisible(self.rows:IndexOfItem(rowContainer));
end
end
end
function TableControl:SetRowVisible(row, visible)
local rowContainer = self.rows:GetItem(row);
if (visible) then
-- Set container height to maximum of cell heights
local maxHeight = 0;
for _, cell in pairs(rowContainer.cells) do
maxHeight = math.max(maxHeight, cell:GetHeight());
end
if (rowContainer ~= self.lastRow) then
maxHeight = maxHeight + self.style.verticalCellSpacing;
end
rowContainer:SetHeight(maxHeight);
else
rowContainer:SetHeight(0);
end
end
-- Last row needs to be slightly different cosmetically
function TableControl:_UpdateLastRow()
local prev = self.lastRow;
local row = self.rows:GetItemCount();
if (row > 0) then
self.lastRow = self.rows:GetItem(row);
self:SetRowVisible(row, true);
else
self.lastRow = nil;
end
if (prev) then
self:SetRowVisible(self.rows:IndexOfItem(prev), true);
end
end
function TableControl:DeleteRow(rowName)
local rowContainer = self.rowsByName[rowName];
if (self.selectedRow == rowContainer) then
self:Deselect();
end
self.rows:RemoveItem(rowContainer);
self.rowsByName[rowName] = nil;
if (self.lastRow == rowContainer) then
self.lastRow = nil;
self:_UpdateLastRow();
end
end
function TableControl:GetRowCells(row)
return unpack(self.rows:GetItem(row).cells);
end
function TableControl:GetCell(row, col)
return self.rows:GetItem(row).cells[col];
end
function TableControl:GetHeading(colName)
local col = self.columnsByName[colName];
if (col) then
local column = self.columns[col];
return column.heading;
end
end
function TableControl:SetCell(row, col, object)
local column = self.columns[col];
local heading = column.heading;
local rowContainer = self.rows:GetItem(row);
-- Delete old cell (if any)
local oldCell = rowContainer.cells[col];
if (oldCell) then
oldCell:SetParent(nil);
if (oldCell.Close) then
oldCell:Close();
end
end
-- Create new cell, or use the supplied object
local newCell = object;
if (object == nil) then
newCell = self:_CreateCell(row, col);
end
-- Place the cell within in the row container
local left = heading:GetLeft();
local width = heading:GetWidth();
newCell:SetParent(rowContainer);
if (column.weight == 0) then
newCell:SetParent(nil);
end
rowContainer.cells[col] = newCell;
newCell:SetWidth(width);
newCell:SetPosition(left, top);
-- Update the row container height, if necessary
if (rowContainer:GetHeight() > 0) then
self:SetRowVisible(true);
end
return newCell;
end
function TableControl:_ApplyStyles(object, styles)
for s in keys(styles) do
local SetFunction = object["Set" .. s];
if (SetFunction) then
SetFunction(object, styles[s]);
end
end
end
function TableControl:_UpdateColumnWidths()
-- Find last visible column
local rightCol;
for col = 1, #self.columns do
local column = self.columns[col];
if (column.weight > 0) then
rightCol = col;
end
end
local totalWidth = self.rowWidth - ((#self.columns - 1) * self.style.horizontalCellSpacing);
local totalWeight = self.totalWeight;
local left = 0;
local weightExpended = 0;
for col = 1, #self.columns do
local column = self.columns[col];
local weight = column.weight;
if (weight > 0) then
local heading = column.heading;
local width = math.floor(0.5 + totalWidth * (weight / totalWeight));
width = math.min(width, self.rowWidth - left)
if (col == rightCol) then
width = self.rowWidth - left;
end
heading:SetLeft(left);
heading:SetWidth(width);
column.lockIcon:SetPosition(width - 19, -11);
-- optimization to do: only need to do the following loop if heading left/width changed
for row = 1, self.rows:GetItemCount() do
local cell = self.rows:GetItem(row).cells[col];
cell:SetLeft(left);
cell:SetWidth(width);
end
weightExpended = weightExpended + weight;
left = left + width + self.style.horizontalCellSpacing;
end
end
self:_PlaceEdgeMovers();
self:_PlaceColumnLockMasks();
end
function TableControl:SetSize(width, height)
self:SetWidth(width);
self:SetHeight(height);
end
function TableControl:SetWidth(width)
Turbine.UI.Control.SetWidth(self, width);
self.rowWidth = width;
if (self.vScrollBar:IsVisible()) then
self.rowWidth = width - 10;
self.vScrollBar:SetLeft(self.rowWidth);
end
self.rows:SetWidth(self.rowWidth);
for row = 1, self.rows:GetItemCount() do
self.rows:GetItem(row):SetWidth(self.rowWidth);
end
self:_UpdateColumnWidths();
end
function TableControl:SetHeight(height)
Turbine.UI.Control.SetHeight(self, height);
self.rows:SetHeight(height - self.rows:GetTop());
self.vScrollBar:SetHeight(height);
self:_PlaceEdgeMovers(); -- update EdgeMover heights
end
function TableControl:_PlaceEdgeMovers()
height = self:GetHeight();
for e = 1, #self.edgeMovers do
local edgeMover = self.edgeMovers[e];
local headingToLeft = self.columns[edgeMover.leftColumn].heading;
local headingToRight = self.columns[edgeMover.rightColumn].heading;
local left = headingToLeft:GetLeft() + headingToLeft:GetWidth();
local right = headingToRight:GetLeft();
if (left > right) then -- columns overlap?
left = right;
end
local center = (left + right) / 2;
local width = edgeMover:GetWidth();
left = math.floor(0.5 + center - (width / 2));
edgeMover:SetLeft(left);
edgeMover:SetHeight(height);
end
end
function TableControl:_PlaceColumnLockMasks()
if (self.selectedRow) then
for col = 1, #self.columns do
local column = self.columns[col];
if (column.locked and (column.weight > 0)) then
column.lockMask:SetParent(self.selectedRow);
column.lockMask:SetPosition(column.heading:GetLeft(), 0);
column.lockMask:SetSize(column.heading:GetWidth(), self.selectedRow:GetHeight());
column.lockMask:SetMouseVisible(true);
else
column.lockMask:SetMouseVisible(false);
end
end
end
end
-- For iterating over an array of cells in a for loop. Example:
-- To change the color of every cell in the first two columns:
-- for cell in tblCtl:CellIterator(1, 1, nil, 2) do
-- cell:SetBackColor(Turbine.UI.Color.White);
-- end
function TableControl:CellIterator(firstRow, firstCol, lastRow, lastCol)
if (firstRow == nil) then
firstRow = 1;
end
if (lastRow == nil) then
lastRow = self.rows:GetItemCount();
end
if (firstCol == nil) then
firstCol = 1;
end
if (lastCol == nil) then
lastCol = #self.columns;
end
local state = {
["self"] = self;
["firstRow"] = firstRow;
["firstCol"] = firstCol;
["lastRow"] = lastRow;
["lastCol"] = lastCol;
["row"] = firstRow;
["column"] = firstCol
};
local function iterator(state)
if (state.row > state.lastRow) then
return nil;
end
local cell = TableControl.GetCell(state.self, state.row, state.column);
state.column = state.column + 1;
if (state.column > state.lastCol) then
state.column = state.firstCol;
state.row = state.row + 1;
end
return cell;
end
return iterator, state, nil;
end
-- For calling member functions of many cells at once. Examples:
-- minCellHeight = math.min(t:CellAggregator(1, 1, 1, 3):GetHeight());
-- t:CellAggregator():SetBackColor(Turbine.UI.Color(1, 1, 1, 1));
function TableControl:CellAggregator(firstRow, firstCol, lastRow, lastCol)
local temp = {};
temp.iterator = {self:CellIterator(firstRow, firstCol, lastRow, lastCol)};
local temp_metatable = {};
temp_metatable.__index = function(tab, key)
rawset(tab, "funcName", key);
return tab;
end;
temp_metatable.__call = function(tab, _, ...)
local funcName = rawget(tab, "funcName");
local results = {};
for cell in unpack(rawget(tab, "iterator")) do
local r = {cell[funcName](cell, ...)};
for v in values(r) do
table.insert(results, v);
end
end
return unpack(results);
end;
setmetatable(temp, temp_metatable);
return temp;
end
Thurallor = Thurallor or {};
Thurallor.UI = Thurallor.UI or {};
Thurallor.UI.TableControl = TableControl;