diff --git a/rhino/panel_base.py b/rhino/panel_base.py index d3ce67e..7e21191 100644 --- a/rhino/panel_base.py +++ b/rhino/panel_base.py @@ -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 = ''.format(mode) + parts = ['') + mode_script = ''.join(parts) if "" in html: html = html.replace("", mode_script + "") 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): diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index 4d0d5cd..8a8a2c2 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -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, diff --git a/src/EbenenSettingsApp.jsx b/src/EbenenSettingsApp.jsx new file mode 100644 index 0000000..21a8bf1 --- /dev/null +++ b/src/EbenenSettingsApp.jsx @@ -0,0 +1,42 @@ +import { useEffect } from 'react' +import EbenenSettingsDialog from './components/EbenenSettingsDialog' +import { notifyReady } from './lib/rhinoBridge' + +function bridgeSend(type, payload = {}) { + if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return } + const json = JSON.stringify({ type, payload }) + document.title = 'RHINOMSG::' + json +} + +export default function EbenenSettingsApp() { + const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {} + + useEffect(() => { + notifyReady() + const blockContext = (ev) => ev.preventDefault() + document.addEventListener('contextmenu', blockContext) + return () => document.removeEventListener('contextmenu', blockContext) + }, []) + + const ebene = initial.ebene || initial + const hatchPatterns = initial.hatchPatterns || ['Solid'] + + if (!ebene || typeof ebene !== 'object' || !ebene.code) { + return
Keine Daten
+ } + + return ( +
+ bridgeSend('SAVE', updated)} + onClose={() => bridgeSend('CANCEL', {})} + /> +
+ ) +} diff --git a/src/GeschossSettingsApp.jsx b/src/GeschossSettingsApp.jsx new file mode 100644 index 0000000..f9088a5 --- /dev/null +++ b/src/GeschossSettingsApp.jsx @@ -0,0 +1,43 @@ +import { useEffect } from 'react' +import GeschossSettingsDialog from './components/GeschossSettingsDialog' +import { onMessage, notifyReady } from './lib/rhinoBridge' + +// Inline send fuer SAVE/CANCEL — schickt direkt an Python-Bridge des +// Satelliten-Fensters. +function bridgeSend(type, payload = {}) { + if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return } + const json = JSON.stringify({ type, payload }) + document.title = 'RHINOMSG::' + json +} + +export default function GeschossSettingsApp() { + // PANEL_PARAMS wurden von Python beim Window-Open injiziert. + const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {} + + useEffect(() => { + notifyReady() + // Native Browser-Context-Menu unterdruecken + const blockContext = (ev) => ev.preventDefault() + document.addEventListener('contextmenu', blockContext) + return () => document.removeEventListener('contextmenu', blockContext) + }, []) + + // Wenn keine Daten da sind: leeres Frame + if (!initial || typeof initial !== 'object') { + return
Keine Daten
+ } + + return ( +
+ bridgeSend('SAVE', updated)} + onClose={() => bridgeSend('CANCEL', {})} + /> +
+ ) +} diff --git a/src/components/EbenenManager.jsx b/src/components/EbenenManager.jsx index 43cbd44..26cb3a1 100644 --- a/src/components/EbenenManager.jsx +++ b/src/components/EbenenManager.jsx @@ -2,8 +2,7 @@ import { useState, useRef, useMemo, useEffect } from 'react' import Icon from './Icon' import ConfirmDeleteEbene from './ConfirmDeleteEbene' import ContextMenu from './ContextMenu' -import EbenenSettingsDialog from './EbenenSettingsDialog' -import { setLayerStyle, deleteEbene, moveSelectionToEbene } from '../lib/rhinoBridge' +import { setLayerStyle, deleteEbene, moveSelectionToEbene, openEbenenSettings } from '../lib/rhinoBridge' const MODES = [ { value: 'all', label: 'Alle anzeigen' }, @@ -266,7 +265,8 @@ export default function EbenenManager({ const [ctxMenu, setCtxMenu] = useState(null) // { x, y, code } const [clipboard, setClipboard] = useState(null) // { color, lw } const [autoEdit, setAutoEdit] = useState(null) // { code, field, token } - const [settingsCode, setSettingsCode] = useState(null) // code der Ebene deren Einstellungen offen sind + // Settings-Dialog laeuft jetzt in einem echten Rhino-Fenster (Satellite- + // Window via Eto.Form + WebView). State hier nicht mehr noetig. // Bei Auto-Edit: in View scrollen useEffect(() => { @@ -409,7 +409,10 @@ export default function EbenenManager({ } const ctxItems = (code) => [ - { label: 'Ebeneneinstellungen…', icon: 'settings', onClick: () => setSettingsCode(code) }, + { label: 'Ebeneneinstellungen…', icon: 'settings', onClick: () => { + const target = ebenen.find(e => e.code === code) + if (target) openEbenenSettings(target, hatchPatterns) + } }, { divider: true }, { label: 'Selektion hierher übertragen', icon: 'move_down', onClick: () => moveSelectionToEbene(code) }, { divider: true }, @@ -583,22 +586,6 @@ export default function EbenenManager({ /> )} - {settingsCode && (() => { - const target = ebenen.find(e => e.code === settingsCode) - if (!target) { setSettingsCode(null); return null } - return ( - { - // Code-Wechsel handhaben (eindeutiger key) - onChange(ebenen.map(e => e.code === settingsCode ? updated : e)) - setSettingsCode(null) - }} - onClose={() => setSettingsCode(null)} - /> - ) - })()} ) } diff --git a/src/components/GeschossManager.jsx b/src/components/GeschossManager.jsx index 650ba18..ea03e2b 100644 --- a/src/components/GeschossManager.jsx +++ b/src/components/GeschossManager.jsx @@ -1,7 +1,7 @@ import { useState } from 'react' import Icon from './Icon' import GeschossDialog from './GeschossDialog' -import GeschossSettingsDialog from './GeschossSettingsDialog' +import { openGeschossSettings } from '../lib/rhinoBridge' function GeschossBadge({ name }) { return {name} @@ -86,7 +86,6 @@ export default function GeschossManager({ mode, onModeChange, }) { const [dialogOpen, setDialogOpen] = useState(false) - const [settingsFor, setSettingsFor] = useState(null) // Geschoss-Objekt oder null const sorted = [...zeichnungsebenen].reverse() const gesamthoehe = zeichnungsebenen @@ -162,7 +161,7 @@ export default function GeschossManager({ mode={mode} onClick={() => onActiveChange(z.id)} onToggleVisible={() => toggleVisible(z.id)} - onSettings={() => setSettingsFor(z)} + onSettings={() => openGeschossSettings(z)} /> ))} @@ -176,16 +175,6 @@ export default function GeschossManager({ /> )} - {settingsFor && ( - { - onChange(zeichnungsebenen.map(z => z.id === updated.id ? updated : z)) - setSettingsFor(null) - }} - onClose={() => setSettingsFor(null)} - /> - )} ) } diff --git a/src/lib/rhinoBridge.js b/src/lib/rhinoBridge.js index 88f225a..3674f0e 100644 --- a/src/lib/rhinoBridge.js +++ b/src/lib/rhinoBridge.js @@ -277,6 +277,15 @@ export function saveCombinationPreset(name, layers) { send('SAVE_PRESET', { na export function saveCurrentAsCombination(name) { send('SAVE_CURRENT_AS_PRESET', { name }) } export function deleteCombinationPreset(name) { send('DELETE_PRESET', { name }) } +// Satelliten-Fenster oeffnen — Python oeffnet ein echtes Rhino-Fenster +// (Eto.Form mit eingebetteter WebView) mit dem Settings-Dialog. +export function openGeschossSettings(geschoss) { + send('OPEN_GESCHOSS_SETTINGS', { geschoss }) +} +export function openEbenenSettings(ebene, hatchPatterns) { + send('OPEN_EBENEN_SETTINGS', { ebene, hatchPatterns }) +} + export function applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, zMode, eMode) { // Split-Panels koennen mit null/[] fuer fremde Slice aufrufen — Backend // fuellt aus doc.Strings. Hier robust gegen alles Falsy. diff --git a/src/main.jsx b/src/main.jsx index 1ac03cc..0263c60 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -3,6 +3,8 @@ import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx' +import GeschossSettingsApp from './GeschossSettingsApp.jsx' +import EbenenSettingsApp from './EbenenSettingsApp.jsx' import GestaltungApp from './GestaltungApp.jsx' import AusschnitteApp from './AusschnitteApp.jsx' import MassstabApp from './MassstabApp.jsx' @@ -14,16 +16,18 @@ import LayoutsApp from './LayoutsApp.jsx' import ElementeApp from './ElementeApp.jsx' const mode = (typeof window !== 'undefined' && window.PANEL_MODE) || 'ebenen' -const RootApp = mode === 'gestaltung' ? GestaltungApp - : mode === 'ausschnitte' ? AusschnitteApp - : mode === 'massstab' ? MassstabApp - : mode === 'werkzeuge' ? WerkzeugeApp - : mode === 'oberleiste' ? OberleisteApp - : mode === 'overrides' ? OverridesApp - : mode === 'dimensionen' ? DimensionenApp - : mode === 'layouts' ? LayoutsApp - : mode === 'elemente' ? ElementeApp - : mode === 'zeichnungsebenen' ? ZeichnungsebenenApp +const RootApp = mode === 'gestaltung' ? GestaltungApp + : mode === 'ausschnitte' ? AusschnitteApp + : mode === 'massstab' ? MassstabApp + : mode === 'werkzeuge' ? WerkzeugeApp + : mode === 'oberleiste' ? OberleisteApp + : mode === 'overrides' ? OverridesApp + : mode === 'dimensionen' ? DimensionenApp + : mode === 'layouts' ? LayoutsApp + : mode === 'elemente' ? ElementeApp + : mode === 'zeichnungsebenen' ? ZeichnungsebenenApp + : mode === 'geschoss_settings' ? GeschossSettingsApp + : mode === 'ebenen_settings' ? EbenenSettingsApp : App window.onerror = function (msg, src, line, col, err) {