lotrointerface.com
Search Downloads

LoTROInterface SVN Reminders

[/] [trunk/] [Thurallor/] [Common/] [UI/] [TableControl_2/] [TableControl.lua] - Rev 2

Compare with Previous | Blame | View Log

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;

Compare with Previous | Blame


All times are GMT -5. The time now is 12:40 PM.


Our Network
EQInterface | EQ2Interface | Minion | WoWInterface | ESOUI | LoTROInterface | MMOUI | Swtorui