Text-Editor: Default-Stile + Stil-Picker im Dialog

User-Wunsch: vorgespeicherte Stile (Heading, Paragraph Helvetica/Georgia)
direkt im Editor anwendbar.

Backend (text_create.py):
- _DEFAULT_STYLES: 7 sinnvolle Architektur-Defaults — Titel (0.40m bold),
  Heading 1 (0.30m bold), Heading 2 (0.20m bold), Paragraph Helvetica
  (0.15m), Paragraph Georgia (0.15m Georgia), Notiz (0.10m italic),
  Bildlegende (0.08m italic)
- list_styles: seedet die Defaults beim ersten Zugriff falls noch keine
  Styles im Doc existieren (analog mass_style)
- Bestehende save_style/delete_style/apply_style funktionieren weiter

Backend (text_editor.py):
- INIT-Payload erweitert um styles[] (Liste aller verfuegbaren Stile
  mit id/name/font/size/bold/italic/underline/align)

Frontend (TextEditorApp.jsx):
- Neuer Stil-Picker als erstes Dropdown in Toolbar-Row 1 (150px)
- Optionen: "— Stil wählen —" + alle verfuegbaren Stile
- onChange: applyStyle(style) — setzt Toolbar-State + appliziert via
  execCommand auf die aktuelle Selektion im WYSIWYG-Editor (oder als
  Default fuer kommendes Tippen wenn keine Selektion)
