lotrointerface.com
Search Downloads

LoTROInterface SVN SequenceBars

[/] [trunk/] [Thurallor/] [SequenceBars/] [Manager.lua] - Rev 216

Compare with Previous | Blame | View Log

Manager = class(Group);

function Manager:Constructor()
    Turbine.UI.Window.Constructor(self);
    self.constructing = true;

    self.manager = self;
    self.watcher = Thurallor.Utils.Watcher;
    self.objectID = "0";
    self.groupID = "0";

    self.objects = {};
    self.eventGenerators = {};
    self.eventCallbacks = {};
    self.eventListeners = {};
    self.eventLuaScripts = {};
    self.eventChatTriggers = {};
    self.keyDownHotkeys = {};
    self.keyUpHotkeys = {};
    self.propagatingEvent = {};
    self.orderOfPropagation = {};
    self.player = Turbine.Gameplay.LocalPlayer:GetInstance();
    self.username = self.player:GetName();
    self:SetUnequipDestination("1..." .. tostring(self.player:GetBackpack():GetSize()));
    self.hudVisible = true;
    self.includees = {};
    self.includers = {};
    self.lastActionTime = 0;
    self.mouseX, self.mouseY = Turbine.UI.Display.GetMousePosition();

    -- Default settings
    self.settings = {};
    self.settings.type = "group";
    self.settings.caption = {};
    self.settings.snapToGrid = false;
    self.settings.gridSize = 35;
    self.settings.language = Turbine.Engine:GetLanguage();
    local name = L:GetText("/Default/GlobalName");
    self.settings.caption.text = string.gsub(name, "<name>", self.username);
    self.settings.caption.width = 80;
    self.settings.caption.height = 20;
    self.settings.caption.font = Turbine.UI.Lotro.Font.TrajanPro15;
    self.settings.color = "FFFFFF";
    self.settings.barIDs = {};
    self.settings.groupIDs = { "new" };
    self.settings.deletedBarIDs = {};
    self.settings.deletedGroupIDs = {};
    self.settings.userEvents = {};
    self.settings.eventHandlers = {};
    self.settings.eventsEnabled = false;
    self.settings.groups = {};
    self.settings.bars = {};
    self.settings.nextObjectID = 1;
    self.settings.pluginVersion = plugin:GetVersion();
    self.settings.uiScale = 32; -- zoom level will be applied to make quickslots this size
    self.settings.uiOpacity = 1.0;

    self.settings.displaySize = { Turbine.UI.Display.GetSize() };
    self.globals = self.settings;
    self:LoadSettings();
    self.optionsPanel = OptionsPanel(self);

    -- Monitor chat messages for user event chat triggers
    self.colorStripper = Turbine.UI.Label();
    self.colorStripper:SetMarkupEnabled(true);
    self.chatReceivedCallback = function(_, args)
        self:ChatReceived(args);
    end
    AddCallback(Turbine.Chat, "Received", self.chatReceivedCallback);
end

