Splash: borderless+transparent+launcher-dedup, idle-dispatch hide
This commit is contained in:
+225
-67
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user