From 5b5730a7df073659cbb6c326f748f7fcbe6625e8 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 18 Jan 2024 15:05:53 -0500
Subject: lots of changes to try to simplify API for viewPaths and related

---
 src/client/views/InkControlPtHandles.tsx | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

(limited to 'src/client/views/InkControlPtHandles.tsx')

diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index a5c2d99d2..7dd57e04d 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -27,7 +27,7 @@ export interface InkControlProps {
 export class InkControlPtHandles extends React.Component<InkControlProps> {
     @observable private _overControl = -1;
     get docView() {
-        return this.props.inkView.props.docViewPath().lastElement();
+        return this.props.inkView.DocumentView?.();
     }
 
     componentDidMount() {
@@ -58,11 +58,11 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
                     if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
                     const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
                     const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
-                    InkStrokeProperties.Instance.moveControlPtHandle(this.docView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
+                    this.docView && InkStrokeProperties.Instance.moveControlPtHandle(this.docView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
                     return false;
                 }),
                 action(() => {
-                    if (this.props.inkView.controlUndo) {
+                    if (this.props.inkView.controlUndo && this.docView) {
                         InkStrokeProperties.Instance.snapControl(this.docView, controlIndex);
                     }
                     this.props.inkView.controlUndo?.end();
@@ -78,11 +78,11 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
                         } else {
                             if (brokenIndices?.includes(equivIndex)) {
                                 if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
-                                InkStrokeProperties.Instance.snapHandleTangent(this.docView, equivIndex, handleIndexA, handleIndexB);
+                                this.docView && InkStrokeProperties.Instance.snapHandleTangent(this.docView, equivIndex, handleIndexA, handleIndexB);
                             }
                             if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
                                 if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
-                                InkStrokeProperties.Instance.snapHandleTangent(this.docView, controlIndex, handleIndexA, handleIndexB);
+                                this.docView && InkStrokeProperties.Instance.snapHandleTangent(this.docView, controlIndex, handleIndexA, handleIndexB);
                             }
                         }
                         this.props.inkView.controlUndo?.end();
@@ -113,7 +113,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
     @action
     onDelete = (e: KeyboardEvent) => {
         if (['-', 'Backspace', 'Delete'].includes(e.key)) {
-            InkStrokeProperties.Instance.deletePoints(this.docView, e.shiftKey);
+            this.docView && InkStrokeProperties.Instance.deletePoints(this.docView, e.shiftKey);
             e.stopPropagation();
         }
     };
-- 
cgit v1.2.3-70-g09d2


From 8ac814bbb81b690a6a10f5a07aa5ce0e8cafe283 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 30 Jan 2024 00:40:43 -0500
Subject: changed dropConverter to keep title of dropped Doc. added paintFunc
 node/ checkbox view to formatted text.  changed paintFunc to be computed
 based on layouytfieldkey being text in a freeformview.  changed some
 inputRules to apply to code blocks.  changed : contextmenu to allow regular
 note to be created.  changed experimental tools to be user tmeplate tools. 
 fixed focus on search bar when opening context menu

---
 package-lock.json                                  |  10 +-
 package.json                                       |   2 +-
 src/Utils.ts                                       |   8 +-
 src/client/documents/Documents.ts                  |  39 ++++-
 src/client/util/CurrentUserUtils.ts                |  70 ++++-----
 src/client/util/DropConverter.ts                   |  16 +-
 src/client/views/ContextMenu.tsx                   |  18 ++-
 src/client/views/GlobalKeyHandler.ts               |   2 +-
 src/client/views/InkControlPtHandles.tsx           |   2 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   6 +-
 src/client/views/nodes/DocumentView.tsx            |  10 +-
 .../views/nodes/FontIconBox/FontIconBox.scss       |  10 ++
 src/client/views/nodes/FontIconBox/FontIconBox.tsx |   8 +-
 .../nodes/formattedText/DashDocCommentView.tsx     |  39 ++++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   7 +-
 .../views/nodes/formattedText/PaintButtonView.tsx  | 113 ++++++++++++++
 .../views/nodes/formattedText/RichTextRules.ts     | 164 +++++++++++++--------
 src/client/views/nodes/formattedText/nodes_rts.ts  |  12 ++
 src/fields/Doc.ts                                  |  10 +-
 19 files changed, 397 insertions(+), 149 deletions(-)
 create mode 100644 src/client/views/nodes/formattedText/PaintButtonView.tsx

(limited to 'src/client/views/InkControlPtHandles.tsx')

diff --git a/package-lock.json b/package-lock.json
index 3564c4f1f..aa4108243 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -154,7 +154,7 @@
                 "prosemirror-commands": "^1.5.2",
                 "prosemirror-find-replace": "^0.9.0",
                 "prosemirror-history": "^1.3.2",
-                "prosemirror-inputrules": "^1.3.0",
+                "prosemirror-inputrules": "github:bobzel/prosemirror-inputrules#3b3b3e0b1a1dcf80490d81675206369b6be96276",
                 "prosemirror-keymap": "^1.2.2",
                 "prosemirror-model": "^1.19.3",
                 "prosemirror-schema-list": "^1.3.0",
@@ -28700,8 +28700,9 @@
         },
         "node_modules/prosemirror-inputrules": {
             "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.3.0.tgz",
-            "integrity": "sha512-z1GRP2vhh5CihYMQYsJSa1cOwXb3SYxALXOIfAkX8nZserARtl9LiL+CEl+T+OFIsXc3mJIHKhbsmRzC0HDAXA==",
+            "resolved": "git+ssh://git@github.com/bobzel/prosemirror-inputrules.git#3b3b3e0b1a1dcf80490d81675206369b6be96276",
+            "integrity": "sha512-l81tcwS7ugPJEvCy78RtEvR2/2mVaTkFHJD9ACRxRDXhrfPWV0FkFYBAO7GolN4XlzIbIsal2MVvq2baLQ2guw==",
+            "license": "MIT",
             "dependencies": {
                 "prosemirror-state": "^1.0.0",
                 "prosemirror-transform": "^1.0.0"
@@ -32239,7 +32240,8 @@
         "node_modules/text-table": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
+            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+            "dev": true
         },
         "node_modules/textarea-caret": {
             "version": "3.1.0",
diff --git a/package.json b/package.json
index 760099bd5..f8144a0b3 100644
--- a/package.json
+++ b/package.json
@@ -237,7 +237,7 @@
         "prosemirror-commands": "^1.5.2",
         "prosemirror-find-replace": "^0.9.0",
         "prosemirror-history": "^1.3.2",
-        "prosemirror-inputrules": "^1.3.0",
+        "prosemirror-inputrules": "github:bobzel/prosemirror-inputrules#3b3b3e0b1a1dcf80490d81675206369b6be96276",
         "prosemirror-keymap": "^1.2.2",
         "prosemirror-model": "^1.19.3",
         "prosemirror-schema-list": "^1.3.0",
diff --git a/src/Utils.ts b/src/Utils.ts
index 502cf7db7..e8bd35ac4 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -514,8 +514,8 @@ export function intersectRect(r1: { left: number; top: number; width: number; he
     return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
 }
 
-export function stringHash(s?:string) {
-    return !s? undefined:  Math.abs(s.split('').reduce((a: any, b: any) => ((a) => a & a)((a << 5) - a + b.charCodeAt(0)),0));
+export function stringHash(s?: string) {
+    return !s ? undefined : Math.abs(s.split('').reduce((a: any, b: any) => (a => a & a)((a << 5) - a + b.charCodeAt(0)), 0));
 }
 
 export function percent2frac(percent: string) {
@@ -852,9 +852,11 @@ export function setupMoveUpEvents(
     (target as any)._downX = (target as any)._lastX = e.clientX;
     (target as any)._downY = (target as any)._lastY = e.clientY;
     (target as any)._noClick = false;
+    var moving = false;
 
     const _moveEvent = (e: PointerEvent): void => {
-        if (Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) {
+        if (moving || Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) {
+            moving = true;
             if ((target as any)._doubleTime) {
                 clearTimeout((target as any)._doubleTime);
                 (target as any)._doubleTime = undefined;
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ac418ecfe..cc983ffa7 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1667,10 +1667,37 @@ export namespace DocUtils {
     }
 
     export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void {
+        const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data)
+            .filter(btnDoc => !btnDoc.hidden)
+            .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
+            .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc.title)
+            .map((dragDoc, i) => ({
+                description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
+                event: undoable((args: { x: number; y: number }) => {
+                    const newDoc = DocUtils.copyDragFactory(dragDoc);
+                    if (newDoc) {
+                        newDoc.author = Doc.CurrentUserEmail;
+                        newDoc.x = x;
+                        newDoc.y = y;
+                        EquationBox.SelectOnLoad = newDoc[Id];
+                        if (newDoc.type === DocumentType.RTF) FormattedTextBox.SetSelectOnLoad(newDoc);
+                        if (pivotField) {
+                            newDoc[pivotField] = pivotValue;
+                        }
+                        docAdder?.(newDoc);
+                    }
+                }, StrCast(dragDoc.title)),
+                icon: Doc.toIcon(dragDoc),
+            })) as ContextMenuProps[];
+        ContextMenu.Instance.addItem({
+            description: 'Create document',
+            subitems: documentList,
+            icon: 'file',
+        });
         !simpleMenu &&
             ContextMenu.Instance.addItem({
-                description: 'Quick Notes',
-                subitems: DocListCast((Doc.UserDoc()['template_notes'] as Doc).data).map((note, i) => ({
+                description: 'Styled Notes',
+                subitems: DocListCast((Doc.UserDoc().template_notes as Doc).data).map((note, i) => ({
                     description: ':' + StrCast(note.title),
                     event: undoable((args: { x: number; y: number }) => {
                         const textDoc = Docs.Create.TextDocument('', {
@@ -1691,14 +1718,14 @@ export namespace DocUtils {
                 })) as ContextMenuProps[],
                 icon: 'sticky-note',
             });
-        const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data)
+        const userDocList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[1]?.data)
             .filter(btnDoc => !btnDoc.hidden)
             .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
             .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyNote && doc.title)
             .map((dragDoc, i) => ({
                 description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
                 event: undoable((args: { x: number; y: number }) => {
-                    const newDoc = DocUtils.copyDragFactory(dragDoc);
+                    const newDoc = DocUtils.delegateDragFactory(dragDoc);
                     if (newDoc) {
                         newDoc.author = Doc.CurrentUserEmail;
                         newDoc.x = x;
@@ -1714,8 +1741,8 @@ export namespace DocUtils {
                 icon: Doc.toIcon(dragDoc),
             })) as ContextMenuProps[];
         ContextMenu.Instance.addItem({
-            description: 'Create document',
-            subitems: documentList,
+            description: 'User Templates',
+            subitems: userDocList,
             icon: 'file',
         });
     } // applies a custom template to a document.  the template is identified by it's short name (e.g, slideView not layout_slideView)
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 8f46f844c..31f0308b7 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -61,49 +61,16 @@ export let resolvedPorts: { server: number, socket: number };
 export class CurrentUserUtils {
 
     // initializes experimental advanced template views - slideView,  headerView
-    static setupExperimentalTemplateButtons(doc: Doc, tempDocs:Opt<Doc>, userBtns:Doc[]) {
-        const requiredTypeNameFields:{btnOpts:DocumentOptions, templateOpts:DocumentOptions, template:(opts:DocumentOptions) => Doc}[] = [
-            {
-                btnOpts: { title: "slide", icon: "address-card" }, 
-                templateOpts: { _width: 400, _height: 300, title: "slideView",  _xMargin: 3, _yMargin: 3, isSystem: true }, 
-                template: (opts:DocumentOptions) => Docs.Create.MultirowDocument(
-                    [
-                        Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200,  isSystem: true }),
-                        Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true,  _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) })
-                    ], opts)
-            },
-            {
-                btnOpts: { title: "mobile", icon: "mobile" }, 
-                templateOpts: { title: "NEW MOBILE BUTTON", onClick: undefined, }, 
-                template: (opts:DocumentOptions) => this.mobileButton(opts,
-                        [this.createToolButton({ ignoreClick: true, icon: "mobile", backgroundColor: "transparent" }),
-                        this.mobileTextContainer({},
-                            [this.mobileButtonText({}, "NEW MOBILE BUTTON"), this.mobileButtonInfo({}, "You can customize this button and make it your own.")])
-                        ]
-                )
-            },
-        ];
-        const requiredTypes = [...requiredTypeNameFields.map(({ btnOpts, template, templateOpts }) => {
-            const tempBtn =  DocListCast(tempDocs?.data)?.find(doc => doc.title === btnOpts.title);
-            const reqdScripts = { onDragStart:  '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }' };
-            const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => {
-                if (templateBtn) {
-                    DocUtils.AssignOpts(templateBtn,btnOpts);
-                    DocUtils.AssignDocField(templateBtn, "dragFactory", opts => template(opts), templateOptions);
-                }
-                return templateBtn;
-            };
-            return DocUtils.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: MakeTemplate(template(templateOpts))}), reqdScripts);
-        }), ...userBtns];
-
+    static setupUserDocumentCreatorButtons(doc: Doc, userDocTemplates: Opt<Doc>) {
+        const userTemplates = DocListCast(userDocTemplates?.data).filter(doc => !Doc.IsSystem(doc));
         const reqdOpts:DocumentOptions = {
-            title: "Experimental Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true,
+            title: "User Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true, hidden: false,
             _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, isSystem: true, _forceActive: true,
             _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
         };
         const reqdScripts = { dropConverter : "convertToButtons(dragData)" };
-        const reqdFuncs = { hidden: "IsNoviceMode()" };
-        return DocUtils.AssignScripts(DocUtils.AssignOpts(tempDocs, reqdOpts, requiredTypes) ?? Docs.Create.MasonryDocument(requiredTypes, reqdOpts), reqdScripts, reqdFuncs);
+        const reqdFuncs = { /* hidden: "IsNoviceMode()"  */ };
+        return DocUtils.AssignScripts(DocUtils.AssignOpts(userDocTemplates, reqdOpts, userTemplates) ?? Docs.Create.MasonryDocument(userTemplates, reqdOpts), reqdScripts, reqdFuncs);
     }
 
     /// Initializes templates for editing click funcs of a document 
@@ -148,7 +115,7 @@ export class CurrentUserUtils {
     static setupNoteTemplates(doc: Doc, field="template_notes") {
         const tempNotes = DocCast(doc[field]);
         const reqdTempOpts:DocumentOptions[] = [
-            { noteType: "Note", backgroundColor: "yellow", icon: "sticky-note"},
+            { noteType: "Postit", backgroundColor: "yellow", icon: "sticky-note"},
             { noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" },
             { noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }];
         const reqdNoteList = reqdTempOpts.map(opts => {
@@ -257,6 +224,16 @@ export class CurrentUserUtils {
             MakeTemplate(Doc.GetProto(header), true, "Untitled Header");
             return header; 
         }
+        const slideView = (opts:DocumentOptions) => {
+            const slide = Docs.Create.MultirowDocument(
+                    [
+                        Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200,  isSystem: true }),
+                        Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true,  _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) })
+                    ], opts);
+
+            MakeTemplate(Doc.GetProto(slide), true, "Untitled Slide View");
+            return slide; 
+        }
         const emptyThings:{key:string,  // the field name where the empty thing will be stored
                           opts:DocumentOptions,   // the document options that are required for the empty thing
                           funcs?:{[key:string]: any},  // computed fields that are rquired for the empth thing
@@ -279,6 +256,7 @@ export class CurrentUserUtils {
             {key: "Script",     creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
             {key: "DataViz",    creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts),   opts: { _width: 300, _height: 300 }},
             {key: "Header",     creator: headerTemplate,                                    opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeView_HideUnrendered: true}},
+            {key: "ViewSlide",  creator: slideView,                                         opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}},
             {key: "Trail",      creator: Docs.Create.PresDocument,                          opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }},
             {key: "Tab",        creator: opts => Docs.Create.FreeformDocument([], opts),    opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }},
             {key: "Slide",      creator: opts => Docs.Create.TreeDocument([], opts),        opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree, 
@@ -307,6 +285,7 @@ export class CurrentUserUtils {
             { toolTip: "Tap or drag to create a scripting box",   title: "Script", icon: "terminal",    dragFactory: doc.emptyScript as Doc,     clickFactory: DocCast(doc.emptyScript),                                             funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data viz node",   title: "DataViz", icon: "chart-bar",  dragFactory: doc.emptyDataViz as Doc,    clickFactory: DocCast(doc.emptyDataViz)},
             { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "file",     dragFactory: doc.emptySlide as Doc,      clickFactory: DocCast(doc.emptySlide),      openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
+            { toolTip: "Tap or drag to create a view slide",      title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc,clickFactory: DocCast(doc.emptyViewSlide),openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data note",       title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader),     openFactoryAsDelegate: true,            funcs: { hidden: "IsNoviceMode()"} },
             { toolTip: "Toggle a Calculator REPL",                title: "replviewer",  icon: "calculator",                                      clickFactory: '<ScriptingRepl />' as any,   openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
            // { toolTip: "Toggle an UndoStack",                     title: "undostacker", icon: "calculator",                                      clickFactory: "<UndoStack />" as any,         openFactoryLocation: OpenWhere.overlay},
@@ -330,7 +309,7 @@ export class CurrentUserUtils {
         });
 
         const reqdOpts:DocumentOptions = {
-            title: "Basic Item Creators", _layout_showTitle: "title", _xMargin: 0, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true,
+            title: "Document Creators", _layout_showTitle: "title", _xMargin: 0, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true,
             _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true,
              childDragAction: 'embed' 
         };
@@ -464,16 +443,17 @@ export class CurrentUserUtils {
     /// Initializes the panel of draggable tools that is opened from the left sidebar.
     static setupToolsBtnPanel(doc: Doc, field:string) {
         const myTools = DocCast(doc[field]);
-        const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, DocListCast(myTools?.data)?.length ? DocListCast(myTools.data)[0]:undefined);
-        const tempBtns = DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined;
-        const userTemplateBtns = DocListCast(tempBtns?.data).filter(btn => !btn.isSystem);
-        const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc, tempBtns, userTemplateBtns);
+        const allTools = DocListCast(myTools?.data);
+        const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, allTools?.length ? allTools[0]:undefined);
+        const userTools = allTools && allTools?.length > 1 ? allTools[1]:undefined;
+        const userBtns = CurrentUserUtils.setupUserDocumentCreatorButtons(doc, userTools);
+        //doc.myUserBtns = new PrefetchProxy(userBtns);
         const reqdToolOps:DocumentOptions = {
             title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0",
             layout_explainer: "This is a palette of documents that can be created.",
             _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, 
         };
-        return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, templateBtns]);
+        return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, userBtns]);
     }
 
     /// initializes the left sidebar dashboard pane
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 8c3b56452..ba981145d 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -3,7 +3,7 @@ import { DocData } from '../../fields/DocSymbols';
 import { ObjectField } from '../../fields/ObjectField';
 import { RichTextField } from '../../fields/RichTextField';
 import { listSpec } from '../../fields/Schema';
-import { ScriptField } from '../../fields/ScriptField';
+import { ComputedField, ScriptField } from '../../fields/ScriptField';
 import { Cast, StrCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
 import { Docs } from '../documents/Documents';
@@ -39,15 +39,12 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und
             any = makeTemplate(d, false) || any;
         }
     });
-    if (first) {
-        if (!docs.length) {
-            // bcz: feels hacky : if the root level document has items, it's not a field template
-            any = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData]) || any;
-        }
-    }
-    if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) {
+    if (first && !docs.length) {
+        // bcz: feels hacky : if the root level document has items, it's not a field template
+        any = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData], true) || any;
+    } else if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) {
         if (!StrCast(layoutDoc.title).startsWith('-')) {
-            any = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData]);
+            any = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData], true);
         }
     }
     rename && (doc.title = rename);