function Manager:LoadSettings()
    Turbine.PluginData.Load(Turbine.DataScope.Character, "SequenceBars", function(loadStr)
        if (loadStr) then
            -- Save backup
            loadStr.backupTime = "#" .. Turbine.Engine.GetGameTime();
            local dateInfo = Turbine.Engine.GetDate();
            Turbine.PluginData.Save(Turbine.DataScope.Character, "SequenceBars_backup" .. dateInfo.DayOfWeek, loadStr);
        
            -- Workaround for Turbine localization bug -- Thanks, Lynx3d!
            local settings = ImportTable(loadStr);
            if (not settings) then
                Turbine.Shell.WriteLine("Failed to parse SequenceBars.plugindata file!");
                return;
            end
        
            -- Previously-saved settings override the defaults
            DeepTableCopy(settings, self.settings);
        end

        -- Make color object from hex string
        self.color = Thurallor.Utils.Color(1, 0, 0, 0);
        self.color:SetHex(self.settings.color);
        
        -- Select language
        self:SetLanguage(self.settings.language);

        -- Eliminate invalid shortcuts before trying to create the bars.
        self:CheckForDefunctSkills();

        _G.manualUpdatesNeeded = "";
        self:LoadBars();
        self:LoadGroups();
        self:UpdateDirectory();
        self.settings.pluginVersion = tonumber(Plugins.SequenceBars:GetVersion());
        self.constructing = nil;
        
        if (#_G.manualUpdatesNeeded > 0) then
            _G.manualUpdatesNeeded = "Since you have upgraded to a new version of SequenceBars, your settings file needs to be updated.  The following item(s) cannot be updated automatically, so you will have to do it yourself:\n\n" .. _G.manualUpdatesNeeded;
            Alert("Settings File Updated", _G.manualUpdatesNeeded, nil, Turbine.UI.Lotro.Font.Verdana14);
            _G.manualUpdatesNeeded = "";
        end

        self:SetWantsKeyEvents(true);
        self:SetWantsEvents(self.settings.eventsEnabled);

        -- Copy user event hotkeys and chat triggers into an efficient data structure
        for eventName, settings in pairs(self.settings.userEvents or {}) do
            for hotkey, text in pairs(settings.keyUpHotkeys or {}) do
                self:SetEventHotkey(eventName, hotkey, "KeyUp", text);
            end
            for hotkey, text in pairs(settings.keyDownHotkeys or {}) do
                self:SetEventHotkey(eventName, hotkey, "KeyDown", text);
            end
            for channel, patternInfo in pairs(settings.chatTriggers or {}) do
                for pattern, text in pairs(patternInfo) do
                    self:SetEventChatTrigger(eventName, channel, pattern, text);
                end
            end
        end

        -- Compile user event lua scripts
        for eventName, settings in pairs(self.settings.userEvents) do
            self:SetEventScript(eventName, settings.luaScript);
        end

        self.displaySizeCallback = function(display)
            self.anchorCoords = nil;
            self:SetGridDisplayEnable(self.showGrid);
        end
        AddCallback(Turbine.UI.Display, "SizeChanged", self.displaySizeCallback);
        
        -- In case the display size changed since the last time the plugin was used:
        self.anchorCoords = nil;
        self:SetWantsUpdates(true);

        self:PropagateEvent("Startup\n");
    end);
end

-- From time to time, SSG will remove skills from the game, invalidating existing shortcuts.
function Manager:CheckForDefunctSkills()
    local errors = {};
    local quickslot = Turbine.UI.Lotro.Quickslot();
    local invalidStr = L:GetText("/InvalidShortcut");
    for barID, barSettings in pairs(self.settings.bars) do
        local slotsInfo = barSettings.sequenceItemInfo;
        for s, info in ipairs(slotsInfo) do
            if ((not info.class) or (info.class == "Turbine.UI.Lotro.Quickslot")) then
                local shortcut = Turbine.UI.Lotro.Shortcut(info.type, info.Data);
                -- Make sure this skill/item still exists
                success, errorMsg = pcall(quickslot.SetShortcut, quickslot, shortcut);
                if (not success) then
                    -- Don't bother reporting the error for deleted bars.
                    if (not barSettings.deleted) then
                        key = shortcut:GetSkillName() or info.type .. "," .. info.Data;
                        key = invalidStr .. " (" .. key .. ")";
                        local barName = barSettings.caption.text;
                        errors[key] = errors[key] or {};
                        errors[key][barName] = errors[key][barName] or {};
                        table.insert(errors[key][barName], s );
                    end
                    slotsInfo[s] = {};
                end
            end
        end
    end
    slotStr = L:GetText("/BarMenu/SettingsMenu/CursorMenu/HomePositionMenu/Slot");
    for key, bars in pairs(errors) do
        Puts("<rgb=#FF0000>" .. key .. "</rgb>");
        for barName in sorted_keys(bars) do
            local slots = bars[barName];
            for n, slot in pairs(slots) do
                slots[n] = slotStr:gsub("<num>", tostring(slot));
            end
            Puts("  " .. barName .. " : " .. table.concat(slots, ", "));
        end
    end
end

function Manager:Unload()
    self:DoSave();
    self.constructing = false;
    self.editingCaption = true;

    -- The following may appear pointless, but failure to clean up (e.g. not unregistering event handlers) can cause crashes.
    for objectID, object in pairs(self.objects) do
--Puts("Destroying object " .. tostring(objectID));
        if (object) then
            object:Destroy();
        end
    end
end

function Manager:SaveSettings(updateBarDirectory, updateEventDirectory)
    if (updateBarDirectory) then
        self.barDirectoryUpdateNeeded = true;
    end
    if (updateEventDirectory) then
        self.eventDirectoryUpdateNeeded = true;
    end
    if (not self.constructing) then
        -- Start the idle timer.
        self.lastActionTime = Turbine.Engine.GetGameTime();
        self.waitingForIdle = true;
        self:SetWantsUpdates(true);
    end
end 

function Manager:Update()

    -- Propagate queued events
    if (next(self.propagatingEvent)) then
        local events = self.orderOfPropagation;
        self.propagatingEvent = {};
        self.orderOfPropagation = {};
        for _, eventName in ipairs(events) do
            self:PropagateEvent(eventName, true);
        end
    end

    -- Adjust bar layout based on new screen size
    if (not self.anchorCoords) then
        local speed = 0.2;
        local prevWidth, prevHeight = unpack(self.settings.displaySize);
        local width, height = Turbine.UI.Display:GetSize();
        local newWidth = prevWidth + (width - prevWidth) * speed;
        local newHeight = prevHeight + (height - prevHeight) * speed;
        if (math.abs(newWidth - width) < 1) then
            newWidth = width;
        end
        if (math.abs(newHeight - height) < 1) then
            newHeight = height;
        end
        self.settings.displaySize = { newWidth, newHeight };
        self:SaveSettings(false);
        self:Redraw(true);
        if ((newWidth ~= width) or (newHeight ~= height)) then
            self.anchorCoords = nil;
        end
    end

    if (self.waitingForIdle) then
        -- Mouse movements mean we're not idle
        mouseX, mouseY = Turbine.UI.Display.GetMousePosition();
        if ((mouseX ~= self.mouseX) or (mouseY ~= self.mouseY) or self.mouseLooking) then
            self.lastActionTime = Turbine.Engine.GetGameTime();
        end
        self.mouseX, self.mouseY = mouseX, mouseY;

        local currentTime = Turbine.Engine.GetGameTime();
        -- Bar/group directory updates will occur when idle time reaches 0.25 s
        if (self.barDirectoryUpdateNeeded or self.eventDirectoryUpdateNeeded) then
            if (currentTime >= self.lastActionTime + 0.25) then
                if (self.editingCaption) then
                    self:UpdateDirectory(false, self.eventDirectoryUpdateNeeded);
                else
                    self:UpdateDirectory(self.barDirectoryUpdateNeeded, self.eventDirectoryUpdateNeeded);
                    self.barDirectoryUpdateNeeded = false;
                end
                self.eventDirectoryUpdateNeeded = false;
            end

        -- Save will only occur when idle time reaches 10 s, and never in combat.
        elseif ((currentTime >= self.lastActionTime + 10) and not self.player:IsInCombat()) then
-- 14-Jul-2020: Data will now only be saved at logout.  Attempt to address occasional crashes that may be associated with frequent saving.
--            self:DoSave();
            self.waitingForIdle = false;
        end
    end
end
    
function Manager:DoSave()
--Puts("Saving...");
    -- Workaround for Turbine localization bug -- Thanks, Lynx3d!
    local saveData = ExportTable(self.settings);
    Turbine.PluginData.Save(Turbine.DataScope.Character, "SequenceBars", saveData, function()
--Puts("Save complete.");
    end);
end

function Manager:ShowDirectory(whichTab)
    if (self.optionsPanel) then
        self.optionsPanel:ShowDirectory(whichTab);
    end
end

function Manager:UpdateDirectory(updateBarDirectory, updateEventDirectory)
    if (self.optionsPanel) then
        self.optionsPanel:UpdateDirectory(updateBarDirectory, updateEventDirectory);
    end
end

function Manager:KeyDown(args)
    -- Keypresses (and mouse-looks) mean we're not idle
    self.lastActionTime = Turbine.Engine.GetGameTime();

    if (args.Action == Turbine.UI.Lotro.Action.RotateCharacter) then
        self.mouseLooking = true;
    end
    
    -- Respond to toggle UI command (usually F12)
    if (args.Action == Turbine.UI.Lotro.Action.ToggleHUD) then
        self.hudVisible = not self.hudVisible;
        self:ApplyHiddenness();
    end

    -- If this hotkey is assigned to a user event, generate it.
    for eventName, _ in pairs (self.keyDownHotkeys[args.Action] or {}) do
        self:PropagateEvent(eventName);
    end
end

function Manager:KeyUp(args)
    -- Keypresses (and mouse-looks) mean we're not idle
    self.lastActionTime = Turbine.Engine.GetGameTime();

    if (args.Action == Turbine.UI.Lotro.Action.RotateCharacter) then
        self.mouseLooking = false;
    end

    -- If this hotkey is assigned to a user event, generate it.
    for eventName, _ in pairs (self.keyUpHotkeys[args.Action] or {}) do
        self:PropagateEvent(eventName);
    end
end

function Manager:ChatReceived(args)
    local channelInfo = self.eventChatTriggers[args.ChatType];
    if (channelInfo) then
        self.colorStripper:SetText(args.Message);
        local message = self.colorStripper:GetText();
        for pattern, eventInfo in pairs(channelInfo) do
            if (string.match(message, pattern)) then
                for eventName, _ in pairs(eventInfo) do
                    self:PropagateEvent(eventName, true);
                end
            end
        end
    end
end

function Manager:IsParentHidden()
    return (not self.hudVisible);
end

function Manager:ShellCommand(cmd, args)
    if (string.match(args, "^" .. L:GetText("/ShellCommand/Options"))) then
        self.optionsPanel:ShowGlobalSettings();
    elseif (string.match(args, "^" .. L:GetText("/ShellCommand/Events"))) then
        local eventNames = self:GetEventNames();
        if (#eventNames > 0) then
            Puts(table.concat(eventNames, "\n"));
        end
    elseif (string.match(args, "^" .. L:GetText("/ShellCommand/Event"))) then
        local eventName = string.sub(args, string.find(args, " ") + 1);
        Puts(" -> \"" .. eventName .. "\"");
        self:PropagateEvent(eventName);
    elseif (string.match(args, "^" .. L:GetText("/ShellCommand/Sort"))) then
        local slots = string.sub(args, string.find(args, " ") + 1);
        local series = Thurallor.Utils.Series(slots);
        local backpack = Thurallor.Utils.Watcher.playerBackpackObject;
        local safety = backpack:GetSize();
        local items = {};
        local function comp(a, b)
            if (a:GetCategory() > b:GetCategory()) then
                return true;
            elseif (a:GetCategory() == b:GetCategory()) then
                if (a:GetIconImageID() > b:GetIconImageID()) then
                    return true;
                elseif (a:GetIconImageID() == b:GetIconImageID()) then
                    if (a:GetName() < b:GetName()) then
                        return true;
                    elseif (a:GetName() == b:GetName()) then
                        if (a:GetQuantity() > b:GetQuantity()) then
                            return true;
                        end
                    end
                end
            end
            return false;
        end
        for bpSlot in series:numbers(safety) do
            local item = backpack:GetItem(bpSlot);
            if (item) then
                item.slot = bpSlot;
                table.insert(items, item);
            end
            table.sort(items, comp);
        end
        for bpSlot in series:numbers(safety) do
            local item = table.remove(items, 1);
            if (item and (item.slot ~= bpSlot)) then
                backpack:PerformItemDrop(item, bpSlot, false);
            end
        end
    else
        -- Unknown command.
        return false;
    end
    return true;
end

function Manager:FindNewObjectID()
    local id = self.settings.nextObjectID;
    self.settings.nextObjectID = id + 1;
    id = self.username .. "." .. tostring(id);
    self.objects[id] = { "reserved" };
    return id;
end

function Manager:GetObject(objectID)
    return self.objects[objectID];
end

function Manager:TransferBar(barID, oldGroup, newGroup)
    local foundPos = Search(oldGroup.settings.barIDs, barID);
    if (foundPos) then
        table.remove(oldGroup.settings.barIDs, foundPos);
        table.insert(newGroup.settings.barIDs, barID);
        local bar = self.objects[barID];
        bar.parent = newGroup;
        self:SaveSettings(true);
        newGroup:ApplyHiddenness();
    end
end

function Manager:TransferGroup(groupID, oldGroup, newGroup)
    local foundPos = Search(oldGroup.settings.groupIDs, groupID);
    if (foundPos) then
        table.remove(oldGroup.settings.groupIDs, foundPos);
        table.insert(newGroup.settings.groupIDs, groupID);
        local group = self.objects[groupID];
        group.parent = newGroup;
        self:SaveSettings(true);
        newGroup:ApplyHiddenness();
    end
end

function Manager:EmptyTrash()
    self.settings.deletedGroupIDs = {};
    for groupID, settings in pairs(self.settings.groups) do
        settings.deletedGroupIDs = {};
        settings.deletedBarIDs = {};
        if (not self.objects[groupID]) then
            self.settings.groups[groupID] = nil;
        end
    end
    self.settings.deletedBarIDs = {};
    for barID, bar in pairs(self.settings.bars) do
        if (not self.objects[barID]) then
            self.settings.bars[barID] = nil;
        end
    end
    self:SaveSettings(false);
end

function Manager:AddSettingsMenuItem(parent, itemName, amSubMenuOf, fromOptionsPanel)
    self.amSubMenuOf = amSubMenuOf or self.amSubMenuOf;
    local item = Turbine.UI.MenuItem(L:GetText(itemName), true, false);
    item._name = itemName;
    item._parent = parent;
    parent._itemsByName = parent._itemsByName or {};
    parent._itemsByName[itemName] = item;
    parent:GetItems():Add(item);

    if (itemName == "Root") then
        local prevContext = L:SetContext("/GlobalMenu");
        parent:GetItems():Clear();
        self:AddSettingsMenuItem(parent, "HideAll");
        if (fromOptionsPanel) then
            L:SetContext("/GroupMenu");
            Group.AddSettingsMenuItem(self, parent, "CreateNewBar");
            Group.AddSettingsMenuItem(self, parent, "CreateNewSubgroup");
            Group.AddSettingsMenuItem(self, parent, "Import");
        end
        if (#self.settings.deletedBarIDs + #self.settings.deletedGroupIDs > 0) then
            self:AddSettingsMenuItem(parent, "Undelete");
        end
        L:SetContext("/GroupMenu");
        Group.AddSettingsMenuItem(self, parent, "ArrangeBars");
        Group.AddSettingsMenuItem(self, parent, "GlobalEventBehaviors");
        L:SetContext("/GlobalMenu");
        self:AddSettingsMenuItem(parent, "GlobalSettings");
        if (self:HaveTrash()) then
            self:AddSettingsMenuItem(parent, "EmptyTrash");
        end
        L:SetContext(prevContext);
    elseif (itemName == "HideAll") then
        item:SetChecked(self.settings.hidden);
        item.Click = function()
            self:SetHidden(not self.settings.hidden);
        end
    elseif (itemName == "GlobalSettings") then
        item.Click = function()
            self.optionsPanel:ShowGlobalSettings();
        end
    elseif (itemName == "EmptyTrash") then
        item.Click = function()
            self:EmptyTrash();
        end
    else
        -- Item is not handled here; send it to Group:AddSettingsMenuItem().
        parent:GetItems():RemoveAt(parent:GetItems():GetCount());
        Group.AddSettingsMenuItem(self, parent, itemName);
    end
    return item;
end

function Manager:GetNearestGridPos(left, top, uiScale)
    local gridSize = math.floor(0.5 + self.settings.gridSize * (uiScale / 32));
    local displayBottom = Turbine.UI.Display:GetHeight() - 1;
    local centerLeft = math.floor(0.5 + Turbine.UI.Display:GetWidth() / 2 + gridSize / 2) + 3;
    local offsetLeft, offsetTop = left - centerLeft, displayBottom - top;
    local newLeft = centerLeft + math.floor(0.5 + offsetLeft / gridSize) * gridSize;
    local newTop = displayBottom - math.floor(0.5 + offsetTop / gridSize) * gridSize;
    return newLeft, newTop, gridSize;
end

function Manager:GetAnchorPosition(anchor)
    if (not self.anchorCoords) then
        local width, height = unpack (self.settings.displaySize);
        local center = math.floor(0.5 + width / 2);
        local middle = math.floor(0.5 + height / 2);
        self.anchorCoords = {
            TopLeft    = { 0, 0      }; TopCenter    = { center, 0      }; TopRight    = { width, 0      };
            MiddleLeft = { 0, middle }; MiddleCenter = { center, middle }; MiddleRight = { width, middle };
            BottomLeft = { 0, height }; BottomCenter = { center, height }; BottomRight = { width, height };
        }
    end
    return unpack(self.anchorCoords[anchor]);
end

function Manager:SetUnequipDestination(bagSlots)
    self.preferredBagSlots = bagSlots;
    self.preferredBagSlotSeries = Thurallor.Utils.Series(bagSlots);
end

function Manager:Unequip(eqSlot)
    local _, item = Thurallor.Utils.Watcher.GetEquippedItem(nil, eqSlot);
    if (item == nil) then
        return;
    end

    local preferred_slots = self.preferredBagSlotSeries;
    local backpack = Thurallor.Utils.Watcher.playerBackpackObject;
    if (not backpack.cache) then
        self.watcher.GetItemQuantity(nil); -- force cache hit
    end
    if (backpack.cache.recentlyFilled == nil) then
        backpack.cache.recentlyFilled = {}; -- cache for keeping track of changes between ticks
    end
    local function trySlot(bpSlot)
        if (not (backpack.cache.recentlyFilled[bpSlot] or backpack:GetItem(bpSlot))) then
            backpack:PerformItemDrop(item, bpSlot);
            backpack.cache.recentlyFilled[bpSlot] = item;
--Puts("Unequipping " .. tostring(item:GetName()) .. " to " .. tostring(bpSlot));
            return true;
        end
    end
    
    -- Search the preferred slots looking for an empty one.
    local safety = backpack:GetSize();
    local maxFullSlot = 1;
    for bpSlot in preferred_slots:numbers(safety) do
        if (trySlot(bpSlot)) then
            return;
        else
            maxFullSlot = math.max(maxFullSlot, bpSlot);
        end
    end
    --Puts(string.gsub(L:GetText("/BagSlotsFull"), "<slots>", self.preferredBagSlots));

    -- None of the preferred slots are empty.  Try bag slots after the preferred range.
    for bpSlot = maxFullSlot + 1, safety do
        if (trySlot(bpSlot)) then
            return;
        end
    end

    -- Try bag slots before the preferred range.
    for bpSlot = 1, maxFullSlot - 1 do
        if (trySlot(bpSlot)) then
            return;
        end
    end

    -- All bag slots are full!
    Puts(L:GetText("/BagsFull"));
end

-- Some trivial wrappers retained for backward compatibility
function Manager:GetSkills()
    return self.watcher.GetSkillsInfo();
end
function Manager:SkillReady(iconID)
    return self.watcher.SkillReady(iconID);
end
function Manager:PlayerHasEffectCategory(category)
    return self.watcher.PlayerHasEffectCategory(category);
end
function Manager:TargetHasEffectCategory(category)
    return self.watcher.TargetHasEffectCategory(category);
end

function Manager:SetLanguage(language)
    self.settings.language = language;
    L:SetLanguage(language);
    if (language == Turbine.Language.Russian) then
        SetCyrillicEnabled(true);
    else
        SetCyrillicEnabled(false);
    end
    if (self.optionsPanel) then
        self.optionsPanel:Localize();
    end
    self:SaveSettings(false);
end

function Manager:SetUseOnRightClick(useOnRightClick)
    self.settings.useOnRightClick = useOnRightClick;
    Group.SetUseOnRightClick(self, useOnRightClick);
    self:SaveSettings(false);
end

function Manager:SetSnapToGrid(snapToGrid)
    self.settings.snapToGrid = snapToGrid;
    if (snapToGrid) then
        self:SnapToGrid();
    end
    self:SaveSettings(false);
end

function Manager:SetGridDisplayEnable(enable)
    self.showGrid = enable;
    if (self.gridDisplay) then
        self.gridDisplay:Close();
        self.gridDisplay = nil;
    end
    if (enable) then
        local left, top, scaledGridSize = self:GetNearestGridPos(0, 0, self.settings.uiScale);
        left, top = left - scaledGridSize, top - scaledGridSize;
        local width, height = self:GetNearestGridPos(Turbine.UI.Display:GetWidth(), Turbine.UI.Display:GetHeight(), self.settings.uiScale);
        width, height = width + (2 * scaledGridSize), height + (2 * scaledGridSize);
        local cellsWide, cellsHigh = width / scaledGridSize, height / scaledGridSize;
        self.gridDisplay = Turbine.UI.Window();
        self.gridDisplay:SetPosition(left, top);
        self.gridDisplay:SetZOrder(-2147483647);
        self.gridDisplay:SetVisible(true);
        self.gridDisplay:SetOpacity(0.5);
        self.gridDisplay:SetMouseVisible(false);
        self.gridDisplay:SetBackground("Thurallor/SequenceBars/Images/grid.tga");
        self.gridDisplay:SetSize(cellsWide * 35, cellsHigh * 35);
        if (scaledGridSize ~= 35) then
            self.gridDisplay:SetStretchMode(1);
            self.gridDisplay:SetSize(width, height);
        end
    end
end

function Manager:HighlightGrid(left, top, slotsRight, slotsDown, uiScale)
    if (not self.gridHighlight) then
        self.gridHighlight = Turbine.UI.Window();
        self.gridHighlight:SetZOrder(-2147483647);
        self.gridHighlight:SetVisible(true);
        self.gridHighlight:SetMouseVisible(false);
        self.gridHighlight:SetBackground(resources.Icon.Grid);
        self.gridHighlight:SetStretchMode(0);
        self.gridHighlight:SetSize(slotsRight * self.settings.gridSize, slotsDown * self.settings.gridSize);
        self.gridHighlight:SetStretchMode(1);
        self.gridHighlight.slotsRight, self.gridHighlight.slotsDown = slotsRight, slotsDown;
    end
    left, top, scaledGridSize = self:GetNearestGridPos(left, top, uiScale);
    self.gridHighlight.prevLeft, self.gridHighlight.prevTop = left, top;
    self.gridHighlight:SetSize(slotsRight * scaledGridSize, slotsDown * scaledGridSize);
    self.gridHighlight:SetPosition(left, top);
end

function Manager:UnhighlightGrid()
    if (self.gridHighlight) then
        self.gridHighlight:Close();
        self.gridHighlight = nil;
    end
end

function Manager:ShowAnchorIcon(anchorPos, x, y)
    if (anchorPos) then
        if (not self.anchorIcon) then
            local width, height = Turbine.UI.Display:GetSize();
            local center, middle = math.floor(0.5 + width / 2), math.floor(0.5 + height / 2);
            local coords = {
                TopLeft =      { 0,           0           };
                TopCenter =    { center - 32, 0           };
                TopRight =     { width - 64,  0           };
                MiddleLeft =   { 0,           middle - 44 };
                MiddleCenter = { center - 32, middle - 44 };
                MiddleRight =  { width - 64,  middle - 44 };
                BottomLeft =   { 0,           height - 88 };
                BottomCenter = { center - 32, height - 88 };
                BottomRight =  { width - 64,  height - 88 };
            }
            local anchorX, anchorY = unpack(coords[anchorPos]);

            self.anchorIcon = Turbine.UI.Window();
            self.anchorIcon:SetMouseVisible(false);
            self.anchorIcon:SetBackground("Thurallor/SequenceBars/Images/anchor.tga");
            self.anchorIcon:SetSize(64, 88);
            self.anchorIcon:SetPosition(anchorX, anchorY);
            self.anchorIcon:SetVisible(true);

            local color = Thurallor.Utils.Color();
            color:SetHex("d8cf74"); -- primary color of anchor icon
            self.anchorLine = Thurallor.UI.Line(anchorX + 31, anchorY + 12, 0, 0, 3, color);
            self.anchorLine:SetMouseVisible(false);
        end
        self.anchorLine:SetEndPoint(x, y);
    elseif (self.anchorIcon) then
        self.anchorIcon:Close();
        self.anchorLine:Close();
        self.anchorIcon, self.anchorLine = nil, nil;
    end
end

function Manager:SetUIOpacity(opacity)
    self.settings.uiOpacity = opacity;
    self:Redraw(true);
    self:SaveSettings(false);
end

function Manager:SetUIScale(scale)
    self.settings.uiScale = scale;
    self:SetGridDisplayEnable(self.showGrid);
    self:Redraw(true);
    self:SaveSettings(false);
end

function Manager:SetIncludees(includer, includees)
--Puts(tostring(includer) .. " is including " .. Serialize(includees));
    if (not includees) then
        -- Destroying bar.  Notify includers.
        self:NotifyIncluders(includer);
    end
    
    -- Delete the previous includes mapping (if any)
    local prevIncludes = self.includees[includer];
    if (prevIncludes) then
        for i = 1, #prevIncludes do
            local includee = prevIncludes[i];
            self.includers[includee][includer] = nil;
--Puts(tostring(includee) .. " will no longer notify " .. tostring(includer));
        end
    end
    
    -- Create the new includes mapping (if any)
    self.includees[includer] = includees;
    if (includees) then
        for i = 1, #includees, 1 do
            local includee = includees[i];
            if (not self.includers[includee]) then
                self.includers[includee] = {};
            end
            self.includers[includee][includer] = true;
--Puts(tostring(includee) .. " will notify " .. tostring(includer));
        end
    end
end

-- This function gets called when a sequence changes, so any bars that include that sequence can be notified to update.
function Manager:NotifyIncluders(includee)
--Puts(tostring(includee) .. " changed");
    if (not self.includers[includee]) then
        return;
    end
    self.notifyList = {};
    self:FindDependencies(includee);
    for barID in keys(self.notifyList) do
        if (barID ~= includee) then
            local bar = self.objects[barID];
            if (bar) then
--Puts("Notifying " .. tostring(barID));
                bar:ShortcutChanged();
            end
        end
    end
    self.notifyList = nil;
end

function Manager:FindDependencies(includee)
    if (self.includers[includee]) then
        for includer in keys(self.includers[includee]) do
            if (not self.notifyList[includer]) then
                self.notifyList[includer] = true;
                self:FindDependencies(includer);
            end
        end
    end
end

--function Manager:AddEventGenerator(bar, eventName)
--    if (not self.eventGenerators[eventName]) then
--        self.eventGenerators[eventName] = {};
--    end
----Puts("Adding event generator for event " .. Serialize(eventName) .. " from bar " .. Serialize(bar.settings.caption.text));
--    self.eventGenerators[eventName][bar] = true;
--    return bar;
--end
--
--function Manager:RemoveEventGenerator(bar, eventName)
----Puts("Removing event generator for event " .. Serialize(eventName) .. " from bar " .. Serialize(bar.settings.caption.text));
--    local eventGenerators = self.eventGenerators[eventName];
--    if (eventGenerators) then
--        eventGenerators[bar] = nil;
--        if (not next(eventGenerators)) then
--            self.eventGenerators[eventName] = nil;
--        end
--    else
----Puts("Not registered!");
--    end
--end

function Manager:SetEventGenerators(enable, bar, userEventsGenerated)
    local eventGenerators = self.eventGenerators;
    for eventName, bars in pairs(eventGenerators) do
        bars[bar] = nil;
        if (not next(bars)) then
            eventGenerators[eventName] = nil;
        end
    end
    if (enable) then
        local barID = bar:GetID();
        for eventName, includeIDs in pairs(userEventsGenerated) do
            eventGenerators[eventName] = eventGenerators[eventName] or {};
            eventGenerators[eventName][bar] = {};
            for otherBarID, _ in pairs(includeIDs) do
                if (otherBarID ~= barID) then
                    eventGenerators[eventName][bar][otherBarID] = true;
                end
            end
        end
    end
    self.eventDirectoryUpdateNeeded = true;
end

function Manager:SetEventListeners(enable, node, userEventsWanted)
    local eventListeners = self.eventListeners;
    for eventName, nodes in pairs(eventListeners) do
        nodes[node] = nil;
        if (not next(nodes)) then
            eventListeners[eventName] = nil;
        end
    end
    if (enable) then
        for eventName, _ in pairs(userEventsWanted) do
            eventListeners[eventName] = eventListeners[eventName] or {};
            eventListeners[eventName][node] = true;
        end
    end
    self.eventDirectoryUpdateNeeded = true;
end

-- Returns a list of bars that generates the specified event
function Manager:GetEventGenerators(eventName)
    local bars = self.eventGenerators[eventName];
    if (bars) then
        local barList, includes = {}, {};
        for bar, incl in pairs(bars) do
            table.insert(barList, bar);
            includes[bar] = {};
            for barID, _ in pairs(incl) do
                local otherBar = self.objects[barID];
                includes[bar][otherBar] = true;
            end
        end
--Puts("barList for " .. eventName .. " is " .. Serialize(barList, 1));
        return barList, includes;
    end
end

function Manager:SetEventScript(eventName, luaScript)
    local settings = self.settings.userEvents[eventName] or {};
    self.settings.userEvents[eventName] = settings;
    settings.luaScript = luaScript;
    local func, errMsg;
    if (luaScript) then
        func, errMsg = loadstring("setfenv(1, _G);" .. luaScript);
        if (not func) then
            errMsg = errMsg:gsub("^%[[^%]]+%]:", "");
            func = function()
                Puts("(" .. eventName .. ") Lua script parse error in line " .. errMsg);
            end
        end
    end
    self.eventLuaScripts[eventName] = func;
    return (not errMsg);
end

-- Sets (or clears, if text == nil) a chat trigger for the specified user event.
function Manager:SetEventChatTrigger(eventName, channel, pattern, text)
    -- Update settings
    local settings = self.settings.userEvents[eventName] or {};
    self.settings.userEvents[eventName] = settings;
    local triggerSettings = settings.chatTriggers or {};
    settings.chatTriggers = triggerSettings;
    local chanInfo = triggerSettings[channel] or {};
    triggerSettings[channel] = chanInfo;
    chanInfo[pattern] = text;

    -- Update look-up tables
    local channelInfo = self.eventChatTriggers[channel] or {};
    self.eventChatTriggers[channel] = channelInfo;
    local patternInfo = channelInfo[pattern] or {};
    channelInfo[pattern] = patternInfo;
    patternInfo[eventName] = text;
    if (not next(patternInfo)) then
        channelInfo[pattern] = nil;
    end
    if (not next(channelInfo)) then
        self.eventChatTriggers[channel] = nil;
    end
end

-- Sets (or clears, if text == nil) a hotkey for the specified user event.
function Manager:SetEventHotkey(eventName, hotkey, event, text)
    local settings = self.settings.userEvents[eventName] or {};
    self.settings.userEvents[eventName] = settings;
    if (event == "KeyUp") then
        settings.keyUpHotkeys = settings.keyUpHotkeys or {};
        settings.keyUpHotkeys[hotkey] = text;
        local hotkeyInfo = self.keyUpHotkeys[hotkey] or {};
        self.keyUpHotkeys[hotkey] = hotkeyInfo
        hotkeyInfo[eventName] = text;
        if (not next(hotkeyInfo)) then
            self.keyUpHotkeys[hotkey] = nil;
        end
    else -- (event == "KeyDown")
        settings.keyDownHotkeys = settings.keyDownHotkeys or {};
        settings.keyDownHotkeys[hotkey] = text;
        local hotkeyInfo = self.keyDownHotkeys[hotkey] or {};
        self.keyDownHotkeys[hotkey] = hotkeyInfo
        hotkeyInfo[eventName] = text;
        if (not next(hotkeyInfo)) then
            self.keyDownHotkeys[hotkey] = nil;
        end
    end
end

-- Clears all chat triggers (if any) for the specified user event.
function Manager:ClearEventChatTriggers(eventName)
    local settings = self.settings.userEvents[eventName] or {};
    for channel, patternInfo in pairs(settings.chatTriggers or {}) do
        for pattern, description in pairs(patternInfo) do
            self:SetEventChatTrigger(eventName, channel, pattern, nil);
        end
    end
end

-- Clears all hotkeys (if any) for the specified user event.
function Manager:ClearEventHotkeys(eventName)
    local settings = self.settings.userEvents[eventName] or {};
    for hotkey, text in pairs(settings.keyDownHotkeys or {}) do
        self:SetEventHotkey(eventName, hotkey, "KeyDown", nil);
    end
    for hotkey, text in pairs(settings.keyUpHotkeys or {}) do
        self:SetEventHotkey(eventName, hotkey, "KeyUp", nil);
    end
end

-- Gets a table of the specified user event's chat triggers (if any), sorted by description.
function Manager:GetEventChatTriggers(eventName)
    local settings = self.settings.userEvents[eventName] or {};
    local infoList = {};
    for channel, patternInfo in pairs(settings.chatTriggers or {}) do
        for pattern, description in pairs(patternInfo) do
            table.insert(infoList, { channel, pattern, description });
        end
    end
    if (next(infoList)) then
        table.sort(infoList, function(a, b)
            return (a[3] < b[3]);
        end);
        return infoList;
    end
end

-- Gets a text description for the specified user event's hotkeys (if any).
function Manager:GetEventHotkeys(eventName)
    local settings = self.settings.userEvents[eventName] or {};
    local textList = {};
    for hotkey, text in pairs(settings.keyDownHotkeys or {}) do
        table.insert(textList, text);
    end
    for hotkey, text in pairs(settings.keyUpHotkeys or {}) do
        table.insert(textList, text);
    end
    if (next(textList)) then
        table.sort(textList);
        return textList;
    end
end

-- Returns a list of bars/groups that are listening for the specified event
function Manager:GetEventListeners(eventName)
    local nodes = self.eventListeners[eventName];
    if (nodes) then
        local nodeList = {};
        for node, _ in pairs(nodes) do
            table.insert(nodeList, node);
        end
        return nodeList;
    end
end

-- Returns the names of all of the events generated by all bars, hotkeys, or chat triggers
function Manager:GetEventNames()
    local foundEventNames = {};
    for eventName, _ in pairs(self.eventGenerators) do
        foundEventNames[eventName] = true;
    end
    for eventName, settings in pairs(self.settings.userEvents) do
        if (next(settings)) then
            foundEventNames[eventName] = true;
        end
    end

    local eventNames = {};
    for eventName, _ in pairs(foundEventNames) do
        table.insert(eventNames, eventName);
    end
    table.sort(eventNames);
    return eventNames;
end

function Manager:CreateUserEvent(eventName)
    self.settings.userEvents[eventName] = { explicitlyCreated = true; };
    self:SaveSettings(false);
end

-- Note: This only deletes the hotkey and lua script (if any) after the event has already been removed from bars/groups
function Manager:DeleteUserEvent(eventName)
    if (self.eventListeners[eventName] or self.eventGenerators[eventName]) then
        return Puts("Error: Can't delete");
    end
    self:ClearEventHotkeys(eventName);
    self:SetEventScript(eventName, nil);
    self.settings.userEvents[eventName] = nil;
    self:SaveSettings(false);
end

function Manager:RenameUserEvent(oldName, newName)

    -- Update settings
    self.settings.userEvents[newName] = self.settings.userEvents[oldName]
    self.settings.userEvents[oldName] = nil;

    -- Update global data structures
    if (self.eventLuaScripts[oldName]) then
        self.eventLuaScripts[newName] = self.eventLuaScripts[oldName];
        self.eventLuaScripts[oldName] = nil;
    end
    for hotkey, action in pairs(self.keyDownHotkeys) do
        if (action[oldName]) then
           action[newName] = action[oldName];
           action[oldName] = nil;
        end
    end
    for hotkey, action in pairs(self.keyUpHotkeys) do
        if (action[oldName]) then
           action[newName] = action[oldName];
           action[oldName] = nil;
        end
    end

    -- Make a list of affected bars/groups
    local nodes = {};
    for node, _ in pairs(self.eventListeners[oldName] or {}) do
        nodes[node] = true;
    end
    for node, _ in pairs(self.eventGenerators[oldName] or {}) do
        nodes[node] = true;
    end

    -- Update bars/groups
    for node, _ in pairs(nodes) do
        node:RenameUserEvent(oldName, newName);
    end

    -- Rebuild bars
    for node, _ in pairs(nodes) do
        if (not node:CanHaveChildren()) then
            node:Rebuild();
        end
    end

    -- Redraw event directory table
    self:SaveSettings(false, true);
end

-- Note: the returned table may have empty elements due to RemoveCallback()
function Manager:GetEventCallbacks()
    return self.eventCallbacks;
end

function Manager:PropagateEvent(eventName, now)
    if (now) then
        local func = self.eventLuaScripts[eventName];
        if (func) then
            local success, errMsg = pcall(func, eventName, self.player, self);
            if (not success) then
                errMsg = errMsg:gsub("^%[[^%]]+%]:", "");
                Puts("(" .. eventName .. ") Lua script run-time error in line " .. errMsg);
            end
        end
        DoCallbacks(self.eventCallbacks, eventName, eventName);
    else
        -- Enqueue event for processing at next Update cycle.  We use a hash table to avoid
        -- generating the same event multiple times in the same tick, which would be useless
        -- and could cause stack overflow.
        if (not self.propagatingEvent[eventName]) then
            self.propagatingEvent[eventName] = true;
            table.insert(self.orderOfPropagation, eventName);
        end
    end
end

function Manager:Destroy()
    RemoveCallback(Turbine.Chat, "Received", self.chatReceivedCallback);
    self:SetWantsKeyEvents(false);
    self:SetWantsEvents(false);
    Node.Destroy(self);
end

Compare with Previous | Blame


All times are GMT -5. The time now is 09:01 PM.


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