#! python 3 # -*- coding: utf-8 -*- # SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (C) 2026 Karim Gabriele Varano """ _startup_splash.py Petrol-grüner Splash-Screen waehrend des DOSSIER-Plugin-Startups. Borderless Eto-Form mit WebView + Inline-HTML im selben Stil wie der Launcher-Splash. Bedeckt visuell die 3+ Sekunden waehrend Rhino die Panels registriert + WindowLayout neu anwendet. Wird von startup.py beim ersten Idle gezeigt und nach Layout-Apply (oder Timeout) wieder versteckt. """ import os import time import Rhino import scriptcontext as sc _SPLASH_KEY = "_dossier_startup_splash" _SPLASH_SHOWN_AT_KEY = "_dossier_startup_splash_shown_at" _SAFETY_TIMEOUT_SEC = 12.0 # spaetestens nach 12s wegmachen, falls Hide-Hook nicht feuert # Marker den der Launcher direkt vor `open -a Rhinoceros` schreibt, damit # Plugin-Splash NICHT zusaetzlich zum Launcher-Splash erscheint. _OWNER_MARKER = os.path.expanduser( "~/Library/Application Support/ch.gabrielevarano.Dossier/splash_owner_launcher.flag" ) _OWNER_FRESH_SEC = 30.0 # Stale-Schutz falls Launcher crasht _SPLASH_HTML = ''' Dossier laedt
DOSSIER.
Rhino 8 Plugin
Plugin laedt — Panels werden platziert
AGPL-3.0 · Karim Gabriele Varano CPython 3.9
''' def _try_borderless_mac(form): """Mac-spezifisch: direkter NSWindow-Zugriff via Eto.ControlObject um titlebar/Decorations komplett zu killen. Eto.Mac.Forms.EtoWindow IST-A NSWindow (Xamarin.Mac-Subclass). StyleMask ist ein .NET-Enum-Property — Python.NET 3 verlangt explizite Enum-Konversion (kein impliziter int → Enum cast mehr). Wir leiten den Enum-Typ zur Laufzeit aus dem Getter ab und konstruieren den Borderless-Wert via System.Enum.ToObject.""" nswindow = getattr(form, "ControlObject", None) if nswindow is None: print("[SPLASH] keine ControlObject auf Form") return False print("[SPLASH] ControlObject type:", str(type(nswindow))) import System ok = False # NSWindowStyleMaskBorderless = 0 # NSWindowStyleMaskTitled = 1, FullSizeContentView = 32768 try: current = nswindow.StyleMask style_type = type(current) borderless = System.Enum.ToObject(style_type, 0) nswindow.StyleMask = borderless print("[SPLASH] StyleMask=0 (Borderless) gesetzt") ok = True except Exception as ex: print("[SPLASH] StyleMask Enum:", ex) # Fallback: FullSizeContentView (32768) + TitlebarAppearsTransparent # damit Content unter die (transparente) Titlebar reicht try: current = nswindow.StyleMask style_type = type(current) full = System.Enum.ToObject(style_type, 1 | 32768) nswindow.StyleMask = full print("[SPLASH] StyleMask=Titled|FullSize gesetzt (Fallback)") ok = True except Exception as ex2: print("[SPLASH] StyleMask Fallback:", ex2) # Titlebar transparent + Titel unsichtbar def _set_prop(prop, value, log=False): try: setattr(nswindow, prop, value) if log: print("[SPLASH] {}={} OK".format(prop, value)) return True except Exception as ex: if log: print("[SPLASH] {}:".format(prop), ex) return False _set_prop("TitlebarAppearsTransparent", True, True) # NSWindowTitleHidden = 1 try: tv_type = type(nswindow.TitleVisibility) nswindow.TitleVisibility = System.Enum.ToObject(tv_type, 1) print("[SPLASH] TitleVisibility=Hidden OK") except Exception as ex: print("[SPLASH] TitleVisibility:", ex) _set_prop("IsOpaque", False) _set_prop("HasShadow", True) _set_prop("MovableByWindowBackground", True) # Clear NSWindow background damit rounded corners aus dem HTML sichtbar # werden. Xamarin.Mac exponiert NSColor.Clear als statische Property. try: from AppKit import NSColor as _NSC clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None) if clear is not None: nswindow.BackgroundColor = clear print("[SPLASH] BackgroundColor=Clear OK") except Exception as ex: print("[SPLASH] BackgroundColor Clear:", ex) # Force-Paint: Splash MUSS sichtbar sein BEVOR Rhino den Script-Thread # weiter belegt. Python-Script blockiert sonst die Main-Loop und der # Splash wuerde erst nach Script-Ende paintet werden — viel zu spaet. try: nswindow.OrderFrontRegardless() except Exception: pass try: nswindow.DisplayIfNeeded() except Exception: pass try: nswindow.Display() except Exception: pass return ok def _try_transparent_webview_mac(wv): """WKWebView transparent machen damit der NSWindow-Hintergrund (oder nichts) durchscheint und runde Ecken sichtbar werden. wv.ControlObject ist die WKWebView.""" wk = getattr(wv, "ControlObject", None) if wk is None: print("[SPLASH] WebView: keine ControlObject"); return print("[SPLASH] WebView ControlObject type:", str(type(wk))) # KVC: setValue:forKey:@"drawsBackground" → @NO. Funktioniert sowohl bei # WebView (alt) als auch WKWebView (NSObject KVC). Das ist der zuverlaessige # Weg WebView-Hintergrund komplett zu entfernen, besser als UnderPageBg. try: from Foundation import NSNumber, NSString try: wk.SetValueForKey(NSNumber.FromBoolean(False), NSString("drawsBackground")) print("[SPLASH] WebView drawsBackground=NO via KVC OK") except Exception as ex: print("[SPLASH] KVC drawsBackground:", ex) except Exception as ex: print("[SPLASH] Foundation import:", ex) try: from AppKit import NSColor as _NSC clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None) if clear is not None: try: wk.UnderPageBackgroundColor = clear except Exception: pass try: layer = getattr(wk, "Layer", None) if layer is not None: layer.BackgroundColor = clear.CGColor layer.Opaque = False print("[SPLASH] WebView Layer transparent OK") except Exception as ex: print("[SPLASH] WebView Layer:", ex) except Exception as ex: print("[SPLASH] WebView NSColor:", ex) def _dispatch_to_main(fn): """Fuehrt fn beim naechsten Rhino-Idle-Event aus. Mac Eto/AppKit erfordert UI-Mutationen auf dem Main-Thread; threading.Timer-Callbacks laufen im falschen Thread und Close() crasht oder no-op't dort.""" handler_ref = [None] def _idle(sender, e): try: Rhino.RhinoApp.Idle -= handler_ref[0] except Exception: pass try: fn() except Exception as ex: print("[SPLASH] dispatched fn:", ex) handler_ref[0] = _idle try: Rhino.RhinoApp.Idle += _idle except Exception as ex: print("[SPLASH] idle subscribe:", ex) try: fn() except Exception: pass def _install_safety_timeout(): """Registriert Idle-Handler der periodisch prueft ob _SAFETY_TIMEOUT_SEC erreicht ist. Cleanup-self wenn Splash bereits zu.""" handler_ref = [None] def _idle(sender, e): try: if sc.sticky.get(_SPLASH_KEY) is None: try: Rhino.RhinoApp.Idle -= handler_ref[0] except Exception: pass return shown_at = sc.sticky.get(_SPLASH_SHOWN_AT_KEY) or 0 if shown_at and (time.time() - shown_at) >= _SAFETY_TIMEOUT_SEC: try: Rhino.RhinoApp.Idle -= handler_ref[0] except Exception: pass print("[SPLASH] safety-timeout — auto-hide") try: _hide_main() except Exception: pass except Exception: pass handler_ref[0] = _idle try: Rhino.RhinoApp.Idle += _idle except Exception as ex: print("[SPLASH] safety install:", ex) def _launcher_owns_splash(): """True wenn Launcher direkt vor Rhino-Launch einen frischen Owner- Marker geschrieben hat. Verhindert doppelte Splashes.""" try: if not os.path.isfile(_OWNER_MARKER): return False age = time.time() - os.path.getmtime(_OWNER_MARKER) if age <= _OWNER_FRESH_SEC: return True except Exception: pass return False def show(): """Zeigt den Splash. Idempotent — zweiter Aufruf bringt das bestehende Fenster nur in den Vordergrund. Auto-Hide nach _SAFETY_TIMEOUT_SEC als Fallback via Idle-Polling (NICHT threading.Timer — Mac UI braucht Main-Thread). Skipt wenn Launcher seinen eigenen Splash zeigt.""" if _launcher_owns_splash(): print("[SPLASH] Launcher zeigt eigenen Splash — skip"); return if sc.sticky.get(_SPLASH_KEY) is not None: print("[SPLASH] schon offen — skip"); return try: import Eto.Forms as ef import Eto.Drawing as ed except Exception as ex: print("[SPLASH] Eto-Import:", ex); return try: form = ef.Form() form.Title = "" # leerer Titel hilft bei Mac-Titlebar-Reduktion # Versuche WindowStyle.None (Eto-API, funktioniert nicht immer auf Mac) try: form.WindowStyle = getattr(ef.WindowStyle, "None") except Exception: pass # Alle Window-Chrome-Optionen aus for attr, val in ( ("Resizable", False), ("Minimizable", False), ("Maximizable", False), ("Closeable", False), ("ShowInTaskbar", False), ("Topmost", True), ): try: setattr(form, attr, val) except Exception: pass try: form.Size = ed.Size(420, 160) except Exception: pass # Transparent so dass WebView's eigene rounded gradient sichtbar wird try: form.BackgroundColor = ed.Colors.Transparent except Exception: try: form.BackgroundColor = ed.Color(0.37, 0.66, 0.59) except Exception: pass wv = ef.WebView() try: # WebView selber transparent damit das Form-Hintergrund durchscheint wv.BackgroundColor = ed.Colors.Transparent except Exception: pass try: wv.LoadHtml(_SPLASH_HTML) except Exception as ex: print("[SPLASH] LoadHtml:", ex) form.Content = wv # Center on screen try: screen = ef.Screen.PrimaryScreen sb = screen.Bounds x = int(sb.X + (sb.Width - form.Size.Width) / 2) y = int(sb.Y + (sb.Height - form.Size.Height) / 2 - 100) form.Location = ed.Point(x, y) except Exception as ex: print("[SPLASH] center:", ex) try: form.Show() except Exception as ex: print("[SPLASH] Show:", ex); return # Mac-spezifischer Borderless-Hack — MUSS nach Show() laufen damit # die NSWindow existiert try: if _try_borderless_mac(form): print("[SPLASH] Borderless (Mac NSWindow) angewendet") except Exception as ex: print("[SPLASH] borderless-mac:", ex) # WebView transparent (rounded corners via HTML border-radius) try: _try_transparent_webview_mac(wv) except Exception as ex: print("[SPLASH] webview-clear:", ex) # Event-Loop einmal explizit pumpen damit Splash gepainted wird # bevor das Script weiter blockiert (sonst sieht Nutzer die Panels # zuerst entstehen und Splash erscheint erst danach). try: ef.Application.Instance.RunIteration() except Exception: pass sc.sticky[_SPLASH_KEY] = form sc.sticky[_SPLASH_SHOWN_AT_KEY] = time.time() print("[SPLASH] visible") # Safety-Timeout via Idle-Polling (Main-Thread, Mac-safe) _install_safety_timeout() except Exception as ex: print("[SPLASH] show:", ex) def _hide_main(): """Synchroner Close — MUSS auf Main-Thread laufen. Nur intern aufrufen, extern hide() verwenden.""" form = sc.sticky.get(_SPLASH_KEY) if form is None: return sc.sticky[_SPLASH_KEY] = None sc.sticky[_SPLASH_SHOWN_AT_KEY] = None try: form.Close() except Exception: try: form.Visible = False except Exception as ex: print("[SPLASH] hide visible:", ex) print("[SPLASH] hidden") def hide(): """Versteckt + entsorgt den Splash. Idempotent + thread-safe — dispatcht auf Rhino-Main-Thread via Idle-Event.""" if sc.sticky.get(_SPLASH_KEY) is None: return _dispatch_to_main(_hide_main)