Skip to content

Commit 180e102

Browse files
authored
Merge pull request #897 from eoyilmaz/891-qt-hotplug-monitoring-thread-port
[#891] Port the Windows hot-plug monitoring thread to the Qt tray
2 parents 7312604 + eebd292 commit 180e102

4 files changed

Lines changed: 473 additions & 36 deletions

File tree

DisplayCAL/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.10.0.dev56
1+
3.10.0.dev57

DisplayCAL/profile_loader.py

Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)