@@ -301,6 +301,12 @@ def _drag_canvas(self):
301301 except Exception :
302302 return None
303303
304+ def _drag_siblings (self ):
305+ """All draggable cards in our list, ordered by on-screen position."""
306+ cards = [w for w in self .master .winfo_children () if isinstance (w , ActionCard )]
307+ cards .sort (key = lambda w : w .winfo_rooty ())
308+ return cards
309+
304310 def _start_drag (self , event ):
305311 """Begin a drag operation. Motion is captured on the toplevel so the drag
306312 stays smooth even when the pointer crosses over neighbouring cards."""
@@ -309,13 +315,13 @@ def _start_drag(self, event):
309315
310316 self ._drag_active = True
311317 self ._drag_start_index = self .index
312- self ._drag_last_pos = None # target slot the indicator currently shows
313- self ._drag_pointer_y = None
318+ self ._drag_start_slot = self .index
319+ self ._drag_slot = self .index
320+ self ._drag_pointer_y = event .y_root
314321 self ._drag_autoscroll_job = None
315322
316- # Highlight the card being dragged (elevated look). We deliberately do NOT
317- # repack it during the drag — the real reorder happens once on release,
318- # which is what keeps the motion fluid.
323+ # Highlight the card being dragged so it is unmistakable among similar
324+ # looking siblings while the pointer moves it.
319325 self .configure (style = 'DragCard.TFrame' )
320326 self .header_frame .configure (style = 'DragCard.TFrame' )
321327 try :
@@ -328,50 +334,15 @@ def _start_drag(self, event):
328334 toplevel .bind ('<B1-Motion>' , self ._drag_motion )
329335 toplevel .bind ('<ButtonRelease-1>' , self ._end_drag )
330336
331- def _drag_insertion_y (self , cards , pos ):
332- """Canvas-Y where the insertion indicator should sit for slot ``pos``
333- (the gap the dragged card would land in)."""
334- if pos >= len (cards ):
335- last = cards [- 1 ]
336- return last .winfo_y () + last .winfo_height ()
337- return cards [pos ].winfo_y ()
338-
339- def _draw_drag_indicator (self , event ):
340- """Draw (and move) the insertion line showing exactly where the dragged
341- card will land. Only redraws when the target slot changes so ordinary
342- motion stays cheap and fluid; the card itself does not move."""
343- cards = [w for w in self .master .winfo_children () if isinstance (w , ActionCard )]
344- cards .sort (key = lambda w : w .winfo_rooty ())
345- pos = 0
346- for c in cards :
347- if c is self :
348- continue
349- cy = c .winfo_rooty () + c .winfo_height () // 2
350- if (self ._drag_pointer_y or 0 ) > cy :
351- pos += 1
352- if pos == self ._drag_last_pos :
353- return
354- self ._drag_last_pos = pos
355-
356- canvas = self ._drag_canvas ()
357- if canvas is None :
358- return
359- canvas .delete ('_drag_ind' )
360- target = self ._drag_insertion_y (cards , pos )
361- w = canvas .winfo_width ()
362- if w > 1 :
363- canvas .create_line (0 , target , w , target , fill = COLORS ['primary' ],
364- width = 4 , tags = '_drag_ind' )
365- canvas .update_idletasks ()
366-
367337 def _drag_motion (self , event ):
368- """Smooth drag: show a live insertion indicator where the card will land,
369- auto-scrolling ONLY while the pointer is pressed against the canvas edge
370- (the list otherwise stays put)."""
338+ """Smooth drag: the card physically follows the pointer. The list is only
339+ repacked when the drop slot actually changes (so ordinary motion stays
340+ cheap), and it auto-scrolls ONLY while the pointer is pressed against the
341+ top/bottom edge band — the list otherwise stays put."""
371342 if not self ._drag_active :
372343 return
373344 self ._drag_pointer_y = event .y_root
374- self ._draw_drag_indicator ( event )
345+ self ._move_to_pointer_slot ( )
375346
376347 canvas = self ._drag_canvas ()
377348 if canvas is None :
@@ -382,16 +353,52 @@ def _drag_motion(self, event):
382353 edge = 40
383354 if event .y_root < top + edge :
384355 depth = (top + edge - event .y_root ) / float (edge )
385- self ._set_autoscroll (canvas , - 1 , depth )
356+ self ._start_autoscroll (canvas , - 1 , depth )
386357 elif event .y_root > top + height - edge :
387358 depth = (event .y_root - (top + height - edge )) / float (edge )
388- self ._set_autoscroll (canvas , 1 , depth )
359+ self ._start_autoscroll (canvas , 1 , depth )
389360 else :
390361 self ._stop_autoscroll ()
391362 except Exception :
392363 pass
393364
394- def _set_autoscroll (self , canvas , direction , depth ):
365+ def _slot_from_pointer (self ):
366+ """Target slot (gap between other cards) the card would land in right
367+ now, based on the last known pointer root-Y. Other cards' slots are the
368+ gaps; the dragged card itself is skipped."""
369+ pos = 0
370+ for c in self ._drag_siblings ():
371+ if c is self :
372+ continue
373+ cy = c .winfo_rooty () + c .winfo_height () // 2
374+ if self ._drag_pointer_y > cy :
375+ pos += 1
376+ return pos
377+
378+ def _move_to_pointer_slot (self ):
379+ """Repack the dragged card so it visually tracks the pointer. Only does
380+ real layout work when the target slot changes (the cheap path)."""
381+ cards = self ._drag_siblings ()
382+ pos = self ._drag_slot_from_pointer (cards )
383+ if pos == self ._drag_slot :
384+ return
385+ self ._drag_slot = pos
386+ self ._repack_at_slot (cards , pos )
387+
388+ def _repack_at_slot (self , cards , pos ):
389+ parent = self .master
390+ others = [c for c in cards if c is not self ]
391+ if pos >= len (others ):
392+ # Move to the end (below every sibling)
393+ self .pack (fill = tk .X , pady = 1 )
394+ else :
395+ self .pack (fill = tk .X , pady = 1 , before = others [pos ])
396+ try :
397+ parent .update_idletasks ()
398+ except Exception :
399+ pass
400+
401+ def _start_autoscroll (self , canvas , direction , depth ):
395402 """Smooth auto-scroll loop while the pointer hovers in the edge band.
396403 Velocity grows with pointer depth; the loop self-cancels once the pointer
397404 leaves the band or the drag ends."""
@@ -415,7 +422,10 @@ def _tick():
415422 return
416423 speed = max (1 , int (depth * 6 ))
417424 canvas .yview_scroll (direction * speed , "units" )
418- self ._draw_drag_indicator (None )
425+ # The list moved under the pointer — re-slot so the dragged card
426+ # keeps following even while the band auto-scrolls.
427+ cards = self ._drag_siblings ()
428+ self ._repack_at_slot (cards , self ._drag_slot_from_pointer (cards ))
419429 except Exception :
420430 self ._drag_autoscroll_job = None
421431 return
@@ -433,7 +443,8 @@ def _stop_autoscroll(self):
433443 self ._drag_autoscroll_job = None
434444
435445 def _end_drag (self , event ):
436- """End drag: remove the indicator, then commit the reorder if it changed."""
446+ """End drag: restore the card's look, then commit the reorder if it
447+ changed. A click without real movement toggles the editor instead."""
437448 toplevel = self .winfo_toplevel ()
438449 toplevel .unbind ('<B1-Motion>' )
439450 toplevel .unbind ('<ButtonRelease-1>' )
@@ -443,14 +454,6 @@ def _end_drag(self, event):
443454 return
444455 self ._drag_active = False
445456
446- # Remove the insertion indicator
447- canvas = self ._drag_canvas ()
448- if canvas is not None :
449- try :
450- canvas .delete ('_drag_ind' )
451- except Exception :
452- pass
453-
454457 # Restore the card's normal colors
455458 self .configure (style = self .card_style )
456459 self .header_frame .configure (style = self .card_style )
@@ -462,19 +465,19 @@ def _end_drag(self, event):
462465 pass
463466
464467 start = self ._drag_start_index
465- target = self ._drag_last_pos
468+ target = self ._drag_slot
466469 self ._drag_start_index = None
467- self ._drag_last_pos = None
470+ self ._drag_slot = None
471+ self ._drag_pointer_y = None
468472
469- # No real motion (a click, or a drag with no slot change) toggles the editor
470- if target is None or target == start :
473+ # A click without real movement toggles the editor
474+ if target == start :
471475 self .toggle_expand ()
472476 return
473477
474- if target != start and 'move_to' in self .callbacks :
478+ if target is not None and target != start and 'move_to' in self .callbacks :
475479 self .callbacks ['move_to' ](start , target )
476480
477-
478481 # --- Context Menu ---
479482 def show_context_menu (self , event ):
480483 """Show right-click context menu."""
0 commit comments