Text-Editor: B/I/U sync via queryCommandState + RTF-Reihenfolge fixen

User: B/I/U Toggle-Buttons hinken hinterher oder zeigen invertiert.
Im WYSIWYG sieht alles richtig aus aber Rhino zeigt nur die LETZTE
gesetzte Schriftart fuer alles.

Fix 1 — B/I/U sync:
- selectionchange-Listener pollt jetzt queryCommandState fuer
  bold/italic/underline und syncen das an Toolbar-State
- toggleBold/Italic/Underline machen nur noch exec() — kein manuelles
  setBold(b => !b) mehr (war out-of-sync wenn execCommand wegen
  fehlender Selection nicht griff)
- B/I/U-Button-Highlight reflektiert jetzt die echte Cursor-Position

Fix 2 — RTF nimmt nur letzte Schrift:
Reihenfolge im _commit war falsch. Vorher: RichText → TextHeight →
_apply_font → _apply_align. _apply_font setzt te.Font (Default-Font)
NACH dem RichText → schiesst die per-Run-Fonts in der RTF tot.

Neue Reihenfolge:
1. PlainText (Initial-Content)
2. TextHeight, Font, Align (Defaults fuer ALLES)
3. RichText (ueberschreibt Defaults pro Run)

Plus Font-Tabelle in _runs_to_rtf jetzt mit \fnil\fcharset0 Marker
(Rhinos RTF-Parser kann sonst font-Eintraege ignorieren und Default
verwenden).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 02:00:03 +02:00
parent 51987dcc38
commit 1596bbd941
2 changed files with 33 additions and 20 deletions
+24 -17
View File
@@ -147,21 +147,13 @@ class TextEditorBridge(panel_base.BaseBridge):
except Exception: pass except Exception: pass
te.Plane = plane te.Plane = plane
# Rich-Text (Phase 2) wenn vorhanden + nicht-trivial, sonst Plain # REIHENFOLGE WICHTIG:
rtf = _runs_to_rtf( # 1. Plain-Text setzen (Initial-Content)
runs, # 2. Default-Font / TextHeight / Align setzen (gilt fuer alles)
st.get("font") or "Helvetica", # 3. RichText setzen (ZULETZT — die per-Run-Format-Codes
base_size_m=float(st.get("size") or 0.2)) if runs else None # ueberschreiben dann die Default-Properties)
applied_rtf = False # Andere Reihenfolge → Defaults schiessen die RTF-Runs weg.
if rtf: te.PlainText = text
try:
te.RichText = rtf
applied_rtf = True
except Exception as ex:
print("[TEXT-EDITOR] RichText set fail:", ex)
if not applied_rtf:
te.PlainText = text
try: te.TextHeight = float(st.get("size") or 0.2) try: te.TextHeight = float(st.get("size") or 0.2)
except Exception: pass except Exception: pass
text_create._apply_font( text_create._apply_font(
@@ -170,6 +162,19 @@ class TextEditorBridge(panel_base.BaseBridge):
st.get("bold"), st.get("italic"), st.get("bold"), st.get("italic"),
st.get("underline")) st.get("underline"))
text_create._apply_align(te, st.get("align") or "left") text_create._apply_align(te, st.get("align") or "left")
# RichText (Phase 2) — ZULETZT, ueberschreibt te.Font fuer
# alle Runs die eine eigene Schrift haben.
rtf = _runs_to_rtf(
runs,
st.get("font") or "Helvetica",
base_size_m=float(st.get("size") or 0.2)) if runs else None
if rtf:
try:
te.RichText = rtf
except Exception as ex:
print("[TEXT-EDITOR] RichText set fail:", ex)
for attr in ("FormatWidth", "TextWidth", "MaskWidth"): for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
try: try:
setattr(te, attr, width); break setattr(te, attr, width); break
@@ -315,10 +320,12 @@ def _runs_to_rtf(runs, default_font, base_size_m=0.20):
colors.append(rgb) colors.append(rgb)
return len(colors) return len(colors)
parts = ["{\\rtf1\\ansi\\deff0"] parts = ["{\\rtf1\\ansi\\ansicpg1252\\deff0"]
parts.append("{\\fonttbl") parts.append("{\\fonttbl")
for i, f in enumerate(fonts): for i, f in enumerate(fonts):
parts.append("{{\\f{} {};}}".format(i, f)) # Rhinos RTF-Parser will fnil/fcharset0 — sonst kann es passieren
# dass der font-Eintrag ignoriert wird und ein Default verwendet
parts.append("{{\\f{}\\fnil\\fcharset0 {};}}".format(i, f))
parts.append("}") parts.append("}")
if colors: if colors:
parts.append("{\\colortbl;") parts.append("{\\colortbl;")
+9 -3
View File
@@ -237,6 +237,10 @@ export default function TextEditorApp() {
if (ed.contains(sel.anchorNode)) { if (ed.contains(sel.anchorNode)) {
try { savedRangeRef.current = sel.getRangeAt(0).cloneRange() } try { savedRangeRef.current = sel.getRangeAt(0).cloneRange() }
catch (e) {} catch (e) {}
// B/I/U Toolbar-State an die echte Cursor-Position syncen
try { setBold(document.queryCommandState('bold')) } catch (e) {}
try { setItalic(document.queryCommandState('italic')) } catch (e) {}
try { setUnderline(document.queryCommandState('underline')) } catch (e) {}
} }
} }
document.addEventListener('selectionchange', onSelChange) document.addEventListener('selectionchange', onSelChange)
@@ -322,9 +326,11 @@ export default function TextEditorApp() {
fn() fn()
} }
const toggleBold = () => { setBold(b => !b); exec('bold') } // setState NICHT manuell — der selectionchange-Listener syncen das
const toggleItalic = () => { setItalic(b => !b); exec('italic') } // an die echte queryCommandState-Antwort, sonst hinkt's hinterher.
const toggleUnderline = () => { setUnderline(b => !b); exec('underline') } const toggleBold = () => exec('bold')
const toggleItalic = () => exec('italic')
const toggleUnderline = () => exec('underline')
const doAlign = (a) => { setAlign(a) const doAlign = (a) => { setAlign(a)
exec(a === 'center' ? 'justifyCenter' : a === 'right' ? 'justifyRight' : 'justifyLeft') } exec(a === 'center' ? 'justifyCenter' : a === 'right' ? 'justifyRight' : 'justifyLeft') }
const doSup = () => exec('superscript') const doSup = () => exec('superscript')