Skip to content

Commit d8970d1

Browse files
feat(clickcasting): drag-to-reorder bindings in Active Bindings list
Adds a thin drag handle on the left edge of each binding row. Clicking and dragging the handle shows a floating ghost that follows the cursor plus an insert-position line, letting users reorder bindings in whatever order they prefer (e.g. Left / Middle / Right, then modified variants) instead of the chronological add order. Disabled bindings keep their original slots; reordering only shuffles the visible set and merges back into db.bindings. After drop: ApplyBindings, RefreshActiveBindings, RefreshSpellGrid — same flow used by the delete button.
1 parent 5895055 commit d8970d1

2 files changed

Lines changed: 247 additions & 2 deletions

File tree

ClickCasting/UI/BindingEditor.lua

Lines changed: 246 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,216 @@ end
558558

559559
-- ============================================================
560560

561+
-- ============================================================
562+
-- BINDING ROW DRAG-TO-REORDER
563+
-- Handle + floating ghost + insert indicator for drag reordering
564+
-- ============================================================
565+
566+
local reorderState = nil -- active drag state
567+
local reorderGhost = nil -- floating cursor-follower frame
568+
local reorderIndicator = nil -- insert-position line
569+
local reorderUpdater = nil -- OnUpdate driver
570+
571+
-- Build the ordered list of currently-visible (enabled) bindings.
572+
local function BuildVisibleSequence()
573+
local seq = {}
574+
local bindings = CC.db and CC.db.bindings or {}
575+
for _, b in ipairs(bindings) do
576+
if b.enabled ~= false then
577+
table.insert(seq, b)
578+
end
579+
end
580+
return seq
581+
end
582+
583+
-- Compute drop zone index (0 .. N) from cursor Y against bindingsContent.
584+
-- Zone 0 = insert before first row, Zone N = insert after last row.
585+
local function ComputeDropZone(visibleCount)
586+
local content = CC.bindingsContent
587+
if not content or visibleCount <= 0 then return 0 end
588+
local scale = content:GetEffectiveScale()
589+
local _, cy = GetCursorPosition()
590+
cy = cy / scale
591+
local top = content:GetTop()
592+
if not top then return 0 end
593+
local relY = top - cy -- distance from content top, in pixels
594+
local rowH = BINDING_ROW_HEIGHT
595+
local zone = math.floor(relY / rowH + 0.5)
596+
if zone < 0 then zone = 0 end
597+
if zone > visibleCount then zone = visibleCount end
598+
return zone
599+
end
600+
601+
local function EnsureGhost()
602+
if reorderGhost then return reorderGhost end
603+
local C = CC.UI_COLORS
604+
local theme = C and C.theme or { r = 1, g = 1, b = 1 }
605+
local g = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
606+
g:SetFrameStrata("TOOLTIP")
607+
g:SetSize(220, 34)
608+
g:SetBackdrop({
609+
bgFile = "Interface\\Buttons\\WHITE8x8",
610+
edgeFile = "Interface\\Buttons\\WHITE8x8",
611+
edgeSize = 1,
612+
})
613+
g:SetBackdropColor(0, 0, 0, 0.88)
614+
g:SetBackdropBorderColor(theme.r, theme.g, theme.b, 1)
615+
local gi = g:CreateTexture(nil, "ARTWORK")
616+
gi:SetSize(24, 24)
617+
gi:SetPoint("LEFT", 4, 0)
618+
gi:SetTexCoord(0.08, 0.92, 0.08, 0.92)
619+
g.icon = gi
620+
local gt = g:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
621+
gt:SetPoint("LEFT", gi, "RIGHT", 6, 0)
622+
gt:SetPoint("RIGHT", -6, 0)
623+
gt:SetJustifyH("LEFT")
624+
gt:SetWordWrap(false)
625+
g.text = gt
626+
g:Hide()
627+
reorderGhost = g
628+
return g
629+
end
630+
631+
local function EnsureIndicator()
632+
if reorderIndicator then return reorderIndicator end
633+
local content = CC.bindingsContent
634+
if not content then return nil end
635+
local C = CC.UI_COLORS
636+
local theme = C and C.theme or { r = 1, g = 1, b = 1 }
637+
local ind = content:CreateTexture(nil, "OVERLAY")
638+
ind:SetHeight(2)
639+
ind:SetColorTexture(theme.r, theme.g, theme.b, 1)
640+
ind:Hide()
641+
reorderIndicator = ind
642+
return ind
643+
end
644+
645+
local function UpdateReorderVisuals()
646+
if not reorderState then return end
647+
local ghost = reorderGhost
648+
if ghost then
649+
local scale = UIParent:GetEffectiveScale()
650+
local cx, cy = GetCursorPosition()
651+
ghost:ClearAllPoints()
652+
ghost:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", cx / scale + 14, cy / scale - 4)
653+
end
654+
local content = CC.bindingsContent
655+
local ind = reorderIndicator
656+
if content and ind then
657+
local zone = ComputeDropZone(reorderState.visibleCount)
658+
reorderState.dropZone = zone
659+
ind:ClearAllPoints()
660+
ind:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -zone * BINDING_ROW_HEIGHT + 1)
661+
ind:SetPoint("TOPRIGHT", content, "TOPRIGHT", 0, -zone * BINDING_ROW_HEIGHT + 1)
662+
ind:Show()
663+
end
664+
end
665+
666+
local function BeginBindingReorder(row, binding)
667+
if InCombatLockdown() then return end -- be defensive, though this is non-secure UI
668+
if not CC.db or not CC.db.bindings then return end
669+
local seq = BuildVisibleSequence()
670+
local sourceVisibleIdx
671+
for i, b in ipairs(seq) do
672+
if b == binding then sourceVisibleIdx = i; break end
673+
end
674+
if not sourceVisibleIdx then return end
675+
676+
reorderState = {
677+
sourceBinding = binding,
678+
sourceRow = row,
679+
sourceVisibleIdx = sourceVisibleIdx,
680+
visibleCount = #seq,
681+
dropZone = sourceVisibleIdx - 1,
682+
}
683+
684+
row:SetAlpha(0.4)
685+
686+
local ghost = EnsureGhost()
687+
if ghost then
688+
if row.icon and row.icon.GetTexture then
689+
ghost.icon:SetTexture(row.icon:GetTexture())
690+
ghost.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
691+
end
692+
if row.name and row.name.GetText then
693+
ghost.text:SetText(row.name:GetText() or "")
694+
end
695+
ghost:Show()
696+
end
697+
698+
EnsureIndicator()
699+
700+
if not reorderUpdater then
701+
reorderUpdater = CreateFrame("Frame")
702+
reorderUpdater:SetScript("OnUpdate", UpdateReorderVisuals)
703+
end
704+
reorderUpdater:Show()
705+
UpdateReorderVisuals()
706+
end
707+
708+
local function CancelBindingReorder()
709+
if reorderState and reorderState.sourceRow then
710+
reorderState.sourceRow:SetAlpha(1)
711+
end
712+
reorderState = nil
713+
if reorderGhost then reorderGhost:Hide() end
714+
if reorderIndicator then reorderIndicator:Hide() end
715+
if reorderUpdater then reorderUpdater:Hide() end
716+
end
717+
718+
local function EndBindingReorder()
719+
if not reorderState then return end
720+
local state = reorderState
721+
local sourceBinding = state.sourceBinding
722+
local sourceVisibleIdx = state.sourceVisibleIdx
723+
local targetZone = state.dropZone or (sourceVisibleIdx - 1)
724+
725+
-- Clean up visuals first (avoid flash during Refresh)
726+
if reorderGhost then reorderGhost:Hide() end
727+
if reorderIndicator then reorderIndicator:Hide() end
728+
if reorderUpdater then reorderUpdater:Hide() end
729+
if state.sourceRow then state.sourceRow:SetAlpha(1) end
730+
reorderState = nil
731+
732+
-- No-op if dropped in its current slot (zones sourceIdx-1 or sourceIdx both mean "stay put")
733+
if targetZone == sourceVisibleIdx - 1 or targetZone == sourceVisibleIdx then
734+
return
735+
end
736+
737+
local seq = BuildVisibleSequence()
738+
-- Re-resolve source index in case array changed underneath
739+
local curSource
740+
for i, b in ipairs(seq) do
741+
if b == sourceBinding then curSource = i; break end
742+
end
743+
if not curSource then return end
744+
745+
table.remove(seq, curSource)
746+
local insertAt = targetZone
747+
if curSource < targetZone then insertAt = insertAt - 1 end
748+
insertAt = math.max(0, math.min(#seq, insertAt))
749+
table.insert(seq, insertAt + 1, sourceBinding)
750+
751+
-- Rebuild CC.db.bindings, preserving disabled entries in their original slots
752+
local oldDb = CC.db.bindings
753+
local newDb = {}
754+
local seqIdx = 1
755+
for _, b in ipairs(oldDb) do
756+
if b.enabled ~= false then
757+
newDb[#newDb + 1] = seq[seqIdx]
758+
seqIdx = seqIdx + 1
759+
else
760+
newDb[#newDb + 1] = b
761+
end
762+
end
763+
CC.db.bindings = newDb
764+
765+
if CC.ApplyBindings then CC:ApplyBindings() end
766+
if CC.RefreshActiveBindings then CC:RefreshActiveBindings() end
767+
if CC.RefreshSpellGrid then CC:RefreshSpellGrid(true) end
768+
end
769+
770+
-- =========================================================================
561771
-- ACTIVE BINDINGS ROW CREATION
562772
-- =========================================================================
563773
function CC:CreateBindingRow(parent, binding, index)
@@ -577,7 +787,7 @@ function CC:CreateBindingRow(parent, binding, index)
577787
-- Icon (larger to fill height better)
578788
local icon = row:CreateTexture(nil, "ARTWORK")
579789
icon:SetSize(32, 32)
580-
icon:SetPoint("LEFT", 4, 0)
790+
icon:SetPoint("LEFT", 14, 0) -- Shifted right to make room for drag handle
581791

582792
-- Set icon based on action type
583793
if binding.actionType == "target" then
@@ -637,7 +847,41 @@ function CC:CreateBindingRow(parent, binding, index)
637847
end
638848
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
639849
row.icon = icon
640-
850+
851+
-- Drag handle (far left, thin strip for reordering)
852+
local dragHandle = CreateFrame("Button", nil, row)
853+
dragHandle:SetPoint("TOPLEFT", 0, 0)
854+
dragHandle:SetPoint("BOTTOMLEFT", 0, 0)
855+
dragHandle:SetWidth(10)
856+
dragHandle:RegisterForDrag("LeftButton")
857+
dragHandle:EnableMouse(true)
858+
859+
local handleIcon = dragHandle:CreateTexture(nil, "OVERLAY")
860+
handleIcon:SetSize(8, 14)
861+
handleIcon:SetPoint("CENTER")
862+
handleIcon:SetTexture("Interface\\AddOns\\DandersFrames\\Media\\Icons\\reorder")
863+
handleIcon:SetVertexColor(C.textDim.r, C.textDim.g, C.textDim.b)
864+
865+
dragHandle:SetScript("OnEnter", function(self)
866+
handleIcon:SetVertexColor(themeColor.r, themeColor.g, themeColor.b)
867+
SetCursor("Interface\\CURSOR\\UI-Cursor-Move")
868+
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
869+
GameTooltip:AddLine(L["Drag to reorder"], 1, 1, 1)
870+
GameTooltip:Show()
871+
end)
872+
dragHandle:SetScript("OnLeave", function()
873+
handleIcon:SetVertexColor(C.textDim.r, C.textDim.g, C.textDim.b)
874+
SetCursor(nil)
875+
GameTooltip:Hide()
876+
end)
877+
dragHandle:SetScript("OnDragStart", function()
878+
BeginBindingReorder(row, binding)
879+
end)
880+
dragHandle:SetScript("OnDragStop", function()
881+
EndBindingReorder()
882+
end)
883+
row.dragHandle = dragHandle
884+
641885
-- Delete button (far right)
642886
local deleteBtn = CreateFrame("Button", nil, row)
643887
deleteBtn:SetSize(20, 20)

Locales/enUS.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ L["Delete"] = true
8787
L["Disable While Mounted"] = true
8888
L["Disable while mounted/flying"] = true
8989
L["Disabled"] = true
90+
L["Drag to reorder"] = true
9091
L["Edit Binding"] = true
9192
L["Edit Copy"] = true
9293
L["Edit Macro"] = true

0 commit comments

Comments
 (0)