Files
DOSSIER/rhino/_startup_splash.py

382 lines
15 KiB
Python

#! 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 = '''<!DOCTYPE html>
<html lang="de"><head><meta charset="utf-8"/><title>Dossier laedt</title>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&display=swap" rel="stylesheet"/>
<style>
:root {
--accent: #5fa896; --accent-soft: #6fb5a3; --accent-deep: #2f5d54;
--paper: #fff; --paper-mute: rgba(255,255,255,0.72); --paper-faint: rgba(255,255,255,0.45);
--font-display: Krungthep, 'Archivo Black', sans-serif;
--font-mono: 'DM Mono', 'Menlo', monospace;
}
html, body { margin:0; padding:0; width:100%; height:100%; background:transparent !important;
color:var(--paper); overflow:hidden; font-family:var(--font-mono); user-select:none;
-webkit-user-select:none; cursor:default; }
.frame { box-sizing:border-box; width:100%; height:100%; padding:22px 26px;
display:grid; grid-template-rows:auto 1fr auto; gap:0;
background: radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
border-radius:16px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.18); }
.brand-row { display:flex; align-items:baseline; justify-content:space-between; gap:12px; }
.brand { font-family:var(--font-display); font-size:28px; letter-spacing:-0.01em;
line-height:1; color:var(--paper); }
.brand-dot { color:var(--accent-deep); }
.version { font-family:var(--font-mono); font-size:10px; letter-spacing:0.10em;
color:var(--paper-mute); text-transform:uppercase; }
.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); }
.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; }
</style></head><body>
<div class="frame">
<div class="brand-row">
<div class="brand">DOSSIER<span class="brand-dot">.</span></div>
<div class="version">Rhino 8 Plugin</div>
</div>
<div>
<div class="status-row">
<span class="dot-pulse"></span>
<span>Plugin laedt &mdash; Panels werden platziert</span>
</div>
<div class="bar"></div>
</div>
<div class="meta-row">
<span>AGPL-3.0 &middot; Karim Gabriele Varano</span>
<span>CPython 3.9</span>
</div>
</div></body></html>
'''
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)