Files
DOSSIER/rhino/_startup_splash.py
T
karim 264327432d Splash + Layout-Skip-Revert
User-Bug: Layout-Skip-Optimierung war zu aggressiv — Mac Rhino haelt die
Panel-Anordnung zwischen Sessions doch nicht im internen State, also
wurden Panels falsch platziert nach Quick-Restart. Skip-Logik raus, der
~3s _-WindowLayout-Apply laeuft wieder jedes Mal. Das ist OK weil der
Splash diese Wartezeit jetzt optisch abdeckt.

Splash verbessert:
- _try_borderless_mac(): direkter NSWindow-Zugriff via Eto.ControlObject
  + ObjC-Methoden (setStyleMask_, setOpaque_, setHasShadow_,
  setBackgroundColor_, setMovableByWindowBackground_) — produziert
  echten borderless Mac-Look wie der Launcher-Splash
- Form-BackgroundColor auf transparent damit das gradient des WebView-
  HTMLs durchscheint (rounded petrol gradient mit weichem Verlauf)
- WebView selber transparenter Hintergrund
- Closeable/Minimizable/Maximizable/Resizable alle False
- [SPLASH] visible log fuer Debug-Sichtbarkeit
2026-05-27 19:36:09 +02:00

224 lines
8.7 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 scriptcontext as sc
_SPLASH_KEY = "_dossier_startup_splash"
_SAFETY_TIMEOUT_SEC = 8.0 # spaetestens nach 8s wegmachen, falls Hide-Hook nicht feuert
_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);
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%; } }
.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 + 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
# 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
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
try:
from AppKit import NSColor as _NSColor # type: ignore
clear = _NSColor.clearColor()
if _try("setBackgroundColor_", clear): ok = True
except Exception: pass
# 4) Bewegbar via background-drag
if _try("setMovableByWindowBackground_", True): ok = True
return ok
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."""
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)
sc.sticky[_SPLASH_KEY] = form
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
except Exception as ex:
print("[SPLASH] show:", ex)
def hide():
"""Versteckt + entsorgt den Splash. Idempotent."""
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)