@@ -82,6 +79,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
                 icon: 'bolt',
                 isSystem: false,
             });
+            dbox.title = ComputedField.MakeFunction('this.dragFactory.title');
             dbox.dragFactory = layoutDoc;
             dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
             dbox.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory)');
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 8dcdd80e5..8c3c9df2e 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -183,11 +183,12 @@ export class ContextMenu extends ObservableReactComponent<{}> {
 
     @computed get menuItems() {
         if (!this._searchString) {
-            return this._items.map((item, ind) => <ContextMenuItem {...item} noexpand={this.itemsNeedSearch ? true : (item as any).noexpand} key={ind + item.description} closeMenu={this.closeMenu} />);
+            return this._items.map((item, ind) => <ContextMenuItem key={item.description + ind} {...item} noexpand={this.itemsNeedSearch ? true : (item as any).noexpand} closeMenu={this.closeMenu} />);
         }
         return this.filteredItems.map((value, index) =>
             Array.isArray(value) ? (
                 <div
+                    key={index + value.join(' -> ')}
                     className="contextMenu-group"
                     style={{
                         background: StrCast(SettingsManager.userVariantColor),
@@ -204,7 +205,10 @@ export class ContextMenu extends ObservableReactComponent<{}> {
         return this._showSearch ? 1 : this._items.reduce((p, mi) => p + ((mi as any).noexpand ? 1 : (mi as any).subitems?.length || 1), 0) > 15;
     }
 
+    _searchRef = React.createRef<HTMLInputElement>(); // bcz: we shouldn't need this, since we set autoFocus on the <input> tag, but for some reason we do...
+
     render() {
+        this.itemsNeedSearch && setTimeout(() => this._searchRef.current?.focus());
         return (
             <div
                 className="contextMenu-cont"
@@ -226,7 +230,17 @@ export class ContextMenu extends ObservableReactComponent<{}> {
                         <span className="icon-background">
                             <FontAwesomeIcon icon="search" size="lg" />
                         </span>
-                        <input style={{ color: 'black' }} className="contextMenu-item contextMenu-description search" type="text" placeholder="Filter Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus />
+                        <input
+                            ref={this._searchRef}
+                            style={{ color: 'black' }}
+                            className="contextMenu-item contextMenu-description search"
+                            type="text"
+                            placeholder="Filter Menu..."
+                            value={this._searchString}
+                            onKeyDown={this.onKeyDown}
+                            onChange={this.onChange}
+                            autoFocus
+                        />
                     </span>
                 )}
                 {this.menuItems}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d134d9e7b..733383002 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -160,7 +160,7 @@ export class KeyManager {
                     if (LightboxView.LightboxDoc) {
                         LightboxView.Instance.SetLightboxDoc(undefined);
                         SelectionManager.DeselectAll();
-                    } else DocumentDecorations.Instance.onCloseClick(true);
+                    } else if (!window.getSelection()?.toString()) DocumentDecorations.Instance.onCloseClick(true);
                     return { stopPropagation: true, preventDefault: true };
                 }
                 break;
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 7dd57e04d..31b13d2c8 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -189,6 +189,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
     @observable _overStart: boolean = false;
     @observable _overEnd: boolean = false;
 
+    _throttle = 0; // need to throttle dragging since the position may change when the control points change.  this allows the stroke to settle so that we don't get increasingly bad jitter
     @action
     dragRotate = (e: React.PointerEvent, pt1: () => { X: number; Y: number }, pt2: () => { X: number; Y: number }) => {
         SnappingManager.SetIsDragging(true);
@@ -196,6 +197,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             this,
             e,
             action(e => {
+                if (this._throttle++ % 2 !== 0) return false;
                 if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('stretch ink');
                 // compute stretch factor by finding scaling along axis between start and end points
                 const p1 = pt1();
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 82ada4fb5..54e8b08b6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import { DateField } from '../../../../fields/DateField';
-import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
 import { DocData, Height, Width } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField';
@@ -53,6 +53,7 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
+import { RichTextField } from '../../../../fields/RichTextField';
 
 export interface collectionFreeformViewProps {
     NativeWidth?: () => number;
@@ -75,7 +76,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, '');
     @computed get paintFunc() {
-        const paintFunc = StrCast(this.Document[DocData].paintFunc).trim();
+        const field = this.layoutDoc[this.fieldKey];
+        const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as Field)).trim();
         return !paintFunc
             ? ''
             : `const dashDiv = document.querySelector('#${this._paintedId}'); 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2e1ba2a7e..51f4b1a68 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -456,11 +456,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc');
     setToggleDetail = undoable(
-        () =>
+        (defaultLayout: string) =>
             (this.Document.onClick = ScriptField.MakeScript(
                 `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
                     .replace('layout_', '')
-                    .replace(/^layout$/, 'detail')}")`,
+                    .replace(/^layout$/, 'detail')}", "${defaultLayout}")`,
                 { documentView: 'any' }
             )),
         'set toggle detail'
@@ -1212,7 +1212,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     };
     public noOnClick = () => this._docViewInternal?.noOnClick();
     public toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => this._docViewInternal?.toggleFollowLink(zoom, setTargetToggle);
-    public setToggleDetail = () => this._docViewInternal?.setToggleDetail();
+    public setToggleDetail = (defaultLayout = '') => this._docViewInternal?.setToggleDetail(defaultLayout);
     public onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => this._docViewInternal?.onContextMenu?.(e, pageX, pageY);
     public cleanupPointerEvents = () => this._docViewInternal?.cleanupPointerEvents();
     public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource);
@@ -1460,8 +1460,8 @@ ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView
     LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0);
 });
 
-ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
-    if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout');
+ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string, defaultLayout = '') {
+    if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true);
     else dv.switchViews(true, detailLayoutKeySuffix, undefined, true);
 });
 
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index db2ffa756..2db285910 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -1,5 +1,15 @@
 @import '../../global/globalCssVariables.module.scss';
 
+// bcz: something's messed up with the IconButton css.  this mostly fixes the fit-all button, the color buttons, the undo +/- expander and the dropdown doc type list (eg 'text')
+.iconButton-container {
+    width: unset !important;
+    min-width: 30px !important;
+    height: unset !important;
+    min-height: 30px;
+    .color {
+        height: 3px !important;
+    }
+}
 .menuButton {
     height: 100%;
     display: flex;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 8290e102c..3577cc8d9 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -381,7 +381,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
             case ButtonType.ColorButton:        return this.colorButton; 
             case ButtonType.MultiToggleButton:  return this.multiToggleButton;
             case ButtonType.ToggleButton:       return this.toggleButton;
-            case ButtonType.ClickButton:
+            case ButtonType.ClickButton:return <IconButton {...btnProps} size={Size.MEDIUM} color={color} />;
             case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} />;
             case ButtonType.TextButton: return <Button     {...btnProps} color={color} 
                         background={SettingsManager.userBackgroundColor} text={StrCast(this.dataDoc.buttonText)}/>;