- queryCommandState-Check fuer Bold/Italic/Underline damit nur toggled
  wird wenn nicht schon im gewuenschten State

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 01:38:23 +02:00
parent 54aa1c9e84
commit e7a1753519
3 changed files with 82 additions and 2 deletions
+30 -1
View File
@@ -80,11 +80,40 @@ def save_settings(doc, partial):
# Text-Stile (Presets, analog mass_style): doc.Strings JSON-Liste mit # Text-Stile (Presets, analog mass_style): doc.Strings JSON-Liste mit
# benannten Settings + aktiver Style-ID. # benannten Settings + aktiver Style-ID.
# Default-Stile fuer Architektur-Workflow — werden bei erstem list_styles
# automatisch erzeugt wenn das Doc noch keine eigenen hat.
_DEFAULT_STYLES = [
{"name": "Titel", "font": "Helvetica", "size": 0.40,
"bold": True, "italic": False, "underline": False, "align": "left"},
{"name": "Heading 1", "font": "Helvetica", "size": 0.30,
"bold": True, "italic": False, "underline": False, "align": "left"},
{"name": "Heading 2", "font": "Helvetica", "size": 0.20,
"bold": True, "italic": False, "underline": False, "align": "left"},
{"name": "Paragraph (Helvetica)", "font": "Helvetica", "size": 0.15,
"bold": False, "italic": False, "underline": False, "align": "left"},
{"name": "Paragraph (Georgia)", "font": "Georgia", "size": 0.15,
"bold": False, "italic": False, "underline": False, "align": "left"},
{"name": "Notiz", "font": "Helvetica", "size": 0.10,
"bold": False, "italic": True, "underline": False, "align": "left"},
{"name": "Bildlegende", "font": "Helvetica", "size": 0.08,
"bold": False, "italic": True, "underline": False, "align": "left"},
]
def list_styles(doc): def list_styles(doc):
if doc is None: return [] if doc is None: return []
try: try:
raw = doc.Strings.GetValue(_STYLES_KEY) raw = doc.Strings.GetValue(_STYLES_KEY)
if not raw: return [] if not raw:
# Seed Defaults bei erstem Zugriff
seeded = [_normalize(s) for s in _DEFAULT_STYLES]
for i, s in enumerate(seeded):
s["id"] = "ts_default_" + str(i)
s["name"] = _DEFAULT_STYLES[i]["name"]
try:
doc.Strings.SetString(_STYLES_KEY, json.dumps(seeded))
except Exception: pass
return seeded
items = json.loads(raw) items = json.loads(raw)
if not isinstance(items, list): return [] if not isinstance(items, list): return []
out = [] out = []
+5
View File
@@ -33,9 +33,14 @@ class TextEditorBridge(panel_base.BaseBridge):
self._form_ref = form self._form_ref = form
def _on_ready(self): def _on_ready(self):
doc = Rhino.RhinoDoc.ActiveDoc
styles = []
try: styles = text_create.list_styles(doc)
except Exception: pass
self.send("INIT", { self.send("INIT", {
"settings": self._initial_settings, "settings": self._initial_settings,
"fonts": self._fonts, "fonts": self._fonts,
"styles": styles,
}) })
def handle(self, data): def handle(self, data):
+47 -1
View File
@@ -207,11 +207,13 @@ export default function TextEditorApp() {
const [rotation, setRotation] = useState(0) const [rotation, setRotation] = useState(0)
const [maskMargin, setMaskMargin] = useState(0) const [maskMargin, setMaskMargin] = useState(0)
const [symbolsOpen, setSymbolsOpen] = useState(false) const [symbolsOpen, setSymbolsOpen] = useState(false)
const [styles, setStyles] = useState([])
const editorRef = useRef(null) const editorRef = useRef(null)
useEffect(() => { useEffect(() => {
onMessage('INIT', (data) => { onMessage('INIT', (data) => {
setFonts(data.fonts || []) setFonts(data.fonts || [])
setStyles(data.styles || [])
const s = data.settings || {} const s = data.settings || {}
if (s.font) setFont(s.font) if (s.font) setFont(s.font)
if (s.size != null) setSize(s.size) if (s.size != null) setSize(s.size)
@@ -245,6 +247,37 @@ export default function TextEditorApp() {
exec(a === 'center' ? 'justifyCenter' : a === 'right' ? 'justifyRight' : 'justifyLeft') } exec(a === 'center' ? 'justifyCenter' : a === 'right' ? 'justifyRight' : 'justifyLeft') }
const doSup = () => exec('superscript') const doSup = () => exec('superscript')
const doSub = () => exec('subscript') const doSub = () => exec('subscript')
// Stil anwenden: Toolbar-State setzen + (wenn Auswahl im Editor) via
// execCommand auf die Selektion applizieren.
const applyStyle = (style) => {
if (!style) return
// Toolbar-State synchronisieren
if (style.font) setFont(style.font)
if (style.size != null) setSize(style.size)
setBold(!!style.bold)
setItalic(!!style.italic)
setUnderline(!!style.underline)
if (style.align) setAlign(style.align)
// Auf Selektion applizieren (oder zumindest fuer kommendes Tippen)
try {
const sel = window.getSelection()
const hasSel = sel && sel.rangeCount > 0 && sel.toString().length > 0
document.execCommand('styleWithCSS', false, true)
if (style.font) document.execCommand('fontName', false, style.font)
// Bold/Italic/Underline togglen, falls Selektion da ist oder nicht
const wantBold = !!style.bold
const wantItal = !!style.italic
const wantUnd = !!style.underline
if (document.queryCommandState('bold') !== wantBold)
document.execCommand('bold')
if (document.queryCommandState('italic') !== wantItal)
document.execCommand('italic')
if (document.queryCommandState('underline') !== wantUnd)
document.execCommand('underline')
} catch (e) { console.error('applyStyle', e) }
editorRef.current?.focus()
}
const onColorPick = (hex) => { const onColorPick = (hex) => {
const m = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i) const m = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i)
if (m) setColor([parseInt(m[1],16), parseInt(m[2],16), parseInt(m[3],16)]) if (m) setColor([parseInt(m[1],16), parseInt(m[2],16), parseInt(m[3],16)])
@@ -279,8 +312,21 @@ export default function TextEditorApp() {
background: 'var(--bg-panel)', background: 'var(--bg-panel)',
boxSizing: 'border-box', overflow: 'hidden', boxSizing: 'border-box', overflow: 'hidden',
}}> }}>
{/* Toolbar Row 1: Font | Size | Color | Layer-Reset */} {/* Toolbar Row 1: Stil | Font | Size | Color | Layer-Reset */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<Dropdown value=""
onChange={(v) => {
const st = styles.find(s => s.id === v)
if (st) applyStyle(st)
}}
width={150}
title="Text-Stil anwenden (auf Selektion oder als Default fuer kommendes Tippen)"
options={[
<option key="__none__" value=""> Stil wählen </option>,
...styles.map(s => (
<option key={s.id} value={s.id}>{s.name}</option>
)),
]} />
<Dropdown value={font} onChange={(v) => { setFont(v); exec('fontName', v) }} <Dropdown value={font} onChange={(v) => { setFont(v); exec('fontName', v) }}
width={150} title="Schrift" width={150} title="Schrift"
options={ options={