264327432d
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
224 lines
8.7 KiB
Python
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 — Panels werden platziert</span>
|
|
</div>
|
|
<div class="bar"></div>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span>AGPL-3.0 · 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)
|