From 76927c2c18aa9fbdfa9585fc3c9cd47a0c510397 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Wed, 5 Aug 2026 02:49:18 -0500 Subject: [PATCH] Add pfUI-native macro icon picker Replace Blizzard's MacroPopupFrame (the name + icon dialog) with a custom pfUI picker driven by IconDataProviderMixin, so the full spell + item + loose icon set is available instead of the stock spell-only list. It has a searchable, filterable icon grid and saves through C_Macro.CreateMacro / C_Macro.EditMacro, which take the icon as a texture string -- so arbitrary icons (including index-less INV_* item icons) persist. The macro-frame API is identical on the modern (Turtle/Octo) and stock-vanilla macro UIs, so the picker works on both with no client detection. The new macroicons module takes over the New / Change-Icon buttons and retires the stock popup; the blizzard macro skin now only skins MacroPopupFrame when that module is disabled. --- init/modules.xml | 1 + modules/macroicons.lua | 331 +++++++++++++++++++++++++++++++++++++++ skins/blizzard/macro.lua | 50 +++--- 3 files changed, 358 insertions(+), 24 deletions(-) create mode 100644 modules/macroicons.lua diff --git a/init/modules.xml b/init/modules.xml index 67111e42..182c993a 100644 --- a/init/modules.xml +++ b/init/modules.xml @@ -71,6 +71,7 @@ + diff --git a/modules/macroicons.lua b/modules/macroicons.lua new file mode 100644 index 00000000..b966ce45 --- /dev/null +++ b/modules/macroicons.lua @@ -0,0 +1,331 @@ +-- Macro icon picker +-- Replaces Blizzard's MacroPopupFrame (the "name + icon" dialog opened by the +-- New / Change-Icon buttons) with a pfUI-native picker driven by +-- IconDataProviderMixin, so the full spell + item + loose icon set is +-- available instead of the stock spell-only list. Saving goes through +-- C_Macro.CreateMacro / C_Macro.EditMacro, which take the icon as a texture +-- string -- so arbitrary icons (including index-less INV_* item icons) persist +-- on both the modern (Turtle/Octo) and stock-vanilla macro UIs. The surrounding +-- MacroFrame (list + body editor) is Blizzard's, already skinned elsewhere. +pfUI:RegisterModule("macroicons", function () + HookAddonOrVariable("Blizzard_MacroUI", function() + local QUESTION_MARK = "INTERFACE\\ICONS\\INV_MISC_QUESTIONMARK" + + local ICON_GRID_COLS = 10 + local ICON_GRID_ROWS = 8 + local ICON_BTN_SIZE = 36 + local ICON_BTN_PAD = 6 + + -- Shared picker state (forward-declared so the grid/filter closures below + -- can capture them before RefreshIconGrid is assigned). + local provider = nil + local selectedIconPath = QUESTION_MARK + local currentFilter = "all" + local searchText = "" + local filtered = nil -- provider indices matching the search, or nil when empty + local RefreshIconGrid, RebuildFilter + + local function EnsureProvider() + if not provider then + -- Spellbook extra type leads with class-relevant spell/talent icons. + provider = CreateAndInitFromMixin(IconDataProviderMixin, IconDataProviderExtraType.Spellbook) + end + end + + -- ============================================================ + -- Popup frame + -- ============================================================ + + local picker = CreateFrame("Frame", "pfMacroIconPicker", UIParent) + picker:SetFrameStrata("DIALOG") + picker:SetSize(472, 498) + -- Starts anchored to the right of the macro panel (re-anchored at open + -- once the skin's backdrop exists); dragging pins it in place after. + picker:SetPoint("BOTTOMLEFT", MacroFrame, "BOTTOMRIGHT", 8, 0) + picker:Hide() + CreateBackdrop(picker, nil, nil, .9) + CreateBackdropShadow(picker) + picker:EnableMouse(true) + picker:SetMovable(true) + picker:RegisterForDrag("LeftButton") + picker:SetScript("OnDragStart", function() this:StartMoving() end) + picker:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + this.userMoved = true + end) + + picker.title = picker:CreateFontString(nil, "OVERLAY", "GameFontNormal") + picker.title:SetPoint("TOP", picker, "TOP", 0, -10) + + picker.nameLabel = picker:CreateFontString(nil, "OVERLAY", "GameFontNormal") + picker.nameLabel:SetPoint("TOPLEFT", picker, "TOPLEFT", 14, -34) + picker.nameLabel:SetText(MACRO_POPUP_TEXT) + + picker.editbox = CreateFrame("EditBox", "pfMacroIconPickerName", picker, "InputBoxTemplate") + picker.editbox:SetSize(280, 20) + picker.editbox:SetPoint("TOPLEFT", picker, "TOPLEFT", 14, -52) + picker.editbox:SetAutoFocus(false) + picker.editbox:SetMaxLetters(16) + CreateBackdrop(picker.editbox) + + picker.selectedPreview = CreateFrame("Frame", nil, picker) + picker.selectedPreview:SetSize(42, 42) + picker.selectedPreview:SetPoint("TOPRIGHT", picker, "TOPRIGHT", -14, -44) + CreateBackdrop(picker.selectedPreview) + picker.selectedPreview.tex = picker.selectedPreview:CreateTexture(nil, "ARTWORK") + picker.selectedPreview.tex:SetAllPoints(picker.selectedPreview) + picker.selectedPreview.tex:SetTexCoord(.08, .92, .08, .92) + + picker.iconLabel = picker:CreateFontString(nil, "OVERLAY", "GameFontNormal") + picker.iconLabel:SetPoint("TOPLEFT", picker, "TOPLEFT", 14, -100) + picker.iconLabel:SetText(T["Choose an Icon:"] or "Choose an Icon:") + + -- Icon grid + scroll + local iconScroll = CreateFrame("ScrollFrame", "pfMacroIconScroll", picker, "FauxScrollFrameTemplate") + iconScroll:SetPoint("TOPLEFT", picker, "TOPLEFT", 14, -130) + iconScroll:SetWidth(ICON_GRID_COLS * (ICON_BTN_SIZE + ICON_BTN_PAD) - ICON_BTN_PAD) + iconScroll:SetHeight(ICON_GRID_ROWS * (ICON_BTN_SIZE + ICON_BTN_PAD) - ICON_BTN_PAD) + + local scrollbar = _G["pfMacroIconScrollScrollBar"] + scrollbar:ClearAllPoints() + scrollbar:SetPoint("TOPLEFT", iconScroll, "TOPRIGHT", 8, -16) + scrollbar:SetPoint("BOTTOMLEFT", iconScroll, "BOTTOMRIGHT", 8, 16) + SkinScrollbar(scrollbar) + + -- Filter dropdown: All / Spells / Items + local filterDropdown = CreateFrame("Frame", "pfMacroIconFilter", picker, "UIDropDownMenuTemplate") + filterDropdown:SetPoint("TOPRIGHT", picker, "TOPRIGHT", 0, -94) + local function ApplyFilter(value) + currentFilter = value + UIDropDownMenu_SetSelectedValue(filterDropdown, value) + if provider then + if value == "spells" then provider:SetIconTypes({ IconDataProviderIconType.Spell }) + elseif value == "items" then provider:SetIconTypes({ IconDataProviderIconType.Item }) + else provider:SetIconTypes(nil) end + RebuildFilter() + RefreshIconGrid() + end + end + UIDropDownMenu_Initialize(filterDropdown, function() + local info + info = {}; info.text = T["All Icons"] or "All Icons"; info.value = "all" + info.func = function() ApplyFilter("all") end + info.checked = currentFilter == "all" + UIDropDownMenu_AddButton(info) + info = {}; info.text = T["Spells"] or "Spells"; info.value = "spells" + info.func = function() ApplyFilter("spells") end + info.checked = currentFilter == "spells" + UIDropDownMenu_AddButton(info) + info = {}; info.text = T["Items"] or "Items"; info.value = "items" + info.func = function() ApplyFilter("items") end + info.checked = currentFilter == "items" + UIDropDownMenu_AddButton(info) + end) + UIDropDownMenu_SetWidth(120, filterDropdown) + UIDropDownMenu_SetSelectedValue(filterDropdown, "all") + SkinDropDown(filterDropdown) + + local iconButtons = {} + for r = 1, ICON_GRID_ROWS do + for c = 1, ICON_GRID_COLS do + local i = (r - 1) * ICON_GRID_COLS + c + local btn = CreateFrame("Button", nil, picker) + btn:SetSize(ICON_BTN_SIZE, ICON_BTN_SIZE) + btn:SetPoint("TOPLEFT", iconScroll, "TOPLEFT", (c-1) * (ICON_BTN_SIZE + ICON_BTN_PAD), -(r-1) * (ICON_BTN_SIZE + ICON_BTN_PAD)) + CreateBackdrop(btn) + btn.texture = btn:CreateTexture(nil, "ARTWORK") + btn.texture:SetAllPoints(btn) + btn.texture:SetTexCoord(.08, .92, .08, .92) + btn:SetScript("OnClick", function() + if this.iconIndex and provider then + local path = provider:GetIconByIndex(this.iconIndex) + if path then selectedIconPath = path end + RefreshIconGrid() + end + end) + btn:SetScript("OnEnter", function() + if not this.iconIndex or not provider then return end + local path = provider:GetIconByIndex(this.iconIndex) + if type(path) == "string" then + local name = string.gsub(path, "^.-INTERFACE\\\\ICONS\\\\", "") + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(name) + GameTooltip:Show() + end + end) + btn:SetScript("OnLeave", GameTooltip_Hide) + iconButtons[i] = btn + end + end + + -- Normalize a texture to an uppercase basename so selection matching + -- works across sources: GetMacroInfo returns "Interface\Icons\" + -- while provider paths are uppercase-prefixed for base icons and + -- mixed-case for spellbook extras. + local function IconKey(path) + if type(path) ~= "string" then return path end + return string.gsub(string.upper(path), "^.*[\\/]", "") + end + + -- Rebuild the search-filtered index list (provider indices whose icon + -- basename contains the search text); nil when the search box is empty. + function RebuildFilter() + EnsureProvider() + if searchText == "" then + filtered = nil + return + end + filtered = {} + for i = 1, provider:GetNumIcons() do + local path = provider:GetIconByIndex(i) + if type(path) == "string" and string.find(IconKey(path), searchText, 1, true) then + table.insert(filtered, i) + end + end + end + + function RefreshIconGrid() + EnsureProvider() + local numIcons = filtered and table.getn(filtered) or provider:GetNumIcons() + local numRows = math.ceil(numIcons / ICON_GRID_COLS) + FauxScrollFrame_Update(iconScroll, numRows, ICON_GRID_ROWS, ICON_BTN_SIZE + ICON_BTN_PAD) + local offset = FauxScrollFrame_GetOffset(iconScroll) + for i = 1, ICON_GRID_ROWS * ICON_GRID_COLS do + local listIdx = i + offset * ICON_GRID_COLS + local btn = iconButtons[i] + if listIdx <= numIcons then + btn:Show() + local providerIdx = filtered and filtered[listIdx] or listIdx + btn.iconIndex = providerIdx + local path = provider:GetIconByIndex(providerIdx) + btn.texture:SetTexture(path) + if IconKey(path) == IconKey(selectedIconPath) then + btn.backdrop:SetBackdropBorderColor(1, 0.82, 0, 1) + else + btn.backdrop:SetBackdropBorderColor(pfUI.cache.er, pfUI.cache.eg, pfUI.cache.eb, pfUI.cache.ea) + end + else + btn:Hide() + btn.iconIndex = nil + end + end + picker.selectedPreview.tex:SetTexture(selectedIconPath) + end + + iconScroll:SetScript("OnVerticalScroll", function() + FauxScrollFrame_OnVerticalScroll(ICON_BTN_SIZE + ICON_BTN_PAD, function() RefreshIconGrid() end) + end) + + -- ============================================================ + -- Open / save + -- ============================================================ + + -- Blizzard's popup disables these while it is open; mirror that so the + -- underlying frame can't be double-driven, then let MacroFrame_Update + -- restore the correct states on close. + local function SetMacroButtons(enabled) + local m = enabled and "Enable" or "Disable" + MacroNewButton[m](MacroNewButton) + MacroEditButton[m](MacroEditButton) + MacroDeleteButton[m](MacroDeleteButton) + end + + local function OpenMacroPopup(mode) + if mode == "edit" and MacroFrame.selectedMacro then + picker.mode = "edit" + local name, texture = GetMacroInfo(MacroFrame.selectedMacro) + picker.editbox:SetText(name or "") + selectedIconPath = texture or QUESTION_MARK + picker.title:SetText(T["Edit Macro"] or "Edit Macro") + else + picker.mode = "new" + picker.editbox:SetText("") + selectedIconPath = QUESTION_MARK + picker.title:SetText(T["New Macro"] or "New Macro") + end + picker.search:SetText("") + if not picker.userMoved then + picker:ClearAllPoints() + picker:SetPoint("BOTTOMLEFT", MacroFrame.backdrop or MacroFrame, "BOTTOMRIGHT", 8, 0) + end + SetMacroButtons(false) + picker:Show() + picker.editbox:SetFocus() + RefreshIconGrid() + end + + local function SaveMacroPopup() + local text = strtrim(picker.editbox:GetText()) + if text == "" then return end + + if picker.mode == "edit" and MacroFrame.selectedMacro then + C_Macro.EditMacro(MacroFrame.selectedMacro, text, selectedIconPath, nil) + MacroFrame_SelectMacro(MacroFrame.selectedMacro) + else + local idx = C_Macro.CreateMacro(text, selectedIconPath, nil, (MacroFrame.macroBase or 0) > 0) + if idx then MacroFrame_SelectMacro(idx) end + end + picker:Hide() + end + + picker:SetScript("OnHide", function() + if provider then provider:Release(); provider = nil end + MacroFrame_Update() + end) + + picker.editbox:SetScript("OnEnterPressed", SaveMacroPopup) + picker.editbox:SetScript("OnEscapePressed", function() picker:Hide() end) + + -- OK / Cancel + local okay = CreateFrame("Button", "pfMacroIconPickerOkay", picker, "UIPanelButtonTemplate") + okay:SetSize(80, 22) + okay:SetText(OKAY) + okay:SetScript("OnClick", SaveMacroPopup) + SkinButton(okay) + + local cancel = CreateFrame("Button", "pfMacroIconPickerCancel", picker, "UIPanelButtonTemplate") + cancel:SetSize(80, 22) + cancel:SetText(CANCEL) + cancel:SetPoint("BOTTOMRIGHT", picker, "BOTTOMRIGHT", -14, 12) + cancel:SetScript("OnClick", function() picker:Hide() end) + SkinButton(cancel) + + -- Okay | Cancel, both anchored to the bottom-right corner. + okay:SetPoint("BOTTOMRIGHT", cancel, "BOTTOMLEFT", -6, 0) + + -- Search box (bottom-left) filters the grid by icon name. + picker.searchLabel = picker:CreateFontString(nil, "OVERLAY", "GameFontNormal") + picker.searchLabel:SetPoint("BOTTOMLEFT", picker, "BOTTOMLEFT", 16, 18) + picker.searchLabel:SetText(SEARCH) + + picker.search = CreateFrame("EditBox", "pfMacroIconSearch", picker, "InputBoxTemplate") + picker.search:SetSize(180, 20) + picker.search:SetPoint("LEFT", picker.searchLabel, "RIGHT", 10, 0) + picker.search:SetAutoFocus(false) + CreateBackdrop(picker.search) + picker.search:SetScript("OnEscapePressed", function() this:ClearFocus() end) + picker.search:SetScript("OnTextChanged", function() + searchText = strupper(strtrim(this:GetText())) + RebuildFilter() + scrollbar:SetValue(0) + RefreshIconGrid() + end) + + -- ============================================================ + -- Take over the New / Change-Icon buttons; retire Blizzard's popup + -- ============================================================ + + MacroNewButton:SetScript("OnClick", function() + MacroFrame_SaveMacro() + OpenMacroPopup("new") + end) + + MacroEditButton:SetScript("OnClick", function() + MacroFrame_SaveMacro() + OpenMacroPopup("edit") + end) + + -- Closing the macro window takes the picker with it. + MacroFrame:HookScript("OnHide", function() picker:Hide() end) + end) +end) diff --git a/skins/blizzard/macro.lua b/skins/blizzard/macro.lua index ec5638ef..c50477d2 100644 --- a/skins/blizzard/macro.lua +++ b/skins/blizzard/macro.lua @@ -60,33 +60,35 @@ pfUI:RegisterSkin("Macro", function () MacroFrameScrollFrame:ClearAllPoints() MacroFrameScrollFrame:SetPoint("TOPLEFT", MacroFrameSelectedMacroBackground, "BOTTOMLEFT", 11, -13) + -- The macroicons module replaces MacroPopupFrame with its own picker. + -- Only skin the stock popup when that module is disabled. + if pfUI_config["disabled"] and pfUI_config["disabled"]["macroicons"] == "1" then + StripTextures(MacroPopupFrame) + CreateBackdrop(MacroPopupFrame, nil, nil, .75) + MacroPopupFrame:SetFrameStrata("DIALOG") + MacroPopupFrame:ClearAllPoints() + MacroPopupFrame:SetPoint("TOPLEFT", MacroFrame.backdrop, "TOPRIGHT", 2*border, 0) + StripTextures(MacroPopupScrollFrame) + SkinScrollbar(MacroPopupScrollFrameScrollBar) - StripTextures(MacroPopupFrame) - CreateBackdrop(MacroPopupFrame, nil, nil, .75) - MacroPopupFrame:SetFrameStrata("DIALOG") - MacroPopupFrame:ClearAllPoints() - MacroPopupFrame:SetPoint("TOPLEFT", MacroFrame.backdrop, "TOPRIGHT", 2*border, 0) + MacroPopupEditBox:DisableDrawLayer("BACKGROUND") + CreateBackdrop(MacroPopupEditBox, nil, true) + MacroPopupEditBox:SetScript("OnEscapePressed", function() + MacroPopupFrame:Hide() + MacroFrame_Update() + end) - StripTextures(MacroPopupScrollFrame) - SkinScrollbar(MacroPopupScrollFrameScrollBar) - - MacroPopupEditBox:DisableDrawLayer("BACKGROUND") - CreateBackdrop(MacroPopupEditBox, nil, true) - MacroPopupEditBox:SetScript("OnEscapePressed", function() - MacroPopupFrame:Hide() - MacroFrame_Update() - end) - - for i=1, NUM_MACRO_ICONS_SHOWN do - local button = _G["MacroPopupButton"..i] - local icon = _G["MacroPopupButton"..i..'Icon'] - StripTextures(button) - SkinButton(button, nil, nil, nil, icon) + for i=1, NUM_MACRO_ICONS_SHOWN do + local button = _G["MacroPopupButton"..i] + local icon = _G["MacroPopupButton"..i..'Icon'] + StripTextures(button) + SkinButton(button, nil, nil, nil, icon) + end + SkinButton(MacroPopupCancelButton) + SkinButton(MacroPopupOkayButton) + MacroPopupOkayButton:ClearAllPoints() + MacroPopupOkayButton:SetPoint("RIGHT", MacroPopupCancelButton, "LEFT", -2*bpad, 0) end - SkinButton(MacroPopupCancelButton) - SkinButton(MacroPopupOkayButton) - MacroPopupOkayButton:ClearAllPoints() - MacroPopupOkayButton:SetPoint("RIGHT", MacroPopupCancelButton, "LEFT", -2*bpad, 0) end) end)