Compare commits
36 Commits
db6c7d79b0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e66a7a6354 | |||
| 63e179e96e | |||
| 013fe665ab | |||
| d9476af220 | |||
| cafc3ce28f | |||
| a8541fa1ee | |||
| 1a62b4ed1f | |||
| 13f36e7817 | |||
| 919feed322 | |||
| d2e55eb3cb | |||
| cddfc7e7af | |||
| 58fae4418a | |||
| cc6cd4028b | |||
| 6bf3864891 | |||
| d2005c4725 | |||
| 76413b6828 | |||
| 01f1f5c7c3 | |||
| 964f88559d | |||
| 766e561839 | |||
| 083cbcbf9c | |||
| dc263a2419 | |||
| 7862871e3f | |||
| 62a6a2cc57 | |||
| b14753491c | |||
| 2736cc0065 | |||
| 0b9b0c5afa | |||
| 948ac29aaf | |||
| 015891449e | |||
| 284f26b8b4 | |||
| fd2ee1903c | |||
| bf9972e624 | |||
| 50824cc0e1 | |||
| 97c4defa51 | |||
| 3a5344e31f | |||
| d769cebbbe | |||
| d5b67920a6 |
@@ -0,0 +1,3 @@
|
|||||||
|
<Bindings>
|
||||||
|
<Binding name="CLICK ExtraActionBarPoolFrameExtraActionButtonTemplate1:LeftButton" header="BINDING_HEADER_COA"/>
|
||||||
|
</Bindings>
|
||||||
@@ -5,7 +5,18 @@
|
|||||||
## Notes: Hides and skins custom CoA-server frames for ElvUI.
|
## Notes: Hides and skins custom CoA-server frames for ElvUI.
|
||||||
## RequiredDeps: ElvUI
|
## RequiredDeps: ElvUI
|
||||||
## X-IconTexture: Interface\AddOns\ElvUI\Media\ElvUILogo
|
## X-IconTexture: Interface\AddOns\ElvUI\Media\ElvUILogo
|
||||||
|
## SavedVariables: ElvUI_CoADB
|
||||||
|
|
||||||
|
Bindings.xml
|
||||||
|
Libraries\AceDBOptions-3.0.lua
|
||||||
core.lua
|
core.lua
|
||||||
Modules\ExtraActionBar.lua
|
Modules\ExtraActionBar.lua
|
||||||
Modules\LayerPicker.lua
|
Modules\LayerPicker.lua
|
||||||
|
Modules\DispelHighlight.lua
|
||||||
|
Modules\ClassResources.lua
|
||||||
|
Modules\Skinning.lua
|
||||||
|
Modules\TalentFrame.lua
|
||||||
|
Modules\VanityFrame.lua
|
||||||
|
Modules\WardrobeFrame.lua
|
||||||
|
Modules\ChallengesFrame.lua
|
||||||
|
Modules\InterfaceOptions.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
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
local E, L, V, P, G = unpack(ElvUI)
|
||||||
|
local CoA = E:GetModule("CoA")
|
||||||
|
local Skin = CoA.Skin
|
||||||
|
|
||||||
|
local FRAME_NAME = "ChallengesFrame"
|
||||||
|
|
||||||
|
-- Unlike the talent/vanity/wardrobe trio, this window is not one of the
|
||||||
|
-- Collections container's children: it's a HIGH-strata frame parented straight
|
||||||
|
-- to UIParent, and it's mouse-enabled itself rather than being dragged by a
|
||||||
|
-- container (probed). So it deliberately doesn't go through
|
||||||
|
-- Skin:ApplyWindowScale or Skin:CollectionTabs -- the first scales Collections,
|
||||||
|
-- which would resize the other three windows and not this one, and the second
|
||||||
|
-- skins the tab row those three share, which this window has no part in. A
|
||||||
|
-- scale slider for it later would set the scale on the frame itself, and the
|
||||||
|
-- drag target moves with it because the frame is its own drag target.
|
||||||
|
|
||||||
|
-- Stock Blizzard panel tabs, unlike every other tab row the plugin skins: plain
|
||||||
|
-- Buttons rather than CheckButtons, and their art is named Left/Middle/Right
|
||||||
|
-- (plus the Disabled set Blizzard swaps in for the selected tab) rather than
|
||||||
|
-- the server's Left/Center/Right -- both flavours are covered by the shared
|
||||||
|
-- strip in Skinning.lua. They're real children of the frame, so no level parent
|
||||||
|
-- is needed: normal parent/child z-order already puts them above its panel.
|
||||||
|
--
|
||||||
|
-- Six exist today, two of them gated behind content an account may not have, so
|
||||||
|
-- the row is walked until the first missing index rather than against a
|
||||||
|
-- hardcoded count -- same shape as Skin:CollectionTabs.
|
||||||
|
--
|
||||||
|
-- This is the one row in the plugin that interlocks: every tab past the first is
|
||||||
|
-- anchored 16px back into the one before it (probed) so the native end caps
|
||||||
|
-- overlap, which reads as a single merged bar once the art is gone. Hence the
|
||||||
|
-- unoverlap flag; the server-authored rows sit apart already and don't take it.
|
||||||
|
local MAX_TABS = 10
|
||||||
|
|
||||||
|
local function SkinTabs()
|
||||||
|
for i = 1, MAX_TABS do
|
||||||
|
local tab = _G[FRAME_NAME.."Tab"..i]
|
||||||
|
if not tab then break end
|
||||||
|
|
||||||
|
Skin:Tab(tab, nil, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The row hangs 6px clear of the frame's bottom edge (probed), which the native
|
||||||
|
-- art spanned with the tabs' own ornate tops -- gone with the art, so the row
|
||||||
|
-- reads as detached from the window it belongs to. Only the first tab carries a
|
||||||
|
-- vertical offset at all, the rest being anchored off its edge, so moving it
|
||||||
|
-- moves the row.
|
||||||
|
--
|
||||||
|
-- Set to the backdrop inset rather than to zero: the tab's flat backdrop starts
|
||||||
|
-- that far inside the tab, so a tab flush with the frame still leaves its
|
||||||
|
-- visible edge floating. Written as an absolute rather than as an adjustment to
|
||||||
|
-- what's there, which keeps it idempotent across the repeated skin passes.
|
||||||
|
local function AnchorTabRow()
|
||||||
|
local tab = _G[FRAME_NAME.."Tab1"]
|
||||||
|
if not tab then return end
|
||||||
|
|
||||||
|
local point, relativeTo, relativePoint, x = tab:GetPoint(1)
|
||||||
|
if not point then return end
|
||||||
|
|
||||||
|
tab:SetPoint(point, relativeTo, relativePoint, x, Skin.TabBackdropInset)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The nine-slice draws the ornate purple border and the teal inner panel over
|
||||||
|
-- the top of the frame, doubling up with the Transparent template's own border
|
||||||
|
-- -- but unlike the talent and wardrobe frames' nine-slices it isn't purely
|
||||||
|
-- decorative: the window's title is one of its regions (probed: nine BORDER
|
||||||
|
-- textures, one ARTWORK texture, and the title FontString). So it's stripped
|
||||||
|
-- rather than run through Skin:HideArt, whose Hide() would take the title down
|
||||||
|
-- with the border. StripTextures only touches Textures, so the art goes and the
|
||||||
|
-- FontString stays.
|
||||||
|
local function StripBorderArt()
|
||||||
|
local nineSlice = _G[FRAME_NAME.."NineSlice"]
|
||||||
|
if not nineSlice then return end
|
||||||
|
|
||||||
|
nineSlice:StripTextures()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- That title has no name of its own -- there is no ChallengesFrameTitleText,
|
||||||
|
-- and none of the other usual suffixes resolve either (probed) -- so it's
|
||||||
|
-- picked out by type, the same way the vanity frame's pager arrows are picked
|
||||||
|
-- out by position. The nine-slice owns exactly one FontString.
|
||||||
|
local function GetTitle()
|
||||||
|
local nineSlice = _G[FRAME_NAME.."NineSlice"]
|
||||||
|
if not nineSlice then return end
|
||||||
|
|
||||||
|
for i = 1, nineSlice:GetNumRegions() do
|
||||||
|
local region = select(i, nineSlice:GetRegions())
|
||||||
|
|
||||||
|
if region:GetObjectType() == "FontString" then return region end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Vertical slack left above the title inside the widened panel.
|
||||||
|
local TITLE_PADDING = 6
|
||||||
|
|
||||||
|
-- The title is drawn above the frame's own top edge -- the native art carries a
|
||||||
|
-- banner up there that the frame's rect doesn't include -- so a panel sized to
|
||||||
|
-- the frame leaves the title floating over the open world. The panel is built
|
||||||
|
-- as a backdrop rather than through SetTemplate for exactly this reason: a
|
||||||
|
-- template's backdrop is drawn as regions of the frame itself and can only ever
|
||||||
|
-- match its rect, while a backdrop is a child frame with points of its own, so
|
||||||
|
-- its top edge can be pushed up to take the title in.
|
||||||
|
--
|
||||||
|
-- The distance is measured live rather than hardcoded: it's whatever gap the
|
||||||
|
-- server's own layout left between the title and the frame, so this stays right
|
||||||
|
-- if that layout changes. Re-measured on every show for the same reason the tab
|
||||||
|
-- heights are (see Skinning.lua) -- on a cold /reload the layout isn't settled
|
||||||
|
-- at skin time, and a one-shot measurement catches a stale value.
|
||||||
|
--
|
||||||
|
-- The hit rect is grown to match. The frame is its own drag target (probed), and
|
||||||
|
-- without this the widened panel would have a band along its top that looks part
|
||||||
|
-- of the window but can't be grabbed.
|
||||||
|
local function UpdatePanelTop(frame)
|
||||||
|
local backdrop = frame.backdrop
|
||||||
|
local title = GetTitle()
|
||||||
|
if not (backdrop and title) then return end
|
||||||
|
|
||||||
|
local frameTop, titleTop = frame:GetTop(), title:GetTop()
|
||||||
|
if not (frameTop and titleTop) then return end
|
||||||
|
|
||||||
|
local offset = titleTop - frameTop + TITLE_PADDING
|
||||||
|
if offset < 0 then offset = 0 end
|
||||||
|
|
||||||
|
backdrop:Point("TOPLEFT", frame, "TOPLEFT", 0, offset)
|
||||||
|
backdrop:Point("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
|
||||||
|
|
||||||
|
-- Negative insets grow the hit rect outwards rather than shrinking it.
|
||||||
|
frame:SetHitRectInsets(0, 0, -offset, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Per-tab contents. Each of the six tabs owns its own copy of these widgets
|
||||||
|
-- rather than sharing one set, so they're skinned per tab; Trials, Rewards,
|
||||||
|
-- Challenges and Gamemodes are done, and the two left follow the same shape.
|
||||||
|
--
|
||||||
|
-- Nothing about one tab's naming carries to the next: the Trials search box is
|
||||||
|
-- "...TrialsTabSearch" and its filter a "...FilterDropDown", where the Rewards
|
||||||
|
-- pair are "...StoreTabSearchBox" and "...Filter" (probed). Every suffix on a
|
||||||
|
-- new tab is worth confirming rather than copying.
|
||||||
|
local TRIALS_TAB = FRAME_NAME.."TrialsTab"
|
||||||
|
local TRIALS_LIST = TRIALS_TAB.."Challenges"
|
||||||
|
local TRIALS_SCROLL = TRIALS_LIST.."ScrollFrame"
|
||||||
|
|
||||||
|
-- The list container's one and only region: a 2px rule drawn across the bottom
|
||||||
|
-- of the panel from the store art (probed). It's the native panel's own divider,
|
||||||
|
-- and with the panel around it flat there's nothing left for it to divide.
|
||||||
|
local LIST_DIVIDER_ART = "perksactivities"
|
||||||
|
|
||||||
|
local function SkinTrialsTab()
|
||||||
|
Skin:SearchBox(_G[TRIALS_TAB.."Search"])
|
||||||
|
|
||||||
|
-- Cleared by file rather than stripped: the container is the scroll frame's
|
||||||
|
-- own parent, so a blind strip is a blunter instrument than one known region
|
||||||
|
-- needs, and StripArtByFile's noop holds if the list ever re-arts it.
|
||||||
|
Skin:StripArtByFile(_G[TRIALS_LIST], LIST_DIVIDER_ART)
|
||||||
|
|
||||||
|
-- The same nine "UI-Silver-Button" slices and ChatFrameExpandArrow caret as
|
||||||
|
-- the vanity and wardrobe dropdowns (probed), so it takes the same handler.
|
||||||
|
-- Nothing is passed for its popout: the Trials tab has exactly three children
|
||||||
|
-- and none of them is a menu (probed), so wherever the list is built it isn't
|
||||||
|
-- there. Skin:Dropdown skips that half when it's handed nil.
|
||||||
|
Skin:Dropdown(_G[TRIALS_TAB.."FilterDropDown"])
|
||||||
|
|
||||||
|
-- The arrows are named off the scroll frame rather than off the bar, which is
|
||||||
|
-- the other reason S:HandleScrollBar can't find them on its own.
|
||||||
|
Skin:ScrollBar(
|
||||||
|
_G[TRIALS_SCROLL.."ScrollBar"],
|
||||||
|
_G[TRIALS_SCROLL.."ScrollBarThumb"],
|
||||||
|
_G[TRIALS_SCROLL.."ScrollUpButton"],
|
||||||
|
_G[TRIALS_SCROLL.."ScrollDownButton"]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local STORE_TAB = FRAME_NAME.."StoreTab"
|
||||||
|
local STORE_LAYOUT = STORE_TAB.."StoreButtonLayout"
|
||||||
|
|
||||||
|
-- Walked to the first missing index rather than against the twenty-one that
|
||||||
|
-- exist today, the same shape as the tab row: this count is a page size, and a
|
||||||
|
-- page size is exactly the sort of thing a content patch moves.
|
||||||
|
local MAX_SLOTS = 40
|
||||||
|
|
||||||
|
-- The bevelled empty-slot plate every bag and auction square on this client
|
||||||
|
-- draws, here behind each reward card (probed). The dashes are escaped because
|
||||||
|
-- StripArtByFile matches with find, where an unescaped dash is a pattern range.
|
||||||
|
local SLOT_ART = "UI%-AuctionFrame%-ItemSlot"
|
||||||
|
|
||||||
|
-- The card's rect is drawn tight around its contents: 140x32 with a 32px icon,
|
||||||
|
-- so the icon meets the top and bottom edges exactly and the name runs to the
|
||||||
|
-- right one. The native plate hid that by bleeding a bevel outwards; a flat
|
||||||
|
-- backdrop on the same rect just looks cramped. The grid leaves far more room
|
||||||
|
-- than this between cards -- tens of pixels each way -- so the box is grown
|
||||||
|
-- rather than the contents moved in, which would fight the store's own layout.
|
||||||
|
local CARD_PADDING = 4
|
||||||
|
|
||||||
|
-- The headings above the grid are drawn out of the quest log's atlas by tex
|
||||||
|
-- coord (probed: three slices plus the button's own NormalTexture, all one
|
||||||
|
-- file). Cleared by file rather than stripped so the label survives.
|
||||||
|
local HEADING_ART = "questmaplogatlas"
|
||||||
|
|
||||||
|
-- A row rather than an item square: 140x32 with a 32px icon flush left, then
|
||||||
|
-- the name, then a SimpleHTML cost (probed). The icon is a region of the button
|
||||||
|
-- itself rather than sitting in a frame of its own, unlike the talent menu's
|
||||||
|
-- rows, so it takes the crop but not a backdrop of its own.
|
||||||
|
--
|
||||||
|
-- The card takes HandleButton's useCreateBackdrop path rather than its default
|
||||||
|
-- SetTemplate one, which is what makes the padding possible at all: a template
|
||||||
|
-- is drawn as regions of the button and can only ever match its rect, while a
|
||||||
|
-- backdrop is a child frame that can be set outside it. Same reasoning as the
|
||||||
|
-- window panel's, one level down. Skin:Button's hover colouring follows either
|
||||||
|
-- way -- it resolves button.backdrop before the button itself.
|
||||||
|
--
|
||||||
|
-- The native hover square needs nothing by name: it's a plain region on the
|
||||||
|
-- HIGHLIGHT layer (probed), which is the whole layer Skin:Button's
|
||||||
|
-- StripHighlightArt takes.
|
||||||
|
--
|
||||||
|
-- The crop is re-applied from a hook rather than set once. The grid is a fixed
|
||||||
|
-- pool the store refills in place on every search, filter and page change, and
|
||||||
|
-- SetTexture resets a texture's coords -- so a one-shot crop would survive
|
||||||
|
-- exactly until the first page turn, and the native icon borders would come
|
||||||
|
-- back on every card at once. The plate underneath needs no such watch:
|
||||||
|
-- StripArtByFile noops its SetTexture, so a refill that re-arts it writes
|
||||||
|
-- nothing.
|
||||||
|
local function SkinStoreCard(slot)
|
||||||
|
if not slot then return end
|
||||||
|
|
||||||
|
Skin:StripArtByFile(slot, SLOT_ART)
|
||||||
|
Skin:Button(slot, nil, nil, true)
|
||||||
|
|
||||||
|
-- Re-set on every pass rather than once: SetOutside writes fixed points, so
|
||||||
|
-- a UI scale change that moves what a pixel is leaves them stale.
|
||||||
|
if slot.backdrop then
|
||||||
|
slot.backdrop:SetOutside(slot, CARD_PADDING, CARD_PADDING)
|
||||||
|
end
|
||||||
|
|
||||||
|
local name = slot:GetName()
|
||||||
|
local icon = name and _G[name.."Icon"]
|
||||||
|
if not icon then return end
|
||||||
|
|
||||||
|
icon:SetTexCoord(unpack(E.TexCoords))
|
||||||
|
|
||||||
|
-- Guarded separately from Skin:Button's own flag, and for the same reason it
|
||||||
|
-- guards its own: this runs again on every show, and hooksecurefunc stacks
|
||||||
|
-- handlers rather than replacing them.
|
||||||
|
if not slot.CoAIconHooked then
|
||||||
|
slot.CoAIconHooked = true
|
||||||
|
|
||||||
|
hooksecurefunc(icon, "SetTexture", function(texture)
|
||||||
|
texture:SetTexCoord(unpack(E.TexCoords))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- "Trial Master's Rewards" and its sibling for the build vendor are Buttons by
|
||||||
|
-- type, but they aren't buttons: they're the grid's section headings, drawn as
|
||||||
|
-- a stylised plaque and doing nothing when clicked. So they get their art taken
|
||||||
|
-- off and nothing put back -- no backdrop, which would draw a box around a
|
||||||
|
-- caption and read as clickable, and no hover border for the same reason. The
|
||||||
|
-- HIGHLIGHT layer goes too, or the caption lights up under the cursor with
|
||||||
|
-- nothing behind it.
|
||||||
|
--
|
||||||
|
-- Walked off the layout rather than named, since the set is one per reward
|
||||||
|
-- vendor and grows with content. Same reasoning as the tab row.
|
||||||
|
local function SkinStoreHeadings()
|
||||||
|
local layout = _G[STORE_LAYOUT]
|
||||||
|
if not layout then return end
|
||||||
|
|
||||||
|
for i = 1, layout:GetNumChildren() do
|
||||||
|
local heading = select(i, layout:GetChildren())
|
||||||
|
|
||||||
|
Skin:StripArtByFile(heading, HEADING_ART)
|
||||||
|
Skin:StripHighlightArt(heading)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Everything on this tab exists before it has ever been opened (probed: the
|
||||||
|
-- same twenty-nine children on a cold reload as after a click), so it goes
|
||||||
|
-- through SkinContents with the rest rather than needing a hook on the tab's
|
||||||
|
-- own OnShow.
|
||||||
|
--
|
||||||
|
-- Two things here are deliberately left alone. The Currency button carries the
|
||||||
|
-- trophy icon and the "Trial Master's Trophy: 0" counter as its only two
|
||||||
|
-- regions and no frame art at all, so there's nothing to strip and a backdrop
|
||||||
|
-- would only make a label look clickable. PageText is a FontString region of
|
||||||
|
-- the tab itself -- the same trap as the vanity store's counters, and the
|
||||||
|
-- reason Skin:Panel grew keepTextures -- so nothing here blind-strips the tab.
|
||||||
|
--
|
||||||
|
-- No scroll bar is wired: the grid pages rather than scrolls, and there's no
|
||||||
|
-- scroll frame anywhere under the tab to hang one off (probed).
|
||||||
|
local function SkinStoreTab()
|
||||||
|
Skin:SearchBox(_G[STORE_TAB.."SearchBox"])
|
||||||
|
|
||||||
|
-- Not a dropdown by structure -- no Middle texture, no Button child, no Text
|
||||||
|
-- fontstring, just a plain Button (probed) -- but it is one by art: the same
|
||||||
|
-- nine "UI-Silver-Button" slices and ChatFrameExpandArrow caret the Trials
|
||||||
|
-- filter wears. Skin:Dropdown matches on the art rather than on the naming,
|
||||||
|
-- so it takes this one too; the Middle-plus-Button structural test is only
|
||||||
|
-- ever a shortcut to that question, and here it answers it wrongly.
|
||||||
|
Skin:Dropdown(_G[STORE_TAB.."Filter"])
|
||||||
|
|
||||||
|
-- Stock "UI-SquareButton" arrows (probed), unlike the hand-built bar on the
|
||||||
|
-- Trials list, so these go through the ElvUI handler unaided.
|
||||||
|
Skin:NextPrevButton(_G[STORE_TAB.."PreviousPageButton"], "left")
|
||||||
|
Skin:NextPrevButton(_G[STORE_TAB.."NextPageButton"], "right")
|
||||||
|
|
||||||
|
SkinStoreHeadings()
|
||||||
|
|
||||||
|
for i = 1, MAX_SLOTS do
|
||||||
|
local slot = _G[STORE_TAB.."Slot"..i]
|
||||||
|
if not slot then break end
|
||||||
|
|
||||||
|
SkinStoreCard(slot)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local CHALLENGES_TAB = FRAME_NAME.."ChallengesTab"
|
||||||
|
local CHALLENGES_LIST = CHALLENGES_TAB.."Challenges"
|
||||||
|
local CHALLENGES_SCROLL = CHALLENGES_LIST.."ScrollFrame"
|
||||||
|
|
||||||
|
-- The one tab whose naming does carry over from another: the same three
|
||||||
|
-- children as Trials, under the same suffixes, with the same store divider as
|
||||||
|
-- its list container's only region, the same nine "UI-Silver-Button" slices and
|
||||||
|
-- caret on its filter, and its arrows named off the scroll frame rather than
|
||||||
|
-- off the bar (probed). So it takes the Trials treatment call for call -- see
|
||||||
|
-- SkinTrialsTab above for why each of the four is the call it is.
|
||||||
|
--
|
||||||
|
-- The divider constant is shared rather than copied: it is the same 2px rule out
|
||||||
|
-- of the same file, not a second one that happens to match today.
|
||||||
|
local function SkinChallengesTab()
|
||||||
|
Skin:SearchBox(_G[CHALLENGES_TAB.."Search"])
|
||||||
|
Skin:StripArtByFile(_G[CHALLENGES_LIST], LIST_DIVIDER_ART)
|
||||||
|
Skin:Dropdown(_G[CHALLENGES_TAB.."FilterDropDown"])
|
||||||
|
Skin:ScrollBar(
|
||||||
|
_G[CHALLENGES_SCROLL.."ScrollBar"],
|
||||||
|
_G[CHALLENGES_SCROLL.."ScrollBarThumb"],
|
||||||
|
_G[CHALLENGES_SCROLL.."ScrollUpButton"],
|
||||||
|
_G[CHALLENGES_SCROLL.."ScrollDownButton"]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local GAMEMODES_TAB = FRAME_NAME.."GamemodesTab"
|
||||||
|
local GAMEMODES_LIST = GAMEMODES_TAB.."Challenges"
|
||||||
|
local GAMEMODES_SCROLL = GAMEMODES_LIST.."ScrollFrame"
|
||||||
|
|
||||||
|
-- The Trials/Challenges shape a third time, and again suffix for suffix: the
|
||||||
|
-- same search box and list names, the same store divider as its list
|
||||||
|
-- container's only region, the same arrows named off the scroll frame rather
|
||||||
|
-- than off the bar (probed). See SkinTrialsTab above for why each call is the
|
||||||
|
-- call it is.
|
||||||
|
--
|
||||||
|
-- What differs is the filter: there isn't one, and that's the tab's own shape
|
||||||
|
-- rather than something missed here. It has exactly two children, the search
|
||||||
|
-- box and the list, and neither of them is a menu (probed), so there is nothing
|
||||||
|
-- for a Skin:Dropdown line to be handed.
|
||||||
|
--
|
||||||
|
-- It owns no regions of its own either (probed), so nothing here needs the
|
||||||
|
-- keepTextures care the Rewards tab's PageText took. And everything under it
|
||||||
|
-- exists before the tab has ever been opened -- two children and no regions on
|
||||||
|
-- a cold reload, unchanged after a click -- so it rides SkinContents like the
|
||||||
|
-- other three rather than needing a hook on the tab's own OnShow.
|
||||||
|
local function SkinGamemodesTab()
|
||||||
|
Skin:SearchBox(_G[GAMEMODES_TAB.."Search"])
|
||||||
|
Skin:StripArtByFile(_G[GAMEMODES_LIST], LIST_DIVIDER_ART)
|
||||||
|
Skin:ScrollBar(
|
||||||
|
_G[GAMEMODES_SCROLL.."ScrollBar"],
|
||||||
|
_G[GAMEMODES_SCROLL.."ScrollBarThumb"],
|
||||||
|
_G[GAMEMODES_SCROLL.."ScrollUpButton"],
|
||||||
|
_G[GAMEMODES_SCROLL.."ScrollDownButton"]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The close button is anchored inside the frame's own top-right corner, which
|
||||||
|
-- stopped being the corner the player sees once the panel grew up over the title
|
||||||
|
-- band -- it ends up floating a title's height below the top edge. Re-anchored
|
||||||
|
-- to the panel instead, and re-run on every pass so it follows the panel's top
|
||||||
|
-- whenever that gets re-measured.
|
||||||
|
--
|
||||||
|
-- No offset of its own: Skin:CloseButton normalises the button to a 32px box
|
||||||
|
-- with a 12px X centred in it, so the margin off the corner is already built in.
|
||||||
|
local function AnchorCloseButton(frame)
|
||||||
|
local close = _G[FRAME_NAME.."CloseButton"]
|
||||||
|
if not (close and frame.backdrop) then return end
|
||||||
|
|
||||||
|
close:ClearAllPoints()
|
||||||
|
close:SetPoint("TOPRIGHT", frame.backdrop, "TOPRIGHT", 0, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Nothing here re-runs on a tab switch, and nothing needs to: switching tabs
|
||||||
|
-- doesn't fire the frame's OnShow, and the one thing a switch does disturb is
|
||||||
|
-- the tab art, which Skin:Tab watches from its own OnUpdate. The title
|
||||||
|
-- fontstring is reused across tabs rather than swapped for a per-tab one -- its
|
||||||
|
-- text changes, the object doesn't -- so it only needs the one pass too.
|
||||||
|
local function SkinContents()
|
||||||
|
StripBorderArt()
|
||||||
|
Skin:Title(GetTitle())
|
||||||
|
UpdatePanelTop(_G[FRAME_NAME])
|
||||||
|
Skin:CloseButton(_G[FRAME_NAME.."CloseButton"])
|
||||||
|
AnchorCloseButton(_G[FRAME_NAME])
|
||||||
|
SkinTabs()
|
||||||
|
AnchorTabRow()
|
||||||
|
SkinTrialsTab()
|
||||||
|
SkinStoreTab()
|
||||||
|
SkinGamemodesTab()
|
||||||
|
SkinChallengesTab()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SkinFrame(frame)
|
||||||
|
if not frame.CoASkinned then
|
||||||
|
frame.CoASkinned = true
|
||||||
|
|
||||||
|
-- A single BACKGROUND texture is all the frame owns (probed), and it's
|
||||||
|
-- decorative -- nothing functional is drawn as a region of this frame,
|
||||||
|
-- unlike the vanity store, whose currency counters are. So it's stripped
|
||||||
|
-- outright. Stripped before the backdrop is made, not after: a backdrop
|
||||||
|
-- built first would be a child frame and survive, but the order is kept
|
||||||
|
-- the same as Skin:Panel's for one less rule to remember.
|
||||||
|
frame:StripTextures()
|
||||||
|
frame:CreateBackdrop("Transparent")
|
||||||
|
|
||||||
|
frame:HookScript("OnShow", SkinContents)
|
||||||
|
end
|
||||||
|
|
||||||
|
SkinContents()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function TryHook()
|
||||||
|
local frame = _G[FRAME_NAME]
|
||||||
|
if not frame then return false end
|
||||||
|
|
||||||
|
SkinFrame(frame)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function CoA:InitializeChallengesFrame()
|
||||||
|
if not E.private.skins.blizzard.enable then return end
|
||||||
|
|
||||||
|
Skin:OnFrameAvailable(TryHook)
|
||||||
|
end
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
local E, L, V, P, G = unpack(ElvUI)
|
||||||
|
local CoA = E:GetModule("CoA")
|
||||||
|
|
||||||
|
-- defaultY staggers the movers' first-run default position (LEFT of screen,
|
||||||
|
-- spaced vertically) so they don't stack on top of each other before the
|
||||||
|
-- player has ever dragged them.
|
||||||
|
local FRAMES = {
|
||||||
|
{name = "CoAResourceSegmentBar", moverText = "Resource Segment Bar", hideKey = "hideResourceSegmentBar", defaultY = 40},
|
||||||
|
{name = "CoAResourceOrb", moverText = "Resource Orb", hideKey = "hideResourceOrb", defaultY = 100},
|
||||||
|
{name = "CoAResourceBar", moverText = "Resource Bar", hideKey = "hideResourceBar", defaultY = 160},
|
||||||
|
{name = "CoAMultiCastActionBarFrame", moverText = "Multi Cast Action Bar", hideKey = "hideMultiCastActionBar", defaultY = 220},
|
||||||
|
}
|
||||||
|
|
||||||
|
local DEFAULT_WIDTH, DEFAULT_HEIGHT = 150, 30
|
||||||
|
|
||||||
|
local FRAME_NAMES = {}
|
||||||
|
for _, def in ipairs(FRAMES) do
|
||||||
|
FRAME_NAMES[def.name] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- These frames' native right-click dropdown offers a "Lock/Unlock Frame"
|
||||||
|
-- entry that's meaningless now that dragging is permanently disabled (see
|
||||||
|
-- DisableDrag below), so strip it. Matched loosely (case-insensitive,
|
||||||
|
-- requiring both words) since the exact wording isn't confirmed, and scoped
|
||||||
|
-- to only our tracked frames' dropdowns so it can't affect unrelated menus.
|
||||||
|
do
|
||||||
|
local function IsTrackedDropdown(menu)
|
||||||
|
if not menu then return false end
|
||||||
|
|
||||||
|
local name = menu.GetName and menu:GetName()
|
||||||
|
local ownerName = name and name:match("^(.-)DropDown$")
|
||||||
|
if ownerName and FRAME_NAMES[ownerName] then return true end
|
||||||
|
|
||||||
|
local parent = menu.GetParent and menu:GetParent()
|
||||||
|
local parentName = parent and parent.GetName and parent:GetName()
|
||||||
|
return parentName and FRAME_NAMES[parentName] or false
|
||||||
|
end
|
||||||
|
|
||||||
|
local orig_AddButton = UIDropDownMenu_AddButton
|
||||||
|
UIDropDownMenu_AddButton = function(info, level)
|
||||||
|
if info and info.text and IsTrackedDropdown(UIDROPDOWNMENU_INIT_MENU) then
|
||||||
|
local text = info.text:lower()
|
||||||
|
if text:find("lock") and text:find("frame") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return orig_AddButton(info, level)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Replace the native "Drag to move" tooltip hint with one that reflects how
|
||||||
|
-- these frames are actually repositioned now (native drag is disabled).
|
||||||
|
-- Scoped by IsMouseOver rather than GameTooltip:GetOwner(), since the owner
|
||||||
|
-- passed to SetOwner may be a child region rather than the tracked frame
|
||||||
|
-- itself. Matched loosely (case-insensitive "drag" + "move") since the exact
|
||||||
|
-- wording isn't confirmed. Hooked on both OnShow and OnUpdate since some
|
||||||
|
-- tooltips keep refreshing their lines while visible.
|
||||||
|
local tooltipFrames = {}
|
||||||
|
local tooltipHooked = false
|
||||||
|
|
||||||
|
local function ReplaceTooltipLines()
|
||||||
|
local hovering = false
|
||||||
|
for frame in pairs(tooltipFrames) do
|
||||||
|
if frame:IsMouseOver() then
|
||||||
|
hovering = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not hovering then return end
|
||||||
|
|
||||||
|
for i = 1, GameTooltip:NumLines() do
|
||||||
|
local line = _G["GameTooltipTextLeft"..i]
|
||||||
|
local text = line and line:GetText()
|
||||||
|
if text then
|
||||||
|
-- Some lines combine the drag hint with other text (e.g. a
|
||||||
|
-- trailing "Right-click for options"), so replace just the
|
||||||
|
-- matched span instead of the whole line.
|
||||||
|
local replaced, count = text:gsub("[Dd]rag.-[Mm]ove", "Toggle ElvUI's anchors to move")
|
||||||
|
if count > 0 then
|
||||||
|
line:SetText(replaced)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FixTooltip(frame)
|
||||||
|
if tooltipFrames[frame] then return end
|
||||||
|
tooltipFrames[frame] = true
|
||||||
|
|
||||||
|
if not tooltipHooked then
|
||||||
|
tooltipHooked = true
|
||||||
|
GameTooltip:HookScript("OnShow", ReplaceTooltipLines)
|
||||||
|
GameTooltip:HookScript("OnUpdate", ReplaceTooltipLines)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
-- Created eagerly (independent of the native frame ever showing up -- several
|
||||||
|
-- of these are class-specific and may never appear for a given character) so
|
||||||
|
-- every mover always appears in Toggle Anchors. The default point is only
|
||||||
|
-- used until the player drags it once; after that E:CreateMover restores the
|
||||||
|
-- saved position from E.db.movers.
|
||||||
|
local function CreateMoverHolder(def)
|
||||||
|
if CoA.classResourceMoversCreated and CoA.classResourceMoversCreated[def.name] then return end
|
||||||
|
CoA.classResourceMoversCreated = CoA.classResourceMoversCreated or {}
|
||||||
|
CoA.classResourceMoversCreated[def.name] = true
|
||||||
|
|
||||||
|
local holder = CreateFrame("Frame", "CoA_"..def.name.."Holder", E.UIParent)
|
||||||
|
holder:Size(DEFAULT_WIDTH, DEFAULT_HEIGHT)
|
||||||
|
holder:Point("LEFT", E.UIParent, "LEFT", 150, def.defaultY)
|
||||||
|
|
||||||
|
E:CreateMover(holder, "CoA_"..def.name.."Mover", def.moverText, nil, nil, nil, "ALL,COA", nil, "CoA,classResources")
|
||||||
|
holder:SetAllPoints(_G["CoA_"..def.name.."Mover"])
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SyncHolderSize(frame, name)
|
||||||
|
local holder = _G["CoA_"..name.."Holder"]
|
||||||
|
if not holder then return end
|
||||||
|
|
||||||
|
local width, height = frame:GetSize()
|
||||||
|
if width > 0 and height > 0 then
|
||||||
|
holder:Size(width, height)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function AnchorFrame(frame, name)
|
||||||
|
local holder = _G["CoA_"..name.."Holder"]
|
||||||
|
if not holder then return end
|
||||||
|
|
||||||
|
QueueAnchor(function()
|
||||||
|
AnchorToHolder(frame, holder)
|
||||||
|
LockPosition(frame, holder)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The server never Show()s a frame the current class doesn't use, so its
|
||||||
|
-- data (powerType, maxValue, ...) is never populated. Forcing Show() on it
|
||||||
|
-- exposes that uninitialized state and renders as a garbage white bar
|
||||||
|
-- texture (same issue and same fix as HideCoAUI). Only force-show a frame
|
||||||
|
-- we've already seen the game display naturally at least once.
|
||||||
|
local seenNaturalShow = {}
|
||||||
|
|
||||||
|
-- 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 and seenNaturalShow[frame] 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)
|
||||||
|
seenNaturalShow[self] = true
|
||||||
|
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)
|
||||||
|
FixTooltip(frame)
|
||||||
|
SyncHolderSize(frame, def.name)
|
||||||
|
AnchorFrame(frame, def.name)
|
||||||
|
|
||||||
|
hooked[def.name] = true
|
||||||
|
else
|
||||||
|
allHooked = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return allHooked
|
||||||
|
end
|
||||||
|
|
||||||
|
function CoA:InitializeClassResources()
|
||||||
|
for _, def in ipairs(FRAMES) do
|
||||||
|
CreateMoverHolder(def)
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
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, keyed to the Character
|
||||||
|
-- Advancement entry that grants them. The IDs are stable and shared across
|
||||||
|
-- every class tree, so querying one from a class that can't take it simply
|
||||||
|
-- reports rank 0 rather than erroring.
|
||||||
|
local TALENT_DISPEL_TYPES = {
|
||||||
|
PROPHET = {talentID = 6324, talentName = "Blight Antidote", types = {Curse = true}},
|
||||||
|
CULTIST = {talentID = 12982, talentName = "Devour Curse", types = {Curse = true}},
|
||||||
|
PYROMANCER = {talentID = 31276, talentName = "Burn Impurities", types = {Magic = true, Disease = true, Bleed = true}},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Cached result of the talent lookup for the player's current build.
|
||||||
|
-- PostUpdate_DebuffHighlight runs once per aura per frame, so the query is
|
||||||
|
-- hoisted out of that path and refreshed only when the build can change.
|
||||||
|
local hasTalentDispel = false
|
||||||
|
|
||||||
|
-- C_CharacterAdvancement is a server addition with no API documentation, so
|
||||||
|
-- every entry point is guarded and a failed call reads as "talent not taken",
|
||||||
|
-- which degrades to the same behaviour as before auto-detection existed.
|
||||||
|
local function HasTalent(talent)
|
||||||
|
local api = C_CharacterAdvancement
|
||||||
|
if not api then return false end
|
||||||
|
|
||||||
|
if type(api.GetTalentRankByID) == "function" then
|
||||||
|
local ok, rank = pcall(api.GetTalentRankByID, talent.talentID)
|
||||||
|
if ok and type(rank) == "number" and rank > 0 then return true end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fall back to matching the talent by name, so an entry ID gone stale after
|
||||||
|
-- a server rebalance doesn't silently switch the feature off. Names are the
|
||||||
|
-- more fragile key of the two, but GetKnownTalentEntries only ever lists
|
||||||
|
-- entries the player has actually learned, which makes this a genuinely
|
||||||
|
-- independent check rather than a second opinion on the same ID. The list
|
||||||
|
-- has been seen with holes in it, so it's walked with pairs.
|
||||||
|
if type(api.GetKnownTalentEntries) == "function" then
|
||||||
|
local ok, entries = pcall(api.GetKnownTalentEntries)
|
||||||
|
if ok and type(entries) == "table" then
|
||||||
|
for _, entry in pairs(entries) do
|
||||||
|
if type(entry) == "table" and entry.Name == talent.talentName then return true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns whether the talent state actually moved, so callers reacting to the
|
||||||
|
-- chattier advancement events can skip the unitframe rebuild when it didn't.
|
||||||
|
local function RefreshTalentState()
|
||||||
|
local _, class = UnitClass("player")
|
||||||
|
local talent = TALENT_DISPEL_TYPES[class]
|
||||||
|
local hasTalent = (talent and HasTalent(talent)) or false
|
||||||
|
|
||||||
|
if hasTalent == hasTalentDispel then return false end
|
||||||
|
|
||||||
|
hasTalentDispel = hasTalent
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
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 hasTalentDispel and CoA.db.profile.dispelHighlightDetectTalents 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()
|
||||||
|
RefreshTalentState()
|
||||||
|
UF:Update_AllFrames()
|
||||||
|
end
|
||||||
|
|
||||||
|
function CoA:RefreshDispelTalents()
|
||||||
|
if RefreshTalentState() then
|
||||||
|
UF:Update_AllFrames()
|
||||||
|
end
|
||||||
|
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, and the advancement data lags in the same way. 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.
|
||||||
|
--
|
||||||
|
-- CHARACTER_ADVANCEMENT_UPDATE_ENTRIES_RESULT fires when a build is confirmed
|
||||||
|
-- in the talent UI, but switching to another saved specialization doesn't
|
||||||
|
-- confirm anything -- that path only fires PENDING_BUILD_UPDATED and
|
||||||
|
-- SUGGESTIONS_UPDATED, so both are needed or the ranks read stale for the rest
|
||||||
|
-- of the session. PENDING_BUILD_UPDATED also fires on every click inside the
|
||||||
|
-- talent UI and SUGGESTIONS_UPDATED is only incidentally related, which is why
|
||||||
|
-- they go through RefreshDispelTalents: it costs one rank lookup and rebuilds
|
||||||
|
-- the unitframes only when the answer changed.
|
||||||
|
function CoA:InitializeDispelHighlight()
|
||||||
|
CoA:RegisterEvent("PLAYER_ENTERING_WORLD", function()
|
||||||
|
CoA:ScheduleTimer("UpdateDispelHighlight", 2)
|
||||||
|
end)
|
||||||
|
|
||||||
|
for _, event in ipairs({
|
||||||
|
"CHARACTER_ADVANCEMENT_UPDATE_ENTRIES_RESULT",
|
||||||
|
"CHARACTER_ADVANCEMENT_PENDING_BUILD_UPDATED",
|
||||||
|
"CHARACTER_ADVANCEMENT_SUGGESTIONS_UPDATED",
|
||||||
|
}) do
|
||||||
|
CoA:RegisterEvent(event, function()
|
||||||
|
CoA:RefreshDispelTalents()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
+110
-23
@@ -1,8 +1,12 @@
|
|||||||
local E, L, V, P, G = unpack(ElvUI)
|
local E, L, V, P, G = unpack(ElvUI)
|
||||||
|
local LSM = E.Libs.LSM
|
||||||
local CoA = E:GetModule("CoA")
|
local CoA = E:GetModule("CoA")
|
||||||
|
|
||||||
local CONTAINER_NAME = "ExtraActionBar"
|
local CONTAINER_NAME = "ExtraActionBar"
|
||||||
local BUTTON_NAME = "ExtraActionBarPoolFrameExtraActionButtonTemplate1"
|
local BUTTON_NAME = "ExtraActionBarPoolFrameExtraActionButtonTemplate1"
|
||||||
|
local BINDING_ACTION = "CLICK "..BUTTON_NAME..":LeftButton"
|
||||||
|
|
||||||
|
_G["BINDING_NAME_"..BINDING_ACTION] = "Extra Action Button"
|
||||||
|
|
||||||
local function SkinGlow(button, name)
|
local function SkinGlow(button, name)
|
||||||
local glow = _G[name.."SpellActivationAlert"]
|
local glow = _G[name.."SpellActivationAlert"]
|
||||||
@@ -16,10 +20,77 @@ local function SkinGlow(button, name)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function SkinButton(button)
|
local RIM_EDGE_RATIO = 6 / 52 -- 6px confirmed empirically at the native 52px size to fully hide the icon's baked-in vanilla border
|
||||||
|
|
||||||
|
local function UpdateRimEdge(button, size)
|
||||||
|
local rim = button and button.CoARim
|
||||||
|
if not rim then return end
|
||||||
|
|
||||||
|
rim:SetBackdrop({edgeFile = E.media.blankTex, edgeSize = size * RIM_EDGE_RATIO})
|
||||||
|
rim:SetBackdropBorderColor(unpack(E.media.bordercolor))
|
||||||
|
end
|
||||||
|
|
||||||
|
local HOTKEY_MARGIN = 3
|
||||||
|
|
||||||
|
local function UpdateHotkeyPosition(button, size)
|
||||||
|
local hotkey = button and _G[button:GetName().."HotKey"]
|
||||||
|
if not hotkey then return end
|
||||||
|
|
||||||
|
local inset = -(size * RIM_EDGE_RATIO + HOTKEY_MARGIN)
|
||||||
|
hotkey:ClearAllPoints()
|
||||||
|
hotkey:Point("TOPRIGHT", inset, inset)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function UpdateSize(button)
|
||||||
|
button = button or _G[BUTTON_NAME]
|
||||||
|
if button then
|
||||||
|
local size = CoA.db.profile.extraActionButtonSize or 52
|
||||||
|
button:SetSize(size, size)
|
||||||
|
UpdateRimEdge(button, size)
|
||||||
|
UpdateHotkeyPosition(button, size)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function CoA:UpdateExtraActionButtonSize()
|
||||||
|
UpdateSize()
|
||||||
|
|
||||||
|
local holder = _G["CoA_ExtraActionBarHolder"]
|
||||||
|
if holder then
|
||||||
|
holder:Size(CoA.db.profile.extraActionButtonSize or 52)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function UpdateHotkeyText(button)
|
||||||
|
button = button or _G[BUTTON_NAME]
|
||||||
|
local hotkey = button and _G[button:GetName().."HotKey"]
|
||||||
|
if not hotkey then return end
|
||||||
|
|
||||||
|
local key = GetBindingKey(BINDING_ACTION)
|
||||||
|
if key then
|
||||||
|
hotkey:SetText(GetBindingText(key, "KEY_"))
|
||||||
|
hotkey:Show()
|
||||||
|
else
|
||||||
|
hotkey:SetText("")
|
||||||
|
hotkey:Hide()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function RecenterAnchor(button, container)
|
||||||
|
local x, y = button:GetCenter()
|
||||||
|
local cx, cy = container:GetCenter()
|
||||||
|
if not (x and y and cx and cy) then return end
|
||||||
|
|
||||||
|
button:ClearAllPoints()
|
||||||
|
button:SetPoint("CENTER", container, "CENTER", x - cx, y - cy)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SkinButton(button, container)
|
||||||
if button.CoASkinned then return end
|
if button.CoASkinned then return end
|
||||||
button.CoASkinned = true
|
button.CoASkinned = true
|
||||||
|
|
||||||
|
RecenterAnchor(button, container)
|
||||||
|
UpdateSize(button)
|
||||||
|
|
||||||
local name = button:GetName()
|
local name = button:GetName()
|
||||||
local icon = _G[name.."Icon"]
|
local icon = _G[name.."Icon"]
|
||||||
local hotkey = _G[name.."HotKey"]
|
local hotkey = _G[name.."HotKey"]
|
||||||
@@ -57,14 +128,18 @@ local function SkinButton(button)
|
|||||||
local rim = CreateFrame("Frame", nil, button.backdrop)
|
local rim = CreateFrame("Frame", nil, button.backdrop)
|
||||||
rim:SetAllPoints(icon)
|
rim:SetAllPoints(icon)
|
||||||
rim:SetFrameLevel(button.backdrop:GetFrameLevel() + 10)
|
rim:SetFrameLevel(button.backdrop:GetFrameLevel() + 10)
|
||||||
rim:SetBackdrop({edgeFile = E.media.blankTex, edgeSize = 6})
|
button.CoARim = rim
|
||||||
rim:SetBackdropBorderColor(unpack(E.media.bordercolor))
|
UpdateRimEdge(button, CoA.db.profile.extraActionButtonSize or 52)
|
||||||
|
|
||||||
|
button:SetFrameLevel(rim:GetFrameLevel() + 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
if hotkey then
|
if hotkey then
|
||||||
hotkey:ClearAllPoints()
|
hotkey:FontTemplate(LSM:Fetch("font", E.db.actionbar.font), E.db.actionbar.fontSize, E.db.actionbar.fontOutline)
|
||||||
hotkey:Point("TOPRIGHT", 0, 0)
|
hotkey:SetTextColor(1, 1, 1)
|
||||||
hotkey:FontTemplate()
|
|
||||||
|
UpdateHotkeyText(button)
|
||||||
|
CoA:RegisterEvent("UPDATE_BINDINGS", function() UpdateHotkeyText(button) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
if macroText then
|
if macroText then
|
||||||
@@ -87,26 +162,34 @@ local function SkinButton(button)
|
|||||||
SkinGlow(button, name)
|
SkinGlow(button, name)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function SetupMover(container, button)
|
-- Created eagerly (independent of the native ExtraActionBar ever showing up)
|
||||||
|
-- so the mover always appears in Toggle Anchors. The default point below is
|
||||||
|
-- only used until the player drags it once; after that E:CreateMover restores
|
||||||
|
-- the saved position from E.db.movers.
|
||||||
|
local function CreateMoverHolder()
|
||||||
if CoA.extraActionBarMoverCreated then return end
|
if CoA.extraActionBarMoverCreated then return end
|
||||||
|
|
||||||
local width, height = button:GetSize()
|
|
||||||
if width == 0 or height == 0 then return end
|
|
||||||
|
|
||||||
local left, bottom = button:GetLeft(), button:GetBottom()
|
|
||||||
if not left or not bottom then return end
|
|
||||||
|
|
||||||
CoA.extraActionBarMoverCreated = true
|
CoA.extraActionBarMoverCreated = true
|
||||||
|
|
||||||
|
local size = CoA.db.profile.extraActionButtonSize or 52
|
||||||
|
local holder = CreateFrame("Frame", "CoA_ExtraActionBarHolder", E.UIParent)
|
||||||
|
holder:Size(size, size)
|
||||||
|
holder:Point("BOTTOM", E.UIParent, "BOTTOM", 0, 260)
|
||||||
|
|
||||||
|
E:CreateMover(holder, "CoA_ExtraActionBarMover", "Extra Action Bar", nil, nil, nil, "ALL,COA", nil, "CoA,skins,extraActionButton")
|
||||||
|
holder:SetAllPoints(_G["CoA_ExtraActionBarMover"])
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SetupContainer(container)
|
||||||
|
if container.CoAContainerSetup then return end
|
||||||
|
container.CoAContainerSetup = true
|
||||||
|
|
||||||
container:StripTextures(true)
|
container:StripTextures(true)
|
||||||
container:EnableMouse(false)
|
container:EnableMouse(false)
|
||||||
|
end
|
||||||
|
|
||||||
local holder = CreateFrame("Frame", "CoA_ExtraActionBarHolder", E.UIParent)
|
local function AnchorContainer(container)
|
||||||
holder:Size(width, height)
|
local holder = _G["CoA_ExtraActionBarHolder"]
|
||||||
holder:Point("BOTTOMLEFT", E.UIParent, "BOTTOMLEFT", left, bottom)
|
if not holder then return end
|
||||||
|
|
||||||
E:CreateMover(holder, "CoA_ExtraActionBarMover", "Extra Action Bar", nil, nil, nil, "ALL,ACTIONBARS", nil, "CoA,skin,extraActionBar")
|
|
||||||
holder:SetAllPoints(_G["CoA_ExtraActionBarMover"])
|
|
||||||
|
|
||||||
local function Anchor()
|
local function Anchor()
|
||||||
container:ClearAllPoints()
|
container:ClearAllPoints()
|
||||||
@@ -128,14 +211,18 @@ local function TryHook()
|
|||||||
local button = _G[BUTTON_NAME]
|
local button = _G[BUTTON_NAME]
|
||||||
|
|
||||||
if container and button then
|
if container and button then
|
||||||
SetupMover(container, button)
|
SkinButton(button, container)
|
||||||
SkinButton(button)
|
SetupContainer(container)
|
||||||
|
AnchorContainer(container)
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
return container ~= nil and button ~= nil
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function CoA:InitializeExtraActionBar()
|
function CoA:InitializeExtraActionBar()
|
||||||
|
CreateMoverHolder()
|
||||||
|
|
||||||
if TryHook() then return end
|
if TryHook() then return end
|
||||||
|
|
||||||
self.extraActionBarTimer = self:ScheduleRepeatingTimer(function()
|
self.extraActionBarTimer = self:ScheduleRepeatingTimer(function()
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
local E, L, V, P, G = unpack(ElvUI)
|
||||||
|
local S = E:GetModule("Skins")
|
||||||
|
local CoA = E:GetModule("CoA")
|
||||||
|
|
||||||
|
local _G = _G
|
||||||
|
local ipairs, pairs, select, type = ipairs, pairs, select, type
|
||||||
|
|
||||||
|
-- Generic sweep over the stock options windows.
|
||||||
|
--
|
||||||
|
-- ElvUI skins those windows from four hardcoded name lists (checkboxes,
|
||||||
|
-- sliders, buttons, dropdowns) in Modules/Skins/Blizzard/BlizzardOptions.lua,
|
||||||
|
-- so anything the client added since upstream last synced stays native --
|
||||||
|
-- 53 widgets on this server, 39 of them in four panels upstream never
|
||||||
|
-- enumerated at all (Battlenet, RaidEffects, Stereo, Voice). Rather than
|
||||||
|
-- carry a parallel name list that goes stale the same way, this walks the
|
||||||
|
-- panel containers and skins by object type.
|
||||||
|
|
||||||
|
local MAX_DEPTH = 3
|
||||||
|
|
||||||
|
-- Every panel is parented at load (probed: the Interface container reports the
|
||||||
|
-- same child count before and after opening the window), so the containers can
|
||||||
|
-- be walked without waiting for a category to be displayed.
|
||||||
|
local ROOTS = {
|
||||||
|
"InterfaceOptionsFramePanelContainer",
|
||||||
|
"VideoOptionsFramePanelContainer",
|
||||||
|
"AudioOptionsFramePanelContainer",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Hooked on all three windows rather than only the Interface one: the Video and
|
||||||
|
-- Audio windows are separate frames reachable straight from the game menu, and
|
||||||
|
-- a player who opens one of those first would otherwise see it unskinned. Each
|
||||||
|
-- show sweeps every root, which costs nothing -- the pass is idempotent.
|
||||||
|
local TRIGGERS = {
|
||||||
|
"InterfaceOptionsFrame",
|
||||||
|
"VideoOptionsFrame",
|
||||||
|
"AudioOptionsFrame",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Panels owned by other addons, keyed by frame. Rebuilt on every sweep rather
|
||||||
|
-- than cached at init: load-on-demand addons register their category late, and
|
||||||
|
-- a set built once would let their panels through on later shows.
|
||||||
|
local addonPanels = {}
|
||||||
|
|
||||||
|
local function RebuildAddOnPanels()
|
||||||
|
for panel in pairs(addonPanels) do
|
||||||
|
addonPanels[panel] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local categories = _G.INTERFACEOPTIONS_ADDONCATEGORIES
|
||||||
|
if type(categories) ~= "table" then return end
|
||||||
|
|
||||||
|
-- The entries are the panel frames themselves, not descriptors (probed).
|
||||||
|
-- Matching on the frame is why this doesn't need a name prefix whitelist:
|
||||||
|
-- an addon can name its panel anything, and the registry is authoritative.
|
||||||
|
for _, panel in ipairs(categories) do
|
||||||
|
if type(panel) == "table" then
|
||||||
|
addonPanels[panel] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Structural test, not a name test. S:HandleDropDownBox uses _G[name.."Button"]
|
||||||
|
-- unguarded when anchoring its backdrop, so a false positive is a Lua error
|
||||||
|
-- rather than a cosmetic miss; it also force-sets the frame's width, which
|
||||||
|
-- mangles a group box even when it doesn't error. A DropDown name suffix was
|
||||||
|
-- considered and rejected -- ElvUI's own dropdown list carries
|
||||||
|
-- CompactUnitFrameProfilesProfileSelector (no suffix) and ...SortByDropdown
|
||||||
|
-- (lowercase d), so suffix matching misses real dropdowns and adds nothing.
|
||||||
|
local function IsDropDown(frame)
|
||||||
|
local name = frame.GetName and frame:GetName()
|
||||||
|
if not name then return false end
|
||||||
|
|
||||||
|
local middle = _G[name.."Middle"]
|
||||||
|
if not middle or not middle.IsObjectType or not middle:IsObjectType("Texture") then return false end
|
||||||
|
|
||||||
|
return _G[name.."Button"] ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- depth is the depth of the children being visited, counting the container's
|
||||||
|
-- own children as 1. Three is enough for the deepest real case: the Controls
|
||||||
|
-- panel's anonymous "Looting Options" group box and the compact raid frame
|
||||||
|
-- profile dialogs both hold their widgets a level below the panel.
|
||||||
|
local function Sweep(frame, depth)
|
||||||
|
for i = 1, frame:GetNumChildren() do
|
||||||
|
local child = select(i, frame:GetChildren())
|
||||||
|
|
||||||
|
if child and not addonPanels[child] then
|
||||||
|
local objectType = child:GetObjectType()
|
||||||
|
|
||||||
|
if objectType == "CheckButton" then
|
||||||
|
S:HandleCheckBox(child)
|
||||||
|
elseif objectType == "Slider" then
|
||||||
|
-- The gate is load-bearing, not an optimisation:
|
||||||
|
-- HandleSliderFrame has no idempotence guard of its own and
|
||||||
|
-- HookScript stacks, so a second pass over a slider ElvUI
|
||||||
|
-- already skinned would run its OnDisable/OnEnable handlers
|
||||||
|
-- twice. E:SetTemplate sets frame.template, so that field is
|
||||||
|
-- the "already skinned" marker for sliders.
|
||||||
|
if not child.template then
|
||||||
|
S:HandleSliderFrame(child)
|
||||||
|
end
|
||||||
|
elseif objectType == "Frame" and IsDropDown(child) then
|
||||||
|
S:HandleDropDownBox(child)
|
||||||
|
elseif depth < MAX_DEPTH then
|
||||||
|
-- Deliberately no Button branch: nothing in the sweep needs one,
|
||||||
|
-- and it would flatten other addons' panel tabs -- object type
|
||||||
|
-- Button, but tabs -- into ElvUI pill buttons.
|
||||||
|
Sweep(child, depth + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SweepOptions()
|
||||||
|
RebuildAddOnPanels()
|
||||||
|
|
||||||
|
for _, name in ipairs(ROOTS) do
|
||||||
|
local root = _G[name]
|
||||||
|
|
||||||
|
if root then
|
||||||
|
Sweep(root, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function CoA:InitializeInterfaceOptions()
|
||||||
|
-- The same two switches that gate ElvUI's own pass: if the user turned the
|
||||||
|
-- stock options skin off, the gap this fills isn't a gap any more.
|
||||||
|
if not E.private.skins.blizzard.enable then return end
|
||||||
|
if not E.private.skins.blizzard.BlizzardOptions then return end
|
||||||
|
|
||||||
|
-- Skinning on show rather than at init puts the sweep after ElvUI's
|
||||||
|
-- Skin_BlizzardOptions callback with no ordering work, and re-running it on
|
||||||
|
-- every show catches panels that only appear once their addon loads.
|
||||||
|
for _, name in ipairs(TRIGGERS) do
|
||||||
|
local frame = _G[name]
|
||||||
|
|
||||||
|
if frame then
|
||||||
|
frame:HookScript("OnShow", SweepOptions)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+77
-20
@@ -1,12 +1,46 @@
|
|||||||
local E, L, V, P, G = unpack(ElvUI)
|
local E, L, V, P, G = unpack(ElvUI)
|
||||||
|
local LSM = E.Libs.LSM
|
||||||
local CoA = E:GetModule("CoA")
|
local CoA = E:GetModule("CoA")
|
||||||
|
|
||||||
local BUTTON_NAME = "LayerPickerFrame"
|
local BUTTON_NAME = "LayerPickerFrame"
|
||||||
|
local MIN_WIDTH, MIN_HEIGHT = 66, 14
|
||||||
|
local PAD_X, PAD_Y = 10, 10
|
||||||
|
|
||||||
|
local function UpdateFont(button)
|
||||||
|
button = button or _G[BUTTON_NAME]
|
||||||
|
local text = button and _G[button:GetName().."Text"]
|
||||||
|
if not text then return end
|
||||||
|
|
||||||
|
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),
|
||||||
|
math.max(MIN_HEIGHT, text:GetStringHeight() + PAD_Y)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SyncHolderSize(button)
|
||||||
|
local holder = _G["CoA_LayerPickerHolder"]
|
||||||
|
button = button or _G[BUTTON_NAME]
|
||||||
|
if holder and button then
|
||||||
|
holder:Size(button:GetSize())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function CoA:UpdateInstanceButtonFont()
|
||||||
|
UpdateFont()
|
||||||
|
SyncHolderSize()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hooked at file scope, so unlike the rest of the skin it stays live even when
|
||||||
|
-- the skin is off -- check the toggle here instead. Reset Position only makes
|
||||||
|
-- sense while the frame is where the server put it; once we've handed it to a
|
||||||
|
-- mover the entry does nothing useful.
|
||||||
do
|
do
|
||||||
local orig_AddButton = UIDropDownMenu_AddButton
|
local orig_AddButton = UIDropDownMenu_AddButton
|
||||||
UIDropDownMenu_AddButton = function(info, level)
|
UIDropDownMenu_AddButton = function(info, level)
|
||||||
if info and info.text == "Reset Position" and UIDROPDOWNMENU_INIT_MENU == LayerPickerFrameDropDown then
|
if CoA.db and CoA.db.profile.skins.instanceSwap
|
||||||
|
and info and info.text == "Reset Position" and UIDROPDOWNMENU_INIT_MENU == LayerPickerFrameDropDown then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -14,32 +48,56 @@ do
|
|||||||
end
|
end
|
||||||
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)
|
local function DisableDrag(button)
|
||||||
if button.CoADragDisabled then return end
|
if button.CoADragDisabled then return end
|
||||||
button.CoADragDisabled = true
|
button.CoADragDisabled = true
|
||||||
|
|
||||||
button:SetScript("OnDragStart", nil)
|
button:SetScript("OnDragStart", nil)
|
||||||
button:SetScript("OnDragStop", 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
|
end
|
||||||
|
|
||||||
local function SetupMover(button)
|
-- Created eagerly (independent of the native LayerPickerFrame ever showing
|
||||||
|
-- up) so the mover always appears in Toggle Anchors. The default point below
|
||||||
|
-- is only used until the player drags it once; after that E:CreateMover
|
||||||
|
-- restores the saved position from E.db.movers.
|
||||||
|
local function CreateMoverHolder()
|
||||||
if CoA.layerPickerMoverCreated then return end
|
if CoA.layerPickerMoverCreated then return end
|
||||||
|
|
||||||
local width, height = button:GetSize()
|
|
||||||
if width == 0 or height == 0 then return end
|
|
||||||
|
|
||||||
local left, bottom = button:GetLeft(), button:GetBottom()
|
|
||||||
if not left or not bottom then return end
|
|
||||||
|
|
||||||
CoA.layerPickerMoverCreated = true
|
CoA.layerPickerMoverCreated = true
|
||||||
|
|
||||||
local holder = CreateFrame("Frame", "CoA_LayerPickerHolder", E.UIParent)
|
local holder = CreateFrame("Frame", "CoA_LayerPickerHolder", E.UIParent)
|
||||||
holder:Size(width, height)
|
holder:Size(MIN_WIDTH, MIN_HEIGHT)
|
||||||
holder:Point("BOTTOMLEFT", E.UIParent, "BOTTOMLEFT", left, bottom)
|
holder:Point("TOPLEFT", E.UIParent, "TOPLEFT", 250, -250)
|
||||||
|
|
||||||
E:CreateMover(holder, "CoA_LayerPickerMover", "Instance", nil, nil, nil, nil, nil, "CoA,skin,instance")
|
E:CreateMover(holder, "CoA_LayerPickerMover", "Instance", nil, nil, nil, "ALL,COA", nil, "CoA,skins,instanceSwap")
|
||||||
holder:SetAllPoints(_G["CoA_LayerPickerMover"])
|
holder:SetAllPoints(_G["CoA_LayerPickerMover"])
|
||||||
_G["CoA_LayerPickerMover"]:SetFrameStrata("FULLSCREEN")
|
_G["CoA_LayerPickerMover"]:SetFrameStrata("FULLSCREEN")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function AnchorButton(button)
|
||||||
|
local holder = _G["CoA_LayerPickerHolder"]
|
||||||
|
if not holder then return end
|
||||||
|
|
||||||
local function Anchor()
|
local function Anchor()
|
||||||
button:ClearAllPoints()
|
button:ClearAllPoints()
|
||||||
@@ -60,16 +118,11 @@ local function SkinButton(button)
|
|||||||
if button.CoASkinned then return end
|
if button.CoASkinned then return end
|
||||||
button.CoASkinned = true
|
button.CoASkinned = true
|
||||||
|
|
||||||
local name = button:GetName()
|
|
||||||
local text = _G[name.."Text"]
|
|
||||||
|
|
||||||
button:StripTextures(true)
|
button:StripTextures(true)
|
||||||
button:CreateBackdrop("Default")
|
button:CreateBackdrop("Default")
|
||||||
button:StyleButton()
|
button:StyleButton()
|
||||||
|
|
||||||
if text then
|
UpdateFont(button)
|
||||||
text:FontTemplate()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function TryHook()
|
local function TryHook()
|
||||||
@@ -77,14 +130,18 @@ local function TryHook()
|
|||||||
|
|
||||||
if button then
|
if button then
|
||||||
DisableDrag(button)
|
DisableDrag(button)
|
||||||
SetupMover(button)
|
|
||||||
SkinButton(button)
|
SkinButton(button)
|
||||||
|
SyncHolderSize(button)
|
||||||
|
AnchorButton(button)
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
return button ~= nil
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function CoA:InitializeLayerPicker()
|
function CoA:InitializeLayerPicker()
|
||||||
|
CreateMoverHolder()
|
||||||
|
|
||||||
if TryHook() then return end
|
if TryHook() then return end
|
||||||
|
|
||||||
self.layerPickerTimer = self:ScheduleRepeatingTimer(function()
|
self.layerPickerTimer = self:ScheduleRepeatingTimer(function()
|
||||||
|
|||||||
@@ -0,0 +1,834 @@
|
|||||||
|
local E, L, V, P, G = unpack(ElvUI)
|
||||||
|
local S = E:GetModule("Skins")
|
||||||
|
local CoA = E:GetModule("CoA")
|
||||||
|
|
||||||
|
-- Shared skinning helpers for the CoA frames (talent, vanity, wardrobe).
|
||||||
|
--
|
||||||
|
-- Those three are built from the same handful of widget templates, so every
|
||||||
|
-- element that appears in more than one of them lives here rather than being
|
||||||
|
-- copied per module. The copies had already drifted -- different backdrop
|
||||||
|
-- insets, different close button sizes, tabs grown in one frame and not the
|
||||||
|
-- other -- and the drift is exactly what reads as "the same control looks
|
||||||
|
-- different depending on which tab I'm on".
|
||||||
|
local Skin = {}
|
||||||
|
CoA.Skin = Skin
|
||||||
|
|
||||||
|
-- Every native close button on these frames is a different size (the vanity
|
||||||
|
-- store's is visibly larger than the talent frame's), and HandleCloseButton
|
||||||
|
-- keeps whatever size it's given -- it only centres a fixed 12px X inside the
|
||||||
|
-- box. Normalised here so the X lands on the same grid in all three, and the
|
||||||
|
-- click target is the same everywhere too.
|
||||||
|
local CLOSE_BUTTON_SIZE = 32
|
||||||
|
|
||||||
|
function Skin:CloseButton(close)
|
||||||
|
if not close then return end
|
||||||
|
|
||||||
|
-- Strictly once: HandleCloseButton strips the button on every call, which
|
||||||
|
-- blanks the X texture it added on the first pass, and its own guard won't
|
||||||
|
-- rebuild it because the field still points at that blanked texture.
|
||||||
|
if not close.CoASkinned then
|
||||||
|
close.CoASkinned = true
|
||||||
|
|
||||||
|
S:HandleCloseButton(close)
|
||||||
|
end
|
||||||
|
|
||||||
|
close:Size(CLOSE_BUTTON_SIZE)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Scale is a per-frame setting rather than one shared number: the three frames
|
||||||
|
-- don't ship at the same scale (the vanity store's is about 9% larger than the
|
||||||
|
-- other two, measured), and that's the server's choice, so the slider is a
|
||||||
|
-- multiplier on whatever each frame was given rather than an absolute. A
|
||||||
|
-- setting of 1 therefore leaves every frame exactly as it came.
|
||||||
|
--
|
||||||
|
-- Scale is applied to Collections, the container all three windows and the tab
|
||||||
|
-- row are children of (confirmed by probe), rather than to a window itself.
|
||||||
|
-- Collections is also the only one of them with mouse enabled -- it's what the
|
||||||
|
-- player drags -- so scaling a window directly shrank the art while leaving the
|
||||||
|
-- drag target at full size, which is why a shrunk window had to be grabbed by
|
||||||
|
-- clicking outside itself. Scaling the container moves its hit area with it,
|
||||||
|
-- and the tabs come along as its children.
|
||||||
|
--
|
||||||
|
-- The setting is still per-window: only one of them is ever open, and each
|
||||||
|
-- re-applies its own on show. The native scale is captured before anything here
|
||||||
|
-- has written one, so re-applying can't compound.
|
||||||
|
local CONTAINER_NAME = "Collections"
|
||||||
|
|
||||||
|
local scaleKey
|
||||||
|
|
||||||
|
local function GetScaleSetting(key)
|
||||||
|
local db = CoA.db and CoA.db.profile.skins.collections
|
||||||
|
|
||||||
|
return (db and db[key]) or 1
|
||||||
|
end
|
||||||
|
|
||||||
|
function Skin:ApplyWindowScale(key)
|
||||||
|
local container = _G[CONTAINER_NAME]
|
||||||
|
if not container then return end
|
||||||
|
|
||||||
|
if not container.CoANativeScale then
|
||||||
|
container.CoANativeScale = container:GetScale()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remembered so the slider can re-apply for whichever window is open.
|
||||||
|
scaleKey = key
|
||||||
|
|
||||||
|
local scale = container.CoANativeScale * GetScaleSetting(key)
|
||||||
|
|
||||||
|
if container:GetScale() ~= scale then
|
||||||
|
container:SetScale(scale)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Live update from the slider: a window only re-runs its own skin pass on show,
|
||||||
|
-- and a scale change should land while one is open.
|
||||||
|
function CoA:UpdateFrameScales()
|
||||||
|
if scaleKey then
|
||||||
|
Skin:ApplyWindowScale(scaleKey)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The talent and wardrobe frames title in Arial Narrow, the vanity store in
|
||||||
|
-- Friz Quadrata (measured live). Arial Narrow is the one that matches the rest
|
||||||
|
-- of the layout, so it's pinned here rather than left to the frames -- and
|
||||||
|
-- pinned by name through LSM rather than as a raw path, so it stays the same
|
||||||
|
-- asset ElvUI itself would resolve.
|
||||||
|
--
|
||||||
|
-- Size is written straight through, with no correction for the frames running
|
||||||
|
-- at different scales: the vanity store's is about 9% larger than the other
|
||||||
|
-- two (measured), so its title renders slightly bigger than theirs. Left as the
|
||||||
|
-- server has it for now.
|
||||||
|
--
|
||||||
|
-- FontTemplate stashes the font and size it was handed on the fontstring, so an
|
||||||
|
-- ElvUI font change later re-applies these rather than resetting them.
|
||||||
|
local TITLE_FONT = "Arial Narrow"
|
||||||
|
local TITLE_SIZE = 13
|
||||||
|
|
||||||
|
function Skin:Title(title)
|
||||||
|
if not title then return end
|
||||||
|
|
||||||
|
title:FontTemplate(E.Libs.LSM:Fetch("font", TITLE_FONT), TITLE_SIZE, "OUTLINE")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Strip first, THEN template: SetTemplate adds its own backdrop as texture
|
||||||
|
-- regions on this same frame, so stripping afterwards wipes it straight back
|
||||||
|
-- off -- which is how the wardrobe's outer panel once came out fully invisible.
|
||||||
|
--
|
||||||
|
-- keepTextures is for frames whose own regions aren't all decorative: the
|
||||||
|
-- vanity store draws its currency counters as regions of the frame itself, and
|
||||||
|
-- a blind strip blanks the counters along with the panel art.
|
||||||
|
function Skin:Panel(frame, keepTextures)
|
||||||
|
if not frame or frame.CoAPanelSkinned then return end
|
||||||
|
frame.CoAPanelSkinned = true
|
||||||
|
|
||||||
|
if not keepTextures then
|
||||||
|
frame:StripTextures()
|
||||||
|
end
|
||||||
|
|
||||||
|
frame:SetTemplate("Transparent")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ornate art with no flat equivalent (portrait medallions, nine-slice borders,
|
||||||
|
-- shadow overlays): it goes rather than getting reskinned. Hidden as well as
|
||||||
|
-- stripped, since some of these put their own art back.
|
||||||
|
function Skin:HideArt(frame)
|
||||||
|
if not frame then return end
|
||||||
|
|
||||||
|
frame:StripTextures()
|
||||||
|
frame:Hide()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Buttons ---------------------------------------------------------------------
|
||||||
|
--
|
||||||
|
-- IsEnabled hands back 0 and 1 on this client rather than false and true
|
||||||
|
-- (probed), and 0 is truthy in Lua, so every enabled check has to normalise
|
||||||
|
-- what it gets back before comparing it.
|
||||||
|
function Skin:IsEnabled(button)
|
||||||
|
local state = button.IsEnabled and button:IsEnabled()
|
||||||
|
|
||||||
|
return state ~= nil and state ~= false and state ~= 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The target is resolved the way ElvUI's own SetModifiedBackdrop and
|
||||||
|
-- SetOriginalBackdrop resolve it -- the backdrop when the button has one, the
|
||||||
|
-- button itself otherwise -- or the colour lands on something that isn't the
|
||||||
|
-- object drawing the border.
|
||||||
|
local function SetButtonBorder(button, lit)
|
||||||
|
local backdrop = button.backdrop or button
|
||||||
|
if not backdrop.SetBackdropBorderColor then return end
|
||||||
|
|
||||||
|
backdrop:SetBackdropBorderColor(unpack(lit and E.media.rgbvaluecolor or E.media.bordercolor))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- S:HandleButton ends with an unconditional pair of OnEnter/OnLeave hooks that
|
||||||
|
-- swap the border to the value colour and back, and neither of them looks at
|
||||||
|
-- IsEnabled. A disabled button still fires both scripts on this client, so a
|
||||||
|
-- dead button lights up under the cursor and reads as clickable -- the greyed
|
||||||
|
-- Activate button, Save changes with nothing pending, Purchase with nothing
|
||||||
|
-- selected. HookScript can't be undone, but handlers fire in registration
|
||||||
|
-- order, so a hook registered after HandleButton runs last and has the final
|
||||||
|
-- say on the colour.
|
||||||
|
--
|
||||||
|
-- This one deliberately doesn't consult IsMouseOver: OnEnter can fire a pixel
|
||||||
|
-- before IsMouseOver turns true (ElvUI's own aura code pads around the same
|
||||||
|
-- quirk), and reading it here would drop the highlight off a live button.
|
||||||
|
local function OnButtonEnter(button)
|
||||||
|
if Skin:IsEnabled(button) then return end
|
||||||
|
|
||||||
|
SetButtonBorder(button, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- For a button that can be disabled while the cursor is already on it: OnEnter
|
||||||
|
-- has been and gone by then, so nothing corrects the border until the pointer
|
||||||
|
-- leaves. Only worth wiring up where something already watches the button's
|
||||||
|
-- state -- it does not justify an OnUpdate of its own.
|
||||||
|
function Skin:RefreshButtonBorder(button)
|
||||||
|
SetButtonBorder(button, self:IsEnabled(button) and button:IsMouseOver())
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Every S:HandleButton call site in the plugin goes through here, so the
|
||||||
|
-- disabled-hover correction is inherited rather than repeated per module. The
|
||||||
|
-- extra arguments are HandleButton's own (strip, isDeclineButton,
|
||||||
|
-- useCreateBackdrop, noSetTemplate) and are passed straight through.
|
||||||
|
function Skin:Button(button, ...)
|
||||||
|
if not button then return end
|
||||||
|
|
||||||
|
S:HandleButton(button, ...)
|
||||||
|
|
||||||
|
-- Unguarded: a pass over a handful of regions is cheap, and re-running it
|
||||||
|
-- catches a region added to the button after the first skin pass.
|
||||||
|
self:StripHighlightArt(button)
|
||||||
|
|
||||||
|
-- Guarded separately from HandleButton's own isSkinned flag: these frames
|
||||||
|
-- re-run their skin pass on every show, and HookScript stacks handlers
|
||||||
|
-- rather than replacing them, so an unguarded re-hook adds another copy
|
||||||
|
-- per show for the life of the session.
|
||||||
|
if not button.CoAButtonSkinned then
|
||||||
|
button.CoAButtonSkinned = true
|
||||||
|
|
||||||
|
button:HookScript("OnEnter", OnButtonEnter)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- S:HandleNextPrevButton routes through S:HandleButton, so it inherits exactly
|
||||||
|
-- the lit-border-on-a-dead-button behaviour OnButtonEnter is here to correct,
|
||||||
|
-- and scroll arrows spend a good deal of their life disabled at one end of their
|
||||||
|
-- list or the other. Same separate guard as Skin:Button, and for the same
|
||||||
|
-- reason: these frames re-run their skin passes, and HookScript stacks.
|
||||||
|
function Skin:NextPrevButton(button, direction)
|
||||||
|
if not button then return end
|
||||||
|
|
||||||
|
S:HandleNextPrevButton(button, direction)
|
||||||
|
|
||||||
|
if not button.CoAButtonSkinned then
|
||||||
|
button.CoAButtonSkinned = true
|
||||||
|
|
||||||
|
button:HookScript("OnEnter", OnButtonEnter)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Several of these controls draw their art as anonymous regions of one atlas
|
||||||
|
-- file rather than through the named Left/Middle/Right fields or the Normal/
|
||||||
|
-- Pushed/Disabled set S:HandleButton knows how to clear, so its own clearing
|
||||||
|
-- can't reach them -- and a blind StripTextures would take the caret or the
|
||||||
|
-- label with it. Those are cleared by file instead.
|
||||||
|
--
|
||||||
|
-- SetTexture is noop'd per region rather than just cleared: whatever re-arts
|
||||||
|
-- the control on a state change (a mouse-down swapping in the pressed variant,
|
||||||
|
-- a tex coord swap between two variants of the same file) is free to re-set the
|
||||||
|
-- file as well, and a cleared texture would come straight back.
|
||||||
|
function Skin:StripArtByFile(frame, pattern)
|
||||||
|
if not frame then return end
|
||||||
|
|
||||||
|
for i = 1, frame:GetNumRegions() do
|
||||||
|
local region = select(i, frame:GetRegions())
|
||||||
|
local texture = region.GetTexture and region:GetTexture()
|
||||||
|
|
||||||
|
if texture and tostring(texture):find(pattern) then
|
||||||
|
region:SetTexture(nil)
|
||||||
|
region.SetTexture = E.noop
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- S:HandleButton clears the button's own Normal/Highlight/Pushed/Disabled
|
||||||
|
-- textures and the named Left/Middle/Right pieces, and nothing else -- so a
|
||||||
|
-- native highlight drawn as a plain region of the button survives it and lights
|
||||||
|
-- up under the cursor against the flat ElvUI backdrop. That was the wardrobe
|
||||||
|
-- Cancel button's red glow, and clearing it by file there fixed exactly that
|
||||||
|
-- one button: probing the layer across everything the plugin skins turned up
|
||||||
|
-- the Save outfit button wearing the same red file, and Disable transmog and
|
||||||
|
-- Disable spell visuals wearing the dialog-box glow, none of which any
|
||||||
|
-- clear-by-file call site was ever going to reach.
|
||||||
|
--
|
||||||
|
-- The whole layer goes rather than named files, because on these frames it only
|
||||||
|
-- ever carries native hover art. Every button probed has exactly one HIGHLIGHT
|
||||||
|
-- region, and on the ones that already looked right it is blank -- so there is
|
||||||
|
-- nothing else living there to lose. ElvUI's own hover treatment isn't caught:
|
||||||
|
-- it's the backdrop border swap HandleButton hooks, and a backdrop is drawn
|
||||||
|
-- through SetBackdrop plus two child border frames, not as a region of the
|
||||||
|
-- button at all.
|
||||||
|
--
|
||||||
|
-- Noop'd rather than only cleared, as in StripArtByFile: a state change that
|
||||||
|
-- re-arts the button would otherwise bring the glow straight back.
|
||||||
|
function Skin:StripHighlightArt(frame)
|
||||||
|
if not frame then return end
|
||||||
|
|
||||||
|
for i = 1, frame:GetNumRegions() do
|
||||||
|
local region = select(i, frame:GetRegions())
|
||||||
|
|
||||||
|
if region:GetObjectType() == "Texture" and region:GetDrawLayer() == "HIGHLIGHT" then
|
||||||
|
region:SetTexture(nil)
|
||||||
|
region.SetTexture = E.noop
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The pill behind the talent frame's Activate button and the wardrobe's Apply
|
||||||
|
-- and Cancel buttons. Two files rather than one: "128GoldRedButton" carries the
|
||||||
|
-- green and grey variants as tex coords, and Cancel alone is drawn from
|
||||||
|
-- "128RedButton" (probed). The shared suffix matches both, and nothing else on
|
||||||
|
-- these frames uses either. Only the pill itself is this pattern's job now --
|
||||||
|
-- the hover art on both files goes with the rest of the HIGHLIGHT layer.
|
||||||
|
Skin.RedButtonArt = "RedButton"
|
||||||
|
|
||||||
|
-- The dropdown pills on the vanity and wardrobe frames are the same widget:
|
||||||
|
-- nine anonymous "Silver-Button" slices.
|
||||||
|
local DROPDOWN_ART = "Silver%-Button"
|
||||||
|
local CARET_ART = "ChatFrameExpandArrow"
|
||||||
|
local CARET_SIZE = 14
|
||||||
|
|
||||||
|
-- The caret is retextured after HandleButton, not before: HandleButton strips
|
||||||
|
-- the pill, and a caret replaced ahead of that gets cleared straight back off.
|
||||||
|
local function SkinDropdownCaret(dropdown)
|
||||||
|
for i = 1, dropdown:GetNumRegions() do
|
||||||
|
local region = select(i, dropdown:GetRegions())
|
||||||
|
local texture = region.GetTexture and region:GetTexture()
|
||||||
|
|
||||||
|
if texture and tostring(texture):find(CARET_ART) then
|
||||||
|
-- The caret is a plain OVERLAY texture on the dropdown itself rather
|
||||||
|
-- than a separate button, so it can't go through
|
||||||
|
-- HandleNextPrevButton and is retextured directly. No ArrowDown
|
||||||
|
-- asset exists -- every other direction in ElvUI is ArrowUp rotated.
|
||||||
|
region:SetTexture(E.Media.Textures.ArrowUp)
|
||||||
|
region:SetVertexColor(1, 1, 1)
|
||||||
|
region:SetTexCoord(0, 1, 0, 1)
|
||||||
|
region:SetRotation(S.ArrowRotation.down)
|
||||||
|
region:SetSize(CARET_SIZE, CARET_SIZE)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- menu is the popout list this dropdown owns; it gets the panel only, matching
|
||||||
|
-- how the talent frame's own popup menus are treated. The option rows inside
|
||||||
|
-- are a later pass.
|
||||||
|
function Skin:Dropdown(dropdown, menu)
|
||||||
|
if not dropdown or dropdown.CoASkinned then return end
|
||||||
|
dropdown.CoASkinned = true
|
||||||
|
|
||||||
|
self:StripArtByFile(dropdown, DROPDOWN_ART)
|
||||||
|
self:Button(dropdown)
|
||||||
|
SkinDropdownCaret(dropdown)
|
||||||
|
|
||||||
|
self:Panel(menu)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The CoA search boxes draw their border and magnifier from the stock
|
||||||
|
-- "CommonSearch" file, tinted to the server's own colour, as anonymous regions
|
||||||
|
-- -- so S:HandleEditBox can't reach them: it only alpha-0s the named Left/
|
||||||
|
-- Middle/Right pieces a stock Blizzard edit box has.
|
||||||
|
--
|
||||||
|
-- Cleared by file rather than by a blind strip, which is what a first pass did:
|
||||||
|
-- an EditBox draws its own caret and its selection highlight as plain solid
|
||||||
|
-- textures of its own (probed: four of them, sub-pixel wide and text-high, shown
|
||||||
|
-- only while the box has focus), and StripTextures takes those with the border,
|
||||||
|
-- leaving a search box you can type in with no cursor in it.
|
||||||
|
local SEARCH_BOX_ART = "CommonSearch"
|
||||||
|
|
||||||
|
function Skin:SearchBox(box)
|
||||||
|
if not box or box.CoASearchSkinned then return end
|
||||||
|
box.CoASearchSkinned = true
|
||||||
|
|
||||||
|
self:StripArtByFile(box, SEARCH_BOX_ART)
|
||||||
|
S:HandleEditBox(box)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Scroll bars ------------------------------------------------------------------
|
||||||
|
--
|
||||||
|
-- S:HandleScrollBar can't be used on these: the CoA bars are hand-built rather
|
||||||
|
-- than the stock Slider widget, so the thumb is a Button with art of its own
|
||||||
|
-- (probed) where a stock bar has a thumb *texture*, and HandleScrollBar reaches
|
||||||
|
-- for the thumb through GetThumbTexture and then calls SetTexture on what it
|
||||||
|
-- finds. It also looks the arrow buttons up as children of the bar, and these
|
||||||
|
-- are named off the scroll frame instead. The treatment below is the same one it
|
||||||
|
-- applies, only with each piece handed in by the caller.
|
||||||
|
--
|
||||||
|
-- The art on both pieces is applied lazily, and that is what defeated two
|
||||||
|
-- earlier passes at this: at skin time the thumb's textures read back nil, so
|
||||||
|
-- matching them by file found nothing to clear, and the scroll code arts them a
|
||||||
|
-- moment later when it first measures the list. Clearing, noop'ing the regions'
|
||||||
|
-- SetTexture, and alpha'ing them all failed the same way -- not because the art
|
||||||
|
-- came back, but because it was never there yet to be taken.
|
||||||
|
--
|
||||||
|
-- So it's handled from OnUpdate, the same answer the tab art needed and for the
|
||||||
|
-- same reason: nothing fires when it happens, and the check is a three-region
|
||||||
|
-- alpha compare that only writes when something has actually re-arted the
|
||||||
|
-- widget. Blanket rather than matched by file: neither the track nor the thumb
|
||||||
|
-- carries anything but its own art -- no label, no counter -- which is the only
|
||||||
|
-- thing StripArtByFile's file matching is there to protect.
|
||||||
|
local function HideScrollArt(frame)
|
||||||
|
for i = 1, frame:GetNumRegions() do
|
||||||
|
local region = select(i, frame:GetRegions())
|
||||||
|
|
||||||
|
if region:GetObjectType() == "Texture" and region:GetAlpha() ~= 0 then
|
||||||
|
region:SetAlpha(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Colours and states taken from ElvUI's own proportional scroll bars
|
||||||
|
-- (S:HandleProportionalScroll), which is what every other scroll bar in this UI
|
||||||
|
-- looks like: the value colour at a quarter alpha, three quarters while hovered
|
||||||
|
-- or held. Its handlers are locals in ElvUI's file, so the four scripts are
|
||||||
|
-- written out here rather than reached for.
|
||||||
|
--
|
||||||
|
-- The active flag is what keeps a thumb lit while it's being dragged: the
|
||||||
|
-- pointer routinely leaves a thin thumb mid-drag, and OnLeave would otherwise
|
||||||
|
-- dim it while the list is still moving under it.
|
||||||
|
local SCROLL_BUTTON_GAP = 1
|
||||||
|
local SCROLL_THUMB_ALPHA = 0.25
|
||||||
|
local SCROLL_THUMB_ACTIVE_ALPHA = 0.75
|
||||||
|
|
||||||
|
local function SetThumbColor(thumb, alpha)
|
||||||
|
if not thumb.backdrop then return end
|
||||||
|
|
||||||
|
local r, g, b = unpack(E.media.rgbvaluecolor)
|
||||||
|
thumb.backdrop:SetBackdropColor(r, g, b, alpha)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ThumbOnEnter(thumb)
|
||||||
|
SetThumbColor(thumb, SCROLL_THUMB_ACTIVE_ALPHA)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ThumbOnLeave(thumb)
|
||||||
|
if thumb.CoAThumbActive then return end
|
||||||
|
|
||||||
|
SetThumbColor(thumb, SCROLL_THUMB_ALPHA)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ThumbOnMouseDown(thumb)
|
||||||
|
thumb.CoAThumbActive = true
|
||||||
|
|
||||||
|
SetThumbColor(thumb, SCROLL_THUMB_ACTIVE_ALPHA)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ThumbOnMouseUp(thumb)
|
||||||
|
thumb.CoAThumbActive = nil
|
||||||
|
|
||||||
|
SetThumbColor(thumb, SCROLL_THUMB_ALPHA)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The arrows re-art themselves the same way the track and thumb do, and one
|
||||||
|
-- pass can't hold against it: S:HandleNextPrevButton guards on its own
|
||||||
|
-- isSkinned flag, so calling it a second time does nothing at all. What comes
|
||||||
|
-- back isn't the ElvUI arrow being overwritten either -- that texture is still
|
||||||
|
-- in place underneath (probed: the native "scrollbarproportional" region shown
|
||||||
|
-- alongside ArrowUp). It's the button's own art, which StripTextures only hid,
|
||||||
|
-- and the scroll frame shows it again whenever it recalculates.
|
||||||
|
--
|
||||||
|
-- So the native art is re-hidden by file from OnUpdate, the same answer the
|
||||||
|
-- track, the thumb and the tab art all needed and for the same reason: nothing
|
||||||
|
-- fires when it happens. The three state textures are captured and their
|
||||||
|
-- setters noop'd as well -- still worth closing off, since whatever refreshes
|
||||||
|
-- these is free to swap in a fresh texture object and orphan the three.
|
||||||
|
--
|
||||||
|
-- Matched by file rather than as "any region that isn't one of ours": ElvUI's
|
||||||
|
-- own panel and border pieces are regions of the button too, so hiding
|
||||||
|
-- everything unrecognised would take the ElvUI square with it. Compared
|
||||||
|
-- case-insensitively -- the client hands paths back from GetTexture in whatever
|
||||||
|
-- case it stored them, not the case they were set in.
|
||||||
|
local ARROW_TEXTURE = E.Media.Textures.ArrowUp:lower()
|
||||||
|
local NATIVE_SCROLL_ART = "scrollbar"
|
||||||
|
|
||||||
|
local function RestoreArrow(button)
|
||||||
|
for i = 1, button:GetNumRegions() do
|
||||||
|
local region = select(i, button:GetRegions())
|
||||||
|
local texture = region.GetTexture and region:GetTexture()
|
||||||
|
|
||||||
|
if texture and tostring(texture):lower():find(NATIVE_SCROLL_ART) and region:IsShown() then
|
||||||
|
region:Hide()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, texture in ipairs(button.CoAArrowTextures) do
|
||||||
|
local current = texture:GetTexture()
|
||||||
|
|
||||||
|
if not current or tostring(current):lower() ~= ARROW_TEXTURE then
|
||||||
|
texture:SetTexture(E.Media.Textures.ArrowUp)
|
||||||
|
texture:SetInside(button)
|
||||||
|
texture:SetTexCoord(0, 1, 0, 1)
|
||||||
|
texture:SetRotation(S.ArrowRotation[button.CoAArrowDirection])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Skin:ScrollArrow(button, direction)
|
||||||
|
if not button or button.CoAArrowSkinned then return end
|
||||||
|
button.CoAArrowSkinned = true
|
||||||
|
|
||||||
|
self:NextPrevButton(button, direction)
|
||||||
|
|
||||||
|
button.CoAArrowDirection = direction
|
||||||
|
button.CoAArrowTextures = {
|
||||||
|
button:GetNormalTexture(),
|
||||||
|
button:GetPushedTexture(),
|
||||||
|
button:GetDisabledTexture(),
|
||||||
|
}
|
||||||
|
|
||||||
|
button.SetNormalTexture = E.noop
|
||||||
|
button.SetPushedTexture = E.noop
|
||||||
|
button.SetDisabledTexture = E.noop
|
||||||
|
button.SetHighlightTexture = E.noop
|
||||||
|
|
||||||
|
button:HookScript("OnUpdate", RestoreArrow)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Skin:ScrollBar(bar, thumb, up, down)
|
||||||
|
if not bar or bar.CoASkinned then return end
|
||||||
|
bar.CoASkinned = true
|
||||||
|
|
||||||
|
-- The track gets no backdrop of its own, only its art taken off, which is
|
||||||
|
-- what ElvUI's own proportional scroll bars do: against these panels an empty
|
||||||
|
-- channel reads cleaner than a second filled block behind the thumb, and the
|
||||||
|
-- thumb is legible on its own at the value colour.
|
||||||
|
bar:HookScript("OnUpdate", HideScrollArt)
|
||||||
|
|
||||||
|
if thumb then
|
||||||
|
thumb:HookScript("OnUpdate", HideScrollArt)
|
||||||
|
|
||||||
|
-- Filling the thumb rather than inset inside it: the thumb is already
|
||||||
|
-- narrower than the track it runs in, so an inset backdrop reads as a thin
|
||||||
|
-- line rather than as a grip. The track's own gutter is the separation.
|
||||||
|
--
|
||||||
|
-- Levelled off the thumb rather than off the track: the thumb sits a level
|
||||||
|
-- above the track already, so a backdrop built at the track's level would
|
||||||
|
-- land underneath the thumb's own regions.
|
||||||
|
thumb:CreateBackdrop("Transparent")
|
||||||
|
thumb.backdrop:SetFrameLevel(thumb:GetFrameLevel() + 1)
|
||||||
|
thumb.backdrop:SetAllPoints()
|
||||||
|
|
||||||
|
SetThumbColor(thumb, SCROLL_THUMB_ALPHA)
|
||||||
|
|
||||||
|
thumb:HookScript("OnEnter", ThumbOnEnter)
|
||||||
|
thumb:HookScript("OnLeave", ThumbOnLeave)
|
||||||
|
thumb:HookScript("OnMouseDown", ThumbOnMouseDown)
|
||||||
|
thumb:HookScript("OnMouseUp", ThumbOnMouseUp)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Re-anchored onto the track's ends after skinning, and cleared first rather
|
||||||
|
-- than pointed on top of what's there: HandleNextPrevButton resizes them to
|
||||||
|
-- ElvUI's own 18px, so whatever the native layout anchored them by no longer
|
||||||
|
-- lines up with the track, and a second point left in place would stretch the
|
||||||
|
-- button between the two.
|
||||||
|
if up then
|
||||||
|
self:ScrollArrow(up, "up")
|
||||||
|
up:ClearAllPoints()
|
||||||
|
up:Point("BOTTOM", bar, "TOP", 0, SCROLL_BUTTON_GAP)
|
||||||
|
end
|
||||||
|
|
||||||
|
if down then
|
||||||
|
self:ScrollArrow(down, "down")
|
||||||
|
down:ClearAllPoints()
|
||||||
|
down:Point("TOP", bar, "BOTTOM", 0, -SCROLL_BUTTON_GAP)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tabs -----------------------------------------------------------------------
|
||||||
|
--
|
||||||
|
-- S:HandleTab still isn't used on any of these. On the server-authored tabs it
|
||||||
|
-- can't be: it clears the tab body by name, looking for a "Middle" piece, and
|
||||||
|
-- those name theirs "Center", so the body survives and the ElvUI backdrop just
|
||||||
|
-- lands behind the old art. The Challenges window's tabs are stock Blizzard
|
||||||
|
-- ones that do use "Middle" (probed), so HandleTab would reach them -- they go
|
||||||
|
-- through the same strip anyway, because one code path is what keeps every tab
|
||||||
|
-- row in this UI looking alike.
|
||||||
|
--
|
||||||
|
-- One list covers all three tab flavours: the talent frame's tabs carry the
|
||||||
|
-- Disabled variants as well, the wardrobe's category tabs don't, the Challenges
|
||||||
|
-- tabs use Blizzard's Left/Middle/Right plus the Disabled set Blizzard swaps in
|
||||||
|
-- to draw the selected tab, and the lookups for the ones that don't exist
|
||||||
|
-- simply come back nil.
|
||||||
|
local TAB_TEXTURES = {
|
||||||
|
"Left", "Center", "Middle", "Right",
|
||||||
|
"LeftDisabled", "CenterDisabled", "MiddleDisabled", "RightDisabled",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Blizzard's own tab code re-sets these textures both when a frame reopens and,
|
||||||
|
-- separately, on every tab switch -- and a switch never fires the owning
|
||||||
|
-- frame's OnShow, only SetChecked. So this has to be idempotent and re-run from
|
||||||
|
-- everywhere rather than skinned once behind a guard.
|
||||||
|
function Skin:StripTabArt(tab)
|
||||||
|
local name = tab:GetName()
|
||||||
|
|
||||||
|
if name then
|
||||||
|
for _, suffix in ipairs(TAB_TEXTURES) do
|
||||||
|
local tex = _G[name..suffix]
|
||||||
|
if tex then tex:SetTexture(nil) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local highlight = tab.GetHighlightTexture and tab:GetHighlightTexture()
|
||||||
|
if highlight then highlight:SetTexture(nil) end
|
||||||
|
|
||||||
|
local checked = tab.GetCheckedTexture and tab:GetCheckedTexture()
|
||||||
|
if checked then checked:SetTexture(nil) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Exposed: a module that re-anchors a tab row against the frame it belongs to
|
||||||
|
-- has to know how far inside its tab the backdrop actually starts, or it lines
|
||||||
|
-- the tab up flush and leaves the visible edge floating by this much.
|
||||||
|
local TAB_BACKDROP_INSET = 3
|
||||||
|
Skin.TabBackdropInset = TAB_BACKDROP_INSET
|
||||||
|
|
||||||
|
-- Native tab sizing is a retail leftover -- every other ElvUI tab row in this
|
||||||
|
-- client reads bigger -- so labelled tabs are grown, off the font's own current
|
||||||
|
-- size rather than a hardcoded number, and stay in step with the user's font
|
||||||
|
-- settings. Icon-only tabs (the wardrobe's category strip) have no label to
|
||||||
|
-- grow around and keep their native size; growing them would only push the
|
||||||
|
-- icons off their own layout.
|
||||||
|
local TAB_GROWTH = 8
|
||||||
|
local TAB_FONT_GROWTH = 3
|
||||||
|
local TAB_LABEL_OFFSET = 12
|
||||||
|
|
||||||
|
-- Grown height self-heals: on a plain /reload a tab's native height isn't
|
||||||
|
-- settled yet at skin time (Blizzard lays it out asynchronously), so a one-shot
|
||||||
|
-- SetHeight caught a stale value and left a shorter tab until something let the
|
||||||
|
-- native layout finish.
|
||||||
|
--
|
||||||
|
-- Compared by equality rather than "did it shrink": the late layout can land on
|
||||||
|
-- a height larger than the stale one this grew from, and a shrink-only check
|
||||||
|
-- would then see current > target and never regrow. GetHeight doesn't read back
|
||||||
|
-- byte-exact after SetHeight either -- UI scale rounds it to a slightly
|
||||||
|
-- different float -- so a strict ~= compare never held and this grew every tick
|
||||||
|
-- without bound. Half a pixel of slack absorbs the rounding while still
|
||||||
|
-- catching a genuine Blizzard-driven change, which is always a full tab's worth
|
||||||
|
-- of height. The same slack covers the width below.
|
||||||
|
local TAB_SIZE_EPSILON = 0.5
|
||||||
|
|
||||||
|
local function UpdateTabSize(tab)
|
||||||
|
if not tab.CoAGrowTab then return end
|
||||||
|
|
||||||
|
local height = tab:GetHeight()
|
||||||
|
|
||||||
|
if not tab.CoAGrownHeight or math.abs(height - tab.CoAGrownHeight) > TAB_SIZE_EPSILON then
|
||||||
|
tab.CoAGrownHeight = height + TAB_GROWTH
|
||||||
|
tab:SetHeight(tab.CoAGrownHeight)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Native panel tabs interlock: each is anchored back into the one before it by
|
||||||
|
-- the width of the art's end cap, so the ornate ends overlap rather than butting
|
||||||
|
-- together. Strip the art, put a flat backdrop in its place, and that overlap
|
||||||
|
-- has nothing left to hide it -- the row reads as one merged bar, and the tabs'
|
||||||
|
-- hit rects overlap along with their backdrops.
|
||||||
|
--
|
||||||
|
-- The tab is narrowed by exactly what its anchor was pulled back by, rather than
|
||||||
|
-- being pushed along to make room: pushing grows the row by the overlap times
|
||||||
|
-- the number of tabs, which on the Challenges window overhangs the frame by four
|
||||||
|
-- times the slack the row has. Absorbing it instead leaves the row's total
|
||||||
|
-- extent exactly where the native layout put it.
|
||||||
|
--
|
||||||
|
-- Opt-in per row rather than detected from the anchor: a tab anchored onto its
|
||||||
|
-- neighbour with a negative offset is the signature of an interlocking row, but
|
||||||
|
-- it would also match any row that merely sits tight, and silently resizing the
|
||||||
|
-- server-authored rows on that guess isn't worth the convenience.
|
||||||
|
local function UnoverlapTab(tab)
|
||||||
|
local point, relativeTo, relativePoint, x, y = tab:GetPoint(1)
|
||||||
|
if not (point and x) or x >= 0 then return end
|
||||||
|
|
||||||
|
tab.CoATabOverlap = -x
|
||||||
|
tab:SetPoint(point, relativeTo, relativePoint, 0, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Re-checked every tick with the same self-healing shape as the grown height,
|
||||||
|
-- and for the same reason: the native tab code re-measures a tab off its own
|
||||||
|
-- label, and a width written once would be handed straight back.
|
||||||
|
local function UpdateTabWidth(tab)
|
||||||
|
local overlap = tab.CoATabOverlap
|
||||||
|
if not overlap then return end
|
||||||
|
|
||||||
|
local width = tab:GetWidth()
|
||||||
|
|
||||||
|
if not tab.CoAUnoverlappedWidth or math.abs(width - tab.CoAUnoverlappedWidth) > TAB_SIZE_EPSILON then
|
||||||
|
tab.CoAUnoverlappedWidth = width - overlap
|
||||||
|
tab:SetWidth(tab.CoAUnoverlappedWidth)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tabs that aren't children of the frame they belong to (the talent frame's
|
||||||
|
-- are separately-placed siblings) sit behind its panel art once grown, so they
|
||||||
|
-- get bumped above it. Re-applied every tick rather than on show or on select:
|
||||||
|
-- bumping from those two alone still left them behind the panel, so whatever
|
||||||
|
-- resets their level isn't either of those events.
|
||||||
|
local function BumpTabLevel(tab)
|
||||||
|
local parent = tab.CoALevelParent
|
||||||
|
if not parent then return end
|
||||||
|
|
||||||
|
tab:SetFrameStrata(parent:GetFrameStrata())
|
||||||
|
tab:SetFrameLevel(parent:GetFrameLevel() + 20)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Only the tab being switched to fires anything at all -- the ones switched
|
||||||
|
-- away from never fire OnClick/OnShow/SetChecked again, yet their native art
|
||||||
|
-- still comes back, so there's no event to catch it from. Checked from OnUpdate
|
||||||
|
-- instead: a cheap GetTexture() compare that only pays for the full strip when
|
||||||
|
-- the art has actually reappeared.
|
||||||
|
--
|
||||||
|
-- Two probes rather than one: a stock Blizzard tab draws its selected state by
|
||||||
|
-- hiding the Left/Middle/Right set and showing the Disabled set in its place,
|
||||||
|
-- so watching only the inactive half would miss art returning on whichever tab
|
||||||
|
-- is currently open. The three pieces of a set are always re-arted together, so
|
||||||
|
-- one probe per set is enough.
|
||||||
|
local TAB_ART_PROBES = {"Left", "LeftDisabled"}
|
||||||
|
|
||||||
|
local function UpdateTabArt(tab)
|
||||||
|
BumpTabLevel(tab)
|
||||||
|
UpdateTabSize(tab)
|
||||||
|
UpdateTabWidth(tab)
|
||||||
|
|
||||||
|
local name = tab:GetName()
|
||||||
|
if not name then return end
|
||||||
|
|
||||||
|
for _, suffix in ipairs(TAB_ART_PROBES) do
|
||||||
|
local tex = _G[name..suffix]
|
||||||
|
|
||||||
|
if tex and tex:GetTexture() then
|
||||||
|
Skin:StripTabArt(tab)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- No fill or border swap marks the open tab: the native tab code already turns
|
||||||
|
-- the label white on the checked one and leaves the rest their normal colour,
|
||||||
|
-- same as the Friends/Character tab rows. Only the art strip has to re-run.
|
||||||
|
local function OnTabChecked(tab)
|
||||||
|
Skin:StripTabArt(tab)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The tab row along the bottom of the talent window is one set of buttons
|
||||||
|
-- shared by all three windows it switches between, not one row per window,
|
||||||
|
-- which is why it lives here rather than in any single frame's module. It
|
||||||
|
-- doesn't need scaling of its own -- it's a child of Collections, so it follows
|
||||||
|
-- whatever ApplyWindowScale sets.
|
||||||
|
local COLLECTION_TAB = "CollectionsPoolFrameCollectionTabTemplate%d"
|
||||||
|
local MAX_COLLECTION_TABS = 10
|
||||||
|
|
||||||
|
function Skin:CollectionTabs(owner)
|
||||||
|
for i = 1, MAX_COLLECTION_TABS do
|
||||||
|
local tab = _G[COLLECTION_TAB:format(i)]
|
||||||
|
if not tab then break end
|
||||||
|
|
||||||
|
self:Tab(tab, owner)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- levelParent is the frame the tab must draw above, and is only needed for tabs
|
||||||
|
-- that aren't its children (see BumpTabLevel). Pass nil for tabs parented to
|
||||||
|
-- the frame they belong to -- normal parent/child z-order already covers those.
|
||||||
|
--
|
||||||
|
-- unoverlap is for rows of interlocking native tabs (see UnoverlapTab). Only the
|
||||||
|
-- tabs anchored onto a neighbour take it; the first tab in a row has nothing to
|
||||||
|
-- pull back from and is left alone by the offset check.
|
||||||
|
function Skin:Tab(tab, levelParent, unoverlap)
|
||||||
|
if not tab then return end
|
||||||
|
|
||||||
|
-- Set on every call rather than only the first: the collection tabs are
|
||||||
|
-- shared, so the frame they have to draw above is whichever window is open.
|
||||||
|
if levelParent then
|
||||||
|
tab.CoALevelParent = levelParent
|
||||||
|
end
|
||||||
|
|
||||||
|
if not tab.CoASkinned then
|
||||||
|
tab.CoASkinned = true
|
||||||
|
|
||||||
|
self:StripTabArt(tab)
|
||||||
|
|
||||||
|
if unoverlap then
|
||||||
|
UnoverlapTab(tab)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Default rather than Transparent: tabs sit below their frame over the
|
||||||
|
-- open world, so a see-through panel reads as washed out instead of as
|
||||||
|
-- the solid tabs the retail layout has.
|
||||||
|
--
|
||||||
|
-- The backdrop's own level is left alone. CreateBackdrop keeps it level
|
||||||
|
-- with the tab, and regions render fine on top of a same-level child;
|
||||||
|
-- forcing it a level above the tab reproduces the
|
||||||
|
-- child-frame-covers-its-parent's-own-regions quirk (see StripRowArt in
|
||||||
|
-- TalentFrame), which blanks the label.
|
||||||
|
tab:CreateBackdrop("Default")
|
||||||
|
tab.backdrop:Point("TOPLEFT", TAB_BACKDROP_INSET, -TAB_BACKDROP_INSET)
|
||||||
|
tab.backdrop:Point("BOTTOMRIGHT", -TAB_BACKDROP_INSET, TAB_BACKDROP_INSET)
|
||||||
|
tab:SetHitRectInsets(TAB_BACKDROP_INSET, TAB_BACKDROP_INSET, TAB_BACKDROP_INSET, TAB_BACKDROP_INSET)
|
||||||
|
|
||||||
|
local fontString = tab.GetFontString and tab:GetFontString()
|
||||||
|
if fontString then
|
||||||
|
tab.CoAGrowTab = true
|
||||||
|
|
||||||
|
local font, size, flags = fontString:GetFont()
|
||||||
|
if font then
|
||||||
|
fontString:SetFont(font, size + TAB_FONT_GROWTH, flags)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Only for tabs that actually carry an icon. On those (the talent
|
||||||
|
-- window's) the label's native anchor sits right off the icon, sized
|
||||||
|
-- for the smaller native font, so grown text collides with it, and
|
||||||
|
-- the label is nudged off whatever point the native layout gave it
|
||||||
|
-- rather than a hardcoded anchor that would fight that layout. The
|
||||||
|
-- wardrobe's category tabs have no icon and centre their label, so
|
||||||
|
-- the same nudge just pushed every one of them off-centre to the
|
||||||
|
-- right.
|
||||||
|
local name = tab:GetName()
|
||||||
|
local icon = name and _G[name.."Icon"]
|
||||||
|
|
||||||
|
if icon and icon.GetTexture and icon:GetTexture() and icon:IsShown() then
|
||||||
|
local point, relTo, relPoint, x, y = fontString:GetPoint(1)
|
||||||
|
|
||||||
|
if point then
|
||||||
|
fontString:SetPoint(point, relTo, relPoint, x + TAB_LABEL_OFFSET, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Guarded: the talent frame's tabs are CheckButtons, the wardrobe's
|
||||||
|
-- category tabs aren't guaranteed to be, and hooksecurefunc errors
|
||||||
|
-- outright on a method that doesn't exist.
|
||||||
|
if tab.SetChecked then
|
||||||
|
hooksecurefunc(tab, "SetChecked", OnTabChecked)
|
||||||
|
end
|
||||||
|
|
||||||
|
tab:HookScript("OnUpdate", UpdateTabArt)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:StripTabArt(tab)
|
||||||
|
BumpTabLevel(tab)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- These frames are created on-demand by their owning addon, the instant the
|
||||||
|
-- player first opens one -- a poll can't catch that before the native art gets
|
||||||
|
-- a paint. ADDON_LOADED fires (synchronously, before control returns to
|
||||||
|
-- whatever code calls :Show()) the moment that addon finishes loading, so
|
||||||
|
-- skinning from it lands before the first-ever :Show(), killing the one-frame
|
||||||
|
-- flicker a poll-based catch can't avoid. Confirmed in-game.
|
||||||
|
function Skin:OnFrameAvailable(tryHook)
|
||||||
|
if tryHook() then return end
|
||||||
|
|
||||||
|
local loader = CreateFrame("Frame")
|
||||||
|
loader:RegisterEvent("ADDON_LOADED")
|
||||||
|
loader:SetScript("OnEvent", function(self)
|
||||||
|
if tryHook() then
|
||||||
|
self:UnregisterEvent("ADDON_LOADED")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
@@ -0,0 +1,735 @@
|
|||||||
|
local E, L, V, P, G = unpack(ElvUI)
|
||||||
|
local S = E:GetModule("Skins")
|
||||||
|
local CoA = E:GetModule("CoA")
|
||||||
|
local Skin = CoA.Skin
|
||||||
|
|
||||||
|
local FRAME_NAME = "CoATalentFrame"
|
||||||
|
|
||||||
|
-- The talent tree is the one part of the frame we must not touch: every node
|
||||||
|
-- is an icon button whose border/overlay textures encode rank and
|
||||||
|
-- availability, so ElvUI's generic button handling would flatten the
|
||||||
|
-- information out of them. The nodes and connectors live in pools under
|
||||||
|
-- SpecTree, with the scene art in FXFrame, so those subtrees are skipped by
|
||||||
|
-- name. TreeView itself is walked, because the bottom bar is inside it.
|
||||||
|
local EXCLUDED = {"PoolFrame", "SpecTree", "ClassTree", "FXFrame", "ShadowOverlay"}
|
||||||
|
|
||||||
|
local function IsTreeSubtree(name)
|
||||||
|
if not name then return false end
|
||||||
|
|
||||||
|
for _, pattern in ipairs(EXCLUDED) do
|
||||||
|
if name:find(pattern) then return true end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function IsDropDown(frame, name)
|
||||||
|
return name ~= nil and _G[name.."Button"] ~= nil and _G[name.."Text"] ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The bottom bar's widgets aren't at a fixed depth (some sit directly on the
|
||||||
|
-- frame, some are nested one or two containers deep), and their names aren't
|
||||||
|
-- documented anywhere, so skinning is driven by walking the frame instead of
|
||||||
|
-- by a hardcoded name list. Depth is capped so a container we didn't expect
|
||||||
|
-- can't drag us down into the tree's pooled widgets.
|
||||||
|
local MAX_DEPTH = 5
|
||||||
|
|
||||||
|
local SkinChildren
|
||||||
|
|
||||||
|
function SkinChildren(frame, depth)
|
||||||
|
if depth > MAX_DEPTH then return end
|
||||||
|
|
||||||
|
for i = 1, frame:GetNumChildren() do
|
||||||
|
local child = select(i, frame:GetChildren())
|
||||||
|
local name = child.GetName and child:GetName()
|
||||||
|
|
||||||
|
if child.GetObjectType and not IsTreeSubtree(name) then
|
||||||
|
local objType = child:GetObjectType()
|
||||||
|
|
||||||
|
if objType == "EditBox" then
|
||||||
|
S:HandleEditBox(child)
|
||||||
|
elseif objType == "Button" then
|
||||||
|
-- Close buttons are handled explicitly -- the frame's in
|
||||||
|
-- SkinFrame, where it needs an anchor and a frame level the walk
|
||||||
|
-- can't supply, and each menu's in SkinBottomBar. Matched on
|
||||||
|
-- "Close" rather than "CloseButton": the menus name theirs just
|
||||||
|
-- "...MenuClose", and it was coming out of the walk as an
|
||||||
|
-- ordinary templated square with the X stripped off it.
|
||||||
|
if not (name and name:find("Close")) then
|
||||||
|
Skin:Button(child)
|
||||||
|
end
|
||||||
|
elseif objType == "Frame" then
|
||||||
|
if IsDropDown(child, name) then
|
||||||
|
S:HandleDropDownBox(child)
|
||||||
|
else
|
||||||
|
SkinChildren(child, depth + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TreeView is a sibling drawn above the close button's own frame level, so
|
||||||
|
-- without the bump the ElvUI close texture ends up behind the tree panel and
|
||||||
|
-- the corner just looks empty. Anchored explicitly because the native corner
|
||||||
|
-- position was set relative to the NineSlice art we hide.
|
||||||
|
local function SkinCloseButton(frame)
|
||||||
|
local close = _G[FRAME_NAME.."CloseButton"]
|
||||||
|
if not close then return end
|
||||||
|
|
||||||
|
Skin:CloseButton(close)
|
||||||
|
|
||||||
|
-- Re-applied on every pass, not once at skin time. The X is drawn on the
|
||||||
|
-- close button's own frame, so it needs to outrank TreeView, and TreeView
|
||||||
|
-- rides along whenever the talent frame is raised while the close button,
|
||||||
|
-- being a sibling, keeps whatever absolute level it was given -- which is
|
||||||
|
-- how the X ends up buried behind the tree panel with the button still
|
||||||
|
-- clickable.
|
||||||
|
close:SetFrameStrata(frame:GetFrameStrata())
|
||||||
|
close:SetFrameLevel(frame:GetFrameLevel() + 20)
|
||||||
|
|
||||||
|
-- HandleCloseButton centres a 12px X inside the button's native 32px box,
|
||||||
|
-- so anchoring the box to the frame corner drops the X well below the
|
||||||
|
-- title. Centring the button on the title's own vertical midpoint puts the
|
||||||
|
-- X on the title line whatever height the bar turns out to be.
|
||||||
|
local title = _G[FRAME_NAME.."TitleText"]
|
||||||
|
local top = frame:GetTop()
|
||||||
|
local titleY = title and select(2, title:GetCenter())
|
||||||
|
|
||||||
|
close:ClearAllPoints()
|
||||||
|
|
||||||
|
if top and titleY then
|
||||||
|
close:Point("CENTER", frame, "TOPRIGHT", -16, titleY - top)
|
||||||
|
else
|
||||||
|
close:Point("TOPRIGHT", frame, "TOPRIGHT", -4, -4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local BOTTOM_BAR = FRAME_NAME.."TreeViewBottomBar"
|
||||||
|
local BOTTOM_BAR_DROPDOWNS = {"SpecDropDown", "BuildDropDown"}
|
||||||
|
|
||||||
|
-- The two menus don't name their list alike: the spec menu's inset and scroll
|
||||||
|
-- frame hang off "List", the build creator's off "BuildList". Assuming they
|
||||||
|
-- shared the layout is why nothing on the build creator's scrollbar was ever
|
||||||
|
-- being found.
|
||||||
|
local BOTTOM_BAR_MENUS = {
|
||||||
|
{menu = "SpecializationMenu", list = "List"},
|
||||||
|
{menu = "BuildCreatorMenu", list = "BuildList"}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Rows are pooled, so this walks by index until it runs out rather than
|
||||||
|
-- tracking how many the list currently holds.
|
||||||
|
local MENU_ROW_LIMIT = 50
|
||||||
|
|
||||||
|
-- Each row's icon sits in a rounded empty-slot ring, which reads as a raised
|
||||||
|
-- bevel against flat panels. The ring goes and the icon gets the usual ElvUI
|
||||||
|
-- treatment: cropped edges and a backdrop sized to it.
|
||||||
|
local function SkinMenuRowIcon(iconFrame)
|
||||||
|
if not iconFrame or iconFrame.CoASkinned then return end
|
||||||
|
iconFrame.CoASkinned = true
|
||||||
|
|
||||||
|
for i = 1, iconFrame:GetNumRegions() do
|
||||||
|
local region = select(i, iconFrame:GetRegions())
|
||||||
|
local texture = region.GetTexture and region:GetTexture()
|
||||||
|
|
||||||
|
if texture and texture:find("EmptySlot") then
|
||||||
|
region:SetTexture(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local name = iconFrame:GetName()
|
||||||
|
local icon = name and _G[name..".Icon"]
|
||||||
|
|
||||||
|
if icon then
|
||||||
|
S:HandleIcon(icon, iconFrame)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ROW_BORDER_EDGES = {"TOP", "BOTTOM", "LEFT", "RIGHT"}
|
||||||
|
|
||||||
|
-- The active row is edged rather than filled, and the edge is four textures on
|
||||||
|
-- the row for the same reason the fill is a texture: a backdrop frame is a
|
||||||
|
-- child, so it either covers the row's art or is covered by the neighbouring
|
||||||
|
-- rows, which overlap far enough that only its bottom line came through.
|
||||||
|
local function CreateRowBorder(row)
|
||||||
|
local border = {}
|
||||||
|
|
||||||
|
for _, edge in ipairs(ROW_BORDER_EDGES) do
|
||||||
|
local line = row:CreateTexture(nil, "OVERLAY")
|
||||||
|
line:Hide()
|
||||||
|
border[edge] = line
|
||||||
|
end
|
||||||
|
|
||||||
|
border.TOP:SetPoint("TOPLEFT", row, "TOPLEFT")
|
||||||
|
border.TOP:SetPoint("TOPRIGHT", row, "TOPRIGHT")
|
||||||
|
border.TOP:SetHeight(E.mult)
|
||||||
|
|
||||||
|
border.BOTTOM:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT")
|
||||||
|
border.BOTTOM:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT")
|
||||||
|
border.BOTTOM:SetHeight(E.mult)
|
||||||
|
|
||||||
|
border.LEFT:SetPoint("TOPLEFT", row, "TOPLEFT")
|
||||||
|
border.LEFT:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT")
|
||||||
|
border.LEFT:SetWidth(E.mult)
|
||||||
|
|
||||||
|
border.RIGHT:SetPoint("TOPRIGHT", row, "TOPRIGHT")
|
||||||
|
border.RIGHT:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT")
|
||||||
|
border.RIGHT:SetWidth(E.mult)
|
||||||
|
|
||||||
|
row.CoABorder = border
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Selection is read off the native overlay rather than tracked here: the row's
|
||||||
|
-- own code decides which entry is active and there's no event for it, so the
|
||||||
|
-- overlay is left in place as the signal and only its art is cleared.
|
||||||
|
local function UpdateRowSelection(row)
|
||||||
|
local border = row.CoABorder
|
||||||
|
if not border then return end
|
||||||
|
|
||||||
|
local overlay = row.CoASelected
|
||||||
|
-- Alpha as well as visibility, because it isn't known which of the two the
|
||||||
|
-- row uses to turn the overlay on.
|
||||||
|
local selected = overlay ~= nil and overlay:IsShown() and overlay:GetAlpha() > 0
|
||||||
|
|
||||||
|
if selected == row.CoASelectionShown then return end
|
||||||
|
row.CoASelectionShown = selected
|
||||||
|
|
||||||
|
local r, g, b = unpack(E.media.rgbvaluecolor)
|
||||||
|
|
||||||
|
for _, edge in ipairs(ROW_BORDER_EDGES) do
|
||||||
|
local line = border[edge]
|
||||||
|
|
||||||
|
line:SetTexture(r, g, b, 1)
|
||||||
|
if selected then line:Show() else line:Hide() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The row's frame is a rounded plate from the PvP queue art, with a matching
|
||||||
|
-- background behind it. Both come off in favour of a flat ElvUI panel, and the
|
||||||
|
-- hover and selection plates have to go with them -- they're cut to the same
|
||||||
|
-- rounded shape, so they read as bevels once the plate underneath is flat.
|
||||||
|
-- Hover becomes the flat white wash ElvUI puts on list rows; selection moves
|
||||||
|
-- onto the 1px edge from CreateRowBorder, which is how the active entry is
|
||||||
|
-- marked everywhere else in ElvUI.
|
||||||
|
--
|
||||||
|
-- Hidden as well as cleared, and re-run from both the row's show and its
|
||||||
|
-- update: a single pass didn't hold, the rows put their art back as they're
|
||||||
|
-- refilled from the pool.
|
||||||
|
local function StripRowArt(row)
|
||||||
|
local name = row:GetName()
|
||||||
|
local border = name and _G[name..".Border"]
|
||||||
|
|
||||||
|
if border then
|
||||||
|
border:SetTexture(nil)
|
||||||
|
border:Hide()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ".H" is the row's highlight texture, so retexturing it is enough to
|
||||||
|
-- replace the hover state; there's no separate object to chase.
|
||||||
|
local hover = name and _G[name..".H"]
|
||||||
|
if hover and hover.SetTexture then
|
||||||
|
hover:SetTexture(1, 1, 1, 0.15)
|
||||||
|
hover:ClearAllPoints()
|
||||||
|
hover:SetInside(row)
|
||||||
|
row.CoAHover = hover
|
||||||
|
end
|
||||||
|
|
||||||
|
local selected = name and _G[name..".Selected"]
|
||||||
|
if selected and selected.SetTexture then
|
||||||
|
selected:SetTexture(nil)
|
||||||
|
row.CoASelected = selected
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, row:GetNumRegions() do
|
||||||
|
local region = select(i, row:GetRegions())
|
||||||
|
local texture = region.GetTexture and region:GetTexture()
|
||||||
|
|
||||||
|
if texture and texture:find("GuildFrame") then
|
||||||
|
region:SetTexture(nil)
|
||||||
|
region:Hide()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Scrolling refills the pooled rows and puts their native hover and selection
|
||||||
|
-- art back, and a pooled row doesn't fire OnShow when it's refilled, so there's
|
||||||
|
-- no event to strip on -- which is why the glow only ever appeared after a
|
||||||
|
-- scroll. The art is checked from the row's update instead. A colour-set
|
||||||
|
-- texture reads back as "SolidTexture", so the check stays a string compare
|
||||||
|
-- rather than a blind re-skin every frame.
|
||||||
|
local function UpdateRowArt(row)
|
||||||
|
local hover, selected = row.CoAHover, row.CoASelected
|
||||||
|
|
||||||
|
if (hover and hover:GetTexture() ~= "SolidTexture")
|
||||||
|
or (selected and selected:GetTexture() ~= nil) then
|
||||||
|
StripRowArt(row)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function UpdateRow(row)
|
||||||
|
UpdateRowArt(row)
|
||||||
|
UpdateRowSelection(row)
|
||||||
|
end
|
||||||
|
|
||||||
|
local SkinMenuRow
|
||||||
|
|
||||||
|
function SkinMenuRow(row)
|
||||||
|
if not row then return end
|
||||||
|
|
||||||
|
if not row.CoASkinned then
|
||||||
|
row.CoASkinned = true
|
||||||
|
|
||||||
|
-- The fill is painted on the row itself rather than through
|
||||||
|
-- CreateBackdrop. The backdrop is a child frame, and here it covered
|
||||||
|
-- the row's name, icon and status text at every frame level tried,
|
||||||
|
-- including one below the row's own. A texture on the row's BACKGROUND
|
||||||
|
-- layer is ordered against those regions inside a single frame instead,
|
||||||
|
-- so it can't outrank them. Opaque rather than the transparent template,
|
||||||
|
-- since the menu panel behind is already see-through and a second
|
||||||
|
-- see-through layer left the rows reading as holes onto the talent scene.
|
||||||
|
local background = row:CreateTexture(nil, "BACKGROUND")
|
||||||
|
background:SetInside(row)
|
||||||
|
background:SetTexture(unpack(E.media.backdropcolor))
|
||||||
|
|
||||||
|
CreateRowBorder(row)
|
||||||
|
|
||||||
|
-- Nooped so a refill can't swap the highlight for a fresh texture
|
||||||
|
-- object, which would leave the retextured one orphaned and the check in
|
||||||
|
-- UpdateRowArt reading a state nothing draws from any more.
|
||||||
|
row.SetHighlightTexture = E.noop
|
||||||
|
|
||||||
|
row:HookScript("OnShow", SkinMenuRow)
|
||||||
|
|
||||||
|
-- No event fires when the active spec changes or when a scroll refills
|
||||||
|
-- the row, so both are resynced from the row's update. The comparisons
|
||||||
|
-- in UpdateRowArt and UpdateRowSelection make every tick that changes
|
||||||
|
-- nothing a no-op.
|
||||||
|
row:HookScript("OnUpdate", UpdateRow)
|
||||||
|
end
|
||||||
|
|
||||||
|
StripRowArt(row)
|
||||||
|
UpdateRowSelection(row)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The Activate button inside an expanded specialization row draws its pill from
|
||||||
|
-- a single atlas across three BACKGROUND regions plus a HIGHLIGHT one, and
|
||||||
|
-- switches between the green and the grey variant by tex coord. There are no
|
||||||
|
-- Normal/Pushed/Disabled textures for HandleButton to clear (probed: both come
|
||||||
|
-- back nil, and every region's vertex colour is white in either state), so the
|
||||||
|
-- art is cleared by file the way the dropdown pills are (Skin:StripArtByFile,
|
||||||
|
-- which noops SetTexture per region as well -- whatever swaps the tex coord on
|
||||||
|
-- a state change is free to re-art the region), and the enabled/disabled
|
||||||
|
-- distinction the atlas was carrying has to be re-created on the label.
|
||||||
|
|
||||||
|
-- GetFontString covers the templated case; the scan is for a label that was
|
||||||
|
-- added as a plain region rather than set as the button's own font string.
|
||||||
|
local function ActivateLabel(button)
|
||||||
|
if button.CoALabel then return button.CoALabel end
|
||||||
|
|
||||||
|
local text = button.GetFontString and button:GetFontString()
|
||||||
|
|
||||||
|
if not text then
|
||||||
|
for i = 1, button:GetNumRegions() do
|
||||||
|
local region = select(i, button:GetRegions())
|
||||||
|
|
||||||
|
if region:GetObjectType() == "FontString" then
|
||||||
|
text = region
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
button.CoALabel = text
|
||||||
|
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Yellow live, grey dead: the same pair ElvUI's own templated buttons use, so
|
||||||
|
-- these read as buttons rather than as labels on a panel.
|
||||||
|
local ACTIVATE_COLOR = {1, 0.82, 0}
|
||||||
|
local ACTIVATE_DISABLED_COLOR = {0.55, 0.55, 0.55}
|
||||||
|
|
||||||
|
-- The active spec's button is disabled, and with the pill gone nothing else
|
||||||
|
-- says so -- the ElvUI panel is drawn the same either way. The label is greyed
|
||||||
|
-- instead, which is how ElvUI marks a dead button everywhere else.
|
||||||
|
--
|
||||||
|
-- The enabled state is compared against the cached one rather than written
|
||||||
|
-- blind because this runs from the button's update: there's no event for a spec
|
||||||
|
-- becoming active. Skin:IsEnabled does the 0/1-to-boolean normalisation this
|
||||||
|
-- client needs (see Skinning.lua).
|
||||||
|
local function UpdateActivateState(button)
|
||||||
|
local enabled = Skin:IsEnabled(button)
|
||||||
|
|
||||||
|
if enabled == button.CoAActivateEnabled then return end
|
||||||
|
button.CoAActivateEnabled = enabled
|
||||||
|
|
||||||
|
-- Activating a spec disables this button under the very cursor that just
|
||||||
|
-- clicked it, so its hover border would stay lit until the pointer moved
|
||||||
|
-- off. This update already watches the state, so it carries the border too.
|
||||||
|
Skin:RefreshButtonBorder(button)
|
||||||
|
|
||||||
|
local text = ActivateLabel(button)
|
||||||
|
if not text then return end
|
||||||
|
|
||||||
|
text:SetTextColor(unpack(enabled and ACTIVATE_COLOR or ACTIVATE_DISABLED_COLOR))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stripped before templating, as with the dropdown pills: HandleButton adds its
|
||||||
|
-- backdrop as regions of this same button, so a strip afterwards takes the
|
||||||
|
-- backdrop with the pill.
|
||||||
|
local function SkinActivateButton(button)
|
||||||
|
if not button then return end
|
||||||
|
|
||||||
|
if not button.CoASkinned then
|
||||||
|
button.CoASkinned = true
|
||||||
|
|
||||||
|
Skin:StripArtByFile(button, Skin.RedButtonArt)
|
||||||
|
Skin:Button(button)
|
||||||
|
|
||||||
|
button:HookScript("OnUpdate", UpdateActivateState)
|
||||||
|
end
|
||||||
|
|
||||||
|
UpdateActivateState(button)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Unlike the row's icon these are named without a literal dot
|
||||||
|
-- ("Button1ExpandedContent", "...ExpandedContentActivateButton"), read off the
|
||||||
|
-- frame stack.
|
||||||
|
--
|
||||||
|
-- The expanded content exists from the moment the menu is built rather than
|
||||||
|
-- being created when its row is expanded (probed: present, hidden, before any
|
||||||
|
-- row had been opened), so it's picked up with the rest of the row. The show
|
||||||
|
-- hook is there for a row whose content is filled in later than this first pass.
|
||||||
|
local function SkinRowExpansion(rowName)
|
||||||
|
local content = _G[rowName.."ExpandedContent"]
|
||||||
|
if not content then return end
|
||||||
|
|
||||||
|
local buttonName = rowName.."ExpandedContentActivateButton"
|
||||||
|
|
||||||
|
SkinActivateButton(_G[buttonName])
|
||||||
|
|
||||||
|
if content.CoASkinned then return end
|
||||||
|
content.CoASkinned = true
|
||||||
|
|
||||||
|
content:HookScript("OnShow", function()
|
||||||
|
SkinActivateButton(_G[buttonName])
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- These children are named with a literal dot ("Button1.SpecIcon"), so they
|
||||||
|
-- only come out of _G by string key, never as plain identifiers.
|
||||||
|
local function SkinMenuRows(listName)
|
||||||
|
for i = 1, MENU_ROW_LIMIT do
|
||||||
|
local rowName = listName.."ScrollFrameButton"..i
|
||||||
|
local row = _G[rowName]
|
||||||
|
if not row then break end
|
||||||
|
|
||||||
|
SkinMenuRow(row)
|
||||||
|
SkinMenuRowIcon(_G[rowName..".SpecIcon"])
|
||||||
|
SkinRowExpansion(rowName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- S:HandleScrollBar can't be used on these: it assumes the thumb is a texture
|
||||||
|
-- and calls SetTexture on it, but this thumb is a button, so the call errors
|
||||||
|
-- and takes the rest of the OnShow skinning down with it.
|
||||||
|
--
|
||||||
|
-- It's a proportional scrollbar: the thumb is drawn as three slices that the
|
||||||
|
-- widget re-applies whenever it resizes, so stripping them doesn't hold. The
|
||||||
|
-- slices are made transparent instead and a value-coloured panel is stretched
|
||||||
|
-- from the first to the last, which is how ElvUI handles the same widget in
|
||||||
|
-- HandleProportionalScroll. Slices are looked up as fields and as globals,
|
||||||
|
-- since only the global names are confirmed here.
|
||||||
|
local function SkinScrollThumb(thumb, thumbName)
|
||||||
|
if not thumb or thumb.backdrop then return end
|
||||||
|
|
||||||
|
local first = thumb.Begin or _G[thumbName.."Begin"]
|
||||||
|
local last = thumb.End or _G[thumbName.."End"]
|
||||||
|
local middle = thumb.Middle or _G[thumbName.."Middle"]
|
||||||
|
|
||||||
|
if first then first:SetAlpha(0) end
|
||||||
|
if last then last:SetAlpha(0) end
|
||||||
|
if middle then middle:SetAlpha(0) end
|
||||||
|
|
||||||
|
local r, g, b = unpack(E.media.rgbvaluecolor)
|
||||||
|
|
||||||
|
thumb:CreateBackdrop("Transparent")
|
||||||
|
thumb.backdrop:SetFrameLevel(thumb:GetFrameLevel() + 1)
|
||||||
|
thumb.backdrop:SetBackdropColor(r, g, b, 0.25)
|
||||||
|
|
||||||
|
if first and last then
|
||||||
|
thumb.backdrop:Point("TOPLEFT", first)
|
||||||
|
thumb.backdrop:Point("BOTTOMRIGHT", last)
|
||||||
|
end
|
||||||
|
|
||||||
|
thumb:HookScript("OnEnter", function(self)
|
||||||
|
if self.backdrop then self.backdrop:SetBackdropColor(r, g, b, 0.75) end
|
||||||
|
end)
|
||||||
|
|
||||||
|
thumb:HookScript("OnLeave", function(self)
|
||||||
|
if self.backdrop then self.backdrop:SetBackdropColor(r, g, b, 0.25) end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The list inside each dropdown popup: a scroll frame with the framed inset
|
||||||
|
-- and overlay art around it. The arrows are named off the scroll frame rather
|
||||||
|
-- than off the scrollbar, so they're looked up here instead.
|
||||||
|
local function SkinMenuScroll(listName)
|
||||||
|
local scrollBar = _G[listName.."ScrollFrameScrollBar"]
|
||||||
|
if not scrollBar then return end
|
||||||
|
|
||||||
|
local inset = _G[listName.."Inset"]
|
||||||
|
if inset then inset:StripTextures() end
|
||||||
|
|
||||||
|
local overlay = _G[listName.."ScrollFrameArtOverlay"]
|
||||||
|
if overlay then overlay:StripTextures() end
|
||||||
|
|
||||||
|
if scrollBar.backdrop then return end
|
||||||
|
|
||||||
|
local frameLevel = scrollBar:GetFrameLevel()
|
||||||
|
|
||||||
|
scrollBar:Width(18)
|
||||||
|
scrollBar:StripTextures()
|
||||||
|
scrollBar:CreateBackdrop()
|
||||||
|
scrollBar.backdrop:SetAllPoints()
|
||||||
|
scrollBar.backdrop:SetFrameLevel(frameLevel)
|
||||||
|
|
||||||
|
local up = _G[listName.."ScrollFrameScrollUpButton"]
|
||||||
|
if up then
|
||||||
|
up:Point("BOTTOM", scrollBar, "TOP", 0, 1)
|
||||||
|
Skin:ScrollArrow(up, "up")
|
||||||
|
end
|
||||||
|
|
||||||
|
local down = _G[listName.."ScrollFrameScrollDownButton"]
|
||||||
|
if down then
|
||||||
|
down:Point("TOP", scrollBar, "BOTTOM", 0, -1)
|
||||||
|
Skin:ScrollArrow(down, "down")
|
||||||
|
end
|
||||||
|
|
||||||
|
local thumbName = listName.."ScrollFrameScrollBarThumb"
|
||||||
|
local thumb = (scrollBar.GetThumbTexture and scrollBar:GetThumbTexture()) or _G[thumbName]
|
||||||
|
|
||||||
|
SkinScrollThumb(thumb, thumbName)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The bar's plate art (BottomBarTexture) is the dark band under the buttons,
|
||||||
|
-- and it's the bar's only region, so a plain StripTextures clears it without
|
||||||
|
-- touching the scene art, which lives on TreeView one level up. The buttons,
|
||||||
|
-- dropdowns and search box sitting on the bar are picked up by the walk.
|
||||||
|
local function SkinBottomBar()
|
||||||
|
local bar = _G[BOTTOM_BAR]
|
||||||
|
if not bar then return end
|
||||||
|
|
||||||
|
bar:StripTextures()
|
||||||
|
|
||||||
|
-- Each dropdown's caret is a child button carrying the arrow as its own
|
||||||
|
-- art, so the walk was treating it as an ordinary button and giving it a
|
||||||
|
-- templated square. Skinned here, ahead of the walk, so it becomes the
|
||||||
|
-- ElvUI chevron; HandleNextPrevButton's isSkinned flag then makes the walk
|
||||||
|
-- leave it alone. noBackdrop keeps it a bare white arrow that takes the
|
||||||
|
-- value colour on hover, and "up" matches the way these menus open.
|
||||||
|
for _, suffix in ipairs(BOTTOM_BAR_DROPDOWNS) do
|
||||||
|
local dropdown = _G[BOTTOM_BAR..suffix]
|
||||||
|
local arrow = _G[BOTTOM_BAR..suffix.."Button"]
|
||||||
|
|
||||||
|
if arrow then
|
||||||
|
S:HandleNextPrevButton(arrow, "up", nil, true)
|
||||||
|
arrow:Size(20, 20)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- These buttons are wider than they look: the label art only ever
|
||||||
|
-- covered the part up to the arrow, so templating the whole frame drew
|
||||||
|
-- a panel running under the icon buttons to their right. Skinned here
|
||||||
|
-- rather than in the walk so the backdrop can be built separately and
|
||||||
|
-- stopped at the arrow, and the dead space to its right is taken out
|
||||||
|
-- of the hit rect so it can't swallow clicks meant for those icons.
|
||||||
|
if dropdown and arrow then
|
||||||
|
Skin:Button(dropdown, nil, nil, true)
|
||||||
|
|
||||||
|
if dropdown.backdrop then
|
||||||
|
dropdown.backdrop:ClearAllPoints()
|
||||||
|
dropdown.backdrop:Point("TOPLEFT", dropdown, "TOPLEFT", -1, 1)
|
||||||
|
dropdown.backdrop:Point("BOTTOMRIGHT", arrow, "BOTTOMRIGHT", 3, -1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local dropdownRight, arrowRight = dropdown:GetRight(), arrow:GetRight()
|
||||||
|
|
||||||
|
if dropdownRight and arrowRight then
|
||||||
|
dropdown:SetHitRectInsets(0, math.max(0, dropdownRight - arrowRight - 3), 0, 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The two dropdown popups are plain frames, which the walk descends into
|
||||||
|
-- for their buttons but never templates, so they get their panel here.
|
||||||
|
for _, entry in ipairs(BOTTOM_BAR_MENUS) do
|
||||||
|
local menuName = BOTTOM_BAR..entry.menu
|
||||||
|
local listName = menuName..entry.list
|
||||||
|
local menu = _G[menuName]
|
||||||
|
|
||||||
|
if menu and not menu.CoASkinned then
|
||||||
|
menu.CoASkinned = true
|
||||||
|
|
||||||
|
Skin:Panel(menu)
|
||||||
|
Skin:CloseButton(_G[menuName.."Close"])
|
||||||
|
|
||||||
|
-- Rows don't exist until the menu is first opened, which happens
|
||||||
|
-- long after the talent frame's own OnShow, so they're picked up
|
||||||
|
-- on the menu's.
|
||||||
|
menu:HookScript("OnShow", function()
|
||||||
|
SkinMenuRows(listName)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
SkinMenuScroll(listName)
|
||||||
|
SkinMenuRows(listName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The scene art belongs to TreeView, which runs on behind the bottom bar, so
|
||||||
|
-- clearing the bar's own plate left the art showing through under the
|
||||||
|
-- buttons. The pieces are stretched over the whole of TreeView and show their
|
||||||
|
-- slice of the file through tex coords, which occupy only part of it -- the
|
||||||
|
-- rest of the file holds other art. So the coords have to be trimmed in
|
||||||
|
-- proportion to their existing values rather than replaced outright, or
|
||||||
|
-- unrelated regions of the file scroll into view.
|
||||||
|
--
|
||||||
|
-- Geometry comes from TreeView and the bar rather than from the texture's own
|
||||||
|
-- rect, so that a re-run reads the same numbers instead of measuring a rect
|
||||||
|
-- it already cropped. Coords are kept from the first pass for the same reason.
|
||||||
|
local function CropTexture(tex, bar, keep)
|
||||||
|
if not tex.CoAOriginalCoords then
|
||||||
|
tex.CoAOriginalCoords = {tex:GetTexCoord()}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- GetTexCoord yields the four corners: UL, LL, UR, LR. These are all
|
||||||
|
-- axis-aligned, so the edges come off the corners that define them.
|
||||||
|
local coords = tex.CoAOriginalCoords
|
||||||
|
local left, top, bottom, right = coords[1], coords[2], coords[4], coords[5]
|
||||||
|
|
||||||
|
tex:SetTexCoord(left, right, top, top + (bottom - top) * keep)
|
||||||
|
tex:Point("BOTTOMRIGHT", bar, "TOPRIGHT", 0, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function CropBackground()
|
||||||
|
local treeView = _G[FRAME_NAME.."TreeView"]
|
||||||
|
local bar = _G[BOTTOM_BAR]
|
||||||
|
if not (treeView and bar) then return end
|
||||||
|
|
||||||
|
local treeTop, treeBottom, barTop = treeView:GetTop(), treeView:GetBottom(), bar:GetTop()
|
||||||
|
if not (treeTop and treeBottom and barTop) then return end
|
||||||
|
|
||||||
|
local height = treeTop - treeBottom
|
||||||
|
if height <= 0 then return end
|
||||||
|
|
||||||
|
local keep = (treeTop - barTop) / height
|
||||||
|
if keep <= 0 or keep >= 1 then return end
|
||||||
|
|
||||||
|
-- Scoped to TreeView's own regions: earlier this looked the names up as
|
||||||
|
-- globals over a fixed range, which pulled in same-named textures owned by
|
||||||
|
-- other frames and re-anchored those too.
|
||||||
|
for i = 1, treeView:GetNumRegions() do
|
||||||
|
local region = select(i, treeView:GetRegions())
|
||||||
|
local name = region.GetName and region:GetName()
|
||||||
|
|
||||||
|
if name and name:find("Background") and region:GetObjectType() == "Texture" then
|
||||||
|
CropTexture(region, bar, keep)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The tab row is shared with the vanity and wardrobe windows and is handled in
|
||||||
|
-- Skinning.lua. The talent frame is passed as its owner while this window is
|
||||||
|
-- the open one: the tabs aren't its children, so they don't draw above its
|
||||||
|
-- panel on their own.
|
||||||
|
local function SkinTabs()
|
||||||
|
Skin:CollectionTabs(_G[FRAME_NAME])
|
||||||
|
end
|
||||||
|
|
||||||
|
local SPEC_CHOICE = FRAME_NAME.."SpecViewPoolFrameCoASpecChoiceTemplate%d"
|
||||||
|
local MAX_SPEC_CHOICES = 10
|
||||||
|
|
||||||
|
-- Only each card's action button. The cards themselves keep their art: the
|
||||||
|
-- portraits and the gold wash on the active spec are the whole point of the
|
||||||
|
-- view, and there's nothing flat that would say the same thing.
|
||||||
|
--
|
||||||
|
-- Skinned by name because the walk can't get here -- the cards hang off a
|
||||||
|
-- PoolFrame, which EXCLUDED skips so the tree's pooled nodes stay untouched.
|
||||||
|
-- Stripped as well as templated: the red plate isn't one of the Left/Middle/
|
||||||
|
-- Right pieces HandleButton clears on its own.
|
||||||
|
local function SkinSpecChoices()
|
||||||
|
for i = 1, MAX_SPEC_CHOICES do
|
||||||
|
local cardName = SPEC_CHOICE:format(i)
|
||||||
|
if not _G[cardName] then break end
|
||||||
|
|
||||||
|
Skin:Button(_G[cardName.."SelectButton"], true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SkinFrame(frame)
|
||||||
|
if not frame.CoASkinned then
|
||||||
|
frame.CoASkinned = true
|
||||||
|
|
||||||
|
Skin:HideArt(_G[FRAME_NAME.."NineSlice"])
|
||||||
|
|
||||||
|
-- The round portrait medallion overhangs the top-left corner and has
|
||||||
|
-- no flat equivalent, so it goes rather than getting reskinned.
|
||||||
|
Skin:HideArt(_G[FRAME_NAME.."PortraitFrame"])
|
||||||
|
|
||||||
|
Skin:Panel(frame)
|
||||||
|
|
||||||
|
-- The choice cards are pulled from the pool as the view opens, so the
|
||||||
|
-- talent frame's own show is too early to catch them. Run on the view's
|
||||||
|
-- show, and again a tick later in case the pool is filled after it.
|
||||||
|
local specView = _G[FRAME_NAME.."SpecView"]
|
||||||
|
if specView then
|
||||||
|
specView:HookScript("OnShow", function()
|
||||||
|
SkinSpecChoices()
|
||||||
|
E:Delay(0.1, SkinSpecChoices)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Children are created lazily as tabs are visited, so the walk has to
|
||||||
|
-- run again on every show rather than once at hook time. The isSkinned
|
||||||
|
-- / backdrop guards inside ElvUI's handlers make re-runs cheap.
|
||||||
|
frame:HookScript("OnShow", function(self)
|
||||||
|
Skin:ApplyWindowScale("advancementScale")
|
||||||
|
Skin:Title(_G[FRAME_NAME.."TitleText"])
|
||||||
|
SkinCloseButton(self)
|
||||||
|
SkinBottomBar()
|
||||||
|
CropBackground()
|
||||||
|
SkinChildren(self, 1)
|
||||||
|
SkinTabs()
|
||||||
|
SkinSpecChoices()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
Skin:ApplyWindowScale("advancementScale")
|
||||||
|
Skin:Title(_G[FRAME_NAME.."TitleText"])
|
||||||
|
SkinCloseButton(frame)
|
||||||
|
SkinBottomBar()
|
||||||
|
-- Not CropBackground() here: this runs pre-Show now (see InitializeTalentFrame),
|
||||||
|
-- before Blizzard's own code sets the background texture's real art-tile
|
||||||
|
-- coords -- it's still XML-template default (full 0..1) at this point. Crop
|
||||||
|
-- caches "original" coords on first call and never recaptures, so cropping
|
||||||
|
-- here would permanently bake in the wrong baseline. OnShow (below) is the
|
||||||
|
-- only place this is safe to run.
|
||||||
|
SkinChildren(frame, 1)
|
||||||
|
SkinTabs()
|
||||||
|
SkinSpecChoices()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function TryHook()
|
||||||
|
local frame = _G[FRAME_NAME]
|
||||||
|
if not frame then return false end
|
||||||
|
|
||||||
|
SkinFrame(frame)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function CoA:InitializeTalentFrame()
|
||||||
|
if not E.private.skins.blizzard.enable then return end
|
||||||
|
|
||||||
|
Skin:OnFrameAvailable(TryHook)
|
||||||
|
end
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
local E, L, V, P, G = unpack(ElvUI)
|
||||||
|
local S = E:GetModule("Skins")
|
||||||
|
local CoA = E:GetModule("CoA")
|
||||||
|
local Skin = CoA.Skin
|
||||||
|
|
||||||
|
local FRAME_NAME = "StoreCollectionFrame"
|
||||||
|
|
||||||
|
-- Confirmed live by probing size/position: these two OVERLAY "Portrait2"
|
||||||
|
-- regions sit at the currency counters (aligned with SPCounterHintButton and
|
||||||
|
-- DPCounterHintButton), not at the top-left corner. An earlier version of
|
||||||
|
-- this matched them as the portrait and stripped them, which blanked the
|
||||||
|
-- counter badges instead. Whatever the real top-left portrait is hasn't been
|
||||||
|
-- identified yet -- it isn't among StoreCollectionFrame's own regions or
|
||||||
|
-- children -- so nothing removes it for now.
|
||||||
|
|
||||||
|
-- Standard UIPanelButtonTemplate art (FontString + Normal/Pushed/Disabled/
|
||||||
|
-- Highlight textures), confirmed by probe -- S:HandleButton's own texture
|
||||||
|
-- clearing handles these directly, no manual stripping needed.
|
||||||
|
local function SkinActionButtons()
|
||||||
|
Skin:Button(_G[FRAME_NAME.."ActivateStoreButton"])
|
||||||
|
Skin:Button(_G[FRAME_NAME.."BuyStoreButton"])
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SkinSearchBox()
|
||||||
|
S:HandleEditBox(_G[FRAME_NAME.."SearchBox"])
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The filter pill and its popout are the shared dropdown widget; see
|
||||||
|
-- Skinning.lua for why S:HandleButton alone can't clear it.
|
||||||
|
local function SkinDropdown()
|
||||||
|
Skin:Dropdown(_G[FRAME_NAME.."Dropdown"], _G[FRAME_NAME.."DropdownMenu"])
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Neither pager button is named (both are anonymous children of
|
||||||
|
-- CollectionList), so direction is worked out from their own anchor offset
|
||||||
|
-- rather than a name match, and re-checked on every pass instead of cached --
|
||||||
|
-- cheap, and self-corrects if the two ever come back in a different order.
|
||||||
|
local function SkinPagerArrows()
|
||||||
|
local list = _G[FRAME_NAME.."CollectionList"]
|
||||||
|
if not list then return end
|
||||||
|
|
||||||
|
local a, b = select(3, list:GetChildren()), select(4, list:GetChildren())
|
||||||
|
if not (a and b and a:GetObjectType() == "Button" and b:GetObjectType() == "Button") then return end
|
||||||
|
|
||||||
|
local _, _, _, ax = a:GetPoint()
|
||||||
|
local _, _, _, bx = b:GetPoint()
|
||||||
|
|
||||||
|
local prevButton, nextButton = a, b
|
||||||
|
if bx and ax and bx < ax then
|
||||||
|
prevButton, nextButton = b, a
|
||||||
|
end
|
||||||
|
|
||||||
|
S:HandleNextPrevButton(prevButton, "left")
|
||||||
|
S:HandleNextPrevButton(nextButton, "right")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SkinContents()
|
||||||
|
-- Scale is set on the shared container while this window is the open one,
|
||||||
|
-- which carries the tab row along with it (see Skinning.lua).
|
||||||
|
Skin:ApplyWindowScale("vanityScale")
|
||||||
|
Skin:CollectionTabs(_G[FRAME_NAME])
|
||||||
|
Skin:Title(_G[FRAME_NAME.."TitleText"])
|
||||||
|
Skin:CloseButton(_G[FRAME_NAME.."CloseButton"])
|
||||||
|
SkinActionButtons()
|
||||||
|
SkinSearchBox()
|
||||||
|
SkinDropdown()
|
||||||
|
SkinPagerArrows()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SkinFrame(frame)
|
||||||
|
if not frame.CoASkinned then
|
||||||
|
frame.CoASkinned = true
|
||||||
|
|
||||||
|
-- Templated without stripping, unlike the talent and wardrobe frames:
|
||||||
|
-- the currency counters are drawn as regions of the frame itself (see
|
||||||
|
-- the note at the top of this file), so a strip blanks them.
|
||||||
|
Skin:Panel(frame, true)
|
||||||
|
|
||||||
|
frame:HookScript("OnShow", SkinContents)
|
||||||
|
end
|
||||||
|
|
||||||
|
SkinContents()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function TryHook()
|
||||||
|
local frame = _G[FRAME_NAME]
|
||||||
|
if not frame then return false end
|
||||||
|
|
||||||
|
SkinFrame(frame)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function CoA:InitializeVanityFrame()
|
||||||
|
if not E.private.skins.blizzard.enable then return end
|
||||||
|
|
||||||
|
Skin:OnFrameAvailable(TryHook)
|
||||||
|
end
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
local E, L, V, P, G = unpack(ElvUI)
|
||||||
|
local S = E:GetModule("Skins")
|
||||||
|
local CoA = E:GetModule("CoA")
|
||||||
|
local Skin = CoA.Skin
|
||||||
|
|
||||||
|
local FRAME_NAME = "AppearanceWardrobeFrame"
|
||||||
|
|
||||||
|
-- Standard UIPanelButtonTemplate art, same as Vanity's action buttons --
|
||||||
|
-- S:HandleButton's own texture clearing handles these directly.
|
||||||
|
local function SkinActionButtons()
|
||||||
|
Skin:Button(_G[FRAME_NAME.."PlayerModelSaveOutfitButton"])
|
||||||
|
Skin:Button(_G[FRAME_NAME.."DisableTransmogButton"])
|
||||||
|
Skin:Button(_G[FRAME_NAME.."DisableSpellVisualsButton"])
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Apply, Cancel and Clear invalid slots only exist while a transmog change is
|
||||||
|
-- pending, which is why they were missed for so long -- but they exist from the
|
||||||
|
-- start rather than being created on the first change (probed: they are
|
||||||
|
-- Buttons, hidden, with nothing pending), so the normal skin pass reaches them.
|
||||||
|
--
|
||||||
|
-- Same widget as the talent frame's Activate button: the pill is drawn across
|
||||||
|
-- plain regions, with no Normal/Pushed texture for HandleButton to clear
|
||||||
|
-- (probed: they come back nil). Apply and Clear invalid slots take the green
|
||||||
|
-- variant of the gold atlas, Cancel is drawn from the red file instead --
|
||||||
|
-- Skin.RedButtonArt matches both files, and Cancel's HIGHLIGHT region goes with
|
||||||
|
-- the rest of its art.
|
||||||
|
--
|
||||||
|
-- The labels don't carry the atlas's green/red split over: the buttons sit in a
|
||||||
|
-- row and read apart by their text, and they take the same yellow every other
|
||||||
|
-- templated button on these frames uses -- the talent frame's live Activate
|
||||||
|
-- label included -- so a pending change doesn't get its own colour scheme. The
|
||||||
|
-- grey is the disabled half of that same pair.
|
||||||
|
local LABEL_COLOR = {1, 0.82, 0}
|
||||||
|
local DISABLED_LABEL_COLOR = {0.55, 0.55, 0.55}
|
||||||
|
|
||||||
|
-- Apply is disabled for as long as an invalid slot is pending, and with the
|
||||||
|
-- pill gone nothing else says so -- the ElvUI panel is drawn the same either
|
||||||
|
-- way. The label is greyed instead, exactly as the talent frame does it for the
|
||||||
|
-- active spec's Activate button.
|
||||||
|
--
|
||||||
|
-- Driven from the button's update because there's no event for the state
|
||||||
|
-- change: clearing the invalid slots enables Apply on the spot. The cached
|
||||||
|
-- state is what keeps that cheap -- the colours are only written when the state
|
||||||
|
-- actually flips.
|
||||||
|
local function UpdatePendingButtonState(button)
|
||||||
|
local enabled = Skin:IsEnabled(button)
|
||||||
|
|
||||||
|
if enabled == button.CoAPendingEnabled then return end
|
||||||
|
button.CoAPendingEnabled = enabled
|
||||||
|
|
||||||
|
-- Clearing the invalid slots enables Apply under the very cursor that just
|
||||||
|
-- clicked Clear invalid slots, so its hover border would stay dark until
|
||||||
|
-- the pointer moved off. This update already watches the state, so it
|
||||||
|
-- carries the border too.
|
||||||
|
Skin:RefreshButtonBorder(button)
|
||||||
|
|
||||||
|
local text = button.GetFontString and button:GetFontString()
|
||||||
|
if text then text:SetTextColor(unpack(enabled and LABEL_COLOR or DISABLED_LABEL_COLOR)) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The strip runs on every pass rather than once behind the skinned guard: the
|
||||||
|
-- art is only there to be cleared once a change is pending, and nothing fires
|
||||||
|
-- the frame's OnShow at that point -- the window is already open. The buttons'
|
||||||
|
-- own OnShow is what catches it, since that is exactly when they appear.
|
||||||
|
local function SkinPendingButton(name)
|
||||||
|
local button = _G[name]
|
||||||
|
if not button then return end
|
||||||
|
|
||||||
|
Skin:StripArtByFile(button, Skin.RedButtonArt)
|
||||||
|
|
||||||
|
if not button.CoASkinned then
|
||||||
|
button.CoASkinned = true
|
||||||
|
|
||||||
|
-- Stripped before templating: HandleButton adds its backdrop as regions
|
||||||
|
-- of this same button, so a strip afterwards takes the backdrop with the
|
||||||
|
-- pill.
|
||||||
|
Skin:Button(button)
|
||||||
|
|
||||||
|
button:HookScript("OnShow", function(self)
|
||||||
|
Skin:StripArtByFile(self, Skin.RedButtonArt)
|
||||||
|
end)
|
||||||
|
|
||||||
|
button:HookScript("OnUpdate", UpdatePendingButtonState)
|
||||||
|
end
|
||||||
|
|
||||||
|
UpdatePendingButtonState(button)
|
||||||
|
end
|
||||||
|
|
||||||
|
local PENDING_BUTTONS = {
|
||||||
|
"PlayerModelApplyButton",
|
||||||
|
"PlayerModelCancelButton",
|
||||||
|
"PlayerModelClearInvalidButton",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function SkinPendingButtons()
|
||||||
|
for _, suffix in ipairs(PENDING_BUTTONS) do
|
||||||
|
SkinPendingButton(FRAME_NAME..suffix)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SkinSearchBox()
|
||||||
|
S:HandleEditBox(_G[FRAME_NAME.."CollectionSearchBox"])
|
||||||
|
end
|
||||||
|
|
||||||
|
-- suffix is "Filter" or "Sorting" -- both are CollectionX buttons with a
|
||||||
|
-- matching CollectionXMenu popout (confirmed by probe), and both are the same
|
||||||
|
-- widget as the vanity frame's single dropdown, so both go through the shared
|
||||||
|
-- handler.
|
||||||
|
local function SkinCollectionDropdown(suffix)
|
||||||
|
Skin:Dropdown(_G[FRAME_NAME.."Collection"..suffix], _G[FRAME_NAME.."Collection"..suffix.."Menu"])
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SkinPagerArrows()
|
||||||
|
local prevButton = _G[FRAME_NAME.."CollectionPageLeftButton"]
|
||||||
|
local nextButton = _G[FRAME_NAME.."CollectionPageRightButton"]
|
||||||
|
|
||||||
|
if prevButton then S:HandleNextPrevButton(prevButton, "left") end
|
||||||
|
if nextButton then S:HandleNextPrevButton(nextButton, "right") end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Same art naming as the talent frame's tabs (confirmed by probe), so they go
|
||||||
|
-- through the same shared handler. No level parent is passed: these are proper
|
||||||
|
-- descendants of Collection rather than separately-placed siblings, so normal
|
||||||
|
-- parent/child z-order already puts them above the panel and the frame-level
|
||||||
|
-- bump the talent tabs need doesn't apply.
|
||||||
|
--
|
||||||
|
-- Named PoolFrameAppearanceTypeTabTemplate1 through 8 (confirmed by probe),
|
||||||
|
-- not pooled/created dynamically like the talent frame's spec choices, so a
|
||||||
|
-- plain indexed loop is enough -- no OnShow hook needed to catch late pool
|
||||||
|
-- fills.
|
||||||
|
local TAB_COUNT = 8
|
||||||
|
|
||||||
|
local function SkinCategoryTabs()
|
||||||
|
for i = 1, TAB_COUNT do
|
||||||
|
Skin:Tab(_G[FRAME_NAME.."CollectionPoolFrameAppearanceTypeTabTemplate"..i])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SkinContents()
|
||||||
|
-- Scale is set on the shared container while this window is the open one,
|
||||||
|
-- which carries the tab row along with it (see Skinning.lua).
|
||||||
|
Skin:ApplyWindowScale("wardrobeScale")
|
||||||
|
Skin:CollectionTabs(_G[FRAME_NAME])
|
||||||
|
Skin:Title(_G[FRAME_NAME.."TitleText"])
|
||||||
|
Skin:CloseButton(_G[FRAME_NAME.."CloseButton"])
|
||||||
|
SkinActionButtons()
|
||||||
|
SkinPendingButtons()
|
||||||
|
SkinSearchBox()
|
||||||
|
SkinCollectionDropdown("Filter")
|
||||||
|
SkinCollectionDropdown("Sorting")
|
||||||
|
SkinPagerArrows()
|
||||||
|
SkinCategoryTabs()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function SkinFrame(frame)
|
||||||
|
if not frame.CoASkinned then
|
||||||
|
frame.CoASkinned = true
|
||||||
|
|
||||||
|
-- The native NineSlice panel draws its own ornate border/background on
|
||||||
|
-- top of the Transparent template's border, doubling up. Same fix as
|
||||||
|
-- the talent frame: strip and hide it.
|
||||||
|
Skin:HideArt(_G[FRAME_NAME.."NineSlice"])
|
||||||
|
|
||||||
|
-- The round medallion overhangs the top-left corner (confirmed by
|
||||||
|
-- probe: 61x61, TOPLEFT -6,8 -- matches the frame's own corner, not a
|
||||||
|
-- counter badge elsewhere, unlike Vanity's same-named "Portrait2"
|
||||||
|
-- texture). No flat equivalent, so it goes rather than getting
|
||||||
|
-- reskinned, same treatment as the talent frame's portrait.
|
||||||
|
Skin:HideArt(_G[FRAME_NAME.."PortraitFrame"])
|
||||||
|
|
||||||
|
-- The frame's own regions (confirmed by probe: 4 total, all BACKGROUND/
|
||||||
|
-- BORDER/OVERLAY textures, none of them functional -- unlike Vanity's
|
||||||
|
-- frame, nothing here doubles as a counter badge) are the wood-panel
|
||||||
|
-- background art, so this one is safe to strip before templating.
|
||||||
|
Skin:Panel(frame)
|
||||||
|
|
||||||
|
-- InsetOverlay's NineSlice and the ShadowOverlay are separate decorative
|
||||||
|
-- art layered over the item grid area (8-piece atlas borders/shadow
|
||||||
|
-- edges, confirmed via probe), stripped outright -- no functional
|
||||||
|
-- content lives on either.
|
||||||
|
Skin:HideArt(_G[FRAME_NAME.."CollectionInsetOverlayNineSlice"])
|
||||||
|
|
||||||
|
-- The actual panel behind the grid: Collection itself (confirmed via
|
||||||
|
-- /fstack -- InsetOverlay was the wrong target, its own rect doesn't
|
||||||
|
-- match the visible panel). Collection is the grid's real parent, so
|
||||||
|
-- no frame-level/strata juggling needed -- children always draw above
|
||||||
|
-- their own parent.
|
||||||
|
--
|
||||||
|
-- 16 of its own regions turned out to hold real art -- a background
|
||||||
|
-- tile plus ~14 atlas border/corner pieces (the ornate corners
|
||||||
|
-- /fstack couldn't ever pick out, since loose regions aren't frames
|
||||||
|
-- and don't show up there) -- none of it functional (the "Collected
|
||||||
|
-- 50/2744" counter is a separate FontString region, untouched by
|
||||||
|
-- StripTextures). Painting an opaque/red template over it earlier
|
||||||
|
-- just masked it; it was still there underneath, which is why
|
||||||
|
-- Transparent let it bleed back through. Strip first, then template,
|
||||||
|
-- same order as the outer frame -- SetTemplate's own WHITE8X8 backdrop
|
||||||
|
-- pieces land as regions on this same frame too, so stripping after
|
||||||
|
-- would wipe them right back off.
|
||||||
|
Skin:Panel(_G[FRAME_NAME.."Collection"])
|
||||||
|
|
||||||
|
-- The 3D model preview's vanilla border is an anonymous child (first
|
||||||
|
-- of PlayerModel's own, confirmed by probe: 8-piece "UIFrame" atlas
|
||||||
|
-- border, sized to match the model panel). No name to key off of, same
|
||||||
|
-- as Vanity's pager arrows -- picked out positionally instead. The
|
||||||
|
-- race-specific scenic backdrop texture is PlayerModel's own region,
|
||||||
|
-- not this child's, so it's untouched.
|
||||||
|
local playerModel = _G[FRAME_NAME.."PlayerModel"]
|
||||||
|
local modelBorder = playerModel and select(1, playerModel:GetChildren())
|
||||||
|
if modelBorder and modelBorder:GetObjectType() == "Frame" then
|
||||||
|
Skin:HideArt(modelBorder)
|
||||||
|
end
|
||||||
|
|
||||||
|
Skin:HideArt(_G[FRAME_NAME.."CollectionShadowOverlay"])
|
||||||
|
|
||||||
|
frame:HookScript("OnShow", SkinContents)
|
||||||
|
end
|
||||||
|
|
||||||
|
SkinContents()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function TryHook()
|
||||||
|
local frame = _G[FRAME_NAME]
|
||||||
|
if not frame then return false end
|
||||||
|
|
||||||
|
SkinFrame(frame)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function CoA:InitializeWardrobeFrame()
|
||||||
|
if not E.private.skins.blizzard.enable then return end
|
||||||
|
|
||||||
|
Skin:OnFrameAvailable(TryHook)
|
||||||
|
end
|
||||||
@@ -1,6 +1,20 @@
|
|||||||
# ElvUI_CoA
|
# ElvUI_CoA (WIP)
|
||||||
|
|
||||||
ElvUI plugin that hides and skins the custom Conquest of Azeroth (Ascension WoW, client 3.3.5) server frames not covered by stock ElvUI.
|
ElvUI plugin that hides and skins the custom CoA (3.3.5) frames not covered by stock ElvUI.
|
||||||
|
|
||||||
|
## Implemented
|
||||||
|
|
||||||
|
- Character advancement, vanity, wardrobe frames: skin, scale
|
||||||
|
- Challenges frame: skin
|
||||||
|
- Class resource frames: hiding
|
||||||
|
- Extra action button frame: skin, anchor, size
|
||||||
|
- Instance swap frame: skin, anchor, font settings
|
||||||
|
- Interface/Video/Audio options: skin the widgets ElvUI's own pass misses
|
||||||
|
- Only highlight unitframes if dispellable by your CoA class.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Download the repo as .zip, extract in `Interface\AddOns`, remove `-master` from the folder name.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,523 @@
|
|||||||
local E, L, V, P, G = unpack(ElvUI)
|
local E, L, V, P, G = unpack(ElvUI)
|
||||||
local EP = E.Libs.EP
|
local EP = E.Libs.EP
|
||||||
|
local ACH = LibStub("LibAceConfigHelper")
|
||||||
|
local AceDB = LibStub("AceDB-3.0")
|
||||||
|
local AceDBOptions = LibStub("AceDBOptions-3.0")
|
||||||
|
|
||||||
local AddOnName = ...
|
local AddOnName = ...
|
||||||
|
|
||||||
|
BINDING_HEADER_COA = "Conquest of Azeroth"
|
||||||
|
|
||||||
|
-- TODO: hook up server restart frame (RestartTimerFrame)
|
||||||
|
|
||||||
local CoA = E:NewModule("CoA", "AceEvent-3.0", "AceTimer-3.0")
|
local CoA = E:NewModule("CoA", "AceEvent-3.0", "AceTimer-3.0")
|
||||||
E.CoA = CoA
|
E.CoA = CoA
|
||||||
|
|
||||||
V.CoA = {}
|
local defaults = {
|
||||||
|
profile = {
|
||||||
|
skins = {
|
||||||
|
extraActionButton = true,
|
||||||
|
instanceSwap = true,
|
||||||
|
collections = {
|
||||||
|
enable = true,
|
||||||
|
-- Multipliers on each frame's own scale, so 1 is "as the server
|
||||||
|
-- built it" rather than a fixed size.
|
||||||
|
advancementScale = 1,
|
||||||
|
vanityScale = 1,
|
||||||
|
wardrobeScale = 1,
|
||||||
|
},
|
||||||
|
-- Deliberately not inside the collections group: that switch covers
|
||||||
|
-- the three windows that share the Collections container and its tab
|
||||||
|
-- row, and this one is a separate window on UIParent with a tab row
|
||||||
|
-- of its own.
|
||||||
|
challenges = true,
|
||||||
|
},
|
||||||
|
extraActionButtonSize = 52,
|
||||||
|
instanceButtonFont = "PT Sans Narrow",
|
||||||
|
instanceButtonFontSize = 12,
|
||||||
|
instanceButtonFontOutline = "OUTLINE",
|
||||||
|
dispelHighlightOnlyMine = false,
|
||||||
|
dispelHighlightDetectTalents = true,
|
||||||
|
hideResourceSegmentBar = false,
|
||||||
|
hideResourceOrb = false,
|
||||||
|
hideResourceBar = false,
|
||||||
|
hideMultiCastActionBar = false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if self.UpdateFrameScales then self:UpdateFrameScales() end
|
||||||
|
end
|
||||||
|
|
||||||
|
CoA:RegisterEvent("ADDON_LOADED", function(_, addon)
|
||||||
|
if addon ~= AddOnName then return end
|
||||||
|
CoA:UnregisterEvent("ADDON_LOADED")
|
||||||
|
|
||||||
|
CoA.db = AceDB:New("ElvUI_CoADB", defaults)
|
||||||
|
|
||||||
|
-- One-time migration: earlier versions pinned every character to a single
|
||||||
|
-- shared "Default" profile. Move each character to its own name-based
|
||||||
|
-- profile (copying over the settings it already had) the first time it logs in.
|
||||||
|
if not CoA.db.char.migratedSharedProfile then
|
||||||
|
local charProfile = CoA.db.keys.char
|
||||||
|
|
||||||
|
if CoA.db:GetCurrentProfile() == "Default" and charProfile ~= "Default" then
|
||||||
|
CoA.db:SetProfile(charProfile)
|
||||||
|
CoA.db:CopyProfile("Default", true)
|
||||||
|
end
|
||||||
|
|
||||||
|
CoA.db.char.migratedSharedProfile = true
|
||||||
|
end
|
||||||
|
|
||||||
|
CoA.db.RegisterCallback(CoA, "OnProfileChanged", "RefreshConfig")
|
||||||
|
CoA.db.RegisterCallback(CoA, "OnProfileCopied", "RefreshConfig")
|
||||||
|
CoA.db.RegisterCallback(CoA, "OnProfileReset", "RefreshConfig")
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- One slider per window rather than a single shared one: the three don't ship
|
||||||
|
-- at the same scale, so a shared value would only line them up by flattening
|
||||||
|
-- the difference the server built in.
|
||||||
|
local function scaleOption(order, name, key)
|
||||||
|
return {
|
||||||
|
order = order,
|
||||||
|
type = "range",
|
||||||
|
name = name,
|
||||||
|
min = 0.5,
|
||||||
|
max = 1.5,
|
||||||
|
step = 0.01,
|
||||||
|
get = function() return CoA.db.profile.skins.collections[key] end,
|
||||||
|
set = function(_, value)
|
||||||
|
CoA.db.profile.skins.collections[key] = value
|
||||||
|
|
||||||
|
if CoA.UpdateFrameScales then
|
||||||
|
CoA:UpdateFrameScales()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
local function getOptions()
|
local function getOptions()
|
||||||
|
local profiles = AceDBOptions:GetOptionsTable(CoA.db)
|
||||||
|
profiles.order = 5
|
||||||
|
|
||||||
local options = {
|
local options = {
|
||||||
order = 55,
|
order = 55,
|
||||||
type = "group",
|
type = "group",
|
||||||
childGroups = "tab",
|
childGroups = "tab",
|
||||||
name = string.format("|cff1784d1%s|r", "Conquest of Azeroth"),
|
name = string.format("|cff1784d1%s|r", "Conquest of Azeroth"),
|
||||||
args = {
|
args = {
|
||||||
extraActionButton = {
|
-- Frame skins live under their own tab, laid out as a vertical tree
|
||||||
|
-- rather than more horizontal tabs: one entry per skinned frame, and
|
||||||
|
-- there are a lot more of those coming.
|
||||||
|
skins = {
|
||||||
order = 1,
|
order = 1,
|
||||||
type = "group",
|
type = "group",
|
||||||
name = "Extra Action Button",
|
childGroups = "tree",
|
||||||
args = {},
|
name = "Skins",
|
||||||
|
args = {
|
||||||
|
extraActionButton = {
|
||||||
|
order = 1,
|
||||||
|
type = "group",
|
||||||
|
name = "Extra Action Button",
|
||||||
|
args = {
|
||||||
|
header = {
|
||||||
|
order = 1,
|
||||||
|
type = "header",
|
||||||
|
name = "Extra Action Button",
|
||||||
|
},
|
||||||
|
-- Skinning is one-way: the native art is stripped and
|
||||||
|
-- replaced in place, so turning a skin off can only take
|
||||||
|
-- effect on the next load. Hence the reload prompt.
|
||||||
|
enable = {
|
||||||
|
order = 2,
|
||||||
|
type = "toggle",
|
||||||
|
name = "Enable",
|
||||||
|
desc = "Skin the Extra Action Button. Requires a UI reload.",
|
||||||
|
get = function() return CoA.db.profile.skins.extraActionButton end,
|
||||||
|
set = function(_, value)
|
||||||
|
CoA.db.profile.skins.extraActionButton = value
|
||||||
|
E:StaticPopup_Show("CONFIG_RL")
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
desc = {
|
||||||
|
order = 3,
|
||||||
|
type = "description",
|
||||||
|
name = "You can move this element with Toggle Anchors.\n",
|
||||||
|
},
|
||||||
|
size = {
|
||||||
|
order = 4,
|
||||||
|
disabled = function() return not CoA.db.profile.skins.extraActionButton end,
|
||||||
|
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 CoA.db.profile.extraActionButtonSize end,
|
||||||
|
set = function(_, value)
|
||||||
|
CoA.db.profile.extraActionButtonSize = value
|
||||||
|
|
||||||
|
if CoA.UpdateExtraActionButtonSize then
|
||||||
|
CoA:UpdateExtraActionButtonSize()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
instanceSwap = {
|
||||||
|
order = 2,
|
||||||
|
type = "group",
|
||||||
|
name = "Instance Swap",
|
||||||
|
args = {
|
||||||
|
header = {
|
||||||
|
order = 1,
|
||||||
|
type = "header",
|
||||||
|
name = "Instance Swap",
|
||||||
|
},
|
||||||
|
enable = {
|
||||||
|
order = 2,
|
||||||
|
type = "toggle",
|
||||||
|
name = "Enable",
|
||||||
|
desc = "Skin the Instance Swap button. Requires a UI reload.",
|
||||||
|
get = function() return CoA.db.profile.skins.instanceSwap end,
|
||||||
|
set = function(_, value)
|
||||||
|
CoA.db.profile.skins.instanceSwap = value
|
||||||
|
E:StaticPopup_Show("CONFIG_RL")
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
desc = {
|
||||||
|
order = 3,
|
||||||
|
type = "description",
|
||||||
|
name = "You can move this element with Toggle Anchors.\n",
|
||||||
|
},
|
||||||
|
instanceFont = {
|
||||||
|
order = 4,
|
||||||
|
disabled = function() return not CoA.db.profile.skins.instanceSwap end,
|
||||||
|
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()
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
-- One entry for the whole talent window: its own frame, the
|
||||||
|
-- tab row along its bottom, and the Vanity and Wardrobe
|
||||||
|
-- windows those tabs open. They're separate frames but one
|
||||||
|
-- feature to the player, and they're skinned as a set --
|
||||||
|
-- named for Collections, the container they all hang off.
|
||||||
|
collections = {
|
||||||
|
order = 3,
|
||||||
|
type = "group",
|
||||||
|
name = "Advancement/Vanity/Wardrobe",
|
||||||
|
args = {
|
||||||
|
header = {
|
||||||
|
order = 1,
|
||||||
|
type = "header",
|
||||||
|
name = "Advancement/Vanity/Wardrobe",
|
||||||
|
},
|
||||||
|
enable = {
|
||||||
|
order = 2,
|
||||||
|
type = "toggle",
|
||||||
|
name = "Enable",
|
||||||
|
desc = "Skin the talent window, its tabs, and the Vanity and Wardrobe windows. Requires a UI reload.",
|
||||||
|
get = function() return CoA.db.profile.skins.collections.enable end,
|
||||||
|
set = function(_, value)
|
||||||
|
CoA.db.profile.skins.collections.enable = value
|
||||||
|
E:StaticPopup_Show("CONFIG_RL")
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
scale = {
|
||||||
|
order = 3,
|
||||||
|
type = "group",
|
||||||
|
inline = true,
|
||||||
|
name = "Scale",
|
||||||
|
disabled = function() return not CoA.db.profile.skins.collections.enable end,
|
||||||
|
args = {
|
||||||
|
advancementScale = scaleOption(1, "Advancement", "advancementScale"),
|
||||||
|
vanityScale = scaleOption(2, "Vanity", "vanityScale"),
|
||||||
|
wardrobeScale = scaleOption(3, "Wardrobe", "wardrobeScale"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
challenges = {
|
||||||
|
order = 4,
|
||||||
|
type = "group",
|
||||||
|
name = "Challenges",
|
||||||
|
args = {
|
||||||
|
header = {
|
||||||
|
order = 1,
|
||||||
|
type = "header",
|
||||||
|
name = "Challenges",
|
||||||
|
},
|
||||||
|
enable = {
|
||||||
|
order = 2,
|
||||||
|
type = "toggle",
|
||||||
|
name = "Enable",
|
||||||
|
desc = "Skin the Challenges window and its tabs. Requires a UI reload.",
|
||||||
|
get = function() return CoA.db.profile.skins.challenges end,
|
||||||
|
set = function(_, value)
|
||||||
|
CoA.db.profile.skins.challenges = value
|
||||||
|
E:StaticPopup_Show("CONFIG_RL")
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
instanceSwap = {
|
classResources = {
|
||||||
order = 2,
|
order = 2,
|
||||||
type = "group",
|
type = "group",
|
||||||
name = "Instance Swap",
|
name = "Class Resources",
|
||||||
args = {},
|
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 CoAResourceSegmentBar",
|
||||||
|
width = "full",
|
||||||
|
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 CoAResourceOrb",
|
||||||
|
width = "full",
|
||||||
|
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 CoAResourceBar",
|
||||||
|
width = "full",
|
||||||
|
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 CoAMultiCastActionBarFrame",
|
||||||
|
width = "full",
|
||||||
|
get = function() return CoA.db.profile.hideMultiCastActionBar end,
|
||||||
|
set = function(_, value)
|
||||||
|
CoA.db.profile.hideMultiCastActionBar = value
|
||||||
|
|
||||||
|
if CoA.UpdateClassResourceVisibility then
|
||||||
|
CoA:UpdateClassResourceVisibility()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
hideAll = {
|
||||||
|
order = 7,
|
||||||
|
type = "execute",
|
||||||
|
name = "Hide All",
|
||||||
|
func = function()
|
||||||
|
CoA.db.profile.hideResourceSegmentBar = true
|
||||||
|
CoA.db.profile.hideResourceOrb = true
|
||||||
|
CoA.db.profile.hideResourceBar = true
|
||||||
|
CoA.db.profile.hideMultiCastActionBar = true
|
||||||
|
|
||||||
|
if CoA.UpdateClassResourceVisibility then
|
||||||
|
CoA:UpdateClassResourceVisibility()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
showAll = {
|
||||||
|
order = 8,
|
||||||
|
type = "execute",
|
||||||
|
name = "Show All",
|
||||||
|
func = function()
|
||||||
|
CoA.db.profile.hideResourceSegmentBar = false
|
||||||
|
CoA.db.profile.hideResourceOrb = false
|
||||||
|
CoA.db.profile.hideResourceBar = false
|
||||||
|
CoA.db.profile.hideMultiCastActionBar = false
|
||||||
|
|
||||||
|
if CoA.UpdateClassResourceVisibility then
|
||||||
|
CoA:UpdateClassResourceVisibility()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
dispelHighlight = {
|
||||||
|
order = 3,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
detectTalents = {
|
||||||
|
order = 3,
|
||||||
|
type = "toggle",
|
||||||
|
name = "Detect Talents",
|
||||||
|
desc = "Read the talents that grant extra dispel types from your current build. Turn this off to fall back to what your class can dispel without any talent.",
|
||||||
|
disabled = function() return not CoA.db.profile.dispelHighlightOnlyMine end,
|
||||||
|
get = function() return CoA.db.profile.dispelHighlightDetectTalents end,
|
||||||
|
set = function(_, value)
|
||||||
|
CoA.db.profile.dispelHighlightDetectTalents = value
|
||||||
|
|
||||||
|
if CoA.UpdateDispelHighlight then
|
||||||
|
CoA:UpdateDispelHighlight()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
profiles = profiles,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
E.Options.args.CoA = options
|
E.Options.args.CoA = options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function RegisterMoverCategory()
|
||||||
|
for _, layout in ipairs(E.ConfigModeLayouts) do
|
||||||
|
if layout == "COA" then return end
|
||||||
|
end
|
||||||
|
|
||||||
|
tinsert(E.ConfigModeLayouts, "COA")
|
||||||
|
E.ConfigModeLocalizedStrings.COA = "CoA"
|
||||||
|
end
|
||||||
|
|
||||||
function CoA:Initialize()
|
function CoA:Initialize()
|
||||||
|
RegisterMoverCategory()
|
||||||
|
|
||||||
EP:RegisterPlugin(AddOnName, getOptions)
|
EP:RegisterPlugin(AddOnName, getOptions)
|
||||||
|
|
||||||
if self.InitializeExtraActionBar then
|
-- ADDON_LOADED normally beats module init, but fall back to the defaults
|
||||||
|
-- rather than error out if a skin gets initialized before the DB exists.
|
||||||
|
local skins = self.db and self.db.profile.skins or defaults.profile.skins
|
||||||
|
|
||||||
|
if self.InitializeExtraActionBar and skins.extraActionButton then
|
||||||
self:InitializeExtraActionBar()
|
self:InitializeExtraActionBar()
|
||||||
end
|
end
|
||||||
|
|
||||||
if self.InitializeLayerPicker then
|
if self.InitializeLayerPicker and skins.instanceSwap then
|
||||||
self:InitializeLayerPicker()
|
self:InitializeLayerPicker()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if self.InitializeDispelHighlight then
|
||||||
|
self:InitializeDispelHighlight()
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.InitializeClassResources then
|
||||||
|
self:InitializeClassResources()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- One switch for all three: the vanity and wardrobe windows are the talent
|
||||||
|
-- frame's own tabs, so skinning one without the others reads as a bug.
|
||||||
|
if skins.collections.enable then
|
||||||
|
if self.InitializeTalentFrame then
|
||||||
|
self:InitializeTalentFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.InitializeVanityFrame then
|
||||||
|
self:InitializeVanityFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.InitializeWardrobeFrame then
|
||||||
|
self:InitializeWardrobeFrame()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Its own switch rather than a place in the group above: the Challenges
|
||||||
|
-- window isn't reachable from the talent frame's tabs, so turning it off
|
||||||
|
-- doesn't leave a half-skinned set behind.
|
||||||
|
if self.InitializeChallengesFrame and skins.challenges then
|
||||||
|
self:InitializeChallengesFrame()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- No switch of its own: this one only fills the gaps ElvUI's own Blizzard
|
||||||
|
-- options pass leaves, so it's on wherever that pass is, and its own gate on
|
||||||
|
-- E.private.skins.blizzard.enable is the same one ElvUI checks.
|
||||||
|
if self.InitializeInterfaceOptions then
|
||||||
|
self:InitializeInterfaceOptions()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function InitializeCallback()
|
local function InitializeCallback()
|
||||||
|
|||||||
Reference in New Issue
Block a user