@@ -392,6 +392,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
     };
 
     render() {
-        return <div onContextMenu={this.specificContextMenu}>{this.renderButton()}</div>;
+        return (
+            <div style={{ margin: 'auto', width: '100%' }} onContextMenu={this.specificContextMenu}>
+                {this.renderButton()}
+            </div>
+        );
     }
 }
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index b7d2a24c2..a72ed1813 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -3,6 +3,8 @@ import * as ReactDOM from 'react-dom/client';
 import { Doc } from '../../../../fields/Doc';
 import { DocServer } from '../../../DocServer';
 import * as React from 'react';
+import { IReactionDisposer, computed, reaction } from 'mobx';
+import { NumCast } from '../../../../fields/Types';
 
 // creates an inline comment in a note when '>>' is typed.
 // the comment sits on the right side of the note and vertically aligns with its anchor in the text.
@@ -10,8 +12,10 @@ import * as React from 'react';
 export class DashDocCommentView {
     dom: HTMLDivElement; // container for label and value
     root: any;
+    node: any;
 
     constructor(node: any, view: any, getPos: any) {
+        this.node = node;
         this.dom = document.createElement('div');
         this.dom.style.width = node.attrs.width;
         this.dom.style.height = node.attrs.height;
@@ -32,10 +36,14 @@ export class DashDocCommentView {
         };
 
         this.root = ReactDOM.createRoot(this.dom);
-        this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docId={node.attrs.docId} />);
+        this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} setHeight={this.setHeight} docId={node.attrs.docId} />);
         (this as any).dom = this.dom;
     }
 
