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:
@@ -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**.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user