lotrointerface.com
Search Downloads

LoTROInterface SVN SequenceBars

[/] [trunk/] [Thurallor/] [SequenceBars/] [Bar.lua] - Rev 201

Compare with Previous | Blame | View Log

Bar = class(Node);

function Bar:Constructor(group, barID, settings)
    Turbine.UI.Window.Constructor(self);
    self.constructing = true;
    self:SetZOrder(0);
    self:SetMouseVisible(false);
    self:SetAllowDrop(true);

    self.slotContainer = Turbine.UI.Control();
    self.slotContainer:SetParent(self);
    self.slotContainer:SetMouseVisible(false);
    
    self.parent = group;
    self.manager = group.manager;
    self.objectID = barID;
    self.onLoad = onLoad;
    self.slots = {};
    self.visibleSlots = {};
    self.cursorPos = 3;
    self.cursorSlot = 1;
    self.globals = self.manager.settings;
    self.player = Turbine.Gameplay.LocalPlayer:GetInstance();
    self.automaticClicks = 0;
    self.wheelPosition = 0;
    self.advancement = {};
    self.reevaluatingConditionals = {};
 
    -- Default settings
    local defaults = {};
    defaults.type = "bar";
    defaults.locked = false;
    defaults.hidden = false;
    defaults.color = Thurallor.Utils.Color();
    defaults.color:SetHSV(math.random(), 1, 1);
    defaults.color = defaults.color:GetHex();
    defaults.cursorStyle = "SmallSquareGlow";
    defaults.caption = { };
    defaults.caption.visible = "Always";
    defaults.caption.clickThru = nil;
    defaults.caption.text = L:GetText("/Default/BarName");
    defaults.caption.position = "beginning";
    defaults.caption.width = 80;
    defaults.caption.height = 35;
    defaults.caption.font = Turbine.UI.Lotro.Font.TrajanPro15;
    defaults.slotSize = 35; -- must be an even number
    defaults.slotSpacing = 0;
    defaults.orientation = "Right";
    defaults.visibleSlots = 5;
    defaults.cursorHomePosition = 2;
    defaults.animation = { duration = 0.1 };
    defaults.sequenceItemInfo = { {}, {}, {}, {}, {} };
    defaults.position = { left = math.floor(Turbine.UI.Display:GetWidth() / 2); top = math.floor(Turbine.UI.Display:GetHeight() / 2 )};
    defaults.anchor = "TopLeft";
    defaults.eventHandlers = {};
    defaults.eventsEnabled = false;
    defaults.hideAutomatics = true;
    defaults.hideInactiveBranches = true;
    defaults.wheelSpeed = 0;
    
    -- Previously-saved settings override the defaults
    DeepTableCopy(settings, defaults);
    DeepTableCopy(defaults, settings);
    self.settings = settings;

    self:ReapplySettings();
    self.constructing = nil;
    self:SetWantsEvents(self.settings.eventsEnabled);
end

function Bar:ReapplySettings()

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

    -- For backward compatibility
    self.settings.cursorStyle = string.gsub(self.settings.cursorStyle, "SilverFrame(%d)", "MetalFrame%1");
    for s = 1, #self.settings.sequenceItemInfo, 1 do
        info = self.settings.sequenceItemInfo[s] or {};
        if (info.action) then
            info.action = string.gsub(info.action, "item.eventName", "item.info.eventName");
            info.action = string.gsub(info.action, "item.bagSlot", "item.info.bagSlot");
        end
        if ((info.class == "Turbine.UI.Lotro.Quickslot") and (info.background)) then
            info.altIcon = info.background;
            info.background = nil;
        end
        if (info.condName == "StanceSelected") then
            if (string.match(info.condExpr, "GetClassAttributes")) then
                info.condExpr = "return (Thurallor.Utils.Watcher.GetStance() == <stance>);";
            end
        end
        if (info.condName and (string.find("SkillTrained|SkillNotTrained|SkillReady|SkillNotReady", info.condName))) then
            if ((type(info.condArgs.skill) == "number") and (info.condArgs.skill == 0)) then
                local skills = Thurallor.Utils.Watcher.GetTrainedSkillsInfo();
                local name = skills.names[1];
                local skill = skills.byName[name];
                local skillInfo = skill:GetSkillInfo();
                info.condArgs.skill = skillInfo:GetIconImageID();
            end
            if ((info.condName == "SkillNotReady") and string.find(info.condExpr, "SkillTrained")) then
                info.condExpr = "return (not Thurallor.Utils.Watcher.SkillReady(<skillName>));";
            end
            if ((info.condName == "SkillTrained") and string.find(info.condExpr, "SkillReady")) then
                info.condExpr = "return (Thurallor.Utils.Watcher.SkillTrained(<skillName>));";
            end
            info.condExpr = string.gsub(info.condExpr, "<skill>", "<skillName>");
            if (info.condArgs.skill and (not info.condArgs.skillName)) then
                local skills = Thurallor.Utils.Watcher.GetSkillsInfo();
                local skill = skills.byIcon[info.condArgs.skill];
                local name;
                if (skill == nil) then
                    local barName = self.settings.caption.text;
                    local group = self.parent;
                    while (group ~= self.manager) do
                        barName = group.settings.caption.text .. ":" .. barName;
                        group = group.parent;
                    end
                    _G.manualUpdatesNeeded = _G.manualUpdatesNeeded .. "• In bar \"" .. barName .. "\", in slot " .. s .. ", please reselect the skill from the menu.\n\n";
                    name = skills.names[1];
                else
                    local skillInfo = skill:GetSkillInfo();
                    name = skillInfo:GetName();
                end
                info.condArgs.skillName = "\"" .. name .. "\"";
                info.condArgs.skill = nil;
            end
        end
        if ((info.type == "SetUnequipDestination") and (type(info.bagSlot) == "number")) then
            local maxSlot = self.player:GetBackpack():GetSize();
            local str = tostring(info.bagSlot);
            if (info.bagSlot < maxSlot) then
                str = str .. ";" .. tostring(info.bagSlot + 1) .. "..." .. tostring(maxSlot);
            end
            if (info.bagSlot > 1) then
                str = str .. ";1..." .. tostring(info.bagSlot - 1);
            end
            info.bagSlot = str;
        end
    end
    for _, handler in pairs(self.settings.eventHandlers) do
        if (handler.action) then
            handler.action = string.gsub(handler.action, "self:SetHidden%(false%); self:BringToFront%(%); ", "self:SetHidden(false); ");
            handler.action = string.gsub(handler.action, "self:SetHidden%(false%); self:Activate%(%); ", "self:SetHidden(false); ");
            handler.action = string.gsub(handler.action, "self:Expand%(%); self:Activate%(%); ", "self:Expand(); ");
            handler.action = string.gsub(handler.action, "self:Expand%(%); self:BringToFront%(%); ", "self:Expand(); ");
            handler.action = string.gsub(handler.action, "self:MoveToMouseCursor%(%); self:Activate%(%); ", "self:MoveToMouseCursor%(%); ");
        end
    end
        if (self.settings.caption.hidden) then
                self.settings.caption.hidden = nil;
                self.settings.caption.visible = "WhenMouseIsPresent";
        end
        
    self:SetCursorStyle(self.settings.cursorStyle);
    self:Rebuild();
end

function Bar:Rebuild()
    self:GetQuickSlots();
    self:FindHiddenSlots();
    self:Redraw();
end

function Bar:Activate()
    -- This used to be called in event handlers after Expand(), MoveToMouseCursor(), and SetHidden(false).
    -- Don't want to use Turbine.UI.Window.Activate() because that would steal focus from the chat window.
end

-- Bring this bar in front of all other bars.
function Bar:BringToFront()
    -- Don't want to use Turbine.UI.Window.Activate() because that would steal focus from the chat window.
    self:SetZOrder(1);
    self:SetZOrder(0);
end

function Bar:CanHaveChildren()
    return false;
end

function Bar:Contains(object)
    return false;
end

function Bar:SetColor(color)
    Node.SetColor(self, color);
    self:Redraw(true);
    self:SetCursorStyle();
    if (self.sequenceEditor) then
        self.sequenceEditor:Redraw();
    end
end

function Bar:SetName(caption)
    Node.SetName(self, caption);
    self:Redraw(true);
    if (self.sequenceEditor) then
        self.sequenceEditor:SetText(caption);
    end
    if (self.exportWindow) then
        self.exportWindow:SetText(caption);
    end
end

function Bar:SetCaptionSize(width, height)
    Node.SetCaptionSize(self, width, height);
    self:Redraw(true);
    self:OffsetPosition(0, 0);
end

function Bar:SetCaptionFont(font)
    self.settings.caption.font = font;
    self:SaveSettings(false);
    self:Redraw(true);
end

function Bar:SetCaptionVisible(visible)
    self.settings.caption.visible = visible;
    self:SaveSettings(false);
    self:Redraw(true);
end

function Bar:SetCaptionClickThru(clickThru)
        if (clickThru == false) then
                clickThru = nil;
        end
        self.settings.caption.clickThru = clickThru;
    self:SaveSettings(false);
    self:Redraw(true);
end

function Bar:ApplyHiddenness()
    local hidden = self:IsHidden();
    if ((not hidden) and (not self:IsVisible())) then
        self.visibleSlots = {};
    end
    self:SetCursorSlot(self.cursorSlot);
    self:SetVisible(not hidden);
end

function Bar:Collapse()
    if (self.settings.collapsed) then
        return;
    end
    if (self.Log) then
        DoCallbacks(self, "Log", {"collapsing", "BAR"});
    end
    self.settings.collapsed = self.cursorPos;
    self:Redraw(true);
    local x1, y1 = self:GetSlotCoords(1);
    if ((self.settings.orientation == "Up") or (self.settings.orientation == "Left")) then
        x1, y1 = self:GetSlotCoords(self.settings.visibleSlots);
    end
    local x2, y2 = self:GetSlotCoords(self.settings.cursorHomePosition);
    self:OffsetPosition(x2 - x1, y2 - y1);
    self:SetCursorPos(1);
    self:SetCursorSlot(self.cursorSlot);
    self:SaveSettings(false);
end

function Bar:IsCollapsed()
    return ((self.settings.visibleSlots == 1) or (self.settings.collapsed ~= nil));
end

function Bar:Expand()
    if (not self.settings.collapsed) then
        return;
    end
    if (self.Log) then
        DoCallbacks(self, "Log", {"expanding", "BAR"});
    end
    local cursorPos = math.min(self.settings.collapsed, self.settings.visibleSlots);
    self.settings.collapsed = nil;
    self:Redraw(true);
    local x1, y1 = self:GetSlotCoords(1);
    if ((self.settings.orientation == "Up") or (self.settings.orientation == "Left")) then
        x1, y1 = self:GetSlotCoords(self.settings.visibleSlots);
    end
    local x2, y2 = self:GetSlotCoords(self.settings.cursorHomePosition);
    self:OffsetPosition(x1 - x2, y1 - y2);
    self:SetCursorPos(cursorPos);
    self:SetCursorSlot(self.cursorSlot);
    self:SaveSettings(false);