+    setHeight = (hgt: number) => {
+        !this.node.attrs.reflow && DocServer.GetRefField(this.node.attrs.docId).then(doc => doc instanceof Doc && (this.dom.style.height = hgt + ''));
+    };
+
     destroy() {
         this.root.unmount();
     }
@@ -51,9 +59,15 @@ interface IDashDocCommentViewInternal {
     docId: string;
     view: any;
     getPos: any;
+    setHeight: (height: number) => void;
 }
 
 export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal> {
+    _reactionDisposer: IReactionDisposer | undefined;
+
+    @computed get _dashDoc() {
+        return DocServer.GetRefField(this.props.docId);
+    }
     constructor(props: any) {
         super(props);
         this.onPointerLeaveCollapsed = this.onPointerLeaveCollapsed.bind(this);
@@ -61,15 +75,32 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
         this.onPointerUpCollapsed = this.onPointerUpCollapsed.bind(this);
         this.onPointerDownCollapsed = this.onPointerDownCollapsed.bind(this);
     }
+    componentDidMount(): void {
+        this._reactionDisposer?.();
+        this._dashDoc.then(
+            doc =>
+                doc instanceof Doc &&
+                (this._reactionDisposer = reaction(
+                    () => NumCast((doc as Doc)._height),
+                    hgt => this.props.setHeight(hgt),
+                    {
+                        fireImmediately: true,
+                    }
+                ))
+        );
+    }
+    componentWillUnmount(): void {
+        this._reactionDisposer?.();
+    }
 
     onPointerLeaveCollapsed(e: any) {
-        DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+        this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
         e.preventDefault();
         e.stopPropagation();
     }
 
     onPointerEnterCollapsed(e: any) {
-        DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+        this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
         e.preventDefault();
         e.stopPropagation();
     }
@@ -82,7 +113,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
             const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
             this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs
             setTimeout(() => {
-                expand && DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+                expand && this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
                 try {
                     this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1))));
                 } catch (e) {}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 0b857794b..973f90501 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -68,6 +68,7 @@ import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
 import { CollectionView } from '../../collections/CollectionView';
