DOSSIER ist dein Architektur-Studio-Plugin fuer Rhino 8 —
Waende, Decken, Treppen, Fenster, Tueren, Raumstempel,
Layouts. Alles aus einer Hand, im selben Stil.
"""
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()