working windows module
This commit is contained in:
+199
-27
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user