Skip to content

Commit c2b8ced

Browse files
author
mergetest
committed
Merge main for v4.8.0 release
2 parents b846710 + 5fd5d22 commit c2b8ced

12 files changed

Lines changed: 326 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# DandersFrames Changelog
22

3+
## [4.8.0]
4+
5+
### Bug Fixes
6+
7+
* (Click Casting) Fixed all binds being dead in your first arena or dungeon of a session until a reload. (by Krathe)
8+
* (Pinned Frames) Fixed an empty "drag to move" box getting stuck on screen after a profile switch, surviving reloads. (by Krathe)
9+
10+
### Improvements
11+
12+
* (Click Casting) Enabling a targeting fallback on a key that already does something now asks for confirmation and names what the key will stop doing.
13+
* (Click Casting) "Clear Blizzard Bindings" now warns that clearing is permanent, and only clears when you press it.
14+
315
## [4.7.5]
416

517
### Bug Fixes

ClickCasting/Bindings.lua

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2408,13 +2408,55 @@ end
24082408

24092409
-- Process all bindings and build unified macro map
24102410
-- Returns: { [keyString] = { macroText = "...", templateBinding = binding } }
2411+
-- Re-resolve click-casting state once cold-start data becomes available. Two
2412+
-- things can be built before GetSpecialization() resolves at the first login
2413+
-- of a session, and both used to stay wrong all day until a /reload:
2414+
-- * loadoutCheckUnresolved (CheckLoadoutProfileSwitch): the spec→profile
2415+
-- auto-switch could not run (or, before the guard, ran with the `or 1`
2416+
-- spec fallback and switched to the WRONG spec's profile) — the field
2417+
-- report: "none of my binds work in my first arena of the day"
2418+
-- * macroMapUnresolved (BuildUnifiedMacroMap): loadSpec-scoped bindings
2419+
-- were dropped from the map (legacy/import-only config; belt)
2420+
-- Debounced; each resolver clears its own flag on success and self-defers in
2421+
-- combat (pendingLoadoutCheck / needsBindingRefresh). No-op in steady state,
2422+
-- so the extra triggers cost nothing.
2423+
function CC:ResolveProvisionalMap(reason)
2424+
if not (self.macroMapUnresolved or self.loadoutCheckUnresolved) then return end
2425+
if not (self.db and self.db.enabled) then return end
2426+
if self.provisionalResolveTimer then self.provisionalResolveTimer:Cancel() end
2427+
self.provisionalResolveTimer = C_Timer.NewTimer(0.5, function()
2428+
CC.provisionalResolveTimer = nil
2429+
if CC.loadoutCheckUnresolved then
2430+
DF:Debug("CLICK", "Re-running deferred loadout profile check (%s)", tostring(reason))
2431+
CC:CheckLoadoutProfileSwitch()
2432+
end
2433+
if CC.macroMapUnresolved then
2434+
DF:Debug("CLICK", "Rebuilding provisional binding map (%s)", tostring(reason))
2435+
CC:ApplyBindings()
2436+
end
2437+
end)
2438+
end
2439+
24112440
function CC:BuildUnifiedMacroMap()
24122441
local macroMap = {}
2413-
2442+
2443+
-- Cold-start guard: ShouldBindingLoad drops every loadSpec-scoped binding
2444+
-- while GetSpecialization() is still nil (first login of a session, before
2445+
-- spec data resolves). The map is built once and cached, so a map built in
2446+
-- that window silently loses those bindings — and an all-spec-scoped setup
2447+
-- comes up EMPTY, which downstream disables clicks on our frames entirely
2448+
-- ("none of my binds work in my first arena of the day until I reload").
2449+
-- Record the condition on the module; the resolve watchers (SPELLS_CHANGED /
2450+
-- PLAYER_SPECIALIZATION_CHANGED / arena prep) rebuild when data arrives, and
2451+
-- ApplyBindingsToFrameUnified refuses to wipe a frame off a suspect map.
2452+
local specKnown = GetSpecialization() ~= nil
2453+
local anySpecScoped = false
2454+
24142455
-- Group all bindings by their key string
24152456
local keyGroups = {}
24162457
for i, binding in ipairs(self.db.bindings) do
24172458
if binding.enabled ~= false then
2459+
if binding.loadSpec then anySpecScoped = true end
24182460
local keyString = self:GetBindingKeyString(binding)
24192461
if keyString then
24202462
if not keyGroups[keyString] then
@@ -2424,6 +2466,11 @@ function CC:BuildUnifiedMacroMap()
24242466
end
24252467
end
24262468
end
2469+
2470+
self.macroMapUnresolved = (anySpecScoped and not specKnown) or nil
2471+
if self.macroMapUnresolved then
2472+
DF:DebugWarn("CLICK", "BuildUnifiedMacroMap: spec unknown with spec-scoped bindings — map is provisional")
2473+
end
24272474

24282475
-- Build macro for each key group
24292476
for keyString, group in pairs(keyGroups) do
@@ -2529,17 +2576,16 @@ function CC:ApplyBindingsToFrameUnified(frame, skipKeyboardUpdate)
25292576
frameName, debugstack(2, 1, 0) or "unknown")
25302577
end
25312578