+import { PaintButtonView } from './PaintButtonView';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 @observer
 export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -1410,6 +1411,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     dashField(node: any, view: any, getPos: any) {
                         return new DashFieldView(node, view, getPos, self);
                     },
+                    paintButton(node: any, view: any, getPos: any) {
+                        return new PaintButtonView(node, view, getPos, self);
+                    },
                     equation(node: any, view: any, getPos: any) {
                         return new EquationView(node, view, getPos, self);
                     },
@@ -1538,15 +1542,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         this._downX = e.clientX;
         this._downY = e.clientY;
         this._downTime = Date.now();
-        this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false;
         FormattedTextBoxComment.textBox = this;
         if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) {
             if (e.clientX < this.ProseRef!.getBoundingClientRect().right) {
                 // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes.
                 e.stopPropagation(); // if the text box's content is active, then it consumes all down events
                 document.addEventListener('pointerup', this.onSelectEnd);
+                (this.ProseRef?.children?.[0] as any).focus();
             }
         }
+        this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false;
         if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
             e.preventDefault();
         }
diff --git a/src/client/views/nodes/formattedText/PaintButtonView.tsx b/src/client/views/nodes/formattedText/PaintButtonView.tsx
new file mode 100644
index 000000000..74423c772
--- /dev/null
+++ b/src/client/views/nodes/formattedText/PaintButtonView.tsx
@@ -0,0 +1,113 @@
+import { action, computed, IReactionDisposer, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import * as ReactDOM from 'react-dom/client';
+import { Doc } from '../../../../fields/Doc';
+import { ObservableReactComponent } from '../../ObservableReactComponent';
+import './DashFieldView.scss';
+import { FormattedTextBox } from './FormattedTextBox';
+import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { CollectionView } from '../../collections/CollectionView';
+import { StrCast } from '../../../../fields/Types';
+
+export class PaintButtonView {
+    dom: HTMLDivElement; // container for label and value
+    root: any;
+    node: any;
+    tbox: FormattedTextBox;
+
+    constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+        this.node = node;
+        this.tbox = tbox;
+        this.dom = document.createElement('div');
+        this.dom.style.width = node.attrs.width;
+        this.dom.style.height = node.attrs.height;
+        this.dom.style.position = 'relative';
+        this.dom.style.display = 'inline-block';
+        this.dom.onkeypress = function (e: any) {
+            e.stopPropagation();
+        };
+        this.dom.onkeydown = function (e: any) {
+            e.stopPropagation();
+        };
+        this.dom.onkeyup = function (e: any) {
+            e.stopPropagation();
+        };
+        this.dom.onmousedown = function (e: any) {
+            e.stopPropagation();
+        };
+
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(<PaintButtonViewInternal node={node} getPos={getPos} width={node.attrs.width} height={node.attrs.height} tbox={tbox} />);
+    }
+    destroy() {
+        setTimeout(() => {
+            try {
+                this.root.unmount();
+            } catch {}
+        });
+    }
+    deselectNode() {
+        this.dom.classList.remove('ProseMirror-selectednode');
+    }
+    selectNode() {
+        this.dom.classList.add('ProseMirror-selectednode');
+    }
+}
+
+interface IPaintButtonViewInternal {
+    tbox: FormattedTextBox;
+    width: number;
+    height: number;
+    node: any;
+    getPos: any;
+}
+
+@observer
+export class PaintButtonViewInternal extends ObservableReactComponent<IPaintButtonViewInternal> {
+    _reactionDisposer: IReactionDisposer | undefined;
+    _textBoxDoc: Doc;
+
+    constructor(props: IPaintButtonViewInternal) {
+        super(props);
+        makeObservable(this);
+        this._textBoxDoc = this._props.tbox.Document;
+    }
+
+    return100 = () => 100;
+    @computed get _checked() {
+        return this._props.tbox.Document.onClick ? true : false;
+    }
+
+    onCheckClick = () => {
+        const textView = this._props.tbox.DocumentView?.();
+        if (textView) {
+            const paintedField = 'layout_' + this._props.tbox.fieldKey + 'Painted';
+            const layoutFieldKey = StrCast(textView.layoutDoc.layout_fieldKey, 'layout');
+            if (textView.layoutDoc.onClick) {
+                textView.layoutDoc[paintedField] = undefined;
+                textView.layoutDoc.onClick = undefined;
+            } else {
+                textView.layoutDoc.type_collection = CollectionViewType.Freeform;
+                textView.dataDoc[paintedField] = CollectionView.LayoutString(this._props.tbox.fieldKey);
+                textView.layoutDoc.layout_fieldKey = paintedField;
+                textView.setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''));
+                textView.layoutDoc.layout_fieldKey = layoutFieldKey;
+            }
+        }
+    };
+
+    render() {
+        return (
+            <div
+                className="dashFieldView"
+                style={{
+                    width: this._props.width,
+                    height: this._props.height,
+                    pointerEvents: this._props.tbox._props.isSelected() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none',
+                }}>
+                <input type="checkbox" value="paint" checked={this._checked} onChange={e => this.onCheckClick()} />
+            </div>
+        );
+    }
+}
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index be32a2c4a..8f7bc5282 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -4,8 +4,7 @@ import { Doc, StrListCast } from '../../../../fields/Doc';
 import { DocData } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
