From de57c320c220c25620f001e30c1cccd6d620cd82 Mon Sep 17 00:00:00 2001 From: karim Date: Mon, 25 May 2026 03:39:03 +0200 Subject: [PATCH] Symbol-Picker als Satellite-Fenster (statt Modal im Elemente-Panel) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX-Verbesserung: Modal-Overlay im engen Elemente-Panel war unpraktisch. Symbol-Picker oeffnet sich jetzt als eigenstaendiges Eto.Form-Fenster (wie Library/Project-Settings). Frontend: - SymbolPicker bekommt embedded-Prop (Satellite-Mount vs Modal-Overlay) - Neuer SymbolPickerApp Satellite-Wrapper (PANEL_PARAMS lesen + Bridge) - main.jsx: 'symbol_picker' Mode-Routing - ElementeApp: Symbol-Button ruft nur noch listLibrary() — Backend oeffnet das Fenster Backend: - _cmd_list_library oeffnet jetzt das Satellite-Window mit eigener Bridge (PICK -> CREATE_SYMBOL, CANCEL -> Close) - PICK schliesst Fenster + triggert interactive GetPoint im Viewport Co-Authored-By: Claude Opus 4.7 --- rhino/elemente.py | 46 ++++++-- src/ElementeApp.jsx | 26 +---- src/SymbolPickerApp.jsx | 25 +++++ src/components/SymbolPicker.jsx | 184 +++++++++++++++++--------------- src/main.jsx | 2 + 5 files changed, 168 insertions(+), 115 deletions(-) create mode 100644 src/SymbolPickerApp.jsx diff --git a/rhino/elemente.py b/rhino/elemente.py index 9788203..c33ea48 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -8092,18 +8092,50 @@ class ElementeBridge(panel_base.BaseBridge): # ------------------------------------------------------------------ def _cmd_list_library(self, p): - """Liefert Library-Manifest ans Frontend (fuer den Symbol-Picker - im Elemente-Panel). Antwort: LIBRARY_LIST message.""" + """Oeffnet den Symbol-Picker als Satellite-Window. Lieferung der + Library-Items + Handling von PICK (User waehlt Item → CREATE_SYMBOL + im aktiven Doc) und CANCEL (Fenster schliessen).""" try: import library manifest = library.load_manifest() - self.send("LIBRARY_LIST", { - "items": manifest.get("items", []), - "name": manifest.get("name", "Dossier-Library"), - }) + items = manifest.get("items", []) except Exception as ex: print("[ELEMENTE] list library:", ex) - self.send("LIBRARY_LIST", {"items": [], "name": ""}) + items = [] + outer_bridge = self + bridge_holder = {"form": None} + class _SymbolPickerBridge(panel_base.BaseBridge): + def __init__(self): + panel_base.BaseBridge.__init__(self, "symbol_picker") + def handle(self, data): + if not isinstance(data, dict): return + t = data.get("type", "") + pp = data.get("payload") or {} + if t == "READY": pass + elif t == "PICK": + lib_id = (pp.get("id") or "").strip() + if lib_id: + # Fenster zu, dann interactive Place + try: + f = bridge_holder.get("form") + if f is not None: f.Close() + except Exception: pass + try: + outer_bridge._cmd_create_symbol({"id": lib_id}) + except Exception as ex: + print("[SYMBOL-PICKER] CREATE_SYMBOL:", ex) + elif t == "CANCEL": + try: + f = bridge_holder.get("form") + if f is not None: f.Close() + except Exception: pass + b = _SymbolPickerBridge() + bridge_holder["form"] = panel_base.open_satellite_window( + "symbol_picker", + params={"items": items}, + title="Symbol einfuegen", + size=(640, 560), + bridge=b) def _cmd_create_symbol(self, p): """Platziert ein Library-Item (symbol/object) im Doc. Interactive diff --git a/src/ElementeApp.jsx b/src/ElementeApp.jsx index bfee380..f4ae695 100644 --- a/src/ElementeApp.jsx +++ b/src/ElementeApp.jsx @@ -9,9 +9,8 @@ import { openSwisstopo, openSwisstopoDialog, openOsmDialog, updateElement, deleteElement, openElementeUebersicht, openElementeProperties, saveOeffStyle, deleteOeffStyle, - listLibrary, createSymbol, + listLibrary, } from './lib/rhinoBridge' -import SymbolPicker from './components/SymbolPicker' const labelXs = { fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', @@ -329,16 +328,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) { const [treppeMenuOpen, setTreppeMenuOpen] = useState(false) const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false) const [traegerMenuOpen, setTraegerMenuOpen] = useState(false) - const [symbolPickerOpen, setSymbolPickerOpen] = useState(false) - const [libraryItems, setLibraryItems] = useState([]) const treppeWrapperRef = useRef(null) - - // Library-Items kommen via LIBRARY_LIST message vom Backend nach LIST_LIBRARY. - useEffect(() => { - onMessage('LIBRARY_LIST', ({ items }) => { - if (Array.isArray(items)) setLibraryItems(items) - }) - }, []) const dis = noGeschoss const baseHint = (label) => noGeschoss ? 'Erst im Ebenen-Manager ein Geschoss aktivieren' @@ -491,9 +481,9 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) { { listLibrary(); setSymbolPickerOpen(true) }} /> + onClick={() => listLibrary()} /> @@ -507,16 +497,6 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) { hint="Öffnet map.geo.admin.ch im Browser zur visuellen Inspektion" onClick={() => openSwisstopo('both')} /> - - {symbolPickerOpen && ( - { - setSymbolPickerOpen(false) - createSymbol(id) - }} - onClose={() => setSymbolPickerOpen(false)} /> - )} ) } diff --git a/src/SymbolPickerApp.jsx b/src/SymbolPickerApp.jsx new file mode 100644 index 0000000..d62c2e9 --- /dev/null +++ b/src/SymbolPickerApp.jsx @@ -0,0 +1,25 @@ +import { useState, useEffect } from 'react' +import SymbolPicker from './components/SymbolPicker' +import { notifyReady, onMessage, send } from './lib/rhinoBridge' + +export default function SymbolPickerApp() { + const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {} + const [items, setItems] = useState(initial.items || []) + + useEffect(() => { + onMessage('LIBRARY_LIST', ({ items }) => { + if (Array.isArray(items)) setItems(items) + }) + notifyReady() + const blockContext = (ev) => ev.preventDefault() + document.addEventListener('contextmenu', blockContext) + return () => document.removeEventListener('contextmenu', blockContext) + }, []) + + return ( + send('PICK', { id })} + onClose={() => send('CANCEL', {})} /> + ) +} diff --git a/src/components/SymbolPicker.jsx b/src/components/SymbolPicker.jsx index 06c1cdc..6e22178 100644 --- a/src/components/SymbolPicker.jsx +++ b/src/components/SymbolPicker.jsx @@ -63,7 +63,7 @@ function ItemCard({ item, onPick }) { ) } -export default function SymbolPicker({ items, onPick, onClose }) { +export default function SymbolPicker({ items, onPick, onClose, embedded = false }) { const [search, setSearch] = useState('') const [typeFilter, setTypeFilter] = useState('all') @@ -84,94 +84,108 @@ export default function SymbolPicker({ items, onPick, onClose }) { }) }, [placable, search, typeFilter]) - return ( -
-
e.stopPropagation()} - style={{ - width: '90%', maxWidth: 640, maxHeight: '80vh', - background: 'var(--bg-dialog)', - border: '1px solid var(--border)', - borderRadius: 12, - display: 'flex', flexDirection: 'column', - overflow: 'hidden', - color: 'var(--text-primary)', - fontFamily: 'var(--font)', fontSize: 11, - boxShadow: '0 8px 32px rgba(0,0,0,0.4)', - }}> - {/* Header */} -
- - - Symbol / Objekt einfuegen - - -
+ const innerStyle = embedded ? { + width: '100%', height: '100%', + background: 'var(--bg-dialog)', + display: 'flex', flexDirection: 'column', + overflow: 'hidden', + color: 'var(--text-primary)', + fontFamily: 'var(--font)', fontSize: 11, + } : { + width: '90%', maxWidth: 640, maxHeight: '80vh', + background: 'var(--bg-dialog)', + border: '1px solid var(--border)', + borderRadius: 12, + display: 'flex', flexDirection: 'column', + overflow: 'hidden', + color: 'var(--text-primary)', + fontFamily: 'var(--font)', fontSize: 11, + boxShadow: '0 8px 32px rgba(0,0,0,0.4)', + } - {/* Toolbar */} -
- setSearch(ev.target.value)} - placeholder="Suchen (Name oder Tag)…" - autoFocus - style={{ flex: 1, height: BAR_H, padding: '0 12px', - fontSize: 11 }} /> - setTypeFilter('all')} /> - setTypeFilter('symbol')} /> - setTypeFilter('object')} /> -
+ const content = ( +
+ {/* Header */} +
+ + + Symbol / Objekt einfuegen + + +
- {/* Grid */} -
- {filtered.length === 0 ? ( -
- {placable.length === 0 - ? 'Keine Symbole/Objekte in der Library.' - : 'Nichts gefunden.'} -
- ) : ( -
- {filtered.map(it => ( - - ))} -
- )} -
+ {/* Toolbar */} +
+ setSearch(ev.target.value)} + placeholder="Suchen (Name oder Tag)…" + autoFocus + style={{ flex: 1, height: BAR_H, padding: '0 12px', + fontSize: 11 }} /> + setTypeFilter('all')} /> + setTypeFilter('symbol')} /> + setTypeFilter('object')} /> +
- {/* Footer */} -
- Klick auf Item → im Viewport Punkt waehlen zum Platzieren. - {filtered.length > 0 && ( - · {filtered.length} / {placable.length} - )} -
+ {/* Grid */} +
+ {filtered.length === 0 ? ( +
+ {placable.length === 0 + ? 'Keine Symbole/Objekte in der Library.' + : 'Nichts gefunden.'} +
+ ) : ( +
+ {filtered.map(it => ( + + ))} +
+ )} +
+ + {/* Footer */} +
+ Klick auf Item → im Viewport Punkt waehlen zum Platzieren. + {filtered.length > 0 && ( + · {filtered.length} / {placable.length} + )}
) + + if (embedded) return content + return ( +
+
e.stopPropagation()}>{content}
+
+ ) } diff --git a/src/main.jsx b/src/main.jsx index afc4813..32ae9dc 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -6,6 +6,7 @@ import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx' import GeschossSettingsApp from './GeschossSettingsApp.jsx' import ProjectSettingsApp from './ProjectSettingsApp.jsx' import LibraryApp from './LibraryApp.jsx' +import SymbolPickerApp from './SymbolPickerApp.jsx' import EbenenSettingsApp from './EbenenSettingsApp.jsx' import GeschossDialogApp from './GeschossDialogApp.jsx' import LayerCombinationsApp from './LayerCombinationsApp.jsx' @@ -43,6 +44,7 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp : mode === 'geschoss_settings' ? GeschossSettingsApp : mode === 'project_settings' ? ProjectSettingsApp : mode === 'library' ? LibraryApp + : mode === 'symbol_picker' ? SymbolPickerApp : mode === 'ebenen_settings' ? EbenenSettingsApp : mode === 'geschoss_dialog' ? GeschossDialogApp : mode === 'layer_combinations' ? LayerCombinationsApp