1+ use crate :: gui:: confirmation_modal:: { ConfirmationModal , ConfirmationResult , DestructiveAction } ;
12use crate :: gui:: LauncherApp ;
23use crate :: mouse_gestures:: db:: {
34 format_gesture_label, load_gestures, save_gestures, BindingEntry , BindingKind , GestureDb ,
@@ -284,6 +285,15 @@ pub struct MgGesturesDialog {
284285 recorder : GestureRecorder ,
285286 token_buffer : String ,
286287 binding_dialog : BindingDialog ,
288+ delete_confirm_modal : ConfirmationModal ,
289+ pending_delete : Option < PendingGestureDelete > ,
290+ }
291+
292+ #[ derive( Debug , Clone ) ]
293+ struct PendingGestureDelete {
294+ idx : usize ,
295+ label : String ,
296+ tokens : String ,
287297}
288298
289299impl Default for MgGesturesDialog {
@@ -298,6 +308,8 @@ impl Default for MgGesturesDialog {
298308 recorder : GestureRecorder :: new ( DirMode :: Four , config) ,
299309 token_buffer : String :: new ( ) ,
300310 binding_dialog : BindingDialog :: default ( ) ,
311+ delete_confirm_modal : ConfirmationModal :: default ( ) ,
312+ pending_delete : None ,
301313 }
302314 }
303315}
@@ -381,6 +393,73 @@ impl MgGesturesDialog {
381393 self . binding_dialog . open = false ;
382394 }
383395
396+ fn queue_gesture_delete ( & mut self , idx : usize ) -> bool {
397+ let Some ( entry) = self . db . gestures . get ( idx) else {
398+ return false ;
399+ } ;
400+ self . pending_delete = Some ( PendingGestureDelete {
401+ idx,
402+ label : entry. label . clone ( ) ,
403+ tokens : entry. tokens . clone ( ) ,
404+ } ) ;
405+ self . delete_confirm_modal
406+ . open_for ( DestructiveAction :: DeleteGesture ) ;
407+ true
408+ }
409+
410+ fn resolve_pending_delete_index ( & self , pending : & PendingGestureDelete ) -> Option < usize > {
411+ if self
412+ . db
413+ . gestures
414+ . get ( pending. idx )
415+ . is_some_and ( |g| g. label == pending. label && g. tokens == pending. tokens )
416+ {
417+ return Some ( pending. idx ) ;
418+ }
419+ self . db
420+ . gestures
421+ . iter ( )
422+ . position ( |g| g. label == pending. label && g. tokens == pending. tokens )
423+ }
424+
425+ fn adjust_indices_after_delete ( & mut self , idx : usize ) {
426+ if let Some ( selected) = self . selected_idx {
427+ if selected == idx {
428+ self . selected_idx = None ;
429+ } else if selected > idx {
430+ self . selected_idx = Some ( selected - 1 ) ;
431+ }
432+ }
433+
434+ if let Some ( rename) = self . rename_idx {
435+ if rename == idx {
436+ self . rename_idx = None ;
437+ } else if rename > idx {
438+ self . rename_idx = Some ( rename - 1 ) ;
439+ }
440+ }
441+
442+ self . ensure_selection ( ) ;
443+ self . binding_dialog . editor . reset ( ) ;
444+ self . binding_dialog . open = false ;
445+ }
446+
447+ fn apply_pending_gesture_delete ( & mut self ) -> bool {
448+ let Some ( pending) = self . pending_delete . take ( ) else {
449+ return false ;
450+ } ;
451+ let Some ( idx) = self . resolve_pending_delete_index ( & pending) else {
452+ return false ;
453+ } ;
454+ self . db . gestures . remove ( idx) ;
455+ self . adjust_indices_after_delete ( idx) ;
456+ true
457+ }
458+
459+ fn cancel_pending_gesture_delete ( & mut self ) {
460+ self . pending_delete = None ;
461+ }
462+
384463 fn save ( & mut self , app : & mut LauncherApp ) {
385464 if let Err ( e) = save_gestures ( GESTURES_FILE , & self . db ) {
386465 app. set_error ( format ! ( "Failed to save mouse gestures: {e}" ) ) ;
@@ -821,7 +900,7 @@ impl MgGesturesDialog {
821900 // ScrollArea creates its own child Ui; re-apply the left clip
822901 // so horizontally-wide rows can't paint into the right panel.
823902 ui. set_clip_rect ( left_clip) ;
824- let mut remove_idx : Option < usize > = None ;
903+ let mut request_delete_idx : Option < usize > = None ;
825904 let gesture_order = self . sorted_gesture_indices ( ) ;
826905 for idx in gesture_order {
827906 let selected = self . selected_idx == Some ( idx) ;
@@ -845,7 +924,7 @@ impl MgGesturesDialog {
845924 self . rename_label = entry. label . clone ( ) ;
846925 }
847926 if ui. button ( "Delete" ) . clicked ( ) {
848- remove_idx = Some ( idx) ;
927+ request_delete_idx = Some ( idx) ;
849928 }
850929 } ) ;
851930 if self . rename_idx == Some ( idx) {
@@ -873,29 +952,8 @@ impl MgGesturesDialog {
873952 } ) ;
874953 }
875954 }
876- if let Some ( idx) = remove_idx {
877- self . db . gestures . remove ( idx) ;
878-
879- if let Some ( selected) = self . selected_idx {
880- if selected == idx {
881- self . selected_idx = None ;
882- } else if selected > idx {
883- self . selected_idx = Some ( selected - 1 ) ;
884- }
885- }
886-
887- if let Some ( rename) = self . rename_idx {
888- if rename == idx {
889- self . rename_idx = None ;
890- } else if rename > idx {
891- self . rename_idx = Some ( rename - 1 ) ;
892- }
893- }
894-
895- self . ensure_selection ( ) ;
896- self . binding_dialog . editor . reset ( ) ;
897- self . binding_dialog . open = false ;
898- save_now = true ;
955+ if let Some ( idx) = request_delete_idx {
956+ let _ = self . queue_gesture_delete ( idx) ;
899957 }
900958 } ) ;
901959 ui. separator ( ) ;
@@ -1074,6 +1132,15 @@ impl MgGesturesDialog {
10741132 } ) ;
10751133 } ) ;
10761134 self . binding_dialog_ui ( ctx, app, & mut save_now) ;
1135+ match self . delete_confirm_modal . ui ( ctx) {
1136+ ConfirmationResult :: Confirmed => {
1137+ if self . apply_pending_gesture_delete ( ) {
1138+ save_now = true ;
1139+ }
1140+ }
1141+ ConfirmationResult :: Cancelled => self . cancel_pending_gesture_delete ( ) ,
1142+ ConfirmationResult :: None => { }
1143+ }
10771144 if save_now {
10781145 self . save ( app) ;
10791146 }
@@ -1084,3 +1151,60 @@ impl MgGesturesDialog {
10841151 }
10851152 }
10861153}
1154+
1155+ #[ cfg( test) ]
1156+ mod tests {
1157+ use super :: * ;
1158+
1159+ fn gesture ( label : & str , tokens : & str ) -> GestureEntry {
1160+ GestureEntry {
1161+ label : label. into ( ) ,
1162+ tokens : tokens. into ( ) ,
1163+ dir_mode : DirMode :: Four ,
1164+ stroke : Vec :: new ( ) ,
1165+ enabled : true ,
1166+ bindings : Vec :: new ( ) ,
1167+ }
1168+ }
1169+
1170+ #[ test]
1171+ fn gesture_delete_is_queued_until_confirmed ( ) {
1172+ let mut dlg = MgGesturesDialog :: default ( ) ;
1173+ dlg. db . gestures = vec ! [ gesture( "A" , "R" ) ] ;
1174+ dlg. selected_idx = Some ( 0 ) ;
1175+
1176+ assert ! ( dlg. queue_gesture_delete( 0 ) ) ;
1177+ assert_eq ! ( dlg. db. gestures. len( ) , 1 ) ;
1178+ assert ! ( dlg. pending_delete. is_some( ) ) ;
1179+ }
1180+
1181+ #[ test]
1182+ fn cancelling_gesture_delete_keeps_db_and_selection ( ) {
1183+ let mut dlg = MgGesturesDialog :: default ( ) ;
1184+ dlg. db . gestures = vec ! [ gesture( "A" , "R" ) , gesture( "B" , "L" ) ] ;
1185+ dlg. selected_idx = Some ( 1 ) ;
1186+ dlg. rename_idx = Some ( 1 ) ;
1187+
1188+ assert ! ( dlg. queue_gesture_delete( 1 ) ) ;
1189+ dlg. cancel_pending_gesture_delete ( ) ;
1190+
1191+ assert_eq ! ( dlg. db. gestures. len( ) , 2 ) ;
1192+ assert_eq ! ( dlg. selected_idx, Some ( 1 ) ) ;
1193+ assert_eq ! ( dlg. rename_idx, Some ( 1 ) ) ;
1194+ }
1195+
1196+ #[ test]
1197+ fn confirmed_gesture_delete_adjusts_indices ( ) {
1198+ let mut dlg = MgGesturesDialog :: default ( ) ;
1199+ dlg. db . gestures = vec ! [ gesture( "A" , "R" ) , gesture( "B" , "L" ) , gesture( "C" , "U" ) ] ;
1200+ dlg. selected_idx = Some ( 2 ) ;
1201+ dlg. rename_idx = Some ( 2 ) ;
1202+
1203+ assert ! ( dlg. queue_gesture_delete( 1 ) ) ;
1204+ assert ! ( dlg. apply_pending_gesture_delete( ) ) ;
1205+
1206+ assert_eq ! ( dlg. db. gestures. len( ) , 2 ) ;
1207+ assert_eq ! ( dlg. selected_idx, Some ( 1 ) ) ;
1208+ assert_eq ! ( dlg. rename_idx, Some ( 1 ) ) ;
1209+ }
1210+ }
0 commit comments