end

function Bar:IsExpanded()
    return ((self.settings.visibleSlots == 1) or (self.settings.collapsed == nil));
end

-- Updates the sequence item when a user drags a shortcut onto the sequence editor
function Bar:ShortcutChanged()
    if (self.Log) then
        DoCallbacks(self, "Log", {"modified with sequence editor", "BAR"});
    end
    self:SaveSettings(false);
    self:Rebuild();
end

-- Initialize "hidden" and "folded" flags
function Bar:FindHiddenSlots()
    for i = 1, #self.slots, 1 do
        local slot = self.slots[i];
        slot.folded = false;
        if (self.settings.hideAutomatics and slot.info.automatic) then
            slot.hidden = true;
        else
            slot.hidden = false;
        end
    end
end

function Bar:GetSequence()
    return self.sequence;
end

function Bar:SetUseOnRightClick(useOnRightClick)
    for _, slot in pairs(self.slots) do
        slot:SetUseOnRightClick(useOnRightClick);
    end
end

-- Creates (or recreates) the array of objects (e.g. Quickslot) according to self.settings.sequenceItemInfo
function Bar:GetQuickSlots()
    -- Hide and destroy existing clickable objects, if any
    for s = 1, #self.slots, 1 do
        self.slots[s]:SetParent(nil);
    end

    -- Unregister as event(s) generator.  We will re-register for each generated event as we loop through the sequence below.
    self.manager:SetEventGenerators(false, self);

    local wantsMouseWheelEvents = self.settings.wheelSpeed and (self.settings.wheelSpeed > 0);
    self.sequence = Sequence(self, self.settings.sequenceItemInfo, wantsMouseWheelEvents);
    self.manager:SetIncludees(self.objectID, self.sequence:GetIncludes());

    -- Register as generator of event(s)
    self.manager:SetEventGenerators(true, self, self.sequence:GetEvents());

    -- Iterate through the sequence configuring the clickable objects.
    self.slots = self.sequence:GetSlots(); -- returns array of Slot instances
    for s = 1, #self.slots, 1 do
        local slot = self.slots[s];
        local info = slot.info;
        self:ConfigureSlot(slot, s);
        AddCallback(slot, "MouseClick", function(sender, args)
            if (self.globals.useOnRightClick or args.Button == Turbine.UI.MouseButton.Left) then
                if (self.Log) then
                    DoCallbacks(self, "Log", {"clicked slot " .. tostring(sender.s), "MOUSE"});
                end
                self:SlotClick(sender.s, false);
            end
        end);
    end
    
    -- Create a "reset" slot to be displayed when all other slots are folded
    if (not self.resetSlot) then
        local info = { type = "Reset"; class = "Turbine.UI.Control"; background = resources.Icon.Reset };
        self.resetSlot = Slot(info);
    end
    self:ConfigureSlot(self.resetSlot, #self.slots);
    AddCallback(self.resetSlot, "MouseClick", function(sender, args)
        if (args.Button == Turbine.UI.MouseButton.Left) then
            if (self.Log) then
                DoCallbacks(self, "Log", {"clicked reset button", "MOUSE"});
            end
            self:Reset();
        end
    end);

    -- Create a "gears" slot to be displayed when running continuously
    if (not self.gearsSlot) then
        self.gearsSlot = Gears(false);
    end
    self:ConfigureSlot(self.gearsSlot, #self.slots);
end

function Bar:ConfigureSlot(slot, s)
    slot:SetZOrder(2);
    slot:SetActionEnabled(true);
    slot:SetAllowDrop(false);
    slot:SetDraggable(false);
    slot:SetUseOnRightClick(self.globals.useOnRightClick);
    slot.bar = self;
    slot.s = s;
    slot.MouseClick = nil; -- avoid multiple callbacks when the slot is reconfigured
    slot.MouseEnter = nil;
    slot.MouseWheel = nil;
    AddCallback(slot, "MouseClick", function(sender, args)
        -- Right-click displays the settings menu.
        if (not self.globals.useOnRightClick and (args.Button == Turbine.UI.MouseButton.Right)) then
            self:ShowSettingsMenu();
        end
    end);
    AddCallback(slot, "MouseEnter", function()
        self:MouseArrive();
    end);
    AddCallback(slot, "MouseWheel", function(sender, args)
        args.s = s;
        self:MouseWheel(args);
    end);
end

function Bar:Redraw(noReset)
    if (self.settings.collapsed) then
        self.displaySlots = 1;
    else
        self.displaySlots = self.settings.visibleSlots;
    end
    local slotsLongDimension = (2 * self.cursorMargin) + (self.displaySlots + 1) * self.settings.slotSize + self.displaySlots * self.settings.slotSpacing;
    local slotsShortDimension = (2 * self.cursorMargin) + self.settings.slotSize;
    
    local captionMargin = 4;
    local captionWidth, captionHeight = self.settings.caption.width, self.settings.caption.height;
    local barWidth, barHeight, slotsWidth, slotsHeight, captionLeft, captionTop;
    if ((self.settings.orientation == "Up") or (self.settings.orientation == "Down")) then
        slotsWidth, slotsHeight = slotsShortDimension, slotsLongDimension;

        -- Position slots and caption horizontally; set bar width
        if (slotsWidth < captionWidth) then
            barWidth = captionWidth;
            self.slotsLeft = math.floor((barWidth - slotsWidth) / 2);
            captionLeft = 0;
        elseif (slotsWidth > captionWidth) then
            barWidth = slotsWidth;
            self.slotsLeft = 0;
            captionLeft = math.floor((barWidth - captionWidth) / 2);
        else -- (slotsWidth == captionWidth)
            barWidth, self.slotsLeft, captionLeft = slotsWidth, 0, 0;
        end

        -- Position slots and caption vertically; set bar height
        if (self.settings.orientation == "Up") then
            if (self.settings.caption.position == "beginning") then
                self.slotsTop = 0;
                captionTop = slotsHeight - self.cursorMargin + captionMargin;
                barHeight = math.max(slotsHeight, captionTop + captionHeight);
            else -- (self.settings.caption.position == "end")
                captionTop = math.max(0, self.cursorMargin + self.settings.slotSize - (captionHeight + captionMargin));
                self.slotsTop = math.max((captionHeight + captionMargin) - (self.cursorMargin + self.settings.slotSize), 0);
                barHeight = self.slotsTop + slotsHeight;
            end
        else -- (self.settings.orientation == "Down")
            if (self.settings.caption.position == "beginning") then
                captionTop = math.max(0, self.cursorMargin - (captionHeight + captionMargin));
                self.slotsTop = math.max((captionHeight + captionMargin) - self.cursorMargin, 0);
                barHeight = self.slotsTop + slotsHeight;
            else -- (self.settings.caption.position == "end")
                self.slotsTop = 0;
                captionTop = slotsHeight - self.cursorMargin - self.settings.slotSize + captionMargin;
                barHeight = math.max(slotsHeight, captionTop + captionHeight);
            end
        end
    else -- ((self.settings.orientation == "Left") or (self.settings.orientation == "Right")) then
        slotsWidth, slotsHeight = slotsLongDimension, slotsShortDimension;

        -- Position slots and caption vertically; set bar height
        if (slotsHeight < captionHeight) then
            barHeight = captionHeight;
            self.slotsTop = math.floor((barHeight - slotsHeight) / 2);
            captionTop = 0;
        elseif (slotsHeight > captionHeight) then
            barHeight = slotsHeight;
            self.slotsTop = 0;
            captionTop = math.floor((barHeight - captionHeight) / 2);
        else -- (slotsHeight == captionHeight)
            barHeight, self.slotsTop, captionTop = slotsHeight, 0, 0;
        end
    
        -- Position slots and caption horizontally; set bar width
        if (self.settings.orientation == "Left") then
            if (self.settings.caption.position == "beginning") then
                self.slotsLeft = 0;
                captionLeft = slotsWidth - self.cursorMargin + captionMargin;
                barWidth = math.max(slotsWidth, captionLeft + captionWidth);
            else -- (self.settings.caption.position == "end")
                captionLeft = math.max(0, self.cursorMargin + self.settings.slotSize - (captionWidth + captionMargin));
                self.slotsLeft = math.max((captionLeft + captionWidth + captionMargin) - (self.cursorMargin + self.settings.slotSize), 0);
                barWidth = self.slotsLeft + slotsWidth;
            end
        else -- (self.settings.orientation == "Right")
            if (self.settings.caption.position == "beginning") then
                captionLeft = math.max(0, self.cursorMargin - (captionWidth + captionMargin));
                self.slotsLeft = math.max((captionWidth + captionMargin) - self.cursorMargin, 0);
                barWidth = self.slotsLeft + slotsWidth;
            else -- (self.settings.caption.position == "end")
                self.slotsLeft = 0;
                captionLeft = slotsWidth - self.cursorMargin - self.settings.slotSize + captionMargin;
                barWidth = math.max(slotsWidth, captionLeft + captionWidth);
            end
        end
    end

    self.slotContainer:SetSize(slotsWidth, slotsHeight);
    self.slotContainer:SetPosition(self.slotsLeft, self.slotsTop);
    self:DrawCaption();
    self.caption:SetSize(captionWidth, captionHeight);
    self.caption:SetPosition(captionLeft, captionTop);

    self:SetStretchMode(0);
    self:SetSize(barWidth, barHeight);
    self:SetStretchMode(1);
    self:SetSize(barWidth * self:GetScale() / 32, barHeight * self:GetScale() / 32);

    self:SetPosition();
    self:SetOpacity(self:GetTransparency());

    -- Draw cursor icon and slots.
    if (not noReset) then
        self:Reset();
    end

    self:ApplyHiddenness();
end

function Bar:Reset()
    if (self.Log) then
        DoCallbacks(self, "Log", {"reset", "BAR"})
    end
    self.sequence:UnfoldAllSlots();
    self.reevaluatingConditionals = {};
    self.animationStopped = false;
    local cursorPos = self.settings.cursorHomePosition;
    if (self.settings.collapsed) then
        self.settings.collapsed = self.settings.cursorHomePosition;
        cursorPos = 1;
    end
    self:SetCursorPos(cursorPos);
    self.clickedPos = cursorPos;
    self.clickedSlot = 1;
    self:SetCursorSlot(1);
    self.gearsSlot:SetDelaying(false);
    self:UnregisterAdvanceCallback(); -- In case we were waiting for advancement when the bar was reset
    if (self.slots[1] and self.slots[1].info.automatic) then
        self:SlotClick(1, true);
    end
end

-- Moves the cursor to the specified position on the bar
function Bar:SetCursorPos(pos)
    local resource = resources.Cursor[self.settings.cursorStyle];
    local left, top = self:GetSlotCoords(pos);
    left = left - self.cursorMargin + resource.xOffset;
    top = top - self.cursorMargin + resource.yOffset;
    self.cursor.left, self.cursor.top = left, top;
    self.cursor:SetPosition(left, top);
    self.cursorPos = pos;
end

function Bar:DrawCaption()
    if (not self.caption) then
        self.caption = Turbine.UI.Control();
        self.caption:SetParent(self);
        self.caption:SetZOrder(10);
        self.caption:SetMouseVisible(true);
        
        -- While mouse is over caption, highlight the label to indicate that it can be clicked.
        function self.caption.MouseEnter(caption)
            caption.label:SetFontStyle(Turbine.UI.FontStyle.None);
                        caption.label:SetOutlineColor(self.color); -- boldface effect
                        caption.label:SetFontStyle(Turbine.UI.FontStyle.Outline);
            caption.label:SetVisible(true);
        end
        function self.caption.MouseLeave(caption)
            caption.label:SetFontStyle(Turbine.UI.FontStyle.None);
            caption.label:SetOutlineColor(Turbine.UI.Color(0.75, 0, 0, 0));
            caption.label:SetFontStyle(Turbine.UI.FontStyle.Outline);
            caption.label:SetVisible(self.settings.caption.visible == "Always");
        end

        function self.caption.MouseClick(caption, args)
            if (self.mouseMoved) then
                self.mouseMoved = false;
                return;
            end

            -- Left-click resets the sequence back to the beginning.
            if (args.Button == Turbine.UI.MouseButton.Left) then
                self:Reset();
            end
            
            -- Right-click displays the settings menu.
            if (args.Button == Turbine.UI.MouseButton.Right) then
                self:ShowSettingsMenu();
            end
        end
        function self.caption.MouseDoubleClick(caption, args)
            -- Left-double-click opens the caption editor.
            if (args.Button == Turbine.UI.MouseButton.Left) then
                self:EditCaption();
            end
            
            -- Right-double-click would open the sequence editor, but
            -- since right-click opens the settings menu, right-double
            -- -click isn't possible.
        end
        
        -- MouseDown, MouseUp, and MouseMove are used to move the window (if not locked)
        function self.caption.MouseDown(caption, args)
            if (args.Button == Turbine.UI.MouseButton.Left) then
                self.mouseDown = true;
                self.mouseMoved = false;
                self.originalMouseX, self.originalMouseY = Turbine.UI.Display.GetMouseX(), Turbine.UI.Display.GetMouseY();
            end
        end
        function self.caption.MouseUp(caption, args)
            if (args.Button == Turbine.UI.MouseButton.Left) then
                self.mouseDown = false;
                if (self.mouseMoved and not self.settings.locked) then
                    if (self.manager.settings.snapToGrid) then
                        local togetherGroup = self:GetTopTogetherGroup();
                        if (togetherGroup) then
                            togetherGroup:SnapToGrid();
                        else
                            self:SnapToGrid();
                        end
                    end
                    self:SaveSettings(false);
                    self.manager:UnhighlightGrid();
                    self.manager:ShowAnchorIcon(nil);
                end
            end
        end
        function self.caption.MouseMove(caption, args)
            if (self.mouseDown and not self.settings.locked) then
                self.mouseMoved = true;
                local newMouseX, newMouseY = Turbine.UI.Display.GetMouseX(), Turbine.UI.Display.GetMouseY();
                local deltaX, deltaY = newMouseX - self.originalMouseX, newMouseY - self.originalMouseY;
                self.originalMouseX, self.originalMouseY = newMouseX, newMouseY;
                
                -- See if we need to move other bars along with this one.
                local togetherGroup = self:GetTopTogetherGroup();
                if (togetherGroup) then
                    togetherGroup:OffsetPosition(deltaX, deltaY);
                else
                    self:OffsetPosition(deltaX, deltaY);
                end
                
                -- Highlight grid if "Snap to grid" is enabled.
                if (self.globals.snapToGrid) then
                    local left, top, width, height = self:GetPosition();
                    if ((self.settings.orientation == "Right") or (self.settings.orientation == "Left")) then
                        width = self.displaySlots;
                        height = 1;
                    else
                        width = 1;
                        height = self.displaySlots;
                    end
                    self.manager:HighlightGrid(left, top, width, height, self:GetScale());
                end
                
                -- Display anchor icon to indicate position reference
                local left, top = self:GetSlotsScreenCoords();
                self.manager:ShowAnchorIcon(self.settings.anchor, left, top);
            end
        end
        function self.caption.DragDrop(caption)
            self:EditSequence();
        end
    end
    local caption = self.caption;
    if (not caption.label) then
        caption.label = Turbine.UI.Label();
        caption.label:SetParent(caption);
        caption.label:SetMouseVisible(false);
    end
    local label = caption.label;
        caption:SetMouseVisible((not self.settings.caption.clickThru) and (self.settings.caption.visible ~= "Never"));
    
    local alignment;
    local where = self.settings.orientation;
    if (self.settings.caption.position == "end") then
        local swap = {Up = "Down"; Down = "Up"; Left = "Right"; Right = "Left"};
        where = swap[where];
    end
    if (where == "Up") then
        alignment = Turbine.UI.ContentAlignment.TopCenter;
    elseif (where == "Down") then
        alignment = Turbine.UI.ContentAlignment.BottomCenter;
    elseif (where == "Right") then
        alignment = Turbine.UI.ContentAlignment.MiddleRight;
    else -- "Left"
        alignment = Turbine.UI.ContentAlignment.MiddleLeft;
    end

    label:SetText(self.settings.caption.text);
    label:SetVisible((self.settings.caption.visible == "Always") or ((self.settings.caption.visible == "WhenMouseIsPresent") and self.mouseInside));
    self:FormatCaptionLabel(label, alignment);
    if (self.captionEditor and self.captionEditor:WantsCaptionUpdates()) then
        local textBox = self.captionEditor:GetTextBox();
        self:FormatCaptionLabel(textBox, alignment);
        self.captionEditor:Redraw();
    end
end

function Bar:MouseArrive()
    if (self.mouseInside) then
        -- Already inside; do nothing.
        return;
    end
    self.mouseInside = true;
    self.parent:MouseArrive(self.objectID);
        self:BringToFront();
        if (self.settings.caption.visible == "WhenMouseIsPresent") then
                self.caption.label:SetVisible(true);
        end
    if (self.Log) then
        DoCallbacks(self, "Log", {"arrived", "MOUSE"});
    end
    DoCallbacks(self, "MouseArrived");
    self:SetWantsUpdates(true);
end

function Bar:MouseInside(mouseLeft, mouseTop)
    if (self:IsHidden()) then
        return false;
    end
    local left, top, width, height = self:GetSlotsScreenCoords();
    local spacing = 1;
    left = left - spacing;
    top = top - spacing;
    local right = left + width + 2 * spacing;
    local bottom = top + height + 2 * spacing;
    return ((mouseLeft >= left) and (mouseLeft < right) and (mouseTop >= top) and (mouseTop < bottom));
end

function Bar:MouseDepart()
    self.mouseInside = false;
    self.parent:MouseDepart(self.objectID);
        if (self.settings.caption.visible == "WhenMouseIsPresent") then
                self.caption.label:SetVisible(false);
        end
    if (self.Log) then
        DoCallbacks(self, "Log", {"departed", "MOUSE"});
    end
    DoCallbacks(self, "MouseDeparted");
end

function Bar:MouseWheel(args)
    local dir = args.Direction;
    local prevSlot = args.s;
    local mousePos = prevSlot and (prevSlot > 0) and self.slots[prevSlot].pos;

    -- Increment wheel position according to chosen wheel speed
    self.wheelPosition = self.wheelPosition + (dir * self.settings.wheelSpeed);
    if (math.abs(self.wheelPosition) < 1) then
        -- More wheel motion is needed
        return;
    end
    dir = self.wheelPosition;
    self.wheelPosition = 0;
    if (self.Log) then
        if (dir > 0) then
            DoCallbacks(self, "Log", {"wheel moved forward", "MOUSE"});
        else
            DoCallbacks(self, "Log", {"wheel moved backward", "MOUSE"});
        end
    end
    
    -- Find next or previous visible slot
    local s = self.cursorSlot;
    repeat
        s = s + dir;
        if (s > #self.slots) then
            s = 1;
        elseif (s < 1) then
            s = #self.slots;
        end
        slot = self.slots[s];
        if ((s == self.cursorSlot) or (not slot)) then
            -- No other visible slots; do nothing.
            return;
        end
    until (not (slot.hidden or slot.isFolded));

    -- Notify slot under cursor that the mouse has exited, to turn off the highlight
    if (prevSlot) then
        DoCallbacks(self.slots[prevSlot], "MouseLeave");
    end
    self:SetCursorSlot(s);
    
    -- Make sure the item under the cursor is always clickable.  This hack is
    -- necessary because as the slot moves, no MouseEnter event occurs and
    -- hence no MouseClick events can be received.
    for object, pos in pairs(self.visibleSlots) do
        if (pos == mousePos) then
            object:SetVisible(false);
            object:SetVisible(true);
            break;
        end
    end
end

function Bar:EditCaption()
    if (self.captionEditor == nil) then
        self.captionEditor = CaptionEditor("BarCaption", self, self.caption.label:GetWidth(), self.caption.label:GetHeight(), self.settings.caption.text);
        self.captionEditor.Closed = function()
            self.captionEditor = nil;
        end
    end
end

function Bar:EditSequence()
    if (self.sequenceEditor == nil) then
        self.sequenceEditor = SequenceEditor(self, self.settings);
        self.sequenceEditor.Closed = function()
            self.sequenceEditor = nil;
        end
    end
end

function Bar:OpenDebugWindow()
    local title = L:GetText("/BarMenu/Debug") .. ": " .. self.settings.caption.text;
    local debugWindow = Thurallor.UI.LogWindow(title, { self });
    debugWindow:SetBackColor(self.color);
    debugWindow:SetTrace("MOUSE", "Mouse actions", true);
    debugWindow:SetTrace("BAR", "Bar status changes", true);
    debugWindow:SetTrace("ADVANCE", "Bar advancement", true);
    debugWindow:SetTrace("SPECIAL-SLOT", "Special slot actions", true);
    debugWindow:SetTrace("EVENT", "Event behaviours", true);
    debugWindow:SetVisible(true);
    debugWindow:AppendText("Debugging started");
    
    -- Forward log information fron the Gears object to the DebugWindow.
    self.gearsSlot.Log = function(_, args)
        if (self.Log == nil) then
            -- All debug windows closed.
            self.gearsSlot.Log = nil;
        else
            DoCallbacks(self, "Log", args);
        end
    end
end

function Bar:FormatCaptionLabel(control, alignment)
    control:SetFont(self.settings.caption.font);
    control:SetForeColor(self.color);
    control:SetFontStyle(Turbine.UI.FontStyle.Outline);
    control:SetOutlineColor(Turbine.UI.Color(0.75, 0, 0, 0));
    control:SetTextAlignment(alignment);
    control:SetWidth(self.settings.caption.width);
    control:SetHeight(self.settings.caption.height);
    control:SetText(control:GetText());
end

function Bar:MoveToMouseCursor()
    -- If animating, go head and complete the animation instantaneously.
    -- User is going to want to click the slot immediately; and we need
    -- to know where the slot is going to end up in order to reposition the
    -- bar properly.
    if (self.animationStartTime) then
        self:Animate(self.settings.animation.duration);
    end

    -- PointToScreen doesn't work with scaled windows, so we have to do the calculations ourselves.
    local winLeft, winTop = Turbine.UI.Window.GetPosition(self);
    local containerX, containerY = self.slotContainer:GetPosition();
    local scale = self:GetScale() / 32;
    containerX = containerX * scale;
    containerY = containerY * scale;
    local cursorX, cursorY = self.cursor:GetPosition();
    cursorX = cursorX * scale + containerX + winLeft;
    cursorY = cursorY * scale + containerY + winTop;
    local cursorWidth = self.cursor:GetWidth() * scale;
    local cursorHeight = self.cursor:GetSize() * scale;
    local cursorCenter = (cursorX + math.floor(cursorWidth / 2));
    local cursorMiddle = (cursorY + math.floor(cursorHeight / 2));
    local mouseX, mouseY = Turbine.UI.Display.GetMouseX(), Turbine.UI.Display.GetMouseY();
    self:OffsetPosition(mouseX - cursorCenter, mouseY - cursorMiddle);
    Turbine.UI.Window.Activate(self);

    -- Make sure the item under the cursor is always clickable.  This hack is
    -- necessary because as the slot moves, no MouseEnter event occurs and
    -- hence no MouseClick events can be received.
    self.slots[self.cursorSlot]:SetVisible(false);
    self.slots[self.cursorSlot]:SetVisible(true);
end

-- Bar is positioned with respect to the topmost/leftmost quickslot.
function Bar:GetSlotsScreenCoords()
    local winLeft, winTop = Turbine.UI.Window.GetPosition(self);
    local firstLeft, firstTop = self:GetSlotCoords(1);
    local lastLeft, lastTop = self:GetSlotCoords(self.displaySlots);
    local slotLeft, slotTop = math.min(firstLeft, lastLeft), math.min(firstTop, lastTop);
    local slotRight = math.max(firstLeft, lastLeft) + self.settings.slotSize;
    local slotBottom = math.max(firstTop, lastTop) + self.settings.slotSize;
    local scale = self:GetScale() / 32;
    -- If animating, use destination position (self.slotsLeft, self.slotsTop) rather than actual current position (self.slotContainer:GetPosition).
    local left = winLeft + scale * (self.slotsLeft + slotLeft);
    local top = winTop + scale * (self.slotsTop + slotTop);
    local width = scale * (slotRight - slotLeft);
    local height = scale * (slotBottom - slotTop);
    return left, top, width, height;
end

function Bar:SetPosition(left, top)
    local anchorLeft, anchorTop = self.manager:GetAnchorPosition(self.settings.anchor);
    local changed = true;
    if (left == nil) then
        left = self.settings.position.left + anchorLeft;
        top = self.settings.position.top + anchorTop;
        changed = false;
    end
    local winLeft, winTop = Turbine.UI.Window.GetPosition(self);
    local slotLeft, slotTop = self:GetSlotsScreenCoords();
    local leftOffset, topOffset = slotLeft - winLeft, slotTop - winTop;
    Turbine.UI.Window.SetPosition(self, left - leftOffset, top - topOffset);
    if (changed) then
        self.settings.position = { left = left - anchorLeft; top = top - anchorTop };
        self:SaveSettings(false);
    end
end

function Bar:SetAnchorPoint(anchor)
    local left, top = self:GetPosition();
    self.settings.anchor = anchor;
    self:SetPosition(left, top);
end

-- Returns the position with respect to the top, left corner of the screen, regardless of anchor point.
function Bar:GetPosition()
    local anchorLeft, anchorTop = self.manager:GetAnchorPosition(self.settings.anchor);
    return anchorLeft + self.settings.position.left, anchorTop + self.settings.position.top;
end

function Bar:OffsetPosition(deltaX, deltaY)
    local left, top = self:GetPosition();
    self:SetPosition(left + deltaX, top + deltaY);
end

-- Finds the topmost group containing this bar which has the "bars move together" option enabled.
function Bar:GetTopTogetherGroup()
    return self.parent:GetTopTogetherGroup();
end

function Bar:SnapToGrid()
    local left, top = self:GetSlotsScreenCoords();
    local newLeft, newTop = self.parent.manager:GetNearestGridPos(left, top, self:GetScale());
    self:SetPosition(newLeft, newTop);
    self:SaveSettings(false);
end

function Bar:AddSettingsMenuItem(parent, itemName, _, fromOptionsPanel)
    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("/BarMenu");
        parent:GetItems():Clear();
        if (fromOptionsPanel and (not self:IsHidden())) then
            self:AddSettingsMenuItem(parent, "Find");
        end
        self:AddSettingsMenuItem(parent, "Hide");
        if (self.settings.visibleSlots > 1) then
            if (self:IsCollapsed()) then
                self:AddSettingsMenuItem(parent, "Expand");
            else
                self:AddSettingsMenuItem(parent, "Collapse");
            end
        end
        self:AddSettingsMenuItem(parent, "Clone");
        self:AddSettingsMenuItem(parent, "Delete");
        self:AddSettingsMenuItem(parent, "Export");
        self:AddSettingsMenuItem(parent, "EditSequence");
        self:AddSettingsMenuItem(parent, "EventBehaviors");
        self:AddSettingsMenuItem(parent, "Settings");
        self:AddSettingsMenuItem(parent, "Debug");
        if (not fromOptionsPanel) then
            if (self.parent ~= self.manager) then
                local groupItem = self:AddSettingsMenuItem(parent, "Group");
                local groupName = self.parent:GetName();
                local text = groupItem:GetText();
                text = text:gsub("<name>", groupName);
                groupItem:SetText(text);
            end
            self:AddSettingsMenuItem(parent, "Global");
            self:AddSettingsMenuItem(parent, "Directory");
        end
        L:SetContext(prevContext);
    elseif (itemName == "Debug") then
        item:SetText(item:GetText() .. "...");
        item.Click = function()
            self:OpenDebugWindow();
        end
    elseif (itemName == "Expand") then
        item.Click = function()
            self:Expand();
        end
    elseif (itemName == "Collapse") then
        item.Click = function()
            self:Collapse();
        end
    elseif (itemName == "Directory") then
        item.Click = function()
            self.manager:ShowDirectory("bars");
        end
    elseif (itemName == "Export") then
        item.Click = function()
            self:Export();
        end
    elseif (itemName == "Find") then
        item.Click = function(ctl)
            self:Find(ctl);
        end
    elseif (itemName == "Hide") then
        item:SetChecked(self.settings.hidden);
        item.Click = function()
            self:SetHidden(not self.settings.hidden);
        end
    elseif (itemName == "Position") then
        local prevContext = L:SetContext("PositionMenu");
        self:AddSettingsMenuItem(item, "RelativeTo");
        self:AddSettingsMenuItem(item, "Draggable");
        L:SetContext(prevContext);
    elseif (itemName == "Draggable") then
        item:SetChecked(not self.settings.locked);
        item.Click = function()
            local locked = not self.settings.locked;
            self:SetLocked(locked);
        end
    elseif (itemName == "RelativeTo") then
        local prevContext = L:SetContext("RelativeToMenu");
        for text in values(L:GetSortedTexts()) do
            local anchor = L:GetItem(text);
            local newItem = self:AddSettingsMenuItem(item, anchor);
            newItem:SetChecked(anchor == self.settings.anchor);
            newItem:SetEnabled(anchor ~= self.settings.anchor);
            newItem.Click = function()
                self:SetAnchorPoint(anchor);
            end
        end
        L:SetContext(prevContext);
    elseif (itemName == "EditSequence") then
        item:SetEnabled(not self.sequenceEditor);
        item.Click = function()
            self:EditSequence();
        end
    elseif (itemName == "EventBehaviors") then
        local prevContext = L:SetContext("/EventBehaviorsMenu");
        if (self.settings.eventsEnabled) then
            self:AddSettingsMenuItem(item, "AddBehavior");
            if (#self.settings.eventHandlers > 0) then
                self:AddSettingsMenuItem(item, "CurrentBehaviors");
            end
            self:AddSettingsMenuItem(item, "DisableEvents");
        else
            self:AddSettingsMenuItem(item, "EnableEvents");
        end
        L:SetContext(prevContext);
    elseif (itemName == "EnableEvents") then
        item.Click = function(ctl)
            self:SetEventHandlersEnabled(true);
            self:RedisplayMenu(ctl._parent);
        end
    elseif (itemName == "DisableEvents") then
        item.Click = function(ctl)
            self:SetEventHandlersEnabled(false);
            self:RedisplayMenu(ctl._parent);
        end
    elseif (itemName == "CurrentBehaviors") then
        if (self.settings.eventHandlers) then
            for h = 1, #self.settings.eventHandlers do
                local handler = self.settings.eventHandlers[h];
                local handlerItem = Turbine.UI.MenuItem(handler.description, true, false);
                item:GetItems():Add(handlerItem);
                handlerItem._parent = item;
                local removeItem = Turbine.UI.MenuItem(L:GetText("Remove"), true, false);
                handlerItem:GetItems():Add(removeItem);
                removeItem._parent = handlerItem;
                removeItem.Click = function(ctl)
                    self:RemoveEventHandler(h);
                    self:RedisplayMenu(ctl._parent._parent);
                end
            end
        end
    elseif (itemName == "AddBehavior") then
        local prevContext = L:SetContext("/EventBehaviorsMenu/Actions");
        self:AddSettingsMenuItem(item, "ShowBar");
        self:AddSettingsMenuItem(item, "HideBar");
        self:AddSettingsMenuItem(item, "ResetBar");
        self.availableEvents = self.manager:GetEventNames();
        self:AddSettingsMenuItem(item, "MoveToCursor");
        self:AddSettingsMenuItem(item, "ExpandBar");
        self:AddSettingsMenuItem(item, "CollapseBar");
        self:AddSettingsMenuItem(item, "SetTransparency");
        self:AddSettingsMenuItem(item, "StartExecuting");
        self:AddSettingsMenuItem(item, "StopExecuting");
        L:SetContext(prevContext);
    elseif (itemName == "SetTransparency") then
        local prevContext = L:SetContext("/BarMenu/SettingsMenu/TransparencyMenu");
        local subItem = self:AddSettingsMenuItem(item, "SetTransparency:");
        subItem._name = "SetTransparency";
        subItem:SetText(L:GetText("UseGlobalSetting"));
        subItem.argName = L:GetText("default");
        subItem.argValue = "nil";
        for i = 0, 10 do
            subItem = self:AddSettingsMenuItem(item, "SetTransparency:");
            subItem._name = "SetTransparency";
            subItem.argName = L:GetText("PartiallyTransparent"):gsub("<pct>", i * 10);
            if (i == 0) then
                subItem:SetText(L:GetText("FullyTransparent"));
                subItem.argValue = "0";
            elseif (i == 10) then
                subItem:SetText(L:GetText("FullyOpaque"));
                subItem.argValue = "1";
            else
                subItem:SetText(subItem.argName);
                subItem.argValue = "0." .. i;
            end
        end
        L:SetContext(prevContext);
    elseif (string.find("|SetTransparency:|ShowBar|HideBar|ResetBar|MoveToCursor|ExpandBar|CollapseBar|StartExecuting|StopExecuting|", "|" .. itemName .. "|")) then
        local prevContext = L:SetContext("/EventBehaviorsMenu/Triggers");
        self.availableEvents = self.manager:GetEventNames();
        
        -- Show only the options that make sense
        if (itemName ~= "MoveToCursor") then
            if ((itemName ~= "ResetBar") and (itemName ~= "StopExecuting")) then
                self:AddSettingsMenuItem(item, "AtStartup");
            end
            self:AddSettingsMenuItem(item, "WhenCombatBegins");
            self:AddSettingsMenuItem(item, "WhenCombatEnds");
        end
        self:AddSettingsMenuItem(item, "WhenTargetChanges");
        if (string.find("|SetTransparency:|ResetBar|ExpandBar|StartExecuting|", "|" .. itemName .. "|")) then
            self:AddSettingsMenuItem(item, "WhenMouseArrives");
        end
        if (string.find("|SetTransparency:|HideBar|ResetBar|CollapseBar|StopExecuting|", "|" .. itemName .. "|")) then
            self:AddSettingsMenuItem(item, "WhenMouseDeparts");
        end
        if (string.find("|SetTransparency:|ShowBar|ResetBar|MoveToCursor|StartExecuting|StopExecuting|", "|" .. itemName .. "|")) then
            self:AddSettingsMenuItem(item, "WhenTraitTreeChanges");
            self:AddSettingsMenuItem(item, "WhenStanceChanges");
        end
        self:AddSettingsMenuItem(item, "WhenPetChanges");
        self:AddSettingsMenuItem(item, "WhenMountChanges");
        self:AddSettingsMenuItem(item, "WhenYouAreIncapacitated");
        self:AddSettingsMenuItem(item, "WhenYouAreRevived");
        self:AddSettingsMenuItem(item, "WhenEventOccurs");
        L:SetContext(prevContext);
    elseif (itemName == "WhenEventOccurs") then
        if (#self.availableEvents > 0) then
            for e = 1, #self.availableEvents do
                local eventName = self.availableEvents[e];
                local eventNameItem = Turbine.UI.MenuItem(eventName, true, false);
                eventNameItem._parent = item;
                item:GetItems():Add(eventNameItem);
                eventNameItem.Click = function(ctl)
                    self:AddEventHandler("WhenEventOccurs:" .. eventName, parent._name, parent.argValue, parent.argName);
                    self:RedisplayMenu(ctl._parent._parent._parent._parent, "CurrentBehaviors");
                end
            end
        else
            local noEventsItem = Turbine.UI.MenuItem(L:GetText("/EventBehaviorsMenu/NoEventsDefined"), false, false);
            item:GetItems():Add(noEventsItem);
        end
        local eventDirectoryItem = Turbine.UI.MenuItem("( . . . )", true, false);
        eventDirectoryItem.Click = function()
            self.manager:ShowDirectory("events");
        end
        item:GetItems():Add(eventDirectoryItem);
    elseif (string.find("|AtStartup|WhenTargetChanges|WhenCombatBegins|WhenCombatEnds|WhenMouseArrives|WhenMouseDeparts|WhenTraitTreeChanges|WhenStanceChanges|WhenPetChanges|WhenMountChanges|WhenYouAreIncapacitated|WhenYouAreRevived|", "|" .. itemName .. "|")) then
        item.Click = function(ctl)
            self:AddEventHandler(itemName, parent._name, parent.argValue, parent.argName);
            self:RedisplayMenu(ctl._parent._parent._parent, "CurrentBehaviors");
        end
    elseif (itemName == "Settings") then
        local prevContext = L:SetContext("SettingsMenu");
        self:AddSettingsMenuItem(item, "Position");
        self:AddSettingsMenuItem(item, "Orientation");
        if ((self.settings.orientation == "Up") or (self.settings.orientation == "Down")) then
            self:AddSettingsMenuItem(item, "Height");
        else
            self:AddSettingsMenuItem(item, "Width");
        end
        self:AddSettingsMenuItem(item, "Cursor");
        self:AddSettingsMenuItem(item, "MouseWheel");
        self:AddSettingsMenuItem(item, "Caption");
        self:AddSettingsMenuItem(item, "Color");
        self:AddSettingsMenuItem(item, "Transparency");
        self:AddSettingsMenuItem(item, "Scale");
        self:AddSettingsMenuItem(item, "Animation");
        self:AddSettingsMenuItem(item, "HideAutomatics");
        self:AddSettingsMenuItem(item, "HideInactiveBranches");
        self:AddSettingsMenuItem(item, "ContinuousExecution");
        L:SetContext(prevContext);
    elseif (itemName == "ContinuousExecution") then
        item:SetChecked(self.settings.continuous);
        item.Click = function()
            self.settings.continuous = not self.settings.continuous;
            if (self.settings.continuous) then
                -- Enforce hiding of automatics and inactive branches
                self.settings.hideAutomatics = true;
                self.settings.hideInactiveBranches = true;
            else
                self.gearsSlot:SetRunning(false);
            end
            self:SaveSettings(false);
            self:ReapplySettings();
        end        
    elseif (itemName == "HideAutomatics") then
        item:SetChecked(self.settings.hideAutomatics);
        item.Click = function()
            self.settings.hideAutomatics = not self.settings.hideAutomatics;
            if (not self.settings.hideAutomatics) then
                -- Continuous mode not allowed if showing automatics
                if (self.settings.continuous) then
                    self.gearsSlot:SetRunning(false);
                    self.settings.continuous = false;
                end
            end
            self:SaveSettings(false);
            self:ReapplySettings();
        end        
    elseif (itemName == "HideInactiveBranches") then
        item:SetChecked(self.settings.hideInactiveBranches);
        item.Click = function()
            self.settings.hideInactiveBranches = not self.settings.hideInactiveBranches;
            if (not self.settings.hideInactiveBranches) then
                -- Continuous mode not allowed if showing inactive branches
                if (self.settings.continuous) then
                    self.gearsSlot:SetRunning(false);
                    self.settings.continuous = false;
                end
            end
            self:SaveSettings(false);
            self:ReapplySettings();
        end        
    elseif (itemName == "Color") then
        item:SetEnabled(not self.colorPicker);
        item.Click = function()
            self:EditColor();
        end
    elseif (itemName == "Transparency") then
        local prevContext = L:SetContext("/BarMenu/SettingsMenu/TransparencyMenu");
        local checked = (self.settings.opacity == nil);
        local globalItem = Turbine.UI.MenuItem(L:GetText("UseGlobalSetting"), true, checked);
        globalItem.Click = function(obj)
            obj:SetChecked(true);
            self:SetTransparency(nil);
        end
        item:GetItems():Add(globalItem);

        checked = not checked;
        local labelItem = Turbine.UI.MenuItem("", true, checked);
        globalItem.labelItem = labelItem;
        labelItem.globalItem = globalItem;
        labelItem.UpdateLabel = function(obj)
            local prevContext = L:SetContext("/BarMenu/SettingsMenu/TransparencyMenu");
            local transparency = self:GetTransparency();
            local text;
            if (transparency == 0) then
                text = L:GetText("FullyTransparent");
            elseif (transparency == 1) then
                text = L:GetText("FullyOpaque");
            else
                text = L:GetText("PartiallyTransparent"):gsub("<pct>", math.floor(0.5 + transparency * 100));
            end
            obj.Label:SetText(text);
            L:SetContext(prevContext);
        end
        labelItem:UpdateLabel();
        labelItem.Click = function(obj)
            obj.KeepOpen = true;
            obj:SetChecked(true);
            self:SetTransparency(self:GetTransparency());
            obj.globalItem:SetChecked(false);
        end
        item:GetItems():Add(labelItem);

        local sliderItem = Turbine.UI.MenuItem("                                        ", false, false);
        local slider = Turbine.UI.Lotro.ScrollBar();
        slider.labelItem = labelItem;
        slider.globalItem = globalItem;
        globalItem.slider = slider;
        slider:SetSize(200, 10);
        slider:SetBackground("Thurallor/SequenceBars/Images/scrollbar_background_horiz.tga");
        slider:SetBlendMode(Turbine.UI.BlendMode.Overlay);
        slider:SetParent(sliderItem.Label);
        slider:SetMinimum(0);
        slider:SetMaximum(100);
        slider:SetSmallChange(1);
        slider:SetLargeChange(10);
        slider:SetValue(math.floor(0.5 + self:GetTransparency() * 100));
        slider.ValueChanged = function(obj)
            self:SetTransparency(obj:GetValue() / 100);
            obj.labelItem:UpdateLabel();
            obj.labelItem:SetChecked(true);
            obj.globalItem:SetChecked(false);
        end
        item:GetItems():Add(sliderItem);
        L:SetContext(prevContext);
    elseif (itemName == "Scale") then
        local prevContext = L:SetContext("/BarMenu/SettingsMenu/ScaleMenu");
        local checked = (self.settings.scale == nil);
        local globalItem = Turbine.UI.MenuItem(L:GetText("UseGlobalSetting"), true, checked);
        globalItem.Click = function(obj)
            obj:SetChecked(true);
            self:SetScale(nil);
        end
        item:GetItems():Add(globalItem);

        checked = not checked;
        local labelItem = Turbine.UI.MenuItem("", true, checked);
        globalItem.labelItem = labelItem;
        labelItem.globalItem = globalItem;
        labelItem.UpdateLabel = function(obj)
            local scale = self:GetScale();
            obj.Label:SetText(L:GetText("/PluginManager/OptionsTab/GlobalSettingsTab/UiScale") .. ": " .. string.format("%d", scale * 100 / 32) .. "%");
        end
        labelItem:UpdateLabel();
        labelItem.Click = function(obj)
            obj.KeepOpen = true;
            obj:SetChecked(true);
            self:SetScale(self:GetScale());
            obj.globalItem:SetChecked(false);
        end
        item:GetItems():Add(labelItem);

        local sliderItem = Turbine.UI.MenuItem("                                        ", false, false);
        local slider = Turbine.UI.Lotro.ScrollBar();
        slider.labelItem = labelItem;
        slider.globalItem = globalItem;
        globalItem.slider = slider;
        slider:SetSize(200, 10);
        slider:SetBackground("Thurallor/SequenceBars/Images/scrollbar_background_horiz.tga");
        slider:SetBlendMode(Turbine.UI.BlendMode.Overlay);
        slider:SetParent(sliderItem.Label);
        slider:SetMinimum(13);
        slider:SetMaximum(96);
        slider:SetSmallChange(1);
        slider:SetLargeChange(10);
        slider:SetValue(self:GetScale());
        slider.ValueChanged = function(obj)
            self:SetScale(obj:GetValue());
            obj.labelItem:UpdateLabel();
            obj.labelItem:SetChecked(true);
            obj.globalItem:SetChecked(false);
        end
        item:GetItems():Add(sliderItem);
        L:SetContext(prevContext);
    elseif (itemName == "MouseWheel") then
        local prevContext = L:SetContext("MouseWheelMenu");
        self:AddSettingsMenuItem(item, "WheelDisabled");
        self:AddSettingsMenuItem(item, "WheelForwardFast");
        self:AddSettingsMenuItem(item, "WheelForwardSlow");
        self:AddSettingsMenuItem(item, "WheelForwardVerySlow");
        self:AddSettingsMenuItem(item, "WheelReverseFast");
        self:AddSettingsMenuItem(item, "WheelReverseSlow");
        self:AddSettingsMenuItem(item, "WheelReverseVerySlow");
        L:SetContext(prevContext);
    elseif (string.find("|WheelDisabled|WheelForwardFast|WheelForwardSlow|WheelForwardVerySlow|WheelReverseFast|WheelReverseSlow|WheelReverseVerySlow|", "|" .. itemName .. "|")) then
        local wheelSpeed = { ["WheelDisabled"] = 0; ["WheelForwardFast"] = 1; ["WheelForwardSlow"] = 0.5; ["WheelForwardVerySlow"] = 0.2; ["WheelReverseFast"] = -1; ["WheelReverseSlow"] = -0.5; ["WheelReverseVerySlow"] = -0.2; };
        local checked = (self.settings.wheelSpeed == wheelSpeed[itemName]);
        item:SetChecked(checked);
        item:SetEnabled(not checked);
        item.Click = function()
            self.wheelPosition = 0;
            self.settings.wheelSpeed = wheelSpeed[itemName];
            self:ReapplySettings();
        end
    elseif (itemName == "Animation") then
        local prevContext = L:SetContext("AnimationMenu");
        self:AddSettingsMenuItem(item, "Instantaneous");
        self:AddSettingsMenuItem(item, "Fast");
        self:AddSettingsMenuItem(item, "Slow");
        self:AddSettingsMenuItem(item, "VerySlow");
        self:AddSettingsMenuItem(item, "Disabled");
        L:SetContext(prevContext);
    elseif (string.find("|Instantaneous|Fast|Slow|VerySlow|Disabled|", "|" .. itemName .. "|")) then
        local duration = { ["Instantaneous"] = 0; ["Fast"] = 0.05; ["Slow"] = 0.1; ["VerySlow"] = 0.5; ["Disabled"] = -1; };
        local checked = (self.settings.animation.duration == duration[itemName]);
        item:SetChecked(checked);
        item:SetEnabled(not checked);
        item.Click = function()
            self:SetAnimationSpeed(duration[itemName]);
        end
    elseif (itemName == "Orientation") then
        local prevContext = L:SetContext("OrientationMenu");
        self:AddSettingsMenuItem(item, "Up");
        self:AddSettingsMenuItem(item, "Right");
        self:AddSettingsMenuItem(item, "Left");
        self:AddSettingsMenuItem(item, "Down");
        L:SetContext(prevContext);
    elseif ((itemName == "Up") or (itemName == "Down") or (itemName == "Left") or (itemName == "Right")) then
        local checked = (self.settings.orientation == itemName);
        item:SetChecked(checked);
        item:SetEnabled(not checked);
        item.Click = function()
            self.settings.orientation = itemName;
            self:SaveSettings(false);
            self:Redraw();
            self:MoveOntoScreen();
        end
    elseif ((itemName == "Width") or (itemName == "Height")) then
        local size = self.settings.visibleSlots;
        local displayWidth, displayHeight = Turbine.UI.Display.GetSize();
        local minSize = 1, maxSize;
        if ((self.settings.orientation == "Up") or (self.settings.orientation == "Down")) then
            maxSize = math.floor(displayHeight / self.settings.slotSize);
        else
            maxSize = math.floor(displayWidth / self.settings.slotSize);
        end
        local prevContext = L:SetContext("/BarMenu/SettingsMenu/HeightWidthMenu");

        local labelItem = Turbine.UI.MenuItem("", true, false);
        labelItem.UpdateLabel = function(obj)
            local size = self.settings.visibleSlots;
            local text;
            if (size == 1) then
                text = L:GetText("/BarMenu/SettingsMenu/HeightWidthMenu/Slot");
            else
                text = L:GetText("/BarMenu/SettingsMenu/HeightWidthMenu/Slots");
            end
            text = text:gsub("<num>", tostring(size));
            obj.Label:SetText(text);
        end
        labelItem:UpdateLabel();
        labelItem.Click = function(obj)
            obj.KeepOpen = true;
        end
        item:GetItems():Add(labelItem);

        local sliderItem = Turbine.UI.MenuItem("                                        ", false, false);
        local slider = Turbine.UI.Lotro.ScrollBar();
        slider.labelItem = labelItem;
        slider:SetSize(200, 10);
        slider:SetBackground("Thurallor/SequenceBars/Images/scrollbar_background_horiz.tga");
        slider:SetBlendMode(Turbine.UI.BlendMode.Overlay);
        slider:SetParent(sliderItem.Label);
        slider:SetMinimum(minSize);
        slider:SetMaximum(maxSize);
        slider:SetSmallChange(1);
        slider:SetLargeChange(5);
        slider:SetValue(size);
        slider.ValueChanged = function(obj)
            local size = obj:GetValue();
            self.cursorHomePosNudger:SetMaxValue(size);
            if (size < self.settings.cursorHomePosition) then
                self.settings.cursorHomePosition = size;
            end
            self:ResizeBar(size);
            self:MoveOntoScreen();                
            obj.labelItem:UpdateLabel();
        end
        item:GetItems():Add(sliderItem);
        L:SetContext(prevContext);
    elseif (itemName == "Cursor") then
        local prevContext = L:SetContext("CursorMenu");
        self:AddSettingsMenuItem(item, "Style");
        self:AddSettingsMenuItem(item, "HomePosition");
        L:SetContext(prevContext);
    elseif (itemName == "HomePosition") then
        local homePos = self.settings.cursorHomePosition;
        local minPos, maxPos = 1, self.settings.visibleSlots;
        local labelItem = Turbine.UI.MenuItem(L:GetText("/BarMenu/SettingsMenu/CursorMenu/HomePositionMenu/Slot"):gsub("<num>", ""), true, false);
        labelItem.Click = function(obj)
            obj.KeepOpen = true;
        end
        item:GetItems():Add(labelItem);

        local nudgerItem = Turbine.UI.MenuItem(" ", false, false);
        local nudger = Thurallor.UI.Nudger(nudgerItem, Turbine.UI.Lotro.Font.TrajanPro14, homePos, minPos, maxPos);
        nudger:SetWidth(30);
        nudger:SetPosition(25, 5);
        nudger.ValueChanged = function(obj)
            self:Expand();
            self.settings.cursorHomePosition = obj:GetValue();
            self:SaveSettings(false);
            self:Redraw();
        end
        self.cursorHomePosNudger = nudger;
        
        nudgerItem:SetHeight(nudger:GetHeight() + 5);
        nudgerItem.SetSize = function() end; -- don't allow the size to be adjusted by ContextMenu
        local container = item:GetItems();
        container:Add(nudgerItem);
        container:Add(Turbine.UI.MenuItem(" ", false, false));
        container:Add(Turbine.UI.MenuItem(" ", false, false));
    elseif (itemName == "Style") then
        local prevContext = L:SetContext("StyleMenu");
        local styles = {};
        for name, info in pairs(resources.Cursor) do
            local checked = (name == self.settings.cursorStyle);
            local text = L:GetText(name);
            local style = Turbine.UI.MenuItem(text, not checked, checked);
            styles[text] = style;
            style.Click = function()
                self:SetCursorStyle(name);
                self:SaveSettings(false);
                self:Redraw();
            end
        end
        -- Display alphabetically.
        for text in sorted_keys(styles) do
            item:GetItems():Add(styles[text]);
        end
        L:SetContext(prevContext);
    elseif (itemName == "Caption") then
        local prevContext = L:SetContext("CaptionMenu");
        self:AddSettingsMenuItem(item, "Edit");
        self:AddSettingsMenuItem(item, "CaptionPosition");
        self:AddSettingsMenuItem(item, "CaptionVisible");
        self:AddSettingsMenuItem(item, "CaptionClickable");
        L:SetContext(prevContext);
    elseif (itemName == "Edit") then
        item:SetEnabled(not self.captionEditor);
        item.Click = function()
            self:EditCaption();
        end
    elseif (itemName == "CaptionClickable") then
        item:SetChecked((not self.settings.caption.clickThru) and (self.settings.caption.visible ~= "Never"));
        item:SetEnabled(self.settings.caption.visible ~= "Never");
        item.Click = function()
            self:SetCaptionClickThru(not self.settings.caption.clickThru);
        end
    elseif (itemName == "CaptionVisible") then
        local prevContext = L:SetContext("CaptionVisibleMenu");
                for _, option in pairs(L:GetItems()) do
                        local subItem = Turbine.UI.MenuItem(L:GetText(option), true, (self.settings.caption.visible == option));
                        subItem.Click = function()
                                self:SetCaptionVisible(option);
                        end
                        item:GetItems():Add(subItem);
                end
        L:SetContext(prevContext);
    elseif (itemName == "CaptionPosition") then
        local prevContext = L:SetContext("CaptionPositionMenu");
        if ((self.settings.orientation == "Up") or (self.settings.orientation == "Down")) then
            self:AddSettingsMenuItem(item, "Top");
            self:AddSettingsMenuItem(item, "Bottom");
        else -- "Left" or "Right"
            self:AddSettingsMenuItem(item, "LeftSide");
            self:AddSettingsMenuItem(item, "RightSide");
        end
        L:SetContext(prevContext);
    elseif (string.find("|Top|Bottom|LeftSide|RightSide|", "|" .. itemName .. "|")) then
        local beginPosMap = { Left = "RightSide"; Right = "LeftSide"; Up = "Bottom"; Down = "Top" };
        local amBeginPos = (itemName == beginPosMap[self.settings.orientation]);
        local beginPosSelected = (self.settings.caption.position == "beginning");
        local checked = (amBeginPos and beginPosSelected) or (not amBeginPos and not beginPosSelected);
        local myOrientation = (amBeginPos and "beginning") or "end";
        item:SetChecked(checked);
        item:SetEnabled(not checked);
        item.Click = function()
            self.settings.caption.position = myOrientation;
            self:SaveSettings(false);
            self:Redraw(true);
            self:MoveOntoScreen();
        end
    elseif (itemName == "Clone") then
        item.Click = function()
            self.parent:CloneBar(self.objectID);
        end
    elseif (itemName == "Group") then
        self.parent:AddSettingsMenuItem(item, "Root", self);
    elseif (itemName == "Global") then
        self.manager:AddSettingsMenuItem(item, "Root", self);
    elseif (itemName == "Delete") then
        item.Click = function()
            self.parent:DeleteBar(self.objectID);
        end
    end
    return item;
end

function Bar:SetTransparency(opacity)
    self.settings.opacity = opacity;
    self:SaveSettings();
    self:Redraw(true);
end

function Bar:GetTransparency()
    return self.settings.opacity or self.globals.uiOpacity;
end

function Bar:SetScale(scale)
    self.settings.scale = scale;
    self:SaveSettings();
    self:Redraw(true);
end

function Bar:GetScale()
    return self.settings.scale or self.globals.uiScale;
end

function Bar:ResizeBar(size)
    self:Expand();
    self.settings.visibleSlots = size;
    if (self.settings.cursorHomePosition > size) then
        self.settings.cursorHomePosition = size;
    end
    self:SaveSettings(false);
    self:Redraw();
end

function Bar:SetCursorStyle(style)
    if (style == nil) then
        style = self.settings.cursorStyle;
    end
    self.settings.cursorStyle = style;
    local resource = resources.Cursor[style];
 
    if (self.cursor) then
        self.cursor:SetParent(nil);
        self.cursor = nil;
    end
    self.cursor = Turbine.UI.Control();
    self.cursor:SetParent(self.slotContainer);
    self.cursor:SetMouseVisible(false);
    if (resource.ApplyColor) then
        self.cursor:SetBackColor(self.color);
    end
    local background = resources.Cursor[style].Background;
    if (background) then
        self.cursor:SetBackground(resource.Background);
        self.cursorWidth, self.cursorHeight = GetAssetSize(resource.Background);
        self.cursor:SetSize(self.cursorWidth, self.cursorHeight);
        -- for now, presume square shape
        self.cursorSize = math.ceil(self.cursorWidth / 2) * 2;
    else
        self.cursorSize = 36;
    end
    if (resource.BackColorBlendMode) then
        self.cursor:SetBackColorBlendMode(resource.BackColorBlendMode);
    end
    if (resource.BlendMode) then
        self.cursor:SetBlendMode(resource.BlendMode);
    end
    if (resource.ZOrder) then
        self.cursor:SetZOrder(2 + resource.ZOrder);
    end
--    self.cursor:SetVisible(not self:IsHidden());
    if (not resource.xOffset) then
        resource.xOffset = 0;
    end
    if (not resource.yOffset) then
        resource.yOffset = 0;
    end
    
    self.cursorMargin = math.ceil((self.cursorSize - self.settings.slotSize) / 2);
    self:SetCursorPos(self.cursorPos);
end

function Bar:SetLocked(state)
    self.settings.locked = state;
    self:SaveSettings(false);
end

function Bar:IsLocked()
    return self.settings.locked;
end

-- Scrolls the sequence such that the specified slot appears under the cursor
function Bar:SetCursorSlot(slot)

    local prevVisibleSlots = self.visibleSlots;
    self.visibleSlots = {};

    -- If advanced past the end of the sequence, start over at the beginning.
    if (slot > #self.slots) then
        slot = 1;
    end
    
    -- Find non-hidden slots to the left of the cursor
    local gameTime = Turbine.Engine.GetGameTime();
    local pos = self.cursorPos - 1;
    local s = slot - 1;
    while ((pos > 0) and (s > 0)) do
        local object = self.slots[s];
        if (not (object.hidden or (self.settings.hideInactiveBranches and self.sequence:SlotIsFolded(s, gameTime)))) then
            self.visibleSlots[object] = pos;
            pos = pos - 1;
        end
        s = s - 1;
    end

    self.cursorSlot = slot;

    -- Find non-hidden slots to the right of the cursor.
    -- Re-evaluate all conditionals except those to the left of the cursor.
    pos = self.cursorPos;
    s = slot;
    while ((pos <= self.displaySlots) and (s <= #self.slots)) do
        local object = self.slots[s];
        if (object.info.type == "If") then
            local success, condResult = pcall(object.condFunc, object, self.manager.player);
            object.condResult = (success and condResult) or false; -- nil => false
        end
        if (not (object.hidden or (self.settings.hideInactiveBranches and self.sequence:SlotIsFolded(s, gameTime)))) then
            self.visibleSlots[object] = pos;
            pos = pos + 1;
        end
        s = s + 1;
    end

    -- If no slots displayed, display the "reset" icon or "gears" if running continuously
    if (pos == self.cursorPos) then
        if (self.settings.continuous) then
            self.visibleSlots[self.gearsSlot] = pos;
        else
            self.visibleSlots[self.resetSlot] = pos;
        end
    end

    -- Make the old slots invisible
--if (self.settings.caption.text == "AoE Tank") then
--    Puts("prevVisibleSlots = " .. PrettyPrint(prevVisibleSlots, "", 1));
--end
    for object, pos in pairs(prevVisibleSlots) do
        if (not self.visibleSlots[object]) then
            object:SetParent(nil);
            object.pos = nil;
        end
    end
    
    -- Make the new slots visible
--if (self.settings.caption.text == "AoE Tank") then
--    Puts("self.visibleSlots = " .. PrettyPrint(prevVisibleSlots, "", 1));
--end
    if (not self.settings.hidden) then
        for object, pos in pairs(self.visibleSlots) do
            if (not prevVisibleSlots[object]) then
                object:SetParent(self.slotContainer);
            end
            local left, top = self:GetSlotCoords(pos);
            object:SetPosition(left - 1, top  - 1);
            object.pos = pos;
        end
    end
end

function Bar:StartExecuting()
    self.gearsSlot:SetRunning(true);
end

function Bar:StopExecuting()
    if (self.Log) then
        DoCallbacks(self, "Log", {"halting continuous execution", "BAR"});
    end
    self.gearsSlot:SetRunning(false);
end

function Bar:SlotClick(s, automatic)

    local slot = self.slots[s];
    if (automatic) then
        if (self.firstAutomaticClick) then
            if (self.wrapped and (s >= self.firstAutomaticClick)) then
                self.firstAutomaticClick = nil;

                -- If running continuously, tell the Gears object to perform a click at the next update ("Tick")
                if (self.settings.continuous) then
                    self.gearsSlot.Tick = function()
                        self.gearsSlot.Tick = nil; -- only once
                        local slot = self.slots[self.cursorSlot];
                        if (slot.info.automatic) then
                            self:SlotClick(s, true);
                        end
                    end
                end
                return;
            end
        else
            self.firstAutomaticClick = s;
            self.wrapped = false;
        end
    else
        self.firstAutomaticClick = nil;
        self.clickedPos = slot.pos;
        self.clickedSlot = s;
        self.reevaluatingConditionals = {};
    end

    -- Do the action associated with the slot, if any
    if (slot.actionFunc) then
        if (self.Log) then
            DoCallbacks(self, "Log", {"(slot " .. tostring(s) .. ") executing: " .. slot.info.type, "SPECIAL-SLOT"});
            if (slot.info.type == "GenerateEvent") then
                DoCallbacks(self, "Log", {"generated: " .. slot.info.eventName, "EVENT"});
            end
        end
        pcall(slot.actionFunc, slot, s);
    end
    
    -- Process reset slots
    if (slot.info.type == "Reset") then
        return self:Reset();
    end
    
    -- Process conditionals (if/then/else) and Lua scripts.
    if (slot.info.type == "If") then
        local success, condResult = pcall(slot.condFunc, slot, self.manager.player);
        slot.condResult = (success and condResult) or false; -- nil => false
        if (self.Log) then
            DoCallbacks(self, "Log", {"(slot " .. tostring(s) .. ") evaluating conditional: " .. tostring(slot.condResult), "SPECIAL-SLOT"});
        end
        if (automatic and slot.info.reevaluate) then
            self:ReevaluateContinuously(s);
        end
        if (not slot.condResult) then
            if (slot.elseSlot) then
                s = slot.elseSlot - 1;
            elseif (slot.endIfSlot) then
                s = slot.endIfSlot - 1;
            end
        end
    elseif (slot.info.type == "Else") then
        doElse = true;
        if (slot.ifSlot) then
            doElse = not self.slots[slot.ifSlot].condResult;
        end
        if (slot.endIfSlot and not doElse) then
            s = slot.endIfSlot - 1;
        end
    end
    
    local nextSlot = s + 1;

    -- If a previous advancement callback was never called, unregister it.
    self:UnregisterAdvanceCallback();

    if (slot.info.advanceEvent) then
        if (slot.info.advanceEvent == "ItemEquipped") then
            if (not IsEquipped(slot:GetItemName())) then
                slot.advanceFunc["ItemEquipped"] = function()
                    self:UnregisterAdvanceCallback();
--                    if (IsEquipped(slot:GetItemName()) then
                        self:AdvanceToSlot(nextSlot, "item equipped");
--                    end
                end
                AddCallback(Thurallor.Utils.Watcher, "ItemEquipped", slot.advanceFunc["ItemEquipped"]);
                table.insert(self.advancement, { object = Thurallor.Utils.Watcher, event = "ItemEquipped", func = slot.advanceFunc["ItemEquipped"] });
            end
        elseif (slot.info.advanceEvent == "DelayComplete") then
            slot.advanceFunc["Reset"] = function()
                self:UnregisterAdvanceCallback();
                self:AdvanceToSlot(nextSlot, "delay complete");
                if (self.settings.continuous) then
                    self.gearsSlot:SetDelaying(false);
                end
            end
            AddCallback(slot, "Reset", slot.advanceFunc["Reset"]);
            table.insert(self.advancement, { object = slot, event = "Reset", func = slot.advanceFunc["Reset"] });
        elseif (slot.info.advanceEvent == "SkillExecuted") then
            local skillNames = slot:GetSkillNames();
            if (skillNames) then
                local skill = slot:GetSkill();
                if (skill) then

                                        -- For most skills, we can use the ResetTimeChanged event to detect execution.
                    if (not slot.info.executeDetectByChat) then
                        slot.advanceFunc["ResetTimeChanged"] = function()
                            self:UnregisterAdvanceCallback();
                            self:AdvanceToSlot(nextSlot, "skill executed (cooldown reset detected)");
                        end
                        AddCallback(skill, "ResetTimeChanged", slot.advanceFunc["ResetTimeChanged"]);
                        table.insert(self.advancement, { object = skill, event = "ResetTimeChanged", func = slot.advanceFunc["ResetTimeChanged"] });

                    -- For other skills, we can wait for chat message indicating skill has fired.
                    else
                        for _, altName in pairs(skillNames) do
                            slot.advanceFunc[altName] = function(_, args)
                                if ((args.ChatType == Turbine.ChatType.PlayerCombat) and (string.find(args.Message, altName, 1, true))) then
                                    self:UnregisterAdvanceCallback();
                                    self:AdvanceToSlot(nextSlot, "skill executed (observed in combat log)");
                                end
                            end
                            AddCallback(Turbine.Chat, "Received", slot.advanceFunc[altName]);
                            table.insert(self.advancement, { object = Turbine.Chat, event = "Received", func = slot.advanceFunc[altName] });
                        end

                    end
                    -- Toggle skills only have a chat message when activated; when deactivated, they get a ResetTimeChanged
                    -- event.  Shouldn't be a problem, since by that time we won't be watching any more.
                else
                    -- The skill is unlearned, so it won't ever execute.
                    table.insert(self.advancement, { object = Turbine.Chat, event = "Never" });
                end
            else
                Puts("Unknown skill in slot " .. s .. "; don't know how to detect its execution");
            end
        end
    end

        -- If skill requires a target, don't advance.
        if (slot.info.requireTarget) then
        if ((slot.info.type == "SelectTarget") and (slot.entity == nil)) then
            if (self.Log) then
                DoCallbacks(self, "Log", {"failed: selection failed", "ADVANCE"});
            end
            return;
        elseif (Thurallor.Utils.Watcher.targetObject == nil) then
            if (self.Log) then
                DoCallbacks(self, "Log", {"failed: target required", "ADVANCE"});
            end
            self:UnregisterAdvanceCallback();
            return;
        end
        end
    
    -- Advance immediately.
    if (not next(self.advancement)) then
        self:AdvanceToSlot(nextSlot);
    end
end

function Bar:ReevaluateContinuously(s)
    local slot = self.slots[s];
    -- Reevaluate this conditional at the next frame update:
    table.insert(self.reevaluatingConditionals, { slot.condFunc, slot.condResult, s });
    self:SetWantsUpdates(true);
end

function Bar:StartSlotCooldown(s, delay)
    local slot = self.slots[s];
    slot.hiddenAfterReset = slot.hidden;
    if (self.settings.continuous) then
        self.gearsSlot:SetDelaying(true);
    else
        slot.hidden = false;
        self:SetCursorPos(self.clickedPos);
        self:SetCursorSlot(s);
    end
    slot:StartCooldown(delay);
end

function Bar:UnregisterAdvanceCallback()
    if (next(self.advancement)) then
        for _, info in pairs(self.advancement) do
            RemoveCallback(info.object, info.event, info.func);
        end
        self.advancement = {};
    end
end

function Bar:AdvanceToSlot(s, cause)
    
    if (s > #self.slots) then
        self.wrapped = true;
        self.reevaluatingConditionals = {};
        s = 1;
    end
    local slot = self.slots[s];

    if (self.Log) then
        if (cause) then
            DoCallbacks(self, "Log", {"to slot " .. tostring(s) .. ", caused by: " .. cause, "ADVANCE"});
        else
            DoCallbacks(self, "Log", {"to slot " .. tostring(s), "ADVANCE"});
        end
    end
    
    -- If the user didn't click on the currently selected slot, move the cursor.
    if (self.clickedPos ~= self.cursorPos) then
        self:SetCursorPos(self.clickedPos);
    end

    -- If the next slot is automatic, auto-click it.
    if (slot.info.automatic) then
        self:SetCursorSlot(s);
        return self:SlotClick(s, true);
    end
    
    -- If animation is disabled via settings, or stopped via a "Stop" slot, refold the slots and do nothing further.
    if ((self.settings.animation.duration == -1) or self.animationStopped) then
        self:SetCursorSlot(self.clickedSlot);
        return;
    end
    
    -- Do animation.
    self:SetCursorSlot(s);
    if (self.settings.animation.duration > 0) then
        self:Animate(0);
    end
end

function Bar:Update()
    local wantsMoreUpdates = false;
    if (self.animationStartTime) then
        local timeDelta = Turbine.Engine.GetGameTime() - self.animationStartTime;
        if (timeDelta > 0) then
            self:Animate(timeDelta);
        end
        if (self.animationStartTime) then
            wantsMoreUpdates = true;
        end
    end
--    if (self.delayedClick) then
--        self:SlotClick(self.delayedClick, true);
--        if (self.delayedClick) then
--            wantsMoreUpdates = true;
--        end
--    end
    if (self.mouseInside) then
        local mouseLeft, mouseTop = Turbine.UI.Display:GetMousePosition();
        if (not self:MouseInside(mouseLeft, mouseTop)) then
            self:MouseDepart();
        end
        if (self.mouseInside) then
            wantsMoreUpdates = true;
        end
    end
    local firstConditional = self.reevaluatingConditionals[1];
    if (firstConditional) then
        for _, condInfo in ipairs(self.reevaluatingConditionals) do
            local condFunc, prevResult, s = unpack(condInfo);
            local slot = self.slots[s];
            local success, condResult = pcall(condFunc, slot, self.manager.player);
            condResult = (success and condResult) or false; -- nil => false
            if (condResult ~= prevResult) then
                self.reevaluatingConditionals = {};
                self:AdvanceToSlot(firstConditional[3], "condition at slot " .. tostring(s) .. " changed from " .. tostring(prevResult) .. " to " .. tostring(condResult));
                break;
            end
        end
        wantsMoreUpdates = true;
    end
    if (not wantsMoreUpdates) then
        self:SetWantsUpdates(false);
    end
end

function Bar:SetAnimationSpeed(duration)
    self.settings.animation.duration = duration;
    self:SaveSettings(false);
end

-- Achieves the scrolling effect.  Gets called by Bar:Update().
function Bar:Animate(timeDelta)
    local progress = timeDelta / self.settings.animation.duration;
    if (progress == 0) then
        self.animationStartTime = Turbine.Engine.GetGameTime();
        self.animationDistance = self.settings.slotSize + self.settings.slotSpacing;
        self:SetWantsUpdates(true);
    elseif (progress >= 1) then
        progress = 1;
        self.animationStartTime = nil;
    end
    local displacement = math.floor(self.animationDistance * (1 - progress));
    if (self.settings.orientation == "Up") then
        self.slotContainer:SetTop(self.slotsTop - displacement);
    elseif (self.settings.orientation == "Down") then
        self.slotContainer:SetTop(self.slotsTop + displacement);
    elseif (self.settings.orientation == "Left") then
        self.slotContainer:SetLeft(self.slotsLeft - displacement);
    elseif (self.settings.orientation == "Right") then
        self.slotContainer:SetLeft(self.slotsLeft + displacement);
    end

    -- Make sure the item under the cursor is always clickable.  This hack is
    -- necessary because as the slot moves, no MouseEnter event occurs and
    -- hence no MouseClick events can be received.
    self.slots[self.cursorSlot]:SetVisible(false);
    self.slots[self.cursorSlot]:SetVisible(true);
end

function Bar:Destroy()
    if (self.sequenceEditor) then
        self.sequenceEditor:Close();
    end
    -- Unregister as event generator
    self.manager:SetEventGenerators(false, self);
    Node.Destroy(self);
    self.manager:SetIncludees(self.objectID, nil);
end

function Bar:GetVisibleLeft()
    local left = self:GetSlotsScreenCoords();
    return left;
end

function Bar:SetVisibleLeft(left)
    self:OffsetPosition(left - self:GetVisibleLeft(), 0);
end

function Bar:GetVisibleTop()
    local _, top = self:GetSlotsScreenCoords();
    return top;
end

function Bar:SetVisibleTop(top)
    self:OffsetPosition(0, top - self:GetVisibleTop());
end

function Bar:GetVisibleWidth()
    local _, _, width = self:GetSlotsScreenCoords();
    return width;
end

function Bar:GetVisibleHeight()
    local _, _, _, height = self:GetSlotsScreenCoords();
    return height;
end

function Bar:GetSlotCoords(slot)
    local slotSize = self.settings.slotSize + self.settings.slotSpacing;
    local offset = self.cursorMargin + (slot - 1) * slotSize;
    local left, top;
    if (self.settings.orientation == "Up") then
        left = self.cursorMargin;
        top = self.slotContainer:GetHeight() - slotSize - offset;
    elseif (self.settings.orientation == "Down") then
        left = self.cursorMargin;
        top = offset;
    elseif (self.settings.orientation == "Left") then
        left = self.slotContainer:GetWidth() - slotSize - offset;
        top = self.cursorMargin;
    else -- "Right"
        left = offset;
        top = self.cursorMargin;
    end
    return left, top
end

function Bar:Find(fromCtl)
    fromCtl.KeepOpen = true;
    self:MoveOntoScreen();
    self.zoomer = Thurallor.UI.Zoomer(fromCtl, self, 1.5);
    AddCallback(self.zoomer, "ZoomComplete", function()
        Turbine.UI.Window.Activate(self);
        self:Flash();
        self.zoomer = nil;
        fromCtl:GetParent():GetParent():Close();
    end);
end

function Bar:MoveOntoScreen()
    local screenWidth, screenHeight = Turbine.UI.Display:GetWidth(), Turbine.UI.Display:GetHeight();
    local left, top = self:PointToScreen(self.caption:GetLeft(), self.caption:GetTop());
    local right, bottom = left + self.caption:GetWidth(), top + self.caption:GetHeight();
    local deltaX, deltaY = 0, 0;
    if (left < 0) then
        deltaX = -left;
    elseif (right > screenWidth) then
        deltaX = screenWidth - right;
    end
    if (top < 0) then
        deltaY = -top;
    elseif (bottom > screenHeight) then
        deltaY = screenHeight - bottom;
    end
    self:OffsetPosition(deltaX, deltaY);
    self:SnapToGrid();
end

function Bar:Flash()
    self:Activate();
    self.caption.startTime = Turbine.Engine.GetGameTime();
    self.caption.label:SetVisible(true);
    if (not self.marquee) then
        self.marquee = Thurallor.UI.Marquee();
        self.marquee:SetParent(self);
        self.marquee:SetSize(self:GetSize());
    end
    self.caption.Update = function(caption, args)
        local timeDelta = Turbine.Engine.GetGameTime() - caption.startTime;
        if (timeDelta > 3) then
            caption.label:SetForeColor(self.color);
            caption.label:SetVisible((not self.settings.caption.clickThru) and (self.settings.caption.visible ~= "Never"));
            caption:SetWantsUpdates(false);
            self.marquee:SetParent(nil);
            self.marquee = nil;
        else
            local progress = (math.sin(timeDelta * 15) + 1) / 2; --math.modf(timeDelta / 0.3);
            caption.label:SetForeColor(Turbine.UI.Color(progress, 1, 1, 0));
        end
    end
    self.caption:SetWantsUpdates(true);
end

function Bar:RenameUserEvent(oldName, newName)

    -- Update event behaviors
    Node.RenameUserEvent(self, oldName, newName);

    -- Update "Generate User Event" slots
    local oldEvents = self.sequence:GetEvents();
    if (oldEvents[oldName]) then

        for s, info in ipairs(self.settings.sequenceItemInfo) do
            if ((info.type == "GenerateEvent") and (info.eventName == oldName)) then
                info.eventName = newName;
            end
        end
    end

    -- Update sequence editor, if such a slot is currently being edited
    if (self.sequenceEditor) then
        self.sequenceEditor:Redraw();
    end

    --self:Rebuild();
    --Manager needs to rebuild all bars after the change is complete, since included bars may not be changed yet.
end

Compare with Previous | Blame


All times are GMT -5. The time now is 04:46 PM.


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