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