@@ -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-- =========================================================================
563773function 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 )
0 commit comments