Manager = class(Group);
function Manager:Constructor()
Turbine.UI.Window.Constructor(self);
self.constructing = true;
self.manager = self;
self.watcher = Thurallor.Utils.Watcher;
self.groupID = "0";
self.objects = {};
self.eventGenerators = {};
self.eventCallbacks = {};
self.propagatingEvent = {};
self.lastSaveTime = 0;
self.player = Turbine.Gameplay.LocalPlayer:GetInstance();
self.username = self.player:GetName();
self.preferredBagSlot = 1;
self.hudVisible = true;
self.includees = {};
self.includers = {};
self.idleTime = Turbine.Engine.GetGameTime();
-- 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.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();
end
function Manager:LoadSettings()
Turbine.PluginData.Load(Turbine.DataScope.Character, "SequenceBars", function(loadStr)
if (loadStr) then
-- 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);
self:LoadBars();
self:LoadGroups();
self:UpdateOptionsPanel();
self.settings.pluginVersion = tonumber(Plugins.SequenceBars:GetVersion());
self.constructing = nil;
self:SetWantsKeyEvents(true);
self:SetWantsEvents(self.settings.eventsEnabled);
self.displaySizeCallback = function(display)
self.anchorCoords = nil;
self:SetWantsUpdates(true);
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
function Manager:Unload()
self.lastSaveTime = 0;
self.constructing = false;
self.editingCaption = true;
self.lastSaveTime = 0; -- force save
self:SaveSettings(false);
-- 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(updateOptionsPanel)
if (updateOptionsPanel) then
self.updateOptionsPanel = true;
end
if (not self.constructing) then
-- Start the idle timer.
self.idleTime = Turbine.Engine.GetGameTime() + 0.25;
self.AfterIdle = self.DoSave;
self:SetWantsUpdates(true);
end
end
function Manager:Update()
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
return;
end
-- Save will only occur when idle time reaches 0.25 s.
local currentTime = Turbine.Engine.GetGameTime();
if (currentTime < self.idleTime) then
return;
end
if (self.AfterIdle) then
self:AfterIdle();
end
self:SetWantsUpdates(false);
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);
if (self.updateOptionsPanel and (not self.editingCaption)) then
self:UpdateOptionsPanel();
self.updateOptionsPanel = false;
end
end
function Manager:UpdateOptionsPanel()
if (not self.optionsPanel) then
self.optionsPanel = OptionsPanel(self);
end
self.optionsPanel:RefreshDirectory();
end
function Manager:KeyDown(args)
if (args.Action == Turbine.UI.Lotro.Action.ToggleHUD) then
self.hudVisible = not self.hudVisible;
self:ApplyHiddenness();
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);
end
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:ShowSettingsMenu(fromOptionsPanel)
-- Build the settings menu.
self.settingsMenu = Turbine.UI.ContextMenu();
Group.AddSettingsMenuItem(self, self.settingsMenu, "Root", fromOptionsPanel);
self.amSubMenu = false;
self:AddSettingsMenuItem(self.settingsMenu, "Root");
self.settingsMenu:ShowMenu();
end
function Manager:AddSettingsMenuItem(parent, itemName, amSubMenu)
if (amSubMenu ~= nil) then
self.amSubMenu = amSubMenu;
end
local item = Turbine.UI.MenuItem(L:GetText(itemName), true, false);
parent:GetItems():Add(item);
if (itemName == "Root") then
local prevContext = L:SetContext("/GlobalMenu");
parent:GetItems():Clear();
self:AddSettingsMenuItem(parent, "HideAll");
if (not self.amSubMenu) 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(sender, args)
self:SetHidden(not self.settings.hidden);
end
elseif (itemName == "GlobalSettings") then
item.Click = function(sender, args)
self.optionsPanel:ShowGlobalSettings();
end
elseif (itemName == "EmptyTrash") then
item.Click = function(sender, args)
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, true);
end
end
function Manager:GetNearestGridPos(left, top)
local gridSize = math.floor(0.5 + self.settings.gridSize * (self.settings.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(bagSlot)
self.preferredBagSlot = bagSlot;
end
function Manager:Unequip(itemSlot)
local lp = Turbine.Gameplay.LocalPlayer:GetInstance();
local equippedItems = lp:GetEquipment();
local item = equippedItems:GetItem(itemSlot);
if (item ~= nil) then
_G.Unequip(itemSlot, "*", self.preferredBagSlot);
end
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);
Turbine.UI.ContextMenu = Turbine.UI.ContextMenu2;
Turbine.UI.MenuItem = Turbine.UI.MenuItem2;
else
Turbine.UI.ContextMenu = Turbine.UI.ContextMenu1;
Turbine.UI.MenuItem = Turbine.UI.MenuItem1;
SetCyrillicEnabled(false);
end
if (self.optionsPanel) then
self.optionsPanel:Localize();
end
self:SaveSettings(false);
end
function Manager:SetSnapToGrid(snapToGrid)
self.settings.snapToGrid = snapToGrid;
if (snapToGrid) then
self:SnapToGrid();
end
self:SaveSettings(false);
end
function Manager:ShowGrid(left, top, slotsRight, slotsDown)
if (not self.gridDisplay) then
self.gridDisplay = Turbine.UI.Window();
self.gridDisplay:SetVisible(true);
self.gridDisplay:SetMouseVisible(false);
self.gridDisplay:SetBackground(resources.Icon.Grid);
self.gridDisplay:SetStretchMode(0);
self.gridDisplay:SetSize(slotsRight * self.settings.gridSize, slotsDown * self.settings.gridSize);
self.gridDisplay:SetStretchMode(1);
self.gridDisplay.slotsRight, self.gridDisplay.slotsDown = slotsRight, slotsDown;
end
left, top, scaledGridSize = self:GetNearestGridPos(left, top);
self.gridDisplay.prevLeft, self.gridDisplay.prevTop = left, top;
self.gridDisplay:SetSize(slotsRight * scaledGridSize, slotsDown * scaledGridSize);
self.gridDisplay:SetPosition(left, top);
end
function Manager:HideGrid()
if (self.gridDisplay) then
self.gridDisplay:Close();
self.gridDisplay = 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: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(object, eventName)
if (not self.eventGenerators[eventName]) then
self.eventGenerators[eventName] = {};
end
table.insert(self.eventGenerators[eventName], object);
local generatorID = #self.eventGenerators[eventName];
--Puts("Adding event generator " .. tostring(generatorID) .. " for event " .. Serialize(eventName) .. " from object " .. Serialize(object.settings.caption.text));
return generatorID;
end
function Manager:RemoveEventGenerator(generatorID, eventName)
--Puts("Removing event generator " .. tostring(generatorID) .. " for event " .. Serialize(eventName));
local eventGenerators = self.eventGenerators[eventName];
if (eventGenerators) then
local object = eventGenerators[generatorID];
--Puts(" (was from object " .. Serialize(object.settings.caption.text) .. ")");
table.remove(eventGenerators, generatorID);
if (#eventGenerators == 0) then
self.eventGenerators[eventName] = nil;
end
else
--Puts("Not registered!");
end
end
-- Returns a list of bars that generates the specified event
function Manager:GetEventGenerators(eventName)
return self.eventGenerators[eventName];
end
-- Returns the names of all of the events generated by all bars
function Manager:GetEventNames()
local eventNames = {};
for k in keys(self.eventGenerators) do
table.insert(eventNames, k);
end
return eventNames;
end
function Manager:GetEventCallbacks()
return self.eventCallbacks;
end
function Manager:PropagateEvent(eventName)
if (self.propagatingEvent[eventName]) then
-- Prevent recursion. There is no reason to generate the same event twice in the same instant.
return;
end
self.propagatingEvent[eventName] = true;
local callbacks = self.eventCallbacks[eventName];
if (type(callbacks) == "table") then
--Puts("Propagating event \"" .. tostring(eventName) .. "\" to " .. tostring(#callbacks) .. " callback functions: " .. Serialize(callbacks));
for f = 1, #callbacks do
local func = callbacks[f];
func(eventName);
end
elseif (type(callbacks) == "function") then
--Puts("Propagating event \"" .. tostring(eventName) .. "\" to a single callback function: " .. Serialize(callbacks));
callbacks(eventName);
end
self.propagatingEvent[eventName] = false;
end
function Manager:Destroy()
self:SetWantsPlayerEvents(false);
Node.Destroy(self);
end