-import { ComputedField } from '../../../../fields/ScriptField';
-import { NumCast } from '../../../../fields/Types';
+import { NumCast, StrCast } from '../../../../fields/Types';
 import { Utils } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs, DocUtils } from '../../../documents/Documents';
@@ -15,6 +14,7 @@ import { RichTextMenu } from './RichTextMenu';
 import { schema } from './schema_rts';
 import { CollectionView } from '../../collections/CollectionView';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { ContextMenu } from '../../ContextMenu';
 
 export class RichTextRules {
     public Document: Doc;
@@ -69,23 +69,39 @@ export class RichTextRules {
 
             // ``` create code block
             textblockTypeInputRule(/^```$/, schema.nodes.code_block),
-
-            new InputRule(new RegExp(/^\^@paint/), (state, match, start, end) => {
-                const { dataDoc, layoutDoc, fieldKey } = this.TextBox;
-                layoutDoc.type_collection = CollectionViewType.Freeform;
-                dataDoc.layout_painted = CollectionView.LayoutString('painted');
-                const layoutFieldKey = layoutDoc.layout_fieldKey;
-                layoutDoc.layout_fieldKey = 'layout_painted';
-                this.TextBox.DocumentView?.().setToggleDetail();
-                layoutDoc.layout_fieldKey = layoutFieldKey;
-                dataDoc.paintFunc = ComputedField.MakeFunction(`toJavascriptString(this['${fieldKey}']?.Text)`);
-                const comment = '/* this is now a paint func */';
-                const tr = state.tr
-                    .deleteRange(start, end)
-                    .insertText(comment)
-                    .insert(start + comment.length, schema.nodes.code_block.create());
-                return tr.setSelection(new TextSelection(tr.doc.resolve(start + comment.length + 2)));
-            }),
+            new InputRule(
+                new RegExp(/(^|\n)\^@paint/), // for code blocks '^' means the beginning of the block, not the line, so need to test for \n
+                (state, match, start, end) => {
+                    const { dataDoc, layoutDoc, fieldKey } = this.TextBox;
+                    layoutDoc.type_collection = CollectionViewType.Freeform;
+                    const paintedField = 'layout_' + this.TextBox.fieldKey + 'Painted';
+                    dataDoc[paintedField] = CollectionView.LayoutString(this.TextBox.fieldKey);
+                    const layoutFieldKey = StrCast(layoutDoc.layout_fieldKey);
+                    layoutDoc.layout_fieldKey = paintedField;
+                    this.TextBox.DocumentView?.().setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''));
+                    layoutDoc.layout_fieldKey = layoutFieldKey;
+                    const comment = '/* enable as paint function ';
+                    const endComment = ' */\n';
+                    const inCode = state.tr.selection.$anchor.node().type === schema.nodes.code_block;
+                    if (inCode) {
+                        const tr = state.tr
+                            .deleteRange(start, end)
+                            .insertText(comment)
+                            .insert(start + comment.length, schema.nodes.paintButton.create())
+                            .insertText(endComment);
+                        return tr.setSelection(new TextSelection(tr.doc.resolve(start + comment.length + endComment.length + 1)));
+                    } else {
+                        const tr = state.tr
+                            .deleteRange(start, end)
+                            .insertText(comment)
+                            .insert(start + comment.length, schema.nodes.paintButton.create())
+                            .insertText(endComment)
+                            .insert(start + comment.length + endComment.length + 1, schema.nodes.code_block.create());
+                        return tr.setSelection(new TextSelection(tr.doc.resolve(start + comment.length + endComment.length + 3)));
+                    }
+                },
+                { inCode: true }
+            ),
 
             // %<font-size> set the font size
             new InputRule(new RegExp(/%([0-9]+)\s$/), (state, match, start, end) => {
@@ -93,6 +109,32 @@ export class RichTextRules {
                 return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
             }),
 
+            //Create annotation to a field on the text document
+            new InputRule(new RegExp(/>::$/), (state, match, start, end) => {
+                const creator = (doc: Doc) => {
+                    const textDoc = this.Document[DocData];
+                    const numInlines = NumCast(textDoc.inlineTextCount);
+                    textDoc.inlineTextCount = numInlines + 1;
+                    const node = (state.doc.resolve(start) as any).nodeAfter;
+                    const newNode = schema.nodes.dashComment.create({ docId: doc[Id], reflow: false });
+                    const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: doc[Id], float: 'right' });
+                    const sm = state.storedMarks || undefined;
+                    this.TextBox.EditorView?.dispatch(
+                        node
+                            ? this.TextBox.EditorView.state.tr
+                                  .insert(start, newNode)
+                                  .replaceRangeWith(start + 1, end + 2, dashDoc)
+                                  .insertText(' ', start + 2)
+                                  .setStoredMarks([...node.marks, ...(sm ? sm : [])])
+                            : this.TextBox.EditorView.state.tr
+                    );
+                };
+                DocUtils.addDocumentCreatorMenuItems(creator, creator, 200, 200);
+                const cm = ContextMenu.Instance;
+                cm.displayMenu(200, 200, undefined, true);
+
+                return null;
+            }),
             //Create annotation to a field on the text document
             new InputRule(new RegExp(/>>$/), (state, match, start, end) => {
                 const textDoc = this.Document[DocData];
@@ -117,7 +159,7 @@ export class RichTextRules {
                 textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
                 textDoc[inlineFieldKey] = ''; // set a default value for the annotation
                 const node = (state.doc.resolve(start) as any).nodeAfter;
-                const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id] });
+                const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id], reflow: true });
                 const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' });
                 const sm = state.storedMarks || undefined;
                 const replaced = node
@@ -265,49 +307,53 @@ export class RichTextRules {
             // [[fieldKey]] => show field
             // [[fieldKey=value]] => show field and also set its value
             // [[fieldKey:docTitle]] => show field of doc
-            new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => {
-                const fieldKey = match[1];
-                const docTitle = match[3]?.replace(':', '');
-                const value = match[2]?.substring(1);
-                const linkToDoc = (target: Doc) => {
-                    const rstate = this.TextBox.EditorView?.state;
-                    const selection = rstate?.selection.$from.pos;
-                    if (rstate) {
-                        this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
-                    }
+            new InputRule(
+                new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/),
+                (state, match, start, end) => {
+                    const fieldKey = match[1];
+                    const docTitle = match[3]?.replace(':', '');
+                    const value = match[2]?.substring(1);
+                    const linkToDoc = (target: Doc) => {
+                        const rstate = this.TextBox.EditorView?.state;
+                        const selection = rstate?.selection.$from.pos;
+                        if (rstate) {
+                            this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
+                        }
 
-                    DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' });
+                        DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' });
 
-                    const fstate = this.TextBox.EditorView?.state;
-                    if (fstate && selection) {
-                        this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
-                    }
-                };
-                const getTitledDoc = (docTitle: string) => {
-                    if (!DocServer.FindDocByTitle(docTitle)) {
-                        Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true }));
-                    }
-                    const titledDoc = DocServer.FindDocByTitle(docTitle);
-                    return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc;
-                };
-                if (!fieldKey) {
-                    if (docTitle) {
-                        const target = getTitledDoc(docTitle);
-                        if (target) {
-                            setTimeout(() => linkToDoc(target));
-                            return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
+                        const fstate = this.TextBox.EditorView?.state;
+                        if (fstate && selection) {
+                            this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
+                        }
+                    };
+                    const getTitledDoc = (docTitle: string) => {
+                        if (!DocServer.FindDocByTitle(docTitle)) {
+                            Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true }));
+                        }
+                        const titledDoc = DocServer.FindDocByTitle(docTitle);
+                        return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc;
+                    };
+                    if (!fieldKey) {
+                        if (docTitle) {
+                            const target = getTitledDoc(docTitle);
+                            if (target) {
+                                setTimeout(() => linkToDoc(target));
+                                return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
+                            }
                         }
+                        return state.tr;
                     }
-                    return state.tr;
-                }
-                if (value !== '' && value !== undefined) {
-                    const num = value.match(/^[0-9.]$/);
-                    this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
-                }
-                const target = getTitledDoc(docTitle);
-                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
-                return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
-            }),
+                    if (value !== '' && value !== undefined) {
+                        const num = value.match(/^[0-9.]$/);
+                        this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
+                    }
+                    const target = getTitledDoc(docTitle);
+                    const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
+                    return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
+                },
+                { inCode: true }
+            ),
 
             // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
             // wiki:title
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 31f001b11..4706a97fa 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -179,6 +179,7 @@ export const nodes: { [index: string]: NodeSpec } = {
     dashComment: {
         attrs: {
             docId: { default: '' },
+            reflow: { default: true },
         },
         inline: true,
         group: 'inline',
@@ -275,6 +276,17 @@ export const nodes: { [index: string]: NodeSpec } = {
         },
     },
 
+    paintButton: {
+        inline: true,
+        attrs: {},
+        group: 'inline',
+        draggable: false,
+        toDOM(node) {
+            const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+            return ['div', { ...node.attrs, ...attrs }];
+        },
+    },
+
     video: {
         inline: true,
         attrs: {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 67f09f37b..30e3aa5f0 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -60,7 +60,7 @@ export namespace Field {
             case 'number':
             case 'boolean':rawjava = String(field);
                            break;
-            default:       rawjava = field?.[ToJavascriptString]?.() ?? 'null';
+            default:       rawjava = field?.[ToJavascriptString]?.() ?? '';
         } // prettier-ignore
         var script = rawjava;
         // this is a bit hacky, but we treat '^@' references to a published document
@@ -866,7 +866,7 @@ export namespace Doc {
     export function FindReferences(infield: Doc | List<any>, references: Set<Doc>, system: boolean | undefined) {
         if (infield instanceof Promise) return;
         if (!(infield instanceof Doc)) {
-            infield.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system));
+            infield?.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system));
             return;
         }
         const doc = infield as Doc;
@@ -1032,13 +1032,13 @@ export namespace Doc {
     //  This function converts a generic field layout display into a field layout that displays a specific
     // metadata field indicated by the title of the template field (not the default field that it was rendering)
     //
-    export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>): boolean {
+    export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>, keepFieldKey = false): boolean {
         // find the metadata field key that this template field doc will display (indicated by its title)
-        const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '') || Doc.LayoutFieldKey(templateField);
+        const metadataFieldKey = keepFieldKey ? Doc.LayoutFieldKey(templateField) : StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '') || Doc.LayoutFieldKey(templateField);
 
         // update the original template to mark it as a template
         templateField.isTemplateForField = metadataFieldKey;
-        templateField.title = metadataFieldKey;
+        !keepFieldKey && (templateField.title = metadataFieldKey);
 
         const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutFieldKey(templateField)];
         const templateCaptionValue = templateField.caption;
-- 
cgit v1.2.3-70-g09d2

