TabMacros: tab-on-action-bar service shared with other addons

The character sheet's tab macros move to TabMacros.lua, which any addon or
AIO window can call (Register, Pickup, EnableDrag); ActionBar's
ActionButton_Update hook shows each registered tab's icon.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adrestie
2026-10-19 05:36:03 +02:00
parent fe70f4b34f
commit 1f1cd55486
9 changed files with 213 additions and 128 deletions
+3 -3
View File
@@ -439,9 +439,9 @@ end
-- The client rewrites the normal texture on every button update: silence it again after.
if hooksecurefunc then
hooksecurefunc("ActionButton_Update", function(self)
-- a character sheet tab on the bar shows the tab's icon (CharacterFrame.lua)
if ForeverUI.ShowCharacterTabIcon then
ForeverUI.ShowCharacterTabIcon(self)
-- a window tab on the bar shows the tab's icon (TabMacros.lua)
if ForeverUI.TabMacros then
ForeverUI.TabMacros.ShowIcon(self)
end
if self and self.foreverSkinned then
silenceNormalTexture(self)
+19 -120
View File
@@ -1710,20 +1710,15 @@ if hooksecurefunc and type(_G["CharacterFrame_TabBoundsCheck"]) == "function" th
hooksecurefunc("CharacterFrame_TabBoundsCheck", keepTabWidth)
end
-- Side tabs on the action bars. 3.3.5 bars hold spells, items, macros, equipment sets and
-- companions only: a dragged tab becomes an account macro whose body opens its screen, made once
-- and found again by its body. On a bar the macro shows the tab's icon (ActionButton_Update hook
-- below). Making a macro needs a free slot and is refused in combat; picking up an existing one
-- is not.
-- Side tabs on the action bars (TabMacros.lua): a dragged tab becomes a macro whose body opens
-- its screen; on a bar it shows the tab's icon.
local TAB_MACRO_BODY = "/run ForeverUI.ToggleCharacterTab(%s)"
local TAB_MACRO_NAMES = {
[1] = "CHARACTER", [2] = "PET", [3] = "REPUTATION", [4] = "SKILLS", [5] = "CURRENCY",
pvp = "PVP", stats = "STATISTICS", mounts = "MOUNTS",
}
local MACRO_NAME_BYTES = 16
-- Macro icon, seen on the cursor and in the macro window: the nearest icon of the client's
-- macro list (Interface\Icons), the question mark (index 1) when none is there
-- Macro icon, seen on the cursor and in the macro window: the nearest game icon
local TAB_MACRO_ICONS = {
[1] = "INV_Chest_Cloth_17", [3] = "Achievement_Reputation_01", [4] = "Trade_Engineering",
[5] = "INV_Misc_Coin_01", stats = "INV_Misc_Note_01", mounts = "Ability_Mount_RidingHorse",
@@ -1735,18 +1730,6 @@ local function tabMacroBody(key)
type(key) == "number" and tostring(key) or ('"' .. key .. '"'))
end
-- The tab a macro body opens, or nil
local function tabMacroKey(body)
local key = body and body:match('ForeverUI%.ToggleCharacterTab%("?(%w+)"?%)')
if not key then
return nil
end
key = tonumber(key) or key
if TAB_MACRO_NAMES[key] then
return key
end
end
-- The tab's icon, also before the sheet was ever opened (its tabs do not exist yet)
local function tabIcon(key)
local tab = tabs[key]
@@ -1765,94 +1748,35 @@ local function tabIcon(key)
return TAB_ICONS[key]
end
-- Cuts a name to the macro limit without splitting a UTF-8 character
local function macroName(key)
local name = _G[TAB_MACRO_NAMES[key]] or TAB_MACRO_NAMES[key]
if #name <= MACRO_NAME_BYTES then
return name
end
local cut = MACRO_NAME_BYTES
while cut > 0 and name:byte(cut + 1) and name:byte(cut + 1) >= 0x80
and name:byte(cut + 1) < 0xC0 do
cut = cut - 1
end
return name:sub(1, cut)
end
local function findTabMacro(key)
if not GetMacroInfo then
return nil
end
local body = tabMacroBody(key)
local last = (MAX_ACCOUNT_MACROS or 36) + (MAX_CHARACTER_MACROS or 18)
for index = 1, last do
local _, _, macroBody = GetMacroInfo(index)
if macroBody and macroBody:gsub("%s+$", "") == body then
return index
end
end
end
local function macroIconIndex(key)
local function tabMacroIcon(key)
local wanted = TAB_MACRO_ICONS[key]
if key == 2 then
wanted = PET_TAB[select(2, UnitClass("player")) or ""] or PET_TAB.HUNTER
return PET_TAB[select(2, UnitClass("player")) or ""] or PET_TAB.HUNTER
elseif type(wanted) == "table" then
wanted = wanted[(UnitFactionGroup and UnitFactionGroup("player")) or "Alliance"]
return wanted[(UnitFactionGroup and UnitFactionGroup("player")) or "Alliance"]
or wanted.Alliance
end
if not wanted or not GetNumMacroIcons or not GetMacroIconInfo then
return 1
end
wanted = "\\" .. wanted:lower()
for index = 1, GetNumMacroIcons() do
local texture = GetMacroIconInfo(index)
if texture and texture:lower():sub(-#wanted) == wanted then
return index
end
end
return 1
return wanted
end
-- Puts the tab's macro on the cursor, making it first if needed
local function pickupTab(key)
if not PickupMacro or not TAB_MACRO_NAMES[key] then
return
end
local index = findTabMacro(key)
if not index then
if InCombatLockdown() then
UIErrorsFrame:AddMessage(ERR_NOT_IN_COMBAT, 1, 0.1, 0.1, 1)
return
end
local account, character = GetNumMacros()
local perCharacter
if (account or 0) >= (MAX_ACCOUNT_MACROS or 36) then
if (character or 0) >= (MAX_CHARACTER_MACROS or 18) then
UIErrorsFrame:AddMessage(L.CHARACTERFRAME_TAB_MACROS_FULL, 1, 0.1, 0.1, 1)
return
end
perCharacter = 1
end
-- On the bars the tab's own icon replaces the macro's
index = CreateMacro(macroName(key), macroIconIndex(key), tabMacroBody(key),
perCharacter, perCharacter)
if not index then
return
end
end
PickupMacro(index)
local function tabMacroInfo(key)
return {
name = function() return _G[TAB_MACRO_NAMES[key]] or TAB_MACRO_NAMES[key] end,
icon = function() return tabIcon(key) end,
macroIcon = function() return tabMacroIcon(key) end,
}
end
-- The bars show the tabs' icons from login, before the sheet is opened
for key in pairs(TAB_MACRO_NAMES) do
ForeverUI.TabMacros.Register(tabMacroBody(key), tabMacroInfo(key))
end
local function enableTabDrag(tab, key)
if tab.foreverDrag then
return
end
tab:RegisterForDrag("LeftButton")
tab:SetScript("OnDragStart", function()
GameTooltip:Hide()
pickupTab(key)
end)
ForeverUI.TabMacros.EnableDrag(tab, tabMacroBody(key), tabMacroInfo(key))
tab.foreverDrag = true
end
@@ -1891,31 +1815,6 @@ function ForeverUI.ToggleCharacterTab(key)
openOwnScreen(group)
end
-- A tab macro on a bar shows the tab's icon, not the macro's question mark
local function showTabIcon(button)
local action = button and button.action
if not action or not GetActionInfo then
return
end
local kind, id = GetActionInfo(action)
if kind ~= "macro" or not GetMacroInfo then
return
end
local _, _, body = GetMacroInfo(id)
local key = tabMacroKey(body)
local icon = key and _G[(button:GetName() or "") .. "Icon"]
local texture = key and tabIcon(key)
if icon and texture then
icon:SetTexture(texture)
end
end
-- Called from ActionBar.lua's ActionButton_Update hook (one hook for every button)
ForeverUI.ShowCharacterTabIcon = showTabIcon
ForeverUI.CharacterTabMacro = { body = tabMacroBody, key = tabMacroKey, icon = tabIcon,
pickup = pickupTab }
-- Creates one of our side tabs, skinned like the client's.
-- key: tabs table key; name: frame name; group: ForeverUI.Panes group it opens
local function createSideTab(key, name, icon, tooltip, group)
+1
View File
@@ -40,6 +40,7 @@ RaidFrame.lua
CastBar.lua
ActionBar.lua
MultiBars.lua
TabMacros.lua
StanceBar.lua
PetBar.lua
TotemBar.lua
+175
View File
@@ -0,0 +1,175 @@
-- Window tabs on the action bars. 3.3.5 bars hold spells, items, macros, equipment sets and
-- companions only: a dragged tab becomes an account macro whose body opens its window, made once
-- and found again by its body. On a bar the macro shows the tab's icon, not the macro's. Making a
-- macro needs a free slot and is refused in combat; picking up an existing one is not.
-- The character sheet's side tabs use it (CharacterFrame.lua), and so can other addons and the
-- server modules' AIO windows when ForeverUI is there:
-- ForeverUI.TabMacros.Register(body, info): the macro with this body is a tab. info:
-- name macro name, cut to 16 bytes (a value, or a function returning it)
-- icon texture shown on the bars (same)
-- macroIcon an Interface\Icons file name, for the cursor and the macro window; the question
-- mark when the client's macro icon list does not have it (same)
-- ForeverUI.TabMacros.Pickup(body, info): registers, then puts the macro on the cursor.
-- ForeverUI.TabMacros.EnableDrag(button, body, info): a left drag of the button does Pickup.
-- body may be a function(button) returning body, info, for a button reused by several tabs.
local M = {}
ForeverUI.TabMacros = M
local L = ForeverUI.L
local MACRO_NAME_BYTES = 16
local BAR_PREFIXES = { "ActionButton", "BonusActionButton", "MultiBarBottomLeftButton",
"MultiBarBottomRightButton", "MultiBarRightButton", "MultiBarLeftButton" }
-- body -> info
local registry = {}
local function value(v)
if type(v) == "function" then
return v()
end
return v
end
local function trim(body)
return type(body) == "string" and (body:gsub("%s+$", "")) or nil
end
-- Cuts a name to the macro limit without splitting a UTF-8 character
local function cutName(name)
if #name <= MACRO_NAME_BYTES then
return name
end
local cut = MACRO_NAME_BYTES
while cut > 0 and name:byte(cut + 1) and name:byte(cut + 1) >= 0x80
and name:byte(cut + 1) < 0xC0 do
cut = cut - 1
end
return name:sub(1, cut)
end
local function find(body)
if not GetMacroInfo then
return nil
end
local last = (MAX_ACCOUNT_MACROS or 36) + (MAX_CHARACTER_MACROS or 18)
for index = 1, last do
local _, _, macroBody = GetMacroInfo(index)
if trim(macroBody) == body then
return index
end
end
end
-- Index of an icon in the client's macro list, 1 (the question mark) when absent
local function iconIndex(wanted)
if type(wanted) ~= "string" or not GetNumMacroIcons or not GetMacroIconInfo then
return 1
end
wanted = "\\" .. wanted:lower()
for index = 1, GetNumMacroIcons() do
local texture = GetMacroIconInfo(index)
if texture and texture:lower():sub(-#wanted) == wanted then
return index
end
end
return 1
end
-- An action button holding a tab macro shows the tab's icon. Called after ActionButton_Update
-- (ActionBar.lua) and on each button when a tab is registered.
function M.ShowIcon(button)
local action = button and button.action
if not action or not GetActionInfo or not GetMacroInfo then
return
end
local kind, id = GetActionInfo(action)
if kind ~= "macro" then
return
end
local _, _, body = GetMacroInfo(id)
local info = registry[trim(body) or ""]
local texture = info and value(info.icon)
local icon = texture and button.GetName and _G[(button:GetName() or "") .. "Icon"]
if icon then
icon:SetTexture(texture)
end
end
local function refreshBars()
for _, prefix in ipairs(BAR_PREFIXES) do
for index = 1, NUM_ACTIONBAR_BUTTONS or 12 do
local button = _G[prefix .. index]
if button then
M.ShowIcon(button)
end
end
end
end
function M.Register(body, info)
body = trim(body)
if not body or type(info) ~= "table" then
return
end
local known = registry[body]
registry[body] = info
-- A module loaded after the bars: its macros get their icon now
if not known then
refreshBars()
end
end
function M.Pickup(body, info)
body = trim(body)
if not body or not PickupMacro then
return
end
M.Register(body, info)
local index = find(body)
if not index then
if InCombatLockdown() then
UIErrorsFrame:AddMessage(ERR_NOT_IN_COMBAT, 1, 0.1, 0.1, 1)
return
end
local account, character = GetNumMacros()
local perCharacter
if (account or 0) >= (MAX_ACCOUNT_MACROS or 36) then
if (character or 0) >= (MAX_CHARACTER_MACROS or 18) then
UIErrorsFrame:AddMessage(L.TABMACROS_FULL, 1, 0.1, 0.1, 1)
return
end
perCharacter = 1
end
local name = cutName(tostring(info and value(info.name) or ""))
if name == "" then
name = "?"
end
index = CreateMacro(name, iconIndex(info and value(info.macroIcon)), body,
perCharacter, perCharacter)
if not index then
return
end
end
PickupMacro(index)
end
function M.EnableDrag(button, body, info)
if not button then
return
end
if type(body) ~= "function" then
M.Register(body, info)
end
button:RegisterForDrag("LeftButton")
button:SetScript("OnDragStart", function(self)
local b, i = body, info
if type(body) == "function" then
b, i = body(self)
end
GameTooltip:Hide()
M.Pickup(b, i)
end)
end
M.registry = registry
+3 -1
View File
@@ -77,7 +77,6 @@ ForeverUI.AddTexts("deDE", {
CHARACTERFRAME_MODEL_USAGE = "Modell: echelle <n> | position <x> <y> <z> | position defaut",
CHARACTERFRAME_PANES_MISSING = "Panes.lua ist nicht geladen. Eine dem Addon hinzugefügte Datei wird erst beim nächsten Spielstart geladen: Schließt den Client und öffnet ihn erneut. Bis dahin bleibt das Charakterfenster inaktiv.",
CHARACTERFRAME_SHOW_DETAILS = "Details anzeigen",
CHARACTERFRAME_TAB_MACROS_FULL = "Kein freier Makroplatz: Löscht ein Makro, um diesen Reiter auf eine Leiste zu legen.",
CHARACTERFRAME_TITLES = "Titel",
-- Chat.lua
@@ -388,6 +387,9 @@ ForeverUI.AddTexts("deDE", {
STATUSBARS_EDIT_LABEL_EXPERIENCE = "Erfahrungsleiste",
STATUSBARS_EDIT_LABEL_REPUTATION = "Rufleiste",
-- TabMacros.lua
TABMACROS_FULL = "Kein freier Makroplatz: Löscht ein Makro, um diesen Reiter auf eine Leiste zu legen.",
-- TabardFrame.lua
TABARDFRAME_POSITION_USAGE = "Wappenrock: /fui tabard position <Tiefe> <seitlich> <Höhe>",
TABARDFRAME_STATE = "Wappenrock: Kamera=%s Skalierung=%s Position=(%s, %s, %s)",
+3 -1
View File
@@ -77,7 +77,6 @@ ForeverUI.AddTexts("enUS", {
CHARACTERFRAME_MODEL_USAGE = "model: echelle <n> | position <x> <y> <z> | position defaut",
CHARACTERFRAME_PANES_MISSING = "Panes.lua is not loaded. A file added to the addon only arrives at the next game start: close and reopen the client. The character sheet stays inactive until then.",
CHARACTERFRAME_SHOW_DETAILS = "Show details",
CHARACTERFRAME_TAB_MACROS_FULL = "No free macro slot: delete a macro to put this tab on a bar.",
CHARACTERFRAME_TITLES = "Titles",
-- Chat.lua
@@ -388,6 +387,9 @@ ForeverUI.AddTexts("enUS", {
STATUSBARS_EDIT_LABEL_EXPERIENCE = "Experience Bar",
STATUSBARS_EDIT_LABEL_REPUTATION = "Reputation Bar",
-- TabMacros.lua
TABMACROS_FULL = "No free macro slot: delete a macro to put this tab on a bar.",
-- TabardFrame.lua
TABARDFRAME_POSITION_USAGE = "tabard: /fui tabard position <depth> <lateral> <height>",
TABARDFRAME_STATE = "tabard: camera=%s scale=%s position=(%s, %s, %s)",
+3 -1
View File
@@ -77,7 +77,6 @@ ForeverUI.AddTexts("esES", {
CHARACTERFRAME_MODEL_USAGE = "modelo: echelle <n> | position <x> <y> <z> | position defaut",
CHARACTERFRAME_PANES_MISSING = "Panes.lua no está cargado. Un archivo añadido al accesorio solo llega en el siguiente inicio del juego: cierra y vuelve a abrir el cliente. La ficha de personaje permanece inactiva hasta entonces.",
CHARACTERFRAME_SHOW_DETAILS = "Mostrar detalles",
CHARACTERFRAME_TAB_MACROS_FULL = "No queda espacio para macros: borra una para poner esta pestaña en una barra.",
CHARACTERFRAME_TITLES = "Títulos",
-- Chat.lua
@@ -388,6 +387,9 @@ ForeverUI.AddTexts("esES", {
STATUSBARS_EDIT_LABEL_EXPERIENCE = "Barra de experiencia",
STATUSBARS_EDIT_LABEL_REPUTATION = "Barra de reputación",
-- TabMacros.lua
TABMACROS_FULL = "No queda espacio para macros: borra una para poner esta pestaña en una barra.",
-- TabardFrame.lua
TABARDFRAME_POSITION_USAGE = "tabardo: /fui tabard position <profundidad> <lateral> <altura>",
TABARDFRAME_STATE = "tabardo: cámara=%s escala=%s posición=(%s, %s, %s)",
+3 -1
View File
@@ -77,7 +77,6 @@ ForeverUI.AddTexts("frFR", {
CHARACTERFRAME_MODEL_USAGE = "modèle : echelle <n> | position <x> <y> <z> | position defaut",
CHARACTERFRAME_PANES_MISSING = "Panes.lua n'est pas chargé. Un fichier ajouté à l'addon n'arrive qu'au prochain démarrage du jeu : fermez et rouvrez le client. La feuille de personnage reste inactive d'ici là.",
CHARACTERFRAME_SHOW_DETAILS = "Afficher les détails",
CHARACTERFRAME_TAB_MACROS_FULL = "Plus de place pour une macro : supprimez-en une pour poser cet onglet sur une barre.",
CHARACTERFRAME_TITLES = "Titres",
-- Chat.lua
@@ -388,6 +387,9 @@ ForeverUI.AddTexts("frFR", {
STATUSBARS_EDIT_LABEL_EXPERIENCE = "Barre d'expérience",
STATUSBARS_EDIT_LABEL_REPUTATION = "Barre de réputation",
-- TabMacros.lua
TABMACROS_FULL = "Plus de place pour une macro : supprimez-en une pour poser cet onglet sur une barre.",
-- TabardFrame.lua
TABARDFRAME_POSITION_USAGE = "tabard : /fui tabard position <profondeur> <latéral> <hauteur>",
TABARDFRAME_STATE = "tabard : caméra=%s échelle=%s position=(%s, %s, %s)",
+3 -1
View File
@@ -77,7 +77,6 @@ ForeverUI.AddTexts("ruRU", {
CHARACTERFRAME_MODEL_USAGE = "модель: echelle <n> | position <x> <y> <z> | position defaut",
CHARACTERFRAME_PANES_MISSING = "Panes.lua не загружен. Файл, добавленный в модификацию, появляется только при следующем запуске игры: закройте и снова откройте клиент. До тех пор окно персонажа будет неактивно.",
CHARACTERFRAME_SHOW_DETAILS = "Показать подробности",
CHARACTERFRAME_TAB_MACROS_FULL = "Нет места для нового макроса: удалите макрос, чтобы поместить эту вкладку на панель.",
CHARACTERFRAME_TITLES = "Звания",
-- Chat.lua
@@ -388,6 +387,9 @@ ForeverUI.AddTexts("ruRU", {
STATUSBARS_EDIT_LABEL_EXPERIENCE = "Индикатор опыта",
STATUSBARS_EDIT_LABEL_REPUTATION = "Индикатор репутации",
-- TabMacros.lua
TABMACROS_FULL = "Нет места для нового макроса: удалите макрос, чтобы поместить эту вкладку на панель.",
-- TabardFrame.lua
TABARDFRAME_POSITION_USAGE = "гербовая накидка: /fui tabard position <глубина> <смещение> <высота>",
TABARDFRAME_STATE = "гербовая накидка: камера=%s масштаб=%s положение=(%s, %s, %s)",