""" cef_browser.py v2 — Embedded Chromium INSIDE Kivy's SDL2 window v1 created a separate Win32 window (same as external Chrome). v2 creates CEF as a **child window** of Kivy's SDL_app window: - No separate taskbar entry - No z-order fighting - No desktop flash - CEF message loop pumped via Kivy Clock (main thread) """ import ctypes import os from pathlib import Path try: from cefpython3 import cefpython as cef CEF_AVAILABLE = True except ImportError: CEF_AVAILABLE = False WS_CHILD = 0x40000000 WS_VISIBLE = 0x10000000 WS_CLIPSIBLINGS = 0x04000000 WS_CLIPCHILDREN = 0x02000000 SW_HIDE = 0 SW_SHOWNORMAL = 1 class CefBrowser: def __init__(self): self._browser = None self._cef_initialized = False self._child_hwnd = None self._kivy_hwnd = None self._clock_event = None self._showing = False # ── Public API ────────────────────────────────────────────────── def show(self, url): if not CEF_AVAILABLE: return False if not self._cef_initialized: self._init_cef() if self._browser is not None: self._browser.Navigate(url) self._show_in_kivy() return True return self._create_embedded(url) def hide(self): self._showing = False if self._clock_event is not None: try: from kivy.clock import Clock Clock.unschedule(self._clock_event) except Exception: pass self._clock_event = None if self._child_hwnd: try: ctypes.windll.user32.ShowWindow(self._child_hwnd, SW_HIDE) except Exception: pass if self._browser is not None: try: self._browser.CloseBrowser(True) except Exception: pass self._browser = None if self._child_hwnd: try: ctypes.windll.user32.DestroyWindow(self._child_hwnd) except Exception: pass self._child_hwnd = None def shutdown(self): self.hide() if self._cef_initialized: try: cef.Shutdown() except Exception: pass self._cef_initialized = False def navigate(self, url): if self._browser is not None: self._browser.Navigate(url) def is_showing(self): return self._showing def resize(self, width, height): """Called when Kivy window resizes — repositions CEF child.""" if self._child_hwnd: ctypes.windll.user32.SetWindowPos( self._child_hwnd, 0, 0, 0, width, height, 0x0004 ) if self._browser: self._browser.SetBounds(0, 0, width, height) # ── Internal ──────────────────────────────────────────────────── def _init_cef(self): settings = { "multi_threaded_message_loop": False, "single_process": True, "log_severity": cef.LOGSEVERITY_WARNING, "user_agent": "Mozilla/5.0 KiwySignage/1.0", "cache_path": str( Path(os.environ.get("KIWY_DATA_DIR", ".")) / ".cef_cache" ), } cef.Initialize(settings=settings) self._cef_initialized = True def _get_kivy_hwnd(self): if self._kivy_hwnd is not None: return self._kivy_hwnd try: import win32gui hwnd = win32gui.FindWindow("SDL_app", None) if hwnd: self._kivy_hwnd = hwnd return hwnd except Exception: pass return None def _create_embedded(self, url): kivy_hwnd = self._get_kivy_hwnd() if not kivy_hwnd: return False user32 = ctypes.windll.user32 rect = (ctypes.c_long * 4)() user32.GetClientRect(kivy_hwnd, ctypes.byref(rect)) w, h = rect[2], rect[3] hinstance = ctypes.windll.kernel32.GetModuleHandleW(None) self._child_hwnd = user32.CreateWindowExW( 0, b'#32770', b'', WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, 0, 0, w, h, kivy_hwnd, 0, hinstance, 0, ) if not self._child_hwnd: return False winfo = cef.WindowInfo() winfo.SetAsChild(self._child_hwnd, [0, 0, w, h]) self._browser = cef.CreateBrowserSync( window_info=winfo, settings={"background_color": 0x00000000}, url=url, ) self._showing = True self._show_in_kivy() self._start_clock_pump() return True def _show_in_kivy(self): if not self._child_hwnd: return kivy_hwnd = self._get_kivy_hwnd() if kivy_hwnd: user32 = ctypes.windll.user32 rect = (ctypes.c_long * 4)() user32.GetClientRect(kivy_hwnd, ctypes.byref(rect)) user32.SetWindowPos( self._child_hwnd, 0, 0, 0, rect[2], rect[3], 0x0004 ) ctypes.windll.user32.ShowWindow(self._child_hwnd, SW_SHOWNORMAL) self._showing = True def _start_clock_pump(self): if self._clock_event is not None: return def _pump(dt): if self._cef_initialized: try: cef.MessageLoopWork() except Exception: pass if self._showing: from kivy.clock import Clock self._clock_event = Clock.schedule_once(_pump, 0.01) from kivy.clock import Clock self._clock_event = Clock.schedule_once(_pump, 0)