Unify Movable Unit Frames

Merge extended movable-frame support into the core module, keep one Ctrl+Shift controller and one alignment grid, preserve saved positions, and retain MIT attribution.
This commit is contained in:
2026-08-31 10:20:56 +02:00
committed by GitHub
parent a8c2a1f1ba
commit ea2b17425f
3 changed files with 325 additions and 99 deletions
+4
View File
@@ -33,6 +33,7 @@ Settings: **Esc → Advanced Options**.
- Safer hooks and fewer destructive global overrides.
- Expanded Item Rarity Borders support for merchant, quest, mail, trade, crafting and loot frames.
- Added independent Item Rarity Glows using the shared item rarity engine.
- Expanded Movable Unit Frames to handle Player, Target, Party, Minimap, Buffs, Debuffs and Weapon Buffs through one shared Ctrl+Shift mover and alignment grid.
- Various stability fixes across legacy ShaguTweaks modules.
## 🔷 Mods using ClassicAPI directly
@@ -147,6 +148,7 @@ The following original ShaguTweaks modules are not included as standalone module
- **Chat Links** → integrated into **Chat Tweaks**
- **Real Health Numbers** → value display integrated into **Unit Frame Big Health**
- **Blue Shaman Class Colors** → no longer required; supported natively by Turtle WoW-like clients
- **Movable Unit Frames Extended** → integrated into **Movable Unit Frames**
## 🙏 Credits
@@ -158,4 +160,6 @@ ClassicAPI compatibility fork maintained by **Dusk-92**.
Additional code and ideas from **pfUI** and **zUI**.
Extended Movable Unit Frames support adapted from **TokensWorth/ShaguTweaks-mods**, originally released under MIT by **GryllsAddons**. See `THIRD_PARTY_NOTICES.md`.
Released under the original **MIT License**.
+33
View File
@@ -0,0 +1,33 @@
# Third-Party Notices
## TokensWorth/ShaguTweaks-mods
Extended frame support in `mods/move-unitframes.lua` contains code adapted
from `TokensWorth/ShaguTweaks-mods`:
- upstream module: `mods/move-unitframes-extended.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.
+288 -99
View File
@@ -1,132 +1,321 @@
-- Movable Unit Frames
-- Extended frame support 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"],
description = T["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."],
description = T["Player, Target, Party, Minimap, Buffs, Weapon Buffs and Debuffs can be moved while <Shift> and <Ctrl> are pressed together."],
expansions = { ["vanilla"] = true },
category = T["Unit Frames"],
enabled = true,
})
local movables = { "PlayerFrame", "TargetFrame" }
module.enable = function(self)
local unlocker = CreateFrame("Frame", nil, UIParent)
ShaguTweaks_config = ShaguTweaks_config or {}
ShaguTweaks_config["MoveUnitframes"] = ShaguTweaks_config["MoveUnitframes"] or {}
for _, frame in pairs(movables) do
_G[frame]:SetClampedToScreen(true)
local movedb = ShaguTweaks_config["MoveUnitframes"]
-- Preserve positions created by the former Extras module. The legacy table
-- is intentionally left untouched so downgrading does not destroy data.
local legacy = ShaguTweaks_config["MoveUnitframesExtended"]
if legacy then
for key, value in pairs(legacy) do
if movedb[key] == nil then
movedb[key] = value
end
end
end
unlocker.grid = CreateFrame("Frame", nil, WorldFrame)
unlocker.grid:SetAllPoints(WorldFrame)
unlocker.grid:Hide()
local unlocked = false
local states = {}
local grid
local size = 1
local line = {}
-- Player/Target keep the original ShaguTweaks user-placed behavior.
-- The formerly-Extended frames use ShaguTweaks_config so their positions
-- survive reload/relog consistently.
local targets = {
{ name = "PlayerFrame", clamp = true, persist = false },
{ name = "TargetFrame", clamp = true, persist = false },
{ name = "PartyMemberFrame1", persist = true },
{ name = "PartyMemberFrame2", persist = true },
{ name = "PartyMemberFrame3", persist = true },
{ name = "PartyMemberFrame4", persist = true },
{ name = "Minimap", moveParent = true, persist = true },
{ name = "BuffButton0", persist = true },
{ name = "BuffButton32", persist = true },
{ name = "TempEnchant1", persist = true },
}
local width = GetScreenWidth()
local height = GetScreenHeight()
local function Resolve(target)
local handle = _G[target.name]
if not handle then return end
local ratio = width / GetScreenHeight()
local rheight = GetScreenHeight() * ratio
local moveFrame = target.moveParent and handle:GetParent() or handle
if not moveFrame then return end
local wStep = width / 64
local hStep = rheight / 64
return handle, moveFrame
end
-- vertical lines
for i = 0, 64 do
if i == 64 / 2 then
line = unlocker.grid:CreateTexture(nil, 'BORDER')
line:SetTexture(.8, .6, 0)
local function PositionKey(target, moveFrame)
if moveFrame.GetName then
local name = moveFrame:GetName()
if name then return name end
end
return target.name
end
local function SavePosition(target, moveFrame)
if not target.persist then return end
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)
if not target.persist then return end
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 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
line = unlocker.grid:CreateTexture(nil, 'BACKGROUND')
line:SetTexture(0, 0, 0, .2)
state.mouseEnabled = nil
end
line:SetPoint("TOPLEFT", unlocker.grid, "TOPLEFT", i*wStep - (size/2), 0)
line:SetPoint('BOTTOMRIGHT', unlocker.grid, 'BOTTOMLEFT', i*wStep + (size/2), 0)
end
-- horizontal lines
for i = 1, floor(height/hStep) do
if i == floor(height/hStep / 2) then
line = unlocker.grid:CreateTexture(nil, 'BORDER')
line:SetTexture(.8, .6, 0)
if moveFrame.IsMovable then
state.movable = moveFrame:IsMovable() and true or false
else
line = unlocker.grid:CreateTexture(nil, 'BACKGROUND')
line:SetTexture(0, 0, 0, .2)
state.movable = nil
end
line:SetPoint("TOPLEFT", unlocker.grid, "TOPLEFT", 0, -(i*hStep) + (size/2))
line:SetPoint('BOTTOMRIGHT', unlocker.grid, 'TOPRIGHT', 0, -(i*hStep + size/2))
end
local function SetMovableState(shouldMove)
if shouldMove and not unlocker.movable then
for _, frame in pairs(movables) do
_G[frame]:SetUserPlaced(true)
_G[frame]:SetMovable(true)
_G[frame]:EnableMouse(true)
_G[frame]:RegisterForDrag("LeftButton")
_G[frame]:SetScript("OnDragStart", function() this:StartMoving() end)
_G[frame]:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
end
unlocker.movable = true
unlocker.grid:Show()
elseif not shouldMove and unlocker.movable then
for _, frame in pairs(movables) do
_G[frame]:SetScript("OnDragStart", function() end)
_G[frame]:SetScript("OnDragStop", function() end)
_G[frame]:StopMovingOrSizing()
end
unlocker.movable = nil
unlocker.grid:Hide()
end
end
local hasModifierEvent = _G.C_EventUtils
and type(_G.C_EventUtils.IsEventValid) == "function"
and _G.C_EventUtils.IsEventValid("MODIFIER_STATE_CHANGED")
if hasModifierEvent then
-- Do not re-query modifier functions from inside MODIFIER_STATE_CHANGED.
-- The event already tells us exactly which physical key transitioned and
-- whether it is now down (arg2=1) or up (arg2=0). Keeping our own state
-- avoids any merged/stale Win32 key-state ambiguity in the callback.
local state = {
LSHIFT = type(_G.IsLeftShiftKeyDown) == "function" and _G.IsLeftShiftKeyDown() and true or false,
RSHIFT = type(_G.IsRightShiftKeyDown) == "function" and _G.IsRightShiftKeyDown() and true or false,
LCTRL = type(_G.IsLeftControlKeyDown) == "function" and _G.IsLeftControlKeyDown() and true or false,
RCTRL = type(_G.IsRightControlKeyDown) == "function" and _G.IsRightControlKeyDown() and true or false,
}
local function RefreshFromState()
SetMovableState((state.LSHIFT or state.RSHIFT) and (state.LCTRL or state.RCTRL))
if moveFrame.IsUserPlaced then
state.userPlaced = moveFrame:IsUserPlaced() and true or false
else
state.userPlaced = nil
end
unlocker:RegisterEvent("MODIFIER_STATE_CHANGED")
unlocker:RegisterEvent("PLAYER_ENTERING_WORLD")
unlocker:SetScript("OnEvent", function()
if event == "MODIFIER_STATE_CHANGED" then
if state[arg1] ~= nil then
state[arg1] = arg2 == 1
end
elseif event == "PLAYER_ENTERING_WORLD" then
state.LSHIFT = type(_G.IsLeftShiftKeyDown) == "function" and _G.IsLeftShiftKeyDown() and true or false
state.RSHIFT = type(_G.IsRightShiftKeyDown) == "function" and _G.IsRightShiftKeyDown() and true or false
state.LCTRL = type(_G.IsLeftControlKeyDown) == "function" and _G.IsLeftControlKeyDown() and true or false
state.RCTRL = type(_G.IsRightControlKeyDown) == "function" and _G.IsRightControlKeyDown() and true or false
if target.clamp and moveFrame.SetClampedToScreen then
moveFrame:SetClampedToScreen(true)
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
RefreshFromState()
moveFrame:StartMoving()
end)
else
-- Compatibility fallback for clients without ClassicAPI's modifier event.
-- This is intentionally tiny: two boolean key checks per rendered frame.
unlocker:SetScript("OnUpdate", function()
SetMovableState(IsShiftKeyDown() and IsControlKeyDown())
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()
if state.dragged then
SavePosition(target, moveFrame)
end
if state.movable ~= nil then
moveFrame:SetMovable(state.movable)
end
-- A dragged frame remains user-placed. An untouched frame is restored
-- exactly to the state it had before Ctrl+Shift was pressed.
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 clients use
-- a small throttled key-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
if target.clamp then
local _, moveFrame = Resolve(target)
if moveFrame and moveFrame.SetClampedToScreen then
moveFrame:SetClampedToScreen(true)
end
end
RestorePosition(target)
end
UpdateLockState()
end