working windows module

This commit is contained in:
ske087
2026-07-31 15:40:59 +03:00
parent d0ea94447a
commit 5c2b3f545f
4 changed files with 556 additions and 181 deletions
+199 -27
View File
@@ -258,33 +258,101 @@ class _Win32Overlay:
cls._hwnd = None
def _bring_kivy_to_front():
"""Bring the Kivy/SDL window to foreground using win32gui.
# Win32 constants used directly (avoid `import win32con` — win32con is a
# pure-Python module in win32\\lib\\ that PyInstaller does NOT bundle because
# it is only reachable through the pywin32.pth file, which frozen apps ignore).
_SW_SHOWNORMAL = 1
_SW_MINIMIZE = 6
_SW_RESTORE = 9
_SWP_NOSIZE = 0x0001
_SWP_NOMOVE = 0x0002
_SWP_NOACTIVATE = 0x0010
_SWP_SHOWWINDOW = 0x0040
_HWND_TOPMOST = -1
_HWND_NOTOPMOST = -2
_GWL_EXSTYLE = -20
_WS_EX_TOPMOST = 0x00000008
Unlike Window.raise_window(), win32gui.SetForegroundWindow
actually works reliably on Windows — it uses the same Win32
API that the Task Manager uses.
def _bring_hwnd_to_front(hwnd):
"""Force a Win32 window to the foreground using only ctypes.
IMPORTANT: Windows restricts SetForegroundWindow() — a process can only
set the foreground window if it was the *last input process* or the
current foreground window is the same thread. To work around this, we
attach our calling thread (and the target window's thread) to the current
foreground window's input thread before calling SetForegroundWindow.
"""
if not hwnd:
return
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
# If minimized, restore first so the window can actually be shown.
if user32.IsIconic(hwnd):
user32.ShowWindow(hwnd, _SW_RESTORE)
try:
fore_hwnd = user32.GetForegroundWindow()
if fore_hwnd and fore_hwnd != hwnd:
fore_tid = user32.GetWindowThreadProcessId(fore_hwnd, None)
target_tid = user32.GetWindowThreadProcessId(hwnd, None)
our_tid = kernel32.GetCurrentThreadId()
if fore_tid != our_tid:
user32.AttachThreadInput(our_tid, fore_tid, True)
user32.AttachThreadInput(target_tid, fore_tid, True)
user32.SetForegroundWindow(hwnd)
user32.AttachThreadInput(target_tid, fore_tid, False)
user32.AttachThreadInput(our_tid, fore_tid, False)
else:
user32.SetForegroundWindow(hwnd)
else:
user32.SetForegroundWindow(hwnd)
except Exception:
pass
user32.ShowWindow(hwnd, _SW_SHOWNORMAL)
user32.BringWindowToTop(hwnd)
user32.SetWindowPos(hwnd, _HWND_TOPMOST, 0, 0, 0, 0, _SWP_NOMOVE | _SWP_NOSIZE)
user32.SetWindowPos(hwnd, _HWND_NOTOPMOST, 0, 0, 0, 0, _SWP_NOMOVE | _SWP_NOSIZE)
def _find_kivy_hwnd():
"""Return the HWND of the Kivy/SDL window, or None."""
try:
import win32gui
import win32con
except Exception:
return None
hwnd_list = []
def _enum_cb(hwnd, hwnd_list):
def _enum_cb(hwnd, _):
try:
cls = win32gui.GetClassName(hwnd)
title = win32gui.GetWindowText(hwnd)
if cls == "SDL_app":
hwnd_list.append(hwnd)
elif "Kiwy" in title or "Signage" in title:
hwnd_list.append(hwnd)
except Exception:
return
if cls == "SDL_app" or "Kiwy" in title or "Signage" in title:
hwnd_list.append(hwnd)
hwnd_list = []
win32gui.EnumWindows(_enum_cb, hwnd_list)
try:
win32gui.EnumWindows(_enum_cb, None)
except Exception:
pass
return hwnd_list[-1] if hwnd_list else None
if hwnd_list:
kivy_hwnd = hwnd_list[-1] # most recent
win32gui.ShowWindow(kivy_hwnd, win32con.SW_SHOWNORMAL)
win32gui.SetForegroundWindow(kivy_hwnd)
win32gui.BringWindowToTop(kivy_hwnd)
def _bring_kivy_to_front():
"""Bring the Kivy/SDL window to the foreground.
Uses win32gui.EnumWindows to find the SDL_app window, then _bring_hwnd_to_front
(ctypes-only) to force it forward — no dependency on the un-bundled
`win32con` module. Falls back to Kivy's built-in raise_window().
"""
try:
hwnd = _find_kivy_hwnd()
if hwnd is None:
return
_bring_hwnd_to_front(hwnd)
except Exception:
# Fallback to Kivy's built-in raise
try:
@@ -295,6 +363,58 @@ def _bring_kivy_to_front():
pass
def _bring_chrome_to_front(proc):
"""Find the top-level window of a launched Chrome/Edge process and bring
it to the foreground (so the weblink is actually visible over Kivy)."""
if proc is None:
return
try:
import win32gui
import win32process
except Exception:
return
target_pid = proc.pid
chrome_hwnd = None
def _enum_cb(hwnd, _):
nonlocal chrome_hwnd
if chrome_hwnd is not None:
return
try:
_, pid = win32process.GetWindowThreadProcessId(hwnd)
except Exception:
return
if pid != target_pid:
return
try:
cls = win32gui.GetClassName(hwnd)
except Exception:
return
# Chrome's top-level window is class 'Chrome_WidgetWin_1' (or 0)
if cls in ('Chrome_WidgetWin_1', 'Chrome_WidgetWin_0', 'ApplicationFrameWindow'):
if win32gui.IsWindowVisible(hwnd):
chrome_hwnd = hwnd
try:
win32gui.EnumWindows(_enum_cb, None)
except Exception:
pass
if chrome_hwnd is not None:
_bring_hwnd_to_front(chrome_hwnd)
else:
# Give the browser a moment to create its window, then retry once.
import time
time.sleep(0.3)
try:
win32gui.EnumWindows(_enum_cb, None)
except Exception:
pass
if chrome_hwnd is not None:
_bring_hwnd_to_front(chrome_hwnd)
def _windows_kill_process_tree(proc):
"""Kill a process AND all its children using taskkill.
@@ -422,18 +542,36 @@ def _patch_main():
Logger.info(f"SignagePlayer: Opening weblink via subprocess: {url} ({browser})")
self._kill_weblink_preload()
# Hide Kivy content
# Hide Kivy content (do NOT minimize — that makes it impossible
# to reliably bring Kivy back to foreground after Chrome closes).
from kivy.core.window import Window as KivyWindow
try:
self.ids.content_area.opacity = 0
KivyWindow.minimize()
except Exception:
pass
_Win32Overlay.show()
# CRITICAL: use a dedicated --user-data-dir. Without it, Chrome
# hands the URL to the existing browser process and this launched
# process exits immediately (poll() != None), so the watchdog
# advances instantly and the weblink never displays. A private
# profile also guarantees a brand-new top-level window we can
# track, bring to front, and taskkill without touching the user's
# normal browser session.
profile_dir = os.path.join(
os.environ.get('KIWY_DATA_DIR', os.getcwd()),
'.kiosk-profile'
)
try:
os.makedirs(profile_dir, exist_ok=True)
except Exception:
pass
self._weblink_proc = subprocess.Popen([
browser,
'--user-data-dir=' + profile_dir,
'--kiosk',
'--new-window',
'--start-maximized',
'--start-fullscreen',
@@ -453,13 +591,15 @@ def _patch_main():
url,
], shell=False)
# Hide the black overlay, then bring CHROME to the front — NOT
# Kivy. Kivy is a borderless fullscreen window; if we raise Kivy
# here the weblink would open *behind* it and never be visible.
weblink_proc = self._weblink_proc
def _hide_overlay(dt):
_Win32Overlay.hide()
try:
KivyWindow.raise_window()
except Exception:
pass
Clock.schedule_once(_hide_overlay, 1.5)
_bring_chrome_to_front(weblink_proc)
Clock.schedule_once(_hide_overlay, 1.0)
Clock.unschedule(self.next_media)
self._start_inactivity_watchdog(duration)
@@ -528,19 +668,31 @@ def _patch_main():
# ── Patch kill_weblink_after_frame for both CEF and subprocess ──
def _windows_kill_weblink_after_frame(self):
"""Close the weblink (CEF or subprocess) immediately before next media."""
"""Close the weblink (CEF or subprocess) immediately before next media.
Restores Kivy content visibility and brings the Kivy window to front
in all cases.
"""
import time
from kivy.logger import Logger
from kivy.clock import Clock
from kivy.core.window import Window as _KivyWindow
self._stop_inactivity_watchdog()
self._kill_weblink_preload()
# Restore Kivy content visibility
try:
self.ids.content_area.opacity = 1
except Exception:
pass
# Try CEF first
cef_browser = _get_cef_browser()
if cef_browser is not None and cef_browser.is_showing():
Logger.info("SignagePlayer: Hiding CEF embedded browser")
cef_browser.hide()
self._weblink_proc = None
_bring_kivy_to_front()
return
# Fallback: subprocess Chrome
@@ -548,6 +700,7 @@ def _patch_main():
self._weblink_proc = None
if proc is None or proc.poll() is not None:
_bring_kivy_to_front()
return
_Win32Overlay.show()
@@ -562,13 +715,32 @@ def _patch_main():
_original_play_current = signage_main.SignagePlayer.play_current_media
def _windows_play_current_media(self, force_reload=False, _after_weblink=False):
"""Wrapped play_current_media — closes weblink immediately on transition."""
"""Wrapped play_current_media — closes weblink immediately on transition.
CRITICAL: Must restore content_area.opacity=1 BEFORE killing the browser,
because the original play_current_media() skips the weblink→media transition
block once self._weblink_proc is None. If opacity stays 0, the next widget
renders but is invisible.
"""
if not _after_weblink:
# Restore Kivy content visibility BEFORE killing the browser so the
# original play_current_media() doesn't need to handle the transition.
try:
self.ids.content_area.opacity = 1
except Exception:
pass
try:
from kivy.core.window import Window as _KivyWindow
_KivyWindow.show()
except Exception:
pass
# Kill CEF browser if showing
cef_browser = _get_cef_browser()
if cef_browser is not None and cef_browser.is_showing():
cef_browser.hide()
self._weblink_proc = None
_bring_kivy_to_front()
# Kill subprocess Chrome if running
proc = self._weblink_proc