diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe643e..68d6613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,58 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## How to contribute + +1. **Add your changes under the top version block** (`## [@project-version@]`). Never edit older version blocks. +2. **Use the correct sub-header** for your change type: + - `### Message` — release notes / personal message to users + - `### Added` — new features + - `### Changed` — changes to existing behaviour + - `### Fixed` — bug fixes + - `### Removed` — removed features +3. **Write one bullet per change**, starting with `- `. Markdown formatting is supported in-game: + - `**bold**` renders gold, `` `code` `` renders cyan. +4. **Do not touch `ChangelogData.lua`** — it is auto-generated by `.scripts/update_changelog.py` on release. + + ## [@project-version@] - @project-date-iso@ -### New Features -- Datatext Drawer - Click one of the four corners of the screen to open the datatext drawer. Drag and drop them anywhere on-screen. Drag them back into the drawer to disable. Drag the right hand corner to resize them. Will continue to expand on these and you're welcome to suggest more/improve whats been built. - -- Meta Talents - Added a new QoL feature to help you keep track of the most popular talents for each spec. Find it in the Quality of Life tab in `/orbit plugins`. - - Select bosses or dungeons to view the most popular talents for that specific encounter, data is fetched and averaged from Warcraft Logs top 100 parses. - - Directly Apply the meta talents to your talent tree with a single click. - -### Bugfixes -- Static Cooldown Timer Texts -- Player Buff Item Enhancements now draw a pixel border for item enhancements (weapon oils, etc) -- Player Buffs/Debuffs swipe now start at low alpha and fill as their duration expires -- Player Buffs/Debuffs now pulse in and out when expiring +### TLDR +- Lots of backend changes in this one, some icon frames may need size changes, have moved away from Scale and towards Pixels instead. +- Overhaul of Glows in Orbit, please revise all glows, added more customization options, better alignment and performance. No longer using LibCustomGlow so should resolve a lot of conflicts from other addons. +- Enabled Minimap plugin by default. Remember can disable all plugins in the `Plugin Manager (/orbit plugins)`. +- Added **Datatexts**, **just click one of the four corners of the screen** to open the datatext drawer and drag and drop them anywhere on-screen. Will continue to expand on these and you're welcome to suggest more/improve whats been built. + +### Added +- **Datatexts Plugin:** Brand-new standalone plugin with 20+ widgets (Gold, Friends, Guild, Durability, Performance, Hearthstone, Spec, Location, Mail, Quest, Combat Timer, and more). Replaces the old Performance and Combat Timer from Menu Items. +- **Glows:** New custom glow library replacing `LibCustomGlow`. Supports **Pixel**, **Medium**, **Autocast**, **Classic**, **Thin**, **Thick** glow types with full per-type configuration (color, frequency, thickness, line count, particles, scale, and direction). +- **StrataEngine:** Centralized Z-index layering engine for root-level UI containers. Supports `BumpUp`/`BumpDown` reordering persisted to profile, ensuring consistent frame stacking across Edit Mode and Canvas Mode. `This is for future implementation and not currently active, but all strata has been updated to use the new engine.` +- **Group Frame Dispel Glow Type:** Added user-selectable glow types (**Pixel** vs **Autocast**) for dispel indicators on group frames, with per-type settings for thickness, line count, and length. + +### Changed +- **Glow System Overhaul:** Migrated all glow rendering (Pandemic, Cooldown Manager, Action Bars, Dispel Indicators) from `LibCustomGlow` to the new `LibOrbitGlow-1.0`. +- **Strata Standardisation:** Replaced all hardcoded strata strings (`"MEDIUM"`, `"TOOLTIP"`, etc.) with `Constants.Strata.*` lookups managed by the StrataEngine. +- **Glow Settings Schema:** Expanded glow configuration panel with per-type sliders (frequency, thickness, particles, scale, line count) that dynamically show/hide based on the selected glow type. +- **Cast Bar Spark:** Player cast bar spark now uses `Pixel:Snap` for sub-pixel–accurate positioning. +- **Unit Health:**: Unit Health now has a checkbox to enable gradient colors instead of colors based on value. +- **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 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. +- **CDM:**: Dropping items/spells on the Cooldown Manager frames should be a bit less buggy now. + +### Removed +- **Menu Items (CombatTimer & Performance):** Removed standalone `CombatTimer.lua` and `Performance.lua` from Menu Items—functionality replaced by the Datatexts plugin. + +### Thanks for your support again! Next update will be focussed on fixing bugs. Hope everyone had a good easter. (Go Echo!) ## [1.0.0] - 2026-03-10 ### Added diff --git a/Orbit/Core/CanvasMode/ComponentSettings.lua b/Orbit/Core/CanvasMode/ComponentSettings.lua index ffa742c..131b2b0 100644 --- a/Orbit/Core/CanvasMode/ComponentSettings.lua +++ b/Orbit/Core/CanvasMode/ComponentSettings.lua @@ -356,6 +356,8 @@ 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. + 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 565ed14..2532dc0 100644 --- a/Orbit/Core/CanvasMode/Creators/IconFrameCreator.lua +++ b/Orbit/Core/CanvasMode/Creators/IconFrameCreator.lua @@ -116,12 +116,25 @@ local function Create(container, preview, key, source, data) end local attached = 0 + 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("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 + local regions = { container.iconVisual:GetRegions() } -- skull anchor: last-created texture + skullTexture = regions[#regions] break end end @@ -131,8 +144,14 @@ 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 + 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) + 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/Core/EditMode/Frame/Guard.lua b/Orbit/Core/EditMode/Frame/Guard.lua index 229740a..e3d6c5d 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,18 @@ function Guard:Protect(frame, parent) end end +-- [ SUSPEND / RESUME ]------------------------------------------------------------------------------ +-- 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 +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 95dcf91..51e45e4 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) @@ -566,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) @@ -714,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 10cc210..490a658 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) @@ -380,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, ...) @@ -394,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 @@ -411,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 @@ -429,3 +419,46 @@ function Plugin:CaptureBlizzardMinimap() self._captured = true end + +-- [ FARMHUD COMPATIBILITY ]------------------------------------------------------------------------- +-- Suspend FrameGuard and surface-sync while FarmHud owns the Minimap. See readme for details. + +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 + + FarmHud:RegisterForeignAddOnObject(self.frame, "Orbit") + + -- 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 3adea8f..f2cf6c1 100644 --- a/Orbit/Plugins/Minimap/MinimapCompartment.lua +++ b/Orbit/Plugins/Minimap/MinimapCompartment.lua @@ -1,5 +1,4 @@ --- Minimap Addon Compartment --- Collects all LibDBIcon minimap buttons + legacy minimap children into a hover-reveal drawer. +-- Minimap Addon Compartment — proxy-icon flyout for LibDBIcon + legacy minimap buttons. ---@type Orbit local Orbit = Orbit @@ -10,26 +9,32 @@ 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) - --- 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. +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 Minimap children that must never be collected into the compartment. local BLIZZARD_MINIMAP_CHILDREN = { ["MinimapBackdrop"] = true, ["MinimapCompassTexture"] = true, ["OrbitMinimapCompartmentButton"] = true, ["OrbitMinimapCompartmentFlyout"] = true, + ["OrbitMinimapButtonHolder"] = true, ["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", @@ -48,17 +53,36 @@ 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 +-- Store reference to the real SetParent method before we override it on collected buttons +local FrameSetParent = UIParent.SetParent + -- [ COMPARTMENT BUTTON ]---------------------------------------------------------------------------- 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) @@ -66,32 +90,22 @@ 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") 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) - btn.iconPushed = btn:CreateTexture(nil, "ARTWORK") - btn.iconPushed:SetAllPoints(btn) - btn.iconPushed:SetAtlas("Map-Filter-Button-down", false) - btn.iconPushed:Hide() + -- Setup visual for canvas mode + btn.visual = btn.icon - 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) + 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) @@ -110,6 +124,20 @@ function Plugin:CreateCompartmentButton() self._compartmentButton = btn end +-- [ HIDDEN BUTTON HOLDER ]-------------------------------------------------------------------------- + +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", HOLDER_OFFSCREEN, -HOLDER_OFFSCREEN) + holder:Hide() + + self._buttonHolder = holder + return holder +end + -- [ COMPARTMENT FLYOUT ]---------------------------------------------------------------------------- function Plugin:CreateCompartmentFlyout() @@ -130,20 +158,33 @@ 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("AnyUp") - overlay:SetScript("OnClick", function() flyout:Hide() end) - flyout._clickOverlay = overlay + -- 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 - flyout:SetScript("OnShow", function(f) f._clickOverlay:Show() end) - flyout:SetScript("OnHide", function(f) f._clickOverlay:Hide() end) + local mouseOverFlyout = f:IsMouseOver() + local mouseOverBtn = self._compartmentButton and self._compartmentButton:IsMouseOver() + -- Use our container (not Minimap surface) — FarmHud reparents Minimap away from our frame. + local mouseOverMinimap = self.frame:IsMouseOver() + + 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 + else + outsideTimer = outsideTimer + elapsed + if outsideTimer >= FLYOUT_CLOSE_DELAY then + outsideTimer = 0 + self:HideCompartmentFlyout() + end + end + end) + + flyout:SetScript("OnShow", function() outsideTimer = 0 end) + flyout:SetScript("OnHide", function() outsideTimer = 0 end) - flyout.rows = {} self._compartmentFlyout = flyout end @@ -154,178 +195,251 @@ function Plugin:ToggleCompartmentFlyout() local flyout = self._compartmentFlyout if flyout:IsShown() then - flyout:Hide() + self:HideCompartmentFlyout() return end - self:PopulateCompartmentFlyout() - flyout:Show() + self:ShowCompartmentFlyout() end -function Plugin:PopulateCompartmentFlyout() - 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 +function Plugin:HideCompartmentFlyout() + if self._compartmentFlyout then + self._compartmentFlyout:Hide() + end +end - -- Hide existing rows - for _, row in ipairs(flyout.rows) do - row:Hide() +function Plugin:ShowCompartmentFlyout() + if not self._compartmentFlyout then + self:CreateCompartmentFlyout() end + local flyout = self._compartmentFlyout - local collected = self._collectedButtons or {} - if #collected == 0 then - flyout:SetSize(140, 30) - flyout:ClearAllPoints() - flyout:SetPoint("BOTTOMRIGHT", anchor, "TOPRIGHT", 0, 2) - if not flyout._emptyText then - flyout._emptyText = flyout:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall") - flyout._emptyText:SetPoint("CENTER") - flyout._emptyText:SetText("No addon buttons found") + self:LayoutButtonsInFlyout() + flyout:Show() +end + +-- [ FLYOUT LAYOUT ]--------------------------------------------------------------------------------- + +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 - flyout._emptyText:Show() - return + 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 - if flyout._emptyText then flyout._emptyText:Hide() 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 - 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("AnyUp") - 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 + -- 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) - -- 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) + -- 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 - row.icon:SetTexCoord(0, 1, 0, 1) - row.icon:Show() + GameTooltip:Show() else - row.icon:Hide() + 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) - -- Label - local displayName = entry.name or "Unknown" - row.label:SetText(displayName) - Orbit.Skin:SkinText(row.label, { font = Orbit.db.GlobalSettings.Font, textSize = 11 }) + proxyButtonPool[originalBtn] = proxy + return proxy +end - local textWidth = row.label:GetStringWidth() - if textWidth > maxTextWidth then - maxTextWidth = textWidth - end +function Plugin:LayoutButtonsInFlyout() + local flyout = self._compartmentFlyout + if not flyout then return end - -- Click handler: trigger the original addon button's OnClick - -- RegisterForClicks("AnyUp") 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 - 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() + 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 + 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) + -- Hide all existing proxy buttons first + for _, proxy in pairs(proxyButtonPool) do + proxy:Hide() + 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) + if #visibleEntries == 0 then + flyout:SetSize(EMPTY_FLYOUT_W, EMPTY_FLYOUT_H) + flyout:ClearAllPoints() + 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") + flyout._emptyText:SetText("No addon buttons found") end - row:Show() + flyout._emptyText:Show() + return end + if flyout._emptyText then flyout._emptyText:Hide() 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() - 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 = width * scale - local flyH = height * scale - local gap = 2 - - -- 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) - - -- Horizontal: prefer aligning to the right edge; fall back to left alignment if it would clip - local expandLeft = (btnRight + flyW) > screenW + 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) + + -- Create/reuse proxy buttons and position them in a grid + for i, entry in ipairs(visibleEntries) do + 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)) + + proxy:ClearAllPoints() + proxy:SetPoint("TOPLEFT", flyout, "TOPLEFT", xOff, yOff) + proxy:Show() + end + end + + -- 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 + + 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 + + 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) + flyout:SetPoint("TOPRIGHT", mmFrame, "BOTTOMRIGHT", 0, -FLYOUT_GAP) end end @@ -342,6 +456,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 +464,18 @@ function Plugin:CollectAddonButtons() local ownButtonName = "Orbit" for name, button in pairs(lib.objects) do if name ~= ownButtonName then - collected[#collected + 1] = { - name = name, - button = button, - icon = button.dataObject and button.dataObject.icon or nil, - source = "libdbicon", - } + 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 = displayName, + button = button, + icon = icon, + source = "libdbicon", + } + end + if signature then seenSignatures[signature] = true end seen[button] = true end end @@ -393,18 +514,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 @@ -415,48 +535,97 @@ function Plugin:CollectAddonButtons() table.sort(collected, function(a, b) return (a.name or "") < (b.name or "") end) end -function Plugin:HideCollectedButtons() +-- [ GRAB / RELEASE BUTTONS ]------------------------------------------------------------------------ + +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: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 - hooksecurefunc(entry.button, "Show", function(b) - if self._compartmentActive 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._orbitOnShowHooked = 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._orbitSetShownHooked then - hooksecurefunc(entry.button, "SetShown", function(b, shown) - if shown and self._compartmentActive then - b:Hide() - end - end) - entry.button._orbitSetShownHooked = true + + -- Reparent to hidden holder via raw SetParent (ours is overridden with doNothing) + FrameSetParent(button, holder) + button:SetFrameStrata(holder:GetFrameStrata()) + + -- Block addons from repositioning their buttons back. + 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 + 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 - 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._collectedButtons = nil end @@ -467,29 +636,29 @@ function Plugin:ApplyAddonCompartment() local useClickAction = self:UsesAddonClickAction() if useClickAction or not self:IsComponentDisabled("Compartment") then + -- Release then re-collect to purge stale state each cycle. + self._compartmentActive = false + self:ReleaseCollectedButtons() 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:RestoreCollectedButtons() 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 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) @@ -513,9 +682,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) @@ -523,8 +692,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 diff --git a/Orbit/Plugins/Minimap/readme.md b/Orbit/Plugins/Minimap/readme.md index 3a2a94f..30152cb 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 @@ -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.