Settings-Dialoge in echten Rhino-Fenstern (Eto.Form + WebView)

Statt Overlay-im-Panel oeffnet sich der Settings-Dialog jetzt als
echtes Rhino-Fenster (verschiebbar, resizable, mehrere parallel).

Infrastruktur in panel_base.py:
- load_inline akzeptiert jetzt `params` (dict) und injiziert sie
  als window.PANEL_PARAMS — Satelliten-Apps lesen ihren initialen
  State daraus.
- Neue Funktion open_satellite_window(mode, params, title, size,
  on_save, on_cancel): erstellt Eto.Forms.Form mit eingebetteter
  WebView, eigenem Inline-Bridge fuer SAVE/CANCEL-Messages, ruft
  Callbacks auf und schliesst das Fenster.

Backend rhinopanel.py:
- Neue Message-Handler OPEN_GESCHOSS_SETTINGS und OPEN_EBENEN_SETTINGS.
- _open_geschoss_settings: oeffnet das Satelliten-Fenster mit dem
  Geschoss als Payload. on_save: replace im doc.Strings z-Liste +
  _apply(save_z=True).
- _open_ebenen_settings: gleich, aber fuer Ebene + hatchPatterns.

Neue React-Entries:
- GeschossSettingsApp.jsx: wrappt GeschossSettingsDialog, liest
  window.PANEL_PARAMS, schickt SAVE/CANCEL direkt via document.title-
  Bridge.
- EbenenSettingsApp.jsx: gleich fuer EbenenSettingsDialog.

main.jsx-Switch erweitert um 'geschoss_settings' und 'ebenen_settings'.

GeschossManager und EbenenManager:
- Inline-Dialog-State und -Rendering entfernt.
- onSettings ruft jetzt openGeschossSettings(z) / openEbenenSettings(e)
  in der Bridge auf → Backend oeffnet das Satelliten-Fenster.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 01:15:12 +02:00
