From 85f15c8b40e65070d8f30662e02ed55dfe3dde3b Mon Sep 17 00:00:00 2001 From: Washik Date: Fri, 6 Feb 2026 03:33:03 +0100 Subject: [PATCH 1/4] add-media-source-callback --- docs/widgets/(Widget)-Media.md | 1 + src/core/utils/win32/aumid.py | 34 ++++++++++++++++++++++++++++++++++ src/core/widgets/yasb/media.py | 9 +++++++++ 3 files changed, 44 insertions(+) diff --git a/docs/widgets/(Widget)-Media.md b/docs/widgets/(Widget)-Media.md index eea7c8b24..35ddad4c4 100644 --- a/docs/widgets/(Widget)-Media.md +++ b/docs/widgets/(Widget)-Media.md @@ -169,6 +169,7 @@ media: - `toggle_label`: Toggles the visibility of the label. - `toggle_play_pause`: Toggles between play and pause states. - `toggle_media_menu`: Toggles the visibility of the media menu popup. +- `open_media_source`: Opens the the source that is playing the media. - `do_nothing`: A placeholder callback that does nothing when triggered. ## Description of Options diff --git a/src/core/utils/win32/aumid.py b/src/core/utils/win32/aumid.py index ad8642286..94d39a79d 100644 --- a/src/core/utils/win32/aumid.py +++ b/src/core/utils/win32/aumid.py @@ -236,3 +236,37 @@ def get_aumid_from_shortcut(shortcut_path: str) -> str | None: pass return aumid + + +def activate_app_by_aumid(aumid: str) -> bool: + """ + Find and activate validity window for the given AUMID. + + Args: + aumid: The App User Model ID to find. + + Returns: + True if a window was found and activation was attempted, False otherwise. + """ + from core.utils.win32.window_actions import set_foreground + + found_hwnd = None + + WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM) + + def enum_window_callback(hwnd, _): + nonlocal found_hwnd + if user32.IsWindowVisible(hwnd): + curr_aumid = get_aumid_for_window(hwnd) + if curr_aumid == aumid: + found_hwnd = hwnd + return False # Stop enumeration + return True + + user32.EnumWindows(WNDENUMPROC(enum_window_callback), 0) + + if found_hwnd: + set_foreground(found_hwnd) + return True + + return False diff --git a/src/core/widgets/yasb/media.py b/src/core/widgets/yasb/media.py index 55ccc295b..9fdc22f44 100644 --- a/src/core/widgets/yasb/media.py +++ b/src/core/widgets/yasb/media.py @@ -38,6 +38,7 @@ CloseHandle, GetApplicationUserModelId, OpenProcess, + activate_app_by_aumid, ) from core.validation.widgets.yasb.media import MediaWidgetConfig from core.widgets.base import BaseWidget @@ -188,6 +189,8 @@ def __init__(self, config: MediaWidgetConfig): self.register_callback("toggle_label", self._toggle_label) self._label.show() + self.register_callback("open_media_source", self._open_media_source) + self._label_alt.hide() self._show_alt_label = False @@ -558,6 +561,12 @@ def _toggle_play_pause(self): AnimationManager.animate(self, self.config.animation.type, self.config.animation.duration) _ = self.media.play_pause() + def _open_media_source(self): + if self.config.animation.enabled: + AnimationManager.animate(self, self.config.animation.type, self.config.animation.duration) + if self.current_session and self.current_session.app_id: + activate_app_by_aumid(self.current_session.app_id) + def _on_timeline_properties_changed(self): """Handle timeline property updates.""" if not self.current_session: From 7b44fea5030f586f569dfbe9bd5d72f84cf6ce52 Mon Sep 17 00:00:00 2001 From: Washik Date: Fri, 6 Feb 2026 19:28:42 +0100 Subject: [PATCH 2/4] minimized-window-fix --- src/core/utils/win32/aumid.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/utils/win32/aumid.py b/src/core/utils/win32/aumid.py index 94d39a79d..3f15684a7 100644 --- a/src/core/utils/win32/aumid.py +++ b/src/core/utils/win32/aumid.py @@ -234,7 +234,6 @@ def get_aumid_from_shortcut(shortcut_path: str) -> str | None: store.contents.lpVtbl.contents.Release(store) except Exception: pass - return aumid @@ -248,7 +247,7 @@ def activate_app_by_aumid(aumid: str) -> bool: Returns: True if a window was found and activation was attempted, False otherwise. """ - from core.utils.win32.window_actions import set_foreground + from core.utils.win32.window_actions import force_foreground_focus, restore_window found_hwnd = None @@ -266,7 +265,9 @@ def enum_window_callback(hwnd, _): user32.EnumWindows(WNDENUMPROC(enum_window_callback), 0) if found_hwnd: - set_foreground(found_hwnd) + if user32.IsIconic(found_hwnd): + restore_window(found_hwnd) + force_foreground_focus(found_hwnd) return True return False From f6b944491b06df0a045d1192b381955c474c4ea1 Mon Sep 17 00:00:00 2001 From: Washik Date: Fri, 6 Feb 2026 19:48:55 +0100 Subject: [PATCH 3/4] focus-fix --- src/core/utils/win32/aumid.py | 42 ++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/core/utils/win32/aumid.py b/src/core/utils/win32/aumid.py index 3f15684a7..264910fa7 100644 --- a/src/core/utils/win32/aumid.py +++ b/src/core/utils/win32/aumid.py @@ -247,7 +247,6 @@ def activate_app_by_aumid(aumid: str) -> bool: Returns: True if a window was found and activation was attempted, False otherwise. """ - from core.utils.win32.window_actions import force_foreground_focus, restore_window found_hwnd = None @@ -265,9 +264,46 @@ def enum_window_callback(hwnd, _): user32.EnumWindows(WNDENUMPROC(enum_window_callback), 0) if found_hwnd: + # 1. Restore if minimized if user32.IsIconic(found_hwnd): - restore_window(found_hwnd) - force_foreground_focus(found_hwnd) + # SW_RESTORE = 9 + user32.ShowWindow(found_hwnd, 9) + + # 2. Robust Focus Stealing + # We need to attach to BOTH the current foreground thread (to get permission to switch) + # AND the target thread (to effectively set focus). + + fg_hwnd = user32.GetForegroundWindow() + # We use the existing GetWindowThreadProcessId wrapper but need a dummy PID + dummy_pid = wt.DWORD(0) + fg_tid = GetWindowThreadProcessId(wt.HWND(fg_hwnd), byref(dummy_pid)) + target_tid = GetWindowThreadProcessId(wt.HWND(found_hwnd), byref(dummy_pid)) + my_tid = kernel32.GetCurrentThreadId() + + attached_fg = False + attached_target = False + + try: + # Attach to current foreground thread + if fg_tid != my_tid and fg_tid: + attached_fg = bool(user32.AttachThreadInput(my_tid, fg_tid, True)) + + # Attach to target thread + if target_tid != my_tid and target_tid != fg_tid and target_tid: + attached_target = bool(user32.AttachThreadInput(my_tid, target_tid, True)) + + # Perform activation + user32.SetForegroundWindow(found_hwnd) + user32.SetFocus(found_hwnd) + user32.BringWindowToTop(found_hwnd) + + finally: + # Detach + if attached_target: + user32.AttachThreadInput(my_tid, target_tid, False) + if attached_fg: + user32.AttachThreadInput(my_tid, fg_tid, False) + return True return False From b1d6cb2f09dff3541214b019b72b8c412f0757ef Mon Sep 17 00:00:00 2001 From: Washik Date: Mon, 9 Feb 2026 02:17:31 +0100 Subject: [PATCH 4/4] use window_actions --- src/core/utils/win32/aumid.py | 101 +++++++++++++++++++-------------- src/core/widgets/yasb/media.py | 37 +++++++++--- 2 files changed, 87 insertions(+), 51 deletions(-) diff --git a/src/core/utils/win32/aumid.py b/src/core/utils/win32/aumid.py index 264910fa7..7388c926a 100644 --- a/src/core/utils/win32/aumid.py +++ b/src/core/utils/win32/aumid.py @@ -66,13 +66,25 @@ class PROPVARIANT(ctypes.Structure): class IPropertyStoreVtbl(ctypes.Structure): _fields_ = [ - ("QueryInterface", WINFUNCTYPE(ctypes.c_long, c_void_p, POINTER(GUID), POINTER(c_void_p))), + ( + "QueryInterface", + WINFUNCTYPE(ctypes.c_long, c_void_p, POINTER(GUID), POINTER(c_void_p)), + ), ("AddRef", WINFUNCTYPE(ctypes.c_ulong, c_void_p)), ("Release", WINFUNCTYPE(ctypes.c_ulong, c_void_p)), ("GetCount", WINFUNCTYPE(ctypes.c_long, c_void_p, POINTER(ctypes.c_uint))), - ("GetAt", WINFUNCTYPE(ctypes.c_long, c_void_p, ctypes.c_uint, POINTER(PROPERTYKEY))), - ("GetValue", WINFUNCTYPE(ctypes.c_long, c_void_p, POINTER(PROPERTYKEY), POINTER(PROPVARIANT))), - ("SetValue", WINFUNCTYPE(ctypes.c_long, c_void_p, POINTER(PROPERTYKEY), POINTER(PROPVARIANT))), + ( + "GetAt", + WINFUNCTYPE(ctypes.c_long, c_void_p, ctypes.c_uint, POINTER(PROPERTYKEY)), + ), + ( + "GetValue", + WINFUNCTYPE(ctypes.c_long, c_void_p, POINTER(PROPERTYKEY), POINTER(PROPVARIANT)), + ), + ( + "SetValue", + WINFUNCTYPE(ctypes.c_long, c_void_p, POINTER(PROPERTYKEY), POINTER(PROPVARIANT)), + ), ("Commit", WINFUNCTYPE(ctypes.c_long, c_void_p)), ] @@ -92,7 +104,13 @@ class IPropertyStore(ctypes.Structure): # SHGetPropertyStoreFromParsingName - to read properties from files (shortcuts) SHGetPropertyStoreFromParsingName = shell32.SHGetPropertyStoreFromParsingName -SHGetPropertyStoreFromParsingName.argtypes = [wt.LPCWSTR, c_void_p, ctypes.c_uint32, POINTER(GUID), POINTER(c_void_p)] +SHGetPropertyStoreFromParsingName.argtypes = [ + wt.LPCWSTR, + c_void_p, + ctypes.c_uint32, + POINTER(GUID), + POINTER(c_void_p), +] SHGetPropertyStoreFromParsingName.restype = ctypes.c_long # PropVariantClear is exported by Ole32.dll @@ -236,17 +254,21 @@ def get_aumid_from_shortcut(shortcut_path: str) -> str | None: pass return aumid + return aumid + -def activate_app_by_aumid(aumid: str) -> bool: +def activate_app_by_aumid(aumid: str, fallback_process_name: str | None = None) -> bool: """ Find and activate validity window for the given AUMID. Args: aumid: The App User Model ID to find. + fallback_process_name: Optional process name (e.g. "firefox.exe") to match if AUMID fails. Returns: True if a window was found and activation was attempted, False otherwise. """ + from core.utils.win32.window_actions import force_foreground_focus, restore_window found_hwnd = None @@ -255,54 +277,45 @@ def activate_app_by_aumid(aumid: str) -> bool: def enum_window_callback(hwnd, _): nonlocal found_hwnd if user32.IsWindowVisible(hwnd): + # 1. Try exact AUMID match curr_aumid = get_aumid_for_window(hwnd) if curr_aumid == aumid: found_hwnd = hwnd return False # Stop enumeration + + # 2. Fallback: Try process name match if provided + if fallback_process_name: + pid = wt.DWORD(0) + GetWindowThreadProcessId(wt.HWND(hwnd), byref(pid)) + if pid.value: + # We need to open the process to get its image name + # PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + hProcess = OpenProcess(0x1000, False, pid.value) + if hProcess: + try: + buf = ctypes.create_unicode_buffer(1024) + size = wt.DWORD(1024) + # QueryFullProcessImageNameW is in kernel32 + if hasattr(kernel32, "QueryFullProcessImageNameW"): + if kernel32.QueryFullProcessImageNameW(hProcess, 0, buf, byref(size)): + full_path = buf.value + if full_path.lower().endswith(fallback_process_name.lower()): + found_hwnd = hwnd + return False + finally: + CloseHandle(hProcess) + return True user32.EnumWindows(WNDENUMPROC(enum_window_callback), 0) if found_hwnd: - # 1. Restore if minimized - if user32.IsIconic(found_hwnd): - # SW_RESTORE = 9 - user32.ShowWindow(found_hwnd, 9) - - # 2. Robust Focus Stealing - # We need to attach to BOTH the current foreground thread (to get permission to switch) - # AND the target thread (to effectively set focus). - - fg_hwnd = user32.GetForegroundWindow() - # We use the existing GetWindowThreadProcessId wrapper but need a dummy PID - dummy_pid = wt.DWORD(0) - fg_tid = GetWindowThreadProcessId(wt.HWND(fg_hwnd), byref(dummy_pid)) - target_tid = GetWindowThreadProcessId(wt.HWND(found_hwnd), byref(dummy_pid)) - my_tid = kernel32.GetCurrentThreadId() + # 1. Force Focus (helps switch workspace) + force_foreground_focus(found_hwnd) - attached_fg = False - attached_target = False - - try: - # Attach to current foreground thread - if fg_tid != my_tid and fg_tid: - attached_fg = bool(user32.AttachThreadInput(my_tid, fg_tid, True)) - - # Attach to target thread - if target_tid != my_tid and target_tid != fg_tid and target_tid: - attached_target = bool(user32.AttachThreadInput(my_tid, target_tid, True)) - - # Perform activation - user32.SetForegroundWindow(found_hwnd) - user32.SetFocus(found_hwnd) - user32.BringWindowToTop(found_hwnd) - - finally: - # Detach - if attached_target: - user32.AttachThreadInput(my_tid, target_tid, False) - if attached_fg: - user32.AttachThreadInput(my_tid, fg_tid, False) + # 2. Restore if minimized (now that it's potentially active/on current workspace) + if user32.IsIconic(found_hwnd): + restore_window(found_hwnd) return True diff --git a/src/core/widgets/yasb/media.py b/src/core/widgets/yasb/media.py index 9fdc22f44..371f359d9 100644 --- a/src/core/widgets/yasb/media.py +++ b/src/core/widgets/yasb/media.py @@ -22,7 +22,12 @@ ) from qasync import asyncSlot # type: ignore -from core.utils.utilities import PopupWidget, ScrollingLabel, add_shadow, refresh_widget_style +from core.utils.utilities import ( + PopupWidget, + ScrollingLabel, + add_shadow, + refresh_widget_style, +) from core.utils.widgets.animation_manager import AnimationManager from core.utils.widgets.media.aumid_process import get_process_name_for_aumid from core.utils.widgets.media.media import MediaSession, SessionState, WindowsMedia @@ -248,7 +253,8 @@ def show_menu(self): self._popup_thumbnail_label.setProperty("class", "thumbnail") self._popup_thumbnail_label.setContentsMargins(0, 0, 0, 0) self._popup_thumbnail_label.setFixedSize( - self.config.media_menu.thumbnail_size, self.config.media_menu.thumbnail_size + self.config.media_menu.thumbnail_size, + self.config.media_menu.thumbnail_size, ) try: # Use thumbnail if available, otherwise create a default one @@ -438,7 +444,10 @@ def show_menu(self): # Initialize slider position if self.current_session is not None and self.current_session.duration > 0: - percent = min(1000, int((self.current_session.current_pos / self.current_session.duration) * 1000)) + percent = min( + 1000, + int((self.current_session.current_pos / self.current_session.duration) * 1000), + ) self._progress_slider.setValue(percent) else: self._progress_slider.setValue(0) @@ -565,7 +574,13 @@ def _open_media_source(self): if self.config.animation.enabled: AnimationManager.animate(self, self.config.animation.type, self.config.animation.duration) if self.current_session and self.current_session.app_id: - activate_app_by_aumid(self.current_session.app_id) + # Try to get process name from mapping for fallback + fallback_process = None + mapping = get_source_app_mapping(self.current_session.app_id) + if mapping and isinstance(mapping, dict): + fallback_process = mapping.get("process") + + activate_app_by_aumid(self.current_session.app_id, fallback_process_name=fallback_process) def _on_timeline_properties_changed(self): """Handle timeline property updates.""" @@ -597,7 +612,10 @@ def _update_interpolated_position(self): # Update widget progress bar first (hide progress bar if duration is too long) if self.current_session.timeline_enabled and (0 < self.current_session.duration < MAX_TIMLINE_DURATION): self._progress_bar.setHidden(False) - new_pos = min(1000, int((self.current_session.current_pos / self.current_session.duration) * 1000)) + new_pos = min( + 1000, + int((self.current_session.current_pos / self.current_session.duration) * 1000), + ) self._progress_bar.setValue(new_pos) else: self._progress_bar.setHidden(True) @@ -889,7 +907,10 @@ def _create_empty_thumbnail(self): # Draw the note head (circle) draw.ellipse( - [(head_x - head_radius, head_y - head_radius), (head_x + head_radius, head_y + head_radius)], + [ + (head_x - head_radius, head_y - head_radius), + (head_x + head_radius, head_y + head_radius), + ], fill=note_color, ) @@ -1378,7 +1399,9 @@ def mousePressEvent(self, ev: QMouseEvent | None): return if self.parent_widget.config.animation.enabled: AnimationManager.animate( - self, self.parent_widget.config.animation.type, self.parent_widget.config.animation.duration + self, + self.parent_widget.config.animation.type, + self.parent_widget.config.animation.duration, ) self.parent_widget.execute_code(self.data)