diff --git a/ElvUI_CoA.toc b/ElvUI_CoA.toc index b1b4d5d..6aa2a2f 100644 --- a/ElvUI_CoA.toc +++ b/ElvUI_CoA.toc @@ -5,8 +5,12 @@ ## Notes: Hides and skins custom CoA-server frames for ElvUI. ## RequiredDeps: ElvUI ## X-IconTexture: Interface\AddOns\ElvUI\Media\ElvUILogo +## SavedVariables: ElvUI_CoADB Bindings.xml +Libraries\AceDBOptions-3.0.lua core.lua Modules\ExtraActionBar.lua Modules\LayerPicker.lua +Modules\DispelHighlight.lua +Modules\ClassResources.lua diff --git a/Libraries/AceDBOptions-3.0.lua b/Libraries/AceDBOptions-3.0.lua new file mode 100644 index 0000000..3c23951 --- /dev/null +++ b/Libraries/AceDBOptions-3.0.lua @@ -0,0 +1,460 @@ +--- AceDBOptions-3.0 provides a universal AceConfig options screen for managing AceDB-3.0 profiles. +-- @class file +-- @name AceDBOptions-3.0 +-- @release $Id$ +local ACEDBO_MAJOR, ACEDBO_MINOR = "AceDBOptions-3.0", 15 +local AceDBOptions = LibStub:NewLibrary(ACEDBO_MAJOR, ACEDBO_MINOR) + +if not AceDBOptions then return end -- No upgrade needed + +-- Lua APIs +local pairs, next = pairs, next + +-- WoW APIs +local UnitClass = UnitClass + +-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded +-- List them here for Mikk's FindGlobals script +-- GLOBALS: NORMAL_FONT_COLOR_CODE, FONT_COLOR_CODE_CLOSE + +AceDBOptions.optionTables = AceDBOptions.optionTables or {} +AceDBOptions.handlers = AceDBOptions.handlers or {} + +--[[ + Localization of AceDBOptions-3.0 +]] + +local L = { + choose = "Existing Profiles", + choose_desc = "You can either create a new profile by entering a name in the editbox, or choose one of the already existing profiles.", + choose_sub = "Select one of your currently available profiles.", + copy = "Copy From", + copy_desc = "Copy the settings from one existing profile into the currently active profile.", + current = "Current Profile:", + default = "Default", + delete = "Delete a Profile", + delete_confirm = "Are you sure you want to delete the selected profile?", + delete_desc = "Delete existing and unused profiles from the database to save space, and cleanup the SavedVariables file.", + delete_sub = "Deletes a profile from the database.", + intro = "You can change the active database profile, so you can have different settings for every character.", + new = "New", + new_sub = "Create a new empty profile.", + profiles = "Profiles", + profiles_sub = "Manage Profiles", + reset = "Reset Profile", + reset_desc = "Reset the current profile back to its default values, in case your configuration is broken, or you simply want to start over.", + reset_sub = "Reset the current profile to the default", +} + +local LOCALE = GetLocale() +if LOCALE == "deDE" then + L["choose"] = "Vorhandene Profile" + L["choose_desc"] = "Du kannst ein neues Profil erstellen, indem du einen neuen Namen in der Eingabebox 'Neu' eingibst, oder wähle eines der vorhandenen Profile aus." + L["choose_sub"] = "Wählt ein bereits vorhandenes Profil aus." + L["copy"] = "Kopieren von..." + L["copy_desc"] = "Kopiere die Einstellungen von einem vorhandenen Profil in das aktive Profil." + L["current"] = "Aktuelles Profil:" + L["default"] = "Standard" + L["delete"] = "Profil löschen" + L["delete_confirm"] = "Willst du das ausgewählte Profil wirklich löschen?" + L["delete_desc"] = "Lösche vorhandene oder unbenutzte Profile aus der Datenbank, um Platz zu sparen und die SavedVariables-Datei 'sauber' zu halten." + L["delete_sub"] = "Löscht ein Profil aus der Datenbank." + L["intro"] = "Hier kannst du das aktive Datenbankprofil ändern, damit du verschiedene Einstellungen für jeden Charakter erstellen kannst, wodurch eine sehr flexible Konfiguration möglich wird." + L["new"] = "Neu" + L["new_sub"] = "Ein neues Profil erstellen." + L["profiles"] = "Profile" + L["profiles_sub"] = "Profile verwalten" + L["reset"] = "Profil zurücksetzen" + L["reset_desc"] = "Setzt das momentane Profil auf Standardwerte zurück, für den Fall, dass mit der Konfiguration etwas schief lief oder weil du einfach neu starten willst." + L["reset_sub"] = "Das aktuelle Profil auf Standard zurücksetzen." +elseif LOCALE == "frFR" then + L["choose"] = "Profils existants" + L["choose_desc"] = "Vous pouvez créer un nouveau profil en entrant un nouveau nom dans la boîte de saisie, ou en choississant un des profils déjà existants." + L["choose_sub"] = "Permet de choisir un des profils déjà disponibles." + L["copy"] = "Copier à partir de" + L["copy_desc"] = "Copie les paramètres d'un profil déjà existant dans le profil actuellement actif." + L["current"] = "Profil actuel :" + L["default"] = "Défaut" + L["delete"] = "Supprimer un profil" + L["delete_confirm"] = "Etes-vous sûr de vouloir supprimer le profil sélectionné ?" + L["delete_desc"] = "Supprime les profils existants inutilisés de la base de données afin de gagner de la place et de nettoyer le fichier SavedVariables." + L["delete_sub"] = "Supprime un profil de la base de données." + L["intro"] = "Vous pouvez changer le profil actuel afin d'avoir des paramètres différents pour chaque personnage, permettant ainsi d'avoir une configuration très flexible." + L["new"] = "Nouveau" + L["new_sub"] = "Créée un nouveau profil vierge." + L["profiles"] = "Profils" + L["profiles_sub"] = "Gestion des profils" + L["reset"] = "Réinitialiser le profil" + L["reset_desc"] = "Réinitialise le profil actuel au cas où votre configuration est corrompue ou si vous voulez tout simplement faire table rase." + L["reset_sub"] = "Réinitialise le profil actuel avec les paramètres par défaut." +elseif LOCALE == "koKR" then + L["choose"] = "저장 중인 프로필" + L["choose_desc"] = "입력창에 새로운 이름을 입력하거나 저장 중인 프로필 중 하나를 선택하여 새로운 프로필을 만들 수 있습니다." + L["choose_sub"] = "현재 이용할 수 있는 프로필 중 하나를 선택합니다." + L["copy"] = "복사해오기" + L["copy_desc"] = "현재 사용 중인 프로필에 선택한 프로필의 설정을 복사합니다." + L["current"] = "현재 프로필:" + L["default"] = "기본값" + L["delete"] = "프로필 삭제" + L["delete_confirm"] = "정말로 선택한 프로필을 삭제할까요?" + L["delete_desc"] = "저장 공간 절약과 SavedVariables 파일의 정리를 위해 데이터베이스에서 사용하지 않는 프로필을 삭제하세요." + L["delete_sub"] = "데이터베이스의 프로필을 삭제합니다." + L["intro"] = "활성 데이터베이스 프로필을 변경할 수 있고, 각 캐릭터 별로 다른 설정을 할 수 있습니다." + L["new"] = "새로운 프로필" + L["new_sub"] = "새로운 프로필을 만듭니다." + L["profiles"] = "프로필" + L["profiles_sub"] = "프로필 관리" + L["reset"] = "프로필 초기화" + L["reset_desc"] = "설정이 깨졌거나 처음부터 다시 설정을 원하는 경우, 현재 프로필을 기본값으로 초기화하세요." + L["reset_sub"] = "현재 프로필을 기본값으로 초기화합니다" +elseif LOCALE == "esES" or LOCALE == "esMX" then + L["choose"] = "Perfiles existentes" + L["choose_desc"] = "Puedes crear un nuevo perfil introduciendo un nombre en el recuadro o puedes seleccionar un perfil de los ya existentes." + L["choose_sub"] = "Selecciona uno de los perfiles disponibles." + L["copy"] = "Copiar de" + L["copy_desc"] = "Copia los ajustes de un perfil existente al perfil actual." + L["current"] = "Perfil actual:" + L["default"] = "Por defecto" + L["delete"] = "Borrar un Perfil" + L["delete_confirm"] = "¿Estas seguro que quieres borrar el perfil seleccionado?" + L["delete_desc"] = "Borra los perfiles existentes y sin uso de la base de datos para ganar espacio y limpiar el archivo SavedVariables." + L["delete_sub"] = "Borra un perfil de la base de datos." + L["intro"] = "Puedes cambiar el perfil activo de tal manera que cada personaje tenga diferentes configuraciones." + L["new"] = "Nuevo" + L["new_sub"] = "Crear un nuevo perfil vacio." + L["profiles"] = "Perfiles" + L["profiles_sub"] = "Manejar Perfiles" + L["reset"] = "Reiniciar Perfil" + L["reset_desc"] = "Reinicia el perfil actual a los valores por defectos, en caso de que se haya estropeado la configuración o quieras volver a empezar de nuevo." + L["reset_sub"] = "Reinicar el perfil actual al de por defecto" +elseif LOCALE == "zhTW" then + L["choose"] = "現有的設定檔" + L["choose_desc"] = "您可以在文字方塊內輸入名字以建立新的設定檔,或是選擇一個現有的設定檔使用。" + L["choose_sub"] = "從當前可用的設定檔裡面選擇一個。" + L["copy"] = "複製自" + L["copy_desc"] = "從一個現有的設定檔,將設定複製到現在使用中的設定檔。" + L["current"] = "目前設定檔:" + L["default"] = "預設" + L["delete"] = "刪除一個設定檔" + L["delete_confirm"] = "確定要刪除所選擇的設定檔嗎?" + L["delete_desc"] = "從資料庫裡刪除不再使用的設定檔,以節省空間,並且清理 SavedVariables 檔案。" + L["delete_sub"] = "從資料庫裡刪除一個設定檔。" + L["intro"] = "您可以從資料庫中選擇一個設定檔來使用,如此就可以讓每個角色使用不同的設定。" + L["new"] = "新建" + L["new_sub"] = "新建一個空的設定檔。" + L["profiles"] = "設定檔" + L["profiles_sub"] = "管理設定檔" + L["reset"] = "重置設定檔" + L["reset_desc"] = "將現用的設定檔重置為預設值;用於設定檔損壞,或者單純想要重來的情況。" + L["reset_sub"] = "將目前的設定檔重置為預設值" +elseif LOCALE == "zhCN" then + L["choose"] = "现有的配置文件" + L["choose_desc"] = "你可以通过在文本框内输入一个名字创立一个新的配置文件,也可以选择一个已经存在的配置文件。" + L["choose_sub"] = "从当前可用的配置文件里面选择一个。" + L["copy"] = "复制自" + L["copy_desc"] = "从当前某个已保存的配置文件复制到当前正使用的配置文件。" + L["current"] = "当前配置文件:" + L["default"] = "默认" + L["delete"] = "删除一个配置文件" + L["delete_confirm"] = "你确定要删除所选择的配置文件么?" + L["delete_desc"] = "从数据库里删除不再使用的配置文件,以节省空间,并且清理SavedVariables文件。" + L["delete_sub"] = "从数据库里删除一个配置文件。" + L["intro"] = "你可以选择一个活动的数据配置文件,这样你的每个角色就可以拥有不同的设置值,可以给你的插件配置带来极大的灵活性。" + L["new"] = "新建" + L["new_sub"] = "新建一个空的配置文件。" + L["profiles"] = "配置文件" + L["profiles_sub"] = "管理配置文件" + L["reset"] = "重置配置文件" + L["reset_desc"] = "将当前的配置文件恢复到它的默认值,用于你的配置文件损坏,或者你只是想重来的情况。" + L["reset_sub"] = "将当前的配置文件恢复为默认值" +elseif LOCALE == "ruRU" then + L["choose"] = "Существующие профили" + L["choose_desc"] = "Вы можете создать новый профиль, введя название в поле ввода, или выбрать один из уже существующих профилей." + L["choose_sub"] = "Выбор одиного из уже доступных профилей" + L["copy"] = "Скопировать из" + L["copy_desc"] = "Скопировать настройки из выбранного профиля в активный." + L["current"] = "Текущий профиль:" + L["default"] = "По умолчанию" + L["delete"] = "Удалить профиль" + L["delete_confirm"] = "Вы уверены, что вы хотите удалить выбранный профиль?" + L["delete_desc"] = "Удалить существующий и неиспользуемый профиль из БД для сохранения места, и очистить SavedVariables файл." + L["delete_sub"] = "Удаление профиля из БД" + L["intro"] = "Изменяя активный профиль, вы можете задать различные настройки модификаций для каждого персонажа." + L["new"] = "Новый" + L["new_sub"] = "Создать новый чистый профиль" + L["profiles"] = "Профили" + L["profiles_sub"] = "Управление профилями" + L["reset"] = "Сброс профиля" + L["reset_desc"] = "Сбросить текущий профиль к стандартным настройкам, если ваша конфигурация испорчена или вы хотите настроить всё заново." + L["reset_sub"] = "Сброс текущего профиля на стандартный" +elseif LOCALE == "itIT" then + L["choose"] = "Profili Esistenti" + L["choose_desc"] = "Puoi creare un nuovo profilo digitando il nome della casella di testo, oppure scegliendone uno tra i profili già esistenti." + L["choose_sub"] = "Seleziona uno dei profili attualmente disponibili." + L["copy"] = "Copia Da" + L["copy_desc"] = "Copia le impostazioni da un profilo esistente, nel profilo attivo in questo momento." + L["current"] = "Profilo Attivo:" + L["default"] = "Standard" + L["delete"] = "Cancella un Profilo" + L["delete_confirm"] = "Sei sicuro di voler cancellare il profilo selezionato?" + L["delete_desc"] = "Cancella i profili non utilizzati dal database per risparmiare spazio e mantenere puliti i file di configurazione SavedVariables." + L["delete_sub"] = "Cancella un profilo dal Database." + L["intro"] = "Puoi cambiare il profilo attivo, in modo da usare impostazioni diverse per ogni personaggio." + L["new"] = "Nuovo" + L["new_sub"] = "Crea un nuovo profilo vuoto." + L["profiles"] = "Profili" + L["profiles_sub"] = "Gestisci Profili" + L["reset"] = "Reimposta Profilo" + L["reset_desc"] = "Riporta il tuo profilo attivo alle sue impostazioni predefinite, nel caso in cui la tua configurazione si sia corrotta, o semplicemente tu voglia re-inizializzarla." + L["reset_sub"] = "Reimposta il profilo ai suoi valori predefiniti." +elseif LOCALE == "ptBR" then + L["choose"] = "Perfis Existentes" + L["choose_desc"] = "Você pode tanto criar um perfil novo tanto digitando um nome na caixa de texto, quanto escolher um dos perfis já existentes." + L["choose_sub"] = "Selecione um de seus perfis atualmente disponíveis." + L["copy"] = "Copiar De" + L["copy_desc"] = "Copia as definições de um perfil existente no perfil atualmente ativo." + L["current"] = "Perfil Autal:" + L["default"] = "Padrão" + L["delete"] = "Remover um Perfil" + L["delete_confirm"] = "Tem certeza que deseja remover o perfil selecionado?" + L["delete_desc"] = "Remove perfis existentes e inutilizados do banco de dados para economizar espaço, e limpar o arquivo SavedVariables." + L["delete_sub"] = "Remove um perfil do banco de dados." + L["intro"] = "Você pode alterar o perfil do banco de dados ativo, para que possa ter definições diferentes para cada personagem." + L["new"] = "Novo" + L["new_sub"] = "Cria um novo perfil vazio." + L["profiles"] = "Perfis" + L["profiles_sub"] = "Gerenciar Perfis" + L["reset"] = "Resetar Perfil" + L["reset_desc"] = "Reseta o perfil atual para os valores padrões, no caso de sua configuração estar quebrada, ou simplesmente se deseja começar novamente." + L["reset_sub"] = "Resetar o perfil atual ao padrão" +end + +local defaultProfiles +local tmpprofiles = {} + +-- Get a list of available profiles for the specified database. +-- You can specify which profiles to include/exclude in the list using the two boolean parameters listed below. +-- @param db The db object to retrieve the profiles from +-- @param common If true, getProfileList will add the default profiles to the return list, even if they have not been created yet +-- @param nocurrent If true, then getProfileList will not display the current profile in the list +-- @return Hashtable of all profiles with the internal name as keys and the display name as value. +local function getProfileList(db, common, nocurrent) + local profiles = {} + + -- copy existing profiles into the table + local currentProfile = db:GetCurrentProfile() + for i,v in pairs(db:GetProfiles(tmpprofiles)) do + if not (nocurrent and v == currentProfile) then + profiles[v] = v + end + end + + -- add our default profiles to choose from ( or rename existing profiles) + for k,v in pairs(defaultProfiles) do + if (common or profiles[k]) and not (nocurrent and k == currentProfile) then + profiles[k] = v + end + end + + return profiles +end + +--[[ + OptionsHandlerPrototype + prototype class for handling the options in a sane way +]] +local OptionsHandlerPrototype = {} + +--[[ Reset the profile ]] +function OptionsHandlerPrototype:Reset() + self.db:ResetProfile() +end + +--[[ Set the profile to value ]] +function OptionsHandlerPrototype:SetProfile(info, value) + self.db:SetProfile(value) +end + +--[[ returns the currently active profile ]] +function OptionsHandlerPrototype:GetCurrentProfile() + return self.db:GetCurrentProfile() +end + +--[[ + List all active profiles + you can control the output with the .arg variable + currently four modes are supported + + (empty) - return all available profiles + "nocurrent" - returns all available profiles except the currently active profile + "common" - returns all avaialble profiles + some commonly used profiles ("char - realm", "realm", "class", "Default") + "both" - common except the active profile +]] +function OptionsHandlerPrototype:ListProfiles(info) + local arg = info.arg + local profiles + if arg == "common" and not self.noDefaultProfiles then + profiles = getProfileList(self.db, true, nil) + elseif arg == "nocurrent" then + profiles = getProfileList(self.db, nil, true) + elseif arg == "both" then -- currently not used + profiles = getProfileList(self.db, (not self.noDefaultProfiles) and true, true) + else + profiles = getProfileList(self.db) + end + + return profiles +end + +function OptionsHandlerPrototype:HasNoProfiles(info) + local profiles = self:ListProfiles(info) + return ((not next(profiles)) and true or false) +end + +--[[ Copy a profile ]] +function OptionsHandlerPrototype:CopyProfile(info, value) + self.db:CopyProfile(value) +end + +--[[ Delete a profile from the db ]] +function OptionsHandlerPrototype:DeleteProfile(info, value) + self.db:DeleteProfile(value) +end + +--[[ fill defaultProfiles with some generic values ]] +local function generateDefaultProfiles(db) + defaultProfiles = { + ["Default"] = L["default"], + [db.keys.char] = db.keys.char, + [db.keys.realm] = db.keys.realm, + [db.keys.class] = UnitClass("player") + } +end + +--[[ create and return a handler object for the db, or upgrade it if it already existed ]] +local function getOptionsHandler(db, noDefaultProfiles) + if not defaultProfiles then + generateDefaultProfiles(db) + end + + local handler = AceDBOptions.handlers[db] or { db = db, noDefaultProfiles = noDefaultProfiles } + + for k,v in pairs(OptionsHandlerPrototype) do + handler[k] = v + end + + AceDBOptions.handlers[db] = handler + return handler +end + +--[[ + the real options table +]] +local optionsTable = { + desc = { + order = 1, + type = "description", + name = L["intro"] .. "\n", + }, + descreset = { + order = 9, + type = "description", + name = L["reset_desc"], + }, + reset = { + order = 10, + type = "execute", + name = L["reset"], + desc = L["reset_sub"], + func = "Reset", + }, + current = { + order = 11, + type = "description", + name = function(info) return L["current"] .. " " .. NORMAL_FONT_COLOR_CODE .. info.handler:GetCurrentProfile() .. FONT_COLOR_CODE_CLOSE end, + width = "default", + }, + choosedesc = { + order = 20, + type = "description", + name = "\n" .. L["choose_desc"], + }, + new = { + name = L["new"], + desc = L["new_sub"], + type = "input", + order = 30, + get = false, + set = "SetProfile", + }, + choose = { + name = L["choose"], + desc = L["choose_sub"], + type = "select", + order = 40, + get = "GetCurrentProfile", + set = "SetProfile", + values = "ListProfiles", + arg = "common", + }, + copydesc = { + order = 50, + type = "description", + name = "\n" .. L["copy_desc"], + }, + copyfrom = { + order = 60, + type = "select", + name = L["copy"], + desc = L["copy_desc"], + get = false, + set = "CopyProfile", + values = "ListProfiles", + disabled = "HasNoProfiles", + arg = "nocurrent", + }, + deldesc = { + order = 70, + type = "description", + name = "\n" .. L["delete_desc"], + }, + delete = { + order = 80, + type = "select", + name = L["delete"], + desc = L["delete_sub"], + get = false, + set = "DeleteProfile", + values = "ListProfiles", + disabled = "HasNoProfiles", + arg = "nocurrent", + confirm = true, + confirmText = L["delete_confirm"], + }, +} + +--- Get/Create a option table that you can use in your addon to control the profiles of AceDB-3.0. +-- @param db The database object to create the options table for. +-- @return The options table to be used in AceConfig-3.0 +-- @usage +-- -- Assuming `options` is your top-level options table and `self.db` is your database: +-- options.args.profiles = LibStub("AceDBOptions-3.0"):GetOptionsTable(self.db) +function AceDBOptions:GetOptionsTable(db, noDefaultProfiles) + local tbl = AceDBOptions.optionTables[db] or { + type = "group", + name = L["profiles"], + desc = L["profiles_sub"], + } + + tbl.handler = getOptionsHandler(db, noDefaultProfiles) + tbl.args = optionsTable + + AceDBOptions.optionTables[db] = tbl + return tbl +end + +-- upgrade existing tables +for db,tbl in pairs(AceDBOptions.optionTables) do + tbl.handler = getOptionsHandler(db) + tbl.args = optionsTable +end diff --git a/Modules/ClassResources.lua b/Modules/ClassResources.lua new file mode 100644 index 0000000..c7925bd --- /dev/null +++ b/Modules/ClassResources.lua @@ -0,0 +1,195 @@ +local E, L, V, P, G = unpack(ElvUI) +local CoA = E:GetModule("CoA") + +local FRAMES = { + {name = "CoAResourceSegmentBar", moverText = "Resource Segment Bar", hideKey = "hideResourceSegmentBar"}, + {name = "CoAResourceOrb", moverText = "Resource Orb", hideKey = "hideResourceOrb"}, + {name = "CoAResourceBar", moverText = "Resource Bar", hideKey = "hideResourceBar"}, + {name = "CoAMultiCastActionBarFrame", moverText = "Multi Cast Action Bar", hideKey = "hideMultiCastActionBar"}, +} + +-- A single nil-out isn't enough: the frame's own update logic re-attaches +-- OnDragStart/OnDragStop (and re-registers drag buttons) on refresh, and +-- native dragging ends with the engine calling SetPoint directly on the +-- frame, severing the live anchor to our mover holder. So we don't just +-- clear drag once, we permanently intercept any future attempt to turn it +-- back on. The CoAClearing* guards stop our own corrective calls from +-- re-triggering these same hooks. +local function DisableDrag(frame) + if frame.CoADragDisabled then return end + frame.CoADragDisabled = true + + frame:SetScript("OnDragStart", nil) + frame:SetScript("OnDragStop", nil) + frame:RegisterForDrag() + + hooksecurefunc(frame, "SetScript", function(self, script, handler) + if handler and (script == "OnDragStart" or script == "OnDragStop") and not self.CoAClearingDragScript then + self.CoAClearingDragScript = true + self:SetScript(script, nil) + self.CoAClearingDragScript = false + end + end) + + hooksecurefunc(frame, "RegisterForDrag", function(self, ...) + if select("#", ...) > 0 and not self.CoAClearingDrag then + self.CoAClearingDrag = true + self:RegisterForDrag() + self.CoAClearingDrag = false + end + end) +end + +-- Anchoring a frame while in combat lockdown can taint it, so anchors +-- queued during combat are batched and applied together on the next +-- PLAYER_REGEN_ENABLED instead of each frame registering its own handler +-- (which would stomp on each other, since AceEvent keeps only the most +-- recently registered callback per event for a given object). +local pendingAnchors = {} +local combatHandlerRegistered = false + +local function QueueAnchor(fn) + if not InCombatLockdown() then + fn() + return + end + + table.insert(pendingAnchors, fn) + + if not combatHandlerRegistered then + combatHandlerRegistered = true + + CoA:RegisterEvent("PLAYER_REGEN_ENABLED", function() + for i = 1, #pendingAnchors do + pendingAnchors[i]() + end + + wipe(pendingAnchors) + CoA:UnregisterEvent("PLAYER_REGEN_ENABLED") + combatHandlerRegistered = false + end) + end +end + +local function AnchorToHolder(frame, holder) + frame:ClearAllPoints() + frame:SetPoint("CENTER", holder, "CENTER") +end + +-- Class resource frames re-anchor themselves (e.g. relative to the player/ +-- target frame) whenever they refresh, which stomps our mover anchor. Since +-- there's no event that fires only for that self-repositioning, we hook +-- SetPoint itself and snap back to the holder any time something else moves +-- the frame. CoARepositioning guards against the corrective SetPoint call +-- re-triggering this same hook. +local function LockPosition(frame, holder) + if frame.CoAPositionLocked then return end + frame.CoAPositionLocked = true + + hooksecurefunc(frame, "SetPoint", function(self) + if self.CoARepositioning then return end + self.CoARepositioning = true + + QueueAnchor(function() + AnchorToHolder(self, holder) + self.CoARepositioning = false + end) + end) +end + +local function SetupMover(frame, name, moverText) + if frame.CoAMoverCreated then return true end + + local width, height = frame:GetSize() + if width == 0 or height == 0 then return false end + + local left, bottom = frame:GetLeft(), frame:GetBottom() + if not left or not bottom then return false end + + frame.CoAMoverCreated = true + + local holder = CreateFrame("Frame", "CoA_"..name.."Holder", E.UIParent) + holder:Size(width, height) + holder:Point("BOTTOMLEFT", E.UIParent, "BOTTOMLEFT", left, bottom) + + E:CreateMover(holder, "CoA_"..name.."Mover", moverText, nil, nil, nil, nil, nil, "CoA,skin,classResources") + holder:SetAllPoints(_G["CoA_"..name.."Mover"]) + + QueueAnchor(function() + AnchorToHolder(frame, holder) + LockPosition(frame, holder) + end) + + return true +end + +-- CoAForceHidden distinguishes frames we hid ourselves (via the options +-- checkbox) from frames the game itself decided to hide, so unchecking the +-- box only restores frames we were suppressing. +local function ApplyVisibility(frame, hideKey) + if CoA.db.profile[hideKey] then + frame.CoAForceHidden = true + frame:Hide() + elseif frame.CoAForceHidden then + frame.CoAForceHidden = false + frame:Show() + end +end + +local function SetupVisibility(frame, hideKey) + if frame.CoAVisibilityHooked then return end + frame.CoAVisibilityHooked = true + + frame:HookScript("OnShow", function(self) + ApplyVisibility(self, hideKey) + end) + + ApplyVisibility(frame, hideKey) +end + +function CoA:UpdateClassResourceVisibility() + for _, def in ipairs(FRAMES) do + local frame = _G[def.name] + if frame then + ApplyVisibility(frame, def.hideKey) + end + end +end + +local hooked = {} + +local function TryHookAll() + local allHooked = true + + for _, def in ipairs(FRAMES) do + if not hooked[def.name] then + local frame = _G[def.name] + + if frame then + DisableDrag(frame) + SetupVisibility(frame, def.hideKey) + + if SetupMover(frame, def.name, def.moverText) then + hooked[def.name] = true + else + allHooked = false + end + else + allHooked = false + end + end + end + + return allHooked +end + +function CoA:InitializeClassResources() + if TryHookAll() then return end + + self.classResourcesTimer = self:ScheduleRepeatingTimer(function() + if TryHookAll() then + self:CancelTimer(self.classResourcesTimer) + self.classResourcesTimer = nil + end + end, 0.5) +end diff --git a/Modules/DispelHighlight.lua b/Modules/DispelHighlight.lua new file mode 100644 index 0000000..b7e1aca --- /dev/null +++ b/Modules/DispelHighlight.lua @@ -0,0 +1,86 @@ +local E, L, V, P, G = unpack(ElvUI) +local UF = E:GetModule("UnitFrames") +local CoA = E:GetModule("CoA") + +-- Base dispel types each custom class can remove, independent of talents. +-- CHRONOMANCER is a special case: its dispel removes the last debuff applied +-- to the target regardless of type, so it's never filtered out here -- we +-- can only approximate this as "treat every typed debuff as dispellable", +-- since the underlying oUF scan only ever surfaces typed debuffs anyway. +local CLASS_DISPEL_TYPES = { + CHRONOMANCER = true, + SUNCLERIC = {Magic = true, Poison = true, Disease = true}, + STARCALLER = {Poison = true, Disease = true}, + PROPHET = {Poison = true}, + WITCHDOCTOR = {Curse = true}, + CULTIST = {Magic = true}, + PYROMANCER = {}, +} + +-- Extra dispel types unlocked by a talent choice. There's no reliable way to +-- auto-detect the talent on this server, so these are gated by a manual +-- checkbox in the options panel instead. +local TALENT_DISPEL_TYPES = { + PROPHET = {flag = "hasBlightAntidote", types = {Curse = true}}, + CULTIST = {flag = "hasDevourCurse", types = {Curse = true}}, + PYROMANCER = {flag = "hasBurnImpurities", types = {Magic = true, Disease = true, Bleed = true}}, +} + +function CoA:CanDispel(debuffType) + local _, class = UnitClass("player") + local baseTypes = CLASS_DISPEL_TYPES[class] + + if baseTypes == true then return true end + if baseTypes and baseTypes[debuffType] then return true end + + local talent = TALENT_DISPEL_TYPES[class] + if talent and CoA.db.profile[talent.flag] and talent.types[debuffType] then return true end + + return false +end + +local function SuppressHighlight(object) + if object.DebuffHighlightBackdrop and object.DBHGlow then + object.DBHGlow:Hide() + elseif object.DebuffHighlightUseTexture then + object.DebuffHighlight:SetTexture(nil) + else + object.DebuffHighlight:SetVertexColor(0, 0, 0, 0) + end +end + +local origPostUpdate = UF.PostUpdate_DebuffHighlight + +local function DispelAwarePostUpdate(dbh, object, debuffType, texture, wasFiltered, style, color) + origPostUpdate(dbh, object, debuffType, texture, wasFiltered, style, color) + + if CoA.db.profile.dispelHighlightOnlyMine and debuffType and not wasFiltered and not CoA:CanDispel(debuffType) then + SuppressHighlight(object) + end +end + +UF.PostUpdate_DebuffHighlight = DispelAwarePostUpdate + +hooksecurefunc(UF, "Configure_DebuffHighlight", function(_, frame) + local dbh = frame.DebuffHighlight + if dbh then + dbh.PostUpdate = UF.PostUpdate_DebuffHighlight + end +end) + +function CoA:UpdateDispelHighlight() + UF:Update_AllFrames() +end + +-- On Ascension, UnitClass("player")'s second return only reliably reports the +-- real custom class (CULTIST, PYROMANCER, ...) a short while after login -- +-- immediately at ADDON_LOADED/PLAYER_LOGIN it can still read back the generic +-- "HERO" base class. If a debuff highlight gets evaluated before that data +-- syncs, CoA:CanDispel wrongly returns false and the highlight stays wrongly +-- suppressed until the next aura change. Force one extra refresh shortly +-- after entering the world so the very first debuff isn't judged too early. +function CoA:InitializeDispelHighlight() + CoA:RegisterEvent("PLAYER_ENTERING_WORLD", function() + CoA:ScheduleTimer("UpdateDispelHighlight", 2) + end) +end diff --git a/Modules/ExtraActionBar.lua b/Modules/ExtraActionBar.lua index 34e5db9..a16174c 100644 --- a/Modules/ExtraActionBar.lua +++ b/Modules/ExtraActionBar.lua @@ -44,7 +44,7 @@ end local function UpdateSize(button) button = button or _G[BUTTON_NAME] if button then - local size = E.global.CoA.extraActionButtonSize or 52 + local size = CoA.db.profile.extraActionButtonSize or 52 button:SetSize(size, size) UpdateRimEdge(button, size) UpdateHotkeyPosition(button, size) @@ -124,7 +124,7 @@ local function SkinButton(button, container) rim:SetAllPoints(icon) rim:SetFrameLevel(button.backdrop:GetFrameLevel() + 10) button.CoARim = rim - UpdateRimEdge(button, E.global.CoA.extraActionButtonSize or 52) + UpdateRimEdge(button, CoA.db.profile.extraActionButtonSize or 52) button:SetFrameLevel(rim:GetFrameLevel() + 1) end @@ -158,13 +158,13 @@ local function SkinButton(button, container) end local function SetupMover(container, button) - if CoA.extraActionBarMoverCreated then return end + if CoA.extraActionBarMoverCreated then return true end local width, height = button:GetSize() - if width == 0 or height == 0 then return end + if width == 0 or height == 0 then return false end local left, bottom = button:GetLeft(), button:GetBottom() - if not left or not bottom then return end + if not left or not bottom then return false end CoA.extraActionBarMoverCreated = true @@ -191,6 +191,8 @@ local function SetupMover(container, button) else Anchor() end + + return true end local function TryHook() @@ -198,11 +200,11 @@ local function TryHook() local button = _G[BUTTON_NAME] if container and button then - SetupMover(container, button) SkinButton(button, container) + return SetupMover(container, button) end - return container ~= nil and button ~= nil + return false end function CoA:InitializeExtraActionBar() diff --git a/Modules/LayerPicker.lua b/Modules/LayerPicker.lua index 2e937ef..e6b2bf3 100644 --- a/Modules/LayerPicker.lua +++ b/Modules/LayerPicker.lua @@ -11,7 +11,7 @@ local function UpdateFont(button) local text = button and _G[button:GetName().."Text"] if not text then return end - text:FontTemplate(LSM:Fetch("font", E.global.CoA.instanceButtonFont), E.global.CoA.instanceButtonFontSize, E.global.CoA.instanceButtonFontOutline) + text:FontTemplate(LSM:Fetch("font", CoA.db.profile.instanceButtonFont), CoA.db.profile.instanceButtonFontSize, CoA.db.profile.instanceButtonFontOutline) button:SetSize( math.max(MIN_WIDTH, text:GetStringWidth() + PAD_X), @@ -34,22 +34,44 @@ do end end +-- A single nil-out isn't enough: native dragging ends with the engine +-- calling SetPoint directly on the frame, severing the live anchor to our +-- mover holder. So we don't just clear drag once, we permanently intercept +-- any future attempt to turn it back on. The CoAClearing* guards stop our +-- own corrective calls from re-triggering these same hooks. local function DisableDrag(button) if button.CoADragDisabled then return end button.CoADragDisabled = true button:SetScript("OnDragStart", nil) button:SetScript("OnDragStop", nil) + button:RegisterForDrag() + + hooksecurefunc(button, "SetScript", function(self, script, handler) + if handler and (script == "OnDragStart" or script == "OnDragStop") and not self.CoAClearingDragScript then + self.CoAClearingDragScript = true + self:SetScript(script, nil) + self.CoAClearingDragScript = false + end + end) + + hooksecurefunc(button, "RegisterForDrag", function(self, ...) + if select("#", ...) > 0 and not self.CoAClearingDrag then + self.CoAClearingDrag = true + self:RegisterForDrag() + self.CoAClearingDrag = false + end + end) end local function SetupMover(button) - if CoA.layerPickerMoverCreated then return end + if CoA.layerPickerMoverCreated then return true end local width, height = button:GetSize() - if width == 0 or height == 0 then return end + if width == 0 or height == 0 then return false end local left, bottom = button:GetLeft(), button:GetBottom() - if not left or not bottom then return end + if not left or not bottom then return false end CoA.layerPickerMoverCreated = true @@ -74,6 +96,8 @@ local function SetupMover(button) else Anchor() end + + return true end local function SkinButton(button) @@ -92,11 +116,11 @@ local function TryHook() if button then DisableDrag(button) - SetupMover(button) SkinButton(button) + return SetupMover(button) end - return button ~= nil + return false end function CoA:InitializeLayerPicker() diff --git a/core.lua b/core.lua index 64c42df..962d308 100644 --- a/core.lua +++ b/core.lua @@ -1,6 +1,8 @@ local E, L, V, P, G = unpack(ElvUI) local EP = E.Libs.EP local ACH = LibStub("LibAceConfigHelper") +local AceDB = LibStub("AceDB-3.0") +local AceDBOptions = LibStub("AceDBOptions-3.0") local AddOnName = ... @@ -9,25 +11,118 @@ BINDING_HEADER_COA = "Conquest of Azeroth" local CoA = E:NewModule("CoA", "AceEvent-3.0", "AceTimer-3.0") E.CoA = CoA -V.CoA = {} - -G.CoA = { - extraActionButtonSize = 52, - instanceButtonFont = "PT Sans Narrow", - instanceButtonFontSize = 12, - instanceButtonFontOutline = "OUTLINE", +local defaults = { + profile = { + extraActionButtonSize = 52, + instanceButtonFont = "PT Sans Narrow", + instanceButtonFontSize = 12, + instanceButtonFontOutline = "OUTLINE", + dispelHighlightOnlyMine = false, + hasBlightAntidote = false, + hasDevourCurse = false, + hasBurnImpurities = false, + hideResourceSegmentBar = false, + hideResourceOrb = false, + hideResourceBar = false, + hideMultiCastActionBar = false, + }, } +CoA.db = AceDB:New("ElvUI_CoADB", defaults, true) + +function CoA:RefreshConfig() + if self.UpdateExtraActionButtonSize then self:UpdateExtraActionButtonSize() end + if self.UpdateInstanceButtonFont then self:UpdateInstanceButtonFont() end + if self.UpdateDispelHighlight then self:UpdateDispelHighlight() end + if self.UpdateClassResourceVisibility then self:UpdateClassResourceVisibility() end +end + +CoA.db.RegisterCallback(CoA, "OnProfileChanged", "RefreshConfig") +CoA.db.RegisterCallback(CoA, "OnProfileCopied", "RefreshConfig") +CoA.db.RegisterCallback(CoA, "OnProfileReset", "RefreshConfig") + local function getOptions() + local profiles = AceDBOptions:GetOptionsTable(CoA.db) + profiles.order = 5 + local options = { order = 55, type = "group", childGroups = "tab", name = string.format("|cff1784d1%s|r", "Conquest of Azeroth"), args = { - extraActionButton = { + classResources = { order = 1, type = "group", + name = "Class Resources", + args = { + header = { + order = 1, + type = "header", + name = "Class Resources", + }, + desc = { + order = 2, + type = "description", + name = "You can move these elements with Toggle Anchors.\n", + }, + hideResourceSegmentBar = { + order = 3, + type = "toggle", + name = "Hide Resource Segment Bar", + get = function() return CoA.db.profile.hideResourceSegmentBar end, + set = function(_, value) + CoA.db.profile.hideResourceSegmentBar = value + + if CoA.UpdateClassResourceVisibility then + CoA:UpdateClassResourceVisibility() + end + end, + }, + hideResourceOrb = { + order = 4, + type = "toggle", + name = "Hide Resource Orb", + get = function() return CoA.db.profile.hideResourceOrb end, + set = function(_, value) + CoA.db.profile.hideResourceOrb = value + + if CoA.UpdateClassResourceVisibility then + CoA:UpdateClassResourceVisibility() + end + end, + }, + hideResourceBar = { + order = 5, + type = "toggle", + name = "Hide Resource Bar", + get = function() return CoA.db.profile.hideResourceBar end, + set = function(_, value) + CoA.db.profile.hideResourceBar = value + + if CoA.UpdateClassResourceVisibility then + CoA:UpdateClassResourceVisibility() + end + end, + }, + hideMultiCastActionBar = { + order = 6, + type = "toggle", + name = "Hide Multi Cast Action Bar", + get = function() return CoA.db.profile.hideMultiCastActionBar end, + set = function(_, value) + CoA.db.profile.hideMultiCastActionBar = value + + if CoA.UpdateClassResourceVisibility then + CoA:UpdateClassResourceVisibility() + end + end, + }, + }, + }, + extraActionButton = { + order = 2, + type = "group", name = "Extra Action Button", args = { header = { @@ -35,17 +130,22 @@ local function getOptions() type = "header", name = "Extra Action Button", }, - size = { + desc = { order = 2, + type = "description", + name = "You can move this element with Toggle Anchors.\n", + }, + size = { + order = 3, type = "range", name = "Size", desc = "Adjust the width/height of the Extra Action Button, in pixels.", min = 30, max = 100, step = 1, - get = function() return E.global.CoA.extraActionButtonSize end, + get = function() return CoA.db.profile.extraActionButtonSize end, set = function(_, value) - E.global.CoA.extraActionButtonSize = value + CoA.db.profile.extraActionButtonSize = value if CoA.UpdateExtraActionButtonSize then CoA:UpdateExtraActionButtonSize() @@ -55,7 +155,7 @@ local function getOptions() }, }, instanceSwap = { - order = 2, + order = 3, type = "group", name = "Instance Swap", args = { @@ -64,42 +164,132 @@ local function getOptions() type = "header", name = "Instance Swap", }, - font = ACH:SharedMediaFont("Font", nil, 2, nil, - function() return E.global.CoA.instanceButtonFont end, - function(_, value) - E.global.CoA.instanceButtonFont = value - - if CoA.UpdateInstanceButtonFont then - CoA:UpdateInstanceButtonFont() - end - end), - fontSize = { + desc = { + order = 2, + type = "description", + name = "You can move this element with Toggle Anchors.\n", + }, + instanceFont = { order = 3, - type = "range", - name = "Font Size", - min = 8, - max = 32, - step = 1, - get = function() return E.global.CoA.instanceButtonFontSize end, - set = function(_, value) - E.global.CoA.instanceButtonFontSize = value + type = "group", + inline = true, + name = "Instance Font", + args = { + font = ACH:SharedMediaFont("Font", nil, 1, nil, + function() return CoA.db.profile.instanceButtonFont end, + function(_, value) + CoA.db.profile.instanceButtonFont = value - if CoA.UpdateInstanceButtonFont then - CoA:UpdateInstanceButtonFont() + if CoA.UpdateInstanceButtonFont then + CoA:UpdateInstanceButtonFont() + end + end), + fontSize = { + order = 2, + type = "range", + name = "Font Size", + min = 8, + max = 32, + step = 1, + get = function() return CoA.db.profile.instanceButtonFontSize end, + set = function(_, value) + CoA.db.profile.instanceButtonFontSize = value + + if CoA.UpdateInstanceButtonFont then + CoA:UpdateInstanceButtonFont() + end + end, + }, + fontOutline = ACH:FontFlags("Font Outline", nil, 3, nil, + function() return CoA.db.profile.instanceButtonFontOutline end, + function(_, value) + CoA.db.profile.instanceButtonFontOutline = value + + if CoA.UpdateInstanceButtonFont then + CoA:UpdateInstanceButtonFont() + end + end), + }, + }, + }, + }, + dispelHighlight = { + order = 4, + type = "group", + name = "Debuff Highlighting", + args = { + header = { + order = 1, + type = "header", + name = "Debuff Highlighting", + }, + onlyMine = { + order = 2, + type = "toggle", + name = "Only Highlight If Dispellable By Me", + desc = "Suppress the debuff highlight on unitframes for debuff types your class cannot dispel.", + get = function() return CoA.db.profile.dispelHighlightOnlyMine end, + set = function(_, value) + CoA.db.profile.dispelHighlightOnlyMine = value + + if CoA.UpdateDispelHighlight then + CoA:UpdateDispelHighlight() end end, }, - fontOutline = ACH:FontFlags("Font Outline", nil, 4, nil, - function() return E.global.CoA.instanceButtonFontOutline end, - function(_, value) - E.global.CoA.instanceButtonFontOutline = value + talents = { + order = 3, + type = "group", + inline = true, + name = "Talents", + args = { + hasBlightAntidote = { + order = 1, + type = "toggle", + name = string.format("Blight Antidote (%s)", LOCALIZED_CLASS_NAMES_MALE.PROPHET), + desc = "Grants Curse dispel.", + get = function() return CoA.db.profile.hasBlightAntidote end, + set = function(_, value) + CoA.db.profile.hasBlightAntidote = value - if CoA.UpdateInstanceButtonFont then - CoA:UpdateInstanceButtonFont() - end - end), + if CoA.UpdateDispelHighlight then + CoA:UpdateDispelHighlight() + end + end, + }, + hasDevourCurse = { + order = 2, + type = "toggle", + name = string.format("Devour Curse (%s)", LOCALIZED_CLASS_NAMES_MALE.CULTIST), + desc = "Grants Curse dispel.", + get = function() return CoA.db.profile.hasDevourCurse end, + set = function(_, value) + CoA.db.profile.hasDevourCurse = value + + if CoA.UpdateDispelHighlight then + CoA:UpdateDispelHighlight() + end + end, + }, + hasBurnImpurities = { + order = 3, + type = "toggle", + name = string.format("Burn Impurities (%s)", LOCALIZED_CLASS_NAMES_MALE.PYROMANCER), + desc = "Grants Magic, Disease, and Bleed dispel.", + get = function() return CoA.db.profile.hasBurnImpurities end, + set = function(_, value) + CoA.db.profile.hasBurnImpurities = value + + if CoA.UpdateDispelHighlight then + CoA:UpdateDispelHighlight() + end + end, + }, + }, + }, }, }, + profiles = profiles, }, } @@ -116,6 +306,14 @@ function CoA:Initialize() if self.InitializeLayerPicker then self:InitializeLayerPicker() end + + if self.InitializeDispelHighlight then + self:InitializeDispelHighlight() + end + + if self.InitializeClassResources then + self:InitializeClassResources() + end end local function InitializeCallback()