From 0e1c3385e444ca69916b628eab563ecb561fecb7 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Mon, 31 Aug 2026 10:11:49 +0200 Subject: [PATCH] Add and optimize requested TokensWorth modules Merge the audited TokensWorth-derived modules and ClassicAPI-focused optimizations into the default branch. --- AUDIT.md | 215 ++++++++++++++++++ README.md | 20 +- ShaguTweaks-extras.toc | 10 + THIRD_PARTY_NOTICES.md | 40 ++++ mods/actionbar-hide-macro.lua | 34 +++ mods/actionbar-mouseover-bar-right.lua | 15 ++ mods/actionbar-mouseover-bar-right2.lua | 15 ++ mods/actionbar-mouseover-common.lua | 138 ++++++++++++ mods/cursor-tooltip.lua | 96 ++++++++ mods/hide-combat-tooltip.lua | 146 ++++++++++++ mods/move-unitframes-extended.lua | 283 ++++++++++++++++++++++++ mods/unitframes-abbrev-names.lua | 140 ++++++++++++ 12 files changed, 1151 insertions(+), 1 deletion(-) create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 mods/actionbar-hide-macro.lua create mode 100644 mods/actionbar-mouseover-bar-right.lua create mode 100644 mods/actionbar-mouseover-bar-right2.lua create mode 100644 mods/actionbar-mouseover-common.lua create mode 100644 mods/cursor-tooltip.lua create mode 100644 mods/hide-combat-tooltip.lua create mode 100644 mods/move-unitframes-extended.lua create mode 100644 mods/unitframes-abbrev-names.lua diff --git a/AUDIT.md b/AUDIT.md index b653653..ffba6c2 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -670,3 +670,218 @@ Static audit is complete. Before merging to the stable branch, test: Do not merge this audit branch solely on static analysis. Treat the branch as a test candidate until the in-game matrix has passed without Lua errors or UI regressions. + + +## TokensWorth requested modules — test branch audit + +Branch: `tokensworth-mods-classicapi-test` + +Seven requested modules were added for runtime testing. **Improved Roll Frames was +not duplicated** because an optimized version already exists in +ShaguTweaks-ClassicAPI. + +### Mouseover Right / Mouseover Right 2 + +The original implementation created one independent mouse-catching overlay for +every action button plus the bar itself. The test version uses one invisible +reveal area per bar and one 100 ms controller that only runs while the bar is +visible. When hidden, there is no polling. + +The helper preserves existing bar OnShow/OnHide scripts and tracks the native +`SHOW_MULTI_ACTIONBAR_3` / `SHOW_MULTI_ACTIONBAR_4` state through +`CVAR_UPDATE`. + +### Hide Macro Text + +One-time UI update only. No ClassicAPI replacement is useful for this FrameXML +operation and no periodic work is installed. + +### Unit Frame Abbreviated Names + +The original target-of-target implementation recalculated the name on every +rendered frame. The test version uses ClassicAPI event validation and +`UNIT_TARGET` when available. Only environments without that event use a +250 ms fallback ticker. + +### Movable Unit Frames Extended + +The permanent per-frame Ctrl+Shift check was removed. The module uses +`ShaguTweaks.API.IsShiftKeyDown`, `API.IsControlKeyDown` and +`MODIFIER_STATE_CHANGED` when ClassicAPI exposes modifier-state events. + +The grid is created lazily on the first unlock. Positions are stored in the +existing `ShaguTweaks_config["MoveUnitframesExtended"]` table. + +For the requested Turtle layout, the debuff anchor is **BuffButton32** instead +of the upstream BuffButton16. + +### Cursor Tooltip + +The original global `GameTooltip_SetDefaultAnchor` replacement was removed. +The test version keeps the native function intact through the ShaguTweaks safe +post-hook helper. Cursor tracking runs only while a default-anchored tooltip is +shown. + +### Hide Combat Tooltip + +The original combat-long per-frame Shift polling was removed. ClassicAPI's +modifier event updates the tooltip only when modifier state actually changes. +A 50 ms combat-only fallback exists for environments without that event. + +The GameTooltip Show method is post-hooked so tooltips opened after entering +combat immediately inherit the correct hidden/Shift-visible state. + +### Runtime validation required + +Before merging this branch, test: + +- login and `/reload` with the seven new modules disabled +- each module individually enabled +- both Mouseover Right modules together +- toggle the two right actionbars in Interface Options while mouseover modules are enabled +- stance/bonus actionbar changes with Hide Macro Text enabled +- long NPC target names and target-of-target changes +- Ctrl+Shift dragging of party frames, minimap, BuffButton0, BuffButton32 and TempEnchant1 +- relog/reload persistence of moved positions +- Cursor Tooltip alone, Hide Combat Tooltip alone, and both enabled together +- combat tooltip Shift reveal/re-hide behavior +- interaction with DragonflightUI-Reforged's injected ShaguTweaks Extras list +- Lua errors and visible FPS regressions + +Static code review is complete; this branch remains a runtime test candidate. + + +### Cursor Tooltip — focused audit / optimization + +A focused audit compared the test implementation with the TokensWorth upstream +module. + +Findings: + +- the feature genuinely needs frame-by-frame cursor coordinates while the + tooltip is visible; there is no ClassicAPI event that can replace pointer + tracking +- the previous test implementation showed its cursor tracker as soon as + `GameTooltip_SetDefaultAnchor` ran, even though default anchoring normally + happens before `GameTooltip:Show()` +- `ClearAllPoints()` on the cursor tracker every rendered frame was + unnecessary +- moving the tracker while the mouse coordinates were unchanged caused + avoidable layout work +- replacing `GameTooltip_SetDefaultAnchor` for every possible tooltip caller + could affect non-`GameTooltip` frames unnecessarily + +The optimized test version now: + +- starts its `OnUpdate` work only while the actual `GameTooltip` is shown +- performs one immediate cursor-position update before Show to avoid a first + frame jump +- skips `SetPoint` when cursor coordinates and UI scale have not changed +- reuses the same CENTER anchor without per-frame `ClearAllPoints` +- keeps the original default-anchor helper for non-`GameTooltip` callers +- keeps `SetClampedToScreen(true)` so the cursor tooltip remains inside the + visible UI area +- retains the intentional default-anchor replacement because a post-hook alone + was not reliable on Vanilla/Turtle layout code + +No ClassicAPI-specific replacement is useful here. `GetCursorPosition`, +tooltip ownership and frame anchoring are native UI operations. The optimized +design therefore keeps the unavoidable cursor-following `OnUpdate`, but only +for the time where that work is actually needed. + + +## Remaining TokensWorth modules — focused audit + +The remaining requested TokensWorth-derived modules were reviewed against their +upstream implementations and the current ClassicAPI/ShaguTweaks architecture. + +### Mouseover Right / Mouseover Right 2 — optimized + +Both options continue to use one shared helper instead of duplicating the +upstream implementation. + +Upstream creates one mouse-catching overlay for each of the 12 buttons plus an +additional bar overlay for each actionbar. The ClassicAPI test version keeps one +hotspot and one controller per bar. + +The focused audit additionally: + +- caches the native actionbar visibility flag from `CVAR_UPDATE` / + `PLAYER_ENTERING_WORLD` instead of looking it up on every watcher tick +- replaces repeated `GetTime()` deadline checks with a simple accumulated + two-second idle timer +- removes a duplicate watcher restart when the hidden bar is revealed +- leaves the watcher disabled while the bar is hidden +- preserves any pre-existing bar `OnShow` / `OnHide` scripts + +The remaining 100 ms watcher only exists while the relevant actionbar is +actually visible and waiting to auto-hide. No ClassicAPI API can replace the +required mouse-over state check. + +### Hide Macro Text — retained as-is + +No hot-path issue was found. + +The module performs a single pass over the native action-button FontStrings and +sets their alpha to zero. It installs no event handlers, no hooks and no +`OnUpdate`. + +Using ClassicAPI would add abstraction without benefit because this is a +one-time FrameXML presentation change. The current implementation is already +effectively zero-cost after enable. + +### Unit Frame Abbreviated Names — further optimized + +The upstream module recalculates target-of-target text on every rendered frame. + +The ClassicAPI version already prefers validated `UNIT_TARGET` and +`UNIT_NAME_UPDATE` events, with a 250 ms fallback only where `UNIT_TARGET` +is unavailable. + +The focused audit additionally: + +- caches the raw unit name and abbreviated result +- skips repeated abbreviation/string work while the underlying unit name is + unchanged +- skips `FontString:SetText` when the displayed text is already correct +- limits the legacy fallback to cases where `targettarget` actually exists + +### Movable Unit Frames Extended — hardened + +The upstream module polls Ctrl+Shift every rendered frame. The ClassicAPI +version uses `MODIFIER_STATE_CHANGED` plus the shared modifier helpers, with a +100 ms fallback only for environments without ClassicAPI modifier events. + +The focused audit found a correctness issue in the first test conversion: +merely pressing and releasing Ctrl+Shift saved absolute positions for every +managed frame, even if the user had not dragged them. On later logins this +could turn untouched Blizzard-managed frames into explicit absolute-position +frames. + +The hardened version now: + +- saves a position only after that specific frame was actually dragged +- preserves and restores the original drag scripts +- preserves and restores the original mouse-enabled state +- preserves the original movable state +- restores the original user-placed state for untouched frames +- only marks a frame user-placed when a real drag starts +- keeps the grid lazily created on first unlock +- retains the requested Turtle WoW `BuffButton32` debuff anchor + +### Current status + +The focused static audit found no additional change worth making to +`actionbar-mouseover-bar-right.lua`, +`actionbar-mouseover-bar-right2.lua` or `actionbar-hide-macro.lua` beyond +their shared/helper behavior. + +Runtime validation is still required before merge, especially for: + +- enabling/disabling the two native right actionbars while mouseover hiding is + active +- repeated reveal/hide cycles with action buttons clicked normally +- abbreviated target-of-target names during rapid target switching +- Ctrl+Shift without dragging anything, followed by relog/reload +- dragging each supported frame individually, followed by relog/reload +- default party/buff/minimap layout remaining unchanged for frames never moved diff --git a/README.md b/README.md index c5a9467..e95fe2d 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ Settings: **Esc → Advanced Options**. - Buy Em All uses ClassicAPI merchant, item count and bag-family data, including specialty bags. - Added modifier-aware Key-Down Casting with Shift/Ctrl/Alt binding support and an independent Alt Self-Cast option. - Added Metric Range for metre labels in range tooltips without changing numeric values. +- Added optimized modules adapted from TokensWorth/ShaguTweaks-mods: Mouseover Right bars, Hide Macro Text, Unit Frame Abbreviated Names, Movable Unit Frames Extended, Cursor Tooltip and Hide Combat Tooltip. +- Movable Unit Frames Extended and Hide Combat Tooltip use ClassicAPI modifier-state events instead of permanent per-frame modifier polling. - Various stability fixes across legacy ShaguTweaks Extras modules. ## 🔷 Mods using ClassicAPI integration @@ -55,12 +57,15 @@ Settings: **Esc → Advanced Options**. - Alt Self-Cast - Macro Icons - Macro Tweaks +- Movable Unit Frames Extended - Raid Frames - Reagent Counter - Reveal World Map - Show Dispel Indicators - Show Bags - Show Micro Menu +- Hide Combat Tooltip +- Unit Frame Abbreviated Names Other modules can also benefit indirectly from ClassicAPI through shared ShaguTweaks libraries and helpers. @@ -76,6 +81,9 @@ Other modules can also benefit indirectly from ClassicAPI through shared ShaguTw - Show Micro Menu - Key-Down Casting - Alt Self-Cast +- Mouseover Right +- Mouseover Right 2 +- Hide Macro Text ### Bags & Inventory @@ -90,6 +98,13 @@ Other modules can also benefit indirectly from ClassicAPI through shared ShaguTw ### Tooltip & Items - Metric Range +- Cursor Tooltip +- Hide Combat Tooltip + +### Unit Frames + +- Unit Frame Abbreviated Names +- Movable Unit Frames Extended ### World Map @@ -127,10 +142,11 @@ Macro Tweaks adds convenient ClassicAPI-backed aliases: ## 🧹 Removed duplicates -The following original Extras modules are not included because improved versions already exist in ShaguTweaks-ClassicAPI: +The following modules are not included because improved versions already exist in ShaguTweaks-ClassicAPI: - **Chat History** → integrated into **Chat Tweaks** - **Show Energy Ticks** → integrated as **Energy & Mana Tick** +- **Improved Roll Frames** → already integrated and optimized in **ShaguTweaks-ClassicAPI** ## 🔧 Compatibility @@ -147,4 +163,6 @@ Additional maintenance and Turtle WoW work by **paokkerkir**. ClassicAPI compatibility fork maintained by **Dusk-92**. +Additional requested modules adapted from **TokensWorth/ShaguTweaks-mods**, originally released under MIT by **GryllsAddons**. See `THIRD_PARTY_NOTICES.md`. + Released under the original **MIT License**. diff --git a/ShaguTweaks-extras.toc b/ShaguTweaks-extras.toc index b59a851..f152fb9 100644 --- a/ShaguTweaks-extras.toc +++ b/ShaguTweaks-extras.toc @@ -26,6 +26,10 @@ mods\actionbar-float.lua mods\classic-snowfall.lua mods\reduced-actionbar-bags.lua mods\reduced-actionbar-micromenu.lua +mods\actionbar-mouseover-common.lua +mods\actionbar-mouseover-bar-right.lua +mods\actionbar-mouseover-bar-right2.lua +mods\actionbar-hide-macro.lua # bags & inventory mods\bag-item-click.lua @@ -37,6 +41,12 @@ mods\buy-em-all.lua # tooltip & items mods\metric-range.lua +mods\cursor-tooltip.lua +mods\hide-combat-tooltip.lua + +# unit frames +mods\unitframes-abbrev-names.lua +mods\move-unitframes-extended.lua # world map mods\worldmap-reveal.lua diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..065361d --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,40 @@ +# Third-Party Notices + +## TokensWorth/ShaguTweaks-mods + +The following modules in this fork contain code adapted from +`TokensWorth/ShaguTweaks-mods`: + +- `mods/actionbar-mouseover-common.lua` +- `mods/actionbar-mouseover-bar-right.lua` +- `mods/actionbar-mouseover-bar-right2.lua` +- `mods/actionbar-hide-macro.lua` +- `mods/unitframes-abbrev-names.lua` +- `mods/move-unitframes-extended.lua` +- `mods/cursor-tooltip.lua` +- `mods/hide-combat-tooltip.lua` + +Source: +https://github.com/TokensWorth/ShaguTweaks-mods + +MIT License + +Copyright (c) 2022 GryllsAddons + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mods/actionbar-hide-macro.lua b/mods/actionbar-hide-macro.lua new file mode 100644 index 0000000..b2894f0 --- /dev/null +++ b/mods/actionbar-hide-macro.lua @@ -0,0 +1,34 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T + +local module = ShaguTweaks:register({ + title = T["Hide Macro Text"], + description = T["Hides macro names on action buttons."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Action Bar"], + enabled = nil, +}) + +module.enable = function(self) + local function HideMacroText(button) + if not button or not button.GetName then return end + + local name = button:GetName() + local text = name and _G[name .. "Name"] + if text then text:SetAlpha(0) end + end + + for i = 1, 24 do + HideMacroText(_G["BonusActionButton" .. i]) + end + + for i = 1, 12 do + HideMacroText(_G["ActionButton" .. i]) + HideMacroText(_G["MultiBarRightButton" .. i]) + HideMacroText(_G["MultiBarLeftButton" .. i]) + HideMacroText(_G["MultiBarBottomLeftButton" .. i]) + HideMacroText(_G["MultiBarBottomRightButton" .. i]) + end +end diff --git a/mods/actionbar-mouseover-bar-right.lua b/mods/actionbar-mouseover-bar-right.lua new file mode 100644 index 0000000..1d40a32 --- /dev/null +++ b/mods/actionbar-mouseover-bar-right.lua @@ -0,0 +1,15 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local T = ShaguTweaks.T + +local module = ShaguTweaks:register({ + title = T["Mouseover Right"], + description = T["Hide the right actionbar and show it on mouseover."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Action Bar"], + enabled = nil, +}) + +module.enable = function(self) + ShaguTweaks.CreateMouseoverActionBar(MultiBarRight, "SHOW_MULTI_ACTIONBAR_3") +end diff --git a/mods/actionbar-mouseover-bar-right2.lua b/mods/actionbar-mouseover-bar-right2.lua new file mode 100644 index 0000000..e345835 --- /dev/null +++ b/mods/actionbar-mouseover-bar-right2.lua @@ -0,0 +1,15 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local T = ShaguTweaks.T + +local module = ShaguTweaks:register({ + title = T["Mouseover Right 2"], + description = T["Hide the second right actionbar and show it on mouseover."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Action Bar"], + enabled = nil, +}) + +module.enable = function(self) + ShaguTweaks.CreateMouseoverActionBar(MultiBarLeft, "SHOW_MULTI_ACTIONBAR_4") +end diff --git a/mods/actionbar-mouseover-common.lua b/mods/actionbar-mouseover-common.lua new file mode 100644 index 0000000..72d8c71 --- /dev/null +++ b/mods/actionbar-mouseover-common.lua @@ -0,0 +1,138 @@ +-- Shared mouseover actionbar helper. +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local _G = ShaguTweaks.GetGlobalEnv() + +if not ShaguTweaks.CreateMouseoverActionBar then + ShaguTweaks.CreateMouseoverActionBar = function(bar, visibilityFlag) + if not bar or not visibilityFlag then return end + if bar.ShaguTweaksMouseoverController then return end + + local controller = CreateFrame("Frame", nil, UIParent) + local hotspot = CreateFrame("Frame", nil, UIParent) + + controller.bar = bar + controller.hotspot = hotspot + controller.visibilityFlag = visibilityFlag + controller.enabled = false + controller.elapsed = 0 + controller.idle = 0 + + bar.ShaguTweaksMouseoverController = controller + + -- One invisible reveal area replaces the original per-button overlay + -- frames. It only accepts mouse input while the actionbar is hidden. + hotspot:SetAllPoints(bar) + hotspot:SetFrameStrata("DIALOG") + hotspot:EnableMouse(false) + hotspot:Hide() + + local function ReadEnabled() + return _G[visibilityFlag] and true or false + end + + local function StopWatching() + controller:SetScript("OnUpdate", nil) + controller.elapsed = 0 + controller.idle = 0 + end + + local function StartWatching() + if not controller.enabled or not bar:IsShown() then return end + + hotspot:EnableMouse(false) + controller.elapsed = 0 + controller.idle = 0 + + controller:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + (arg1 or 0) + if this.elapsed < .10 then return end + + local step = this.elapsed + this.elapsed = 0 + + if not this.enabled then + StopWatching() + hotspot:EnableMouse(false) + hotspot:Hide() + return + end + + if not bar:IsShown() then + StopWatching() + hotspot:Show() + hotspot:EnableMouse(true) + return + end + + if MouseIsOver(bar) then + this.idle = 0 + return + end + + this.idle = this.idle + step + if this.idle >= 2 then + StopWatching() + bar:Hide() + end + end) + end + + local function Sync() + controller.enabled = ReadEnabled() + + if not controller.enabled then + StopWatching() + hotspot:EnableMouse(false) + hotspot:Hide() + return + end + + hotspot:Show() + + if bar:IsShown() then + StartWatching() + else + hotspot:EnableMouse(true) + end + end + + hotspot:SetScript("OnEnter", function() + if not controller.enabled then return end + + hotspot:EnableMouse(false) + bar:Show() + -- The bar's OnShow script starts the watcher. Avoid a second redundant + -- StartWatching() call here. + end) + + -- Keep existing scripts intact while observing native/UI-addon visibility + -- changes. These wrappers are installed once per bar. + local oldOnShow = bar:GetScript("OnShow") + bar:SetScript("OnShow", function() + if oldOnShow then oldOnShow() end + if controller.enabled then StartWatching() end + end) + + local oldOnHide = bar:GetScript("OnHide") + bar:SetScript("OnHide", function() + if oldOnHide then oldOnHide() end + + StopWatching() + + if controller.enabled then + hotspot:Show() + hotspot:EnableMouse(true) + else + hotspot:EnableMouse(false) + hotspot:Hide() + end + end) + + controller:RegisterEvent("PLAYER_ENTERING_WORLD") + controller:RegisterEvent("CVAR_UPDATE") + controller:SetScript("OnEvent", Sync) + + Sync() + end +end diff --git a/mods/cursor-tooltip.lua b/mods/cursor-tooltip.lua new file mode 100644 index 0000000..9a122b1 --- /dev/null +++ b/mods/cursor-tooltip.lua @@ -0,0 +1,96 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local hooksecurefunc = ShaguTweaks.hooksecurefunc + +local module = ShaguTweaks:register({ + title = T["Cursor Tooltip"], + description = T["Attaches default tooltips to the cursor."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Tooltip & Items"], + enabled = nil, +}) + +module.enable = function(self) + if ShaguTweaks.CursorTooltipInstalled then return end + ShaguTweaks.CursorTooltipInstalled = true + + -- Keep the native helper for non-GameTooltip callers. The feature only + -- needs to move the normal world/UI GameTooltip and should not unexpectedly + -- change other tooltip frames that may reuse the same helper. + local originalDefaultAnchor = ShaguTweaks.CursorTooltipOriginalDefaultAnchor + or _G.GameTooltip_SetDefaultAnchor + + ShaguTweaks.CursorTooltipOriginalDefaultAnchor = originalDefaultAnchor + + local cursor = CreateFrame("Frame", nil, UIParent) + cursor:SetWidth(1) + cursor:SetHeight(1) + cursor:Hide() + + local following = false + local lastX, lastY, lastScale + + local function UpdateCursor(force) + local scale = UIParent:GetEffectiveScale() + if not scale or scale == 0 then scale = UIParent:GetScale() end + if not scale or scale == 0 then scale = 1 end + + local x, y = GetCursorPosition() + + -- GetCursorPosition is cheap and required for following the pointer, but + -- avoid a frame-layout operation while the mouse is completely still. + if not force and x == lastX and y == lastY and scale == lastScale then + return + end + + lastX, lastY, lastScale = x, y, scale + + -- Reusing the same CENTER anchor updates it in place; ClearAllPoints every + -- rendered frame is unnecessary. + cursor:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x / scale, y / scale) + end + + cursor:SetScript("OnUpdate", function() + if not following or not GameTooltip:IsShown() then + this:Hide() + return + end + + UpdateCursor(false) + end) + + function _G.GameTooltip_SetDefaultAnchor(tooltip, parent) + if tooltip ~= GameTooltip then + return originalDefaultAnchor(tooltip, parent) + end + + tooltip:SetOwner(parent or UIParent, "ANCHOR_NONE") + tooltip:ClearAllPoints() + + -- Position once before Show() so there is no first-frame jump from the + -- cursor frame's default origin. + UpdateCursor(true) + + tooltip:SetPoint("BOTTOMLEFT", cursor, "TOPRIGHT", 12, 12) + tooltip:SetClampedToScreen(true) + tooltip.default = 1 + + following = true + end + + -- Default anchoring normally happens before GameTooltip:Show(). Start the + -- per-frame cursor tracker only once the tooltip is actually visible. + hooksecurefunc(GameTooltip, "Show", function() + if not following then return end + + UpdateCursor(true) + cursor:Show() + end) + + hooksecurefunc(GameTooltip, "Hide", function() + following = false + cursor:Hide() + end) +end diff --git a/mods/hide-combat-tooltip.lua b/mods/hide-combat-tooltip.lua new file mode 100644 index 0000000..7c4242a --- /dev/null +++ b/mods/hide-combat-tooltip.lua @@ -0,0 +1,146 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local T = ShaguTweaks.T +local API = ShaguTweaks.API + +local module = ShaguTweaks:register({ + title = T["Hide Combat Tooltip"], + description = T["Hides the tooltip in combat. Hold Shift to show it temporarily."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Tooltip & Items"], + enabled = nil, +}) + +module.enable = function(self) + if ShaguTweaks.HideCombatTooltipInstalled then return end + ShaguTweaks.HideCombatTooltipInstalled = true + + local controller = CreateFrame("Frame", nil, UIParent) + local inCombat = UnitAffectingCombat("player") and true or false + local shiftDown = API.IsShiftKeyDown() and true or false + local guardActive = false + local elapsed = 0 + + local function Apply() + if not inCombat then + if GameTooltip:GetAlpha() ~= 1 then + GameTooltip:SetAlpha(1) + end + return + end + + local alpha = shiftDown and 1 or 0 + if GameTooltip:GetAlpha() ~= alpha then + GameTooltip:SetAlpha(alpha) + end + end + + local function GuardTick() + elapsed = elapsed + (arg1 or 0) + if elapsed < .05 then return end + elapsed = 0 + + -- ClassicAPI provides modifier events, so normal clients don't need to + -- query Shift here. This fallback is only for old environments. + if not API.modifierstate then + shiftDown = API.IsShiftKeyDown() and true or false + end + + Apply() + end + + local function StartGuard() + if guardActive or not inCombat or not GameTooltip:IsShown() then return end + + guardActive = true + elapsed = 0 + controller:SetScript("OnUpdate", GuardTick) + end + + local function StopGuard() + if not guardActive then return end + + guardActive = false + elapsed = 0 + controller:SetScript("OnUpdate", nil) + end + + local function RefreshShift() + shiftDown = API.IsShiftKeyDown() and true or false + + if inCombat and GameTooltip:IsShown() then + Apply() + StartGuard() + end + end + + local function EnterCombat() + inCombat = true + shiftDown = API.IsShiftKeyDown() and true or false + + if GameTooltip:IsShown() then + Apply() + StartGuard() + end + end + + local function LeaveCombat() + inCombat = false + StopGuard() + Apply() + end + + -- Vanilla/Turtle can restore GameTooltip alpha internally while updating a + -- visible unit tooltip. A purely event-driven implementation therefore does + -- not stay hidden reliably. The guard below exists only while BOTH combat + -- and an actual GameTooltip are active, and is throttled to 20 Hz instead of + -- running the full upstream logic every rendered frame. + local oldOnShow = GameTooltip:GetScript("OnShow") + GameTooltip:SetScript("OnShow", function() + if oldOnShow then oldOnShow() end + + if inCombat then + shiftDown = API.IsShiftKeyDown() and true or false + Apply() + StartGuard() + else + Apply() + end + end) + + local oldOnHide = GameTooltip:GetScript("OnHide") + GameTooltip:SetScript("OnHide", function() + if oldOnHide then oldOnHide() end + StopGuard() + end) + + controller:RegisterEvent("PLAYER_ENTERING_WORLD") + controller:RegisterEvent("PLAYER_REGEN_DISABLED") + controller:RegisterEvent("PLAYER_REGEN_ENABLED") + + if API.modifierstate then + controller:RegisterEvent("MODIFIER_STATE_CHANGED") + end + + controller:SetScript("OnEvent", function() + if event == "PLAYER_REGEN_DISABLED" then + EnterCombat() + elseif event == "PLAYER_REGEN_ENABLED" then + LeaveCombat() + elseif event == "MODIFIER_STATE_CHANGED" then + RefreshShift() + elseif event == "PLAYER_ENTERING_WORLD" then + if UnitAffectingCombat("player") then + EnterCombat() + else + LeaveCombat() + end + end + end) + + if inCombat then + EnterCombat() + else + LeaveCombat() + end +end diff --git a/mods/move-unitframes-extended.lua b/mods/move-unitframes-extended.lua new file mode 100644 index 0000000..e6f9d17 --- /dev/null +++ b/mods/move-unitframes-extended.lua @@ -0,0 +1,283 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API + +local module = ShaguTweaks:register({ + title = T["Movable Unit Frames Extended"], + description = T["Party frames, minimap, buffs, weapon buffs and debuffs can be moved while Ctrl+Shift are held."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Unit Frames"], + enabled = nil, +}) + +module.enable = function(self) + ShaguTweaks_config = ShaguTweaks_config or {} + ShaguTweaks_config["MoveUnitframesExtended"] = ShaguTweaks_config["MoveUnitframesExtended"] or {} + + local movedb = ShaguTweaks_config["MoveUnitframesExtended"] + local unlocked = false + local states = {} + + -- Turtle WoW places the first debuff at BuffButton32 in the layout this + -- module targets. BuffButton16 from the original mod is intentionally not + -- used here. + local targets = { + { name = "PartyMemberFrame1" }, + { name = "PartyMemberFrame2" }, + { name = "PartyMemberFrame3" }, + { name = "PartyMemberFrame4" }, + { name = "Minimap", moveParent = true }, + { name = "BuffButton0" }, + { name = "BuffButton32" }, + { name = "TempEnchant1" }, + } + + local function Resolve(target) + local handle = _G[target.name] + if not handle then return end + + local moveFrame = target.moveParent and handle:GetParent() or handle + if not moveFrame then return end + + return handle, moveFrame + end + + local function PositionKey(target, moveFrame) + return (moveFrame.GetName and moveFrame:GetName()) or target.name + end + + local function SavePosition(target, moveFrame) + if not moveFrame then + local _, resolved = Resolve(target) + moveFrame = resolved + end + if not moveFrame then return end + + local left = moveFrame:GetLeft() + local top = moveFrame:GetTop() + if not left or not top then return end + + movedb[PositionKey(target, moveFrame)] = { left, top } + end + + local function RestorePosition(target) + local _, moveFrame = Resolve(target) + if not moveFrame then return end + + local pos = movedb[PositionKey(target, moveFrame)] + if not pos or not pos[1] or not pos[2] then return end + + moveFrame:SetMovable(true) + if moveFrame.SetUserPlaced then moveFrame:SetUserPlaced(true) end + moveFrame:ClearAllPoints() + moveFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", pos[1], pos[2]) + end + + local grid + local function CreateGrid() + if grid then return grid end + + grid = CreateFrame("Frame", nil, WorldFrame) + grid:SetAllPoints(WorldFrame) + grid:Hide() + + local size = 1 + local width = GetScreenWidth() + local height = GetScreenHeight() + local ratio = width / height + local adjustedHeight = height * ratio + local wStep = width / 64 + local hStep = adjustedHeight / 64 + + for i = 0, 64 do + local line = grid:CreateTexture(nil, i == 32 and "BORDER" or "BACKGROUND") + if i == 32 then + line:SetTexture(.8, .6, 0) + else + line:SetTexture(0, 0, 0, .2) + end + line:SetPoint("TOPLEFT", grid, "TOPLEFT", i * wStep - (size / 2), 0) + line:SetPoint("BOTTOMRIGHT", grid, "BOTTOMLEFT", i * wStep + (size / 2), 0) + end + + local rows = floor(height / hStep) + local middle = floor(rows / 2) + + for i = 1, rows do + local line = grid:CreateTexture(nil, i == middle and "BORDER" or "BACKGROUND") + if i == middle then + line:SetTexture(.8, .6, 0) + else + line:SetTexture(0, 0, 0, .2) + end + line:SetPoint("TOPLEFT", grid, "TOPLEFT", 0, -(i * hStep) + (size / 2)) + line:SetPoint("BOTTOMRIGHT", grid, "TOPRIGHT", 0, -(i * hStep + size / 2)) + end + + return grid + end + + local function UnlockTarget(index, target) + local handle, moveFrame = Resolve(target) + if not handle or not moveFrame then return end + + states[index] = states[index] or {} + local state = states[index] + if state.active then return end + + state.active = true + state.dragged = false + state.handle = handle + state.moveFrame = moveFrame + state.onDragStart = handle:GetScript("OnDragStart") + state.onDragStop = handle:GetScript("OnDragStop") + + if handle.IsMouseEnabled then + state.mouseEnabled = handle:IsMouseEnabled() and true or false + else + state.mouseEnabled = nil + end + + if moveFrame.IsMovable then + state.movable = moveFrame:IsMovable() and true or false + else + state.movable = nil + end + + if moveFrame.IsUserPlaced then + state.userPlaced = moveFrame:IsUserPlaced() and true or false + else + state.userPlaced = nil + end + + moveFrame:SetMovable(true) + handle:EnableMouse(true) + handle:RegisterForDrag("LeftButton") + + handle:SetScript("OnDragStart", function() + state.dragged = true + + if moveFrame.SetUserPlaced then + moveFrame:SetUserPlaced(true) + end + + moveFrame:StartMoving() + end) + + handle:SetScript("OnDragStop", function() + moveFrame:StopMovingOrSizing() + + if state.dragged then + SavePosition(target, moveFrame) + end + end) + end + + local function LockTarget(index, target) + local state = states[index] + if not state or not state.active then return end + + local handle = state.handle + local moveFrame = state.moveFrame + + if moveFrame then + moveFrame:StopMovingOrSizing() + + -- Only persist an anchor if the user actually dragged this frame. The + -- previous test version saved every frame whenever Ctrl+Shift was + -- released, which could turn untouched default-managed UI elements into + -- permanently absolute-positioned frames. + if state.dragged then + SavePosition(target, moveFrame) + end + + if state.movable ~= nil then + moveFrame:SetMovable(state.movable) + end + + if not state.dragged + and state.userPlaced ~= nil + and moveFrame.SetUserPlaced then + moveFrame:SetUserPlaced(state.userPlaced) + end + end + + if handle then + handle:SetScript("OnDragStart", state.onDragStart) + handle:SetScript("OnDragStop", state.onDragStop) + + if state.mouseEnabled ~= nil then + handle:EnableMouse(state.mouseEnabled) + end + end + + state.active = false + end + + local function UnlockAll() + if unlocked then return end + unlocked = true + + for i, target in ipairs(targets) do + UnlockTarget(i, target) + end + + CreateGrid():Show() + end + + local function LockAll() + if not unlocked then return end + + for i, target in ipairs(targets) do + LockTarget(i, target) + end + + if grid then grid:Hide() end + unlocked = false + end + + local function UpdateLockState() + if API.IsShiftKeyDown() and API.IsControlKeyDown() then + UnlockAll() + else + LockAll() + end + end + + local events = CreateFrame("Frame") + events:RegisterEvent("PLAYER_ENTERING_WORLD") + + if API.modifierstate then + events:RegisterEvent("MODIFIER_STATE_CHANGED") + end + + events:SetScript("OnEvent", function() + if event == "PLAYER_ENTERING_WORLD" then + for _, target in ipairs(targets) do + RestorePosition(target) + end + end + + UpdateLockState() + end) + + -- ClassicAPI supplies MODIFIER_STATE_CHANGED. Only old/fallback environments + -- use a throttled state check. + if not API.modifierstate then + events.elapsed = 0 + events:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + (arg1 or 0) + if this.elapsed < .10 then return end + this.elapsed = 0 + UpdateLockState() + end) + end + + for _, target in ipairs(targets) do + RestorePosition(target) + end + + UpdateLockState() +end diff --git a/mods/unitframes-abbrev-names.lua b/mods/unitframes-abbrev-names.lua new file mode 100644 index 0000000..aa8697b --- /dev/null +++ b/mods/unitframes-abbrev-names.lua @@ -0,0 +1,140 @@ +-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons). + +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API + +local module = ShaguTweaks:register({ + title = T["Unit Frame Abbreviated Names"], + description = T["Abbreviates long target and target-of-target names."], + expansions = { ["vanilla"] = true, ["tbc"] = nil }, + category = T["Unit Frames"], + enabled = nil, +}) + +module.enable = function(self) + local maxLength = 15 + local cache = {} + + local function AbbrevWord(word) + return string.sub(word, 1, 1) .. ". " + end + + local function GetShortName(unit) + local name = UnitName(unit) + if not name then + cache[unit] = nil + return + end + + local cached = cache[unit] + if cached and cached.raw == name then + return cached.short + end + + local short = name + + if strlen(short) > maxLength then + short = string.gsub(short, "^(%S+) ", AbbrevWord) + end + + if strlen(short) > maxLength then + short = string.gsub(short, "(%S+) ", AbbrevWord) + end + + cache[unit] = { + raw = name, + short = short, + } + + return short + end + + local function GetNameText(frame) + if not frame then return end + if frame.name then return frame.name end + + if frame.GetName then + local frameName = frame:GetName() + if frameName then + return _G[frameName .. "Name"] + end + end + end + + local function UpdateFrame(frame, unit) + local text = GetNameText(frame) + local name = GetShortName(unit) + if not text or not name then return end + + -- Avoid forcing a FontString update when the displayed value is already + -- correct. This matters most for the legacy 250 ms target-of-target + -- fallback. + if not text.GetText or text:GetText() ~= name then + text:SetText(name) + end + end + + local function UpdateTarget() + UpdateFrame(TargetFrame, "target") + end + + local function UpdateTargetTarget() + if TargetofTargetFrame then + UpdateFrame(TargetofTargetFrame, "targettarget") + end + end + + local function EventValid(name) + return API.eventutils + and _G.C_EventUtils + and _G.C_EventUtils.IsEventValid + and _G.C_EventUtils.IsEventValid(name) + end + + local events = CreateFrame("Frame") + events:RegisterEvent("PLAYER_ENTERING_WORLD") + events:RegisterEvent("PLAYER_TARGET_CHANGED") + + local hasUnitTarget = EventValid("UNIT_TARGET") + local hasUnitName = EventValid("UNIT_NAME_UPDATE") + + if hasUnitTarget then events:RegisterEvent("UNIT_TARGET") end + if hasUnitName then events:RegisterEvent("UNIT_NAME_UPDATE") end + + events:SetScript("OnEvent", function() + if event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_TARGET_CHANGED" then + UpdateTarget() + UpdateTargetTarget() + elseif event == "UNIT_TARGET" then + if arg1 == "target" then UpdateTargetTarget() end + elseif event == "UNIT_NAME_UPDATE" then + if arg1 == "target" then + UpdateTarget() + elseif arg1 == "targettarget" then + UpdateTargetTarget() + end + end + end) + + -- ClassicAPI normally provides UNIT_TARGET. Keep a lightweight Vanilla + -- fallback only when that event isn't available instead of running every + -- rendered frame like the original module. + if not hasUnitTarget then + local fallback = CreateFrame("Frame") + fallback.elapsed = 0 + + fallback:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + (arg1 or 0) + if this.elapsed < .25 then return end + this.elapsed = 0 + + if UnitName("targettarget") then + UpdateTargetTarget() + end + end) + end + + UpdateTarget() + UpdateTargetTarget() +end