Splash: borderless+transparent+launcher-dedup, idle-dispatch hide

This commit is contained in:
2026-05-27 20:09:09 +02:00
parent 264327432d
commit f8d1cfe3fe
4 changed files with 254 additions and 82 deletions
+11
View File
@@ -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();
}
+225 -67
View File
@@ -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 = '''<!DOCTYPE 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)
+3 -4
View File
@@ -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)
+15 -11
View File
@@ -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()