From f8d1cfe3fe79167793e3c2ea05a3dd89a0b77f1f Mon Sep 17 00:00:00 2001 From: karim Date: Wed, 27 May 2026 20:09:09 +0200 Subject: [PATCH] Splash: borderless+transparent+launcher-dedup, idle-dispatch hide --- launcher/src-tauri/src/lib.rs | 11 ++ rhino/_startup_splash.py | 292 ++++++++++++++++++++++++++-------- rhino/oberleiste.py | 7 +- rhino/startup.py | 26 +-- 4 files changed, 254 insertions(+), 82 deletions(-) diff --git a/launcher/src-tauri/src/lib.rs b/launcher/src-tauri/src/lib.rs index ed038b1..0c9f614 100644 --- a/launcher/src-tauri/src/lib.rs +++ b/launcher/src-tauri/src/lib.rs @@ -294,6 +294,13 @@ fn plugin_loaded_marker_path() -> PathBuf { dossier_dir().join("plugin_loaded.flag") } +fn splash_owner_marker_path() -> PathBuf { + // Vor Launch von Rhino schreibt Launcher diesen Marker → Plugin-Splash + // (rhino/_startup_splash.py) prueft beim Show ob Marker frisch (<30s) + // ist und skippt dann, damit nicht beide Splashes gleichzeitig laufen. + dossier_dir().join("splash_owner_launcher.flag") +} + fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), String> { let settings = load_settings(); // XML-Edit nur sinnvoll wenn Rhino nicht laeuft (sonst ueberschreibt's @@ -314,8 +321,11 @@ fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), Stri // Splash NUR zeigen wenn Auto-Load aktiv (sonst gibt's nichts zu warten). let show_splash = settings.auto_load_plugin; let marker = plugin_loaded_marker_path(); + let owner_marker = splash_owner_marker_path(); if show_splash { let _ = fs::remove_file(&marker); + // Owner-Marker: signalisiert dem Plugin-Splash dass Launcher uebernimmt + let _ = fs::write(&owner_marker, b"launcher"); if let Some(splash) = app.get_webview_window("splash") { let _ = splash.show(); } @@ -344,6 +354,7 @@ fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), Stri std::thread::sleep(std::time::Duration::from_millis(250)); } let _ = fs::remove_file(&marker); + let _ = fs::remove_file(&owner_marker); if let Some(splash) = app_clone.get_webview_window("splash") { let _ = splash.hide(); } diff --git a/rhino/_startup_splash.py b/rhino/_startup_splash.py index 443326e..2af3d97 100644 --- a/rhino/_startup_splash.py +++ b/rhino/_startup_splash.py @@ -12,10 +12,21 @@ 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" -_SAFETY_TIMEOUT_SEC = 8.0 # spaetestens nach 8s wegmachen, falls Hide-Hook nicht feuert +_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 = ''' @@ -46,19 +57,9 @@ html, body { margin:0; padding:0; width:100%; height:100%; background:transparen .status-row { align-self:end; display:flex; align-items:center; gap:10px; margin-top:18px; font-size:11px; letter-spacing:0.10em; color:var(--paper); text-transform:uppercase; } -.dot-pulse { width:7px; height:7px; border-radius:50%; background:var(--paper); - animation:pulse 1.6s ease-out infinite; } -@keyframes pulse { - 0% { box-shadow:0 0 0 0 rgba(255,255,255,0.55); transform:scale(1); } - 70% { box-shadow:0 0 0 9px rgba(255,255,255,0); transform:scale(1.05); } - 100% { box-shadow:0 0 0 0 rgba(255,255,255,0); transform:scale(1); } -} -.bar { position:relative; height:2px; width:100%; background:rgba(255,255,255,0.18); - border-radius:2px; overflow:hidden; margin-top:12px; } -.bar::after { content:""; position:absolute; top:0; left:-35%; width:35%; height:100%; - background: linear-gradient(90deg, transparent, var(--paper), transparent); - animation: slide 1.6s linear infinite; } -@keyframes slide { to { left:100%; } } +.dot-pulse { width:7px; height:7px; border-radius:50%; background:var(--paper); } +.bar { position:relative; height:2px; width:100%; background:rgba(255,255,255,0.28); + border-radius:2px; margin-top:12px; } .meta-row { display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin-top:10px; font-size:9px; letter-spacing:0.14em; color:var(--paper-faint); text-transform:uppercase; } @@ -85,52 +86,194 @@ html, body { margin:0; padding:0; width:100%; height:100%; background:transparen def _try_borderless_mac(form): """Mac-spezifisch: direkter NSWindow-Zugriff via Eto.ControlObject um - titlebar/Decorations komplett zu killen + rounded corners zu setzen. - Auf Mac ist Eto.Forms.Form.WindowStyle.None inkonsistent — der echte - Borderless-Effekt geht nur ueber AppKit. Probiert verschiedene Wege, - keiner ist fatal wenn er nicht klappt.""" - nswindow = None - try: - # Eto auf Mac: Form.ControlObject ist die NSWindow-Instanz - nswindow = getattr(form, "ControlObject", None) - except Exception: pass - if nswindow is None: return False + titlebar/Decorations komplett zu killen. - # NSBorderlessWindowMask = 0, NSFullSizeContentViewWindowMask = 1<<15 - # NSWindowStyleMaskTitled = 1 - BORDERLESS = 0 - FULL_SIZE = 1 << 15 - - def _try(method_name, *args): - try: - m = getattr(nswindow, method_name, None) - if m is None: return False - m(*args) - return True - except Exception: return False + 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 - # 1) Style-Mask auf borderless (entfernt Titlebar/Border) - if _try("setStyleMask_", BORDERLESS): - ok = True - # 2) Hintergrund transparent damit WebView's eigene rounded box scheint - if _try("setOpaque_", False): ok = True - if _try("setHasShadow_", True): ok = True - # 3) Background color clear + + # NSWindowStyleMaskBorderless = 0 + # NSWindowStyleMaskTitled = 1, FullSizeContentView = 32768 try: - from AppKit import NSColor as _NSColor # type: ignore - clear = _NSColor.clearColor() - if _try("setBackgroundColor_", clear): ok = True + 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 - # 4) Bewegbar via background-drag - if _try("setMovableByWindowBackground_", True): ok = True + 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 falls hide() vergessen wird.""" + 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: @@ -193,31 +336,46 @@ def show(): 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 — wenn nach 8s niemand hide() ruft, automatisch weg - try: - import threading - def _auto_hide(): - try: hide() - except Exception: pass - threading.Timer(_SAFETY_TIMEOUT_SEC, _auto_hide).start() - except Exception: pass + # Safety-Timeout via Idle-Polling (Main-Thread, Mac-safe) + _install_safety_timeout() except Exception as ex: print("[SPLASH] show:", ex) -def hide(): - """Versteckt + entsorgt den Splash. Idempotent.""" +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 - try: - sc.sticky[_SPLASH_KEY] = None - try: form.Close() - except Exception: - try: form.Visible = False - except Exception: pass - except Exception as ex: - print("[SPLASH] hide:", ex) + 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) diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index e74b264..3d85ad3 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -1372,12 +1372,11 @@ class OberleisteBridge(panel_base.BaseBridge): except Exception as ex: print("[OBERLEISTE] auto-apply (layout/colors):", ex) # Splash-Screen (falls noch offen) jetzt wegmachen — Layout-Apply ist - # durch, Panels sind in finaler Position. Lazy via Timer 200ms damit - # die Layout-Animation kurz auf den finalen Panels sichtbar wird. + # durch, Panels sind in finaler Position. hide() dispatcht intern + # auf Main-Thread via Rhino.Idle (Mac-safe). try: - import threading import _startup_splash as _ss - threading.Timer(0.2, _ss.hide).start() + _ss.hide() except Exception as ex: print("[OBERLEISTE] splash hide:", ex) self._send_state(force=True) diff --git a/rhino/startup.py b/rhino/startup.py index b3bfda4..911b15d 100644 --- a/rhino/startup.py +++ b/rhino/startup.py @@ -15,16 +15,26 @@ import json import Rhino import scriptcontext as sc +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +# Splash SOFORT als allererstes — bevor irgendwas anderes passiert, damit der +# Nutzer waehrend Python-Imports + Panel-Registrierung nicht in eine schwarze +# Rhino-Oberflaeche schaut. Skipt automatisch wenn Launcher seinen eigenen +# Splash zeigt (Owner-Marker-Check). +try: + import _startup_splash as _splash_first + _splash_first.show() +except Exception as _ex_splash: + print("[STARTUP] splash early:", _ex_splash) + # DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start. print("[STARTUP] Python: {}".format(sys.version)) print("[STARTUP] Implementation: {}".format( sys.implementation.name if hasattr(sys, "implementation") else "n/a (IPy2)")) print("[STARTUP] Platform: {}".format(sys.platform)) -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) - # Pfad zur Custom-UI (Toolbars/Sidebar) — wird einmal pro Session geladen _UI_FILE = os.path.join(_HERE, "DOSSIERUI.rhw") @@ -240,13 +250,7 @@ def _load_all(sender, e): Rhino.RhinoApp.Idle -= _load_all except Exception: pass - # Splash zeigen bevor irgendwas laeuft — verdeckt visuell die ~3s - # Panel-Init + WindowLayout-Apply - try: - import _startup_splash - _startup_splash.show() - except Exception as ex: - print("[STARTUP] splash show:", ex) + # Splash wird ganz oben in startup.py (vor diesem Idle) gezeigt. print("[STARTUP] Lade DOSSIER-Panels...") # Migration einmal fuer das beim Start aktive Doc _migrate_active_doc()