From 81ce939c8d3fa3f8377a00fc39d69e92e45eae89 Mon Sep 17 00:00:00 2001 From: Lars-Martin Date: Tue, 7 Apr 2026 19:26:34 +0200 Subject: [PATCH 1/5] fix: minimap canvas icons, compartment border, professions atlas, difficulty fallback --- Orbit/Plugins/Minimap/Minimap.lua | 42 ++++++++- Orbit/Plugins/Minimap/MinimapCapture.lua | 9 +- Orbit/Plugins/Minimap/MinimapCompartment.lua | 97 ++++++++++++-------- 3 files changed, 102 insertions(+), 46 deletions(-) diff --git a/Orbit/Plugins/Minimap/Minimap.lua b/Orbit/Plugins/Minimap/Minimap.lua index 95dcf91..5f98630 100644 --- a/Orbit/Plugins/Minimap/Minimap.lua +++ b/Orbit/Plugins/Minimap/Minimap.lua @@ -269,7 +269,11 @@ function Plugin:OnLoad() isFontString = true, onPositionChange = MPC("Coords"), }) - OrbitEngine.ComponentDrag:Attach(self._compartmentButton, self.frame, { key = "Compartment", onPositionChange = MPC("Compartment") }) + OrbitEngine.ComponentDrag:Attach(self._compartmentButton, self.frame, { + key = "Compartment", + sourceOverride = self._compartmentButton.icon, + onPositionChange = MPC("Compartment"), + }) OrbitEngine.ComponentDrag:Attach(self.frame.ZoomContainer, self.frame, { key = "Zoom", onPositionChange = MPC("Zoom") }) if self.frame.DifficultyIcon then OrbitEngine.ComponentDrag:Attach(self.frame.DifficultyIcon, self.frame, { key = DIFFICULTY_ICON_KEY, onPositionChange = MPC(DIFFICULTY_ICON_KEY) }) end if self.frame.DifficultyText then @@ -520,6 +524,42 @@ function Plugin:UpdateDifficultyVisuals(textMultiplier) end end + if not iconFrame.PreviewIcon then + iconFrame.PreviewIcon = iconFrame:CreateTexture(nil, "OVERLAY") + -- Use the 25 player difficulty skull + iconFrame.PreviewIcon:SetTexture("Interface\\Minimap\\UI-DungeonDifficulty-Button") + iconFrame.PreviewIcon:SetTexCoord(0.5, 0.75, 0.0703125, 0.4140625) + iconFrame.PreviewIcon:SetSize(18, 18) + iconFrame.PreviewIcon:SetPoint("CENTER", iconFrame, "CENTER", 0.5, 0.5) + end + if not iconFrame.PreviewText then + iconFrame.PreviewText = iconFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + iconFrame.PreviewText:SetText("25") + iconFrame.PreviewText:SetPoint("TOP", iconFrame.PreviewIcon, "BOTTOM", -1, 4) + end + + local realIconShown = false + if difficulty:IsShown() or mode == "icon" then + for _, sub in ipairs({ difficulty.Default, difficulty.Guild, difficulty.ChallengeMode }) do + if sub and sub:IsShown() then + for _, region in ipairs({ sub:GetRegions() }) do + if region and region:GetObjectType() == "Texture" and region:IsShown() and region ~= sub.Background and region ~= sub.Border then + realIconShown = true + break + end + end + end + end + end + + if keepVisible and not realIconShown and mode == "icon" then + iconFrame.PreviewIcon:Show() + iconFrame.PreviewText:Show() + else + iconFrame.PreviewIcon:Hide() + iconFrame.PreviewText:Hide() + end + iconFrame.orbitOriginalWidth = difficulty.orbitOriginalWidth or iconFrame.orbitOriginalWidth or 16 iconFrame.orbitOriginalHeight = difficulty.orbitOriginalHeight or iconFrame.orbitOriginalHeight or 16 iconFrame:SetScale(1) diff --git a/Orbit/Plugins/Minimap/MinimapCapture.lua b/Orbit/Plugins/Minimap/MinimapCapture.lua index 10cc210..c7dc241 100644 --- a/Orbit/Plugins/Minimap/MinimapCapture.lua +++ b/Orbit/Plugins/Minimap/MinimapCapture.lua @@ -294,14 +294,7 @@ function Plugin:ReparentBlizzardComponents() craftingOrder.Icon = craftingOrder:CreateTexture(nil, "ARTWORK") craftingOrder.Icon:SetSize(20, 20) craftingOrder.Icon:SetPoint("CENTER") - - local info = C_Texture.GetAtlasInfo("UI-HUD-Minimap-CraftingOrder-Over-2x") - if info and info.file then - craftingOrder.Icon:SetTexture(info.file) - craftingOrder.Icon:SetTexCoord(info.leftTexCoord, info.rightTexCoord, info.topTexCoord, info.bottomTexCoord) - else - craftingOrder.Icon:SetAtlas("UI-HUD-Minimap-CraftingOrder-Over-2x", true) - end + craftingOrder.Icon:SetAtlas("UI-HUD-Minimap-CraftingOrder-Over-2x", true) -- Ignore native scaling so the atlas stays crisp craftingOrder.Icon:SetScale(1) diff --git a/Orbit/Plugins/Minimap/MinimapCompartment.lua b/Orbit/Plugins/Minimap/MinimapCompartment.lua index 3adea8f..f71f871 100644 --- a/Orbit/Plugins/Minimap/MinimapCompartment.lua +++ b/Orbit/Plugins/Minimap/MinimapCompartment.lua @@ -48,6 +48,23 @@ local function IsPinFrame(name) return false end +local function NormalizeCompartmentDisplayName(name) + local displayName = name or "Unknown" + displayName = displayName:gsub("^LibDBIcon10_", "") + displayName = displayName:gsub("MinimapButton", "") + displayName = displayName:gsub("Minimap", "") + displayName = displayName:gsub("Button$", "") + if displayName == "" then + displayName = name or "Unknown" + end + return displayName +end + +local function BuildCollectedButtonSignature(name, icon) + if type(icon) ~= "string" or icon == "" then return nil end + return string.lower((name or "unknown")) .. "|" .. icon +end + -- Minimum button width to be considered a real addon button (map pins are typically <20px). local MIN_BUTTON_SIZE = 20 @@ -66,11 +83,6 @@ function Plugin:CreateCompartmentButton() btn.orbitOriginalWidth = COMPARTMENT_BUTTON_SIZE btn.orbitOriginalHeight = COMPARTMENT_BUTTON_SIZE - -- Background - btn.bg = btn:CreateTexture(nil, "BACKGROUND") - btn.bg:SetAllPoints(btn) - btn.bg:SetColorTexture(0.1, 0.1, 0.1, 0.8) - btn.highlight = btn:CreateTexture(nil, "HIGHLIGHT") btn.highlight:SetAllPoints(btn) btn.highlight:SetTexture("Interface\\Buttons\\UI-Common-MouseHilight") @@ -81,6 +93,9 @@ function Plugin:CreateCompartmentButton() btn.icon = btn:CreateTexture(nil, "ARTWORK") btn.icon:SetAllPoints(btn) btn.icon:SetAtlas("Map-Filter-Button", false) + + -- Setup visual for canvas mode + btn.visual = btn.icon btn.iconPushed = btn:CreateTexture(nil, "ARTWORK") btn.iconPushed:SetAllPoints(btn) @@ -90,9 +105,6 @@ function Plugin:CreateCompartmentButton() btn:SetScript("OnMouseDown", function() btn.icon:Hide(); btn.iconPushed:Show() end) btn:SetScript("OnMouseUp", function() btn.iconPushed:Hide(); btn.icon:Show() end) - -- Border - Orbit.Skin:SkinBorder(btn, btn, 1, BORDER_COLOR) - -- Start hidden; revealed on minimap hover btn:SetAlpha(0) @@ -136,7 +148,7 @@ function Plugin:CreateCompartmentFlyout() overlay:SetFrameStrata(Orbit.Constants.Strata.Dialog) overlay:SetFrameLevel(flyout:GetFrameLevel() - 1) overlay:Hide() - overlay:RegisterForClicks("AnyUp") + overlay:RegisterForClicks("LeftButtonUp", "RightButtonUp") overlay:SetScript("OnClick", function() flyout:Hide() end) flyout._clickOverlay = overlay @@ -194,7 +206,7 @@ function Plugin:PopulateCompartmentFlyout() if not row then row = CreateFrame("Button", nil, flyout) row:SetHeight(COMPARTMENT_ROW_HEIGHT) - row:RegisterForClicks("AnyUp") + row:RegisterForClicks("LeftButtonUp", "RightButtonUp") row:SetHighlightTexture(COMPARTMENT_HIGHLIGHT_TEXTURE, "ADD") row:GetHighlightTexture():SetAlpha(0.15) @@ -234,12 +246,16 @@ function Plugin:PopulateCompartmentFlyout() end -- Click handler: trigger the original addon button's OnClick - -- RegisterForClicks("AnyUp") is set on row creation so right-clicks are received. + -- RegisterForClicks("LeftButtonUp", "RightButtonUp") is set on row creation so right-clicks are received. row:SetScript("OnClick", function(_, button) if not entry.button then return end local btn = entry.button local b = button or "LeftButton" - if btn.dataObject and btn.dataObject.OnClick then + if entry.source == "libdbicon" and btn.dataObject and btn.dataObject.OnClick then + btn.dataObject.OnClick(btn, b) + elseif btn.Click and btn.IsObjectType and btn:IsObjectType("Button") then + btn:Click(b) + elseif btn.dataObject and btn.dataObject.OnClick then btn.dataObject.OnClick(btn, b) elseif btn:GetScript("OnClick") then btn:GetScript("OnClick")(btn, b) @@ -342,6 +358,7 @@ function Plugin:CollectAddonButtons() -- Track already-collected frame references so we don't double-collect local seen = {} + local seenSignatures = {} -- 1) LibDBIcon registered buttons local lib = LibStub and LibStub("LibDBIcon-1.0", true) @@ -349,12 +366,18 @@ function Plugin:CollectAddonButtons() local ownButtonName = "Orbit" for name, button in pairs(lib.objects) do if name ~= ownButtonName then + local displayName = NormalizeCompartmentDisplayName(name) + local icon = button.dataObject and button.dataObject.icon or nil + local signature = BuildCollectedButtonSignature(displayName, icon) + if not signature or not seenSignatures[signature] then collected[#collected + 1] = { - name = name, + name = displayName, button = button, - icon = button.dataObject and button.dataObject.icon or nil, + icon = icon, source = "libdbicon", } + end + if signature then seenSignatures[signature] = true end seen[button] = true end end @@ -393,18 +416,17 @@ function Plugin:CollectAddonButtons() elseif child.GetNormalTexture and child:GetNormalTexture() then icon = child:GetNormalTexture():GetTexture() end - local displayName = frameName or tostring(child) - displayName = displayName:gsub("^LibDBIcon10_", "") - displayName = displayName:gsub("MinimapButton", "") - displayName = displayName:gsub("Minimap", "") - displayName = displayName:gsub("Button$", "") - if displayName == "" then displayName = frameName or "Unknown" end - collected[#collected + 1] = { - name = displayName, - button = child, - icon = icon, - source = "minimap_child", - } + local displayName = NormalizeCompartmentDisplayName(frameName or tostring(child)) + local signature = BuildCollectedButtonSignature(displayName, icon) + if not signature or not seenSignatures[signature] then + collected[#collected + 1] = { + name = displayName, + button = child, + icon = icon, + source = "minimap_child", + } + end + if signature then seenSignatures[signature] = true end seen[child] = true end end @@ -419,26 +441,27 @@ function Plugin:HideCollectedButtons() if not self._collectedButtons then return end for _, entry in ipairs(self._collectedButtons) do if entry.button then + entry.button._orbitCompartmentManaged = true entry.button:Hide() -- Prevent re-showing by addons that call Show() periodically. -- hooksecurefunc is taint-safe; the hook fires after the original Show(). -- We hide immediately afterwards when the compartment is active. - if not entry.button._orbitOnShowHooked then + if not entry.button._orbitOnShowHookInstalled then hooksecurefunc(entry.button, "Show", function(b) - if self._compartmentActive then + if self._compartmentActive and b._orbitCompartmentManaged and not self._restoringCollectedButtons then b:Hide() end end) - entry.button._orbitOnShowHooked = true + entry.button._orbitOnShowHookInstalled = true end -- For direct minimap children, also suppress SetShown - if entry.source == "minimap_child" and not entry.button._orbitSetShownHooked then + if entry.source == "minimap_child" and not entry.button._orbitSetShownHookInstalled then hooksecurefunc(entry.button, "SetShown", function(b, shown) - if shown and self._compartmentActive then + if shown and self._compartmentActive and b._orbitCompartmentManaged and not self._restoringCollectedButtons then b:Hide() end end) - entry.button._orbitSetShownHooked = true + entry.button._orbitSetShownHookInstalled = true end end end @@ -446,17 +469,16 @@ end function Plugin:RestoreCollectedButtons() if not self._collectedButtons then return end + self._restoringCollectedButtons = true for _, entry in ipairs(self._collectedButtons) do if entry.button then - -- Hooks installed via hooksecurefunc cannot be removed; just clear the - -- flag so the hook body becomes a no-op after the compartment is inactive. - entry.button._orbitOnShowHooked = nil - entry.button._orbitSetShownHooked = nil + entry.button._orbitCompartmentManaged = nil if not (entry.button.db and entry.button.db.hide) then entry.button:Show() end end end + self._restoringCollectedButtons = nil self._collectedButtons = nil end @@ -467,10 +489,11 @@ function Plugin:ApplyAddonCompartment() local useClickAction = self:UsesAddonClickAction() if useClickAction or not self:IsComponentDisabled("Compartment") then - self._compartmentActive = true -- Restore any previously-hooked buttons before re-collecting, so stale hooks -- (e.g. on frames that are no longer eligible) are cleaned up each cycle. + self._compartmentActive = false self:RestoreCollectedButtons() + self._compartmentActive = true self:CollectAddonButtons() self:HideCollectedButtons() From b4f61da161fececaadadd0746f04b50296bef80a Mon Sep 17 00:00:00 2001 From: Lars-Martin Date: Tue, 7 Apr 2026 19:30:14 +0200 Subject: [PATCH 2/5] refactor: reparent minimap buttons instead of hide+proxy --- Orbit/Plugins/Minimap/MinimapCompartment.lua | 410 +++++++++++-------- 1 file changed, 241 insertions(+), 169 deletions(-) diff --git a/Orbit/Plugins/Minimap/MinimapCompartment.lua b/Orbit/Plugins/Minimap/MinimapCompartment.lua index f71f871..4e18fc2 100644 --- a/Orbit/Plugins/Minimap/MinimapCompartment.lua +++ b/Orbit/Plugins/Minimap/MinimapCompartment.lua @@ -1,5 +1,8 @@ -- Minimap Addon Compartment --- Collects all LibDBIcon minimap buttons + legacy minimap children into a hover-reveal drawer. +-- Collects all LibDBIcon minimap buttons + legacy minimap children into a flyout drawer. +-- Uses the REPARENT approach: actual addon buttons are reparented into our container, +-- preserving their native click handlers, tooltips, and right-click menus intact. +-- Inspired by MinimapButtonButton (MBB) — the most reliable method for minimap button collection. ---@type Orbit local Orbit = Orbit @@ -10,12 +13,13 @@ local Plugin = Orbit:GetPlugin(SYSTEM_ID) local BORDER_COLOR = Orbit.MinimapConstants.BORDER_COLOR local COMPARTMENT_BUTTON_SIZE = 24 -local COMPARTMENT_ICON_SIZE = 20 -local COMPARTMENT_ROW_HEIGHT = 22 local COMPARTMENT_PADDING = 6 -local COMPARTMENT_ICON_PADDING = 4 -local COMPARTMENT_MAX_WIDTH = 220 -local COMPARTMENT_HIGHLIGHT_TEXTURE = 136810 -- Blizzard white highlight (Interface\BUTTONS\WHITE8x8) +local FLYOUT_BUTTON_SIZE = 28 -- Size for each reparented button in the flyout grid +local FLYOUT_BUTTON_SPACING = 2 -- Spacing between buttons in the grid +local FLYOUT_COLUMNS = 6 -- Number of columns in the flyout grid + +-- No-op function used to block addons from repositioning their buttons +local function doNothing() end -- Blizzard-owned children of Minimap that must never be collected into the compartment. -- Includes reparented Blizzard frames (Missions, Difficulty, etc.) so they are never @@ -25,6 +29,7 @@ local BLIZZARD_MINIMAP_CHILDREN = { ["MinimapCompassTexture"] = true, ["OrbitMinimapCompartmentButton"] = true, ["OrbitMinimapCompartmentFlyout"] = true, + ["OrbitMinimapButtonHolder"] = true, ["ExpansionLandingPageMinimapButton"] = true, } @@ -68,6 +73,10 @@ end -- Minimum button width to be considered a real addon button (map pins are typically <20px). local MIN_BUTTON_SIZE = 20 +-- Store references to the real frame methods before we override them +local FrameClearAllPoints = UIParent.ClearAllPoints +local FrameSetPoint = UIParent.SetPoint + -- [ COMPARTMENT BUTTON ]---------------------------------------------------------------------------- function Plugin:CreateCompartmentButton() @@ -93,7 +102,7 @@ function Plugin:CreateCompartmentButton() btn.icon = btn:CreateTexture(nil, "ARTWORK") btn.icon:SetAllPoints(btn) btn.icon:SetAtlas("Map-Filter-Button", false) - + -- Setup visual for canvas mode btn.visual = btn.icon @@ -122,6 +131,23 @@ function Plugin:CreateCompartmentButton() self._compartmentButton = btn end +-- [ HIDDEN BUTTON HOLDER ]-------------------------------------------------------------------------- +-- A hidden frame that holds reparented addon buttons while the compartment is active. +-- Buttons are reparented here instead of being hidden via Hide(), which avoids all +-- Show/Hide hook issues. The holder is always hidden, so buttons inside it are invisible. + +function Plugin:GetOrCreateButtonHolder() + if self._buttonHolder then return self._buttonHolder end + + local holder = CreateFrame("Frame", "OrbitMinimapButtonHolder", UIParent) + holder:SetSize(1, 1) + holder:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -500, 500) -- offscreen + holder:Hide() + + self._buttonHolder = holder + return holder +end + -- [ COMPARTMENT FLYOUT ]---------------------------------------------------------------------------- function Plugin:CreateCompartmentFlyout() @@ -149,13 +175,16 @@ function Plugin:CreateCompartmentFlyout() overlay:SetFrameLevel(flyout:GetFrameLevel() - 1) overlay:Hide() overlay:RegisterForClicks("LeftButtonUp", "RightButtonUp") - overlay:SetScript("OnClick", function() flyout:Hide() end) + overlay:SetScript("OnClick", function() self:HideCompartmentFlyout() end) flyout._clickOverlay = overlay flyout:SetScript("OnShow", function(f) f._clickOverlay:Show() end) - flyout:SetScript("OnHide", function(f) f._clickOverlay:Hide() end) + flyout:SetScript("OnHide", function(f) + f._clickOverlay:Hide() + -- When flyout closes, reparent buttons back to the hidden holder + self:RetractButtonsToHolder() + end) - flyout.rows = {} self._compartmentFlyout = flyout end @@ -166,27 +195,52 @@ function Plugin:ToggleCompartmentFlyout() local flyout = self._compartmentFlyout if flyout:IsShown() then - flyout:Hide() + self:HideCompartmentFlyout() return end - self:PopulateCompartmentFlyout() + self:ShowCompartmentFlyout() +end + +function Plugin:HideCompartmentFlyout() + if self._compartmentFlyout then + self._compartmentFlyout:Hide() + end +end + +function Plugin:ShowCompartmentFlyout() + if not self._compartmentFlyout then + self:CreateCompartmentFlyout() + end + local flyout = self._compartmentFlyout + + self:LayoutButtonsInFlyout() flyout:Show() end -function Plugin:PopulateCompartmentFlyout() +-- [ FLYOUT LAYOUT ]--------------------------------------------------------------------------------- +-- Reparent the actual buttons into the flyout and arrange them in a grid. +-- No proxy rows, no click forwarding — buttons handle everything natively. + +function Plugin:LayoutButtonsInFlyout() local flyout = self._compartmentFlyout if not flyout then return end local btn = self._compartmentButton local anchor = btn and btn:IsShown() and btn or self.frame - -- Hide existing rows - for _, row in ipairs(flyout.rows) do - row:Hide() + local collected = self._collectedButtons or {} + -- Count visible buttons (respect LibDBIcon .db.hide) + local visibleEntries = {} + for _, entry in ipairs(collected) do + if entry.button then + local hidden = entry.button.db and entry.button.db.hide + if not hidden then + visibleEntries[#visibleEntries + 1] = entry + end + end end - local collected = self._collectedButtons or {} - if #collected == 0 then + if #visibleEntries == 0 then flyout:SetSize(140, 30) flyout:ClearAllPoints() flyout:SetPoint("BOTTOMRIGHT", anchor, "TOPRIGHT", 0, 2) @@ -200,119 +254,70 @@ function Plugin:PopulateCompartmentFlyout() end if flyout._emptyText then flyout._emptyText:Hide() end - local maxTextWidth = 0 - for i, entry in ipairs(collected) do - local row = flyout.rows[i] - if not row then - row = CreateFrame("Button", nil, flyout) - row:SetHeight(COMPARTMENT_ROW_HEIGHT) - row:RegisterForClicks("LeftButtonUp", "RightButtonUp") - row:SetHighlightTexture(COMPARTMENT_HIGHLIGHT_TEXTURE, "ADD") - row:GetHighlightTexture():SetAlpha(0.15) - - row.icon = row:CreateTexture(nil, "ARTWORK") - row.icon:SetSize(COMPARTMENT_ICON_SIZE, COMPARTMENT_ICON_SIZE) - row.icon:SetPoint("LEFT", COMPARTMENT_PADDING, 0) - - row.label = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") - row.label:SetPoint("LEFT", row.icon, "RIGHT", COMPARTMENT_ICON_PADDING, 0) - row.label:SetJustifyH("LEFT") - - flyout.rows[i] = row - end - - -- Icon - local iconTexture = entry.icon - if iconTexture then - if type(iconTexture) == "string" and C_Texture.GetAtlasInfo(iconTexture) then - row.icon:SetAtlas(iconTexture) - else - row.icon:SetTexture(iconTexture) + local cols = math.min(FLYOUT_COLUMNS, #visibleEntries) + local rows = math.ceil(#visibleEntries / cols) + local cellSize = FLYOUT_BUTTON_SIZE + FLYOUT_BUTTON_SPACING + + local flyoutWidth = (cols * cellSize) + FLYOUT_BUTTON_SPACING + (COMPARTMENT_PADDING * 2) + local flyoutHeight = (rows * cellSize) + FLYOUT_BUTTON_SPACING + (COMPARTMENT_PADDING * 2) + flyout:SetSize(flyoutWidth, flyoutHeight) + + -- Reparent each button into the flyout and position in grid + for i, entry in ipairs(visibleEntries) do + local button = entry.button + if button then + local col = (i - 1) % cols + local row = math.floor((i - 1) / cols) + local xOff = COMPARTMENT_PADDING + FLYOUT_BUTTON_SPACING + (col * cellSize) + local yOff = -(COMPARTMENT_PADDING + FLYOUT_BUTTON_SPACING + (row * cellSize)) + + -- Reparent into flyout — this makes the button visible and clickable inside the flyout + button:SetParent(flyout) + button:SetFrameStrata(flyout:GetFrameStrata()) + button:SetFrameLevel(flyout:GetFrameLevel() + 2) + + -- Position in grid using stored real methods (we overrode ClearAllPoints/SetPoint) + FrameClearAllPoints(button) + FrameSetPoint(button, "TOPLEFT", flyout, "TOPLEFT", xOff, yOff) + + -- Normalize size + button:SetSize(FLYOUT_BUTTON_SIZE, FLYOUT_BUTTON_SIZE) + button:SetIgnoreParentScale(false) + button:SetScale(1) + + -- Strip the circular minimap button border/overlay textures for a cleaner look. + -- LibDBIcon buttons typically have overlay/border textures we can hide. + if not button._orbitSkinned then + -- Hide common minimap button decoration textures + for _, region in ipairs({ button:GetRegions() }) do + if region:IsObjectType("Texture") then + local tex = region:GetTexture() + if tex and type(tex) == "string" then + local texLower = tex:lower() + if texLower:find("border") or texLower:find("trackingborder") + or texLower:find("minimap%-trackingborder") or texLower:find("overlay") then + region:SetTexture(nil) + end + end + -- Also hide by draw layer — OVERLAY is typically the ring/border + local layer = region:GetDrawLayer() + if layer == "OVERLAY" and region ~= (button.icon or button.Icon) then + region:SetAlpha(0) + end + end + end + button._orbitSkinned = true end - row.icon:SetTexCoord(0, 1, 0, 1) - row.icon:Show() - else - row.icon:Hide() - end - - -- Label - local displayName = entry.name or "Unknown" - row.label:SetText(displayName) - Orbit.Skin:SkinText(row.label, { font = Orbit.db.GlobalSettings.Font, textSize = 11 }) - local textWidth = row.label:GetStringWidth() - if textWidth > maxTextWidth then - maxTextWidth = textWidth - end + -- Disable drag scripts on collected buttons (they shouldn't be draggable in the flyout) + button:SetScript("OnDragStart", nil) + button:SetScript("OnDragStop", nil) - -- Click handler: trigger the original addon button's OnClick - -- RegisterForClicks("LeftButtonUp", "RightButtonUp") is set on row creation so right-clicks are received. - row:SetScript("OnClick", function(_, button) - if not entry.button then return end - local btn = entry.button - local b = button or "LeftButton" - if entry.source == "libdbicon" and btn.dataObject and btn.dataObject.OnClick then - btn.dataObject.OnClick(btn, b) - elseif btn.Click and btn.IsObjectType and btn:IsObjectType("Button") then - btn:Click(b) - elseif btn.dataObject and btn.dataObject.OnClick then - btn.dataObject.OnClick(btn, b) - elseif btn:GetScript("OnClick") then - btn:GetScript("OnClick")(btn, b) - end - -- Close flyout on left-click; leave open on right-click so context - -- menus (which open over the flyout) can appear without losing context. - if b ~= "RightButton" then - flyout:Hide() - end - end) - - -- Tooltip passthrough - row:SetScript("OnEnter", function(r) - if not entry.button then return end - local btn = entry.button - local obj = btn.dataObject - if obj and obj.OnTooltipShow then - GameTooltip:SetOwner(r, "ANCHOR_RIGHT") - obj.OnTooltipShow(GameTooltip) - GameTooltip:Show() - elseif obj and obj.OnEnter then - obj.OnEnter(btn) - elseif btn:GetScript("OnEnter") then - btn:GetScript("OnEnter")(btn) - end - end) - row:SetScript("OnLeave", function() - GameTooltip:Hide() - local btn = entry.button - if btn then - local obj = btn.dataObject - if obj and obj.OnLeave then - obj.OnLeave(btn) - elseif btn:GetScript("OnLeave") then - btn:GetScript("OnLeave")(btn) - end - end - end) - - -- Position - row:ClearAllPoints() - if i == 1 then - row:SetPoint("TOPLEFT", flyout, "TOPLEFT", 0, -COMPARTMENT_PADDING) - row:SetPoint("TOPRIGHT", flyout, "TOPRIGHT", 0, -COMPARTMENT_PADDING) - else - row:SetPoint("TOPLEFT", flyout.rows[i - 1], "BOTTOMLEFT", 0, 0) - row:SetPoint("TOPRIGHT", flyout.rows[i - 1], "BOTTOMRIGHT", 0, 0) + -- Show the button — it's now a child of the visible flyout + button:Show() end - row:Show() end - -- Size flyout to fit content - local rowWidth = COMPARTMENT_PADDING + COMPARTMENT_ICON_SIZE + COMPARTMENT_ICON_PADDING + maxTextWidth + COMPARTMENT_PADDING + 10 - local width = math.min(math.max(rowWidth, 120), COMPARTMENT_MAX_WIDTH) - local height = (#collected * COMPARTMENT_ROW_HEIGHT) + (COMPARTMENT_PADDING * 2) - flyout:SetSize(width, height) - -- Position the flyout intelligently based on available screen space local scale = anchor:GetEffectiveScale() local screenW = GetScreenWidth() * UIParent:GetEffectiveScale() @@ -321,8 +326,8 @@ function Plugin:PopulateCompartmentFlyout() local btnRight = anchor:GetRight() * scale local btnTop = anchor:GetTop() * scale local btnBottom = anchor:GetBottom() * scale - local flyW = width * scale - local flyH = height * scale + local flyW = flyoutWidth * scale + local flyH = flyoutHeight * scale local gap = 2 -- Vertical: prefer expanding upward; fall back to downward if not enough room above @@ -345,6 +350,21 @@ function Plugin:PopulateCompartmentFlyout() end end +-- [ RETRACT BUTTONS TO HOLDER ]--------------------------------------------------------------------- +-- When the flyout closes, reparent all buttons back to the hidden holder frame. +-- They remain invisible (holder is hidden) but their frames still exist and can be +-- reparented back into the flyout when it re-opens. + +function Plugin:RetractButtonsToHolder() + local holder = self:GetOrCreateButtonHolder() + local collected = self._collectedButtons or {} + for _, entry in ipairs(collected) do + if entry.button and entry.button:GetParent() ~= holder then + entry.button:SetParent(holder) + end + end +end + -- [ BUTTON COLLECTION ]----------------------------------------------------------------------------- function Plugin:CollectAddonButtons() @@ -370,12 +390,12 @@ function Plugin:CollectAddonButtons() local icon = button.dataObject and button.dataObject.icon or nil local signature = BuildCollectedButtonSignature(displayName, icon) if not signature or not seenSignatures[signature] then - collected[#collected + 1] = { - name = displayName, - button = button, - icon = icon, - source = "libdbicon", - } + collected[#collected + 1] = { + name = displayName, + button = button, + icon = icon, + source = "libdbicon", + } end if signature then seenSignatures[signature] = true end seen[button] = true @@ -437,48 +457,100 @@ function Plugin:CollectAddonButtons() table.sort(collected, function(a, b) return (a.name or "") < (b.name or "") end) end -function Plugin:HideCollectedButtons() +-- [ GRAB / RELEASE BUTTONS ]------------------------------------------------------------------------ +-- GrabCollectedButtons: reparents buttons to the hidden holder and blocks repositioning. +-- ReleaseCollectedButtons: restores buttons to Minimap with their original positioning. + +function Plugin:GrabCollectedButtons() if not self._collectedButtons then return end + local holder = self:GetOrCreateButtonHolder() + for _, entry in ipairs(self._collectedButtons) do - if entry.button then - entry.button._orbitCompartmentManaged = true - entry.button:Hide() - -- Prevent re-showing by addons that call Show() periodically. - -- hooksecurefunc is taint-safe; the hook fires after the original Show(). - -- We hide immediately afterwards when the compartment is active. - if not entry.button._orbitOnShowHookInstalled then - hooksecurefunc(entry.button, "Show", function(b) - if self._compartmentActive and b._orbitCompartmentManaged and not self._restoringCollectedButtons then - b:Hide() + local button = entry.button + if button then + -- Save original parent and position so we can restore later + if not button._orbitOrigParent then + button._orbitOrigParent = button:GetParent() + local n = button:GetNumPoints() + if n > 0 then + button._orbitOrigPoints = {} + for i = 1, n do + button._orbitOrigPoints[i] = { button:GetPoint(i) } end - end) - entry.button._orbitOnShowHookInstalled = true + end + button._orbitOrigWidth = button:GetWidth() + button._orbitOrigHeight = button:GetHeight() + button._orbitOrigScale = button:GetScale() end - -- For direct minimap children, also suppress SetShown - if entry.source == "minimap_child" and not entry.button._orbitSetShownHookInstalled then - hooksecurefunc(entry.button, "SetShown", function(b, shown) - if shown and self._compartmentActive and b._orbitCompartmentManaged and not self._restoringCollectedButtons then - b:Hide() - end - end) - entry.button._orbitSetShownHookInstalled = true + + -- Reparent to hidden holder — this hides the button without calling Hide() + button:SetParent(holder) + button:SetFrameStrata(holder:GetFrameStrata()) + + -- Block addons from moving their buttons back. + -- Like MBB, we override the positioning methods with no-ops. + if not button._orbitMethodsOverridden then + button.ClearAllPoints = doNothing + button.SetPoint = doNothing + button.SetParent = doNothing + button._orbitMethodsOverridden = true end + + -- Disable drag scripts + button:SetScript("OnDragStart", nil) + button:SetScript("OnDragStop", nil) + button:SetIgnoreParentScale(false) end end end -function Plugin:RestoreCollectedButtons() +function Plugin:ReleaseCollectedButtons() if not self._collectedButtons then return end - self._restoringCollectedButtons = true + for _, entry in ipairs(self._collectedButtons) do - if entry.button then - entry.button._orbitCompartmentManaged = nil - if not (entry.button.db and entry.button.db.hide) then - entry.button:Show() + local button = entry.button + if button then + -- Restore original frame methods + if button._orbitMethodsOverridden then + button.ClearAllPoints = nil -- removes override, restores metatable method + button.SetPoint = nil + button.SetParent = nil + button._orbitMethodsOverridden = nil + end + + -- Restore original parent and position + local origParent = button._orbitOrigParent + if origParent then + button:SetParent(origParent) + + if button._orbitOrigPoints then + button:ClearAllPoints() + for _, pt in ipairs(button._orbitOrigPoints) do + button:SetPoint(unpack(pt)) + end + end + if button._orbitOrigWidth and button._orbitOrigHeight then + button:SetSize(button._orbitOrigWidth, button._orbitOrigHeight) + end + if button._orbitOrigScale then + button:SetScale(button._orbitOrigScale) + end + end + + -- Clean up saved state + button._orbitOrigParent = nil + button._orbitOrigPoints = nil + button._orbitOrigWidth = nil + button._orbitOrigHeight = nil + button._orbitOrigScale = nil + + -- Show the button if it wasn't explicitly hidden by the addon + if not (button.db and button.db.hide) then + button:Show() end end end - self._restoringCollectedButtons = nil + self._collectedButtons = nil end @@ -489,17 +561,17 @@ function Plugin:ApplyAddonCompartment() local useClickAction = self:UsesAddonClickAction() if useClickAction or not self:IsComponentDisabled("Compartment") then - -- Restore any previously-hooked buttons before re-collecting, so stale hooks + -- Release any previously-grabbed buttons before re-collecting, so stale state -- (e.g. on frames that are no longer eligible) are cleaned up each cycle. self._compartmentActive = false - self:RestoreCollectedButtons() + self:ReleaseCollectedButtons() self._compartmentActive = true self:CollectAddonButtons() - self:HideCollectedButtons() + self:GrabCollectedButtons() -- Setup hover reveal for the compartment button local btn = self._compartmentButton - if useClickAction then btn:Hide() else btn:Show() end + if useClickAction then btn:Hide() else btn:Show() end if not frame._compartmentHoverHooked then local minimap = Minimap @@ -536,9 +608,9 @@ function Plugin:ApplyAddonCompartment() C_Timer.After(0.1, function() pending = false if self._compartmentActive then - self:RestoreCollectedButtons() + self:ReleaseCollectedButtons() self:CollectAddonButtons() - self:HideCollectedButtons() + self:GrabCollectedButtons() end end) end) @@ -546,8 +618,8 @@ function Plugin:ApplyAddonCompartment() end else self._compartmentActive = false - self:RestoreCollectedButtons() - if self._compartmentButton then self._compartmentButton:Hide() end if self._compartmentFlyout then self._compartmentFlyout:Hide() end + self:ReleaseCollectedButtons() + if self._compartmentButton then self._compartmentButton:Hide() end end end From 89bec89154ce0589a1a44b56de9d285ed20b1cd1 Mon Sep 17 00:00:00 2001 From: Lars-Martin Date: Tue, 7 Apr 2026 21:11:33 +0200 Subject: [PATCH 3/5] fix: minimap compartment proxy rewrite, difficulty canvas, display dropdown --- CHANGELOG.md | 3 + Orbit/Core/CanvasMode/ComponentSettings.lua | 4 + .../CanvasMode/Creators/IconFrameCreator.lua | 25 +- Orbit/Plugins/Minimap/MinimapCompartment.lua | 367 +++++++++++------- Orbit/Plugins/Minimap/readme.md | 2 +- 5 files changed, 254 insertions(+), 147 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e508836..583a935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **UI Changes:** Various tweaks to UI elements and options. Bugfixes too. ### Fixed +- **Minimap Compartment:** Rewrote addon button flyout to use proxy icon buttons instead of reparenting. Fixes dark rendering on LibDBIcon buttons and flyout overlay blocking clicks. +- **Minimap Difficulty (Canvas):** Skull icon now renders without the background banner by default. A `Show Background` toggle controls it. Placeholder "25" label anchored directly beneath the skull. +- **Minimap Difficulty Display Dropdown:** Switching between Icon/Text in canvas settings now reflects the correct state. The value was being lost when the dialog reopened. - **Cast Bar Preview (Sticky):** Fixed Target/Focus cast bar previews persisting on screen after exiting Edit Mode by clearing preview state before hiding and adding combat-exit guards. Hopefully combat stickiness too. - **Cast Bar Preview (Ticks):** Fixed channel tick marks failing to render in the configuration preview. - **Healer Aura Filtering:** Fixed healer-tracked auras not being excluded from the general Buffs frame on group frames when `HealerAuras` is enabled.Icon Canvas. diff --git a/Orbit/Core/CanvasMode/ComponentSettings.lua b/Orbit/Core/CanvasMode/ComponentSettings.lua index 27d1566..1154ea1 100644 --- a/Orbit/Core/CanvasMode/ComponentSettings.lua +++ b/Orbit/Core/CanvasMode/ComponentSettings.lua @@ -349,6 +349,10 @@ function Settings:OnValueChanged(key, value) local plugin = self.plugin local systemIndex = self.systemIndex local canvasDialog = OrbitEngine.CanvasModeDialog + -- Persist display choice before canvasDialog:Open() restarts the Transaction. + if plugin and plugin.SetSetting then + plugin:SetSetting(systemIndex, "DifficultyDisplay", value) + end C_Timer.After(0, function() if not (canvasDialog and plugin and canvasDialog.targetFrame and canvasDialog:IsShown()) then return end canvasDialog:Open(canvasDialog.targetFrame, canvasDialog.targetPlugin, canvasDialog.targetSystemIndex) diff --git a/Orbit/Core/CanvasMode/Creators/IconFrameCreator.lua b/Orbit/Core/CanvasMode/Creators/IconFrameCreator.lua index 565ed14..6bd97be 100644 --- a/Orbit/Core/CanvasMode/Creators/IconFrameCreator.lua +++ b/Orbit/Core/CanvasMode/Creators/IconFrameCreator.lua @@ -116,12 +116,26 @@ local function Create(container, preview, key, source, data) end local attached = 0 + local skullTexture -- reference to the actual icon texture for label anchoring + -- Respect the "Show Background" toggle: skip Background/Border when disabled. + local savedOverrides = data and data.overrides or {} + local showBg = savedOverrides.DifficultyShowBackground + if showBg == nil then + local plugin = Orbit:GetPlugin("Orbit_Minimap") + showBg = plugin and plugin.GetSetting and plugin:GetSetting("Orbit_Minimap", "DifficultyShowBackground") + end + if activeFrame then - if Attach(activeFrame.Background, 1) then attached = attached + 1 end - if Attach(activeFrame.Border, 1) then attached = attached + 1 end + if showBg then + Attach(activeFrame.Background, 1) + Attach(activeFrame.Border, 1) + end for _, region in ipairs({ activeFrame:GetRegions() }) do if region ~= activeFrame.Background and region ~= activeFrame.Border and region:IsShown() and Attach(region) then attached = attached + 1 + -- Capture the last-created texture on iconVisual as our skull anchor + local regions = { container.iconVisual:GetRegions() } + skullTexture = regions[#regions] break end end @@ -131,8 +145,15 @@ local function Create(container, preview, key, source, data) texture:SetSize(source.Icon:GetWidth(), source.Icon:GetHeight()) texture:SetPoint("CENTER", container.iconVisual, "CENTER") CopyDifficultyTexture(texture, source.Icon, 1) + skullTexture = texture end + -- Placeholder group-size number beneath the skull icon for layout preview. + local labelAnchor = skullTexture or container.iconVisual + local previewLabel = container.iconVisual:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + previewLabel:SetText("25") + previewLabel:SetPoint("TOP", labelAnchor, "BOTTOM", 0, 2) + local function UpdateDifficultySize(self, size) local targetWidth = (size and size > 0) and size or baseWidth local scale = (baseWidth > 0) and (targetWidth / baseWidth) or 1 diff --git a/Orbit/Plugins/Minimap/MinimapCompartment.lua b/Orbit/Plugins/Minimap/MinimapCompartment.lua index 4e18fc2..6d09cdd 100644 --- a/Orbit/Plugins/Minimap/MinimapCompartment.lua +++ b/Orbit/Plugins/Minimap/MinimapCompartment.lua @@ -1,8 +1,4 @@ --- Minimap Addon Compartment --- Collects all LibDBIcon minimap buttons + legacy minimap children into a flyout drawer. --- Uses the REPARENT approach: actual addon buttons are reparented into our container, --- preserving their native click handlers, tooltips, and right-click menus intact. --- Inspired by MinimapButtonButton (MBB) — the most reliable method for minimap button collection. +-- Minimap Addon Compartment — proxy-icon flyout for LibDBIcon + legacy minimap buttons. ---@type Orbit local Orbit = Orbit @@ -14,16 +10,21 @@ local Plugin = Orbit:GetPlugin(SYSTEM_ID) local BORDER_COLOR = Orbit.MinimapConstants.BORDER_COLOR local COMPARTMENT_BUTTON_SIZE = 24 local COMPARTMENT_PADDING = 6 -local FLYOUT_BUTTON_SIZE = 28 -- Size for each reparented button in the flyout grid +local FLYOUT_BUTTON_SIZE = 28 -- Size for each proxy button in the flyout grid local FLYOUT_BUTTON_SPACING = 2 -- Spacing between buttons in the grid local FLYOUT_COLUMNS = 6 -- Number of columns in the flyout grid +local FLYOUT_GAP = 4 -- Pixel gap between flyout and minimap edge +local FLYOUT_CLOSE_DELAY = 0.35 -- Seconds of mouse-outside before auto-closing +local FADE_IN_DURATION = 0.15 +local FADE_OUT_DURATION = 0.3 +local HOLDER_OFFSCREEN = -500 -- Offscreen position for hidden button holder +local EMPTY_FLYOUT_W = 140 +local EMPTY_FLYOUT_H = 30 -- No-op function used to block addons from repositioning their buttons local function doNothing() end --- Blizzard-owned children of Minimap that must never be collected into the compartment. --- Includes reparented Blizzard frames (Missions, Difficulty, etc.) so they are never --- accidentally swept up even if timing or parent-chain quirks expose them as Minimap children. +-- Blizzard-owned Minimap children that must never be collected into the compartment. local BLIZZARD_MINIMAP_CHILDREN = { ["MinimapBackdrop"] = true, ["MinimapCompassTexture"] = true, @@ -33,8 +34,7 @@ local BLIZZARD_MINIMAP_CHILDREN = { ["ExpansionLandingPageMinimapButton"] = true, } --- Name-prefix patterns for map pin/POI overlay frames that get parented to Minimap --- but are not addon buttons (HandyNotes, TomTom, Questie, GatherMate, etc.). +-- Name-prefix patterns for map pin/POI frames (not addon buttons). local PIN_FRAME_PATTERNS = { "^HandyNotes", "^TomTom", @@ -73,9 +73,8 @@ end -- Minimum button width to be considered a real addon button (map pins are typically <20px). local MIN_BUTTON_SIZE = 20 --- Store references to the real frame methods before we override them -local FrameClearAllPoints = UIParent.ClearAllPoints -local FrameSetPoint = UIParent.SetPoint +-- Store reference to the real SetParent method before we override it on collected buttons +local FrameSetParent = UIParent.SetParent -- [ COMPARTMENT BUTTON ]---------------------------------------------------------------------------- @@ -83,8 +82,7 @@ function Plugin:CreateCompartmentButton() if self._compartmentButton then return end local frame = self.frame - -- Drawer toggle button (bottom-right corner of minimap, hidden until hover) - -- Parented to Overlay so it renders above the Minimap render surface + -- Drawer toggle — parented to Overlay so it renders above the Minimap surface local btn = CreateFrame("Button", "OrbitMinimapCompartmentButton", frame.Overlay or frame) btn:SetSize(COMPARTMENT_BUTTON_SIZE, COMPARTMENT_BUTTON_SIZE) btn:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -2, 2) @@ -98,7 +96,7 @@ function Plugin:CreateCompartmentButton() btn.highlight:SetAlpha(0.5) btn.highlight:SetBlendMode("ADD") - -- Atlas icon: Blizzard's map-filter funnel (same as used by World Map filter button) + -- Atlas icon: Blizzard's map-filter funnel btn.icon = btn:CreateTexture(nil, "ARTWORK") btn.icon:SetAllPoints(btn) btn.icon:SetAtlas("Map-Filter-Button", false) @@ -106,13 +104,8 @@ function Plugin:CreateCompartmentButton() -- Setup visual for canvas mode btn.visual = btn.icon - btn.iconPushed = btn:CreateTexture(nil, "ARTWORK") - btn.iconPushed:SetAllPoints(btn) - btn.iconPushed:SetAtlas("Map-Filter-Button-down", false) - btn.iconPushed:Hide() - - btn:SetScript("OnMouseDown", function() btn.icon:Hide(); btn.iconPushed:Show() end) - btn:SetScript("OnMouseUp", function() btn.iconPushed:Hide(); btn.icon:Show() end) + btn:SetScript("OnMouseDown", function() btn.icon:SetAlpha(0.6) end) + btn:SetScript("OnMouseUp", function() btn.icon:SetAlpha(1) end) -- Start hidden; revealed on minimap hover btn:SetAlpha(0) @@ -132,16 +125,13 @@ function Plugin:CreateCompartmentButton() end -- [ HIDDEN BUTTON HOLDER ]-------------------------------------------------------------------------- --- A hidden frame that holds reparented addon buttons while the compartment is active. --- Buttons are reparented here instead of being hidden via Hide(), which avoids all --- Show/Hide hook issues. The holder is always hidden, so buttons inside it are invisible. function Plugin:GetOrCreateButtonHolder() if self._buttonHolder then return self._buttonHolder end local holder = CreateFrame("Frame", "OrbitMinimapButtonHolder", UIParent) holder:SetSize(1, 1) - holder:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -500, 500) -- offscreen + holder:SetPoint("TOPLEFT", UIParent, "TOPLEFT", HOLDER_OFFSCREEN, -HOLDER_OFFSCREEN) holder:Hide() self._buttonHolder = holder @@ -168,21 +158,36 @@ function Plugin:CreateCompartmentFlyout() -- Border Orbit.Skin:SkinBorder(flyout, flyout, Orbit.db.GlobalSettings.BorderSize or 2, BORDER_COLOR) - -- Close on click-away via fullscreen overlay - local overlay = CreateFrame("Button", nil, UIParent) - overlay:SetAllPoints(UIParent) - overlay:SetFrameStrata(Orbit.Constants.Strata.Dialog) - overlay:SetFrameLevel(flyout:GetFrameLevel() - 1) - overlay:Hide() - overlay:RegisterForClicks("LeftButtonUp", "RightButtonUp") - overlay:SetScript("OnClick", function() self:HideCompartmentFlyout() end) - flyout._clickOverlay = overlay - - flyout:SetScript("OnShow", function(f) f._clickOverlay:Show() end) + -- Auto-close via OnUpdate polling when mouse leaves the flyout + minimap area. + local outsideTimer = 0 + + flyout:SetScript("OnUpdate", function(f, elapsed) + if not f:IsShown() then return end + + local mouseOverFlyout = f:IsMouseOver() + local mouseOverBtn = self._compartmentButton and self._compartmentButton:IsMouseOver() + local mouseOverMinimap = Minimap and Minimap:IsMouseOver() + + -- Also check if any child button has a tooltip/menu open (GameTooltip anchored inside) + local tooltipShown = GameTooltip:IsShown() and GameTooltip:GetOwner() + and GameTooltip:GetOwner():GetParent() == f + + if mouseOverFlyout or mouseOverBtn or mouseOverMinimap or tooltipShown then + outsideTimer = 0 + else + outsideTimer = outsideTimer + elapsed + if outsideTimer >= FLYOUT_CLOSE_DELAY then + outsideTimer = 0 + self:HideCompartmentFlyout() + end + end + end) + + flyout:SetScript("OnShow", function(f) + outsideTimer = 0 + end) flyout:SetScript("OnHide", function(f) - f._clickOverlay:Hide() - -- When flyout closes, reparent buttons back to the hidden holder - self:RetractButtonsToHolder() + outsideTimer = 0 end) self._compartmentFlyout = flyout @@ -219,14 +224,139 @@ function Plugin:ShowCompartmentFlyout() end -- [ FLYOUT LAYOUT ]--------------------------------------------------------------------------------- --- Reparent the actual buttons into the flyout and arrange them in a grid. --- No proxy rows, no click forwarding — buttons handle everything natively. + +local proxyButtonPool = {} -- Reusable pool of proxy buttons keyed by original button + +local function GetProxyIcon(originalBtn) + -- Extract the icon texture from the original button + local tex = nil + -- Try .icon first (LibDBIcon standard field) + pcall(function() + if originalBtn.icon and originalBtn.icon.GetTexture then + tex = originalBtn.icon:GetTexture() + end + end) + -- Fallback: dataObject.icon + if not tex then + pcall(function() + if originalBtn.dataObject and originalBtn.dataObject.icon then + tex = originalBtn.dataObject.icon + end + end) + end + -- Fallback: scan regions for a texture with content + if not tex then + pcall(function() + for _, region in ipairs({ originalBtn:GetRegions() }) do + if region and region:IsObjectType("Texture") then + local t = region:GetTexture() + if t and region ~= originalBtn.background and region ~= originalBtn.border then + tex = t + break + end + end + end + end) + end + -- Fallback: GetNormalTexture + if not tex then + pcall(function() + if originalBtn.GetNormalTexture and originalBtn:GetNormalTexture() then + tex = originalBtn:GetNormalTexture():GetTexture() + end + end) + end + return tex +end + +local function GetOrCreateProxyButton(originalBtn, parent, plugin) + if proxyButtonPool[originalBtn] then + local proxy = proxyButtonPool[originalBtn] + proxy:SetParent(parent) + proxy:SetSize(FLYOUT_BUTTON_SIZE, FLYOUT_BUTTON_SIZE) + -- Refresh icon in case the addon changed it + local tex = GetProxyIcon(originalBtn) + if tex then proxy._icon:SetTexture(tex) end + return proxy + end + + local proxy = CreateFrame("Button", nil, parent) + proxy:SetSize(FLYOUT_BUTTON_SIZE, FLYOUT_BUTTON_SIZE) + proxy._originalBtn = originalBtn + + -- Create a clean icon texture + local icon = proxy:CreateTexture(nil, "ARTWORK") + icon:SetAllPoints() + proxy._icon = icon + + local tex = GetProxyIcon(originalBtn) + if tex then + icon:SetTexture(tex) + else + icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") + end + -- Copy texcoords from original if available + pcall(function() + if originalBtn.icon and originalBtn.icon.GetTexCoord then + icon:SetTexCoord(originalBtn.icon:GetTexCoord()) + end + end) + icon:SetDesaturated(false) + icon:SetAlpha(1) + icon:SetVertexColor(1, 1, 1, 1) + + -- Highlight on hover + local highlight = proxy:CreateTexture(nil, "HIGHLIGHT") + highlight:SetAllPoints() + highlight:SetColorTexture(1, 1, 1, 0.15) + + -- Forward clicks via dataObject.OnClick; AnyUp only to avoid double-fire. + proxy:RegisterForClicks("AnyUp") + proxy:SetScript("OnClick", function(_, button) + local btn = originalBtn + local b = button or "LeftButton" + if btn.dataObject and btn.dataObject.OnClick then + btn.dataObject.OnClick(btn, b) + elseif btn:GetScript("OnClick") then + pcall(function() btn:GetScript("OnClick")(btn, b) end) + else + pcall(function() btn:Click(b) end) + end + end) + + -- Forward tooltip + proxy:SetScript("OnEnter", function(self) + if originalBtn.dataObject then + local dObj = originalBtn.dataObject + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + if dObj.OnTooltipShow then + dObj.OnTooltipShow(GameTooltip) + elseif dObj.text then + GameTooltip:SetText(dObj.text) + end + GameTooltip:Show() + else + pcall(function() + local script = originalBtn:GetScript("OnEnter") + if script then script(originalBtn) end + end) + end + end) + proxy:SetScript("OnLeave", function() + GameTooltip:Hide() + pcall(function() + local script = originalBtn:GetScript("OnLeave") + if script then script(originalBtn) end + end) + end) + + proxyButtonPool[originalBtn] = proxy + return proxy +end function Plugin:LayoutButtonsInFlyout() local flyout = self._compartmentFlyout if not flyout then return end - local btn = self._compartmentButton - local anchor = btn and btn:IsShown() and btn or self.frame local collected = self._collectedButtons or {} -- Count visible buttons (respect LibDBIcon .db.hide) @@ -240,10 +370,15 @@ function Plugin:LayoutButtonsInFlyout() end end + -- Hide all existing proxy buttons first + for _, proxy in pairs(proxyButtonPool) do + proxy:Hide() + end + if #visibleEntries == 0 then - flyout:SetSize(140, 30) + flyout:SetSize(EMPTY_FLYOUT_W, EMPTY_FLYOUT_H) flyout:ClearAllPoints() - flyout:SetPoint("BOTTOMRIGHT", anchor, "TOPRIGHT", 0, 2) + flyout:SetPoint("TOPRIGHT", self.frame, "BOTTOMRIGHT", 0, -FLYOUT_GAP) if not flyout._emptyText then flyout._emptyText = flyout:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall") flyout._emptyText:SetPoint("CENTER") @@ -262,106 +397,54 @@ function Plugin:LayoutButtonsInFlyout() local flyoutHeight = (rows * cellSize) + FLYOUT_BUTTON_SPACING + (COMPARTMENT_PADDING * 2) flyout:SetSize(flyoutWidth, flyoutHeight) - -- Reparent each button into the flyout and position in grid + -- Create/reuse proxy buttons and position them in a grid for i, entry in ipairs(visibleEntries) do - local button = entry.button - if button then + local originalBtn = entry.button + if originalBtn then + local proxy = GetOrCreateProxyButton(originalBtn, flyout, self) + proxy:SetFrameLevel(flyout:GetFrameLevel() + 5) + local col = (i - 1) % cols local row = math.floor((i - 1) / cols) local xOff = COMPARTMENT_PADDING + FLYOUT_BUTTON_SPACING + (col * cellSize) local yOff = -(COMPARTMENT_PADDING + FLYOUT_BUTTON_SPACING + (row * cellSize)) - -- Reparent into flyout — this makes the button visible and clickable inside the flyout - button:SetParent(flyout) - button:SetFrameStrata(flyout:GetFrameStrata()) - button:SetFrameLevel(flyout:GetFrameLevel() + 2) - - -- Position in grid using stored real methods (we overrode ClearAllPoints/SetPoint) - FrameClearAllPoints(button) - FrameSetPoint(button, "TOPLEFT", flyout, "TOPLEFT", xOff, yOff) - - -- Normalize size - button:SetSize(FLYOUT_BUTTON_SIZE, FLYOUT_BUTTON_SIZE) - button:SetIgnoreParentScale(false) - button:SetScale(1) - - -- Strip the circular minimap button border/overlay textures for a cleaner look. - -- LibDBIcon buttons typically have overlay/border textures we can hide. - if not button._orbitSkinned then - -- Hide common minimap button decoration textures - for _, region in ipairs({ button:GetRegions() }) do - if region:IsObjectType("Texture") then - local tex = region:GetTexture() - if tex and type(tex) == "string" then - local texLower = tex:lower() - if texLower:find("border") or texLower:find("trackingborder") - or texLower:find("minimap%-trackingborder") or texLower:find("overlay") then - region:SetTexture(nil) - end - end - -- Also hide by draw layer — OVERLAY is typically the ring/border - local layer = region:GetDrawLayer() - if layer == "OVERLAY" and region ~= (button.icon or button.Icon) then - region:SetAlpha(0) - end - end - end - button._orbitSkinned = true - end - - -- Disable drag scripts on collected buttons (they shouldn't be draggable in the flyout) - button:SetScript("OnDragStart", nil) - button:SetScript("OnDragStop", nil) - - -- Show the button — it's now a child of the visible flyout - button:Show() + proxy:ClearAllPoints() + proxy:SetPoint("TOPLEFT", flyout, "TOPLEFT", xOff, yOff) + proxy:Show() end end - -- Position the flyout intelligently based on available screen space - local scale = anchor:GetEffectiveScale() - local screenW = GetScreenWidth() * UIParent:GetEffectiveScale() - local screenH = GetScreenHeight() * UIParent:GetEffectiveScale() - local btnLeft = anchor:GetLeft() * scale - local btnRight = anchor:GetRight() * scale - local btnTop = anchor:GetTop() * scale - local btnBottom = anchor:GetBottom() * scale - local flyW = flyoutWidth * scale - local flyH = flyoutHeight * scale - local gap = 2 + -- Position flyout outside the minimap, choosing the side with most space. + local mmFrame = self.frame + local scale = mmFrame:GetEffectiveScale() + local uiScale = UIParent:GetEffectiveScale() + local screenW = GetScreenWidth() * uiScale + local screenH = GetScreenHeight() * uiScale - -- Vertical: prefer expanding upward; fall back to downward if not enough room above - local spaceAbove = screenH - btnTop - local spaceBelow = btnBottom - local expandUp = (spaceAbove >= flyH + gap) or (spaceAbove >= spaceBelow) + local mmLeft = (mmFrame:GetLeft() or 0) * scale + local mmRight = (mmFrame:GetRight() or screenW) * scale + local mmTop = (mmFrame:GetTop() or screenH) * scale + local mmBottom = (mmFrame:GetBottom() or 0) * scale - -- Horizontal: prefer aligning to the right edge; fall back to left alignment if it would clip - local expandLeft = (btnRight + flyW) > screenW + local flyW = flyoutWidth * scale + local flyH = flyoutHeight * scale + local spaceLeft = mmLeft + local spaceRight = screenW - mmRight + local spaceAbove = screenH - mmTop + local spaceBelow = mmBottom flyout:ClearAllPoints() - if expandUp and not expandLeft then - flyout:SetPoint("BOTTOMRIGHT", anchor, "TOPRIGHT", 0, gap) - elseif expandUp and expandLeft then - flyout:SetPoint("BOTTOMLEFT", anchor, "TOPLEFT", 0, gap) - elseif not expandUp and not expandLeft then - flyout:SetPoint("TOPRIGHT", anchor, "BOTTOMRIGHT", 0, -gap) + if spaceBelow >= flyH + FLYOUT_GAP then + flyout:SetPoint("TOPRIGHT", mmFrame, "BOTTOMRIGHT", 0, -FLYOUT_GAP) + elseif spaceAbove >= flyH + FLYOUT_GAP then + flyout:SetPoint("BOTTOMRIGHT", mmFrame, "TOPRIGHT", 0, FLYOUT_GAP) + elseif spaceLeft >= flyW + FLYOUT_GAP then + flyout:SetPoint("TOPRIGHT", mmFrame, "TOPLEFT", -FLYOUT_GAP, 0) + elseif spaceRight >= flyW + FLYOUT_GAP then + flyout:SetPoint("TOPLEFT", mmFrame, "TOPRIGHT", FLYOUT_GAP, 0) else - flyout:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -gap) - end -end - --- [ RETRACT BUTTONS TO HOLDER ]--------------------------------------------------------------------- --- When the flyout closes, reparent all buttons back to the hidden holder frame. --- They remain invisible (holder is hidden) but their frames still exist and can be --- reparented back into the flyout when it re-opens. - -function Plugin:RetractButtonsToHolder() - local holder = self:GetOrCreateButtonHolder() - local collected = self._collectedButtons or {} - for _, entry in ipairs(collected) do - if entry.button and entry.button:GetParent() ~= holder then - entry.button:SetParent(holder) - end + flyout:SetPoint("TOPRIGHT", mmFrame, "BOTTOMRIGHT", 0, -FLYOUT_GAP) end end @@ -458,8 +541,6 @@ function Plugin:CollectAddonButtons() end -- [ GRAB / RELEASE BUTTONS ]------------------------------------------------------------------------ --- GrabCollectedButtons: reparents buttons to the hidden holder and blocks repositioning. --- ReleaseCollectedButtons: restores buttons to Minimap with their original positioning. function Plugin:GrabCollectedButtons() if not self._collectedButtons then return end @@ -483,12 +564,11 @@ function Plugin:GrabCollectedButtons() button._orbitOrigScale = button:GetScale() end - -- Reparent to hidden holder — this hides the button without calling Hide() - button:SetParent(holder) + -- Reparent to hidden holder via raw SetParent (ours is overridden with doNothing) + FrameSetParent(button, holder) button:SetFrameStrata(holder:GetFrameStrata()) - -- Block addons from moving their buttons back. - -- Like MBB, we override the positioning methods with no-ops. + -- Block addons from repositioning their buttons back. if not button._orbitMethodsOverridden then button.ClearAllPoints = doNothing button.SetPoint = doNothing @@ -561,8 +641,7 @@ function Plugin:ApplyAddonCompartment() local useClickAction = self:UsesAddonClickAction() if useClickAction or not self:IsComponentDisabled("Compartment") then - -- Release any previously-grabbed buttons before re-collecting, so stale state - -- (e.g. on frames that are no longer eligible) are cleaned up each cycle. + -- Release then re-collect to purge stale state each cycle. self._compartmentActive = false self:ReleaseCollectedButtons() self._compartmentActive = true @@ -577,14 +656,14 @@ function Plugin:ApplyAddonCompartment() local minimap = Minimap local function ShowCompartmentButton() if not btn:IsShown() then return end - UIFrameFadeIn(btn, 0.15, btn:GetAlpha(), 1) + UIFrameFadeIn(btn, FADE_IN_DURATION, btn:GetAlpha(), 1) end local function HideCompartmentButton() if not btn:IsShown() then return end if btn:IsMouseOver() then return end if minimap and minimap:IsMouseOver() then return end if self._compartmentFlyout and self._compartmentFlyout:IsShown() then return end - UIFrameFadeOut(btn, 0.3, btn:GetAlpha(), 0) + UIFrameFadeOut(btn, FADE_OUT_DURATION, btn:GetAlpha(), 0) end frame:HookScript("OnEnter", ShowCompartmentButton) frame:HookScript("OnLeave", HideCompartmentButton) diff --git a/Orbit/Plugins/Minimap/readme.md b/Orbit/Plugins/Minimap/readme.md index 3a2a94f..e618e96 100644 --- a/Orbit/Plugins/Minimap/readme.md +++ b/Orbit/Plugins/Minimap/readme.md @@ -39,7 +39,7 @@ all components below are individually positionable via canvas mode and can be di the minimap supports configurable left-, middle-, and right-click actions via plugin settings. -canvas overrides (font, size, color) are supported for ZoneText, Clock, Coords, and Difficulty text mode via the canvas component settings panel. canvas always shows the difficulty background to make placement easier in icon mode. `Difficulty` icon mode and text mode are handled as separate internal components, so preview sizing/alignment no longer depends on switching one component between two different geometries. +canvas overrides (font, size, color) are supported for ZoneText, Clock, Coords, and Difficulty text mode via the canvas component settings panel. canvas respects the `DifficultyShowBackground` toggle in icon mode and shows a placeholder group-size number beneath the skull. `Difficulty` icon mode and text mode are handled as separate internal components, so preview sizing/alignment no longer depends on switching one component between two different geometries. ## blizzard frames affected From a9a6b27020d5788457410ec43ccd809f26b15358 Mon Sep 17 00:00:00 2001 From: Lars-Martin Date: Sat, 11 Apr 2026 07:47:50 +0200 Subject: [PATCH 4/5] =?UTF-8?q?feat(minimap):=20FarmHud=20compatibility=20?= =?UTF-8?q?=E2=80=94=20suspend=20FrameGuard=20during=20HUD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 + Orbit/Core/EditMode/Frame/Guard.lua | 19 +++++- Orbit/Plugins/Minimap/Minimap.lua | 6 +- Orbit/Plugins/Minimap/MinimapCapture.lua | 68 +++++++++++++++++--- Orbit/Plugins/Minimap/MinimapCompartment.lua | 4 +- Orbit/Plugins/Minimap/readme.md | 12 ++++ 6 files changed, 98 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 583a935..68d6613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,8 +40,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - **Minimap Compartment:** Rewrote addon button flyout to use proxy icon buttons instead of reparenting. Fixes dark rendering on LibDBIcon buttons and flyout overlay blocking clicks. +- **Minimap Compartment Flyout:** Mouse-leave detection now checks the Orbit container instead of the raw `Minimap` surface, fixing premature flyout closure when third-party addons reparent the minimap. - **Minimap Difficulty (Canvas):** Skull icon now renders without the background banner by default. A `Show Background` toggle controls it. Placeholder "25" label anchored directly beneath the skull. - **Minimap Difficulty Display Dropdown:** Switching between Icon/Text in canvas settings now reflects the correct state. The value was being lost when the dialog reopened. +- **FarmHud Compatibility:** Added `Guard:Suspend`/`Guard:Resume` to `FrameGuard` so cooperating addons can temporarily reparent the minimap surface. Orbit now hooks FarmHud's show/hide to suspend protection, skip surface re-sync in `ApplySettings`, and resume on close — preventing the mount/dismount/shapeshift resource-display loss reported by users. - **Cast Bar Preview (Sticky):** Fixed Target/Focus cast bar previews persisting on screen after exiting Edit Mode by clearing preview state before hiding and adding combat-exit guards. Hopefully combat stickiness too. - **Cast Bar Preview (Ticks):** Fixed channel tick marks failing to render in the configuration preview. - **Healer Aura Filtering:** Fixed healer-tracked auras not being excluded from the general Buffs frame on group frames when `HealerAuras` is enabled.Icon Canvas. diff --git a/Orbit/Core/EditMode/Frame/Guard.lua b/Orbit/Core/EditMode/Frame/Guard.lua index 229740a..ba2463b 100644 --- a/Orbit/Core/EditMode/Frame/Guard.lua +++ b/Orbit/Core/EditMode/Frame/Guard.lua @@ -13,7 +13,7 @@ function Guard:Protect(frame, parent) if not frame._orbitGuardHooked then hooksecurefunc(frame, "SetParent", function(s, p) - if s._orbitRestoring then return end + if s._orbitRestoring or s._orbitGuardSuspended then return end local intended = s._orbitGuardParent if intended and p ~= intended then s._orbitRestoring = true @@ -24,7 +24,7 @@ function Guard:Protect(frame, parent) end) frame:HookScript("OnHide", function(s) - if s._orbitRestoring then return end + if s._orbitRestoring or s._orbitGuardSuspended then return end if s._orbitGuardEnforceShow then s._orbitRestoring = true s:Show() @@ -37,6 +37,21 @@ function Guard:Protect(frame, parent) end end +-- [ SUSPEND / RESUME ]------------------------------------------------------------------------------ +-- Temporarily disables guard enforcement (SetParent snap-back and enforce-show) +-- without removing the hooks. Used when a cooperating addon (e.g. FarmHud) needs +-- to reparent the minimap surface for its own overlay. + +function Guard:Suspend(frame) + if not frame then return end + frame._orbitGuardSuspended = true +end + +function Guard:Resume(frame) + if not frame then return end + frame._orbitGuardSuspended = nil +end + -- [ UPDATE ]---------------------------------------------------------------------------------------- function Guard:UpdateProtection(frame, parent, onRestoreFunc, options) diff --git a/Orbit/Plugins/Minimap/Minimap.lua b/Orbit/Plugins/Minimap/Minimap.lua index 5f98630..51e45e4 100644 --- a/Orbit/Plugins/Minimap/Minimap.lua +++ b/Orbit/Plugins/Minimap/Minimap.lua @@ -606,8 +606,9 @@ function Plugin:ApplySettings() SetCVar("rotateMinimap", rotate and "1" or "0") -- Keep the Minimap render surface in sync with the container. + -- Skip when FarmHud is active — it owns the surface position/size while its HUD is open. local minimapSurface = self:GetBlizzardMinimap() - if minimapSurface then + if minimapSurface and not self._farmHudActive then minimapSurface._orbitIntendedSize = size -- Micro-size bounce for C++ redraw minimapSurface:SetSize(size - 1, size - 1) @@ -754,8 +755,9 @@ function Plugin:ApplySettings() OrbitEngine.Frame:RestorePosition(frame, self, SYSTEM_ID) -- Ensure minimap is captured (e.g. after reload). + -- Skip when FarmHud is active — it legitimately reparented the minimap for its HUD overlay. local minimap = self:GetBlizzardMinimap() - if minimap and minimap:GetParent() ~= frame then + if minimap and minimap:GetParent() ~= frame and not self._farmHudActive then self:CaptureBlizzardMinimap() end diff --git a/Orbit/Plugins/Minimap/MinimapCapture.lua b/Orbit/Plugins/Minimap/MinimapCapture.lua index c7dc241..fd2b5e3 100644 --- a/Orbit/Plugins/Minimap/MinimapCapture.lua +++ b/Orbit/Plugins/Minimap/MinimapCapture.lua @@ -373,7 +373,7 @@ function Plugin:CaptureBlizzardMinimap() -- the minimap away from our intended values. if not minimap._orbitSetPointHooked then hooksecurefunc(minimap, "SetPoint", function(f, ...) - if f._orbitRestoringPoint then return end + if f._orbitRestoringPoint or f._orbitGuardSuspended then return end if f:GetParent() == self.frame then local point = ... local relFrame = select(2, ...) @@ -387,7 +387,7 @@ function Plugin:CaptureBlizzardMinimap() end end) hooksecurefunc(minimap, "SetSize", function(f, w, h) - if f._orbitRestoringPoint then return end + if f._orbitRestoringPoint or f._orbitGuardSuspended then return end if f:GetParent() == self.frame then local intended = self.frame:GetWidth() if intended and (math.abs(w - intended) > 0.5 or math.abs(h - intended) > 0.5) then @@ -404,12 +404,9 @@ function Plugin:CaptureBlizzardMinimap() -- a MEDIUM-strata Button with SetPropagateMouseClicks(true) that covers the whole -- minimap area and sits above most third-party overlays. No per-frame hook needed. - -- FarmHud integration: register our container so FarmHud knows about it. - C_Timer.After(0, function() - if FarmHud and FarmHud.RegisterForeignAddOnObject then - FarmHud:RegisterForeignAddOnObject(self.frame, "Orbit") - end - end) + -- FarmHud integration: register our container and hook show/hide to suspend + -- FrameGuard protection so FarmHud can reparent the minimap surface freely. + self:HookFarmHud() -- Update zoom button state after scroll-wheel zoom if not minimap._orbitScrollHooked then @@ -422,3 +419,58 @@ function Plugin:CaptureBlizzardMinimap() self._captured = true end + +-- [ FARMHUD COMPATIBILITY ]------------------------------------------------------------------------- +-- FarmHud reparents the Minimap surface to its own full-screen HUD frame when +-- toggled on, and restores it when toggled off. Our FrameGuard (SetParent hook +-- + enforce-show) and the SetPoint/SetSize protection hooks would fight this, +-- causing the minimap to snap back immediately. We cooperate by: +-- 1. Registering our container via FarmHud:RegisterForeignAddOnObject so +-- FarmHud knows to move our children to its dummy placeholder. +-- 2. Hooking FarmHud's OnShow/OnHide to suspend and resume FrameGuard and +-- hide/show our Overlay and ClickCapture (they would float over the HUD). +-- The _farmHudActive flag is also checked in ApplySettings() to skip minimap +-- surface re-sync and recapture while FarmHud owns the surface. + +function Plugin:HookFarmHud() + if self._farmHudHooked then return end + + -- Deferred: FarmHud may load after Orbit. + C_Timer.After(0, function() + if not FarmHud then return end + + -- Let FarmHud move our container's children to its dummy placeholder. + if FarmHud.RegisterForeignAddOnObject then + FarmHud:RegisterForeignAddOnObject(self.frame, "Orbit") + end + + -- Hook OnShow/OnHide only once. + if not self._farmHudHooked then + FarmHud:HookScript("OnShow", function() self:OnFarmHudShow() end) + FarmHud:HookScript("OnHide", function() self:OnFarmHudHide() end) + self._farmHudHooked = true + end + end) +end + +function Plugin:OnFarmHudShow() + self._farmHudActive = true + + local minimap = self:GetBlizzardMinimap() + if minimap then + OrbitEngine.FrameGuard:Suspend(minimap) + end +end + +function Plugin:OnFarmHudHide() + self._farmHudActive = nil + + local minimap = self:GetBlizzardMinimap() + if minimap then + OrbitEngine.FrameGuard:Resume(minimap) + end + + -- Re-apply settings to recapture the minimap surface and restore our layout. + self:ApplySettings() +end + diff --git a/Orbit/Plugins/Minimap/MinimapCompartment.lua b/Orbit/Plugins/Minimap/MinimapCompartment.lua index 6d09cdd..43ce3d0 100644 --- a/Orbit/Plugins/Minimap/MinimapCompartment.lua +++ b/Orbit/Plugins/Minimap/MinimapCompartment.lua @@ -166,7 +166,9 @@ function Plugin:CreateCompartmentFlyout() local mouseOverFlyout = f:IsMouseOver() local mouseOverBtn = self._compartmentButton and self._compartmentButton:IsMouseOver() - local mouseOverMinimap = Minimap and Minimap:IsMouseOver() + -- When FarmHud is active the Minimap surface is reparented to the HUD frame, + -- so IsMouseOver would check the wrong screen region. Fall back to our container. + local mouseOverMinimap = self.frame and self.frame:IsMouseOver() -- Also check if any child button has a tooltip/menu open (GameTooltip anchored inside) local tooltipShown = GameTooltip:IsShown() and GameTooltip:GetOwner() diff --git a/Orbit/Plugins/Minimap/readme.md b/Orbit/Plugins/Minimap/readme.md index e618e96..30152cb 100644 --- a/Orbit/Plugins/Minimap/readme.md +++ b/Orbit/Plugins/Minimap/readme.md @@ -71,3 +71,15 @@ canvas overrides (font, size, color) are supported for ZoneText, Clock, Coords, ## data flow savedvariables → `ApplySettings()` → sizes container, skins border, applies component visibility via `IsComponentDisabled()`, restores canvas positions via `ComponentDrag:RestoreFramePositions()`, applies canvas overrides via `OverrideUtils.ApplyOverrides()`, sets scale + +## third-party addon compatibility + +### FarmHud + +FarmHud reparents the `Minimap` surface to its own full-screen HUD frame while active, then restores it on hide. orbit cooperates via: + +1. **`RegisterForeignAddOnObject`** — tells FarmHud about our container so it can move child frames to its dummy placeholder. +2. **`Guard:Suspend` / `Guard:Resume`** — on FarmHud show, FrameGuard protection (SetParent snap-back + enforce-show) is suspended so FarmHud can freely reparent and resize the surface. on hide, protection is resumed. +3. **SetPoint / SetSize hooks** — also check the suspension flag and yield while FarmHud is active. +4. **`ApplySettings()` guards** — minimap surface sync (size bounce, re-anchor) and the recapture check are skipped when `_farmHudActive` is set, so visibility events (mount/dismount/shapeshift) don't fight FarmHud's layout. +5. **No UI hiding needed** — FarmHud's HUD is a separate full-screen frame, not layered under our container. Our Overlay, ClickCapture, and compartment all remain functional on the minimap slot while FarmHud is open. From 8ef3cfcdc75db7852b85a2bc7a790f6657ba5077 Mon Sep 17 00:00:00 2001 From: Lars-Martin Date: Sat, 11 Apr 2026 07:51:28 +0200 Subject: [PATCH 5/5] chore: coding guidelines pass on branch changes --- Orbit/Core/CanvasMode/ComponentSettings.lua | 4 +--- .../CanvasMode/Creators/IconFrameCreator.lua | 12 +++++------- Orbit/Core/EditMode/Frame/Guard.lua | 5 +---- Orbit/Plugins/Minimap/MinimapCapture.lua | 16 ++-------------- Orbit/Plugins/Minimap/MinimapCompartment.lua | 17 +++++------------ 5 files changed, 14 insertions(+), 40 deletions(-) diff --git a/Orbit/Core/CanvasMode/ComponentSettings.lua b/Orbit/Core/CanvasMode/ComponentSettings.lua index 2a46b6a..131b2b0 100644 --- a/Orbit/Core/CanvasMode/ComponentSettings.lua +++ b/Orbit/Core/CanvasMode/ComponentSettings.lua @@ -357,9 +357,7 @@ function Settings:OnValueChanged(key, value) local systemIndex = self.systemIndex local canvasDialog = OrbitEngine.CanvasModeDialog -- Persist display choice before canvasDialog:Open() restarts the Transaction. - if plugin and plugin.SetSetting then - plugin:SetSetting(systemIndex, "DifficultyDisplay", value) - end + plugin:SetSetting(systemIndex, "DifficultyDisplay", value) C_Timer.After(0, function() if not (canvasDialog and plugin and canvasDialog.targetFrame and canvasDialog:IsShown()) then return end canvasDialog:Open(canvasDialog.targetFrame, canvasDialog.targetPlugin, canvasDialog.targetSystemIndex) diff --git a/Orbit/Core/CanvasMode/Creators/IconFrameCreator.lua b/Orbit/Core/CanvasMode/Creators/IconFrameCreator.lua index 6bd97be..2532dc0 100644 --- a/Orbit/Core/CanvasMode/Creators/IconFrameCreator.lua +++ b/Orbit/Core/CanvasMode/Creators/IconFrameCreator.lua @@ -116,13 +116,13 @@ local function Create(container, preview, key, source, data) end local attached = 0 - local skullTexture -- reference to the actual icon texture for label anchoring - -- Respect the "Show Background" toggle: skip Background/Border when disabled. + local skullTexture -- icon texture reference used to anchor the preview label + -- Respect DifficultyShowBackground: skip Background/Border art when disabled. local savedOverrides = data and data.overrides or {} local showBg = savedOverrides.DifficultyShowBackground if showBg == nil then local plugin = Orbit:GetPlugin("Orbit_Minimap") - showBg = plugin and plugin.GetSetting and plugin:GetSetting("Orbit_Minimap", "DifficultyShowBackground") + showBg = plugin and plugin:GetSetting("Orbit_Minimap", "DifficultyShowBackground") end if activeFrame then @@ -133,8 +133,7 @@ local function Create(container, preview, key, source, data) for _, region in ipairs({ activeFrame:GetRegions() }) do if region ~= activeFrame.Background and region ~= activeFrame.Border and region:IsShown() and Attach(region) then attached = attached + 1 - -- Capture the last-created texture on iconVisual as our skull anchor - local regions = { container.iconVisual:GetRegions() } + local regions = { container.iconVisual:GetRegions() } -- skull anchor: last-created texture skullTexture = regions[#regions] break end @@ -148,8 +147,7 @@ local function Create(container, preview, key, source, data) skullTexture = texture end - -- Placeholder group-size number beneath the skull icon for layout preview. - local labelAnchor = skullTexture or container.iconVisual + local labelAnchor = skullTexture or container.iconVisual -- placeholder "25" beneath skull local previewLabel = container.iconVisual:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") previewLabel:SetText("25") previewLabel:SetPoint("TOP", labelAnchor, "BOTTOM", 0, 2) diff --git a/Orbit/Core/EditMode/Frame/Guard.lua b/Orbit/Core/EditMode/Frame/Guard.lua index ba2463b..e3d6c5d 100644 --- a/Orbit/Core/EditMode/Frame/Guard.lua +++ b/Orbit/Core/EditMode/Frame/Guard.lua @@ -38,10 +38,7 @@ function Guard:Protect(frame, parent) end -- [ SUSPEND / RESUME ]------------------------------------------------------------------------------ --- Temporarily disables guard enforcement (SetParent snap-back and enforce-show) --- without removing the hooks. Used when a cooperating addon (e.g. FarmHud) needs --- to reparent the minimap surface for its own overlay. - +-- Disables guard enforcement without removing hooks (e.g. FarmHud owns the surface temporarily). function Guard:Suspend(frame) if not frame then return end frame._orbitGuardSuspended = true diff --git a/Orbit/Plugins/Minimap/MinimapCapture.lua b/Orbit/Plugins/Minimap/MinimapCapture.lua index fd2b5e3..490a658 100644 --- a/Orbit/Plugins/Minimap/MinimapCapture.lua +++ b/Orbit/Plugins/Minimap/MinimapCapture.lua @@ -421,16 +421,7 @@ function Plugin:CaptureBlizzardMinimap() end -- [ FARMHUD COMPATIBILITY ]------------------------------------------------------------------------- --- FarmHud reparents the Minimap surface to its own full-screen HUD frame when --- toggled on, and restores it when toggled off. Our FrameGuard (SetParent hook --- + enforce-show) and the SetPoint/SetSize protection hooks would fight this, --- causing the minimap to snap back immediately. We cooperate by: --- 1. Registering our container via FarmHud:RegisterForeignAddOnObject so --- FarmHud knows to move our children to its dummy placeholder. --- 2. Hooking FarmHud's OnShow/OnHide to suspend and resume FrameGuard and --- hide/show our Overlay and ClickCapture (they would float over the HUD). --- The _farmHudActive flag is also checked in ApplySettings() to skip minimap --- surface re-sync and recapture while FarmHud owns the surface. +-- Suspend FrameGuard and surface-sync while FarmHud owns the Minimap. See readme for details. function Plugin:HookFarmHud() if self._farmHudHooked then return end @@ -439,10 +430,7 @@ function Plugin:HookFarmHud() C_Timer.After(0, function() if not FarmHud then return end - -- Let FarmHud move our container's children to its dummy placeholder. - if FarmHud.RegisterForeignAddOnObject then - FarmHud:RegisterForeignAddOnObject(self.frame, "Orbit") - end + FarmHud:RegisterForeignAddOnObject(self.frame, "Orbit") -- Hook OnShow/OnHide only once. if not self._farmHudHooked then diff --git a/Orbit/Plugins/Minimap/MinimapCompartment.lua b/Orbit/Plugins/Minimap/MinimapCompartment.lua index 43ce3d0..f2cf6c1 100644 --- a/Orbit/Plugins/Minimap/MinimapCompartment.lua +++ b/Orbit/Plugins/Minimap/MinimapCompartment.lua @@ -166,13 +166,10 @@ function Plugin:CreateCompartmentFlyout() local mouseOverFlyout = f:IsMouseOver() local mouseOverBtn = self._compartmentButton and self._compartmentButton:IsMouseOver() - -- When FarmHud is active the Minimap surface is reparented to the HUD frame, - -- so IsMouseOver would check the wrong screen region. Fall back to our container. - local mouseOverMinimap = self.frame and self.frame:IsMouseOver() + -- Use our container (not Minimap surface) — FarmHud reparents Minimap away from our frame. + local mouseOverMinimap = self.frame:IsMouseOver() - -- Also check if any child button has a tooltip/menu open (GameTooltip anchored inside) - local tooltipShown = GameTooltip:IsShown() and GameTooltip:GetOwner() - and GameTooltip:GetOwner():GetParent() == f + local tooltipShown = GameTooltip:IsShown() and GameTooltip:GetOwner() and GameTooltip:GetOwner():GetParent() == f -- proxy tooltip open if mouseOverFlyout or mouseOverBtn or mouseOverMinimap or tooltipShown then outsideTimer = 0 @@ -185,12 +182,8 @@ function Plugin:CreateCompartmentFlyout() end end) - flyout:SetScript("OnShow", function(f) - outsideTimer = 0 - end) - flyout:SetScript("OnHide", function(f) - outsideTimer = 0 - end) + flyout:SetScript("OnShow", function() outsideTimer = 0 end) + flyout:SetScript("OnHide", function() outsideTimer = 0 end) self._compartmentFlyout = flyout end