@@ -2948,6 +2948,30 @@ def _can_fix_profile_associations(self) -> bool:
29482948
29492949 def _check_keep_running (self ) -> bool :
29502950 """Check if the application should keep running."""
2951+ numwindows = len (self ._enumerate_own_top_level_windows ())
2952+ if numwindows < self .numwindows :
2953+ # One of our windows has been closed by an external event
2954+ # (i.e. WM_CLOSE). This is a hint that something external is trying
2955+ # to get us to exit. Comply by closing our main top-level window to
2956+ # initiate clean shutdown.
2957+ print ("Window count" , self .numwindows , "->" , numwindows )
2958+ return False
2959+ self .numwindows = numwindows
2960+ return True
2961+
2962+ def _enumerate_own_top_level_windows (self ) -> list :
2963+ """Toolkit-specific hook: list this process's own top-level UI windows.
2964+
2965+ Used by :meth:`_check_keep_running` to notice when something external
2966+ (e.g. a Windows session shutdown/logoff sending ``WM_CLOSE``) has
2967+ closed one of our windows, as a hint that we should comply and exit.
2968+ This default (wx) implementation enumerates the current thread's
2969+ native windows plus wx's own top-level window list. Overridden by
2970+ toolkit subclasses (e.g. the Qt port).
2971+
2972+ Returns:
2973+ list: The window handles/objects currently considered "ours".
2974+ """
29512975 windows = []
29522976 # print '-' * 79
29532977 with contextlib .suppress (pywintypes .error ):
@@ -2963,16 +2987,7 @@ def _check_keep_running(self) -> bool:
29632987 not in {"TaskBarNotification" , "DisplayIdentification" , "profile_info" }
29642988 ]
29652989 )
2966- numwindows = len (windows )
2967- if numwindows < self .numwindows :
2968- # One of our windows has been closed by an external event
2969- # (i.e. WM_CLOSE). This is a hint that something external is trying
2970- # to get us to exit. Comply by closing our main top-level window to
2971- # initiate clean shutdown.
2972- print ("Window count" , self .numwindows , "->" , numwindows )
2973- return False
2974- self .numwindows = numwindows
2975- return True
2990+ return windows
29762991
29772992 def _enumerate_own_windows_callback (self , hwnd : int , windowlist : list ) -> None :
29782993 """Callback for enumerating own windows.
@@ -3011,17 +3026,15 @@ def _process_display_changed(self) -> None:
30113026 self ._display_changed_event = True
30123027 self ._next = True
30133028 if getattr (self , "profile_associations_dlg" , None ):
3014- wx .CallAfter (
3015- wx .CallLater ,
3029+ self ._post_to_gui_thread_delayed (
30163030 1000 ,
30173031 lambda : (
30183032 self .profile_associations_dlg
30193033 and self .profile_associations_dlg .update (True )
30203034 ),
30213035 )
30223036 if getattr (self , "fix_profile_associations_dlg" , None ):
3023- wx .CallAfter (
3024- wx .CallLater ,
3037+ self ._post_to_gui_thread_delayed (
30253038 1000 ,
30263039 lambda : (
30273040 self .fix_profile_associations_dlg
@@ -3147,11 +3160,11 @@ def _check_display_conf_wrapper(self) -> None:
31473160 except Exception as exception :
31483161 if self .lock .locked ():
31493162 self .lock .release ()
3150- wx . CallAfter (self ._handle_fatal_error , exception )
3163+ self . _post_to_gui_thread (self ._handle_fatal_error , exception )
31513164
31523165 def _handle_fatal_error (self , exception : Exception ) -> None :
31533166 handle_error (exception )
3154- wx . CallAfter (self .exit )
3167+ self . _post_to_gui_thread (self .exit )
31553168
31563169 def _check_display_conf (self ) -> None :
31573170 """Thread to monitor display configuration changes."""
@@ -3209,19 +3222,19 @@ def _check_display_conf(self) -> None:
32093222 profile_associations_changed ,
32103223 )
32113224 if apply_profiles and not idle :
3212- wx . CallAfter ( lambda : self and self . taskbar_icon . animate () )
3225+ self . _post_to_gui_thread ( self . _animate_busy_icon )
32133226 self .__apply_profiles = apply_profiles
32143227 first_run = False
32153228 if profile_associations_changed and not self ._has_display_changed :
32163229 if getattr (self , "profile_associations_dlg" , None ):
3217- wx . CallAfter (
3230+ self . _post_to_gui_thread (
32183231 lambda : (
32193232 self .profile_associations_dlg
32203233 and self .profile_associations_dlg .update_profiles ()
32213234 )
32223235 )
32233236 if getattr (self , "fix_profile_associations_dlg" , None ):
3224- wx . CallAfter (
3237+ self . _post_to_gui_thread (
32253238 lambda : (
32263239 self .fix_profile_associations_dlg
32273240 and self .fix_profile_associations_dlg .update ()
@@ -3237,11 +3250,11 @@ def _check_display_conf(self) -> None:
32373250 if locked :
32383251 print ("DisplayConfigurationMonitoringThread: Released lock" )
32393252 if "--oneshot" in sys .argv [1 :]:
3240- wx . CallAfter (self .exit )
3253+ self . _post_to_gui_thread (self .exit )
32413254 break
32423255 if "--profile-associations" in sys .argv [1 :]:
32433256 sys .argv .remove ("--profile-associations" )
3244- wx . CallAfter (self ._set_profile_associations , None )
3257+ self . _post_to_gui_thread (self ._set_profile_associations , None )
32453258 # Wait three seconds
32463259 timeout = 0
32473260 while (
@@ -3253,7 +3266,7 @@ def _check_display_conf(self) -> None:
32533266 ):
32543267 if round (timeout * 100 ) % 25 == 0 and not self ._check_keep_running ():
32553268 self .monitoring = False
3256- wx . CallAfter ( lambda : self .frame and self . frame . Close ( force = True ) )
3269+ self . _post_to_gui_thread ( self ._request_forced_shutdown )
32573270 break
32583271 time .sleep (0.1 )
32593272 timeout += 0.1
@@ -4174,7 +4187,7 @@ def _handle_profile_updates(
41744187 elif (
41754188 apply_profiles != self .__apply_profiles or profile_associations_changed
41764189 ):
4177- wx . CallAfter ( lambda : self and self . taskbar_icon . set_visual_state () )
4190+ self . _post_to_gui_thread ( self . _refresh_visual_state )
41784191
41794192 def shutdown (self ) -> None :
41804193 """Shut down the profile loader."""
@@ -4927,6 +4940,60 @@ def _refresh_visual_state(self) -> None:
49274940 """
49284941 self .taskbar_icon .set_visual_state ()
49294942
4943+ def _post_to_gui_thread (self , func : Callable , * args : object ) -> None :
4944+ """Toolkit-specific hook: run ``func(*args)`` on the GUI thread, ASAP.
4945+
4946+ Used by the display-configuration monitoring thread
4947+ (:meth:`_check_display_conf` and friends) to safely touch UI state
4948+ from a background thread. This default (wx) implementation forwards
4949+ to ``wx.CallAfter``. Overridden by toolkit subclasses (e.g. the Qt
4950+ port).
4951+
4952+ Args:
4953+ func (Callable): The callable to run on the GUI thread.
4954+ *args (object): Positional arguments to pass to ``func``.
4955+ """
4956+ wx .CallAfter (func , * args )
4957+
4958+ def _post_to_gui_thread_delayed (
4959+ self , delay_ms : int , func : Callable , * args : object
4960+ ) -> None :
4961+ """Toolkit-specific hook: run ``func(*args)`` on the GUI thread after a delay.
4962+
4963+ This default (wx) implementation forwards to ``wx.CallLater``, itself
4964+ scheduled via ``wx.CallAfter`` since ``wx.CallLater`` must be
4965+ constructed on the GUI thread. Overridden by toolkit subclasses (e.g.
4966+ the Qt port).
4967+
4968+ Args:
4969+ delay_ms (int): The delay in milliseconds before ``func`` runs.
4970+ func (Callable): The callable to run on the GUI thread.
4971+ *args (object): Positional arguments to pass to ``func``.
4972+ """
4973+ wx .CallAfter (wx .CallLater , delay_ms , func , * args )
4974+
4975+ def _animate_busy_icon (self ) -> None :
4976+ """Toolkit-specific hook: show a "processing" indicator while applying.
4977+
4978+ This default (wx) implementation triggers the taskbar icon's
4979+ frame-by-frame animation. Overridden by toolkit subclasses that
4980+ render icon state statically instead (e.g. the Qt port -- see its
4981+ module docstring).
4982+ """
4983+ self .taskbar_icon .animate ()
4984+
4985+ def _request_forced_shutdown (self ) -> None :
4986+ """Toolkit-specific hook: force-close on the GUI thread, no confirmation.
4987+
4988+ Used by :meth:`_check_display_conf` when
4989+ :meth:`_check_keep_running` notices something external (e.g. a
4990+ Windows session shutdown/logoff) asked us to exit. This default (wx)
4991+ implementation force-closes the persistent frame. Overridden by
4992+ toolkit subclasses (e.g. the Qt port).
4993+ """
4994+ if self .frame :
4995+ self .frame .Close (force = True )
4996+
49304997 def _should_apply_profiles (
49314998 self ,
49324999 enumerate_windows_and_processes : bool = True ,
0 commit comments