@@ -53,6 +53,7 @@ def _invoke_in_main_thread(fn, *args, **kwargs):
5353class SysDependencyManager :
5454 def __init__ (self , parent_widget = None ):
5555 self .parent_widget = parent_widget
56+ self ._cancelled_procs : set [int ] = set ()
5657 # Register list of system dependency tasks on the parent widget for global coordination
5758 try :
5859 if parent_widget is not None and not hasattr (
@@ -97,6 +98,81 @@ def _unregister_task(self, proc: QProcess) -> None:
9798 tasks .remove (t )
9899 except Exception :
99100 pass
101+ try :
102+ if proc is not None :
103+ self ._cancelled_procs .discard (id (proc ))
104+ except Exception :
105+ pass
106+
107+ def _mark_process_cancelled (self , proc : Optional [QProcess ]) -> None :
108+ try :
109+ if proc is not None :
110+ self ._cancelled_procs .add (id (proc ))
111+ except Exception :
112+ pass
113+
114+ def _is_process_cancelled (self , proc : Optional [QProcess ]) -> bool :
115+ try :
116+ return proc is not None and id (proc ) in self ._cancelled_procs
117+ except Exception :
118+ return False
119+
120+ def _log_cancel (self , fr : str , en : str ) -> None :
121+ text = self .tr (fr , en )
122+ try :
123+ if (
124+ self .parent_widget is not None
125+ and hasattr (self .parent_widget , "_safe_log" )
126+ and callable (self .parent_widget ._safe_log )
127+ ):
128+ self .parent_widget ._safe_log (text )
129+ except Exception :
130+ pass
131+ try :
132+ self ._dbg (text )
133+ except Exception :
134+ pass
135+
136+ def _cancel_task (
137+ self , proc : Optional [QProcess ], dlg : Optional [ProgressDialog ], label_fr : str , label_en : str
138+ ) -> None :
139+ self ._mark_process_cancelled (proc )
140+ self ._log_cancel (
141+ f"🛑 Annulation demandée: { label_fr } ." ,
142+ f"🛑 Cancellation requested: { label_en } ." ,
143+ )
144+ try :
145+ if proc is not None and proc .state () != QProcess .NotRunning :
146+ proc .kill ()
147+ except Exception :
148+ pass
149+ try :
150+ if dlg is not None :
151+ dlg .close ()
152+ except Exception :
153+ pass
154+ try :
155+ if proc is not None :
156+ self ._unregister_task (proc )
157+ except Exception :
158+ pass
159+
160+ def _bind_cancel_button (
161+ self ,
162+ dlg : Optional [ProgressDialog ],
163+ proc : Optional [QProcess ],
164+ label_fr : str ,
165+ label_en : str ,
166+ ) -> None :
167+ try :
168+ btn = getattr (dlg , "btn_cancel" , None ) if dlg is not None else None
169+ if btn is None :
170+ return
171+ btn .clicked .connect (
172+ lambda : self ._cancel_task (proc , dlg , label_fr , label_en )
173+ )
174+ except Exception :
175+ pass
100176
101177 # ------------- Debug/telemetry helpers -------------
102178 def set_debug (self , enabled : bool = True ) -> None :
@@ -532,14 +608,31 @@ def install_packages_windows(self, packages: list[dict]) -> Optional[QProcess]:
532608 "Installing Windows dependencies" ,
533609 ),
534610 self .parent_widget ,
611+ cancelable = True ,
535612 )
536613 dlg .set_message (self .tr ("Préparation…" , "Preparing…" ))
537614 dlg .progress .setRange (0 , 0 )
538615 dlg .show ()
539616 queue = list (packages )
540617 proc = QProcess (self .parent_widget )
618+ state = {"cancelled" : False }
619+
620+ def _cancel_winget ():
621+ if state ["cancelled" ]:
622+ return
623+ state ["cancelled" ] = True
624+ self ._cancel_task (proc , dlg , "installation winget" , "winget installation" )
625+
626+ try :
627+ btn = getattr (dlg , "btn_cancel" , None )
628+ if btn is not None :
629+ btn .clicked .connect (_cancel_winget )
630+ except Exception :
631+ pass
541632
542633 def _start_next ():
634+ if state ["cancelled" ] or self ._is_process_cancelled (proc ):
635+ return
543636 if not queue :
544637 try :
545638 dlg .close ()
@@ -581,6 +674,8 @@ def _start_next():
581674 proc .start ()
582675
583676 def _on_output (p : QProcess , error : bool = False ):
677+ if state ["cancelled" ] or self ._is_process_cancelled (proc ):
678+ return
584679 try :
585680 data = (
586681 p .readAllStandardError ().data ().decode ()
@@ -601,6 +696,8 @@ def _on_output(p: QProcess, error: bool = False):
601696 pass
602697
603698 def _on_finished (_ec , _es ):
699+ if state ["cancelled" ] or self ._is_process_cancelled (proc ):
700+ return
604701 _start_next ()
605702
606703 proc .readyReadStandardOutput .connect (lambda p = proc : _on_output (p , False ))
@@ -632,18 +729,25 @@ def start_process_with_progress(
632729 Le dialogue se ferme automatiquement à la fin du processus.
633730 """
634731 try :
635- dlg = ProgressDialog (self .tr (title_fr , title_en ), self .parent_widget )
732+ dlg = ProgressDialog (
733+ self .tr (title_fr , title_en ), self .parent_widget , cancelable = True
734+ )
636735 dlg .set_message (self .tr (start_msg_fr , start_msg_en ))
637736 dlg .progress .setRange (0 , 0 ) # indéterminé
638737 dlg .show ()
639738 proc = QProcess (self .parent_widget )
739+ self ._bind_cancel_button (
740+ dlg , proc , "installation des dépendances" , "dependencies installation"
741+ )
640742 if cwd :
641743 proc .setWorkingDirectory (cwd )
642744 proc .setProgram (program )
643745 proc .setArguments (list (args or []))
644746
645747 # Mise à jour du message avec la dernière ligne reçue
646748 def _on_output (p : QProcess , error : bool = False ):
749+ if self ._is_process_cancelled (proc ):
750+ return
647751 try :
648752 data = (
649753 p .readAllStandardError ().data ().decode ()
@@ -710,11 +814,16 @@ def run_sudo_shell_with_progress(
710814 "This sudo operation is supported on Linux only." ,
711815 )
712816 return None
713- dlg = ProgressDialog (self .tr (title_fr , title_en ), self .parent_widget )
817+ dlg = ProgressDialog (
818+ self .tr (title_fr , title_en ), self .parent_widget , cancelable = True
819+ )
714820 dlg .set_message (self .tr (start_msg_fr , start_msg_en ))
715821 dlg .progress .setRange (0 , 0 )
716822 dlg .show ()
717823 proc = QProcess (self .parent_widget )
824+ self ._bind_cancel_button (
825+ dlg , proc , "installation des dépendances" , "dependencies installation"
826+ )
718827 if cwd :
719828 proc .setWorkingDirectory (cwd )
720829 # Utiliser bash -lc pour exécuter la chaîne
@@ -723,6 +832,8 @@ def run_sudo_shell_with_progress(
723832
724833 # maj message sur sortie
725834 def _on_output (p : QProcess , error : bool = False ):
835+ if self ._is_process_cancelled (proc ):
836+ return
726837 try :
727838 data = (
728839 p .readAllStandardError ().data ().decode ()
0 commit comments