Startup-Splash (petrol-gruen) waehrend Plugin-Init

Verdeckt visuell die 3+ Sekunden Wartezeit beim Cold-Start (Panel-
Registration + WindowLayout-Apply). Stilistisch identisch zum
Launcher-Splash: petrol-gruener Verlauf mit "DOSSIER."-Logo, pulsierendem
Dot, animierter Progress-Bar.

Architektur:
- _startup_splash.py: zentrale show() / hide() Helpers
  - Borderless Eto.Forms.Form (420x160), Topmost, kein Taskbar-Eintrag
  - WebView mit Inline-HTML (gleicher Stil wie launcher/public/splash.html)
  - Sticky-Key _dossier_startup_splash haelt die Form-Referenz
  - Safety-Timeout 8s falls hide() vergessen wird

- startup.py _load_all: show() ganz am Anfang (bevor Imports laufen)
- oberleiste._on_ready: hide() via 200ms-Timer NACH window-layout-apply
  (bzw. nach skip) — Layout-Animation ist auf Panels in Finalposition
  kurz sichtbar bevor Splash verschwindet

Effekt: User sieht sofort einen schoenen Branded-Loading-Screen statt
3s grauer Rhino-UI mit halb-geladenen Panels.
This commit is contained in:
2026-05-27 19:31:00 +02:00
parent edaf83229b
commit 6a13ede6b7
3 changed files with 172 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
#! 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 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:
return
try:
import Eto.Forms as ef
import Eto.Drawing as ed
form = ef.Form()
form.Title = "Dossier"
# WindowStyle.None — "None" ist Python-keyword, daher via getattr
try: form.WindowStyle = getattr(ef.WindowStyle, "None")
except Exception: pass
try: form.Resizable = False
except Exception: pass
try: form.Topmost = True
except Exception: pass
try: form.ShowInTaskbar = False
except Exception: pass
try: form.Size = ed.Size(420, 160)
except Exception: pass
try:
# Hintergrund weiss, das WebView-content hat seine eigene
# gerundete Petrol-Box — Form muss nur kein graues Border zeigen
form.BackgroundColor = ed.Color(0.18, 0.50, 0.45)
except Exception: pass
wv = ef.WebView()
try:
wv.LoadHtml(_SPLASH_HTML)
except Exception as ex:
print("[SPLASH] LoadHtml:", ex)
form.Content = wv
try:
# Center on screen
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
sc.sticky[_SPLASH_KEY] = form
# 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)
+9
View File
@@ -1380,6 +1380,15 @@ class OberleisteBridge(panel_base.BaseBridge):
sc.sticky["_dossier_view_colors_applied"] = True sc.sticky["_dossier_view_colors_applied"] = True
except Exception as ex: except Exception as ex:
print("[OBERLEISTE] auto-apply (layout/colors):", 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.
try:
import threading
import _startup_splash as _ss
threading.Timer(0.2, _ss.hide).start()
except Exception as ex:
print("[OBERLEISTE] splash hide:", ex)
self._send_state(force=True) self._send_state(force=True)
def handle(self, data): def handle(self, data):
+7
View File
@@ -240,6 +240,13 @@ def _load_all(sender, e):
Rhino.RhinoApp.Idle -= _load_all Rhino.RhinoApp.Idle -= _load_all
except Exception: except Exception:
pass 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)
print("[STARTUP] Lade DOSSIER-Panels...") print("[STARTUP] Lade DOSSIER-Panels...")
# Migration einmal fuer das beim Start aktive Doc # Migration einmal fuer das beim Start aktive Doc
_migrate_active_doc() _migrate_active_doc()