diff --git a/README.md b/README.md index 7485e98..e2ef526 100644 --- a/README.md +++ b/README.md @@ -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**. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..18ef46a --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -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. diff --git a/mods/move-unitframes.lua b/mods/move-unitframes.lua index 0e8ecd0..8fa0ff1 100644 --- a/mods/move-unitframes.lua +++ b/mods/move-unitframes.lua @@ -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 and are pressed together."], + description = T["Player, Target, Party, Minimap, Buffs, Weapon Buffs and Debuffs can be moved while and 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