#! python3 # -*- coding: utf-8 -*- # SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (C) 2026 Karim Gabriele Varano """ welcome.py Welcome-Screen + Shortcuts-Cheatsheet als WebView-Dialog im DOSSIER-Style (passend zum Splashscreen — Petrol-Gradient, Mono-Font). Funktionen: - show_welcome() — erscheint NACH dem Splash (eigener Idle-Timer), einmal pro Version. User kann "nicht mehr anzeigen" rechts unten anklicken. - show_cheatsheet() — DOSSIER-Shortcut-Liste, aufrufbar via dkeys-Alias. Marker-Datei fuer "schon gesehen" wird in ~/Library/Application Support/ch.gabrielevarano.Dossier/welcome_shown abgelegt. """ import os import json import Rhino DOSSIER_VERSION = "0.6.3" DOSSIER_GITHUB = "https://github.com/karimgvarano/DOSSIER" DOSSIER_SUPPORT_EMAIL = "karim@gabrielevarano.ch" _WELCOME_DIR = os.path.expanduser( "~/Library/Application Support/ch.gabrielevarano.Dossier") _WELCOME_FLAG = os.path.join(_WELCOME_DIR, "welcome_shown.txt") _WELCOME_OPTOUT = os.path.join(_WELCOME_DIR, "welcome_dontshow.txt") _SPLASH_MIN_DELAY_SEC = 3.5 _HERE = os.path.dirname(os.path.abspath(__file__)) _SHORTCUTS_JSON = os.path.join(_HERE, "aliases", "shortcuts_default.json") def _has_optout(): return os.path.exists(_WELCOME_OPTOUT) def _has_seen_version(version): try: if not os.path.exists(_WELCOME_FLAG): return False with open(_WELCOME_FLAG, "r") as f: return f.read().strip() == version except Exception: return False def _mark_seen(version): try: os.makedirs(_WELCOME_DIR, exist_ok=True) with open(_WELCOME_FLAG, "w") as f: f.write(version) except Exception as ex: print("[WELCOME] mark-seen err:", ex) def _write_optout(): try: os.makedirs(_WELCOME_DIR, exist_ok=True) with open(_WELCOME_OPTOUT, "w") as f: f.write("1") except Exception as ex: print("[WELCOME] optout-write err:", ex) def _load_shortcuts(): try: with open(_SHORTCUTS_JSON, "r", encoding="utf-8") as f: data = json.load(f) items = [] for k, v in data.items(): if k.startswith("_") or not isinstance(v, dict): continue items.append({ "id": k, "trigger": v.get("trigger", ""), "label": v.get("label", k), "type": v.get("type", ""), }) return items except Exception as ex: print("[WELCOME] shortcuts-load err:", ex) return [] # ---- HTML — DOSSIER-Style passend zum Splash ---------------------------- _WELCOME_HTML = """
×
DOSSIER.
Version {ver}
Willkommen im Studio
DOSSIER ist dein Architektur-Studio-Plugin fuer Rhino 8 — Waende, Decken, Treppen, Fenster, Tueren, Raumstempel, Layouts. Alles aus einer Hand, im selben Stil.
Einstieg
""" _CHEATSHEET_HTML = """
×
DOSSIER. Shortcuts
v {ver}
{sections}
""" def _build_cheatsheet_html(): items = _load_shortcuts() groups = { "DOSSIER BIM": [], "2D-Werkzeuge": [], "Views & Navigation": [], "Modify-Tools": [], "Sonstige Aliases": [], } bim_ids = {"wand", "tuer", "fenster", "decke", "treppe", "stuetze", "traeger", "raum", "symbol", "stempel", "dach", "aussparung"} view_ids = {"view_plan", "view_3d", "view_material", "zoom_ext", "zoom_sel", "geschoss_up", "geschoss_down", "panel_layer", "panel_elemente"} twod_ids = {"text", "line", "arc", "rectangle", "polyline", "curve", "hatch", "polygon", "ellipse", "circle"} for it in items: i = it["id"] if i in bim_ids: groups["DOSSIER BIM"].append(it) elif i in view_ids: groups["Views & Navigation"].append(it) elif i.startswith("mod_"): groups["Modify-Tools"].append(it) elif i in twod_ids or i.endswith("_alias"): groups["2D-Werkzeuge"].append(it) else: groups["Sonstige Aliases"].append(it) def _row(it): trig = it["trigger"] trig = trig.replace("Cmd+", "⌘+").replace("Shift+", "⇧+").replace("Alt+", "⌥+") return ('{}' '{}' .format(trig, it["label"])) sections = [] for gname, gitems in groups.items(): if not gitems: continue rows = "".join(_row(it) for it in gitems) sections.append('

{}

{}
'.format(gname, rows)) return _CHEATSHEET_HTML.format(ver=DOSSIER_VERSION, sections="".join(sections)) # ---- Dialog-Anzeige ------------------------------------------------------ def _try_borderless_mac(form): """Borderless NSWindow + transparenten Hintergrund (analog _startup_splash).""" try: import System nsw = getattr(form, "ControlObject", None) if nsw is None: return False # StyleMask = 0 (Borderless) try: cur = nsw.StyleMask nsw.StyleMask = System.Enum.ToObject(type(cur), 0) except Exception as ex: print("[WELCOME] StyleMask:", ex) # Transparent background damit border-radius vom HTML sichtbar for prop, val in [("TitlebarAppearsTransparent", True), ("IsOpaque", False), ("HasShadow", True), ("MovableByWindowBackground", True)]: try: setattr(nsw, prop, val) except Exception: pass try: tv_type = type(nsw.TitleVisibility) nsw.TitleVisibility = System.Enum.ToObject(tv_type, 1) except Exception: pass try: from AppKit import NSColor as _NSC clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None) if clear is not None: nsw.BackgroundColor = clear except Exception: pass return True except Exception as ex: print("[WELCOME] borderless:", ex) return False def _webview_transparent(web): """WKWebView vollstaendig transparent — KVC drawsBackground=NO, UnderPageBackgroundColor=Clear, Layer.BackgroundColor=CGColor.Clear.""" wk = getattr(web, "ControlObject", None) if wk is None: return try: from Foundation import NSNumber, NSString try: wk.SetValueForKey(NSNumber.FromBoolean(False), NSString("drawsBackground")) except Exception as ex: print("[WELCOME] KVC drawsBackground:", ex) except Exception as ex: print("[WELCOME] Foundation:", 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 except Exception as ex: print("[WELCOME] Layer:", ex) except Exception as ex: print("[WELCOME] NSColor:", ex) def _show_html_form(title, html, width=620, height=720, on_navigating=None, borderless=True): """Eto.Forms.Form mit WebView + Inline-HTML. Optional borderless + Navigation-Hook fuer custom URL-Schemes.""" try: import Eto.Forms as ef import Eto.Drawing as ed except Exception as ex: print("[WELCOME] Eto.Forms not available:", ex) return None try: form = ef.Form() form.Title = title form.ClientSize = ed.Size(width, height) form.Topmost = False form.Resizable = False if borderless: try: form.WindowStyle = getattr(ef.WindowStyle, "None") except Exception: pass for attr, val in (("Minimizable", False), ("Maximizable", False), ("Closeable", False), ("ShowInTaskbar", False)): try: setattr(form, attr, val) except Exception: pass try: form.BackgroundColor = ed.Colors.Transparent except Exception: pass web = ef.WebView() web.Size = ed.Size(width, height) if borderless: try: web.BackgroundColor = ed.Colors.Transparent except Exception: pass if on_navigating is not None: try: web.DocumentLoading += on_navigating except Exception as ex: print("[WELCOME] nav-hook:", ex) try: web.LoadHtml(html) except Exception as e: print("[WELCOME] LoadHtml:", e) form.Content = web try: form.Owner = Rhino.UI.RhinoEtoApp.MainWindow except Exception: pass form.Show() if borderless: _try_borderless_mac(form) _webview_transparent(web) try: ef.Application.Instance.RunIteration() except Exception: pass return form except Exception as ex: print("[WELCOME] form show:", ex) return None def show_welcome(force=False): """Zeigt Welcome NACH Splash. Erscheint bei jedem Start ausser der User klickt 'Nicht mehr anzeigen' (= optout-File). WICHTIG: UI muss auf Main-Thread laufen (Mac Cocoa) — Rhino-Idle-Event feuert dort, deshalb defern wir die Anzeige.""" if not force and _has_optout(): print("[WELCOME] optout active ({}) — skip".format(_WELCOME_OPTOUT)) return print("[WELCOME] geplant — Anzeige nach Splash (>{:.1f}s)".format(_SPLASH_MIN_DELAY_SEC)) import time state = {"start": time.time(), "fired": False} def _on_idle(sender, e): if state["fired"]: return if time.time() - state["start"] < _SPLASH_MIN_DELAY_SEC: return state["fired"] = True try: Rhino.RhinoApp.Idle -= _on_idle except Exception: pass try: print("[WELCOME] Anzeige starten") _show_welcome_now() except Exception as ex: print("[WELCOME] show err:", ex) try: Rhino.RhinoApp.Idle += _on_idle except Exception as ex: print("[WELCOME] idle-hook err:", ex) def _show_welcome_now(): html = _WELCOME_HTML.format( ver=DOSSIER_VERSION, github=DOSSIER_GITHUB, email=DOSSIER_SUPPORT_EMAIL) form_ref = [None] def _on_nav(sender, e): try: url = e.Uri.ToString() if hasattr(e, "Uri") else str(getattr(e, "Url", "")) except Exception: url = "" if not url: return if url.startswith("dossier:optout"): # Optout-Checkbox-Klick. URL-Form: dossier:optout?true/false checked = url.endswith("true") if checked: _write_optout() else: try: if os.path.exists(_WELCOME_OPTOUT): os.remove(_WELCOME_OPTOUT) except Exception: pass try: e.Cancel = True except Exception: pass elif url.startswith("dossier:cheatsheet"): try: e.Cancel = True except Exception: pass show_cheatsheet() try: if form_ref[0] is not None: form_ref[0].Close() except Exception: pass elif url.startswith("dossier:close"): try: e.Cancel = True except Exception: pass try: if form_ref[0] is not None: form_ref[0].Close() except Exception: pass form_ref[0] = _show_html_form("Willkommen bei DOSSIER", html, 600, 620, on_navigating=_on_nav) def show_cheatsheet(): html = _build_cheatsheet_html() form_ref = [None] def _on_nav(sender, e): try: url = e.Uri.ToString() if hasattr(e, "Uri") else str(getattr(e, "Url", "")) except Exception: url = "" if not url: return if url.startswith("dossier:close"): try: e.Cancel = True except Exception: pass try: if form_ref[0] is not None: form_ref[0].Close() except Exception: pass elif url.startswith("dossier:back"): try: e.Cancel = True except Exception: pass try: if form_ref[0] is not None: form_ref[0].Close() except Exception: pass try: _show_welcome_now() except Exception as ex: print("[WELCOME] back:", ex) form_ref[0] = _show_html_form("DOSSIER Shortcuts", html, 640, 760, on_navigating=_on_nav) if __name__ == "__main__": show_cheatsheet()