Symbol-Picker als Satellite-Fenster (statt Modal im Elemente-Panel)
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 <noreply@anthropic.com>
This commit is contained in:
+39
-7
@@ -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
|
||||
|
||||
+3
-23
@@ -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 }) {
|
||||
<PillGroup label="Library">
|
||||
<PillButton icon="inventory_2" label="Symbol"
|
||||
hint={dis ? baseHint('Symbol') :
|
||||
'Library-Item auswählen · im Viewport Punkt klicken zum Platzieren'}
|
||||
'Library-Picker oeffnen · Item waehlen · im Viewport Punkt klicken zum Platzieren'}
|
||||
disabled={dis}
|
||||
onClick={() => { listLibrary(); setSymbolPickerOpen(true) }} />
|
||||
onClick={() => listLibrary()} />
|
||||
</PillGroup>
|
||||
|
||||
<PillGroup label="Importer">
|
||||
@@ -507,16 +497,6 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount }) {
|
||||
hint="Öffnet map.geo.admin.ch im Browser zur visuellen Inspektion"
|
||||
onClick={() => openSwisstopo('both')} />
|
||||
</PillGroup>
|
||||
|
||||
{symbolPickerOpen && (
|
||||
<SymbolPicker
|
||||
items={libraryItems}
|
||||
onPick={(id) => {
|
||||
setSymbolPickerOpen(false)
|
||||
createSymbol(id)
|
||||
}}
|
||||
onClose={() => setSymbolPickerOpen(false)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<SymbolPicker embedded
|
||||
items={items}
|
||||
onPick={(id) => send('PICK', { id })}
|
||||
onClose={() => send('CANCEL', {})} />
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0, zIndex: 200,
|
||||
background: 'var(--bg-overlay)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}} onClick={onClose}>
|
||||
<div onClick={(e) => 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 */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '10px 14px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<Icon name="inventory_2" size={14}
|
||||
style={{ color: 'var(--accent)' }} />
|
||||
<span style={{ flex: 1, fontWeight: 600, fontSize: 12 }}>
|
||||
Symbol / Objekt einfuegen
|
||||
</span>
|
||||
<BarButton icon="close" onClick={onClose} title="Schliessen" />
|
||||
</div>
|
||||
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 */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 14px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<input type="text" value={search}
|
||||
onChange={(ev) => setSearch(ev.target.value)}
|
||||
placeholder="Suchen (Name oder Tag)…"
|
||||
autoFocus
|
||||
style={{ flex: 1, height: BAR_H, padding: '0 12px',
|
||||
fontSize: 11 }} />
|
||||
<BarToggle label="Alle" active={typeFilter === 'all'}
|
||||
onClick={() => setTypeFilter('all')} />
|
||||
<BarToggle label="Symbole" active={typeFilter === 'symbol'}
|
||||
onClick={() => setTypeFilter('symbol')} />
|
||||
<BarToggle label="Objekte" active={typeFilter === 'object'}
|
||||
onClick={() => setTypeFilter('object')} />
|
||||
</div>
|
||||
const content = (
|
||||
<div style={innerStyle}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '10px 14px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<Icon name="inventory_2" size={14}
|
||||
style={{ color: 'var(--accent)' }} />
|
||||
<span style={{ flex: 1, fontWeight: 600, fontSize: 12 }}>
|
||||
Symbol / Objekt einfuegen
|
||||
</span>
|
||||
<BarButton icon="close" onClick={onClose} title="Schliessen" />
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div style={{ flex: 1, minHeight: 200, overflowY: 'auto',
|
||||
padding: 12 }}>
|
||||
{filtered.length === 0 ? (
|
||||
<div style={{ padding: 40, textAlign: 'center',
|
||||
color: 'var(--text-muted)' }}>
|
||||
{placable.length === 0
|
||||
? 'Keine Symbole/Objekte in der Library.'
|
||||
: 'Nichts gefunden.'}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(130px, 1fr))',
|
||||
gap: 8,
|
||||
}}>
|
||||
{filtered.map(it => (
|
||||
<ItemCard key={it.id} item={it} onPick={onPick} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Toolbar */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 14px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<input type="text" value={search}
|
||||
onChange={(ev) => setSearch(ev.target.value)}
|
||||
placeholder="Suchen (Name oder Tag)…"
|
||||
autoFocus
|
||||
style={{ flex: 1, height: BAR_H, padding: '0 12px',
|
||||
fontSize: 11 }} />
|
||||
<BarToggle label="Alle" active={typeFilter === 'all'}
|
||||
onClick={() => setTypeFilter('all')} />
|
||||
<BarToggle label="Symbole" active={typeFilter === 'symbol'}
|
||||
onClick={() => setTypeFilter('symbol')} />
|
||||
<BarToggle label="Objekte" active={typeFilter === 'object'}
|
||||
onClick={() => setTypeFilter('object')} />
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
padding: '8px 14px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
background: 'var(--bg-section)',
|
||||
fontSize: 10, color: 'var(--text-muted)',
|
||||
}}>
|
||||
Klick auf Item → im Viewport Punkt waehlen zum Platzieren.
|
||||
{filtered.length > 0 && (
|
||||
<span> · {filtered.length} / {placable.length}</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Grid */}
|
||||
<div style={{ flex: 1, minHeight: 200, overflowY: 'auto',
|
||||
padding: 12 }}>
|
||||
{filtered.length === 0 ? (
|
||||
<div style={{ padding: 40, textAlign: 'center',
|
||||
color: 'var(--text-muted)' }}>
|
||||
{placable.length === 0
|
||||
? 'Keine Symbole/Objekte in der Library.'
|
||||
: 'Nichts gefunden.'}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(130px, 1fr))',
|
||||
gap: 8,
|
||||
}}>
|
||||
{filtered.map(it => (
|
||||
<ItemCard key={it.id} item={it} onPick={onPick} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
padding: '8px 14px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
background: 'var(--bg-section)',
|
||||
fontSize: 10, color: 'var(--text-muted)',
|
||||
}}>
|
||||
Klick auf Item → im Viewport Punkt waehlen zum Platzieren.
|
||||
{filtered.length > 0 && (
|
||||
<span> · {filtered.length} / {placable.length}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (embedded) return content
|
||||
return (
|
||||
<div onClick={onClose}
|
||||
style={{
|
||||
position: 'fixed', inset: 0, zIndex: 200,
|
||||
background: 'var(--bg-overlay)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<div onClick={(e) => e.stopPropagation()}>{content}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user