You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
411 lines
12 KiB
411 lines
12 KiB
|
3 years ago
|
-- ----------------------------------------------------------------------------
|
||
|
|
-- A persistent timer for World Bosses.
|
||
|
|
-- ----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
-- addonName, addonTable = ...;
|
||
|
|
local _, WBT = ...;
|
||
|
|
|
||
|
|
local Util = WBT.Util;
|
||
|
|
local Com = WBT.Com;
|
||
|
|
local Options = {}; -- Must be initialized later.
|
||
|
|
|
||
|
|
-- Provides the GUI API and performs checks if the GUI is shown etc.
|
||
|
|
local GUI = {};
|
||
|
|
WBT.GUI = GUI;
|
||
|
|
|
||
|
|
WBT.G_window = {};
|
||
|
|
|
||
|
|
local WIDTH_DEFAULT = 200;
|
||
|
|
local HEIGHT_BASE = 30;
|
||
|
|
local HEIGHT_DEFAULT = 106;
|
||
|
|
local MAX_ENTRIES_DEFAULT = 7;
|
||
|
|
|
||
|
|
local WIDTH_EXTENDED = 240;
|
||
|
|
|
||
|
|
-- The sum of the relatives is not 1, because I've had issues with "Flow"
|
||
|
|
-- elements sometimes overflowing to next line then.
|
||
|
|
local BTN_OPTS_REL_WIDTH = 30/100;
|
||
|
|
local BTN_REQ_REL_WIDTH = 30/100;
|
||
|
|
local BTN_SHARE_REL_WIDTH = 39/100; -- Needs the extra width or name might not show.
|
||
|
|
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
function GUI:Init()
|
||
|
|
Options = WBT.Options;
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:CreateLabels()
|
||
|
|
self.labels = {};
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:Restart()
|
||
|
|
self:New();
|
||
|
|
end
|
||
|
|
|
||
|
|
local function ShowBossZoneOnlyAndOutsideZone()
|
||
|
|
return Options.show_boss_zone_only.get() and not WBT.InBossZone();
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:ShouldShow()
|
||
|
|
if (ShowBossZoneOnlyAndOutsideZone()) then
|
||
|
|
return false;
|
||
|
|
end
|
||
|
|
local should_show =
|
||
|
|
not self.visible
|
||
|
|
and not WBT.db.global.hide_gui;
|
||
|
|
|
||
|
|
|
||
|
|
return should_show;
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:ShouldHide()
|
||
|
|
-- Should GUI only be shown in boss zone, but is not in one?
|
||
|
|
-- Are no bosses considered dead (waiting for respawn)?
|
||
|
|
|
||
|
|
local should_hide = (WBT.db.global.hide_gui or ShowBossZoneOnlyAndOutsideZone())
|
||
|
|
and self.visible;
|
||
|
|
|
||
|
|
return should_hide;
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:Show()
|
||
|
|
if self.released then
|
||
|
|
self:Restart();
|
||
|
|
end
|
||
|
|
|
||
|
|
self.gui_container.frame:Show();
|
||
|
|
self.visible = true;
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:Hide()
|
||
|
|
if self.released then
|
||
|
|
return;
|
||
|
|
end
|
||
|
|
|
||
|
|
self.gui_container.frame:Hide();
|
||
|
|
self.visible = false;
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:UpdateGUIVisibility()
|
||
|
|
if self:ShouldShow() then
|
||
|
|
self:Show();
|
||
|
|
elseif self:ShouldHide() then
|
||
|
|
self:Hide();
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:LockOrUnlock()
|
||
|
|
self.window.frame:SetMovable(not Options.lock.get());
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Ensure that right clicking outside of the window
|
||
|
|
-- does not trigger the interactive label, or hide
|
||
|
|
-- other GUI components.
|
||
|
|
function GUI.LabelWidth(width)
|
||
|
|
return width - 15;
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:CreateNewLabel(guid)
|
||
|
|
local gui = self;
|
||
|
|
local label = GUI.AceGUI:Create("InteractiveLabel");
|
||
|
|
label:SetWidth(GUI.LabelWidth(self.width));
|
||
|
|
label:SetCallback("OnClick", function(self)
|
||
|
|
local text = self.label:GetText();
|
||
|
|
if text and text ~= "" then
|
||
|
|
WBT.ResetBoss(guid);
|
||
|
|
end
|
||
|
|
gui:Update();
|
||
|
|
end);
|
||
|
|
label.userdata.added = false;
|
||
|
|
self.labels[guid] = label;
|
||
|
|
return label;
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:UpdateHeight(n_entries)
|
||
|
|
local new_height = n_entries <= MAX_ENTRIES_DEFAULT and HEIGHT_DEFAULT + n_entries
|
||
|
|
or (self.window.content:GetNumChildren() * 11 + HEIGHT_BASE);
|
||
|
|
if self.height == new_height then
|
||
|
|
return;
|
||
|
|
end
|
||
|
|
|
||
|
|
self.window:SetHeight(new_height);
|
||
|
|
self.height = new_height;
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:UpdateWidth()
|
||
|
|
local new_width = Options.multi_realm.get() and WIDTH_EXTENDED or WIDTH_DEFAULT;
|
||
|
|
if self.width == new_width then
|
||
|
|
return;
|
||
|
|
end
|
||
|
|
|
||
|
|
self.width = new_width;
|
||
|
|
self.window:SetWidth(new_width);
|
||
|
|
self.btn_container:SetWidth(new_width);
|
||
|
|
for _, label in pairs(self.labels) do
|
||
|
|
label:SetWidth(GUI.LabelWidth(new_width));
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI.GetLabelText(kill_info, all_info)
|
||
|
|
local prefix = "";
|
||
|
|
if all_info then
|
||
|
|
prefix = Util.ColoredString(Util.COLOR_DARKGREEN, strsub(kill_info.realm_name_normalized, 0, 3)) .. ":"
|
||
|
|
.. Util.ColoredString(Util.WarmodeColor(kill_info.realm_type), strsub(kill_info.realm_type, 0, 1)) .. ":";
|
||
|
|
end
|
||
|
|
return prefix .. WBT.GetColoredBossName(kill_info.name) .. ": " .. WBT.GetSpawnTimeOutput(kill_info);
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:RemoveLabel(guid, label)
|
||
|
|
-- This table is always a set, and can therefore be treated as such.
|
||
|
|
Util.RemoveFromSet(self.window.children, label);
|
||
|
|
label:Release();
|
||
|
|
self.labels[guid] = nil;
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:Rebuild()
|
||
|
|
if not self.labels then
|
||
|
|
return; -- GUI hasn't been built, so nothing to rebuild.
|
||
|
|
end
|
||
|
|
|
||
|
|
for guid, kill_info in pairs(WBT.db.global.kill_infos) do
|
||
|
|
local label = self.labels[guid];
|
||
|
|
if label == nil or getmetatable(kill_info) ~= WBT.KillInfo then
|
||
|
|
-- Do nothing.
|
||
|
|
else
|
||
|
|
self:RemoveLabel(guid, label);
|
||
|
|
end
|
||
|
|
end
|
||
|
|
self:Update();
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Returns true when a kill_info has a fresh kill that needs to be updated for the label.
|
||
|
|
function GUI.KillInfoHasFreshKill(kill_info, label)
|
||
|
|
local t_death = kill_info:GetServerDeathTime();
|
||
|
|
if label.userdata.t_death ~= t_death then
|
||
|
|
label.userdata.t_death = t_death;
|
||
|
|
return true;
|
||
|
|
else
|
||
|
|
return false;
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- True when the timer for a cyclic label restarted (i.e. made a lap).
|
||
|
|
-- This includes the event that a kill info goes from non-expired to expired.
|
||
|
|
function GUI.CyclicKillInfoRestarted(kill_info, label)
|
||
|
|
local t_next_spawn = kill_info:GetSpawnTimeSec();
|
||
|
|
if label.userdata.time_next_spawn < t_next_spawn then
|
||
|
|
label.userdata.time_next_spawn = t_next_spawn;
|
||
|
|
return true;
|
||
|
|
else
|
||
|
|
return false;
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Builds and/or updates what labels as necessary.
|
||
|
|
function GUI:UpdateContent()
|
||
|
|
local n_shown_labels = 0;
|
||
|
|
local needs_rebuild = false;
|
||
|
|
|
||
|
|
-- Note that labels are added in a certain order, which corresponds to the order they will
|
||
|
|
-- be displayed in the Window. If there is a need to re-order a label, then the solution
|
||
|
|
-- is to call GUI.Rebuild().
|
||
|
|
-- The reason for a rebuild instead of sorting is that the labels are bound to internal AceGUI objects
|
||
|
|
-- and I don't want to try to sort them.
|
||
|
|
for guid, kill_info in Util.spairs(WBT.db.global.kill_infos, WBT.KillInfo.CompareTo) do
|
||
|
|
local label = self.labels[guid] or self:CreateNewLabel(guid);
|
||
|
|
if getmetatable(kill_info) ~= WBT.KillInfo then
|
||
|
|
-- Do nothing.
|
||
|
|
elseif WBT.IsDead(guid)
|
||
|
|
and (not(kill_info.cyclic) or Options.cyclic.get())
|
||
|
|
and (WBT.ThisServerAndWarmode(kill_info) or Options.multi_realm.get()) then
|
||
|
|
n_shown_labels = n_shown_labels + 1;
|
||
|
|
label:SetText(GUI.GetLabelText(kill_info, Options.multi_realm.get()));
|
||
|
|
|
||
|
|
if not label.userdata.added then
|
||
|
|
self.window:AddChild(label);
|
||
|
|
label.userdata.added = true;
|
||
|
|
else
|
||
|
|
if GUI.KillInfoHasFreshKill(kill_info, label) or GUI.CyclicKillInfoRestarted(kill_info, label) then
|
||
|
|
-- The order of the labels needs to be updated, so rebuild.
|
||
|
|
needs_rebuild = true;
|
||
|
|
end
|
||
|
|
end
|
||
|
|
else
|
||
|
|
if label.userdata.added then
|
||
|
|
-- The label is apparently not automatically removed from the
|
||
|
|
-- self.window.children table, so it has to be done manually.
|
||
|
|
self:RemoveLabel(guid, label);
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if needs_rebuild then
|
||
|
|
self:Rebuild(); -- Warning: recursive call!
|
||
|
|
else
|
||
|
|
self:UpdateHeight(n_shown_labels);
|
||
|
|
self:UpdateWidth();
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:Update()
|
||
|
|
self:UpdateGUIVisibility();
|
||
|
|
|
||
|
|
if not(self.visible) then
|
||
|
|
return;
|
||
|
|
end
|
||
|
|
|
||
|
|
self:LockOrUnlock();
|
||
|
|
self:UpdateContent();
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:UpdatePosition(gp)
|
||
|
|
local relativeTo = nil;
|
||
|
|
self.window:ClearAllPoints();
|
||
|
|
self.window:SetPoint(gp.point, relativeTo, gp.xOfs, gp.yOfs);
|
||
|
|
end
|
||
|
|
|
||
|
|
local function GetDefaultPosition()
|
||
|
|
return {
|
||
|
|
point = "Center",
|
||
|
|
relativeToName = "UIParrent",
|
||
|
|
realtivePoint = nil,
|
||
|
|
xOfs = 0,
|
||
|
|
yOfs = 0,
|
||
|
|
};
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:InitPosition()
|
||
|
|
local gui_position = WBT.db.char.gui_position;
|
||
|
|
local gp;
|
||
|
|
if gui_position ~= nil then
|
||
|
|
gp = gui_position;
|
||
|
|
else
|
||
|
|
gp = GetDefaultPosition();
|
||
|
|
end
|
||
|
|
self:UpdatePosition(gp);
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:SaveGuiPoint()
|
||
|
|
local point, relativeTo, relativePoint, xOfs, yOfs = WBT.G_window:GetPoint();
|
||
|
|
WBT.db.char.gui_position = {
|
||
|
|
point = point,
|
||
|
|
relativeToName = "UIParrent",
|
||
|
|
relativePoint = relativePoint,
|
||
|
|
xOfs = xOfs,
|
||
|
|
yOfs = yOfs,
|
||
|
|
};
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:RecordPositioning()
|
||
|
|
hooksecurefunc(self.window.frame, "StopMovingOrSizing", self.SaveGuiPoint);
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:ResetPosition()
|
||
|
|
local gp = GetDefaultPosition();
|
||
|
|
self:UpdatePosition(gp);
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI.SetupAceGUI()
|
||
|
|
GUI.AceGUI = LibStub("AceGUI-3.0"); -- Need to create AceGUI during/after 'OnInit' or 'OnEnabled'
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:CleanUpWidgetsAndRelease()
|
||
|
|
GUI.AceGUI:Release(self.gui_container);
|
||
|
|
|
||
|
|
-- Remove references to fields. Hopefully the widget is
|
||
|
|
self.labels = nil;
|
||
|
|
self.visible = nil;
|
||
|
|
self.gui_container = nil;
|
||
|
|
self.window = nil;
|
||
|
|
|
||
|
|
self.released = true;
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:NewBasicWindow()
|
||
|
|
local window = GUI.AceGUI:Create("Window");
|
||
|
|
|
||
|
|
window.frame:SetFrameStrata("LOW");
|
||
|
|
window:SetWidth(self.width);
|
||
|
|
window:SetHeight(self.height);
|
||
|
|
|
||
|
|
window:SetTitle("WorldBossTimers");
|
||
|
|
window:SetLayout("List");
|
||
|
|
window:EnableResize(false);
|
||
|
|
|
||
|
|
self.visible = false;
|
||
|
|
|
||
|
|
return window;
|
||
|
|
end
|
||
|
|
|
||
|
|
-- "Decorator" of default closeOnClick, see AceGUIContainer-Window.lua.
|
||
|
|
local function closeOnClick(this)
|
||
|
|
PlaySound(799);
|
||
|
|
this.obj:Hide();
|
||
|
|
WBT.db.global.hide_gui = true;
|
||
|
|
end
|
||
|
|
|
||
|
|
function GUI:New()
|
||
|
|
if self.gui_container then
|
||
|
|
self.gui_container:Release();
|
||
|
|
end
|
||
|
|
|
||
|
|
self.released = false;
|
||
|
|
|
||
|
|
self.width = WIDTH_DEFAULT;
|
||
|
|
self.height = HEIGHT_DEFAULT;
|
||
|
|
self.window = GUI:NewBasicWindow();
|
||
|
|
self.window.closebutton:SetScript("OnClick", closeOnClick);
|
||
|
|
WBT.G_window = self.window;
|
||
|
|
|
||
|
|
self.btn_req = GUI.AceGUI:Create("Button");
|
||
|
|
self.btn_req:SetRelativeWidth(BTN_REQ_REL_WIDTH);
|
||
|
|
self.btn_req:SetText("Req.");
|
||
|
|
self.btn_req:SetCallback("OnClick", WBT.RequestKillData);
|
||
|
|
|
||
|
|
self.btn_opts = GUI.AceGUI:Create("Button");
|
||
|
|
self.btn_opts:SetText("/wbt");
|
||
|
|
self.btn_opts:SetRelativeWidth(BTN_OPTS_REL_WIDTH);
|
||
|
|
self.btn_opts:SetCallback("OnClick", function() WBT.AceConfigDialog:Open(WBT.addon_name); end);
|
||
|
|
|
||
|
|
self.btn_share = GUI.AceGUI:Create("Button");
|
||
|
|
self.btn_share:SetRelativeWidth(BTN_SHARE_REL_WIDTH);
|
||
|
|
self.btn_share:SetText("Share");
|
||
|
|
self.btn_share:SetCallback("OnClick", WBT.Functions.AnnounceTimerInChat);
|
||
|
|
|
||
|
|
self.btn_container = GUI.AceGUI:Create("SimpleGroup");
|
||
|
|
self.btn_container.frame:SetFrameStrata("LOW");
|
||
|
|
self.btn_container:SetLayout("Flow");
|
||
|
|
self.btn_container:SetWidth(self.width);
|
||
|
|
self.btn_container:AddChild(self.btn_opts);
|
||
|
|
self.btn_container:AddChild(self.btn_req);
|
||
|
|
self.btn_container:AddChild(self.btn_share);
|
||
|
|
|
||
|
|
self.gui_container = GUI.AceGUI:Create("SimpleGroup");
|
||
|
|
self.gui_container.frame:SetFrameStrata("LOW");
|
||
|
|
self.gui_container:SetLayout("List");
|
||
|
|
self.gui_container:AddChild(self.window);
|
||
|
|
self.gui_container:AddChild(self.btn_container);
|
||
|
|
|
||
|
|
-- I didn't notice any "OnClose" for the gui_container
|
||
|
|
-- ("SimpleGroup") so it's handled through the
|
||
|
|
-- Window class instead.
|
||
|
|
self.window:SetCallback("OnClose", function()
|
||
|
|
self:CleanUpWidgetsAndRelease();
|
||
|
|
end);
|
||
|
|
|
||
|
|
self:CreateLabels();
|
||
|
|
self:InitPosition();
|
||
|
|
self:RecordPositioning();
|
||
|
|
|
||
|
|
self:Show(); -- Just sets a well defined state of visibility...
|
||
|
|
self:UpdateGUIVisibility(); -- ... that will be updated here.
|
||
|
|
|
||
|
|
return self;
|
||
|
|
end
|
||
|
|
|
||
|
|
|