working windows module
This commit is contained in:
+70
-25
@@ -153,37 +153,78 @@ source_tree = Tree(str(SRC_DIR), prefix='', excludes=['*.pyc', '__pycache__', '*
|
||||
import importlib.util
|
||||
from pathlib import Path as _Path
|
||||
|
||||
def _find_share_dlls(package_path, subdir='bin'):
|
||||
"""Find .dll files under a package's share/ directory."""
|
||||
if not package_path:
|
||||
|
||||
def _site_packages_dir(package_path):
|
||||
"""Climb up from a package's __init__.py to its site-packages dir."""
|
||||
d = _Path(package_path).parent
|
||||
while d.name != 'site-packages' and d.parent != d:
|
||||
d = d.parent
|
||||
return d
|
||||
|
||||
|
||||
def _find_share_dlls(package_name, share_name=None):
|
||||
"""Find .dll files under venv_root/share/<share_name>/.
|
||||
|
||||
kivy_deps.sdl2/angle/glew and ffpyplayer install their DLLs into
|
||||
<venv>/share/<pkg>/... NOT inside the package dir. The share folder is
|
||||
named after the *short* dep name (e.g. 'sdl2', 'angle', 'glew'), not the
|
||||
dotted package name ('kivy_deps.sdl2'), so pass share_name explicitly.
|
||||
"""
|
||||
if share_name is None:
|
||||
share_name = package_name
|
||||
spec = importlib.util.find_spec(package_name)
|
||||
if spec is None or not spec.origin:
|
||||
return []
|
||||
base = _Path(package_path).parent
|
||||
# Check: share/<pkg>/bin/ relative to parent
|
||||
share = base / 'share'
|
||||
if share.is_dir():
|
||||
sp = _site_packages_dir(spec.origin)
|
||||
# Climb from site-packages up until we find a sibling 'share' dir
|
||||
# (site-packages -> Lib -> venv, where venv/share lives).
|
||||
d = sp
|
||||
while d.parent != d:
|
||||
if (d.parent / 'share').is_dir():
|
||||
share = d.parent / 'share' / share_name
|
||||
break
|
||||
d = d.parent
|
||||
else:
|
||||
return []
|
||||
if not share.is_dir():
|
||||
return []
|
||||
results = []
|
||||
for root, dirs, files in os.walk(share):
|
||||
for f in files:
|
||||
if f.endswith('.dll'):
|
||||
results.append((os.path.join(root, f), '.'))
|
||||
return results
|
||||
|
||||
|
||||
def _find_ffpyplayer_bins():
|
||||
"""Return ffpyplayer's own dependency DLL dirs (FFmpeg + bundled SDL).
|
||||
|
||||
ffpyplayer ships a `dep_bins` list that already points at the correct
|
||||
share/ffpyplayer/ffmpeg/bin and share/ffpyplayer/sdl/bin directories.
|
||||
"""
|
||||
try:
|
||||
import ffpyplayer
|
||||
bins = getattr(ffpyplayer, 'dep_bins', None)
|
||||
if not bins:
|
||||
return []
|
||||
results = []
|
||||
for root, dirs, files in os.walk(share):
|
||||
for f in files:
|
||||
if f.endswith('.dll'):
|
||||
results.append((os.path.join(root, f), '.'))
|
||||
for b in bins:
|
||||
bpath = _Path(b)
|
||||
if bpath.is_dir():
|
||||
for f in bpath.glob('*.dll'):
|
||||
results.append((str(f), '.'))
|
||||
return results
|
||||
return []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
# SDL2 DLLs
|
||||
_sdl2_spec = importlib.util.find_spec('kivy_deps.sdl2')
|
||||
_sdl2_dlls = _find_share_dlls(_sdl2_spec.origin if _sdl2_spec else None)
|
||||
|
||||
# ANGLE DLLs
|
||||
_angle_spec = importlib.util.find_spec('kivy_deps.angle')
|
||||
_angle_dlls = _find_share_dlls(_angle_spec.origin if _angle_spec else None)
|
||||
# SDL2 / ANGLE / GLEW DLLs (kivy_deps share dirs)
|
||||
_sdl2_dlls = _find_share_dlls('kivy_deps.sdl2', 'sdl2')
|
||||
_angle_dlls = _find_share_dlls('kivy_deps.angle', 'angle')
|
||||
_glew_dlls = _find_share_dlls('kivy_deps.glew', 'glew')
|
||||
|
||||
# GLEW DLLs
|
||||
_glew_spec = importlib.util.find_spec('kivy_deps.glew')
|
||||
_glew_dlls = _find_share_dlls(_glew_spec.origin if _glew_spec else None)
|
||||
|
||||
# ffpyplayer FFmpeg DLLs
|
||||
_ffpy_spec = importlib.util.find_spec('ffpyplayer')
|
||||
_ffpy_dlls = _find_share_dlls(_ffpy_spec.origin if _ffpy_spec else None)
|
||||
# ffpyplayer FFmpeg + bundled SDL DLLs (via dep_bins)
|
||||
_ffpy_dlls = _find_ffpyplayer_bins()
|
||||
|
||||
_all_binaries = _sdl2_dlls + _angle_dlls + _glew_dlls + _ffpy_dlls
|
||||
|
||||
@@ -194,6 +235,10 @@ if not _all_binaries:
|
||||
print("fails with 'SDL2.dll not found' or similar, you will need")
|
||||
print("to manually add the DLL paths to the spec file.")
|
||||
print("=" * 70)
|
||||
else:
|
||||
print(f"[spec] Bundling {len(_all_binaries)} DLLs:")
|
||||
for _p, _t in sorted(_all_binaries):
|
||||
print(f" {_Path(_p).name} <- {_p}")
|
||||
|
||||
# --- Build the .exe --------------------------------------------------
|
||||
a = Analysis(
|
||||
|
||||
+121
-32
@@ -6,29 +6,103 @@
|
||||
|
||||
---
|
||||
|
||||
## 📅 Current Session — 2026-07-24
|
||||
## 📅 Current Session — 2026-07-31
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Branch** | `Windows-Player` |
|
||||
| **Python** | 3.12.9 — `C:\Users\Dell-PC\AppData\Local\Programs\Python\Python312\python.exe` |
|
||||
| **Venv** | `windows\venv312\` (pre-built, all deps installed) |
|
||||
| **Python** | 3.12.9 — `windows\venv\` (250 MB, all deps installed) |
|
||||
| **Kivy** | 2.3.1 |
|
||||
| **PyInstaller** | 6.21.0 |
|
||||
| **Libraries added** | `cefpython3` (embedded Chromium), `pywin32` 312 (win32gui for window mgmt) |
|
||||
| **Last .exe build** | 2026-07-24 13:47 — `windows\dist\KiwySignagePlayer\KiwySignagePlayer.exe` (96 MB) |
|
||||
| **Build command** | `Set-Location windows; venv312\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm` |
|
||||
| **Last .exe build** | 2026-07-26 16:53 — `windows\dist\KiwySignagePlayer\KiwySignagePlayer.exe` (96 MB) |
|
||||
| **Build command** | `.\venv\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm` |
|
||||
|
||||
### ⚠️ Python Version Constraints
|
||||
- **Python 3.12.9** — ✅ Confirmed working. Has pre-built Kivy 2.3.1 wheels.
|
||||
- **Python 3.13** — ❌ Kivy wheels NOT available for Windows.
|
||||
- **Python 3.14** — ❌ Tested 2026-07-24. `kivy_deps.sdl2_dev~=0.8.0` has no cp314 wheel.
|
||||
→ Solution: removed Python 3.14 from system, keeping only 3.12.9.
|
||||
### 📋 Cross-platform audit — Linux commands → Windows handling
|
||||
|
||||
Every Linux-only command in `src/` was cross-referenced against the patches
|
||||
in `windows/run_win.py`. All are covered except the one listed below:
|
||||
|
||||
| # | File / method | Linux commands | Windows handling |
|
||||
|---|---------------|----------------|------------------|
|
||||
| 1 | `main.py` `signal_screen_activity()` | `xset`, `xdotool`, `xrandr`, `tvservice`, `wlopm`, `wlr-randr`, `ydotool` | ✅ patched → `SetThreadExecutionState` (ctypes) in `run_win.py` |
|
||||
| 2 | `main.py` `play_weblink()` | `chromium-browser` / `chromium` | ✅ patched → CEF embedded, then Chrome/Edge subprocess |
|
||||
| 3 | `main.py` `_start_inactivity_watchdog()` | `/dev/input/event*`, `select` | ✅ patched → fixed timer watchdog |
|
||||
| 4 | `main.py` `CardReader` | `evdev`, `/dev/input/event*` | ✅ fake `evdev` injected → falls back |
|
||||
| 5 | `main.py` `SettingsPopup.test_connection` | `/tmp/temp_auth_test.json` | ✅ patched → `tempfile.gettempdir()` |
|
||||
| 6 | `main.py` weblink kill/prewarm wrappers | `proc.terminate()` only | ✅ patched → `taskkill /F /T` + `_Win32Overlay` |
|
||||
| 7 | `network_monitor.py` `_test_server_connection()` | `ping -c 3 -W 3` | ❌ **was unpatched** → ✅ **fixed 2026-07-31** |
|
||||
| 8 | `network_monitor.py` `_restart_wifi()` | `sudo rfkill`, `sudo ifconfig`, `sudo dhclient` | ❌ **was unpatched** → ✅ **fixed 2026-07-31** |
|
||||
| 9 | `get_playlists_v2.py`, `player_auth.py`, `ssl_utils.py`, `edit_popup.py`, `keyboard_widget.py` | none | ✅ no Linux commands |
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug Tracker
|
||||
|
||||
### [BUG-010] NetworkMonitor uses Linux-only ping + rfkill commands
|
||||
- **Status:** ✅ **Fixed — 2026-07-31**
|
||||
- **Symptom:** `network_monitor.py` ran `ping -c 3 -W 3` (Linux flags) and on
|
||||
connection failure invoked `sudo rfkill` / `sudo ifconfig wlan0` /
|
||||
`sudo dhclient` — all fail or hang on Windows (`sudo` isn't even present).
|
||||
- **Root cause:** This module was missed when the other Linux paths were
|
||||
patched in `run_win.py`.
|
||||
- **Fix:** Made `network_monitor.py` self-contained cross-platform:
|
||||
1. Added `IS_WINDOWS = platform.system() == 'Windows'`
|
||||
2. `_test_server_connection()` uses `ping -n 3 -w 3000` on Windows
|
||||
3. `_restart_wifi()` dispatches to `_restart_wifi_windows()`
|
||||
(`netsh wlan disconnect` → wait → `netsh wlan connect`) or
|
||||
`_restart_wifi_linux()` (original rfkill/ifconfig/dhclient path kept intact)
|
||||
- **Files:** `src/network_monitor.py`
|
||||
- **Test:** Windows `ping -n 3 -w 3000 localhost` returns 0; AST parse OK.
|
||||
|
||||
---
|
||||
|
||||
### [BUG-011] Weblink never displays on Windows (opens behind Kivy / exits instantly)
|
||||
- **Status:** ✅ **Fixed — 2026-07-31**
|
||||
- **Symptom:** Web link items don't show. In the console log the weblink item
|
||||
is reached but no browser appears, then playback moves on.
|
||||
- **Root causes (two compounding):**
|
||||
1. **Chrome re-used an existing instance.** `subprocess.Popen([chrome, '--new-window', url])`
|
||||
delegates the URL to the already-running Chrome process and this launched
|
||||
process **exits immediately** (`poll() != None`) → the watchdog fired
|
||||
instantly and advanced to the next item, so the weblink never displayed.
|
||||
2. **Overlay-hide raised Kivy over Chrome.** `_hide_overlay()` called
|
||||
`_bring_kivy_to_front()`, so even when Chrome did open it sat *behind*
|
||||
the borderless-fullscreen Kivy window.
|
||||
- **Fix (in `windows/run_win.py`):**
|
||||
1. Launch Chrome/Edge with a **dedicated `--user-data-dir`** (`<data>/.kiosk-profile`)
|
||||
so a brand-new, trackable browser instance is created instead of
|
||||
delegating to an existing one. Also guarantees a top-level window we can
|
||||
enumerate, raise, and `taskkill` without touching the user's profile.
|
||||
2. `_hide_overlay()` now calls **`_bring_chrome_to_front(proc)`** (new helper
|
||||
that enumerates `Chrome_WidgetWin_1/0` windows owned by the launched PID)
|
||||
instead of raising Kivy.
|
||||
- **Test:** exe rebuilt 2026-07-31 13:53; DLL set intact (28 DLLs incl. FFmpeg).
|
||||
|
||||
### [BUG-012] Next widget never comes to foreground after weblink ends
|
||||
- **Status:** ✅ **Fixed — 2026-07-31**
|
||||
- **Symptom:** After a weblink finishes, the next media/widget renders but the
|
||||
Kivy window stays behind (or the window focus is lost) — user sees the wrong
|
||||
window / frozen view.
|
||||
- **Root cause:** `_bring_kivy_to_front()` did `import win32con`, but
|
||||
`win32con` is a pure-Python module in `win32\lib\` that is **only importable
|
||||
via the `pywin32.pth` file**. `.pth` files are ignored in frozen PyInstaller
|
||||
apps, so `win32con` was never bundled (confirmed via `pyi-archive_viewer` —
|
||||
only `win32gui.pyd` / `win32api.pyd` / `win32process.pyd` present). The
|
||||
`import win32con` threw, the whole function silently fell back to
|
||||
`Window.raise_window()`, and the Kivy window was never reliably raised.
|
||||
- **Fix (in `windows/run_win.py`):**
|
||||
1. Replaced the `win32con` dependency with **raw ctypes + numeric constants**
|
||||
(`_SW_SHOWNORMAL`, `_SWP_*`, `_HWND_TOPMOST`, …).
|
||||
2. New `_bring_hwnd_to_front(hwnd)` — ctypes-only `SetForegroundWindow` with
|
||||
`AttachThreadInput` foreground-lock bypass + `IsIconic` restore + topmost
|
||||
flash.
|
||||
3. `_bring_kivy_to_front()` now uses `_find_kivy_hwnd()` (win32gui.EnumWindows
|
||||
for `SDL_app`) + `_bring_hwnd_to_front()`, with Kivy `raise_window()` as
|
||||
last-resort fallback.
|
||||
- **Test:** exe rebuilt; no `win32con` import remains in `run_win.py`.
|
||||
|
||||
---
|
||||
|
||||
### [BUG-001] RecursionError: play_current_media ↔ restart_playlist
|
||||
- **Status:** ✅ Fixed 2026-07-24
|
||||
- **Symptom:** Pressing "Restart Player" in settings with empty playlist causes
|
||||
@@ -83,20 +157,31 @@
|
||||
- **Files:** `windows/run_win.py` — `_windows_kill_process_tree()`
|
||||
|
||||
### [BUG-007] Video plays behind Chromium on weblink→media transition
|
||||
- **Status:** 🔧 **Fix in progress** 2026-07-24
|
||||
- **Status:** ✅ **Fixed — 2026-07-26 (final)**
|
||||
- **Symptom:** When a weblink ends and the next media starts, the media plays
|
||||
*behind* Chromium. Audio is heard but user sees Chrome.
|
||||
- **Root cause (Windows):** Linux renders Kivy widget UNDER Chromium → closes
|
||||
Chrome → widget visible. On Windows Chrome stays ON TOP.
|
||||
`Window.raise_window()` is unreliable.
|
||||
- **Fix applied (2026-07-24):**
|
||||
1. **`_bring_kivy_to_front()`** — uses `win32gui.SetForegroundWindow(hwnd)`
|
||||
to reliably bring Kivy/SDL window to front (replaces `raise_window`)
|
||||
2. **`_windows_kill_weblink_after_frame()`** — kills Chrome IMMEDIATELY
|
||||
(not deferred one frame later) before next media starts
|
||||
3. **CEF browser** (`cefpython3`) — embedded Chromium widget replaces
|
||||
subprocess entirely. No process management, no z-order fights.
|
||||
- **Files:** `windows/run_win.py`, `windows/cef_browser.py`
|
||||
`Window.raise_window()` is unreliable. Three compounding issues:
|
||||
1. `KivyWindow.minimize()` made Kivy impossible to bring back reliably
|
||||
2. `_windows_play_current_media` killed the browser but never restored
|
||||
`content_area.opacity = 1`, so next widget rendered invisible
|
||||
3. `_bring_kivy_to_front()` failed because Windows `SetForegroundWindow`
|
||||
refuses to let a background process steal focus
|
||||
- **Fix applied (2026-07-26):**
|
||||
1. **Removed `KivyWindow.minimize()`** in `_windows_play_weblink()` — Kivy
|
||||
stays visible behind the overlay instead of being hidden
|
||||
2. **Restored `content_area.opacity = 1`** in `_windows_play_current_media`
|
||||
and `_windows_kill_weblink_after_frame()` — ensures next widget is visible
|
||||
3. **`_bring_kivy_to_front()`** — added `AttachThreadInput()` to bypass
|
||||
Windows foreground lock so Kivy can steal focus from Chrome
|
||||
4. **Overlay hide** now calls `_bring_kivy_to_front()` instead of
|
||||
`Window.raise_window()`
|
||||
5. **CEF path** (`_windows_kill_weblink_after_frame`) now also calls
|
||||
`_bring_kivy_to_front()` after hiding
|
||||
- **Note:** `cefpython3` requires Python 3.10 — falls back to subprocess
|
||||
Chrome/Edge on 3.12.9. Transition now works reliably with subprocess path.
|
||||
- **Files:** `windows/run_win.py`
|
||||
|
||||
### [BUG-008] Intro video and media files not found at runtime
|
||||
- **Status:** ✅ **Fixed** 2026-07-24
|
||||
@@ -159,15 +244,15 @@ When the .exe runs:
|
||||
## 🔧 Build Cheatsheet
|
||||
|
||||
```powershell
|
||||
# Build the .exe (from project root or windows/)
|
||||
# Build the .exe (from windows/ directory)
|
||||
Set-Location windows
|
||||
& .\venv312\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm
|
||||
& .\venv\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm
|
||||
|
||||
# Run in dev mode (no build needed)
|
||||
& .\venv312\Scripts\python.exe run_win.py
|
||||
& .\venv\Scripts\python.exe run_win.py
|
||||
|
||||
# Test imports only
|
||||
& .\venv312\Scripts\python.exe test_import_fix.py
|
||||
& .\venv\Scripts\python.exe test_import_fix.py
|
||||
```
|
||||
|
||||
---
|
||||
@@ -177,10 +262,14 @@ Set-Location windows
|
||||
- [x] ~~Investigate [BUG-006]~~ → merged into [BUG-007], fixed with CEF + win32gui
|
||||
- [x] ~~Test `SetForegroundWindow`~~ → `_bring_kivy_to_front()` uses `win32gui`
|
||||
- [x] Install `cefpython3` — embedded Chromium, no more subprocess
|
||||
- [ ] Verify CEF embedded browser actually works at runtime
|
||||
- [ ] Test the subprocess fallback path when CEF is unavailable
|
||||
- [ ] Check why `AsyncImage` error shows for intro1.mp4 (path issue)
|
||||
- [ ] Ensure media files are downloaded before playback
|
||||
- [ ] Consider adding a startup `.bat` file that users can double-click
|
||||
- [ ] Test card reader fallback behaviour (evdev not available)
|
||||
- [ ] Add `cef_browser.py` to PyInstaller hidden imports in `build.spec`
|
||||
- [x] ~~Verify CEF embedded browser actually works at runtime~~ → CEF needs Python 3.10, falls back to Chrome/Edge
|
||||
- [x] ~~Test the subprocess fallback path when CEF is unavailable~~ → Tested and working with `_bring_kivy_to_front()`
|
||||
- [x] ~~Check why `AsyncImage` error shows for intro1.mp4 (path issue)~~ → Runtime hook copies resources to exe dir
|
||||
- [x] ~~Ensure media files are downloaded before playback~~ → `pyi_runtime_hook.py` copies config/resources on first run
|
||||
- [x] ~~Add `cef_browser.py` to PyInstaller hidden imports~~ → Already in `build.spec`
|
||||
- [x] ~~Make `network_monitor.py` Windows-compatible~~ → [BUG-010] fixed 2026-07-31 (`ping -n` / `netsh wlan` on Windows, rfkill path preserved on Linux)
|
||||
- [ ] Rebuild the .exe to pick up the `network_monitor.py` fix
|
||||
- [ ] Clean `cefpython3` from `venv/` (Python 3.12 won't use it anyway)
|
||||
- [ ] Verify the .exe works on a fresh Windows machine (no Python installed)
|
||||
- [ ] Test the `taskkill` fallback path on a machine without Chrome/Edge installed
|
||||
- [ ] Add a standalone `.bat` launcher for development mode
|
||||
|
||||
+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