parent e6a39531f4
commit 1ba0bda429
8 changed files with 297 additions and 46 deletions
+100 -3
View File
@@ -198,8 +198,12 @@ class BaseBridge(object):
# --- HTML laden -------------------------------------------------------------
def load_inline(wv, mode):
"""Laedt dist/index.html inline und injiziert window.PANEL_MODE."""
def load_inline(wv, mode, params=None):
"""Laedt dist/index.html inline und injiziert window.PANEL_MODE.
`params` (optional dict): wird als `window.PANEL_PARAMS` injiziert. Wird
von Satelliten-Fenstern (z.B. Settings-Dialoge) verwendet um initial-
State an die React-App zu uebergeben."""
if not os.path.exists(_DIST):
print("[{}] dist nicht gefunden".format(mode.upper()))
return
@@ -207,7 +211,14 @@ def load_inline(wv, mode):
with open(_DIST, "rb") as f:
html = f.read().decode("utf-8")
mode_script = '<script>window.PANEL_MODE="{}";</script>'.format(mode)
parts = ['<script>window.PANEL_MODE="{}";'.format(mode)]
if params is not None:
try:
parts.append('window.PANEL_PARAMS=' + json.dumps(params, ensure_ascii=False) + ';')
except Exception as ex:
print("[{}] PANEL_PARAMS serialize: {}".format(mode.upper(), ex))
parts.append('</script>')
mode_script = ''.join(parts)
if "</head>" in html:
html = html.replace("</head>", mode_script + "</head>")
else:
@@ -267,6 +278,92 @@ def attach_webview(panel, bridge, mode):
Rhino.RhinoApp.Idle += on_idle
# --- Satelliten-Fenster (echtes Rhino-Fenster mit eingebetteter WebView) ----
def open_satellite_window(mode, params=None, title=None, size=(420, 560),
on_save=None, on_cancel=None):
"""Oeffnet ein echtes Rhino-Fenster (Eto.Form) mit eingebetteter WebView.
Die WebView laedt die React-App mit dem gegebenen `mode` und `params`.
Die React-App sendet via Bridge `SAVE`/`CANCEL`-Messages. Wir rufen
dann die jeweilige Callback-Funktion auf (mit dem Save-Payload) und
schliessen das Fenster.
Returns die Form-Instance (User kann sie speichern um sie spaeter
programmatisch zu schliessen)."""
form = forms.Form()
if title is None: title = mode.replace('_', ' ').title()
form.Title = title
try:
form.ClientSize = drawing.Size(int(size[0]), int(size[1]))
except Exception: pass
form.Resizable = True
form.Topmost = False
wv = forms.WebView()
# Inline-Bridge fuer Satelliten-Fenster: handle SAVE/CANCEL, schliesse
# bei beiden das Fenster.
class _SatelliteBridge(BaseBridge):
def __init__(self):
BaseBridge.__init__(self, mode)
def handle(self, data):
t = data.get("type", "")
p = data.get("payload") or {}
if t == "READY":
# React liest PANEL_PARAMS direkt vom window-Object — wir
# muessen also nichts mehr aktiv senden.
pass
elif t == "SAVE":
if on_save is not None:
try: on_save(p)
except Exception as ex:
print("[{}] on_save: {}".format(mode.upper(), ex))
try: form.Close()
except Exception: pass
elif t == "CANCEL":
if on_cancel is not None:
try: on_cancel()
except Exception: pass
try: form.Close()
except Exception: pass
bridge = _SatelliteBridge()
bridge.set_webview(wv)
def on_title_(s, e):
title_str = e.Title or ""
if not title_str.startswith("RHINOMSG::"):
return
try:
bridge.handle_raw(title_str[10:])
except Exception as ex:
print("[{}] Message-Fehler: {}".format(mode.upper(), ex))
finally:
try:
wv.ExecuteScript("document.title='{}';".format(mode.upper()))
except Exception:
pass
def on_loaded(s, e):
try: wv.ExecuteScript("window.RHINO_MODE=true;")
except Exception: pass
wv.DocumentTitleChanged += on_title_
wv.DocumentLoaded += on_loaded
form.Content = wv
form.Show()
# HTML nach Show() laden — sonst ist die WebView eventuell noch nicht
# gerendert und die JS-Bridge initialisiert sich seltsam.
try:
load_inline(wv, mode, params=params)
except Exception as ex:
print("[{}] Inline-Fehler: {}".format(mode.upper(), ex))
return form
# --- Dynamic .NET Type ------------------------------------------------------
def create_dockable_type(guid_str, type_name, assembly_name):
+80
View File
@@ -216,9 +216,89 @@ class EbenenBridge(panel_base.BaseBridge):
elif t == "DELETE_PRESET":
self._delete_preset(p.get("name") or "")
self._send_combination()
elif t == "OPEN_GESCHOSS_SETTINGS":
self._open_geschoss_settings(p.get("geschoss") or {})
elif t == "OPEN_EBENEN_SETTINGS":
self._open_ebenen_settings(p.get("ebene") or {},
p.get("hatchPatterns") or [])
# ---- Helpers ----
def _open_geschoss_settings(self, geschoss):
"""Oeffnet ein echtes Rhino-Fenster (Eto.Form mit WebView) mit dem
GeschossSettingsDialog. Save updated den Eintrag in doc.Strings +
triggert Cross-Panel-Sync."""
if not isinstance(geschoss, dict) or not geschoss.get("id"):
print("[EBENEN] open_geschoss_settings: kein Geschoss-Payload")
return
gid = geschoss["id"]
def on_save(updated):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
if not z_raw:
print("[EBENEN] save_geschoss: kein z-Store"); return
try:
z_list = json.loads(z_raw)
except Exception as ex:
print("[EBENEN] save_geschoss JSON:", ex); return
replaced = False
for i, z in enumerate(z_list):
if isinstance(z, dict) and z.get("id") == gid:
z_list[i] = updated
replaced = True
break
if not replaced:
print("[EBENEN] save_geschoss: id {} nicht gefunden".format(gid))
return
# Build_layers + Save via _apply (durchlaeuft ohne save_e)
e_raw = doc.Strings.GetValue("dossier_ebenen")
try: e_list = json.loads(e_raw) if e_raw else []
except Exception: e_list = []
self._apply(z_list, e_list, save_z=True, save_e=False)
panel_base.open_satellite_window(
"geschoss_settings",
params=geschoss,
title="Zeichnungsebene: {}".format(geschoss.get("name", "")),
size=(380, 540),
on_save=on_save)
def _open_ebenen_settings(self, ebene, hatch_patterns):
"""Oeffnet ein echtes Rhino-Fenster mit dem EbenenSettingsDialog."""
if not isinstance(ebene, dict) or not ebene.get("code"):
print("[EBENEN] open_ebenen_settings: kein Ebene-Payload")
return
old_code = ebene["code"]
def on_save(updated):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
e_raw = doc.Strings.GetValue("dossier_ebenen")
if not e_raw:
print("[EBENEN] save_ebene: kein e-Store"); return
try:
e_list = json.loads(e_raw)
except Exception as ex:
print("[EBENEN] save_ebene JSON:", ex); return
replaced = False
for i, e in enumerate(e_list):
if isinstance(e, dict) and e.get("code") == old_code:
e_list[i] = updated
replaced = True
break
if not replaced:
print("[EBENEN] save_ebene: code {} nicht gefunden".format(old_code))
return
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
try: z_list = json.loads(z_raw) if z_raw else []
except Exception: z_list = []
self._apply(z_list, e_list, save_z=False, save_e=True)
panel_base.open_satellite_window(
"ebenen_settings",
params={"ebene": ebene, "hatchPatterns": hatch_patterns},
title="Ebene: {}_{}".format(ebene.get("code", ""), ebene.get("name", "")),
size=(420, 600),
on_save=on_save)
def _apply(self, zeichnungsebenen, ebenen, save_z=True, save_e=True):
print("[EBENEN] _apply START z={} e={} (save_z={} save_e={})".format(
len(zeichnungsebenen) if zeichnungsebenen else 0,