2532-
-- Clear existing bindings first
2533-
self:ClearBindingsFromFrame(frame)
2534-
25352579
-- Build unified macro map if not already built
25362580
if not self.unifiedMacroMap then
25372581
self.unifiedMacroMap = self:BuildUnifiedMacroMap()
25382582
-- Refresh keyboard bindings on all frames since map was just built
25392583
self:RefreshKeyboardBindings()
25402584
end
2541-
2542-
-- Check if this frame has ANY bindings that apply to it
2585+
2586+
-- Check if this frame has ANY bindings that apply to it — BEFORE the
2587+
-- destructive clear below, so a provisional (cold-start) map can bail out
2588+
-- without touching the frame's existing state.
25432589
local hasAnyBindings = false
25442590
local isDandersFrame = frame.dfIsDandersFrame == true
25452591
local isBlizzardFrame = frame.dfIsBlizzardFrame == true
@@ -2565,6 +2611,16 @@ function CC:ApplyBindingsToFrameUnified(frame, skipKeyboardUpdate)
25652611

25662612
-- If no bindings apply to this frame
25672613
if not hasAnyBindings then
2614+
-- Provisional map (spec not yet resolved at build time): the emptiness
2615+
-- is almost certainly the cold-start drop, not the user's config. Do
2616+
-- NOT clear/disable anything — leave the frame exactly as it is; the
2617+
-- resolve watchers rebuild and re-apply once spec data arrives.
2618+
if self.macroMapUnresolved then
2619+
DF:Debug("CLICK", "ApplyBindings %s deferred — provisional map (spec unresolved)", frameName)
2620+
return
2621+
end
2622+
-- Genuinely no bindings for this frame: clean it up.
2623+
self:ClearBindingsFromFrame(frame)
25682624
if isDandersFrame then
25692625
-- For DandersFrames, completely disable clicks when no bindings apply
25702626
-- Our own frames get type1/type2 set in InitializeHeaderChild as a safety net
@@ -2579,7 +2635,11 @@ function CC:ApplyBindingsToFrameUnified(frame, skipKeyboardUpdate)
25792635
end
25802636
return
25812637
end
2582-
2638+
2639+
-- Clear existing bindings first (moved below the applicability check so a
2640+
-- provisional-map bailout above never strips a frame's working state)
2641+
self:ClearBindingsFromFrame(frame)
2642+
25832643
-- Register for clicks based on castOnDown option
25842644
if frame.RegisterForClicks then
25852645
local castOnDown = self.profile and self.profile.options and self.profile.options.castOnDown

ClickCasting/Constants.lua

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -441,22 +441,28 @@ local FRAME_INFO = {
441441
CC.FRAME_INFO = FRAME_INFO
442442

443443
-- Targeting Fallback info (checkboxes - for keyboard/hover bindings)
444+
-- Shared warning: every fallback below forces the binding's key onto the
445+
-- PERMANENT global bind list (BuildHovercastSetupScript) — the key then does
446+
-- this binding everywhere and stops performing its normal action, even away
447+
-- from the frames. The editor also confirms via ConfirmGlobalKeyCapture when
448+
-- the key already has a Blizzard binding.
449+
local GLOBAL_CAPTURE_WARNING = " While enabled, the key is captured everywhere — it performs this binding instead of its normal action, even away from the frames."
444450
local FALLBACK_INFO = {
445451
mouseover = {
446452
name = "Global",
447-
desc = "Cast on nameplates or characters in the world. Not needed for party/raid frames.",
453+
desc = "Cast on nameplates or characters in the world. Not needed for party/raid frames." .. GLOBAL_CAPTURE_WARNING,
448454
},
449455
target = {
450456
name = "Target",
451-
desc = "Cast on your current target if no frame or mouseover unit is found.",
457+
desc = "Cast on your current target if no frame or mouseover unit is found." .. GLOBAL_CAPTURE_WARNING,
452458
},
453459
selfCast = {
454460
name = "Self",
455-
desc = "Cast on yourself as a last resort if no other valid target is found.",
461+
desc = "Cast on yourself as a last resort if no other valid target is found." .. GLOBAL_CAPTURE_WARNING,
456462
},
457463
alwaysCast = {
458464
name = "Always Cast",
459-
desc = "If no rule above matches (hovering nothing, or an ineligible unit), cast anyway using the spell's normal targeting. Lets ground-targeted spells show their aiming circle when pressed in the open. If Self is also enabled, Self applies first.",
465+
desc = "If no rule above matches (hovering nothing, or an ineligible unit), cast anyway using the spell's normal targeting. Lets ground-targeted spells show their aiming circle when pressed in the open. If Self is also enabled, Self applies first." .. GLOBAL_CAPTURE_WARNING,
460466
},
461467
stopSpellTarget = {
462468
name = "Cancel Targeting",

ClickCasting/Events.lua

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ function CC:RegisterEvents()
2929
-- Nameplate events for click-casting on nameplates
3030
eventFrame:RegisterEvent("NAME_PLATE_UNIT_ADDED")
3131
eventFrame:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
32+
33+
-- Spell data arrival — resolves a provisional (cold-start) binding map
34+
eventFrame:RegisterEvent("SPELLS_CHANGED")
3235

3336
eventFrame:SetScript("OnEvent", function(_, event, ...)
3437
if event == "PLAYER_REGEN_ENABLED" then
@@ -39,6 +42,12 @@ function CC:RegisterEvents()
3942
elseif event == "PLAYER_SPECIALIZATION_CHANGED" or event == "ACTIVE_PLAYER_SPECIALIZATION_CHANGED" then
4043
-- Spec changed - check for profile switch
4144
CC:OnSpecChanged()
45+
-- Cold-start resolve: a map built before GetSpecialization()
46+
-- resolved dropped every spec-scoped binding — rebuild it now
47+
CC:ResolveProvisionalMap("spec-resolved")
48+
elseif event == "SPELLS_CHANGED" then
49+
-- Spell data arrived/changed — no-op unless the map is provisional
50+
CC:ResolveProvisionalMap("spells-changed")
4251
elseif event == "TRAIT_CONFIG_UPDATED" or event == "TRAIT_CONFIG_CREATED" or event == "ACTIVE_COMBAT_CONFIG_CHANGED" then
4352
-- Loadout/talent changed - check for profile switch and reapply bindings (with debounce)
4453
if not InCombatLockdown() then
@@ -96,15 +105,26 @@ function CC:RegisterEvents()
96105
-- so a broken hover-bind state never survives a zone change
97106
CC:RunBindingRepair("zone-in", true)
98107

99-
-- Check for loadout-based profile on initial load
108+
-- Cold-start resolve: if the login build ran before spec data
109+
-- was available, the map is provisional — rebuild it on the
110+
-- first loading screen so it is correct BEFORE the first
111+
-- arena/dungeon of the session, not only after a /reload
112+
CC:ResolveProvisionalMap("zone-in")
113+
114+
-- Check for loadout-based profile on initial load. No combat
115+
-- guard here: CheckLoadoutProfileSwitch defers itself via
116+
-- pendingLoadoutCheck in lockdown — the old call-site guard
117+
-- silently DROPPED the check when zone-in+1s landed in combat
118+
-- (the arena-load race), leaving the previous spec's profile
119+
-- active for the whole match.
100120
C_Timer.After(1, function()
101-
if not InCombatLockdown() then
102-
CC:CheckLoadoutProfileSwitch()
103-
end
121+
CC:CheckLoadoutProfileSwitch()
104122
end)
105123
end)
106124
elseif event == "ARENA_PREP_OPPONENT_SPECIALIZATIONS" then
107125
-- Arena frames should now exist
126+
-- Belt: never enter an arena on a provisional (cold-start) map
127+
CC:ResolveProvisionalMap("arena-prep")
108128
CC:OnArenaPrep()
109129
elseif event == "INSTANCE_ENCOUNTER_ENGAGE_UNIT" then
110130
-- Boss frames should now exist

ClickCasting/Frames.lua

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,12 @@ function CC:InitializeSavedVariables()
7676
if self.db.global.disableWhileFlying == nil then
7777
self.db.global.disableWhileFlying = false
7878
end
79-
79+
80+
-- Retired: the persisted "re-wipe Blizzard's click-cast profile on every
81+
-- enable" flag (see the removal note in DisableBlizzardClickCast).
82+
-- Clearing is a one-time, explicitly confirmed action now.
83+
self.db.clearBlizzardOnEnable = nil
84+
8085
-- Ensure classes table exists
8186
if not self.db.classes then
8287
self.db.classes = {}
@@ -568,12 +573,14 @@ function CC:DisableBlizzardClickCasting()
568573

569574
-- Hook into Blizzard's click cast system to prevent conflicts
570575
if not self.blizzardClickCastDisabled then
571-
-- Reset Blizzard's click-casting profile on first run (if user chose to clear)
572-
-- This completely removes any Blizzard click-cast bindings
573-
if self.db.clearBlizzardOnEnable and C_ClickBindings and C_ClickBindings.ResetCurrentProfile then
574-
C_ClickBindings.ResetCurrentProfile()
575-
end
576-
576+
-- (Removed) The clearBlizzardOnEnable auto re-wipe. Pressing "Clear
577+
-- Blizzard Bindings" once used to persist the flag and silently
578+
-- ResetCurrentProfile() — a PERMANENT wipe of the user's native
579+
-- click-cast profile — on every future enable. Clearing is now a
580+
-- one-time action taken only when the button is pressed (with a
581+
-- permanence warning in the dialog); the stored flag is stripped in
582+
-- InitializeSavedVariables.
583+
577584
-- Clear any existing Blizzard click cast config on our frames
578585
if SetUnitFrameClickCastConfig then
579586
-- Hook to prevent Blizzard from setting click casts on frames we manage

ClickCasting/Profiles.lua

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,10 +492,27 @@ end
492492
-- Check and auto-switch profile based on current loadout
493493
function CC:CheckLoadoutProfileSwitch()
494494
if InCombatLockdown() then
495-
-- Will be called again when combat ends
495+
-- Defer, don't drop: entering an arena/dungeon starts combat quickly,
496+
-- and silently losing the check leaves the previous spec's profile
497+
-- active for the whole match. OnCombatEnd drains pendingLoadoutCheck.
498+
self.pendingLoadoutCheck = true
496499
return
497500
end
498-
501+
502+
-- Cold-start guard: at the first login of a session this can run BEFORE
503+
-- GetSpecialization() resolves. GetCurrentSpec()'s `or 1` fallback would
504+
-- then MASK the missing data and switch to spec 1's profile — the wrong
505+
-- profile for anyone whose actual spec is 2+ ("none of my binds work in
506+
-- my first arena of the day until I reload"). Record the unresolved state
507+
-- and let the resolve watchers (spec/spell events, loading screens, arena
508+
-- prep) re-run this check once real data arrives.
509+
if not GetSpecialization() then
510+
self.loadoutCheckUnresolved = true
511+
DF:Debug("CLICK", "CheckLoadoutProfileSwitch: spec data not ready — deferred to resolve watchers")
512+
return
513+
end
514+
self.loadoutCheckUnresolved = nil
515+
499516
local specIndex = GetCurrentSpec()
500517
local loadoutID = GetCurrentLoadoutConfigID()
501518
local assignedProfile, isSpecific = self:GetProfileForLoadout(specIndex, loadoutID)

ClickCasting/UI/BindingEditor.lua

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,12 +710,46 @@ function CC:CreateEditBindingPanel()
710710
fallbackSubtitle:SetTextColor(0.5, 0.5, 0.5)
711711
panel.fallbackSubtitle = fallbackSubtitle
712712

713+
-- Enabling any global-forcing fallback (Mouseover/Target/Self/Always Cast)
714+
-- puts the binding's key on the PERMANENT global bind list
715+
-- (BuildHovercastSetupScript): the key then performs this binding
716+
-- everywhere and stops doing its normal job, even away from the frames —
717+
-- a wheel bind eats camera zoom, for example. If the key currently has a
718+
-- Blizzard binding, confirm the capture and name exactly what gets
719+
-- replaced. The flag is already set by the checkbox handler; Cancel
720+
-- reverts it (nothing persists until Save anyway).
721+
local function ConfirmGlobalKeyCapture(cb, flagKey)
722+
if not cb:GetChecked() then return end -- unchecking never captures
723+
local b = panel.pendingBinding
724+
if not b or (b.bindType ~= "key" and b.bindType ~= "scroll") then return end
725+
local keyString = CC:GetBindingKeyString(b)
726+
if not keyString or keyString == "" then return end
727+
local action = GetBindingAction and GetBindingAction(keyString)
728+
if not action or action == "" then return end
729+
local actionName = (GetBindingText and GetBindingText(action)) or action
730+
DF:ShowPopupAlert({
731+
title = L["Key Used Elsewhere"],
732+
message = format(L["This will capture %s everywhere — even away from the frames — and replace its current action:"], keyString)
733+
.. "\n\n|cffffcc00" .. actionName .. "|r",
734+
buttons = {
735+
{ label = L["Enable Anyway"], onClick = nil },
736+
{ label = L["Cancel"], onClick = function()
737+
cb:SetChecked(false)
738+
if panel.pendingBinding and panel.pendingBinding.fallback then
739+
panel.pendingBinding.fallback[flagKey] = false
740+
end
741+
end },
742+
},
743+
})
744+
end
745+
713746
-- Mouseover checkbox
714747
local mouseoverCB = CreateCheckbox(advancedContent, FALLBACK_INFO.mouseover.name, FALLBACK_INFO.mouseover.desc)
715748
mouseoverCB:SetPoint("TOPLEFT", 18, -38)
716749
mouseoverCB:SetScript("OnClick", function(self)
717750
panel.pendingBinding.fallback = panel.pendingBinding.fallback or {}
718751
panel.pendingBinding.fallback.mouseover = self:GetChecked()
752+
ConfirmGlobalKeyCapture(self, "mouseover")
719753
end)
720754
panel.mouseoverCB = mouseoverCB
721755

@@ -725,6 +759,7 @@ function CC:CreateEditBindingPanel()
725759
targetFallbackCB:SetScript("OnClick", function(self)
726760
panel.pendingBinding.fallback = panel.pendingBinding.fallback or {}
727761
panel.pendingBinding.fallback.target = self:GetChecked()
762+
ConfirmGlobalKeyCapture(self, "target")
728763
end)
729764
panel.targetFallbackCB = targetFallbackCB
730765

@@ -734,6 +769,7 @@ function CC:CreateEditBindingPanel()
734769
selfCB:SetScript("OnClick", function(self)
735770
panel.pendingBinding.fallback = panel.pendingBinding.fallback or {}
736771
panel.pendingBinding.fallback.selfCast = self:GetChecked()
772+
ConfirmGlobalKeyCapture(self, "selfCast")
737773
end)
738774
panel.selfCB = selfCB
739775

@@ -743,6 +779,7 @@ function CC:CreateEditBindingPanel()
743779
alwaysCastCB:SetScript("OnClick", function(self)
744780
panel.pendingBinding.fallback = panel.pendingBinding.fallback or {}
745781
panel.pendingBinding.fallback.alwaysCast = self:GetChecked()
782+
ConfirmGlobalKeyCapture(self, "alwaysCast")
746783
end)
747784
panel.alwaysCastCB = alwaysCastCB
748785

0 commit comments

Comments
 (0)