!7 srecord Entry(S type, S text) {} cmodule DialogEditor { L entries = synchroList(); S globalID = aGlobalID(); transient JPanel stack; transient JTextArea taRaw; transient ReliableSingleThread rstFromRaw; start { rstFromRaw = dm_rstWithDelay(500, r { if (taRaw != null) fromRaw(getText(taRaw)) }); onChangeAndNow(r { if (taRaw != null) setText(taRaw, toRaw()); if (empty(entries) || nempty(last(entries).text)) { new Entry e; entries.add(e); change(); if (stack != null) addAndValidate(stack, makeLine(e)); } }); } visualize { stack = dynamicVStack2(); fillStack(); taRaw = jTypeWriterTextArea(toRaw()); main.onChange(taRaw, rstFromRaw); ret centerAndSouthWithMargins(jtabs( "Dialog" := jscroll_trackWidth(withRightMargin(stack)), "Raw" := withMargin(taRaw)), withLabel("Dialog ID:", dm_textField('globalID))); } void fillStack { if (stack == null) ret; removeAllComponents(stack); for (Entry e : cloneList(entries)) addAndValidate(stack, makeLine(e)); } JPanel makeLine(final Entry e) { final JComboBox cb = jcombobox(ai_lineTypes(), e.type); final JTextField tf = jtextfield(e.text); main.onChange(cb, r { e.type = selectedItem(cb); change(); }); main.onChange(tf, r { e.text = gtt(tf); change(); }); ret westAndCenterWithMargin(cb, tf); } S toRaw() { ret lines(nempties(map(entries, func(Entry e) -> S { bothEmpty(e.text, e.type) ? "" : or2(e.type, "?") + ": " + unnull(e.text) }))); } void fromRaw(S raw) { L entries = synchroList(); for (S type, text : parseColonProperties(raw)) { entries.add(nu Entry(type := ai_expandLineType(type), +text)); } if (empty(entries) || nempty(last(entries).text)) entries.add(new Entry); if (setField(+entries)) { print("entries changed: " + entries); fillStack(); } } }