From e7a1753519bd80d7950e84cc2c734c623eed781c Mon Sep 17 00:00:00 2001 From: karim Date: Thu, 21 May 2026 01:38:23 +0200 Subject: [PATCH] Text-Editor: Default-Stile + Stil-Picker im Dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- rhino/text_create.py | 31 +++++++++++++++++++++++++++- rhino/text_editor.py | 5 +++++ src/TextEditorApp.jsx | 48 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/rhino/text_create.py b/rhino/text_create.py index 4a461c4..053a6c2 100644 --- a/rhino/text_create.py +++ b/rhino/text_create.py @@ -80,11 +80,40 @@ def save_settings(doc, partial): # Text-Stile (Presets, analog mass_style): doc.Strings JSON-Liste mit # 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): if doc is None: return [] try: 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) if not isinstance(items, list): return [] out = [] diff --git a/rhino/text_editor.py b/rhino/text_editor.py index 245cd6c..c03af8f 100644 --- a/rhino/text_editor.py +++ b/rhino/text_editor.py @@ -33,9 +33,14 @@ class TextEditorBridge(panel_base.BaseBridge): self._form_ref = form def _on_ready(self): + doc = Rhino.RhinoDoc.ActiveDoc + styles = [] + try: styles = text_create.list_styles(doc) + except Exception: pass self.send("INIT", { "settings": self._initial_settings, "fonts": self._fonts, + "styles": styles, }) def handle(self, data): diff --git a/src/TextEditorApp.jsx b/src/TextEditorApp.jsx index 50767d3..f7b066f 100644 --- a/src/TextEditorApp.jsx +++ b/src/TextEditorApp.jsx @@ -207,11 +207,13 @@ export default function TextEditorApp() { const [rotation, setRotation] = useState(0) const [maskMargin, setMaskMargin] = useState(0) const [symbolsOpen, setSymbolsOpen] = useState(false) + const [styles, setStyles] = useState([]) const editorRef = useRef(null) useEffect(() => { onMessage('INIT', (data) => { setFonts(data.fonts || []) + setStyles(data.styles || []) const s = data.settings || {} if (s.font) setFont(s.font) if (s.size != null) setSize(s.size) @@ -245,6 +247,37 @@ export default function TextEditorApp() { exec(a === 'center' ? 'justifyCenter' : a === 'right' ? 'justifyRight' : 'justifyLeft') } const doSup = () => exec('superscript') 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 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)]) @@ -279,8 +312,21 @@ export default function TextEditorApp() { background: 'var(--bg-panel)', boxSizing: 'border-box', overflow: 'hidden', }}> - {/* Toolbar Row 1: Font | Size | Color | Layer-Reset */} + {/* Toolbar Row 1: Stil | Font | Size | Color | Layer-Reset */}
+ { + 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={[ + , + ...styles.map(s => ( + + )), + ]} /> { setFont(v); exec('fontName', v) }} width={150} title="Schrift" options={