From 632dec95b9fccccef13b50cb41fc598613a9df1e Mon Sep 17 00:00:00 2001
From: yunahi <60233430+yunahi@users.noreply.github.com>
Date: Mon, 8 Jun 2020 10:56:36 +0900
Subject: added ink options menu

---
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index c753a703d..fb7784b58 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -458,7 +458,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             case GestureUtils.Gestures.Stroke:
                 const points = ge.points;
                 const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
-                const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, InkingControl.Instance.selectedWidth, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
+                const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
                 this.addDocument(inkDoc);
                 e.stopPropagation();
                 break;
-- 
cgit v1.2.3-70-g09d2


From 3dea269077151542bc2450bccd749ede87681556 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 8 Jun 2020 14:18:11 -0400
Subject: a bunch of cleanup to fix import order and to organize/restructure
 ink things in the right places.

---
 package-lock.json                                  |  46 +-
 src/client/apis/youtube/YoutubeBox.tsx             |   7 +-
 src/client/cognitive_services/CognitiveServices.ts |   3 +-
 src/client/documents/Documents.ts                  | 495 +++++++++++----------
 src/client/util/CurrentUserUtils.ts                |   8 +-
 src/client/util/DragManager.ts                     |   2 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |   4 +-
 src/client/util/Import & Export/ImageUtils.ts      |   2 +-
 src/client/util/InteractionUtils.tsx               |  23 +-
 src/client/views/AntimodeMenu.tsx                  |   1 -
 src/client/views/DocComponent.tsx                  |   8 +-
 src/client/views/GestureOverlay.tsx                |  83 ++--
 src/client/views/InkingControl.tsx                 | 109 +----
 src/client/views/InkingStroke.tsx                  |  45 +-
 src/client/views/PreviewCursor.tsx                 |   4 +-
 src/client/views/Touchable.tsx                     |   2 -
 src/client/views/collections/CollectionMapView.tsx |   2 +-
 src/client/views/collections/CollectionSubView.tsx |  25 +-
 .../views/collections/CollectionTreeView.tsx       |   2 +-
 src/client/views/collections/CollectionView.tsx    |  42 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  17 +-
 .../collectionFreeForm/InkOptionsMenu.tsx          | 283 +++---------
 src/client/views/nodes/ColorBox.tsx                |  50 ++-
 src/client/views/nodes/DocumentView.tsx            |  13 +-
 src/client/views/nodes/ImageBox.tsx                |   2 +-
 src/client/views/nodes/PresBox.tsx                 |   4 +-
 src/client/views/nodes/ScreenshotBox.tsx           |  12 +-
 src/client/views/nodes/VideoBox.tsx                |   7 +-
 src/client/views/nodes/WebBox.tsx                  |   3 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  12 +-
 src/client/views/pdf/PDFViewer.tsx                 |   6 +-
 src/client/views/webcam/DashWebRTCVideo.tsx        |   4 +-
 src/fields/Doc.ts                                  | 322 ++++++--------
 src/pen-gestures/GestureUtils.ts                   |  10 +-
 34 files changed, 701 insertions(+), 957 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/package-lock.json b/package-lock.json
index b3fb00ab6..c956fd3e9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2832,8 +2832,7 @@
                         },
                         "ansi-regex": {
                             "version": "2.1.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "aproba": {
                             "version": "1.2.0",
@@ -2851,13 +2850,11 @@
                         },
                         "balanced-match": {
                             "version": "1.0.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "brace-expansion": {
                             "version": "1.1.11",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "balanced-match": "^1.0.0",
                                 "concat-map": "0.0.1"
@@ -2870,18 +2867,15 @@
                         },
                         "code-point-at": {
                             "version": "1.1.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "concat-map": {
                             "version": "0.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "console-control-strings": {
                             "version": "1.1.0",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "core-util-is": {
                             "version": "1.0.2",
@@ -2984,8 +2978,7 @@
                         },
                         "inherits": {
                             "version": "2.0.4",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "ini": {
                             "version": "1.3.5",
@@ -2995,7 +2988,6 @@
                         "is-fullwidth-code-point": {
                             "version": "1.0.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "number-is-nan": "^1.0.0"
                             }
@@ -3008,20 +3000,17 @@
                         "minimatch": {
                             "version": "3.0.4",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "brace-expansion": "^1.1.7"
                             }
                         },
                         "minimist": {
                             "version": "1.2.5",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "minipass": {
                             "version": "2.9.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "safe-buffer": "^5.1.2",
                                 "yallist": "^3.0.0"
@@ -3038,7 +3027,6 @@
                         "mkdirp": {
                             "version": "0.5.3",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "minimist": "^1.2.5"
                             }
@@ -3094,8 +3082,7 @@
                         },
                         "npm-normalize-package-bin": {
                             "version": "1.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "npm-packlist": {
                             "version": "1.4.8",
@@ -3120,8 +3107,7 @@
                         },
                         "number-is-nan": {
                             "version": "1.0.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "object-assign": {
                             "version": "4.1.1",
@@ -3131,7 +3117,6 @@
                         "once": {
                             "version": "1.4.0",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "wrappy": "1"
                             }
@@ -3200,8 +3185,7 @@
                         },
                         "safe-buffer": {
                             "version": "5.1.2",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "safer-buffer": {
                             "version": "2.1.2",
@@ -3231,7 +3215,6 @@
                         "string-width": {
                             "version": "1.0.2",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "code-point-at": "^1.0.0",
                                 "is-fullwidth-code-point": "^1.0.0",
@@ -3249,7 +3232,6 @@
                         "strip-ansi": {
                             "version": "3.0.1",
                             "bundled": true,
-                            "optional": true,
                             "requires": {
                                 "ansi-regex": "^2.0.0"
                             }
@@ -3288,13 +3270,11 @@
                         },
                         "wrappy": {
                             "version": "1.0.2",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         },
                         "yallist": {
                             "version": "3.1.1",
-                            "bundled": true,
-                            "optional": true
+                            "bundled": true
                         }
                     }
                 }
@@ -18074,4 +18054,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index ce7f49e64..a7045ccb7 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -6,7 +6,6 @@ import { Utils } from "../../../Utils";
 import { DocServer } from "../../DocServer";
 import { Docs } from "../../documents/Documents";
 import { DocumentDecorations } from "../../views/DocumentDecorations";
-import { InkingControl } from "../../views/InkingControl";
 import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
 import "../../views/nodes/WebBox.scss";
 import "./YoutubeBox.scss";
@@ -156,14 +155,14 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
     @action
     processVideoDetails = (videoDetails: any[]) => {
         this.videoDetails = videoDetails;
-        this.props.Document.cachedDetails = Docs.Get.FromJson({ data: videoDetails, title: "detailBackUp" });
+        this.props.Document.cachedDetails = Doc.Get.FromJson({ data: videoDetails, title: "detailBackUp" });
     }
 
     /**
      * The function that stores the search results in the props document.
      */
     backUpSearchResults = (videos: any[]) => {
-        this.props.Document.cachedSearchResults = Docs.Get.FromJson({ data: videos, title: "videosBackUp" });
+        this.props.Document.cachedSearchResults = Doc.Get.FromJson({ data: videos, title: "videosBackUp" });
     }
 
     /**
@@ -350,7 +349,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
 
         const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
 
-        const classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
         return (
             <>
                 <div className={classname}  >
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index b816d1617..6b0b3e029 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -1,7 +1,6 @@
 import * as request from "request-promise";
 import { Doc, Field } from "../../fields/Doc";
 import { Cast } from "../../fields/Types";
-import { Docs } from "../documents/Documents";
 import { Utils } from "../../Utils";
 import { InkData } from "../../fields/InkField";
 import { UndoManager } from "../util/UndoManager";
@@ -195,7 +194,7 @@ export namespace CognitiveServices {
                 let results = await ExecuteQuery(Service.Handwriting, Manager, inkData);
                 if (results) {
                     results.recognitionUnits && (results = results.recognitionUnits);
-                    target[keys[0]] = Docs.Get.FromJson({ data: results, title: "Ink Analysis" });
+                    target[keys[0]] = Doc.Get.FromJson({ data: results, title: "Ink Analysis" });
                     const recognizedText = results.map((item: any) => item.recognizedText);
                     const recognizedObjects = results.map((item: any) => item.recognizedObject);
                     const individualWords = recognizedText.filter((text: string) => text && text.split(" ").length === 1);
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 71bf8a516..994f8a147 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -9,11 +9,11 @@ import { ScriptingBox } from "../views/nodes/ScriptingBox";
 import { VideoBox } from "../views/nodes/VideoBox";
 import { WebBox } from "../views/nodes/WebBox";
 import { OmitKeys, JSONUtils, Utils } from "../../Utils";
-import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast } from "../../fields/Doc";
+import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast, HeightSym, WidthSym } from "../../fields/Doc";
 import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../fields/URLField";
 import { HtmlField } from "../../fields/HtmlField";
 import { List } from "../../fields/List";
-import { Cast, NumCast, StrCast } from "../../fields/Types";
+import { Cast, NumCast, StrCast, FieldValue } from "../../fields/Types";
 import { DocServer } from "../DocServer";
 import { dropActionType } from "../util/DragManager";
 import { DateField } from "../../fields/DateField";
@@ -36,11 +36,9 @@ import { PresElementBox } from "../views/presentationview/PresElementBox";
 import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
 import { QueryBox } from "../views/nodes/QueryBox";
 import { ColorBox } from "../views/nodes/ColorBox";
-import { LinkAnchorBox } from "../views/nodes/LinkAnchorBox";
 import { DocHolderBox } from "../views/nodes/DocHolderBox";
 import { InkingStroke } from "../views/InkingStroke";
 import { InkField } from "../../fields/InkField";
-import { InkingControl } from "../views/InkingControl";
 import { RichTextField } from "../../fields/RichTextField";
 import { extname } from "path";
 import { MessageStore } from "../../server/Message";
@@ -49,6 +47,12 @@ import { ContextMenu } from "../views/ContextMenu";
 import { LinkBox } from "../views/nodes/LinkBox";
 import { ScreenshotBox } from "../views/nodes/ScreenshotBox";
 import { ComparisonBox } from "../views/nodes/ComparisonBox";
+import { Id } from "../../fields/FieldSymbols";
+import { listSpec } from "../../fields/Schema";
+import { ObjectField } from "../../fields/ObjectField";
+import { RefField } from "../../fields/RefField";
+import { runInAction } from "mobx";
+import { UndoManager } from "../util/UndoManager";
 const path = require('path');
 
 export interface DocumentOptions {
@@ -455,7 +459,7 @@ export namespace Docs {
                     const doc = StackingDocument(deviceImages, { title, _LODdisable: true, hero: new ImageField(constructed[0].url) });
                     doc.nameAliases = new List<string>([title.toLowerCase()]);
                     // add the parsed attributes to this main document
-                    Docs.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } });
+                    Doc.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } });
                     Doc.AddDocToList(parentProto, "data", doc);
                 } else if (errors) {
                     console.log(errors);
@@ -777,225 +781,6 @@ export namespace Docs {
             return InstanceFromProto(proto, undefined, options);
         }
     }
-
-    export namespace Get {
-
-        const primitives = ["string", "number", "boolean"];
-
-        export interface JsonConversionOpts {
-            data: any;
-            title?: string;
-            appendToExisting?: { targetDoc: Doc, fieldKey?: string };
-            excludeEmptyObjects?: boolean;
-        }
-
-        const defaultKey = "json";
-
-        /**
-         * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily
-         * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields.
-         * 
-         * After building a hierarchy within / below a top-level document, it then returns that top-level parent.
-         * 
-         * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the
-         * string is invalid JSON, so we should assume that the input is the result of a JSON.parse()
-         * call that returned a regular string value to be stored as a Field.
-         * 
-         * If we've received something other than a string, since the caller might also pass in the results of a
-         * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number.
-         * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation.
-         * 
-         * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else,
-         * lacking the key value structure, gets stored as a field in a wrapper document.
-         * 
-         * @param data for convenience and flexibility, either a valid JSON string to be parsed,
-         * or the result of any JSON.parse() call.
-         * @param title an optional title to give to the highest parent document in the hierarchy.
-         * If whether this function creates a new document or appendToExisting is specified and that document already has a title,
-         * because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title.
-         * @param appendToExisting **if specified**, there are two cases, both of which return the target document:
-         * 
-         * 1) the json to be converted can be represented as a document, in which case the target document will act as the root
-         * of the tree and receive all the conversion results as new fields on itself
-         * 2) the json can't be represented as a document, in which case the function will assign the field-level conversion
-         * results to either the specified key on the target document, or to its "json" key by default.
-         * 
-         * If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls)
-         * to act as the root of the tree.
-         * 
-         * One might choose to specify this field if you want to write to a document returned from a Document.Create function call,
-         * say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created
-         * from a default call to new Doc.
-         * 
-         * @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even
-         * if they contain no data. By default, empty objects and arrays are ignored.
-         */
-        export function FromJson({ data, title, appendToExisting, excludeEmptyObjects }: JsonConversionOpts): Opt<Doc> {
-            if (excludeEmptyObjects === undefined) {
-                excludeEmptyObjects = true;
-            }
-            if (data === undefined || data === null || ![...primitives, "object"].includes(typeof data)) {
-                return undefined;
-            }
-            let resolved: any;
-            try {
-                resolved = JSON.parse(typeof data === "string" ? data : JSON.stringify(data));
-            } catch (e) {
-                return undefined;
-            }
-            let output: Opt<Doc>;
-            if (typeof resolved === "object" && !(resolved instanceof Array)) {
-                output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc);
-            } else {
-                const result = toField(resolved, excludeEmptyObjects);
-                if (appendToExisting) {
-                    (output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result;
-                } else {
-                    (output = new Doc).json = result;
-                }
-            }
-            title && output && (output.title = title);
-            return output;
-        }
-
-        /**
-         * For each value of the object, recursively convert it to its appropriate field value
-         * and store the field at the appropriate key in the document if it is not undefined
-         * @param object the object to convert
-         * @returns the object mapped from JSON to field values, where each mapping 
-         * might involve arbitrary recursion (since toField might itself call convertObject)
-         */
-        const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => {
-            const hasEntries = Object.keys(object).length;
-            if (hasEntries || !excludeEmptyObjects) {
-                const resolved = target ?? new Doc;
-                if (hasEntries) {
-                    let result: Opt<Field>;
-                    Object.keys(object).map(key => {
-                        // if excludeEmptyObjects is true, any qualifying conversions from toField will
-                        // be undefined, and thus the results that would have
-                        // otherwise been empty (List or Doc)s will just not be written
-                        if (result = toField(object[key], excludeEmptyObjects, key)) {
-                            resolved[key] = result;
-                        }
-                    });
-                }
-                title && (resolved.title = title);
-                return resolved;
-            }
-        };
-
-        /**
-         * For each element in the list, recursively convert it to a document or other field 
-         * and push the field to the list if it is not undefined
-         * @param list the list to convert
-         * @returns the list mapped from JSON to field values, where each mapping 
-         * might involve arbitrary recursion (since toField might itself call convertList)
-         */
-        const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => {
-            const target = new List();
-            let result: Opt<Field>;
-            // if excludeEmptyObjects is true, any qualifying conversions from toField will
-            // be undefined, and thus the results that would have
-            // otherwise been empty (List or Doc)s will just not be written
-            list.map(item => (result = toField(item, excludeEmptyObjects)) && target.push(result));
-            if (target.length || !excludeEmptyObjects) {
-                return target;
-            }
-        };
-
-        const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<Field> => {
-            if (data === null || data === undefined) {
-                return undefined;
-            }
-            if (primitives.includes(typeof data)) {
-                return data;
-            }
-            if (typeof data === "object") {
-                return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data, excludeEmptyObjects, title, undefined);
-            }
-            throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`);
-        };
-
-        export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
-            let created: Doc | undefined;
-            let layout: ((fieldKey: string) => string) | undefined;
-            const field = target[fieldKey];
-            const resolved = options || {};
-            if (field instanceof ImageField) {
-                created = Docs.Create.ImageDocument((field).url.href, resolved);
-                layout = ImageBox.LayoutString;
-            } else if (field instanceof Doc) {
-                created = field;
-            } else if (field instanceof VideoField) {
-                created = Docs.Create.VideoDocument((field).url.href, resolved);
-                layout = VideoBox.LayoutString;
-            } else if (field instanceof PdfField) {
-                created = Docs.Create.PdfDocument((field).url.href, resolved);
-                layout = PDFBox.LayoutString;
-            } else if (field instanceof AudioField) {
-                created = Docs.Create.AudioDocument((field).url.href, resolved);
-                layout = AudioBox.LayoutString;
-            } else if (field instanceof InkField) {
-                const { selectedColor, selectedWidth, selectedTool, selectedBezier } = InkingControl.Instance;
-                created = Docs.Create.InkDocument(selectedColor, selectedTool, selectedWidth, selectedBezier, (field).inkData, resolved);
-                layout = InkingStroke.LayoutString;
-            } else if (field instanceof List && field[0] instanceof Doc) {
-                created = Docs.Create.StackingDocument(DocListCast(field), resolved);
-                layout = CollectionView.LayoutString;
-            } else {
-                created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved });
-                layout = FormattedTextBox.LayoutString;
-            }
-            if (created) {
-                created.layout = layout?.(fieldKey);
-                created.title = fieldKey;
-                proto && created.proto && (created.proto = Doc.GetProto(proto));
-            }
-            return created;
-        }
-
-        export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
-            let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
-            if (type.indexOf("image") !== -1) {
-                ctor = Docs.Create.ImageDocument;
-                if (!options._width) options._width = 300;
-            }
-            if (type.indexOf("video") !== -1) {
-                ctor = Docs.Create.VideoDocument;
-                if (!options._width) options._width = 600;
-                if (!options._height) options._height = options._width * 2 / 3;
-            }
-            if (type.indexOf("audio") !== -1) {
-                ctor = Docs.Create.AudioDocument;
-            }
-            if (type.indexOf("pdf") !== -1) {
-                ctor = Docs.Create.PdfDocument;
-                if (!options._width) options._width = 400;
-                if (!options._height) options._height = options._width * 1200 / 927;
-            }
-            if (type.indexOf("html") !== -1) {
-                if (path.includes(window.location.hostname)) {
-                    const s = path.split('/');
-                    const id = s[s.length - 1];
-                    return DocServer.GetRefField(id).then(field => {
-                        if (field instanceof Doc) {
-                            const alias = Doc.MakeAlias(field);
-                            alias.x = options.x || 0;
-                            alias.y = options.y || 0;
-                            alias._width = options._width || 300;
-                            alias._height = options._height || options._width || 300;
-                            return alias;
-                        }
-                        return undefined;
-                    });
-                }
-                ctor = Docs.Create.WebDocument;
-                options = { ...options, _nativeWidth: 850, _nativeHeight: 962, _width: 500, _height: 566, title: path, };
-            }
-            return ctor ? ctor(path, options) : undefined;
-        }
-    }
 }
 
 export namespace DocUtils {
@@ -1049,6 +834,155 @@ export namespace DocUtils {
         return linkDoc;
     }
 
+
+    export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
+        let created: Doc | undefined;
+        let layout: ((fieldKey: string) => string) | undefined;
+        const field = target[fieldKey];
+        const resolved = options || {};
+        if (field instanceof ImageField) {
+            created = Docs.Create.ImageDocument((field).url.href, resolved);
+            layout = ImageBox.LayoutString;
+        } else if (field instanceof Doc) {
+            created = field;
+        } else if (field instanceof VideoField) {
+            created = Docs.Create.VideoDocument((field).url.href, resolved);
+            layout = VideoBox.LayoutString;
+        } else if (field instanceof PdfField) {
+            created = Docs.Create.PdfDocument((field).url.href, resolved);
+            layout = PDFBox.LayoutString;
+        } else if (field instanceof AudioField) {
+            created = Docs.Create.AudioDocument((field).url.href, resolved);
+            layout = AudioBox.LayoutString;
+        } else if (field instanceof InkField) {
+            created = Docs.Create.InkDocument(InkingStroke.InkColor, Doc.selectedTool, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, (field).inkData, resolved);
+            layout = InkingStroke.LayoutString;
+        } else if (field instanceof List && field[0] instanceof Doc) {
+            created = Docs.Create.StackingDocument(DocListCast(field), resolved);
+            layout = CollectionView.LayoutString;
+        } else {
+            created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved });
+            layout = FormattedTextBox.LayoutString;
+        }
+        if (created) {
+            created.layout = layout?.(fieldKey);
+            created.title = fieldKey;
+            proto && created.proto && (created.proto = Doc.GetProto(proto));
+        }
+        return created;
+    }
+
+    export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
+        let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
+        if (type.indexOf("image") !== -1) {
+            ctor = Docs.Create.ImageDocument;
+            if (!options._width) options._width = 300;
+        }
+        if (type.indexOf("video") !== -1) {
+            ctor = Docs.Create.VideoDocument;
+            if (!options._width) options._width = 600;
+            if (!options._height) options._height = options._width * 2 / 3;
+        }
+        if (type.indexOf("audio") !== -1) {
+            ctor = Docs.Create.AudioDocument;
+        }
+        if (type.indexOf("pdf") !== -1) {
+            ctor = Docs.Create.PdfDocument;
+            if (!options._width) options._width = 400;
+            if (!options._height) options._height = options._width * 1200 / 927;
+        }
+        if (type.indexOf("html") !== -1) {
+            if (path.includes(window.location.hostname)) {
+                const s = path.split('/');
+                const id = s[s.length - 1];
+                return DocServer.GetRefField(id).then(field => {
+                    if (field instanceof Doc) {
+                        const alias = Doc.MakeAlias(field);
+                        alias.x = options.x || 0;
+                        alias.y = options.y || 0;
+                        alias._width = options._width || 300;
+                        alias._height = options._height || options._width || 300;
+                        return alias;
+                    }
+                    return undefined;
+                });
+            }
+            ctor = Docs.Create.WebDocument;
+            options = { ...options, _nativeWidth: 850, _nativeHeight: 962, _width: 500, _height: 566, title: path, };
+        }
+        return ctor ? ctor(path, options) : undefined;
+    }
+
+    export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc {
+        if (Doc.IsBaseProto(doc)) return doc;
+        if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
+        const copy = new Doc(undefined, true);
+        cloneMap.set(doc[Id], copy);
+        if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
+        const exclude = Cast(doc.excludeFields, listSpec("string"), []);
+        Object.keys(doc).forEach(key => {
+            if (exclude.includes(key)) return;
+            const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+            const field = ProxyField.WithoutProxy(() => doc[key]);
+            const copyObjectField = (field: ObjectField) => {
+                const list = Cast(doc[key], listSpec(Doc));
+                if (list !== undefined && !(list instanceof Promise)) {
+                    copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => DocUtils.makeClone(d as Doc, cloneMap, rtfs)));
+                } else if (doc[key] instanceof Doc) {
+                    copy[key] = key.includes("layout[") ? undefined : DocUtils.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields 
+                } else {
+                    copy[key] = ObjectField.MakeCopy(field);
+                    if (field instanceof RichTextField) {
+                        if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) {
+                            rtfs.push({ copy, key, field });
+                        }
+                    }
+                }
+            };
+            if (key === "proto") {
+                if (doc[key] instanceof Doc) {
+                    copy[key] = DocUtils.makeClone(doc[key]!, cloneMap, rtfs);
+                }
+            } else {
+                if (field instanceof RefField) {
+                    copy[key] = field;
+                } else if (cfield instanceof ComputedField) {
+                    copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
+                    (key === "links" && field instanceof ObjectField) && copyObjectField(field);
+                } else if (field instanceof ObjectField) {
+                    copyObjectField(field);
+                } else if (field instanceof Promise) {
+                    debugger; //This shouldn't happend...
+                } else {
+                    copy[key] = field;
+                }
+            }
+        });
+        Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
+        copy.cloneOf = doc;
+        cloneMap.set(doc[Id], copy);
+        return copy;
+    }
+    export function MakeClone(doc: Doc): Doc {
+        const cloneMap = new Map<string, Doc>();
+        const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
+        const copy = DocUtils.makeClone(doc, cloneMap, rtfMap);
+        rtfMap.map(({ copy, key, field }) => {
+            const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
+                const mapped = cloneMap.get(id);
+                return attr + "\"" + (mapped ? mapped[Id] : id) + "\"";
+            };
+            const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => {
+                const mapped = cloneMap.get(id);
+                return href + (mapped ? mapped[Id] : id);
+            };
+            const regex = `(${Utils.prepend("/doc/")})([^"]*)`;
+            const re = new RegExp(regex, "g");
+            copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
+        });
+        return copy;
+    }
+
     export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number): void {
         ContextMenu.Instance.addItem({
             description: "Add Note ...",
@@ -1083,6 +1017,119 @@ export namespace DocUtils {
             })) as ContextMenuProps[],
             icon: "eye"
         });
+    }// applies a custom template to a document.  the template is identified by it's short name (e.g, slideView not layout_slideView)
+    export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) {
+        const batch = UndoManager.StartBatch("makeCustomViewClicked");
+        runInAction(() => {
+            doc.layoutKey = "layout_" + templateSignature;
+            if (doc[doc.layoutKey] === undefined) {
+                createCustomView(doc, creator, templateSignature, docLayoutTemplate);
+            }
+        });
+        batch.end();
+    }
+    export function findTemplate(templateName: string, type: string, signature: string) {
+        let docLayoutTemplate: Opt<Doc>;
+        const iconViews = DocListCast(Cast(Doc.UserDoc()["template-icons"], Doc, null)?.data);
+        const templBtns = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data);
+        const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)?.data);
+        const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data);
+        const allTemplates = iconViews.concat(templBtns).concat(noteTypes).concat(clickFuncs).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc);
+        // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document.  but type is not reliable and there could be other types of template searches so this should be generalized
+        // first try to find a template that matches the specific document type (<typeName>_<templateName>).  otherwise, fallback to a general match on <templateName>
+        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + "_" + type && (docLayoutTemplate = tempDoc));
+        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc));
+        return docLayoutTemplate;
+    }
+    export function createCustomView(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) {
+        const templateName = templateSignature.replace(/\(.*\)/, "");
+        docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.type), templateSignature);
+
+        const customName = "layout_" + templateSignature;
+        const _width = NumCast(doc._width);
+        const _height = NumCast(doc._height);
+        const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
+
+        let fieldTemplate: Opt<Doc>;
+        if (doc.data instanceof RichTextField || typeof (doc.data) === "string") {
+            fieldTemplate = Docs.Create.TextDocument("", options);
+        } else if (doc.data instanceof PdfField) {
+            fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
+        } else if (doc.data instanceof VideoField) {
+            fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
+        } else if (doc.data instanceof AudioField) {
+            fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
+        } else if (doc.data instanceof ImageField) {
+            fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
+        }
+        const docTemplate = docLayoutTemplate || creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
+
+        fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate);
+        docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined);
+    }
+    export function makeCustomView(doc: Doc, custom: boolean, layout: string) {
+        Doc.setNativeView(doc);
+        if (custom) {
+            makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined);
+        }
+    }
+    export function iconify(doc: Doc) {
+        const layoutKey = Cast(doc.layoutKey, "string", null);
+        DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, "icon", undefined);
+        if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") doc.deiconifyLayout = layoutKey.replace("layout_", "");
+    }
+
+    export function pileup(docList: Doc[], x?: number, y?: number) {
+        let w = 0, h = 0;
+        runInAction(() => {
+            docList.forEach(d => {
+                DocUtils.iconify(d);
+                w = Math.max(d[WidthSym](), w);
+                h = Math.max(d[HeightSym](), h);
+            });
+            h = Math.max(h, w * 4 / 3); // converting to an icon does not update the height right away.  so this is a fallback hack to try to do something reasonable
+            docList.forEach((d, i) => {
+                d.x = Math.cos(Math.PI * 2 * i / docList.length) * 10 - w / 2;
+                d.y = Math.sin(Math.PI * 2 * i / docList.length) * 10 - h / 2;
+                d.displayTimecode = undefined;  // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+            });
+        });
+        if (x !== undefined && y !== undefined) {
+            const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: x - 55, y: y - 55, _width: 110, _height: 100, _LODdisable: true });
+            newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55;
+            newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55;
+            newCollection._width = newCollection._height = 110;
+            //newCollection.borderRounding = "40px";
+            newCollection._jitterRotation = 10;
+            newCollection._backgroundColor = "gray";
+            newCollection._overflow = "visible";
+            return newCollection;
+        }
+    }
+
+
+    export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) {
+        let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey);
+        if (!(optionsCollection instanceof Doc)) {
+            optionsCollection = Docs.Create.StackingDocument([], { title: `${enumeratedFieldKey} field set` }, enumeratedFieldKey);
+            Doc.AddDocToList((Doc.UserDoc().fieldTypes as Doc), "data", optionsCollection as Doc);
+        }
+        const options = optionsCollection as Doc;
+        const targetDoc = doc && Doc.GetProto(Cast(doc.rootDocument, Doc, null) || doc);
+        const docFind = `options.data.find(doc => doc.title === (this.rootDocument||this)["${enumeratedFieldKey}"])?`;
+        targetDoc && (targetDoc.backgroundColor = ComputedField.MakeFunction(docFind + `._backgroundColor || "white"`, undefined, { options }));
+        targetDoc && (targetDoc.color = ComputedField.MakeFunction(docFind + `.color || "black"`, undefined, { options }));
+        targetDoc && (targetDoc.borderRounding = ComputedField.MakeFunction(docFind + `.borderRounding`, undefined, { options }));
+        enumerations.map(enumeration => {
+            const found = DocListCast(options.data).find(d => d.title === enumeration.title);
+            if (found) {
+                found._backgroundColor = enumeration._backgroundColor || found._backgroundColor;
+                found._color = enumeration.color || found._color;
+            } else {
+                Doc.AddDocToList(options, "data", Docs.Create.TextDocument(enumeration.title, enumeration));
+            }
+        });
+        return optionsCollection;
     }
 }
 
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 496099557..d2614a898 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -2,7 +2,7 @@ import { computed, observable, reaction } from "mobx";
 import * as rp from 'request-promise';
 import { Utils } from "../../Utils";
 import { DocServer } from "../DocServer";
-import { Docs, DocumentOptions } from "../documents/Documents";
+import { Docs, DocumentOptions, DocUtils } from "../documents/Documents";
 import { UndoManager } from "./UndoManager";
 import { Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
 import { List } from "../../fields/List";
@@ -221,7 +221,7 @@ export class CurrentUserUtils {
         ];
         if (doc.fieldTypes === undefined) {
             doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations" });
-            Doc.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues);
+            DocUtils.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues);
         }
 
         if (doc["template-notes"] === undefined) {
@@ -612,7 +612,7 @@ export class CurrentUserUtils {
     static setupDockedButtons(doc: Doc) {
         if (doc["dockedBtn-pen"] === undefined) {
             doc["dockedBtn-pen"] = CurrentUserUtils.ficon({
-                onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this, this.inkWidth, this.backgroundColor)"),
+                onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)"),
                 author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activePen.inkPen,  this)`), activePen: doc
             });
         }
@@ -692,6 +692,8 @@ export class CurrentUserUtils {
         doc.title = Doc.CurrentUserEmail;
         doc.activePen = doc;
         doc.inkColor = StrCast(doc.backgroundColor, "rgb(0, 0, 0)");
+        doc.inkWidth = StrCast(doc.inkWidth, "1");
+        doc.inkBezier = StrCast(doc.inkBezier, "");
         doc.fontSize = NumCast(doc.fontSize, 12);
         doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // 
         doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // 
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 2a9c1633a..e7a14f9c5 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -210,7 +210,7 @@ export namespace DragManager {
             e.docDragData && (e.docDragData.droppedDocuments =
                 dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) :
                     dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) :
-                        dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeClone(d) : d)
+                        dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeDelegate(d) : d)
             );
             e.docDragData?.droppedDocuments.forEach((drop: Doc, i: number) =>
                 (dragData?.removeDropProperties || []).concat(Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), [])).map(prop => drop[prop] = undefined)
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 1e8f07049..4e5cde558 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -123,10 +123,10 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             }
             const { accessPaths, exifData } = result;
             const path = Utils.prepend(accessPaths.agnostic.client);
-            const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name });
+            const document = await Doc.Get.DocumentFromType(type, path, { _width: 300, title: name });
             const { data, error } = exifData;
             if (document) {
-                Doc.GetProto(document).exif = error || Docs.Get.FromJson({ data });
+                Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data });
                 docs.push(document);
             }
         }));
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 072e5f58a..0d12b39b8 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -20,7 +20,7 @@ export namespace ImageUtils {
             nativeHeight,
             exifData: { error, data }
         } = await Networking.PostToServer("/inspectImage", { source });
-        document.exif = error || Docs.Get.FromJson({ data });
+        document.exif = error || Doc.Get.FromJson({ data });
         const proto = Doc.GetProto(document);
         proto["data-nativeWidth"] = nativeWidth;
         proto["data-nativeHeight"] = nativeHeight;
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index ab1ccb25a..3948611f0 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,7 +1,6 @@
 import React = require("react");
 import * as beziercurve from 'bezier-curve';
 import * as fitCurve from 'fit-curve';
-import InkOptionsMenu from "../views/collections/collectionFreeForm/InkOptionsMenu";
 
 export namespace InteractionUtils {
     export const MOUSETYPE = "mouse";
@@ -90,24 +89,12 @@ export namespace InteractionUtils {
         return myTouches;
     }
 
-    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string) {
+    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string, scalex: number, scaley: number, shape: string) {
         var pts = "";
-        var shape = "";
-        if (InkOptionsMenu.Instance._circle) {
-            shape = "circle";
-        } else if (InkOptionsMenu.Instance._rectangle) {
-            shape = "rectangle";
-        } else if (InkOptionsMenu.Instance._triangle) {
-            shape = "triangle";
-        } else if (InkOptionsMenu.Instance._arrow) {
-            shape = "arrow";
-        } else if (InkOptionsMenu.Instance._line) {
-            shape = "line";
-        }
-        if (shape !== "") {
+        if (shape) {
             //if any of the shape are true
             const shapePts = makePolygon(shape, points);
-            pts = shapePts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+            pts = shapePts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, "");
         }
         else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) {
             //pointer is up (first and last points are the same)
@@ -124,10 +111,10 @@ export namespace InteractionUtils {
                     newPts.push({ X: point[0], Y: point[1] });
                 }
             }
-            pts = newPts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+            pts = newPts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, "");
         } else {
             //in the middle of drawing
-            pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+            pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, "");
         }
         return (
             <polyline
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index f810361c6..9a6121d20 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -1,5 +1,4 @@
 import React = require("react");
-import { observer } from "mobx-react";
 import { observable, action } from "mobx";
 import "./AntimodeMenu.scss";
 
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 1ba9fcc32..c398b2633 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -2,8 +2,6 @@ import { Doc, Opt, DataSym, DocListCast } from '../../fields/Doc';
 import { Touchable } from './Touchable';
 import { computed, action, observable } from 'mobx';
 import { Cast, BoolCast, ScriptCast } from '../../fields/Types';
-import { listSpec } from '../../fields/Schema';
-import { InkingControl } from './InkingControl';
 import { InkTool } from '../../fields/InkField';
 import { InteractionUtils } from '../util/InteractionUtils';
 import { List } from '../../fields/List';
@@ -59,7 +57,7 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor:
 
         lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result;
 
-        active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);//  && !InkingControl.Instance.selectedTool;  // bcz: inking state shouldn't affect static tools 
+        active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);//  && !Doc.selectedTool;  // bcz: inking state shouldn't affect static tools 
         protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
     }
     return Component;
@@ -148,9 +146,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
         }
 
         whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
-        active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
+        active = (outsideReaction?: boolean) => ((Doc.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
             (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0 || BoolCast((this.layoutDoc as any).forceActive)) ? true : false)
-        annotationsActive = (outsideReaction?: boolean) => (InkingControl.Instance.selectedTool !== InkTool.None || (this.props.Document.isBackground && this.props.active()) ||
+        annotationsActive = (outsideReaction?: boolean) => (Doc.selectedTool !== InkTool.None || (this.props.Document.isBackground && this.props.active()) ||
             (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
     }
     return Component;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 5714970c1..4ea75d7d7 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,38 +1,30 @@
 import React = require("react");
-import { Touchable } from "./Touchable";
+import { action, computed, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import "./GestureOverlay.scss";
-import { computed, observable, action, runInAction, IReactionDisposer, reaction, flow, trace } from "mobx";
+import { Doc } from "../../fields/Doc";
+import { InkData, InkTool } from "../../fields/InkField";
+import { Cast, FieldValue, NumCast } from "../../fields/Types";
+import MobileInkOverlay from "../../mobile/MobileInkOverlay";
+import MobileInterface from "../../mobile/MobileInterface";
 import { GestureUtils } from "../../pen-gestures/GestureUtils";
+import { MobileInkOverlayContent } from "../../server/Message";
+import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero } from "../../Utils";
+import { CognitiveServices } from "../cognitive_services/CognitiveServices";
+import { DocServer } from "../DocServer";
+import { DocUtils } from "../documents/Documents";
+import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import { InteractionUtils } from "../util/InteractionUtils";
-import { InkingControl } from "./InkingControl";
-import { InkTool, InkData } from "../../fields/InkField";
-import { Doc } from "../../fields/Doc";
 import { LinkManager } from "../util/LinkManager";
-import { DocUtils, Docs } from "../documents/Documents";
-import { undoBatch } from "../util/UndoManager";
 import { Scripting } from "../util/Scripting";
-import { FieldValue, Cast, NumCast, BoolCast } from "../../fields/Types";
-import { CurrentUserUtils } from "../util/CurrentUserUtils";
-import HorizontalPalette from "./Palette";
-import { Utils, emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, numberRange, returnZero } from "../../Utils";
-import { DocumentView } from "./nodes/DocumentView";
 import { Transform } from "../util/Transform";
-import { DocumentContentsView } from "./nodes/DocumentContentsView";
-import { CognitiveServices } from "../cognitive_services/CognitiveServices";
-import { DocServer } from "../DocServer";
-import htmlToImage from "html-to-image";
-import { ScriptField } from "../../fields/ScriptField";
-import { listSpec } from "../../fields/Schema";
-import { List } from "../../fields/List";
-import { CollectionViewType } from "./collections/CollectionView";
-import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
-import MobileInterface from "../../mobile/MobileInterface";
-import { MobileInkOverlayContent } from "../../server/Message";
-import MobileInkOverlay from "../../mobile/MobileInkOverlay";
+import "./GestureOverlay.scss";
+import { InkingControl } from "./InkingControl";
+import { DocumentView } from "./nodes/DocumentView";
 import { RadialMenu } from "./nodes/RadialMenu";
-import { SelectionManager } from "../util/SelectionManager";
-import InkOptionsMenu from "./collections/collectionFreeForm/InkOptionsMenu";
+import HorizontalPalette from "./Palette";
+import { Touchable } from "./Touchable";
+import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
+import { InkingStroke } from "./InkingStroke";
 
 
 @observer
@@ -500,7 +492,7 @@ export default class GestureOverlay extends Touchable {
 
     @action
     onPointerDown = (e: React.PointerEvent) => {
-        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
             this._points.push({ X: e.clientX, Y: e.clientY });
             e.stopPropagation();
             e.preventDefault();
@@ -514,7 +506,7 @@ export default class GestureOverlay extends Touchable {
 
     @action
     onPointerMove = (e: PointerEvent) => {
-        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
             this._points.push({ X: e.clientX, Y: e.clientY });
             e.stopPropagation();
             e.preventDefault();
@@ -585,12 +577,11 @@ export default class GestureOverlay extends Touchable {
             //push first points to so interactionUtil knows pointer is up
             this._points.push({ X: this._points[0].X, Y: this._points[0].Y });
             if (MobileInterface.Instance && MobileInterface.Instance.drawingInk) {
-                const { selectedColor, selectedWidth } = InkingControl.Instance;
                 DocServer.Mobile.dispatchGesturePoints({
                     points: this._points,
                     bounds: B,
-                    color: selectedColor,
-                    width: selectedWidth
+                    color: InkingStroke.InkColor,
+                    width: InkingStroke.InkWidth
                 });
             }
 
@@ -633,21 +624,11 @@ export default class GestureOverlay extends Touchable {
                 }
             }
             //if any of the shape is activated in the InkOptionsMenu
-            else if (InkOptionsMenu.Instance._circle || InkOptionsMenu.Instance._triangle || InkOptionsMenu.Instance._rectangle || InkOptionsMenu.Instance._line || InkOptionsMenu.Instance._arrow) {
-                if (InkOptionsMenu.Instance._circle) {
-                    this.makePolygon("circle", false);
-                } else if (InkOptionsMenu.Instance._triangle) {
-                    this.makePolygon("triangle", false);
-                } else if (InkOptionsMenu.Instance._rectangle) {
-                    this.makePolygon("rectangle", false);
-                } else if (InkOptionsMenu.Instance._line) {
-                    this.makePolygon("line", false);
-                } else if (InkOptionsMenu.Instance._arrow) {
-                    this.makePolygon("arrow", false);
-                }
+            else if (InkingStroke.InkShape) {
+                this.makePolygon(InkingStroke.InkShape, false);
                 this.dispatchGesture(GestureUtils.Gestures.Stroke);
                 this._points = [];
-                InkOptionsMenu.Instance.allFalse();
+                InkingStroke.InkShape = "";
             }
             // if we're not drawing in a toolglass try to recognize as gesture
             else {
@@ -827,11 +808,11 @@ export default class GestureOverlay extends Touchable {
             [this._strokes.map(l => {
                 const b = this.getBounds(l);
                 return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                    {InteractionUtils.CreatePolyline(l, b.left, b.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier)}
+                    {InteractionUtils.CreatePolyline(l, b.left, b.top, InkingStroke.InkColor, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, 1, 1, InkingStroke.InkShape)}
                 </svg>;
             }),
             this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier)}
+                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, InkingStroke.InkColor, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, 1, 1, InkingStroke.InkShape)}
             </svg>]
         ];
     }
@@ -921,15 +902,15 @@ Scripting.addGlobal(function setToolglass(tool: any) {
 });
 Scripting.addGlobal(function setPen(width: any, color: any) {
     runInAction(() => {
-        GestureOverlay.Instance.SavedColor = InkingControl.Instance.selectedColor;
-        InkingControl.Instance.updateSelectedColor(color);
-        GestureOverlay.Instance.SavedWidth = InkingControl.Instance.selectedWidth;
+        GestureOverlay.Instance.SavedColor = InkingStroke.InkColor;
+        InkingControl.Instance.switchColor(color);
+        GestureOverlay.Instance.SavedWidth = InkingStroke.InkWidth;
         InkingControl.Instance.switchWidth(width);
     });
 });
 Scripting.addGlobal(function resetPen() {
     runInAction(() => {
-        InkingControl.Instance.updateSelectedColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)");
+        InkingControl.Instance.switchColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)");
         InkingControl.Instance.switchWidth(GestureOverlay.Instance.SavedWidth ?? "2");
     });
 });
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 83109db1c..349bc6ffc 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,22 +1,12 @@
-import { action, computed, observable } from "mobx";
-import { ColorState } from 'react-color';
+import { action, observable } from "mobx";
 import { Doc } from "../../fields/Doc";
 import { InkTool } from "../../fields/InkField";
-import { FieldValue, NumCast, StrCast } from "../../fields/Types";
 import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import { Scripting } from "../util/Scripting";
-import { SelectionManager } from "../util/SelectionManager";
-import { undoBatch } from "../util/UndoManager";
-import GestureOverlay from "./GestureOverlay";
-import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox";
-import InkOptionsMenu from "./collections/collectionFreeForm/InkOptionsMenu";
+import { InkingStroke } from "./InkingStroke";
 
 export class InkingControl {
     @observable static Instance: InkingControl;
-    @computed private get _selectedTool(): InkTool { return FieldValue(NumCast(Doc.UserDoc().inkTool)) ?? InkTool.None; }
-    @computed private get _selectedColor(): string { return CurrentUserUtils.ActivePen ? FieldValue(StrCast(CurrentUserUtils.ActivePen.backgroundColor)) ?? "rgb(0, 0, 0)" : "rgb(0, 0, 0)"; }
-    @computed private get _selectedWidth(): string { return FieldValue(StrCast(Doc.UserDoc().inkWidth)) ?? "2"; }
-    @computed private get _selectedBezier(): string { return FieldValue(StrCast(Doc.UserDoc().inkBezier)) ?? "2"; }
     @observable public _open: boolean = false;
     constructor() {
         InkingControl.Instance = this;
@@ -26,110 +16,27 @@ export class InkingControl {
         // this._selectedTool = tool;
         Doc.UserDoc().inkTool = tool;
     });
-    decimalToHexString(number: number) {
-        if (number < 0) {
-            number = 0xFFFFFFFF + number + 1;
-        }
-        return (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
-    }
 
-    @action
-    inkOptionsMenuChangeColor = (color: string) => {
-        const col: ColorState = {
-            hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" },
-            rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "",
-        };
-        this.switchColor(col);
-        InkOptionsMenu.Instance._colorBt = false;
-    }
-
-    @undoBatch
-    switchColor = action((color: ColorState): void => {
-        Doc.UserDoc().backgroundColor = color.hex.startsWith("#") ?
-            color.hex + (color.rgb.a ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex;
-        InkOptionsMenu.Instance._color = StrCast(Doc.UserDoc().backgroundColor);
-        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = color.hex);
-
-        if (InkingControl.Instance.selectedTool === InkTool.None) {
-            const selected = SelectionManager.SelectedDocuments();
-            selected.map(view => {
-                const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
-                    view.props.Document.layout instanceof Doc ? view.props.Document.layout :
-                        view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document);
-                if (targetDoc) {
-                    if (StrCast(Doc.Layout(view.props.Document).layout).indexOf("FormattedTextBox") !== -1 && FormattedTextBox.HadSelection) {
-                        Doc.Layout(view.props.Document).color = Doc.UserDoc().bacgroundColor;
-                    } else {
-                        Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific.  'backgroundColor' would apply to all templates, but has no UI at the moment
-                    }
-                }
-            });
-        }
-    });
     @action
     switchWidth = (width: string): void => {
-        // this._selectedWidth = width;
         if (!isNaN(parseInt(width))) {
-            Doc.UserDoc().inkWidth = width;
+            CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkWidth = InkingStroke.InkWidth = width);
         }
-        InkOptionsMenu.Instance._widthBt = false;
     }
 
     @action
     switchBezier = (bezier: string): void => {
-        if (!isNaN(parseInt(bezier))) {
-            Doc.UserDoc().inkBezier = bezier;
-        }
+        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkBezier = InkingStroke.InkBezierApprox = isNaN(parseInt(bezier)) ? "" : bezier);
     }
 
     @action
-    inkOptionsMenuChangeBezier = (e: React.PointerEvent): void => {
-        if (InkOptionsMenu.Instance._bezierBt === true) {
-            Doc.UserDoc().inkBezier = "300";
-        } else {
-            Doc.UserDoc().inkBezier = "0";
-        }
-    }
-
-    @computed
-    get selectedTool() {
-        return this._selectedTool;
-    }
-
-    @computed
-    get selectedColor() {
-        return this._selectedColor;
-    }
-
-    @action
-    updateSelectedColor(value: string) {
-        // this._selectedColor = value;
-        Doc.UserDoc().inkColor = value;
-    }
-
-    @computed
-    get selectedWidth() {
-        return this._selectedWidth;
-    }
-
-    @computed
-    get selectedBezier() {
-        return this._selectedBezier;
+    switchColor(value: string) {
+        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkColor = InkingStroke.InkColor = value);
     }
 }
-Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) {
-    InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color);
-    //setup InkOptionsMenu(change jumpto value if necessary.Currenlty hardcoded to 300,300)
-    pen ? InkOptionsMenu.Instance.jumpTo(300, 300) : InkOptionsMenu.Instance.fadeOut(true);
-    InkOptionsMenu.Instance.changeColor = InkingControl.Instance.inkOptionsMenuChangeColor;
-    InkOptionsMenu.Instance.changeBezier = InkingControl.Instance.inkOptionsMenuChangeBezier;
-    InkOptionsMenu.Instance.changeWidth = InkingControl.Instance.switchWidth;
-    InkOptionsMenu.Instance._widthSelected = width;
-    InkOptionsMenu.Instance._color = color;
-});
-Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
+Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.switchColor(color); });
 Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); });
 Scripting.addGlobal(function activateStamp(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Stamp : InkTool.None); });
 Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); });
 Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); });
-Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.updateSelectedColor(color); });
\ No newline at end of file
+Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.switchColor(color); });
\ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 3dc0a5b20..78d729eee 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,19 +1,19 @@
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faPaintBrush } from "@fortawesome/free-solid-svg-icons";
+import { observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { documentSchema } from "../../fields/documentSchemas";
 import { InkData, InkField, InkTool } from "../../fields/InkField";
 import { makeInterface } from "../../fields/Schema";
-import { Cast, StrCast, NumCast } from "../../fields/Types";
+import { Cast, StrCast } from "../../fields/Types";
+import { TraceMobx } from "../../fields/util";
+import { CognitiveServices } from "../cognitive_services/CognitiveServices";
+import { InteractionUtils } from "../util/InteractionUtils";
+import { ContextMenu } from "./ContextMenu";
 import { ViewBoxBaseComponent } from "./DocComponent";
-import { InkingControl } from "./InkingControl";
 import "./InkingStroke.scss";
 import { FieldView, FieldViewProps } from "./nodes/FieldView";
 import React = require("react");
-import { TraceMobx } from "../../fields/util";
-import { InteractionUtils } from "../util/InteractionUtils";
-import { ContextMenu } from "./ContextMenu";
-import { CognitiveServices } from "../cognitive_services/CognitiveServices";
-import { faPaintBrush } from "@fortawesome/free-solid-svg-icons";
-import { library } from "@fortawesome/fontawesome-svg-core";
 
 library.add(faPaintBrush);
 
@@ -23,6 +23,22 @@ const InkDocument = makeInterface(documentSchema);
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
     public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
+    @observable public static InkColor: string;
+    @observable public static InkWidth: string;
+    @observable public static InkBezierApprox: string;
+    @observable public static InkShape: string;
+
+    constructor(props: any) {
+        super(props);
+        if (InkingStroke.InkBezierApprox === undefined) {
+            runInAction(() => {
+                InkingStroke.InkBezierApprox = "";
+                InkingStroke.InkWidth = "1";
+                InkingStroke.InkColor = "black";
+                InkingStroke.InkShape = "";
+            });
+        }
+    }
 
     private analyzeStrokes = () => {
         const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
@@ -38,22 +54,19 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const top = Math.min(...ys);
         const right = Math.max(...xs);
         const bottom = Math.max(...ys);
-        const points = InteractionUtils.CreatePolyline(data, left, top,
-            StrCast(this.layoutDoc.color, InkingControl.Instance.selectedColor),
-            StrCast(this.layoutDoc.strokeWidth, InkingControl.Instance.selectedWidth),
-            StrCast(this.layoutDoc.strokeBezier, InkingControl.Instance.selectedBezier));
         const width = right - left;
         const height = bottom - top;
         const scaleX = this.props.PanelWidth() / width;
         const scaleY = this.props.PanelHeight() / height;
+        const points = InteractionUtils.CreatePolyline(data, left, top,
+            StrCast(this.layoutDoc.color, InkingStroke.InkColor || "black"),
+            StrCast(this.layoutDoc.strokeWidth, InkingStroke.InkWidth || "1"),
+            StrCast(this.layoutDoc.strokeBezier, InkingStroke.InkBezierApprox || ""), scaleX, scaleY, "");
         return (
             <svg className="inkingStroke"
                 width={width}
                 height={height}
-                style={{
-                    transform: `scale(${scaleX}, ${scaleY})`,
-                    mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
-                }}
+                style={{ mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset" }}
                 onContextMenu={() => {
                     ContextMenu.Instance.addItem({
                         description: "Analyze Stroke",
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index dd65681d4..33150ab7c 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
 import "normalize.css";
 import * as React from 'react';
 import "./PreviewCursor.scss";
-import { Docs } from '../documents/Documents';
+import { Docs, DocUtils } from '../documents/Documents';
 import { Doc } from '../../fields/Doc';
 import { Transform } from "../util/Transform";
 import { DocServer } from '../DocServer';
@@ -65,7 +65,7 @@ export class PreviewCursor extends React.Component<{}> {
                         count++;
                         if (doc instanceof Doc) {
                             i === 1 && (first = doc);
-                            const alias = Doc.MakeClone(doc);
+                            const alias = DocUtils.MakeClone(doc);
                             const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx;
                             const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty;
                             alias.x = newPoint[0] + deltaX;
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 10d023d83..5e48d5ffb 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -1,8 +1,6 @@
 import * as React from 'react';
 import { action } from 'mobx';
 import { InteractionUtils } from '../util/InteractionUtils';
-import { SelectionManager } from '../util/SelectionManager';
-import { RadialMenu } from './nodes/RadialMenu';
 
 const HOLD_DURATION = 1000;
 
diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx
index a0b7cd8a8..cfec3a6bc 100644
--- a/src/client/views/collections/CollectionMapView.tsx
+++ b/src/client/views/collections/CollectionMapView.tsx
@@ -42,7 +42,7 @@ const query = async (data: string | google.maps.LatLngLiteral) => {
 };
 
 @observer
-class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) {
+export class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) {
 
     private _cancelAddrReq = new Map<string, boolean>();
     private _cancelLocReq = new Map<string, boolean>();
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 423eb1d90..b60419258 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -6,27 +6,19 @@ import { Id } from "../../../fields/FieldSymbols";
 import { List } from "../../../fields/List";
 import { listSpec } from "../../../fields/Schema";
 import { ScriptField } from "../../../fields/ScriptField";
+import { WebField } from "../../../fields/URLField";
 import { Cast, ScriptCast, NumCast } from "../../../fields/Types";
 import { GestureUtils } from "../../../pen-gestures/GestureUtils";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
 import { Upload } from "../../../server/SharedMediaTypes";
 import { Utils } from "../../../Utils";
-import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { DocServer } from "../../DocServer";
-import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
 import { Networking } from "../../Network";
-import { DragManager, dropActionType } from "../../util/DragManager";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
 import { InteractionUtils } from "../../util/InteractionUtils";
 import { undoBatch, UndoManager } from "../../util/UndoManager";
 import { DocComponent } from "../DocComponent";
 import { FieldViewProps } from "../nodes/FieldView";
-import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox";
-import { CollectionView } from "./CollectionView";
 import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
-import { WebField } from "../../../fields/URLField";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc | Doc[]) => boolean;
@@ -405,7 +397,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                     const stringContents = await new Promise<string>(resolve => item.getAsString(resolve));
                     const type = "html";// (await rp.head(Utils.CorsProxy(stringContents)))["content-type"];
                     if (type) {
-                        const doc = await Docs.Get.DocumentFromType(type, stringContents, options);
+                        const doc = await DocUtils.DocumentFromType(type, stringContents, options);
                         doc && generatedDocuments.push(doc);
                     }
                 }
@@ -435,7 +427,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                 }
                 const full = { ...options, _width: 400, title: name };
                 const pathname = Utils.prepend(result.accessPaths.agnostic.client);
-                const doc = await Docs.Get.DocumentFromType(type, pathname, full);
+                const doc = await DocUtils.DocumentFromType(type, pathname, full);
                 if (!doc) {
                     continue;
                 }
@@ -450,9 +442,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                 generatedDocuments.push(doc);
             }
             if (generatedDocuments.length) {
-                const set = generatedDocuments.length > 1 && generatedDocuments.map(d => Doc.iconify(d));
+                const set = generatedDocuments.length > 1 && generatedDocuments.map(d => DocUtils.iconify(d));
                 if (set) {
-                    addDocument(Doc.pileup(generatedDocuments, options.x!, options.y!)!);
+                    addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!);
                 } else {
                     generatedDocuments.forEach(addDocument);
                 }
@@ -469,3 +461,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
     return CollectionSubView;
 }
 
+import { DragManager, dropActionType } from "../../util/DragManager";
+import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
+import { CurrentUserUtils } from "../../util/CurrentUserUtils";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox";
+import { CollectionView } from "./CollectionView";
+import { SelectionManager } from "../../util/SelectionManager";
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index b2e1c0f73..e891c4274 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -757,7 +757,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
         const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
         const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
         onClicks.push({
-            description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit"
+            description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit"
         });
         !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
     }
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index fecba32c5..873f61331 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -14,36 +14,16 @@ import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../fields/Ty
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { Utils, setupMoveUpEvents, returnFalse, returnZero, emptyPath, emptyFunction, returnOne } from '../../../Utils';
-import { DocumentType } from '../../documents/DocumentTypes';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 import { ContextMenu } from "../ContextMenu";
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import { ScriptBox } from '../ScriptBox';
 import { Touchable } from '../Touchable';
-import { CollectionCarouselView } from './CollectionCarouselView';
-import { CollectionDockingView } from "./CollectionDockingView";
-import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
-import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
-import { CollectionLinearView } from './CollectionLinearView';
-import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
-import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
-import { CollectionSchemaView } from "./CollectionSchemaView";
-import { CollectionStackingView } from './CollectionStackingView';
-import { CollectionStaffView } from './CollectionStaffView';
-import { SubCollectionViewProps } from './CollectionSubView';
-import { CollectionTimeView } from './CollectionTimeView';
-import { CollectionTreeView } from "./CollectionTreeView";
-import './CollectionView.scss';
-import { CollectionViewBaseChrome } from './CollectionViewChromes';
-import { CurrentUserUtils } from '../../util/CurrentUserUtils';
 import { Id } from '../../../fields/FieldSymbols';
 import { listSpec } from '../../../fields/Schema';
-import { Docs } from '../../documents/Documents';
 import { ScriptField, ComputedField } from '../../../fields/ScriptField';
 import { InteractionUtils } from '../../util/InteractionUtils';
 import { ObjectField } from '../../../fields/ObjectField';
-import CollectionMapView from './CollectionMapView';
-import { CollectionPileView } from './CollectionPileView';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -519,4 +499,24 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
             {this.facetWidth() < 10 ? (null) : this.filterView}
         </div>);
     }
-}
\ No newline at end of file
+}
+import { SubCollectionViewProps } from './CollectionSubView';
+import { CollectionCarouselView } from './CollectionCarouselView';
+import { CollectionDockingView } from "./CollectionDockingView";
+import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionLinearView } from './CollectionLinearView';
+import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
+import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
+import { CollectionSchemaView } from "./CollectionSchemaView";
+import { CollectionStackingView } from './CollectionStackingView';
+import { CollectionStaffView } from './CollectionStaffView';
+import { CollectionTimeView } from './CollectionTimeView';
+import { CollectionTreeView } from "./CollectionTreeView";
+import { CollectionMapView } from './CollectionMapView';
+import { CollectionPileView } from './CollectionPileView';
+import './CollectionView.scss';
+import { CollectionViewBaseChrome } from './CollectionViewChromes';
+import { CurrentUserUtils } from '../../util/CurrentUserUtils';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index fb7784b58..576c0c560 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -29,7 +29,6 @@ import { undoBatch, UndoManager } from "../../../util/UndoManager";
 import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
 import { ContextMenu } from "../../ContextMenu";
 import { ContextMenuProps } from "../../ContextMenuItem";
-import { InkingControl } from "../../InkingControl";
 import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocumentViewProps, DocumentView } from "../../nodes/DocumentView";
 import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
@@ -46,7 +45,7 @@ import React = require("react");
 import { CollectionViewType } from "../CollectionView";
 import { Timeline } from "../../animationtimeline/Timeline";
 import { SnappingManager } from "../../../util/SnappingManager";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { InkingStroke } from "../../InkingStroke";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -391,7 +390,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+        if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
             return;
         }
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
@@ -408,7 +407,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             document.addEventListener("pointermove", this.onPointerMove);
             document.addEventListener("pointerup", this.onPointerUp);
             // if not using a pen and in no ink mode
-            if (InkingControl.Instance.selectedTool === InkTool.None) {
+            if (Doc.selectedTool === InkTool.None) {
                 this._downX = this._lastX = e.pageX;
                 this._downY = this._lastY = e.pageY;
             }
@@ -432,13 +431,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     this.addMoveListeners();
                     this.removeEndListeners();
                     this.addEndListeners();
-                    // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) {
+                    // if (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen) {
                     //     e.stopPropagation();
                     //     e.preventDefault();
                     //     const point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
                     //     this._points.push({ X: point[0], Y: point[1] });
                     // }
-                    if (InkingControl.Instance.selectedTool === InkTool.None) {
+                    if (Doc.selectedTool === InkTool.None) {
                         this._lastX = pt.pageX;
                         this._lastY = pt.pageY;
                         e.preventDefault();
@@ -458,7 +457,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             case GestureUtils.Gestures.Stroke:
                 const points = ge.points;
                 const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
-                const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, InkingControl.Instance.selectedWidth, InkingControl.Instance.selectedBezier, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
+                const inkDoc = Docs.Create.InkDocument(InkingStroke.InkColor, Doc.selectedTool, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
                 this.addDocument(inkDoc);
                 e.stopPropagation();
                 break;
@@ -619,7 +618,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             return;
         }
         if (!e.cancelBubble) {
-            const selectedTool = InkingControl.Instance.selectedTool;
+            const selectedTool = Doc.selectedTool;
             if (selectedTool === InkTool.None) {
                 if (this._hitCluster && this.tryDragCluster(e)) {
                     e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -641,7 +640,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
             const pt = myTouches[0];
             if (pt) {
-                if (InkingControl.Instance.selectedTool === InkTool.None) {
+                if (Doc.selectedTool === InkTool.None) {
                     if (this._hitCluster && this.tryDragCluster(e)) {
                         e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
                         e.preventDefault();
diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
index 44488cbcf..b35c9682e 100644
--- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
@@ -1,269 +1,117 @@
 import React = require("react");
 import AntimodeMenu from "../../AntimodeMenu";
 import { observer } from "mobx-react";
-import { unimplementedFunction } from "../../../../Utils";
 import { observable, action } from "mobx";
 import "./InkOptionsMenu.scss";
-
+import { InkingStroke } from "../../InkingStroke";
+import { Scripting } from "../../../util/Scripting";
+import { InkTool } from "../../../../fields/InkField";
+import { InkingControl } from "../../InkingControl";
+import { StrCast } from "../../../../fields/Types";
+import { ColorState } from "react-color";
+import { ColorBox } from "../../nodes/ColorBox";
 
 @observer
 export default class InkOptionsMenu extends AntimodeMenu {
     static Instance: InkOptionsMenu;
-    public changeColor: (color: string) => void = unimplementedFunction;
-    public changeBezier: (e: React.PointerEvent) => void = unimplementedFunction;
-    public changeWidth: (color: string) => void = unimplementedFunction;
-
-    private _palette: (string)[];
-    private _width: (string)[];
-
-
-    public _circle: boolean;
-    public _triangle: boolean;
-    public _rectangle: boolean;
-    public _arrow: boolean;
-    public _line: boolean;
-    public _widthSelected: string;
-
-    @observable public _circleBt: boolean;
-    @observable public _triangleBt: boolean;
-    @observable public _rectangleBt: boolean;
-    @observable public _arrowBt: boolean;
-    @observable public _lineBt: boolean;
-    @observable public _colorBt: boolean;
-    @observable public _color: string;
-    @observable public _bezierBt: boolean;
-    @observable public _widthBt: boolean;
 
+    private _palette = ["D0021B", "F5A623", "F8E71C", "8B572A", "7ED321", "417505", "9013FE", "4A90E2", "50E3C2", "B8E986", "000000", "4A4A4A", "9B9B9B", "FFFFFF"];
+    private _width = ["1", "5", "10", "100", "200", "300"];
+    private _buttons = ["circle", "triangle", "rectangle", "arrow", "line"];
+    private _icons = ["O", "∆", "ロ", "➜", "-"];
 
+    @observable _colorBtn = false;
+    @observable _widthBtn = false;
 
     constructor(props: Readonly<{}>) {
         super(props);
         InkOptionsMenu.Instance = this;
-        this._canFade = false;
-
-        this._circle = false;
-        this._triangle = false;
-        this._rectangle = false;
-        this._arrow = false;
-        this._line = false;
-        this._circleBt = false;
-        this._triangleBt = false;
-        this._rectangleBt = false;
-        this._arrowBt = false;
-        this._lineBt = false;
-        this._colorBt = false;
-        this._bezierBt = false;
-        this._widthBt = false;
-
-        this._color = "";
-        this._widthSelected = "";
-
-
-        this._palette = [
-            "D0021B", "F5A623", "F8E71C", "8B572A", "7ED321", "417505", "9013FE", "4A90E2", "50E3C2", "B8E986", "000000", "4A4A4A", "9B9B9B", "FFFFFF",
-        ];
-
-        this._width = [
-            "1", "5", "10", "100", "200", "300"
-        ];
-
-    }
-
-
-
-    drag = (e: React.PointerEvent) => {
-        this.dragStart(e);
+        this._canFade = false; // don't let the inking menu fade away
     }
 
-
-
-
-
-    @action
-    toggleCircle = (e: React.PointerEvent) => {
-        const curr = this._circle;
-        this.allFalse();
-        curr ? this._circle = false : this._circle = true;
-        this._circleBt = this._circle;
-    }
-    @action
-    toggleTriangle = (e: React.PointerEvent) => {
-        const curr = this._triangle;
-        this.allFalse();
-        curr ? this._triangle = false : this._triangle = true;
-        this._triangleBt = this._triangle;
-    }
-    @action
-    toggleRectangle = (e: React.PointerEvent) => {
-        const curr = this._rectangle;
-        this.allFalse();
-        curr ? this._rectangle = false : this._rectangle = true;
-        this._rectangleBt = this._rectangle;
-    }
-    @action
-    toggleArrow = (e: React.PointerEvent) => {
-        const curr = this._arrow;
-        this.allFalse();
-        curr ? this._arrow = false : this._arrow = true;
-        this._arrowBt = this._arrow;
-    }
     @action
-    toggleLine = (e: React.PointerEvent) => {
-        const curr = this._line;
-        this.allFalse();
-        curr ? this._line = false : this._line = true;
-        this._lineBt = this._line;
+    changeColor = (color: string) => {
+        const col: ColorState = {
+            hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" },
+            rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "",
+        };
+        ColorBox.switchColor(col);
     }
 
     @action
-    changeBezierClick = (e: React.PointerEvent) => {
-        const curr = this._bezierBt;
-        this.allFalse();
-        curr ? this._bezierBt = false : this._bezierBt = true;
-        this.changeBezier(e);
-    }
-
-    @action
-    changeWidthClick = (e: React.PointerEvent) => {
-        this._widthBt ? this._widthBt = false : this._widthBt = true;
-    }
-    @action
-    changeColorClick = (e: React.PointerEvent) => {
-        this._colorBt ? this._colorBt = false : this._colorBt = true;
-    }
-
-    allFalse = () => {
-        this._circle = false;
-        this._triangle = false;
-        this._rectangle = false;
-        this._arrow = false;
-        this._line = false;
-        this._circleBt = false;
-        this._triangleBt = false;
-        this._rectangleBt = false;
-        this._arrowBt = false;
-        this._lineBt = false;
-        this._bezierBt = false;
+    changeBezier = (e: React.PointerEvent): void => {
+        InkingControl.Instance.switchBezier(!InkingStroke.InkBezierApprox ? "300" : "");
     }
 
     render() {
-        var widthPicker;
-        if (this._widthBt) {
+        var widthPicker = <button
+            className="antimodeMenu-button"
+            key="width"
+            onPointerDown={action(e => this._widthBtn = !this._widthBtn)}
+            style={{ backgroundColor: this._widthBtn ? "121212" : "" }}>
+            W
+        </button>;
+        if (this._widthBtn) {
             widthPicker = <div className="btn2-group">
-                <button
-                    className="antimodeMenu-button"
-                    key="width"
-                    onPointerDown={this.changeWidthClick}
-                    style={this._widthBt ? { backgroundColor: "121212" } : {}}>
-                    W
-                </button>
+                {widthPicker}
                 {this._width.map(wid => {
                     return <button
                         className="antimodeMenu-button"
                         key={wid}
-                        onPointerDown={() => this.changeWidth(wid)}
-                        style={this._colorBt ? { backgroundColor: "121212" } : {}}>
+                        onPointerDown={action(() => { InkingControl.Instance.switchWidth(wid); this._widthBtn = false; })}
+                        style={{ backgroundColor: this._widthBtn ? "121212" : "" }}>
                         {wid}
                     </button>;
-
                 })}
             </div>;
-        } else {
-            widthPicker = <button
-                className="antimodeMenu-button"
-                key="width"
-                onPointerDown={this.changeWidthClick}
-                style={this._widthBt ? { backgroundColor: "121212" } : {}}>
-                W
-        </button>;
         }
 
-        var colorPicker;
-        if (this._colorBt) {
+        var colorPicker = <button
+            className="antimodeMenu-button"
+            key="color"
+            title="colorChanger"
+            onPointerDown={action(e => this._colorBtn = !this._colorBtn)}
+            style={{ backgroundColor: this._colorBtn ? "121212" : "" }}>
+            <div className="color-preview" style={{ backgroundColor: InkingStroke.InkColor ?? "121212" }}></div>
+        </button>;
+        if (this._colorBtn) {
             colorPicker = <div className="btn-group">
-                <button
-                    className="antimodeMenu-button"
-                    key="color"
-                    onPointerDown={this.changeColorClick}
-                    style={this._colorBt ? { backgroundColor: "121212" } : {}}>
-                    <div className="color-preview" style={this._color === "" ? { backgroundColor: "121212" } : { backgroundColor: this._color }}></div>
-                </button>
+                {colorPicker}
                 {this._palette.map(color => {
                     return <button
                         className="antimodeMenu-button"
                         key={color}
-                        onPointerDown={() => this.changeColor(color)}
-                        style={this._colorBt ? { backgroundColor: "121212" } : {}}>
+                        onPointerDown={action(() => { this.changeColor(color); this._colorBtn = false; })}
+                        style={{ backgroundColor: this._colorBtn ? "121212" : "" }}>
                         <div className="color-preview" style={{ backgroundColor: color }}></div>
                     </button>;
                 })}
             </div>;
-        } else {
-            colorPicker = <button
-                className="antimodeMenu-button"
-                title="colorChanger"
-                key="color"
-                onPointerDown={this.changeColorClick}
-                style={this._colorBt ? { backgroundColor: "121212" } : {}}>
-                <div className="color-preview" style={this._color === "" ? { backgroundColor: "121212" } : { backgroundColor: this._color }}></div>
-            </button>;
         }
 
-
         const buttons = [
-            <button
-                className="antimodeMenu-button"
+            <button className="antimodeMenu-button"
                 title="Drag"
                 key="drag"
-                onPointerDown={this.drag}>
+                onPointerDown={e => this.dragStart(e)}>
                 ✜
             </button>,
-            <button
-                className="antimodeMenu-button"
-                title="Draw Circle"
-                key="circle"
-                onPointerDown={this.toggleCircle}
-                style={this._circleBt ? { backgroundColor: "121212" } : {}}>
-                O
-            </button>,
-            <button
-                className="antimodeMenu-button"
-                title="Draw Traingle"
-                key="triangle"
-                onPointerDown={this.toggleTriangle}
-                style={this._triangleBt ? { backgroundColor: "121212" } : {}}>
-                ∆
-            </button>,
-            <button
-                className="antimodeMenu-button"
-                title="Draw Rectangle"
-                key="rectangle"
-                onPointerDown={this.toggleRectangle}
-                style={this._rectangleBt ? { backgroundColor: "121212" } : {}}>
-                ロ
-            </button>,
-            <button
-                className="antimodeMenu-button"
-                title="Draw Arrow"
-                key="arrow"
-                onPointerDown={this.toggleArrow}
-                style={this._arrowBt ? { backgroundColor: "121212" } : {}}>
-                ➜
-                </button>,
-            <button
-                className="antimodeMenu-button"
-                title="Draw Line"
-                key="line"
-                onPointerDown={this.toggleLine}
-                style={this._lineBt ? { backgroundColor: "121212" } : {}}>
-                –
-            </button>,
+            <>
+                {this._buttons.map((btn, i) => <button
+                    className="antimodeMenu-button"
+                    title={`Draw ${btn}`}
+                    key={btn}
+                    onPointerDown={action(e => InkingStroke.InkShape = btn)}
+                    style={btn === InkingStroke.InkShape ? { backgroundColor: "121212" } : {}}>
+                    {this._icons[i]}
+                </button>)},
+            </>,
             <button
                 className="antimodeMenu-button"
                 title="Bezier changer"
                 key="bezier"
-                onPointerDown={this.changeBezierClick}
-                style={this._bezierBt ? { backgroundColor: "121212" } : {}}>
+                onPointerDown={e => this.changeBezier(e)}
+                style={InkingStroke.InkBezierApprox ? { backgroundColor: "121212" } : {}}>
                 B
             </button>,
             widthPicker,
@@ -271,4 +119,15 @@ export default class InkOptionsMenu extends AntimodeMenu {
         ];
         return this.getElement(buttons);
     }
-}
\ No newline at end of file
+}
+Scripting.addGlobal(function activatePen(pen: any) {
+    InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None);
+    if (pen) {
+        InkingControl.Instance.switchWidth(StrCast(pen.inkWidth, "1"));
+        InkingControl.Instance.switchColor(StrCast(pen.inkColor, "black"));
+        InkingControl.Instance.switchBezier(StrCast(pen.inkBezier, ""));
+        InkOptionsMenu.Instance.jumpTo(300, 300);
+    } else {
+        InkOptionsMenu.Instance.fadeOut(true);
+    }
+});
\ No newline at end of file
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 2ddf2c74a..762c57ae9 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -1,6 +1,6 @@
 import React = require("react");
 import { observer } from "mobx-react";
-import { SketchPicker } from 'react-color';
+import { SketchPicker, ColorState } from 'react-color';
 import { documentSchema } from "../../../fields/documentSchemas";
 import { makeInterface } from "../../../fields/Schema";
 import { StrCast } from "../../../fields/Types";
@@ -10,6 +10,12 @@ import { ViewBoxBaseComponent } from "../DocComponent";
 import { InkingControl } from "../InkingControl";
 import "./ColorBox.scss";
 import { FieldView, FieldViewProps } from './FieldView';
+import { InkingStroke } from "../InkingStroke";
+import { Doc } from "../../../fields/Doc";
+import { InkTool } from "../../../fields/InkField";
+import { undoBatch } from "../../util/UndoManager";
+import { action } from "mobx";
+import { FormattedTextBox } from "./formattedText/FormattedTextBox";
 
 type ColorDocument = makeInterface<[typeof documentSchema]>;
 const ColorDocument = makeInterface(documentSchema);
@@ -18,20 +24,52 @@ const ColorDocument = makeInterface(documentSchema);
 export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument>(ColorDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
 
+    static decimalToHexString(number: number) {
+        if (number < 0) {
+            number = 0xFFFFFFFF + number + 1;
+        }
+        return (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
+    }
+
+    @undoBatch
+    @action
+    static switchColor(color: ColorState) {
+        Doc.UserDoc().backgroundColor = color.hex.startsWith("#") ?
+            color.hex + (color.rgb.a ? ColorBox.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex;
+        InkingStroke.InkColor = StrCast(Doc.UserDoc().backgroundColor);
+        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkColor = color.hex);
+
+        if (Doc.selectedTool === InkTool.None) {
+            const selected = SelectionManager.SelectedDocuments();
+            selected.map(view => {
+                const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
+                    view.props.Document.layout instanceof Doc ? view.props.Document.layout :
+                        view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document);
+                if (targetDoc) {
+                    if (StrCast(Doc.Layout(view.props.Document).layout).indexOf("FormattedTextBox") !== -1 && FormattedTextBox.HadSelection) {
+                        Doc.Layout(view.props.Document).color = Doc.UserDoc().bacgroundColor;
+                    } else {
+                        Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific.  'backgroundColor' would apply to all templates, but has no UI at the moment
+                    }
+                }
+            });
+        }
+    }
+
     render() {
         const selDoc = SelectionManager.SelectedDocuments()?.[0]?.rootDoc;
         return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`}
             onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}
             style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
 
-            <SketchPicker onChange={InkingControl.Instance.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
+            <SketchPicker onChange={ColorBox.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
                 color={StrCast(CurrentUserUtils.ActivePen ? CurrentUserUtils.ActivePen.backgroundColor : undefined,
                     StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} />
             <div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}>
-                <div> {InkingControl.Instance.selectedWidth ?? 2}</div>
-                <input type="range" value={InkingControl.Instance.selectedWidth ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchWidth(e.target.value)} />
-                <div> {InkingControl.Instance.selectedBezier ?? 2}</div>
-                <input type="range" value={InkingControl.Instance.selectedBezier ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchBezier(e.target.value)} />
+                <div> {InkingStroke.InkWidth ?? 2}</div>
+                <input type="range" value={InkingStroke.InkWidth ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchWidth(e.target.value)} />
+                <div> {InkingStroke.InkBezierApprox ?? 2}</div>
+                <input type="range" value={InkingStroke.InkBezierApprox ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchBezier(e.target.value)} />
                 <br />
                 <br />
             </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e245e045c..196e61a31 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -37,7 +37,6 @@ import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent } from "../DocComponent";
 import { EditableView } from '../EditableView';
-import { InkingControl } from '../InkingControl';
 import { KeyphraseQueryView } from '../KeyphraseQueryView';
 import { DocumentContentsView } from "./DocumentContentsView";
 import "./DocumentView.scss";
@@ -330,7 +329,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 } else func();
             } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself
                 const alias = Doc.MakeAlias(this.props.Document);
-                Doc.makeCustomViewClicked(alias, undefined, "onClick");
+                DocUtils.makeCustomViewClicked(alias, undefined, "onClick");
                 this.props.addDocTab(alias, "onRight");
                 // UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick");
                 //ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click");
@@ -513,7 +512,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         // console.log(e.button)
         // console.log(e.nativeEvent)
         // continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document)
-        if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+        if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
             if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
                 e.stopPropagation();
                 // TODO: check here for panning/inking
@@ -544,7 +543,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     onPointerMove = (e: PointerEvent): void => {
 
         if ((e as any).formattedHandled) { e.stopPropagation(); return; }
-        if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) return;
+        if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) return;
         if (e.cancelBubble && this.active) {
             document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
         }
@@ -728,7 +727,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: this.toggleFollowInPlace, icon: "concierge-bell" });
         onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link on Right", event: this.toggleFollowOnRight, icon: "concierge-bell" });
         onClicks.push({ description: this.Document.isLinkButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" });
-        onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" });
+        onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" });
         !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
 
 
@@ -1091,14 +1090,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @computed get ignorePointerEvents() {
         return this.props.pointerEvents === false ||
             (this.Document.isBackground && !this.isSelected() && !SnappingManager.GetIsDragging()) ||
-            (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None);
+            (this.Document.type === DocumentType.INK && Doc.selectedTool !== InkTool.None);
     }
     @undoBatch
     @action
     setCustomView = (custom: boolean, layout: string): void => {
         Doc.setNativeView(this.props.Document);
         if (custom) {
-            Doc.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
+            DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
         }
     }
     @observable _animateScalingTo = 0;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 6913dfbc7..cabf30c13 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -192,7 +192,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
 
     extractFaces = () => {
         const converter = (results: any) => {
-            return results.map((face: CognitiveServices.Image.Face) => Docs.Get.FromJson({ data: face, title: `Face: ${face.faceId}` })!);
+            return results.map((face: CognitiveServices.Image.Face) => Doc.Get.FromJson({ data: face, title: `Face: ${face.faceId}` })!);
         };
         this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-faces"], this.url, Service.Face, converter);
     }
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 9f1e99c77..81669dc2a 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -11,7 +11,6 @@ import { DocumentManager } from "../../util/DocumentManager";
 import { undoBatch } from "../../util/UndoManager";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { CollectionView, CollectionViewType } from "../collections/CollectionView";
-import { InkingControl } from "../InkingControl";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./PresBox.scss";
 import { ViewBoxBaseComponent } from "../DocComponent";
@@ -20,6 +19,7 @@ import { Docs } from "../../documents/Documents";
 import { PrefetchProxy } from "../../../fields/Proxy";
 import { ScriptField } from "../../../fields/ScriptField";
 import { Scripting } from "../../util/Scripting";
+import { InkingStroke } from "../InkingStroke";
 
 type PresBoxSchema = makeInterface<[typeof documentSchema]>;
 const PresBoxDocument = makeInterface(documentSchema);
@@ -293,7 +293,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
     selectElement = (doc: Doc) => this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.itemIndex));
     getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
     panelHeight = () => this.props.PanelHeight() - 20;
-    active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.layoutDoc.isBackground) &&
+    active = (outsideReaction?: boolean) => ((Doc.selectedTool === InkTool.None && !this.layoutDoc.isBackground) &&
         (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
 
     render() {
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 29e3c008a..1184f32f1 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -5,21 +5,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from 'request-promise';
+import { Doc } from "../../../fields/Doc";
 import { documentSchema } from "../../../fields/documentSchemas";
-import { makeInterface, listSpec } from "../../../fields/Schema";
+import { listSpec, makeInterface } from "../../../fields/Schema";
 import { Cast, NumCast } from "../../../fields/Types";
 import { VideoField } from "../../../fields/URLField";
-import { emptyFunction, returnFalse, returnOne, Utils, returnZero } from "../../../Utils";
-import { Docs, DocUtils } from "../../documents/Documents";
+import { emptyFunction, returnFalse, returnOne, returnZero, Utils } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxBaseComponent } from "../DocComponent";
-import { InkingControl } from "../InkingControl";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ScreenshotBox.scss";
-import { Doc, WidthSym, HeightSym } from "../../../fields/Doc";
-import { OverlayView } from "../OverlayView";
 const path = require('path');
 
 type ScreenshotDocument = makeInterface<[typeof documentSchema]>;
@@ -134,7 +132,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh
     }
 
     @computed get content() {
-        const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+        const interactive = Doc.selectedTool || !this.props.isSelected() ? "" : "-interactive";
         const style = "videoBox-content" + interactive;
         return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
             style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 9d02239fc..8c8b7a622 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -15,7 +15,6 @@ import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
-import { InkingControl } from "../InkingControl";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
 import { documentSchema } from "../../../fields/documentSchemas";
@@ -229,7 +228,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
 
     @computed get content() {
         const field = Cast(this.dataDoc[this.fieldKey], VideoField);
-        const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+        const interactive = Doc.selectedTool || !this.props.isSelected() ? "" : "-interactive";
         const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div>Loading</div> :
             <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
@@ -273,8 +272,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
             this._reactionDisposer && this._reactionDisposer();
             this._youtubeReactionDisposer && this._youtubeReactionDisposer();
             this._reactionDisposer = reaction(() => this.layoutDoc.currentTimecode, () => !this._playing && this.Seek((this.layoutDoc.currentTimecode || 0)));
-            this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => {
-                const interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting;
+            this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, Doc.selectedTool], () => {
+                const interactive = Doc.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting;
                 iframe.style.pointerEvents = interactive ? "all" : "none";
             }, { fireImmediately: true });
         };
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index c4ab3c9e2..dcc87068b 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -15,7 +15,6 @@ import { DragManager } from "../../util/DragManager";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
-import { InkingControl } from "../InkingControl";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./WebBox.scss";
 import React = require("react");
@@ -425,7 +424,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         const frozen = !this.props.isSelected() || decInteracting;
 
         return (<>
-            <div className={"webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")}
+            <div className={"webBox-cont" + (this.props.isSelected() && Doc.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")}
                 onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
                 {view}
             </div>;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fc131cd38..c0a8bec40 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -52,14 +52,12 @@ import { ContextMenu } from '../../ContextMenu';
 import { ContextMenuProps } from '../../ContextMenuItem';
 import { ViewBoxAnnotatableComponent } from "../../DocComponent";
 import { DocumentButtonBar } from '../../DocumentButtonBar';
-import { InkingControl } from "../../InkingControl";
 import { AudioBox } from '../AudioBox';
 import { FieldView, FieldViewProps } from "../FieldView";
 import "./FormattedTextBox.scss";
 import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
 import React = require("react");
-import { ScriptField } from '../../../../fields/ScriptField';
-import GoogleAuthenticationManager from '../../../apis/GoogleAuthenticationManager';
+import { InkingStroke } from '../../InkingStroke';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -479,11 +477,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             changeItems.push({
                 description: StrCast(note.title), event: undoBatch(() => {
                     Doc.setNativeView(this.rootDoc);
-                    Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
+                    DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
                 }), icon: "eye"
             });
         });
-        changeItems.push({ description: "FreeForm", event: undoBatch(() => Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" });
+        changeItems.push({ description: "FreeForm", event: undoBatch(() => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" });
         !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" });
     }
 
@@ -1213,7 +1211,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         TraceMobx();
         const scale = this.props.ContentScaling() * NumCast(this.layoutDoc.scale, 1);
         const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
-        const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
+        const interactive = Doc.selectedTool || this.layoutDoc.isBackground;
         if (this.props.isSelected()) {
             this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props);
         } else if (FormattedTextBoxComment.textBox === this) {
@@ -1268,7 +1266,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     </div>
                     {!this.layoutDoc._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
                         <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> :
-                        <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
+                        <div className={"formattedTextBox-sidebar" + (Doc.selectedTool !== InkTool.None ? "-inking" : "")}
                             style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
                             <CollectionFreeFormView {...this.props}
                                 PanelHeight={this.props.PanelHeight}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5bad248be..c18f69e1d 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -2,7 +2,6 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
 import { observer } from "mobx-react";
 import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
-import * as rp from "request-promise";
 import { Dictionary } from "typescript-collections";
 import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
 import { documentSchema } from "../../../fields/documentSchemas";
@@ -26,7 +25,6 @@ import { CollectionFreeFormView } from "../collections/collectionFreeForm/Collec
 import { CollectionView } from "../collections/CollectionView";
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
-import { InkingControl } from "../InkingControl";
 import Annotation from "./Annotation";
 import PDFMenu from "./PDFMenu";
 import "./PDFViewer.scss";
@@ -583,7 +581,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
         const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
         Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]);
         clipDoc.rootDocument = targetDoc;
-        Doc.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined);
+        DocUtils.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined);
         targetDoc.layoutKey = "layout";
         // const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
         // Doc.GetProto(targetDoc).snipped = this.dataDoc[this.props.fieldKey][Copy]();
@@ -652,7 +650,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
     panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0);
     panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0);
     @computed get overlayLayer() {
-        return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay"
+        return <div className={`pdfViewer-overlay${Doc.selectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay"
             style={{ transform: `scale(${this._zoomed})` }}>
             <CollectionFreeFormView {...this.props}
                 LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? emptyPath}
diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx
index 2ea011316..0b1e90b1d 100644
--- a/src/client/views/webcam/DashWebRTCVideo.tsx
+++ b/src/client/views/webcam/DashWebRTCVideo.tsx
@@ -4,10 +4,8 @@ import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeForm
 import { FieldViewProps, FieldView } from "../nodes/FieldView";
 import { observable, action } from "mobx";
 import { DocumentDecorations } from "../DocumentDecorations";
-import { InkingControl } from "../InkingControl";
 import "../../views/nodes/WebBox.scss";
 import "./DashWebRTCVideo.scss";
-import adapter from 'webrtc-adapter';
 import { initialize, hangup, refreshVideos } from "./WebCamLogic";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
@@ -73,7 +71,7 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV
             </div >;
 
         const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
-        const classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
 
 
         return (
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 6891bf652..d635836bb 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -6,20 +6,18 @@ import { DocumentType } from "../client/documents/DocumentTypes";
 import { Scripting, scriptingGlobal } from "../client/util/Scripting";
 import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
 import { UndoManager } from "../client/util/UndoManager";
-import { intersectRect, Utils } from "../Utils";
-import { HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update, Copy } from "./FieldSymbols";
+import { intersectRect } from "../Utils";
+import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols";
+import { InkTool } from "./InkField";
 import { List } from "./List";
 import { ObjectField } from "./ObjectField";
 import { PrefetchProxy, ProxyField } from "./Proxy";
 import { FieldId, RefField } from "./RefField";
 import { RichTextField } from "./RichTextField";
 import { listSpec } from "./Schema";
-import { ComputedField, ScriptField } from "./ScriptField";
-import { Cast, FieldValue, NumCast, StrCast, ToConstructor, ScriptCast } from "./Types";
+import { ComputedField } from "./ScriptField";
+import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
 import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
-import { Docs, DocumentOptions } from "../client/documents/Documents";
-import { PdfField, VideoField, AudioField, ImageField } from "./URLField";
-import { LinkManager } from "../client/util/LinkManager";
 
 export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
@@ -134,6 +132,7 @@ function fetchProto(doc: Doc) {
 @scriptingGlobal
 @Deserializable("Doc", fetchProto).withFields(["id"])
 export class Doc extends RefField {
+    @computed public static get selectedTool(): InkTool { return FieldValue(NumCast(Doc.UserDoc().inkTool)) ?? InkTool.None; }
     constructor(id?: FieldId, forceSave?: boolean) {
         super(id);
         const doc = new Proxy<this>(this, {
@@ -618,76 +617,6 @@ export namespace Doc {
         return copy;
     }
 
-    export function MakeClone(doc: Doc): Doc {
-        const cloneMap = new Map<string, Doc>();
-        const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
-        const copy = Doc.makeClone(doc, cloneMap, rtfMap);
-        rtfMap.map(({ copy, key, field }) => {
-            const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
-                const mapped = cloneMap.get(id);
-                return attr + "\"" + (mapped ? mapped[Id] : id) + "\"";
-            };
-            const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => {
-                const mapped = cloneMap.get(id);
-                return href + (mapped ? mapped[Id] : id);
-            };
-            const regex = `(${Utils.prepend("/doc/")})([^"]*)`;
-            const re = new RegExp(regex, "g");
-            copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
-        });
-        return copy;
-    }
-
-    export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc {
-        if (Doc.IsBaseProto(doc)) return doc;
-        if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
-        const copy = new Doc(undefined, true);
-        cloneMap.set(doc[Id], copy);
-        if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
-        const exclude = Cast(doc.excludeFields, listSpec("string"), []);
-        Object.keys(doc).forEach(key => {
-            if (exclude.includes(key)) return;
-            const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-            const field = ProxyField.WithoutProxy(() => doc[key]);
-            const copyObjectField = (field: ObjectField) => {
-                const list = Cast(doc[key], listSpec(Doc));
-                if (list !== undefined && !(list instanceof Promise)) {
-                    copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.makeClone(d as Doc, cloneMap, rtfs)));
-                } else if (doc[key] instanceof Doc) {
-                    copy[key] = key.includes("layout[") ? undefined : Doc.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields 
-                } else {
-                    copy[key] = ObjectField.MakeCopy(field);
-                    if (field instanceof RichTextField) {
-                        if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) {
-                            rtfs.push({ copy, key, field });
-                        }
-                    }
-                }
-            };
-            if (key === "proto") {
-                if (doc[key] instanceof Doc) {
-                    copy[key] = Doc.makeClone(doc[key]!, cloneMap, rtfs);
-                }
-            } else {
-                if (field instanceof RefField) {
-                    copy[key] = field;
-                } else if (cfield instanceof ComputedField) {
-                    copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
-                    (key === "links" && field instanceof ObjectField) && copyObjectField(field);
-                } else if (field instanceof ObjectField) {
-                    copyObjectField(field);
-                } else if (field instanceof Promise) {
-                    debugger; //This shouldn't happend...
-                } else {
-                    copy[key] = field;
-                }
-            }
-        });
-        Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
-        copy.cloneOf = doc;
-        cloneMap.set(doc[Id], copy);
-        return copy;
-    }
 
     export function MakeDelegate(doc: Doc, id?: string, title?: string): Doc;
     export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc>;
@@ -998,120 +927,147 @@ export namespace Doc {
         return false;
     }
 
-    // applies a custom template to a document.  the template is identified by it's short name (e.g, slideView not layout_slideView)
-    export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) {
-        const batch = UndoManager.StartBatch("makeCustomViewClicked");
-        runInAction(() => {
-            doc.layoutKey = "layout_" + templateSignature;
-            if (doc[doc.layoutKey] === undefined) {
-                createCustomView(doc, creator, templateSignature, docLayoutTemplate);
-            }
-        });
-        batch.end();
-    }
-    export function findTemplate(templateName: string, type: string, signature: string) {
-        let docLayoutTemplate: Opt<Doc>;
-        const iconViews = DocListCast(Cast(Doc.UserDoc()["template-icons"], Doc, null)?.data);
-        const templBtns = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data);
-        const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)?.data);
-        const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data);
-        const allTemplates = iconViews.concat(templBtns).concat(noteTypes).concat(clickFuncs).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc);
-        // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document.  but type is not reliable and there could be other types of template searches so this should be generalized
-        // first try to find a template that matches the specific document type (<typeName>_<templateName>).  otherwise, fallback to a general match on <templateName>
-        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + "_" + type && (docLayoutTemplate = tempDoc));
-        !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc));
-        return docLayoutTemplate;
-    }
-    export function createCustomView(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) {
-        const templateName = templateSignature.replace(/\(.*\)/, "");
-        docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.type), templateSignature);
-
-        const customName = "layout_" + templateSignature;
-        const _width = NumCast(doc._width);
-        const _height = NumCast(doc._height);
-        const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
-
-        let fieldTemplate: Opt<Doc>;
-        if (doc.data instanceof RichTextField || typeof (doc.data) === "string") {
-            fieldTemplate = Docs.Create.TextDocument("", options);
-        } else if (doc.data instanceof PdfField) {
-            fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
-        } else if (doc.data instanceof VideoField) {
-            fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
-        } else if (doc.data instanceof AudioField) {
-            fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
-        } else if (doc.data instanceof ImageField) {
-            fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
-        }
-        const docTemplate = docLayoutTemplate || creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
 
-        fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate);
-        docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined);
-    }
-    export function makeCustomView(doc: Doc, custom: boolean, layout: string) {
-        Doc.setNativeView(doc);
-        if (custom) {
-            makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined);
+    export namespace Get {
+
+        const primitives = ["string", "number", "boolean"];
+
+        export interface JsonConversionOpts {
+            data: any;
+            title?: string;
+            appendToExisting?: { targetDoc: Doc, fieldKey?: string };
+            excludeEmptyObjects?: boolean;
         }
-    }
-    export function iconify(doc: Doc) {
-        const layoutKey = Cast(doc.layoutKey, "string", null);
-        Doc.makeCustomViewClicked(doc, Docs.Create.StackingDocument, "icon", undefined);
-        if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") doc.deiconifyLayout = layoutKey.replace("layout_", "");
-    }
 
-    export function pileup(docList: Doc[], x?: number, y?: number) {
-        let w = 0, h = 0;
-        runInAction(() => {
-            docList.forEach(d => {
-                Doc.iconify(d);
-                w = Math.max(d[WidthSym](), w);
-                h = Math.max(d[HeightSym](), h);
-            });
-            h = Math.max(h, w * 4 / 3); // converting to an icon does not update the height right away.  so this is a fallback hack to try to do something reasonable
-            docList.forEach((d, i) => {
-                d.x = Math.cos(Math.PI * 2 * i / docList.length) * 10 - w / 2;
-                d.y = Math.sin(Math.PI * 2 * i / docList.length) * 10 - h / 2;
-                d.displayTimecode = undefined;  // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
-            });
-        });
-        if (x !== undefined && y !== undefined) {
-            const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: x - 55, y: y - 55, _width: 110, _height: 100, _LODdisable: true });
-            newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55;
-            newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55;
-            newCollection._width = newCollection._height = 110;
-            //newCollection.borderRounding = "40px";
-            newCollection._jitterRotation = 10;
-            newCollection._backgroundColor = "gray";
-            newCollection._overflow = "visible";
-            return newCollection;
+        const defaultKey = "json";
+
+        /**
+         * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily
+         * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields.
+         * 
+         * After building a hierarchy within / below a top-level document, it then returns that top-level parent.
+         * 
+         * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the
+         * string is invalid JSON, so we should assume that the input is the result of a JSON.parse()
+         * call that returned a regular string value to be stored as a Field.
+         * 
+         * If we've received something other than a string, since the caller might also pass in the results of a
+         * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number.
+         * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation.
+         * 
+         * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else,
+         * lacking the key value structure, gets stored as a field in a wrapper document.
+         * 
+         * @param data for convenience and flexibility, either a valid JSON string to be parsed,
+         * or the result of any JSON.parse() call.
+         * @param title an optional title to give to the highest parent document in the hierarchy.
+         * If whether this function creates a new document or appendToExisting is specified and that document already has a title,
+         * because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title.
+         * @param appendToExisting **if specified**, there are two cases, both of which return the target document:
+         * 
+         * 1) the json to be converted can be represented as a document, in which case the target document will act as the root
+         * of the tree and receive all the conversion results as new fields on itself
+         * 2) the json can't be represented as a document, in which case the function will assign the field-level conversion
+         * results to either the specified key on the target document, or to its "json" key by default.
+         * 
+         * If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls)
+         * to act as the root of the tree.
+         * 
+         * One might choose to specify this field if you want to write to a document returned from a Document.Create function call,
+         * say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created
+         * from a default call to new Doc.
+         * 
+         * @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even
+         * if they contain no data. By default, empty objects and arrays are ignored.
+         */
+        export function FromJson({ data, title, appendToExisting, excludeEmptyObjects }: JsonConversionOpts): Opt<Doc> {
+            if (excludeEmptyObjects === undefined) {
+                excludeEmptyObjects = true;
+            }
+            if (data === undefined || data === null || ![...primitives, "object"].includes(typeof data)) {
+                return undefined;
+            }
+            let resolved: any;
+            try {
+                resolved = JSON.parse(typeof data === "string" ? data : JSON.stringify(data));
+            } catch (e) {
+                return undefined;
+            }
+            let output: Opt<Doc>;
+            if (typeof resolved === "object" && !(resolved instanceof Array)) {
+                output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc);
+            } else {
+                const result = toField(resolved, excludeEmptyObjects);
+                if (appendToExisting) {
+                    (output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result;
+                } else {
+                    (output = new Doc).json = result;
+                }
+            }
+            title && output && (output.title = title);
+            return output;
         }
-    }
 
+        /**
+         * For each value of the object, recursively convert it to its appropriate field value
+         * and store the field at the appropriate key in the document if it is not undefined
+         * @param object the object to convert
+         * @returns the object mapped from JSON to field values, where each mapping 
+         * might involve arbitrary recursion (since toField might itself call convertObject)
+         */
+        const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => {
+            const hasEntries = Object.keys(object).length;
+            if (hasEntries || !excludeEmptyObjects) {
+                const resolved = target ?? new Doc;
+                if (hasEntries) {
+                    let result: Opt<Field>;
+                    Object.keys(object).map(key => {
+                        // if excludeEmptyObjects is true, any qualifying conversions from toField will
+                        // be undefined, and thus the results that would have
+                        // otherwise been empty (List or Doc)s will just not be written
+                        if (result = toField(object[key], excludeEmptyObjects, key)) {
+                            resolved[key] = result;
+                        }
+                    });
+                }
+                title && (resolved.title = title);
+                return resolved;
+            }
+        };
 
-    export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) {
-        let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey);
-        if (!(optionsCollection instanceof Doc)) {
-            optionsCollection = Docs.Create.StackingDocument([], { title: `${enumeratedFieldKey} field set` }, enumeratedFieldKey);
-            Doc.AddDocToList((Doc.UserDoc().fieldTypes as Doc), "data", optionsCollection as Doc);
-        }
-        const options = optionsCollection as Doc;
-        const targetDoc = doc && Doc.GetProto(Cast(doc.rootDocument, Doc, null) || doc);
-        const docFind = `options.data.find(doc => doc.title === (this.rootDocument||this)["${enumeratedFieldKey}"])?`;
-        targetDoc && (targetDoc.backgroundColor = ComputedField.MakeFunction(docFind + `._backgroundColor || "white"`, undefined, { options }));
-        targetDoc && (targetDoc.color = ComputedField.MakeFunction(docFind + `.color || "black"`, undefined, { options }));
-        targetDoc && (targetDoc.borderRounding = ComputedField.MakeFunction(docFind + `.borderRounding`, undefined, { options }));
-        enumerations.map(enumeration => {
-            const found = DocListCast(options.data).find(d => d.title === enumeration.title);
-            if (found) {
-                found._backgroundColor = enumeration._backgroundColor || found._backgroundColor;
-                found._color = enumeration.color || found._color;
-            } else {
-                Doc.AddDocToList(options, "data", Docs.Create.TextDocument(enumeration.title, enumeration));
+        /**
+         * For each element in the list, recursively convert it to a document or other field 
+         * and push the field to the list if it is not undefined
+         * @param list the list to convert
+         * @returns the list mapped from JSON to field values, where each mapping 
+         * might involve arbitrary recursion (since toField might itself call convertList)
+         */
+        const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => {
+            const target = new List();
+            let result: Opt<Field>;
+            // if excludeEmptyObjects is true, any qualifying conversions from toField will
+            // be undefined, and thus the results that would have
+            // otherwise been empty (List or Doc)s will just not be written
+            list.map(item => (result = toField(item, excludeEmptyObjects)) && target.push(result));
+            if (target.length || !excludeEmptyObjects) {
+                return target;
             }
-        });
-        return optionsCollection;
+        };
+
+        const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<Field> => {
+            if (data === null || data === undefined) {
+                return undefined;
+            }
+            if (primitives.includes(typeof data)) {
+                return data;
+            }
+            if (typeof data === "object") {
+                return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data, excludeEmptyObjects, title, undefined);
+            }
+            throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`);
+        };
     }
+
 }
 
 Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; });
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index a50cca2b0..20e14a68d 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -1,12 +1,6 @@
-import { NDollarRecognizer } from "./ndollar";
-import { Type } from "typescript";
-import { InkField, PointData } from "../fields/InkField";
-import { Docs } from "../client/documents/Documents";
-import { Doc, WidthSym, HeightSym } from "../fields/Doc";
-import { NumCast } from "../fields/Types";
-import { CollectionFreeFormView } from "../client/views/collections/collectionFreeForm/CollectionFreeFormView";
 import { Rect } from "react-measure";
-import { Scripting } from "../client/util/Scripting";
+import { PointData } from "../fields/InkField";
+import { NDollarRecognizer } from "./ndollar";
 
 export namespace GestureUtils {
     export class GestureEvent {
-- 
cgit v1.2.3-70-g09d2


From 4cc9c6ae9a2735266994c62c01818de4bdc25ac6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 8 Jun 2020 20:45:38 -0400
Subject: final cleanup of ink related stuff so that things are more structured
 and we avoid input cycles

---
 src/Utils.ts                                       |  13 ++
 .../apis/google_docs/GooglePhotosClientUtils.ts    |   4 +-
 src/client/apis/youtube/YoutubeBox.tsx             |   2 +-
 src/client/documents/Documents.ts                  |  81 +------------
 src/client/util/CurrentUserUtils.ts                |  59 +++++-----
 src/client/util/LinkManager.ts                     |  10 +-
 src/client/views/DocComponent.tsx                  |   6 +-
 src/client/views/GestureOverlay.tsx                |  35 +++---
 src/client/views/GlobalKeyHandler.ts               |  37 +++---
 src/client/views/InkingControl.scss                | 131 ---------------------
 src/client/views/InkingControl.tsx                 |  42 -------
 src/client/views/InkingStroke.tsx                  |  47 ++++----
 src/client/views/PreviewCursor.tsx                 |   4 +-
 src/client/views/collections/CollectionView.tsx    |  33 ++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  16 +--
 .../collectionFreeForm/InkOptionsMenu.tsx          |  95 ++++++++-------
 src/client/views/nodes/ColorBox.tsx                |  43 +++----
 src/client/views/nodes/DocumentView.tsx            |   6 +-
 src/client/views/nodes/PresBox.tsx                 |   2 +-
 src/client/views/nodes/ScreenshotBox.tsx           |   2 +-
 src/client/views/nodes/VideoBox.tsx                |   6 +-
 src/client/views/nodes/WebBox.tsx                  |   2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +-
 src/client/views/pdf/PDFViewer.tsx                 |   2 +-
 src/client/views/webcam/DashWebRTCVideo.tsx        |   4 +-
 src/fields/Doc.ts                                  |  79 ++++++++++++-
 src/fields/InkField.ts                             |  10 +-
 src/mobile/MobileInterface.tsx                     |   7 +-
 28 files changed, 312 insertions(+), 470 deletions(-)
 delete mode 100644 src/client/views/InkingControl.scss
 delete mode 100644 src/client/views/InkingControl.tsx

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index ef5002bec..e527634fd 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,6 +2,7 @@ import v4 = require('uuid/v4');
 import v5 = require("uuid/v5");
 import { Socket, Room } from 'socket.io';
 import { Message } from './server/Message';
+import { ColorState } from 'react-color';
 
 export namespace Utils {
     export let DRAG_THRESHOLD = 4;
@@ -75,6 +76,18 @@ export namespace Utils {
         document.body.removeChild(textArea);
     }
 
+    export function decimalToHexString(number: number) {
+        if (number < 0) {
+            number = 0xFFFFFFFF + number + 1;
+        }
+        return (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
+    }
+
+    export function colorString(color: ColorState) {
+        return color.hex.startsWith("#") ?
+            color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex;
+    }
+
     export function fromRGBAstr(rgba: string) {
         const rm = rgba.match(/rgb[a]?\(([ 0-9]+)/);
         const r = rm ? Number(rm[1]) : 0;
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index fef71ffeb..a604c7de1 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -8,7 +8,7 @@ import { Cast, StrCast } from "../../../fields/Types";
 import { ImageField } from "../../../fields/URLField";
 import { MediaItem, NewMediaItemResult } from "../../../server/apis/google/SharedTypes";
 import { Utils } from "../../../Utils";
-import { Docs, DocumentOptions } from "../../documents/Documents";
+import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
 import { Networking } from "../../Network";
 import { FormattedTextBox } from "../../views/nodes/formattedText/FormattedTextBox";
 import GoogleAuthenticationManager from "../GoogleAuthenticationManager";
@@ -332,7 +332,7 @@ export namespace GooglePhotos {
                 const url = data.url.href;
                 const target = Doc.MakeAlias(source);
                 const description = parseDescription(target, descriptionKey);
-                await Doc.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
+                await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
                 media.push({ url, description });
             }
             if (media.length) {
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index a7045ccb7..748d571c0 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -349,7 +349,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
 
         const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
 
-        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.GetSelectedTool() && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
         return (
             <>
                 <div className={classname}  >
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 994f8a147..9c807eff2 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -37,7 +37,7 @@ import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
 import { QueryBox } from "../views/nodes/QueryBox";
 import { ColorBox } from "../views/nodes/ColorBox";
 import { DocHolderBox } from "../views/nodes/DocHolderBox";
-import { InkingStroke } from "../views/InkingStroke";
+import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../views/InkingStroke";
 import { InkField } from "../../fields/InkField";
 import { RichTextField } from "../../fields/RichTextField";
 import { extname } from "path";
@@ -47,10 +47,6 @@ import { ContextMenu } from "../views/ContextMenu";
 import { LinkBox } from "../views/nodes/LinkBox";
 import { ScreenshotBox } from "../views/nodes/ScreenshotBox";
 import { ComparisonBox } from "../views/nodes/ComparisonBox";
-import { Id } from "../../fields/FieldSymbols";
-import { listSpec } from "../../fields/Schema";
-import { ObjectField } from "../../fields/ObjectField";
-import { RefField } from "../../fields/RefField";
 import { runInAction } from "mobx";
 import { UndoManager } from "../util/UndoManager";
 const path = require('path');
@@ -149,7 +145,7 @@ export interface DocumentOptions {
     dbDoc?: Doc;
     linkRelationship?: string; // type of relatinoship a link represents
     ischecked?: ScriptField; // returns whether a font icon box is checked
-    activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
+    activeInkPen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
     onClick?: ScriptField;
     onDoubleClick?: ScriptField;
     onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked
@@ -855,7 +851,7 @@ export namespace DocUtils {
             created = Docs.Create.AudioDocument((field).url.href, resolved);
             layout = AudioBox.LayoutString;
         } else if (field instanceof InkField) {
-            created = Docs.Create.InkDocument(InkingStroke.InkColor, Doc.selectedTool, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, (field).inkData, resolved);
+            created = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), (field).inkData, resolved);
             layout = InkingStroke.LayoutString;
         } else if (field instanceof List && field[0] instanceof Doc) {
             created = Docs.Create.StackingDocument(DocListCast(field), resolved);
@@ -913,76 +909,6 @@ export namespace DocUtils {
         return ctor ? ctor(path, options) : undefined;
     }
 
-    export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc {
-        if (Doc.IsBaseProto(doc)) return doc;
-        if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
-        const copy = new Doc(undefined, true);
-        cloneMap.set(doc[Id], copy);
-        if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
-        const exclude = Cast(doc.excludeFields, listSpec("string"), []);
-        Object.keys(doc).forEach(key => {
-            if (exclude.includes(key)) return;
-            const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-            const field = ProxyField.WithoutProxy(() => doc[key]);
-            const copyObjectField = (field: ObjectField) => {
-                const list = Cast(doc[key], listSpec(Doc));
-                if (list !== undefined && !(list instanceof Promise)) {
-                    copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => DocUtils.makeClone(d as Doc, cloneMap, rtfs)));
-                } else if (doc[key] instanceof Doc) {
-                    copy[key] = key.includes("layout[") ? undefined : DocUtils.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields 
-                } else {
-                    copy[key] = ObjectField.MakeCopy(field);
-                    if (field instanceof RichTextField) {
-                        if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) {
-                            rtfs.push({ copy, key, field });
-                        }
-                    }
-                }
-            };
-            if (key === "proto") {
-                if (doc[key] instanceof Doc) {
-                    copy[key] = DocUtils.makeClone(doc[key]!, cloneMap, rtfs);
-                }
-            } else {
-                if (field instanceof RefField) {
-                    copy[key] = field;
-                } else if (cfield instanceof ComputedField) {
-                    copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
-                    (key === "links" && field instanceof ObjectField) && copyObjectField(field);
-                } else if (field instanceof ObjectField) {
-                    copyObjectField(field);
-                } else if (field instanceof Promise) {
-                    debugger; //This shouldn't happend...
-                } else {
-                    copy[key] = field;
-                }
-            }
-        });
-        Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
-        copy.cloneOf = doc;
-        cloneMap.set(doc[Id], copy);
-        return copy;
-    }
-    export function MakeClone(doc: Doc): Doc {
-        const cloneMap = new Map<string, Doc>();
-        const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
-        const copy = DocUtils.makeClone(doc, cloneMap, rtfMap);
-        rtfMap.map(({ copy, key, field }) => {
-            const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
-                const mapped = cloneMap.get(id);
-                return attr + "\"" + (mapped ? mapped[Id] : id) + "\"";
-            };
-            const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => {
-                const mapped = cloneMap.get(id);
-                return href + (mapped ? mapped[Id] : id);
-            };
-            const regex = `(${Utils.prepend("/doc/")})([^"]*)`;
-            const re = new RegExp(regex, "g");
-            copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
-        });
-        return copy;
-    }
-
     export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number): void {
         ContextMenu.Instance.addItem({
             description: "Add Note ...",
@@ -1107,7 +1033,6 @@ export namespace DocUtils {
         }
     }
 
-
     export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) {
         let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey);
         if (!(optionsCollection instanceof Doc)) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index d2614a898..b0cea9947 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -11,7 +11,6 @@ import { ScriptField, ComputedField } from "../../fields/ScriptField";
 import { Cast, PromiseValue, StrCast, NumCast } from "../../fields/Types";
 import { nullAudio } from "../../fields/URLField";
 import { DragManager } from "./DragManager";
-import { InkingControl } from "../views/InkingControl";
 import { Scripting } from "./Scripting";
 import { CollectionViewType } from "../views/collections/CollectionView";
 import { makeTemplate } from "./DropConverter";
@@ -32,7 +31,6 @@ export class CurrentUserUtils {
     public static get MainDocId() { return this.mainDocId; }
     public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
     @computed public static get UserDocument() { return Doc.UserDoc(); }
-    @computed public static get ActivePen() { return Doc.UserDoc().activePen instanceof Doc && (Doc.UserDoc().activePen as Doc).inkPen as Doc; }
 
     @observable public static GuestTarget: Doc | undefined;
     @observable public static GuestWorkspace: Doc | undefined;
@@ -301,7 +299,7 @@ export class CurrentUserUtils {
 
     static creatorBtnDescriptors(doc: Doc): {
         title: string, label: string, icon: string, drag?: string, ignoreClick?: boolean,
-        click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc
+        click?: string, ischecked?: string, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc
     }[] {
         if (doc.emptyPresentation === undefined) {
             doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(),
@@ -334,11 +332,11 @@ export class CurrentUserUtils {
             { title: "Drag an import folder", label: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
             { title: "Drag a mobile view", label: "Phone", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
             { title: "Drag an instance of the device collection", label: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' },
-            // { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
-            // { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            // { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            // { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc },
-            // { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc },
+            // { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen,  this)`, activeInkPen: doc },
+            // { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            // { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            // { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "pink", activeInkPen: doc },
+            // { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activeInkPen = this;', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "white", activeInkPen: doc },
             { title: "Drag a document previewer", label: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc },
             { title: "Toggle a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
             { title: "Connect a Google Account", label: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
@@ -358,7 +356,7 @@ export class CurrentUserUtils {
             }
         }
         const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title));
-        const creatorBtns = buttons.map(({ title, label, icon, ignoreClick, drag, click, ischecked, activePen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({
+        const creatorBtns = buttons.map(({ title, label, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({
             _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100,
             icon,
             title,
@@ -368,7 +366,7 @@ export class CurrentUserUtils {
             onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined,
             onClick: click ? ScriptField.MakeScript(click) : undefined,
             ischecked: ischecked ? ComputedField.MakeFunction(ischecked) : undefined,
-            activePen,
+            activeInkPen,
             backgroundColor,
             removeDropProperties: new List<string>(["dropAction"]),
             dragFactory,
@@ -387,31 +385,31 @@ export class CurrentUserUtils {
     }
 
     static setupMobileButtons(doc: Doc, buttons?: string[]) {
-        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
             { title: "record", icon: "microphone", ignoreClick: true, click: "FILL" },
-            { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
-            { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc },
-            { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc },
-            // { title: "draw", icon: "pen-nib", click: 'switchMobileView(setupMobileInkingDoc, renderMobileInking, onSwitchMobileInking);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "red", activePen: doc },
+            { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen,  this)`, activeInkPen: doc },
+            { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "pink", activeInkPen: doc },
+            { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activeInkPen = this;', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "white", activeInkPen: doc },
+            // { title: "draw", icon: "pen-nib", click: 'switchMobileView(setupMobileInkingDoc, renderMobileInking, onSwitchMobileInking);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "red", activeInkPen: doc },
             { title: "upload", icon: "upload", click: 'switchMobileView(setupMobileUploadDoc, renderMobileUpload, onSwitchMobileUpload);', backgroundColor: "orange" },
             // { title: "upload", icon: "upload", click: 'uploadImageMobile();', backgroundColor: "cyan" },
         ];
         return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
             _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
             onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
-            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
+            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activeInkPen: data.activeInkPen,
             backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
         }));
     }
 
     static setupThumbButtons(doc: Doc) {
-        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, clipboard?: Doc, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
-            { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
-            { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
+        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, clipboard?: Doc, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+            { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen,  this)`, activeInkPen: doc },
+            { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
+            { title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
         ];
         return docProtoData.map(data => Docs.Create.FontIconDocument({
             _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, title: data.title, icon: data.icon,
@@ -419,7 +417,7 @@ export class CurrentUserUtils {
             onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
             clipboard: data.clipboard,
             onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
-            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen, pointerHack: true,
+            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activeInkPen: data.activeInkPen, pointerHack: true,
             backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
         }));
     }
@@ -612,8 +610,8 @@ export class CurrentUserUtils {
     static setupDockedButtons(doc: Doc) {
         if (doc["dockedBtn-pen"] === undefined) {
             doc["dockedBtn-pen"] = CurrentUserUtils.ficon({
-                onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)"),
-                author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activePen.inkPen,  this)`), activePen: doc
+                onClick: ScriptField.MakeScript("activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)"),
+                author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activeInkPen,  this)`), activeInkPen: doc
             });
         }
         if (doc["dockedBtn-undo"] === undefined) {
@@ -688,12 +686,11 @@ export class CurrentUserUtils {
     }
 
     static async updateUserDocument(doc: Doc) {
-        new InkingControl();
         doc.title = Doc.CurrentUserEmail;
-        doc.activePen = doc;
-        doc.inkColor = StrCast(doc.backgroundColor, "rgb(0, 0, 0)");
-        doc.inkWidth = StrCast(doc.inkWidth, "1");
-        doc.inkBezier = StrCast(doc.inkBezier, "");
+        doc.activeInkPen = doc;
+        doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)");
+        doc.activeInkWidth = StrCast(doc.activeInkWidth, "1");
+        doc.activeInkBezier = StrCast(doc.activeInkBezier, "");
         doc.fontSize = NumCast(doc.fontSize, 12);
         doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // 
         doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // 
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 8e6ccf098..95528e25a 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -2,10 +2,8 @@ import { Doc, DocListCast } from "../../fields/Doc";
 import { List } from "../../fields/List";
 import { listSpec } from "../../fields/Schema";
 import { Cast, StrCast } from "../../fields/Types";
-import { Docs } from "../documents/Documents";
 import { Scripting } from "./Scripting";
 
-
 /* 
  * link doc: 
  * - anchor1: doc
@@ -34,16 +32,12 @@ export class LinkManager {
     // the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes'
     // lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type 
     public get LinkManagerDoc(): Doc | undefined {
-        return Docs.Prototypes.MainLinkDocument();
+        return Doc.UserDoc().globalLinkDatabase as Doc;
     }
 
     public getAllLinks(): Doc[] {
         const ldoc = LinkManager.Instance.LinkManagerDoc;
-        if (ldoc) {
-            const docs = DocListCast(ldoc.data);
-            return docs;
-        }
-        return [];
+        return ldoc ? DocListCast(ldoc.data) : [];
     }
 
     public addLink(linkDoc: Doc): boolean {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index c398b2633..3af570f1e 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -57,7 +57,7 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor:
 
         lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result;
 
-        active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);//  && !Doc.selectedTool;  // bcz: inking state shouldn't affect static tools 
+        active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);//  && !Doc.SelectedTool();  // bcz: inking state shouldn't affect static tools 
         protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
     }
     return Component;
@@ -146,9 +146,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
         }
 
         whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
-        active = (outsideReaction?: boolean) => ((Doc.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
+        active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.props.Document.isBackground) &&
             (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0 || BoolCast((this.layoutDoc as any).forceActive)) ? true : false)
-        annotationsActive = (outsideReaction?: boolean) => (Doc.selectedTool !== InkTool.None || (this.props.Document.isBackground && this.props.active()) ||
+        annotationsActive = (outsideReaction?: boolean) => (Doc.GetSelectedTool() !== InkTool.None || (this.props.Document.isBackground && this.props.active()) ||
             (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
     }
     return Component;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 4ea75d7d7..3384b5fce 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -18,19 +18,18 @@ import { LinkManager } from "../util/LinkManager";
 import { Scripting } from "../util/Scripting";
 import { Transform } from "../util/Transform";
 import "./GestureOverlay.scss";
-import { InkingControl } from "./InkingControl";
+import { ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke";
 import { DocumentView } from "./nodes/DocumentView";
 import { RadialMenu } from "./nodes/RadialMenu";
 import HorizontalPalette from "./Palette";
 import { Touchable } from "./Touchable";
 import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
-import { InkingStroke } from "./InkingStroke";
-
 
 @observer
 export default class GestureOverlay extends Touchable {
     static Instance: GestureOverlay;
 
+    @observable public InkShape: string = "";
     @observable public SavedColor?: string;
     @observable public SavedWidth?: string;
     @observable public Tool: ToolglassTools = ToolglassTools.None;
@@ -492,7 +491,7 @@ export default class GestureOverlay extends Touchable {
 
     @action
     onPointerDown = (e: React.PointerEvent) => {
-        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) {
             this._points.push({ X: e.clientX, Y: e.clientY });
             e.stopPropagation();
             e.preventDefault();
@@ -506,7 +505,7 @@ export default class GestureOverlay extends Touchable {
 
     @action
     onPointerMove = (e: PointerEvent) => {
-        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) {
             this._points.push({ X: e.clientX, Y: e.clientY });
             e.stopPropagation();
             e.preventDefault();
@@ -580,8 +579,8 @@ export default class GestureOverlay extends Touchable {
                 DocServer.Mobile.dispatchGesturePoints({
                     points: this._points,
                     bounds: B,
-                    color: InkingStroke.InkColor,
-                    width: InkingStroke.InkWidth
+                    color: ActiveInkColor(),
+                    width: ActiveInkWidth()
                 });
             }
 
@@ -624,11 +623,11 @@ export default class GestureOverlay extends Touchable {
                 }
             }
             //if any of the shape is activated in the InkOptionsMenu
-            else if (InkingStroke.InkShape) {
-                this.makePolygon(InkingStroke.InkShape, false);
+            else if (this.InkShape) {
+                this.makePolygon(this.InkShape, false);
                 this.dispatchGesture(GestureUtils.Gestures.Stroke);
                 this._points = [];
-                InkingStroke.InkShape = "";
+                this.InkShape = "";
             }
             // if we're not drawing in a toolglass try to recognize as gesture
             else {
@@ -808,11 +807,11 @@ export default class GestureOverlay extends Touchable {
             [this._strokes.map(l => {
                 const b = this.getBounds(l);
                 return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                    {InteractionUtils.CreatePolyline(l, b.left, b.top, InkingStroke.InkColor, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, 1, 1, InkingStroke.InkShape)}
+                    {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape)}
                 </svg>;
             }),
             this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, InkingStroke.InkColor, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, 1, 1, InkingStroke.InkShape)}
+                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape)}
             </svg>]
         ];
     }
@@ -902,16 +901,16 @@ Scripting.addGlobal(function setToolglass(tool: any) {
 });
 Scripting.addGlobal(function setPen(width: any, color: any) {
     runInAction(() => {
-        GestureOverlay.Instance.SavedColor = InkingStroke.InkColor;
-        InkingControl.Instance.switchColor(color);
-        GestureOverlay.Instance.SavedWidth = InkingStroke.InkWidth;
-        InkingControl.Instance.switchWidth(width);
+        GestureOverlay.Instance.SavedColor = ActiveInkColor();
+        SetActiveInkColor(color);
+        GestureOverlay.Instance.SavedWidth = ActiveInkWidth();
+        SetActiveInkWidth(width);
     });
 });
 Scripting.addGlobal(function resetPen() {
     runInAction(() => {
-        InkingControl.Instance.switchColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)");
-        InkingControl.Instance.switchWidth(GestureOverlay.Instance.SavedWidth ?? "2");
+        SetActiveInkColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)");
+        SetActiveInkWidth(GestureOverlay.Instance.SavedWidth ?? "2");
     });
 });
 Scripting.addGlobal(function createText(text: any, x: any, y: any) {
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 255142771..2d5af5386 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -1,26 +1,25 @@
-import { UndoManager, undoBatch } from "../util/UndoManager";
-import { SelectionManager } from "../util/SelectionManager";
-import { CollectionDockingView } from "./collections/CollectionDockingView";
-import { MainView } from "./MainView";
-import { DragManager } from "../util/DragManager";
-import { action, runInAction } from "mobx";
+import { action } from "mobx";
+import { DateField } from "../../fields/DateField";
 import { Doc, DocListCast } from "../../fields/Doc";
-import { DictationManager } from "../util/DictationManager";
-import SharingManager from "../util/SharingManager";
-import { Cast, PromiseValue, NumCast } from "../../fields/Types";
-import { ScriptField } from "../../fields/ScriptField";
-import { InkingControl } from "./InkingControl";
+import { Id } from "../../fields/FieldSymbols";
 import { InkTool } from "../../fields/InkField";
-import { DocumentView } from "./nodes/DocumentView";
+import { List } from "../../fields/List";
+import { ScriptField } from "../../fields/ScriptField";
+import { Cast, PromiseValue } from "../../fields/Types";
 import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager";
-import { CollectionFreeFormView } from "./collections/collectionFreeForm/CollectionFreeFormView";
+import { DocServer } from "../DocServer";
+import { DocumentType } from "../documents/DocumentTypes";
+import { DictationManager } from "../util/DictationManager";
+import { DragManager } from "../util/DragManager";
+import { SelectionManager } from "../util/SelectionManager";
+import SharingManager from "../util/SharingManager";
+import { undoBatch, UndoManager } from "../util/UndoManager";
+import { CollectionDockingView } from "./collections/CollectionDockingView";
 import { MarqueeView } from "./collections/collectionFreeForm/MarqueeView";
-import { Id } from "../../fields/FieldSymbols";
 import { DocumentDecorations } from "./DocumentDecorations";
-import { DocumentType } from "../documents/DocumentTypes";
-import { DocServer } from "../DocServer";
-import { List } from "../../fields/List";
-import { DateField } from "../../fields/DateField";
+import { InkingStroke } from "./InkingStroke";
+import { MainView } from "./MainView";
+import { DocumentView } from "./nodes/DocumentView";
 
 const modifiers = ["control", "meta", "shift", "alt"];
 type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -78,7 +77,7 @@ export default class KeyManager {
                 break;
             case "escape":
                 const main = MainView.Instance;
-                InkingControl.Instance.switchTool(InkTool.None);
+                Doc.SetSelectedTool(InkTool.None);
                 if (main.isPointerDown) {
                     DragManager.AbortDrag();
                 } else {
diff --git a/src/client/views/InkingControl.scss b/src/client/views/InkingControl.scss
deleted file mode 100644
index 465e14d07..000000000
--- a/src/client/views/InkingControl.scss
+++ /dev/null
@@ -1,131 +0,0 @@
-@import "globalCssVariables";
-.inking-control {
-    bottom: 20px;
-    margin: 0;
-    padding: 0;
-    display: flex;
-    label,
-    input,
-    option {
-        font-size: 12px;
-    }
-    input[type="range"] {
-        -webkit-appearance: none;
-        background-color: transparent;
-        vertical-align: middle;
-        margin-top: 8px;
-        &:focus {
-            outline: none;
-        }
-        &::-webkit-slider-runnable-track {
-            width: 100%;
-            height: 3px;
-            border-radius: 1.5px;
-            cursor: pointer;
-            background: $intermediate-color;
-        }
-        &::-webkit-slider-thumb {
-            height: 12px;
-            width: 12px;
-            border: 1px solid $intermediate-color;
-            border-radius: 6px;
-            background: $light-color;
-            cursor: pointer;
-            -webkit-appearance: none;
-            margin-top: -4px;
-        }
-        &::-moz-range-track {
-            width: 100%;
-            height: 3px;
-            border-radius: 1.5px;
-            cursor: pointer;
-            background: $light-color;
-        }
-        &::-moz-range-thumb {
-            height: 12px;
-            width: 12px;
-            border: 1px solid $intermediate-color;
-            border-radius: 6px;
-            background: $light-color;
-            cursor: pointer;
-            -webkit-appearance: none;
-            margin-top: -4px;
-        }
-    }
-    input[type="text"] {
-        border: none;
-        padding: 0 0px;
-        background: transparent;
-        color: $dark-color;
-        font-size: 12px;
-        margin-top: 4px;
-    }
-    .ink-panel {
-        height: 24px;
-        vertical-align: middle;
-        line-height: 28px;
-        padding: 0 10px;
-        color: $intermediate-color;
-        &:first {
-            margin-top: 0;
-        }
-    }
-    .ink-tools {
-        display: flex;
-        background-color: transparent;
-        border-radius: 0;
-        padding: 0;
-        button {
-            height: 36px;
-            padding: 0px;
-            padding-bottom: 3px;
-            margin-left: 10px;
-            background-color: transparent;
-            color: $intermediate-color;
-        }
-        button:hover {
-            transform: scale(1.15);
-        }
-    }
-    .ink-size {
-        display: flex;
-        justify-content: space-between;
-        input[type="text"] {
-            width: 42px;
-        }
-        >* {
-            margin-right: 6px;
-            &:last-child {
-                margin-right: 0;
-            }
-        }
-    }
-    .ink-color {
-        display: flex;
-        position: relative;
-        padding-right: 0;
-        label {
-            margin-right: 6px;
-        }
-        .ink-color-display {
-            border-radius: 11px;
-            width: 22px;
-            height: 22px;
-            cursor: pointer;
-            text-align: center; //   span {
-            //     color: $light-color;
-            //     font-size: 8px;
-            //     user-select: none;
-            //   }
-        }
-        .ink-color-picker {
-            background-color: $light-color;
-            border-radius: 5px;
-            padding: 12px;
-            position: absolute;
-            bottom: 36px;
-            left: -3px;
-            box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
deleted file mode 100644
index 349bc6ffc..000000000
--- a/src/client/views/InkingControl.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { action, observable } from "mobx";
-import { Doc } from "../../fields/Doc";
-import { InkTool } from "../../fields/InkField";
-import { CurrentUserUtils } from "../util/CurrentUserUtils";
-import { Scripting } from "../util/Scripting";
-import { InkingStroke } from "./InkingStroke";
-
-export class InkingControl {
-    @observable static Instance: InkingControl;
-    @observable public _open: boolean = false;
-    constructor() {
-        InkingControl.Instance = this;
-    }
-
-    switchTool = action((tool: InkTool): void => {
-        // this._selectedTool = tool;
-        Doc.UserDoc().inkTool = tool;
-    });
-
-    @action
-    switchWidth = (width: string): void => {
-        if (!isNaN(parseInt(width))) {
-            CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkWidth = InkingStroke.InkWidth = width);
-        }
-    }
-
-    @action
-    switchBezier = (bezier: string): void => {
-        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkBezier = InkingStroke.InkBezierApprox = isNaN(parseInt(bezier)) ? "" : bezier);
-    }
-
-    @action
-    switchColor(value: string) {
-        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkColor = InkingStroke.InkColor = value);
-    }
-}
-Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.switchColor(color); });
-Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); });
-Scripting.addGlobal(function activateStamp(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Stamp : InkTool.None); });
-Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); });
-Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); });
-Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.switchColor(color); });
\ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 78d729eee..b545ede54 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,6 +1,6 @@
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faPaintBrush } from "@fortawesome/free-solid-svg-icons";
-import { observable, runInAction } from "mobx";
+import { observable, runInAction, action } from "mobx";
 import { observer } from "mobx-react";
 import { documentSchema } from "../../fields/documentSchemas";
 import { InkData, InkField, InkTool } from "../../fields/InkField";
@@ -14,6 +14,8 @@ import { ViewBoxBaseComponent } from "./DocComponent";
 import "./InkingStroke.scss";
 import { FieldView, FieldViewProps } from "./nodes/FieldView";
 import React = require("react");
+import { Scripting } from "../util/Scripting";
+import { Doc } from "../../fields/Doc";
 
 library.add(faPaintBrush);
 
@@ -23,22 +25,6 @@ const InkDocument = makeInterface(documentSchema);
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
     public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
-    @observable public static InkColor: string;
-    @observable public static InkWidth: string;
-    @observable public static InkBezierApprox: string;
-    @observable public static InkShape: string;
-
-    constructor(props: any) {
-        super(props);
-        if (InkingStroke.InkBezierApprox === undefined) {
-            runInAction(() => {
-                InkingStroke.InkBezierApprox = "";
-                InkingStroke.InkWidth = "1";
-                InkingStroke.InkColor = "black";
-                InkingStroke.InkShape = "";
-            });
-        }
-    }
 
     private analyzeStrokes = () => {
         const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
@@ -59,9 +45,9 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const scaleX = this.props.PanelWidth() / width;
         const scaleY = this.props.PanelHeight() / height;
         const points = InteractionUtils.CreatePolyline(data, left, top,
-            StrCast(this.layoutDoc.color, InkingStroke.InkColor || "black"),
-            StrCast(this.layoutDoc.strokeWidth, InkingStroke.InkWidth || "1"),
-            StrCast(this.layoutDoc.strokeBezier, InkingStroke.InkBezierApprox || ""), scaleX, scaleY, "");
+            StrCast(this.layoutDoc.color, ActiveInkColor()),
+            StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth()),
+            StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "");
         return (
             <svg className="inkingStroke"
                 width={width}
@@ -79,4 +65,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             </svg>
         );
     }
-}
\ No newline at end of file
+}
+
+
+export function SetActiveInkWidth(width: string): void { !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); }
+export function SetActiveBezierApprox(bezier: string): void { ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? "" : bezier); }
+export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeInkColor = value); }
+export function ActiveInkPen(): Doc { return Cast(Doc.UserDoc().activeInkPen, Doc, null); }
+export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); }
+export function ActiveInkWidth(): string { return StrCast(ActiveInkPen()?.activeInkWidth, "1"); }
+export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); }
+Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) {
+    Doc.SetSelectedTool(pen ? InkTool.Highlighter : InkTool.None);
+    SetActiveInkWidth(width);
+    SetActiveInkColor(color);
+});
+Scripting.addGlobal(function activateEraser(pen: any) { return Doc.SetSelectedTool(pen ? InkTool.Eraser : InkTool.None); });
+Scripting.addGlobal(function activateStamp(pen: any) { return Doc.SetSelectedTool(pen ? InkTool.Stamp : InkTool.None); });
+Scripting.addGlobal(function deactivateInk() { return Doc.SetSelectedTool(InkTool.None); });
+Scripting.addGlobal(function setInkWidth(width: any) { return Doc.SetSelectedTool(width); });
+Scripting.addGlobal(function setInkColor(color: any) { return Doc.SetSelectedTool(color); });
\ No newline at end of file
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 33150ab7c..dd65681d4 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
 import "normalize.css";
 import * as React from 'react';
 import "./PreviewCursor.scss";
-import { Docs, DocUtils } from '../documents/Documents';
+import { Docs } from '../documents/Documents';
 import { Doc } from '../../fields/Doc';
 import { Transform } from "../util/Transform";
 import { DocServer } from '../DocServer';
@@ -65,7 +65,7 @@ export class PreviewCursor extends React.Component<{}> {
                         count++;
                         if (doc instanceof Doc) {
                             i === 1 && (first = doc);
-                            const alias = DocUtils.MakeClone(doc);
+                            const alias = Doc.MakeClone(doc);
                             const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx;
                             const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty;
                             alias.x = newPoint[0] + deltaX;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 873f61331..8a73565c8 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,34 +1,37 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEye, faEdit } from '@fortawesome/free-regular-svg-icons';
+import { faEdit, faEye } from '@fortawesome/free-regular-svg-icons';
+import { faColumns, faCopy, faEllipsisV, faFingerprint, faGlobeAmericas, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree, faGlobeAmericas } from '@fortawesome/free-solid-svg-icons';
-import { action, observable, computed } from 'mobx';
+import { action, computed, observable } from 'mobx';
 import { observer } from "mobx-react";
 import * as React from 'react';
 import Lightbox from 'react-image-lightbox-with-rotate';
 import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
 import { DateField } from '../../../fields/DateField';
-import { DataSym, Doc, DocListCast, Field, Opt, AclSym, AclAddonly, AclReadonly } from '../../../fields/Doc';
+import { AclAddonly, AclReadonly, AclSym, DataSym, Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
-import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../fields/Types';
+import { ObjectField } from '../../../fields/ObjectField';
+import { listSpec } from '../../../fields/Schema';
+import { ComputedField, ScriptField } from '../../../fields/ScriptField';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { Utils, setupMoveUpEvents, returnFalse, returnZero, emptyPath, emptyFunction, returnOne } from '../../../Utils';
+import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
+import { DocumentType } from '../../documents/DocumentTypes';
 import { ImageUtils } from '../../util/Import & Export/ImageUtils';
+import { InteractionUtils } from '../../util/InteractionUtils';
 import { ContextMenu } from "../ContextMenu";
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import { ScriptBox } from '../ScriptBox';
 import { Touchable } from '../Touchable';
-import { Id } from '../../../fields/FieldSymbols';
-import { listSpec } from '../../../fields/Schema';
-import { ScriptField, ComputedField } from '../../../fields/ScriptField';
-import { InteractionUtils } from '../../util/InteractionUtils';
-import { ObjectField } from '../../../fields/ObjectField';
+import './CollectionView.scss';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
 export const COLLECTION_BORDER_WIDTH = 2;
 const path = require('path');
+
 library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faGlobeAmericas, faEllipsisV, faImage, faEye as any, faCopy);
 
 export enum CollectionViewType {
@@ -500,6 +503,12 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
         </div>);
     }
 }
+
+
+
+// to avoid an import cycle that will cause runtime errors, 
+// we import all of the bindings needed within methods after the
+// class has been defined.
 import { SubCollectionViewProps } from './CollectionSubView';
 import { CollectionCarouselView } from './CollectionCarouselView';
 import { CollectionDockingView } from "./CollectionDockingView";
@@ -515,8 +524,6 @@ import { CollectionTimeView } from './CollectionTimeView';
 import { CollectionTreeView } from "./CollectionTreeView";
 import { CollectionMapView } from './CollectionMapView';
 import { CollectionPileView } from './CollectionPileView';
-import './CollectionView.scss';
 import { CollectionViewBaseChrome } from './CollectionViewChromes';
 import { CurrentUserUtils } from '../../util/CurrentUserUtils';
-import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 576c0c560..9c0e5e917 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -45,7 +45,7 @@ import React = require("react");
 import { CollectionViewType } from "../CollectionView";
 import { Timeline } from "../../animationtimeline/Timeline";
 import { SnappingManager } from "../../../util/SnappingManager";
-import { InkingStroke } from "../../InkingStroke";
+import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../../InkingStroke";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -390,7 +390,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
+        if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) {
             return;
         }
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
@@ -407,7 +407,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             document.addEventListener("pointermove", this.onPointerMove);
             document.addEventListener("pointerup", this.onPointerUp);
             // if not using a pen and in no ink mode
-            if (Doc.selectedTool === InkTool.None) {
+            if (Doc.GetSelectedTool() === InkTool.None) {
                 this._downX = this._lastX = e.pageX;
                 this._downY = this._lastY = e.pageY;
             }
@@ -431,13 +431,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     this.addMoveListeners();
                     this.removeEndListeners();
                     this.addEndListeners();
-                    // if (Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen) {
+                    // if (Doc.SelectedTool() === InkTool.Highlighter || Doc.SelectedTool() === InkTool.Pen) {
                     //     e.stopPropagation();
                     //     e.preventDefault();
                     //     const point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
                     //     this._points.push({ X: point[0], Y: point[1] });
                     // }
-                    if (Doc.selectedTool === InkTool.None) {
+                    if (Doc.GetSelectedTool() === InkTool.None) {
                         this._lastX = pt.pageX;
                         this._lastY = pt.pageY;
                         e.preventDefault();
@@ -457,7 +457,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             case GestureUtils.Gestures.Stroke:
                 const points = ge.points;
                 const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
-                const inkDoc = Docs.Create.InkDocument(InkingStroke.InkColor, Doc.selectedTool, InkingStroke.InkWidth, InkingStroke.InkBezierApprox, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
+                const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
                 this.addDocument(inkDoc);
                 e.stopPropagation();
                 break;
@@ -618,7 +618,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             return;
         }
         if (!e.cancelBubble) {
-            const selectedTool = Doc.selectedTool;
+            const selectedTool = Doc.GetSelectedTool();
             if (selectedTool === InkTool.None) {
                 if (this._hitCluster && this.tryDragCluster(e)) {
                     e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -640,7 +640,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
             const pt = myTouches[0];
             if (pt) {
-                if (Doc.selectedTool === InkTool.None) {
+                if (Doc.GetSelectedTool() === InkTool.None) {
                     if (this._hitCluster && this.tryDragCluster(e)) {
                         e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
                         e.preventDefault();
diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
index b35c9682e..5a27f74e5 100644
--- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx
@@ -1,15 +1,15 @@
 import React = require("react");
 import AntimodeMenu from "../../AntimodeMenu";
 import { observer } from "mobx-react";
-import { observable, action } from "mobx";
+import { observable, action, computed } from "mobx";
 import "./InkOptionsMenu.scss";
-import { InkingStroke } from "../../InkingStroke";
+import { ActiveInkColor, ActiveInkBezierApprox, SetActiveInkWidth, SetActiveInkColor, SetActiveBezierApprox } from "../../InkingStroke";
 import { Scripting } from "../../../util/Scripting";
 import { InkTool } from "../../../../fields/InkField";
-import { InkingControl } from "../../InkingControl";
-import { StrCast } from "../../../../fields/Types";
 import { ColorState } from "react-color";
-import { ColorBox } from "../../nodes/ColorBox";
+import { Utils } from "../../../../Utils";
+import GestureOverlay from "../../GestureOverlay";
+import { Doc } from "../../../../fields/Doc";
 
 @observer
 export default class InkOptionsMenu extends AntimodeMenu {
@@ -35,15 +35,15 @@ export default class InkOptionsMenu extends AntimodeMenu {
             hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" },
             rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "",
         };
-        ColorBox.switchColor(col);
+        SetActiveInkColor(Utils.colorString(col));
     }
 
     @action
     changeBezier = (e: React.PointerEvent): void => {
-        InkingControl.Instance.switchBezier(!InkingStroke.InkBezierApprox ? "300" : "");
+        SetActiveBezierApprox(!ActiveInkBezierApprox() ? "300" : "");
     }
 
-    render() {
+    @computed get widthPicker() {
         var widthPicker = <button
             className="antimodeMenu-button"
             key="width"
@@ -52,30 +52,33 @@ export default class InkOptionsMenu extends AntimodeMenu {
             W
         </button>;
         if (this._widthBtn) {
-            widthPicker = <div className="btn2-group">
+            widthPicker = <div className="btn2-group" key="width">
                 {widthPicker}
                 {this._width.map(wid => {
                     return <button
                         className="antimodeMenu-button"
                         key={wid}
-                        onPointerDown={action(() => { InkingControl.Instance.switchWidth(wid); this._widthBtn = false; })}
+                        onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; })}
                         style={{ backgroundColor: this._widthBtn ? "121212" : "" }}>
                         {wid}
                     </button>;
                 })}
             </div>;
         }
+        return widthPicker;
+    }
 
+    @computed get colorPicker() {
         var colorPicker = <button
             className="antimodeMenu-button"
             key="color"
             title="colorChanger"
             onPointerDown={action(e => this._colorBtn = !this._colorBtn)}
             style={{ backgroundColor: this._colorBtn ? "121212" : "" }}>
-            <div className="color-preview" style={{ backgroundColor: InkingStroke.InkColor ?? "121212" }}></div>
+            <div className="color-preview" style={{ backgroundColor: ActiveInkColor() ?? "121212" }}></div>
         </button>;
         if (this._colorBtn) {
-            colorPicker = <div className="btn-group">
+            colorPicker = <div className="btn-group" key="color">
                 {colorPicker}
                 {this._palette.map(color => {
                     return <button
@@ -88,46 +91,50 @@ export default class InkOptionsMenu extends AntimodeMenu {
                 })}
             </div>;
         }
+        return colorPicker;
+    }
 
-        const buttons = [
-            <button className="antimodeMenu-button"
-                title="Drag"
-                key="drag"
-                onPointerDown={e => this.dragStart(e)}>
-                ✜
-            </button>,
-            <>
-                {this._buttons.map((btn, i) => <button
-                    className="antimodeMenu-button"
-                    title={`Draw ${btn}`}
-                    key={btn}
-                    onPointerDown={action(e => InkingStroke.InkShape = btn)}
-                    style={btn === InkingStroke.InkShape ? { backgroundColor: "121212" } : {}}>
-                    {this._icons[i]}
-                </button>)},
-            </>,
-            <button
+    @computed get shapeButtons() {
+        return <>
+            {this._buttons.map((btn, i) => <button
                 className="antimodeMenu-button"
-                title="Bezier changer"
-                key="bezier"
-                onPointerDown={e => this.changeBezier(e)}
-                style={InkingStroke.InkBezierApprox ? { backgroundColor: "121212" } : {}}>
-                B
-            </button>,
-            widthPicker,
-            colorPicker,
+                title={`Draw ${btn}`}
+                key={btn}
+                onPointerDown={action(e => GestureOverlay.Instance.InkShape = btn)}
+                style={{ backgroundColor: btn === GestureOverlay.Instance.InkShape ? "121212" : "" }}>
+                {this._icons[i]}
+            </button>)},
+        </>;
+    }
+
+    @computed get bezierButton() {
+        return <button
+            className="antimodeMenu-button"
+            title="Bezier changer"
+            key="bezier"
+            onPointerDown={e => this.changeBezier(e)}
+            style={ { backgroundColor:ActiveInkBezierApprox() ? "121212":"" } }>
+            B
+        </button>;
+    }
+
+    render() {
+        const buttons = [
+            <button className="antimodeMenu-button" title="Drag" key="drag" onPointerDown={e => this.dragStart(e)}>  ✜  </button>,
+            this.shapeButtons,
+            this.bezierButton,
+            this.widthPicker,
+            this.colorPicker,
         ];
         return this.getElement(buttons);
     }
 }
-Scripting.addGlobal(function activatePen(pen: any) {
-    InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None);
-    if (pen) {
-        InkingControl.Instance.switchWidth(StrCast(pen.inkWidth, "1"));
-        InkingControl.Instance.switchColor(StrCast(pen.inkColor, "black"));
-        InkingControl.Instance.switchBezier(StrCast(pen.inkBezier, ""));
+Scripting.addGlobal(function activatePen(penBtn: any) {
+    if (penBtn) {
+        Doc.SetSelectedTool(InkTool.Pen);
         InkOptionsMenu.Instance.jumpTo(300, 300);
     } else {
+        Doc.SetSelectedTool(InkTool.None);
         InkOptionsMenu.Instance.fadeOut(true);
     }
 });
\ No newline at end of file
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 762c57ae9..0d6258cf3 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -1,20 +1,19 @@
 import React = require("react");
+import { action } from "mobx";
 import { observer } from "mobx-react";
-import { SketchPicker, ColorState } from 'react-color';
+import { ColorState, SketchPicker } from 'react-color';
+import { Doc } from "../../../fields/Doc";
+import { Utils } from "../../../Utils";
 import { documentSchema } from "../../../fields/documentSchemas";
+import { InkTool } from "../../../fields/InkField";
 import { makeInterface } from "../../../fields/Schema";
 import { StrCast } from "../../../fields/Types";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
 import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
 import { ViewBoxBaseComponent } from "../DocComponent";
-import { InkingControl } from "../InkingControl";
+import { ActiveInkPen, ActiveInkWidth, ActiveInkBezierApprox, SetActiveInkColor, SetActiveInkWidth, SetActiveBezierApprox } from "../InkingStroke";
 import "./ColorBox.scss";
 import { FieldView, FieldViewProps } from './FieldView';
-import { InkingStroke } from "../InkingStroke";
-import { Doc } from "../../../fields/Doc";
-import { InkTool } from "../../../fields/InkField";
-import { undoBatch } from "../../util/UndoManager";
-import { action } from "mobx";
 import { FormattedTextBox } from "./formattedText/FormattedTextBox";
 
 type ColorDocument = makeInterface<[typeof documentSchema]>;
@@ -24,22 +23,13 @@ const ColorDocument = makeInterface(documentSchema);
 export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument>(ColorDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
 
-    static decimalToHexString(number: number) {
-        if (number < 0) {
-            number = 0xFFFFFFFF + number + 1;
-        }
-        return (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
-    }
-
     @undoBatch
     @action
     static switchColor(color: ColorState) {
-        Doc.UserDoc().backgroundColor = color.hex.startsWith("#") ?
-            color.hex + (color.rgb.a ? ColorBox.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex;
-        InkingStroke.InkColor = StrCast(Doc.UserDoc().backgroundColor);
-        CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.inkColor = color.hex);
+        Doc.UserDoc().backgroundColor = Utils.colorString(color);
+        SetActiveInkColor(color.hex);
 
-        if (Doc.selectedTool === InkTool.None) {
+        if (Doc.GetSelectedTool() === InkTool.None) {
             const selected = SelectionManager.SelectedDocuments();
             selected.map(view => {
                 const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
@@ -56,6 +46,9 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument
         }
     }
 
+    constructor(props: any) {
+        super(props);
+    }
     render() {
         const selDoc = SelectionManager.SelectedDocuments()?.[0]?.rootDoc;
         return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`}
@@ -63,13 +56,13 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument
             style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
 
             <SketchPicker onChange={ColorBox.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
-                color={StrCast(CurrentUserUtils.ActivePen ? CurrentUserUtils.ActivePen.backgroundColor : undefined,
+                color={StrCast(ActiveInkPen()?.backgroundColor,
                     StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} />
             <div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}>
-                <div> {InkingStroke.InkWidth ?? 2}</div>
-                <input type="range" value={InkingStroke.InkWidth ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchWidth(e.target.value)} />
-                <div> {InkingStroke.InkBezierApprox ?? 2}</div>
-                <input type="range" value={InkingStroke.InkBezierApprox ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchBezier(e.target.value)} />
+                <div> {ActiveInkWidth() ?? 2}</div>
+                <input type="range" value={ActiveInkWidth() ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveInkWidth(e.target.value)} />
+                <div> {ActiveInkBezierApprox() ?? 2}</div>
+                <input type="range" value={ActiveInkBezierApprox() ?? 2} defaultValue={2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveBezierApprox(e.target.value)} />
                 <br />
                 <br />
             </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 196e61a31..98be1adc0 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -512,7 +512,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         // console.log(e.button)
         // console.log(e.nativeEvent)
         // continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document)
-        if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) {
+        if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) {
             if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
                 e.stopPropagation();
                 // TODO: check here for panning/inking
@@ -543,7 +543,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     onPointerMove = (e: PointerEvent): void => {
 
         if ((e as any).formattedHandled) { e.stopPropagation(); return; }
-        if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || Doc.selectedTool === InkTool.Highlighter || Doc.selectedTool === InkTool.Pen)) return;
+        if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) return;
         if (e.cancelBubble && this.active) {
             document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
         }
@@ -1090,7 +1090,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @computed get ignorePointerEvents() {
         return this.props.pointerEvents === false ||
             (this.Document.isBackground && !this.isSelected() && !SnappingManager.GetIsDragging()) ||
-            (this.Document.type === DocumentType.INK && Doc.selectedTool !== InkTool.None);
+            (this.Document.type === DocumentType.INK && Doc.GetSelectedTool() !== InkTool.None);
     }
     @undoBatch
     @action
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 81669dc2a..dbc879920 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -293,7 +293,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
     selectElement = (doc: Doc) => this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.itemIndex));
     getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
     panelHeight = () => this.props.PanelHeight() - 20;
-    active = (outsideReaction?: boolean) => ((Doc.selectedTool === InkTool.None && !this.layoutDoc.isBackground) &&
+    active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground) &&
         (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
 
     render() {
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 1184f32f1..d75b864cf 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -132,7 +132,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh
     }
 
     @computed get content() {
-        const interactive = Doc.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+        const interactive = Doc.GetSelectedTool() || !this.props.isSelected() ? "" : "-interactive";
         const style = "videoBox-content" + interactive;
         return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
             style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 8c8b7a622..1e0a859d3 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -228,7 +228,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
 
     @computed get content() {
         const field = Cast(this.dataDoc[this.fieldKey], VideoField);
-        const interactive = Doc.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+        const interactive = Doc.GetSelectedTool() || !this.props.isSelected() ? "" : "-interactive";
         const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div>Loading</div> :
             <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
@@ -272,8 +272,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
             this._reactionDisposer && this._reactionDisposer();
             this._youtubeReactionDisposer && this._youtubeReactionDisposer();
             this._reactionDisposer = reaction(() => this.layoutDoc.currentTimecode, () => !this._playing && this.Seek((this.layoutDoc.currentTimecode || 0)));
-            this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, Doc.selectedTool], () => {
-                const interactive = Doc.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting;
+            this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, Doc.GetSelectedTool()], () => {
+                const interactive = Doc.GetSelectedTool() === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting;
                 iframe.style.pointerEvents = interactive ? "all" : "none";
             }, { fireImmediately: true });
         };
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index dcc87068b..8fdde61e0 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -424,7 +424,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         const frozen = !this.props.isSelected() || decInteracting;
 
         return (<>
-            <div className={"webBox-cont" + (this.props.isSelected() && Doc.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")}
+            <div className={"webBox-cont" + (this.props.isSelected() && Doc.GetSelectedTool() === InkTool.None && !decInteracting ? "-interactive" : "")}
                 onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
                 {view}
             </div>;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index c0a8bec40..2c1d93222 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1211,7 +1211,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         TraceMobx();
         const scale = this.props.ContentScaling() * NumCast(this.layoutDoc.scale, 1);
         const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
-        const interactive = Doc.selectedTool || this.layoutDoc.isBackground;
+        const interactive = Doc.GetSelectedTool() || this.layoutDoc.isBackground;
         if (this.props.isSelected()) {
             this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props);
         } else if (FormattedTextBoxComment.textBox === this) {
@@ -1266,7 +1266,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     </div>
                     {!this.layoutDoc._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
                         <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> :
-                        <div className={"formattedTextBox-sidebar" + (Doc.selectedTool !== InkTool.None ? "-inking" : "")}
+                        <div className={"formattedTextBox-sidebar" + (Doc.GetSelectedTool() !== InkTool.None ? "-inking" : "")}
                             style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
                             <CollectionFreeFormView {...this.props}
                                 PanelHeight={this.props.PanelHeight}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index c18f69e1d..8cf5026c8 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -650,7 +650,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
     panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0);
     panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0);
     @computed get overlayLayer() {
-        return <div className={`pdfViewer-overlay${Doc.selectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay"
+        return <div className={`pdfViewer-overlay${Doc.GetSelectedTool() !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay"
             style={{ transform: `scale(${this._zoomed})` }}>
             <CollectionFreeFormView {...this.props}
                 LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? emptyPath}
diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx
index 0b1e90b1d..83de9cc15 100644
--- a/src/client/views/webcam/DashWebRTCVideo.tsx
+++ b/src/client/views/webcam/DashWebRTCVideo.tsx
@@ -10,6 +10,7 @@ import { initialize, hangup, refreshVideos } from "./WebCamLogic";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
 import { faSync, faPhoneSlash } from "@fortawesome/free-solid-svg-icons";
+import { Doc } from "../../../fields/Doc";
 
 library.add(faSync);
 library.add(faPhoneSlash);
@@ -71,8 +72,7 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV
             </div >;
 
         const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
-        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
-
+        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.GetSelectedTool() && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
 
         return (
             <>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index d635836bb..981025483 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -6,7 +6,7 @@ import { DocumentType } from "../client/documents/DocumentTypes";
 import { Scripting, scriptingGlobal } from "../client/util/Scripting";
 import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
 import { UndoManager } from "../client/util/UndoManager";
-import { intersectRect } from "../Utils";
+import { intersectRect, Utils } from "../Utils";
 import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols";
 import { InkTool } from "./InkField";
 import { List } from "./List";
@@ -18,6 +18,7 @@ import { listSpec } from "./Schema";
 import { ComputedField } from "./ScriptField";
 import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
 import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
+import { LinkManager } from "../client/util/LinkManager";
 
 export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
@@ -132,7 +133,6 @@ function fetchProto(doc: Doc) {
 @scriptingGlobal
 @Deserializable("Doc", fetchProto).withFields(["id"])
 export class Doc extends RefField {
-    @computed public static get selectedTool(): InkTool { return FieldValue(NumCast(Doc.UserDoc().inkTool)) ?? InkTool.None; }
     constructor(id?: FieldId, forceSave?: boolean) {
         super(id);
         const doc = new Proxy<this>(this, {
@@ -486,6 +486,78 @@ export namespace Doc {
         return alias;
     }
 
+
+
+    export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc {
+        if (Doc.IsBaseProto(doc)) return doc;
+        if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
+        const copy = new Doc(undefined, true);
+        cloneMap.set(doc[Id], copy);
+        if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
+        const exclude = Cast(doc.excludeFields, listSpec("string"), []);
+        Object.keys(doc).forEach(key => {
+            if (exclude.includes(key)) return;
+            const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+            const field = ProxyField.WithoutProxy(() => doc[key]);
+            const copyObjectField = (field: ObjectField) => {
+                const list = Cast(doc[key], listSpec(Doc));
+                if (list !== undefined && !(list instanceof Promise)) {
+                    copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.makeClone(d as Doc, cloneMap, rtfs)));
+                } else if (doc[key] instanceof Doc) {
+                    copy[key] = key.includes("layout[") ? undefined : Doc.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields 
+                } else {
+                    copy[key] = ObjectField.MakeCopy(field);
+                    if (field instanceof RichTextField) {
+                        if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) {
+                            rtfs.push({ copy, key, field });
+                        }
+                    }
+                }
+            };
+            if (key === "proto") {
+                if (doc[key] instanceof Doc) {
+                    copy[key] = Doc.makeClone(doc[key]!, cloneMap, rtfs);
+                }
+            } else {
+                if (field instanceof RefField) {
+                    copy[key] = field;
+                } else if (cfield instanceof ComputedField) {
+                    copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
+                    (key === "links" && field instanceof ObjectField) && copyObjectField(field);
+                } else if (field instanceof ObjectField) {
+                    copyObjectField(field);
+                } else if (field instanceof Promise) {
+                    debugger; //This shouldn't happend...
+                } else {
+                    copy[key] = field;
+                }
+            }
+        });
+        Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
+        copy.cloneOf = doc;
+        cloneMap.set(doc[Id], copy);
+        return copy;
+    }
+    export function MakeClone(doc: Doc): Doc {
+        const cloneMap = new Map<string, Doc>();
+        const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
+        const copy = Doc.makeClone(doc, cloneMap, rtfMap);
+        rtfMap.map(({ copy, key, field }) => {
+            const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
+                const mapped = cloneMap.get(id);
+                return attr + "\"" + (mapped ? mapped[Id] : id) + "\"";
+            };
+            const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => {
+                const mapped = cloneMap.get(id);
+                return href + (mapped ? mapped[Id] : id);
+            };
+            const regex = `(${Utils.prepend("/doc/")})([^"]*)`;
+            const re = new RegExp(regex, "g");
+            copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
+        });
+        return copy;
+    }
+
     //
     // Determines whether the layout needs to be expanded (as a template).
     // template expansion is rquired when the layout is a template doc/field and there's a datadoc which isn't equal to the layout template
@@ -743,6 +815,9 @@ export namespace Doc {
     export function SearchQuery(): string { return manager._searchQuery; }
     export function SetSearchQuery(query: string) { runInAction(() => manager._searchQuery = query); }
     export function UserDoc(): Doc { return manager._user_doc; }
+
+    export function SetSelectedTool(tool: InkTool) { Doc.UserDoc().activeInkTool = tool; }
+    export function GetSelectedTool(): InkTool { return (FieldValue(StrCast(Doc.UserDoc().activeInkTool)) ?? InkTool.None) as InkTool; }
     export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
     export function IsBrushed(doc: Doc) {
         return computedFn(function IsBrushed(doc: Doc) {
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index bb93de5ac..51a5768bf 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -4,11 +4,11 @@ import { ObjectField } from "./ObjectField";
 import { Copy, ToScriptString, ToString } from "./FieldSymbols";
 
 export enum InkTool {
-    None,
-    Pen,
-    Highlighter,
-    Eraser,
-    Stamp
+    None = "none",
+    Pen = "pen",
+    Highlighter = "highlighter",
+    Eraser = "eraser",
+    Stamp = "stamp"
 }
 
 export interface PointData {
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 6c2e797d6..da14ffc88 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -12,7 +12,6 @@ import { Scripting } from '../client/util/Scripting';
 import { Transform } from '../client/util/Transform';
 import { DocumentDecorations } from '../client/views/DocumentDecorations';
 import GestureOverlay from '../client/views/GestureOverlay';
-import { InkingControl } from '../client/views/InkingControl';
 import { DocumentView } from '../client/views/nodes/DocumentView';
 import { RadialMenu } from '../client/views/nodes/RadialMenu';
 import { PreviewCursor } from '../client/views/PreviewCursor';
@@ -26,6 +25,7 @@ import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
 import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero } from '../Utils';
 import "./MobileInterface.scss";
 import { CollectionView } from '../client/views/collections/CollectionView';
+import { InkingStroke } from '../client/views/InkingStroke';
 
 library.add(faLongArrowAltLeft);
 
@@ -68,7 +68,7 @@ export default class MobileInterface extends React.Component {
     }
 
     onSwitchInking = () => {
-        InkingControl.Instance.switchTool(InkTool.Pen);
+        Doc.SetSelectedTool(InkTool.Pen);
         MobileInterface.Instance.drawingInk = true;
 
         DocServer.Mobile.dispatchOverlayTrigger({
@@ -134,7 +134,7 @@ export default class MobileInterface extends React.Component {
 
     onBack = (e: React.MouseEvent) => {
         this.switchCurrentView((userDoc: Doc) => this.mainDoc);
-        InkingControl.Instance.switchTool(InkTool.None); // TODO: switch to previous tool
+        Doc.SetSelectedTool(InkTool.None); // TODO: switch to previous tool
 
         DocServer.Mobile.dispatchOverlayTrigger({
             enableOverlay: false,
@@ -187,6 +187,7 @@ export default class MobileInterface extends React.Component {
                         Document={this.mainContainer}
                         DataDoc={undefined}
                         LibraryPath={emptyPath}
+                        filterAddDocument={returnTrue}
                         fieldKey={""}
                         dropAction={"alias"}
                         bringToFront={emptyFunction}
-- 
cgit v1.2.3-70-g09d2


From 34d43b0d7ff1829514566e2b6b8514f6176e7c60 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 9 Jun 2020 11:48:22 -0400
Subject: fixed keyFrame animation positioning issues when adding a doc at t !=
 0. added isInkMask field and inkStroke menu option to create an ink mask. 
 added cantLeaveCollection flag.

---
 src/client/views/InkingStroke.tsx                   | 21 +++++++++++++++------
 src/client/views/collections/CollectionSubView.tsx  |  2 +-
 src/client/views/collections/CollectionView.tsx     |  3 ++-
 .../collectionFreeForm/CollectionFreeFormView.tsx   |  2 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx  | 17 ++++++++---------
 src/fields/documentSchemas.ts                       |  2 ++
 6 files changed, 29 insertions(+), 18 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index b545ede54..7dac2e3b5 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -31,6 +31,14 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
     }
 
+    private makeMask = () => {
+        this.props.Document._backgroundColor = "rgba(0,0,0,0.7)";
+        this.props.Document.mixBlendMode = "hard-light";
+        this.props.Document.color = "#9b9b9bff";
+        this.props.Document.cantLeaveCollection = true;
+        this.props.Document.isInkMask = true;
+    }
+
     render() {
         TraceMobx();
         const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
@@ -52,13 +60,14 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             <svg className="inkingStroke"
                 width={width}
                 height={height}
-                style={{ mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset" }}
+                style={{
+                    pointerEvents: this.props.Document.isInkMask ? "all" : undefined,
+                    transform: this.props.Document.isInkMask ? "translate(2500px, 2500px)" : undefined,
+                    mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset"
+                }}
                 onContextMenu={() => {
-                    ContextMenu.Instance.addItem({
-                        description: "Analyze Stroke",
-                        event: this.analyzeStrokes,
-                        icon: "paint-brush"
-                    });
+                    ContextMenu.Instance.addItem({ description: "Analyze Stroke", event: this.analyzeStrokes, icon: "paint-brush" });
+                    ContextMenu.Instance.addItem({ description: "Make Mask", event: this.makeMask, icon: "paint-brush" });
                 }}
             >
                 {points}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 167bce131..47d571bcc 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -221,7 +221,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                 } else {
                     added = this.addDocument(docDragData.droppedDocuments);
                 }
-                e.stopPropagation();
+                added && e.stopPropagation();
                 return added;
             }
             else if (de.complete.annoDragData) {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 3330abbc4..7f2b5ff8e 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -163,7 +163,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
         if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
             return true;
         }
-        return this.removeDocument(doc) ? addDocument(doc) : false;
+        const first = doc instanceof Doc ? doc : doc[0];
+        return !first?.cantLeaveCollection && this.removeDocument(doc) ? addDocument(doc) : false;
     }
 
     showIsTagged = () => {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 379156179..d512f815c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -202,7 +202,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         const x = (z ? xpo : xp) - docDragData.offset[0];
         const y = (z ? ypo : yp) - docDragData.offset[1];
         const zsorted = this.childLayoutPairs.map(pair => pair.layout).slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
-        zsorted.forEach((doc, index) => doc.zIndex = index + 1);
+        zsorted.forEach((doc, index) => doc.zIndex = doc.isInkMask ? 5000 : index + 1);
         const dropPos = [NumCast(docDragData.droppedDocuments[0].x), NumCast(docDragData.droppedDocuments[0].y)];
         for (let i = 0; i < docDragData.droppedDocuments.length; i++) {
             const d = docDragData.droppedDocuments[i];
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 910aa744d..77a2e04e6 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -36,7 +36,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         return min + rnd * (max - min);
     }
     get displayName() { return "CollectionFreeFormDocumentView(" + this.rootDoc.title + ")"; } // this makes mobx trace() statements more descriptive
-    get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${this.random(-1, 1) * this.props.jitterRotation}deg)`; }
+    get maskCentering() { return this.props.Document.isInkMask ? 2500 : 0; }
+    get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X - this.maskCentering}px, ${this.Y - this.maskCentering}px) rotate(${this.random(-1, 1) * this.props.jitterRotation}deg)`; }
     get X() { return this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
     get Y() { return this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
     get Opacity() { return this.dataProvider ? this.dataProvider.opacity : Cast(this.layoutDoc.opacity, "number", null); }
@@ -79,8 +80,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     public static setValues(time: number, d: Doc, x?: number, y?: number, opacity?: number) {
         const timecode = Math.round(time);
-        Cast(d["x-indexed"], listSpec("number"), [])[Math.max(0, timecode - 1)] = x as any as number;
-        Cast(d["y-indexed"], listSpec("number"), [])[Math.max(0, timecode - 1)] = y as any as number;
         Cast(d["x-indexed"], listSpec("number"), [])[timecode] = x as any as number;
         Cast(d["y-indexed"], listSpec("number"), [])[timecode] = y as any as number;
         Cast(d["opacity-indexed"], listSpec("number"), null)[timecode] = opacity as any as number;
@@ -110,12 +109,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             const xlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]);
             const ylist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]);
             const olist = new List<number>(numberRange(timecode + 1).map(t => progressivize && t < i ? 0 : 1));
-            xlist[Math.max(curTimecode - 1, 0)] = xlist[curTimecode] = NumCast(doc.x);
-            ylist[Math.max(curTimecode - 1, 0)] = ylist[curTimecode] = NumCast(doc.y);
+            xlist[curTimecode] = NumCast(doc.x);
+            ylist[curTimecode] = NumCast(doc.y);
             doc["x-indexed"] = xlist;
             doc["y-indexed"] = ylist;
             doc["opacity-indexed"] = olist;
-            doc.activeFrame = ComputedField.MakeFunction("self.context ? (self.context.currentFrame||0) : 0");
+            doc.activeFrame = ComputedField.MakeFunction("self.context?.currentFrame||0");
             doc.x = ComputedField.MakeInterpolated("x", "activeFrame");
             doc.y = ComputedField.MakeInterpolated("y", "activeFrame");
             doc.opacity = ComputedField.MakeInterpolated("opacity", "activeFrame");
@@ -151,12 +150,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 outline: this.Highlight ? "orange solid 2px" : "",
                 transform: this.transform,
                 transition: this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition),
-                width: this.width,
-                height: this.height,
+                width: this.props.Document.isInkMask ? 5000 : this.width,
+                height: this.props.Document.isInkMask ? 5000 : this.height,
                 zIndex: this.ZInd,
                 mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any,
                 display: this.ZInd === -99 ? "none" : undefined,
-                pointerEvents: this.props.Document.isBackground || this.Opacity === 0 ? "none" : this.props.pointerEvents ? "all" : undefined
+                pointerEvents: this.props.Document.isBackground || this.Opacity === 0 || this.props.Document.isInkMask ? "none" : this.props.pointerEvents ? "all" : undefined
             }} >
             {Doc.UserDoc().renderStyle !== "comic" ? (null) :
                 <div style={{ width: "100%", height: "100%", position: "absolute" }}>
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 6474ed148..353333028 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -58,6 +58,7 @@ export const documentSchema = createSchema({
     color: "string",            // foreground color of document
     fitToBox: "boolean",        // whether freeform view contents should be zoomed/panned to fill the area of the document view
     fontSize: "string",
+    isInkMask: "boolean",       // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
     layout: "string",           // this is the native layout string for the document.  templates can be added using other fields and setting layoutKey below
     layoutKey: "string",        // holds the field key for the field that actually holds the current lyoat
     letterSpacing: "string",
@@ -83,6 +84,7 @@ export const documentSchema = createSchema({
     _lockedTransform: "boolean",// whether a freeformview can pan/zoom
 
     // drag drop properties
+    cantLeaveCollection: "boolean",// whether document can be dropped into a different collection
     dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
     dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias", "copy", "move")
     targetDropAction: "string", // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move'
-- 
cgit v1.2.3-70-g09d2


From 747581ac65f706710becad034c5f5abff76b8e51 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 9 Jun 2020 17:26:05 -0400
Subject: fixed lots of errors/warnings.  added 'a' to be able embed documents
 on drop.  added 'l' toggle to perist lasso/marquee mode.

---
 src/client/apis/youtube/YoutubeBox.tsx             |  3 +-
 src/client/documents/Documents.ts                  |  2 +-
 src/client/util/DragManager.ts                     | 50 +++++++++++++++-------
 .../util/Import & Export/DirectoryImportBox.tsx    |  4 +-
 src/client/views/DocumentDecorations.tsx           |  2 +-
 src/client/views/GlobalKeyHandler.ts               |  1 +
 src/client/views/InkingStroke.tsx                  |  4 +-
 src/client/views/TemplateMenu.tsx                  |  4 +-
 src/client/views/animationtimeline/Keyframe.tsx    |  8 ++--
 src/client/views/animationtimeline/Timeline.tsx    |  2 +-
 .../views/animationtimeline/TimelineOverview.tsx   |  4 +-
 .../views/collections/CollectionPileView.tsx       |  7 +--
 .../views/collections/CollectionStackingView.tsx   |  3 +-
 .../CollectionStackingViewFieldColumn.tsx          |  2 +-
 src/client/views/collections/CollectionSubView.tsx |  4 +-
 .../views/collections/CollectionTimeView.tsx       |  3 +-
 src/client/views/collections/CollectionView.tsx    |  2 +-
 .../views/collections/CollectionViewChromes.tsx    |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 13 +++---
 .../collectionFreeForm/MarqueeView.scss            |  2 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  9 ++--
 src/client/views/nodes/ScreenshotBox.tsx           |  3 +-
 src/client/views/nodes/VideoBox.tsx                |  2 +-
 .../views/nodes/formattedText/DashDocView.tsx      |  4 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |  7 +--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  4 +-
 .../views/nodes/formattedText/RichTextSchema.tsx   |  4 +-
 src/client/views/webcam/DashWebRTCVideo.tsx        |  3 +-
 src/fields/Doc.ts                                  |  4 +-
 src/fields/RichTextUtils.ts                        |  6 +--
 src/server/ApiManagers/UploadManager.ts            |  3 +-
 31 files changed, 96 insertions(+), 75 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index 748d571c0..2a1f55710 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -10,6 +10,7 @@ import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
 import "../../views/nodes/WebBox.scss";
 import "./YoutubeBox.scss";
 import React = require("react");
+import { InkTool } from '../../../fields/InkField';
 
 interface VideoTemplate {
     thumbnailUrl: string;
@@ -349,7 +350,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
 
         const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
 
-        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.GetSelectedTool() && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+        const classname = "webBox-cont" + (this.props.isSelected() && Doc.GetSelectedTool() === InkTool.None && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
         return (
             <>
                 <div className={classname}  >
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 8d3e5a771..8d867348f 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -631,7 +631,7 @@ export namespace Docs {
             return doc;
         }
 
-        export function InkDocument(color: string, tool: number, strokeWidth: string, strokeBezier: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) {
+        export function InkDocument(color: string, tool: string, strokeWidth: string, strokeBezier: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) {
             const I = new Doc();
             I.type = DocumentType.INK;
             I.layout = InkingStroke.LayoutString("data");
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index e7a14f9c5..f1afaf734 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -67,6 +67,7 @@ export function SetupDrag(
 
 export namespace DragManager {
     let dragDiv: HTMLDivElement;
+    let dragLabel: HTMLDivElement;
     export let StartWindowDrag: Opt<((e: any, dragDocs: Doc[]) => void)> = undefined;
 
     export function Root() {
@@ -97,7 +98,8 @@ export namespace DragManager {
             readonly shiftKey: boolean,
             readonly altKey: boolean,
             readonly metaKey: boolean,
-            readonly ctrlKey: boolean
+            readonly ctrlKey: boolean,
+            readonly embedKey: boolean,
         ) { }
     }
 
@@ -309,14 +311,24 @@ export namespace DragManager {
         };
     }
     export let docsBeingDragged: Doc[] = [];
+    export let CanEmbed = false;
     export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
         eles = eles.filter(e => e);
+        CanEmbed = false;
         if (!dragDiv) {
             dragDiv = document.createElement("div");
             dragDiv.className = "dragManager-dragDiv";
             dragDiv.style.pointerEvents = "none";
+            dragLabel = document.createElement("div") as HTMLDivElement;
+            dragLabel.className = "dragManager-dragLabel";
+            dragLabel.style.zIndex = "100001";
+            dragLabel.style.fontSize = "10";
+            dragLabel.style.position = "absolute";
+            dragLabel.innerText = "press 'a' to embed on drop";
+            dragDiv.appendChild(dragLabel);
             DragManager.Root().appendChild(dragDiv);
         }
+        dragLabel.style.display = "";
         SnappingManager.SetIsDragging(true);
         const scaleXs: number[] = [];
         const scaleYs: number[] = [];
@@ -358,6 +370,7 @@ export namespace DragManager {
             dragElement.style.transform = `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0)}px) scale(${scaleX}, ${scaleY})`;
             dragElement.style.width = `${rect.width / scaleX}px`;
             dragElement.style.height = `${rect.height / scaleY}px`;
+            dragLabel.style.transform = `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0) - 20}px)`;
 
             if (docsBeingDragged.length) {
                 const pdfBox = dragElement.getElementsByTagName("canvas");
@@ -399,20 +412,21 @@ export namespace DragManager {
             if (dragData instanceof DocumentDragData) {
                 dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : undefined;
             }
-            if (e.shiftKey && dragData.droppedDocuments.length === 1) {
-                !dragData.dropAction && (dragData.dropAction = alias);
-                if (dragData.dropAction === "move") {
-                    dragData.removeDocument?.(dragData.draggedDocuments[0]);
+            if (e)
+                if (e.shiftKey && dragData.droppedDocuments.length === 1) {
+                    !dragData.dropAction && (dragData.dropAction = alias);
+                    if (dragData.dropAction === "move") {
+                        dragData.removeDocument?.(dragData.draggedDocuments[0]);
+                    }
+                    AbortDrag();
+                    finishDrag?.(new DragCompleteEvent(true, dragData));
+                    DragManager.StartWindowDrag?.({
+                        pageX: e.pageX,
+                        pageY: e.pageY,
+                        preventDefault: emptyFunction,
+                        button: 0
+                    }, dragData.droppedDocuments);
                 }
-                AbortDrag();
-                finishDrag?.(new DragCompleteEvent(true, dragData));
-                DragManager.StartWindowDrag?.({
-                    pageX: e.pageX,
-                    pageY: e.pageY,
-                    preventDefault: emptyFunction,
-                    button: 0
-                }, dragData.droppedDocuments);
-            }
 
             const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom);
 
@@ -421,12 +435,14 @@ export namespace DragManager {
             const moveY = thisY - lastY;
             lastX = thisX;
             lastY = thisY;
+            dragLabel.style.transform = `translate(${xs[0] + moveX + (options?.offsetX || 0)}px, ${ys[0] + moveY + (options?.offsetY || 0) - 20}px)`;
             dragElements.map((dragElement, i) => (dragElement.style.transform =
                 `translate(${(xs[i] += moveX) + (options?.offsetX || 0)}px, ${(ys[i] += moveY) + (options?.offsetY || 0)}px)  scale(${scaleXs[i]}, ${scaleYs[i]})`)
             );
         };
 
         const hideDragShowOriginalElements = () => {
+            dragLabel.style.display = "none";
             dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
             eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = false) : (ele.hidden = false));
         };
@@ -481,7 +497,8 @@ export namespace DragManager {
                         shiftKey: e.shiftKey,
                         altKey: e.altKey,
                         metaKey: e.metaKey,
-                        ctrlKey: e.ctrlKey
+                        ctrlKey: e.ctrlKey,
+                        embedKey: CanEmbed
                     }
                 })
             );
@@ -496,7 +513,8 @@ export namespace DragManager {
                         shiftKey: e.shiftKey,
                         altKey: e.altKey,
                         metaKey: e.metaKey,
-                        ctrlKey: e.ctrlKey
+                        ctrlKey: e.ctrlKey,
+                        embedKey: CanEmbed
                     }
                 })
             );
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 4e5cde558..25c556697 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -7,7 +7,7 @@ import Measure, { ContentRect } from "react-measure";
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faTag, faPlus, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
-import { Docs, DocumentOptions } from "../../documents/Documents";
+import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
 import { observer } from "mobx-react";
 import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry";
 import { Utils } from "../../../Utils";
@@ -123,7 +123,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             }
             const { accessPaths, exifData } = result;
             const path = Utils.prepend(accessPaths.agnostic.client);
-            const document = await Doc.Get.DocumentFromType(type, path, { _width: 300, title: name });
+            const document = await DocUtils.DocumentFromType(type, path, { _width: 300, title: name });
             const { data, error } = exifData;
             if (document) {
                 Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data });
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index e7a237e62..6ca7331d6 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -403,7 +403,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     onPointerUp = (e: PointerEvent): void => {
         SelectionManager.SelectedDocuments().map(dv => {
             if (NumCast(dv.layoutDoc._delayAutoHeight) < this._dragHeights.get(dv.layoutDoc)!) {
-                dv.nativeWidth > 0 && Doc.toggleNativeDimensions(dv.layoutDoc, dv.props.ContentScaling(), dv.panelWidth(), dv.panelHeight());
+                dv.nativeWidth > 0 && Doc.toggleNativeDimensions(dv.layoutDoc, dv.props.ContentScaling(), dv.props.PanelWidth(), dv.props.PanelHeight());
                 dv.layoutDoc._autoHeight = true;
             }
             dv.layoutDoc._delayAutoHeight = undefined;
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 2d5af5386..772cb07b1 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -72,6 +72,7 @@ export default class KeyManager {
 
     private unmodified = action((keyname: string, e: KeyboardEvent) => {
         switch (keyname) {
+            case "a": DragManager.CanEmbed = true;
             case " ":
                 MarqueeView.DragMarquee = !MarqueeView.DragMarquee;
                 break;
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 7dac2e3b5..7d73247af 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -92,5 +92,5 @@ Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) {
 Scripting.addGlobal(function activateEraser(pen: any) { return Doc.SetSelectedTool(pen ? InkTool.Eraser : InkTool.None); });
 Scripting.addGlobal(function activateStamp(pen: any) { return Doc.SetSelectedTool(pen ? InkTool.Stamp : InkTool.None); });
 Scripting.addGlobal(function deactivateInk() { return Doc.SetSelectedTool(InkTool.None); });
-Scripting.addGlobal(function setInkWidth(width: any) { return Doc.SetSelectedTool(width); });
-Scripting.addGlobal(function setInkColor(color: any) { return Doc.SetSelectedTool(color); });
\ No newline at end of file
+Scripting.addGlobal(function setInkWidth(width: any) { return SetActiveInkWidth(width); });
+Scripting.addGlobal(function setInkColor(color: any) { return SetActiveInkColor(color); });
\ No newline at end of file
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 77e6ebf44..f135823a8 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -7,7 +7,7 @@ import { DocumentView } from "./nodes/DocumentView";
 import { Template } from "./Templates";
 import React = require("react");
 import { Doc, DocListCast } from "../../fields/Doc";
-import { Docs, } from "../documents/Documents";
+import { Docs, DocUtils, } from "../documents/Documents";
 import { StrCast, Cast } from "../../fields/Types";
 import { CollectionTreeView } from "./collections/CollectionTreeView";
 import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZero } from "../../Utils";
@@ -176,5 +176,5 @@ Scripting.addGlobal(function switchView(doc: Doc, template: Doc | undefined) {
         template = Cast(template.dragFactory, Doc, null);
     }
     const templateTitle = StrCast(template?.title);
-    return templateTitle && Doc.makeCustomViewClicked(doc, Docs.Create.FreeformDocument, templateTitle, template);
+    return templateTitle && DocUtils.makeCustomViewClicked(doc, Docs.Create.FreeformDocument, templateTitle, template);
 });
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
index b562bd957..3a7182a94 100644
--- a/src/client/views/animationtimeline/Keyframe.tsx
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -180,10 +180,10 @@ export class Keyframe extends React.Component<IProps> {
             const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade);
             const fadeOut = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade);
             const finish = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.end);
-            (fadeIn as Doc).opacity = 1;
-            (fadeOut as Doc).opacity = 1;
-            (start as Doc).opacity = 0.1;
-            (finish as Doc).opacity = 0.1;
+            fadeIn.opacity = 1;
+            fadeOut.opacity = 1;
+            start.opacity = 0.1;
+            finish.opacity = 0.1;
             this.forceUpdate(); //not needed, if setTimeout is gone...
         }, 1000);
     }
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index 43f15a33f..ab984f727 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -445,7 +445,7 @@ export class Timeline extends React.Component<FieldViewProps> {
 
     // @computed
     getCurrentTime = () => {
-        let current = KeyframeFunc.convertPixelTime(this._currentBarX, "mili", "time", this._tickSpacing, this._tickIncrement);
+        const current = KeyframeFunc.convertPixelTime(this._currentBarX, "mili", "time", this._tickSpacing, this._tickIncrement);
         return this.toReadTime(current > this._time ? this._time : current);
     }
 
diff --git a/src/client/views/animationtimeline/TimelineOverview.tsx b/src/client/views/animationtimeline/TimelineOverview.tsx
index 31e248823..81a5587e4 100644
--- a/src/client/views/animationtimeline/TimelineOverview.tsx
+++ b/src/client/views/animationtimeline/TimelineOverview.tsx
@@ -42,9 +42,9 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps>{
         this.setOverviewWidth();
 
         this._authoringReaction = reaction(
-            () => this.props.parent._isAuthoring,
+            () => this.props.isAuthoring,
             () => {
-                if (!this.props.parent._isAuthoring) {
+                if (!this.props.isAuthoring) {
                     runInAction(() => {
                         this.setOverviewWidth();
                     });
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index fc48e0327..22a3877ab 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -12,6 +12,7 @@ import { SelectionManager } from "../../util/SelectionManager";
 import { UndoManager, undoBatch } from "../../util/UndoManager";
 import { SnappingManager } from "../../util/SnappingManager";
 import { DragManager } from "../../util/DragManager";
+import { DocUtils } from "../../documents/Documents";
 
 @observer
 export class CollectionPileView extends CollectionSubView(doc => doc) {
@@ -45,12 +46,12 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
         if (this.layoutEngine() === 'starburst') {
             const defaultSize = 110;
             this.layoutDoc._overflow = undefined;
-            this.childDocs.forEach(d => Doc.iconify(d));
+            this.childDocs.forEach(d => DocUtils.iconify(d));
             this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2;
             this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2;
             this.layoutDoc._width = NumCast(this.layoutDoc._starburstPileWidth, defaultSize);
             this.layoutDoc._height = NumCast(this.layoutDoc._starburstPileHeight, defaultSize);
-            Doc.pileup(this.childDocs);
+            DocUtils.pileup(this.childDocs);
             this.layoutDoc._panX = 0;
             this.layoutDoc._panY = -10;
             this.props.Document._pileLayoutEngine = 'pass';
@@ -76,7 +77,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
     onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
         if (super.onInternalDrop(e, de)) {
             if (de.complete.docDragData) {
-                Doc.pileup(this.childDocs);
+                DocUtils.pileup(this.childDocs);
             }
         }
         return true;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index bd48d1727..0a1b03522 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -27,6 +27,7 @@ import { CollectionSubView } from "./CollectionSubView";
 import { CollectionViewType } from "./CollectionView";
 import { SnappingManager } from "../../util/SnappingManager";
 import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
+import { DocUtils } from "../../documents/Documents";
 const _global = (window /* browser */ || global /* node */) as any;
 
 type StackingDocument = makeInterface<[typeof collectionSchema, typeof documentSchema]>;
@@ -412,7 +413,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)
         if (value && this.sectionHeaders) {
             const schemaHdrField = new SchemaHeaderField(value);
             this.sectionHeaders.push(schemaHdrField);
-            Doc.addFieldEnumerations(undefined, this.pivotField, [{ title: value, _backgroundColor: schemaHdrField.color }]);
+            DocUtils.addFieldEnumerations(undefined, this.pivotField, [{ title: value, _backgroundColor: schemaHdrField.color }]);
             return true;
         }
         return false;
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index a269b21f5..bcd55f0fe 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -235,7 +235,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
         Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey =>
             docItems.push({
                 description: ":" + fieldKey, event: () => {
-                    const created = Docs.Get.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.parent.props.Document));
+                    const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.parent.props.Document));
                     if (created) {
                         if (this.props.parent.Document.isTemplateDoc) {
                             Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 47d571bcc..93d20c475 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -10,7 +10,7 @@ import { WebField } from "../../../fields/URLField";
 import { Cast, ScriptCast, NumCast } from "../../../fields/Types";
 import { GestureUtils } from "../../../pen-gestures/GestureUtils";
 import { Upload } from "../../../server/SharedMediaTypes";
-import { Utils } from "../../../Utils";
+import { Utils, returnFalse } from "../../../Utils";
 import { DocServer } from "../../DocServer";
 import { Networking } from "../../Network";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
@@ -217,7 +217,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                     const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
                     const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
                     const res = addedDocs.length ? this.addDocument(addedDocs) : true;
-                    added = movedDocs.length ? docDragData.moveDocument(movedDocs, this.props.Document, this.addDocument) : res;
+                    added = movedDocs.length ? docDragData.moveDocument(movedDocs, this.props.Document, de.embedKey || !this.props.isAnnotationOverlay ? this.addDocument : returnFalse) : res;
                 } else {
                     added = this.addDocument(docDragData.droppedDocuments);
                 }
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 15bc0bfd5..c2d682361 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -19,6 +19,7 @@ const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
 import React = require("react");
+import { DocUtils } from "../../documents/Documents";
 
 @observer
 export class CollectionTimeView extends CollectionSubView(doc => doc) {
@@ -28,7 +29,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
     @observable _childClickedScript: Opt<ScriptField>;
     @observable _viewDefDivClick: Opt<ScriptField>;
     async componentDidMount() {
-        const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || Doc.findTemplate("detailView", StrCast(this.props.Document.type), "");
+        const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.props.Document.type), "");
         const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
         runInAction(() => {
             this._childClickedScript = ScriptField.MakeScript(childText, { this: Doc.name, shiftKey: "boolean" }, { detailView: detailView! });
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index c3fc3a40a..7e5f427ba 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -164,7 +164,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
             return true;
         }
         const first = doc instanceof Doc ? doc : doc[0];
-        return !first?.cantLeaveCollection && this.removeDocument(doc) ? addDocument(doc) : false;
+        return !first?.cantLeaveCollection && addDocument !== returnFalse && this.removeDocument(doc) ? addDocument(doc) : false;
     }
 
     showIsTagged = () => {
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 29a3e559a..3dc740c25 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -93,7 +93,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
                 this.props.collapse?.(true);
                 break;
         }
-    })
+    });
 
     @undoBatch
     viewChanged = (e: React.ChangeEvent) => {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d512f815c..aa4fc48f3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -177,7 +177,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             }
         }
         return retVal;
-    })
+    });
 
     private selectDocuments = (docs: Doc[]) => {
         SelectionManager.DeselectAll();
@@ -251,11 +251,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
         // if (this.props.Document.isBackground) return false;
         const [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
-        if (this.isAnnotationOverlay !== true && de.complete.linkDragData)
+        if (this.isAnnotationOverlay !== true && de.complete.linkDragData) {
             return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp);
-        if (de.complete.annoDragData?.dropDocument && super.onInternalDrop(e, de))
+        } else if (de.complete.annoDragData?.dropDocument && super.onInternalDrop(e, de)) {
             return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp);
-        if (de.complete.docDragData?.droppedDocuments.length && this.internalDocDrop(e, de, de.complete.docDragData, xp, yp)) {
+        } else if (de.complete.docDragData?.droppedDocuments.length && this.internalDocDrop(e, de, de.complete.docDragData, xp, yp)) {
             return true;
         } else {
             UndoManager.Undo();
@@ -530,9 +530,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                         }
                     });
 
-                    console.log(this._wordPalette)
                     CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => {
-                        console.log(results);
                         const wordResults = results.filter((r: any) => r.category === "inkWord");
                         for (const word of wordResults) {
                             const indices: number[] = word.strokeIds;
@@ -617,8 +615,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             return;
         }
         if (!e.cancelBubble) {
-            const selectedTool = Doc.GetSelectedTool();
-            if (selectedTool === InkTool.None) {
+            if (Doc.GetSelectedTool() === InkTool.None) {
                 if (this._hitCluster && this.tryDragCluster(e)) {
                     e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
                     e.preventDefault();
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index a811dd15a..45b83c3e3 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -28,6 +28,6 @@
         white-space:nowrap;
     }
     .marquee-legend::after {
-        content: "Press: c (collection), s (summary), or Delete"
+        content: "Press 'l' for lasso"
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 73dd41a15..7cf526ab8 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -62,7 +62,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         }
         this._pointsX = [];
         this._pointsY = [];
-        this._freeHand = false;
     }
 
     @undoBatch
@@ -352,7 +351,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         const selected = this.marqueeSelect(false);
         SelectionManager.DeselectAll();
         selected.forEach(d => this.props.removeDocument(d));
-        const newCollection = Doc.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2);
+        const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2);
         this.props.addDocument(newCollection!);
         this.props.selectDocuments([newCollection!], []);
         MarqueeOptionsMenu.Instance.fadeOut(true);
@@ -527,7 +526,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             }
             this.cleanupInteractions(false);
         }
-        if (e.key === "r") {
+        if (e.key === "r" || e.key === "l") {
             this._commandExecuted = true;
             e.stopPropagation();
             e.preventDefault();
@@ -537,7 +536,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @action
     changeFreeHand = (x: boolean) => {
-        this._freeHand = x;
+        this._freeHand = !this._freeHand;
     }
     // @action
     // marqueeInkSelect(ink: Map<any, any>) {
@@ -697,7 +696,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 width: `${Math.abs(v[0])}`,
                 height: `${Math.abs(v[1])}`, zIndex: 2000
             }} >
-                {/* <span className="marquee-legend" /> */}
+                <span className="marquee-legend"></span>
             </div>;
 
         } else {
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index d75b864cf..f7dee0896 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -18,6 +18,7 @@ import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxBaseComponent } from "../DocComponent";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ScreenshotBox.scss";
+import { InkTool } from "../../../fields/InkField";
 const path = require('path');
 
 type ScreenshotDocument = makeInterface<[typeof documentSchema]>;
@@ -132,7 +133,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh
     }
 
     @computed get content() {
-        const interactive = Doc.GetSelectedTool() || !this.props.isSelected() ? "" : "-interactive";
+        const interactive = Doc.GetSelectedTool() !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
         const style = "videoBox-content" + interactive;
         return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
             style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 1e0a859d3..e4dbceca6 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -228,7 +228,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
 
     @computed get content() {
         const field = Cast(this.dataDoc[this.fieldKey], VideoField);
-        const interactive = Doc.GetSelectedTool() || !this.props.isSelected() ? "" : "-interactive";
+        const interactive = Doc.GetSelectedTool() !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
         const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div>Loading</div> :
             <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 05e6a5959..55b3f6f1e 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -7,7 +7,7 @@ import { ComputedField } from "../../../../fields/ScriptField";
 import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types";
 import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero } from "../../../../Utils";
 import { DocServer } from "../../../DocServer";
-import { Docs } from "../../../documents/Documents";
+import { Docs, DocUtils } from "../../../documents/Documents";
 import { DocumentView } from "../DocumentView";
 import { FormattedTextBox } from "./FormattedTextBox";
 import { Transform } from "../../../util/Transform";
@@ -48,7 +48,7 @@ export class DashDocView extends React.Component<IDashDocView> {
                     if (dashDocBase instanceof Doc) {
                         const aliasedDoc = Doc.MakeAlias(dashDocBase, docid + alias);
                         aliasedDoc.layoutKey = "layout";
-                        node.attrs.fieldKey && Doc.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
+                        node.attrs.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
                         this._dashDoc = aliasedDoc;
                         //                        self.doRender(aliasedDoc, removeDoc, node, view, getPos);
                     }
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index d05e8f1ea..9a1b909c1 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -12,6 +12,7 @@ import React = require("react");
 import * as ReactDOM from 'react-dom';
 import "./DashFieldView.scss";
 import { observer } from "mobx-react";
+import { DocUtils } from "../../../documents/Documents";
 
 
 export class DashFieldView {
@@ -117,7 +118,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
     @action
     fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => {
         if (e.key === "Enter") {  // handle the enter key by "submitting" the current text to Dash's database. 
-            e.ctrlKey && Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: span.textContent! }]);
+            e.ctrlKey && DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: span.textContent! }]);
             this.updateText(span.textContent!, true);
             e.preventDefault();// prevent default to avoid a newline from being generated and wiping out this field view
         }
@@ -147,7 +148,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
                 (options instanceof Doc) && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title)));
                 if (modText) {
                     //  elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText;
-                    Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, []);
+                    DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, []);
                     this._dashDoc![this._fieldKey] = modText;
                 } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key
                 else if (nodeText.startsWith(":=")) {
@@ -167,7 +168,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
     // display a collection of all the enumerable values for this field
     onPointerDownEnumerables = async (e: any) => {
         e.stopPropagation();
-        const collview = await Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: this._fieldKey }]);
+        const collview = await DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: this._fieldKey }]);
         collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "onRight");
     }
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index e55588f8a..1fab54d7e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1211,7 +1211,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         TraceMobx();
         const scale = this.props.ContentScaling() * NumCast(this.layoutDoc.scale, 1);
         const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
-        const interactive = Doc.GetSelectedTool() || this.layoutDoc.isBackground;
+        const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground;
         if (this.props.isSelected()) {
             this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props);
         } else if (FormattedTextBoxComment.textBox === this) {
@@ -1232,7 +1232,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                         background: Doc.UserDoc().renderStyle === "comic" ? "transparent" : this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""),
                         opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
                         color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"),
-                        pointerEvents: interactive ? "none" : undefined,
+                        pointerEvents: interactive ? undefined : "none",
                         fontSize: Cast(this.layoutDoc._fontSize, "number", null),
                         fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit")
                     }}
diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx
index 91280dea4..1cc7ec8bf 100644
--- a/src/client/views/nodes/formattedText/RichTextSchema.tsx
+++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx
@@ -18,7 +18,7 @@ import { ComputedField } from "../../../../fields/ScriptField";
 import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../../fields/Types";
 import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils";
 import { DocServer } from "../../../DocServer";
-import { Docs } from "../../../documents/Documents";
+import { Docs, DocUtils } from "../../../documents/Documents";
 import { CollectionViewType } from "../../collections/CollectionView";
 import { DocumentView } from "../DocumentView";
 import { FormattedTextBox } from "./FormattedTextBox";
@@ -245,7 +245,7 @@ export class DashDocView {
                     if (dashDocBase instanceof Doc) {
                         const aliasedDoc = Doc.MakeAlias(dashDocBase, docid + alias);
                         aliasedDoc.layoutKey = "layout";
-                        node.attrs.fieldKey && Doc.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
+                        node.attrs.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
                         self.doRender(aliasedDoc, removeDoc, node, view, getPos);
                     }
                 });
diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx
index 83de9cc15..647e1ce6f 100644
--- a/src/client/views/webcam/DashWebRTCVideo.tsx
+++ b/src/client/views/webcam/DashWebRTCVideo.tsx
@@ -11,6 +11,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
 import { faSync, faPhoneSlash } from "@fortawesome/free-solid-svg-icons";
 import { Doc } from "../../../fields/Doc";
+import { InkTool } from "../../../fields/InkField";
 
 library.add(faSync);
 library.add(faPhoneSlash);
@@ -72,7 +73,7 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV
             </div >;
 
         const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
-        const classname = "webBox-cont" + (this.props.isSelected() && !Doc.GetSelectedTool() && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+        const classname = "webBox-cont" + (this.props.isSelected() && Doc.GetSelectedTool() === InkTool.None && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
 
         return (
             <>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 6f712f2ed..ffef9a384 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -690,7 +690,7 @@ export namespace Doc {
                 }
             }
         });
-        copy["author"] = Doc.CurrentUserEmail;
+        copy.author = Doc.CurrentUserEmail;
         return copy;
     }
 
@@ -822,7 +822,7 @@ export namespace Doc {
     export function UserDoc(): Doc { return manager._user_doc; }
 
     export function SetSelectedTool(tool: InkTool) { Doc.UserDoc().activeInkTool = tool; }
-    export function GetSelectedTool(): InkTool { return (FieldValue(StrCast(Doc.UserDoc().activeInkTool)) ?? InkTool.None) as InkTool; }
+    export function GetSelectedTool(): InkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkTool; }
     export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
     export function IsBrushed(doc: Doc) {
         return computedFn(function IsBrushed(doc: Doc) {
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index c475d0d73..f81ec8c6d 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -3,7 +3,7 @@ import { docs_v1 } from "googleapis";
 import { Fragment, Mark, Node } from "prosemirror-model";
 import { sinkListItem } from "prosemirror-schema-list";
 import { Utils } from "../Utils";
-import { Docs } from "../client/documents/Documents";
+import { Docs, DocUtils } from "../client/documents/Documents";
 import { schema } from "../client/views/nodes/formattedText/schema_rts";
 import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
 import { DocServer } from "../client/DocServer";
@@ -272,7 +272,7 @@ export namespace RichTextUtils {
             const backingDocId = StrCast(textNote[guid]);
             if (!backingDocId) {
                 const backingDoc = Docs.Create.ImageDocument(agnostic, { _width: 300, _height: 300 });
-                Doc.makeCustomViewClicked(backingDoc, Docs.Create.FreeformDocument);
+                DocUtils.makeCustomViewClicked(backingDoc, Docs.Create.FreeformDocument);
                 docid = backingDoc[Id];
                 textNote[guid] = docid;
             } else {
@@ -401,7 +401,7 @@ export namespace RichTextUtils {
                                     let exported = (await Cast(linkDoc.anchor2, Doc))!;
                                     if (!exported.customLayout) {
                                         exported = Doc.MakeAlias(exported);
-                                        Doc.makeCustomViewClicked(exported, Docs.Create.FreeformDocument);
+                                        DocUtils.makeCustomViewClicked(exported, Docs.Create.FreeformDocument);
                                         linkDoc.anchor2 = exported;
                                     }
                                     url = Utils.shareUrl(exported[Id]);
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 756bde738..fe39b84e6 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -301,8 +301,7 @@ async function captureYoutubeScreenshot(targetUrl: string): Promise<Opt<Buffer>>
     await videoPlayer?.click();
     await delay(1000);
     // hide youtube player controls.
-    await page.evaluate(() =>
-        (document.querySelector('.ytp-chrome-bottom') as any).style.display = 'none');
+    await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none');
 
     const buffer = await videoPlayer?.screenshot({ encoding: "binary" });
     await browser.close();
-- 
cgit v1.2.3-70-g09d2


From 1b65063a3cce0123215c914af6328a8a81ece33f Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 13 Jun 2020 20:53:31 -0400
Subject: fixed text sidebar when text is scaled.  started to add multiple
 links to a text selection

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 25 +++++++------
 .../collections/collectionFreeForm/MarqueeView.tsx |  2 +-
 .../nodes/formattedText/FormattedTextBox.scss      | 43 ++++++++++++++++++++++
 .../views/nodes/formattedText/FormattedTextBox.tsx | 18 ++++++---
 .../formattedText/FormattedTextBoxComment.tsx      | 19 +++++-----
 src/client/views/nodes/formattedText/marks_rts.ts  | 11 +++++-
 6 files changed, 90 insertions(+), 28 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 678ad2a53..67356bff7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -52,7 +52,6 @@ library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressAr
 export const panZoomSchema = createSchema({
     _panX: "number",
     _panY: "number",
-    scale: "number",
     currentTimecode: "number",
     displayTimecode: "number",
     currentFrame: "number",
@@ -76,6 +75,7 @@ export type collectionFreeformViewProps = {
     forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox)
     viewDefDivClick?: ScriptField;
     childPointerEvents?: boolean;
+    scaleField?: string;
 };
 
 @observer
@@ -108,6 +108,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     @computed get nativeWidth() { return this.fitToContent ? 0 : NumCast(this.Document._nativeWidth, this.props.NativeWidth()); }
     @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight, this.props.NativeHeight()); }
     private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
+    private get scaleFieldKey() { return this.props.scaleField || "scale"; }
     private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
     private easing = () => this.props.Document.panTransformType === "Ease";
     private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0;
@@ -115,14 +116,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     private zoomScaling = () => (this.fitToContentScaling / this.parentScaling) * (this.fitToContent ?
         Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y),
             this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
-        this.Document.scale || 1)
+        NumCast(this.Document[this.scaleFieldKey], 1));
 
     @computed get cachedCenteringShiftX(): number {
-        const scaling = this.fitToContent ? 1 : this.contentScaling;
+        const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling;
         return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0;  // shift so pan position is at center of window for non-overlay collections
     }
     @computed get cachedCenteringShiftY(): number {
-        const scaling = this.fitToContent ? 1 : this.contentScaling;
+        const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling;
         return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections
     }
     @computed get cachedGetLocalTransform(): Transform {
@@ -779,7 +780,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
         if (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling()) {
             const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
-            this.props.Document.scale = Math.abs(safeScale);
+            this.props.Document[this.scaleFieldKey] = Math.abs(safeScale);
             this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
         }
     }
@@ -795,7 +796,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             if (!e.ctrlKey && MarqueeView.DragMarquee) this.setPan(this.panX() + e.deltaX, this.panY() + e.deltaY, "None", true);
             else this.zoom(e.clientX, e.clientY, e.deltaY);
         }
-        this.props.Document.targetScale = NumCast(this.props.Document.scale);
+        this.props.Document.targetScale = NumCast(this.props.Document[this.scaleFieldKey]);
     }
 
     @action
@@ -852,7 +853,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     scaleAtPt(docpt: number[], scale: number) {
         const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
         this.Document.panTransformType = "Ease";
-        this.layoutDoc.scale = scale;
+        this.layoutDoc[this.scaleFieldKey] = scale;
         const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
         const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] };
         const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y);
@@ -896,7 +897,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
             HistoryUtil.pushState(newState);
 
-            const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType };
+            const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document.panTransformType };
 
             // if (!willZoom && DocumentView._focusHack.length) {
             //     Doc.BrushDoc(this.props.Document);
@@ -915,7 +916,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 if (afterFocus?.()) {
                     this.Document._panX = savedState.px;
                     this.Document._panY = savedState.py;
-                    this.Document.scale = savedState.s;
+                    this.Document[this.scaleFieldKey] = savedState.s;
                     this.Document.panTransformType = savedState.pt;
                 }
             }, 500);
@@ -924,7 +925,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     }
 
     setScaleToZoom = (doc: Doc, scale: number = 0.75) => {
-        this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height));
+        this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height));
     }
 
     @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; }
@@ -1125,7 +1126,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             }));
 
         if (this.props.isAnnotationOverlay) {
-            this.props.Document.scale = Math.max(1, NumCast(this.props.Document.scale));
+            this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey]));
         }
 
         return elements;
@@ -1206,7 +1207,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         const options = ContextMenu.Instance.findByDescription("Options...");
         const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
 
-        optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
+        optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" });
         optionItems.push({ description: "toggle snap line display", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" });
         optionItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
         optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 085625e69..5f09fa0ee 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -122,7 +122,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : "";
                 const tbox = Docs.Create.TextDocument("", {
                     _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: NumCast(Doc.UserDoc().fontSize),
-                    _fontFamily: StrCast(Doc.UserDoc().fontFamily), _backgroundColor: StrCast(Doc.UserDoc().backgroundColor),
+                    _fontFamily: StrCast(Doc.UserDoc().fontFamily),
                     title: "-typed text-"
                 });
                 const template = FormattedTextBox.DefaultLayout;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 49114d967..df4a05dd7 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -67,6 +67,7 @@
     display: inline-block;
     position: absolute;
     right: 0;
+    overflow: hidden;
 
     .collectionfreeformview-container {
         position: relative;
@@ -232,6 +233,48 @@ footnote::after {
     }
 }
 
+.prosemirror-anchor {
+    overflow:hidden;
+    display:inline-grid;
+}
+.prosemirror-linkBtn {
+    background:unset;
+    color:unset;
+    padding:0;
+    text-transform: unset;
+    letter-spacing: unset;
+    font-size:unset;
+}
+.prosemirror-links {
+    display: none;
+    position: absolute;
+    background-color: gray;
+    padding-bottom: 10px;
+    margin-top: 1em;
+    z-index: 1;
+  }
+  .prosemirror-hrefoptions{
+      width:0px;
+      border:unset;
+      padding:0px;
+
+  }
+  
+  .prosemirror-links a {
+    float: left;
+    color: white;
+    text-decoration: none;
+  }
+  
+  .prosemirror-links a:hover {
+    background-color: #eee;
+    color: black;
+  }
+  
+  .prosemirror-anchor:hover .prosemirror-links {
+    display: grid;
+  }
+
 .ProseMirror {
     touch-action: none;
     span {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 1fab54d7e..a8f5b7827 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -585,9 +585,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
 
     makeLinkToSelection(linkDocId: string, title: string, location: string, targetDocId: string) {
         if (this._editorView) {
-            const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId });
-            this._editorView.dispatch(this._editorView.state.tr.removeMark(this._editorView.state.selection.from, this._editorView.state.selection.to, this._editorView.state.schema.marks.link).
-                addMark(this._editorView.state.selection.from, this._editorView.state.selection.to, link));
+            const newRef = Utils.prepend("/doc/" + linkDocId);
+            const allHrefs = [{ href: newRef, title }];
+            let child: any = null;
+            const sel = this._editorView.state.selection;
+            this._editorView.state.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
+            if (child) {
+                allHrefs.push(...(child.marks.find((m: Mark) => m.type.name === schema.marks.link.name)?.attrs.allHrefs ?? []));
+            }
+            const link = this._editorView.state.schema.marks.link.create({ href: newRef, allHrefs, title, location, linkId: linkDocId, targetId: targetDocId });
+            this._editorView.dispatch(this._editorView.state.tr.addMark(this._editorView.state.selection.from, this._editorView.state.selection.to, link));
         }
     }
     componentDidMount() {
@@ -987,7 +994,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         FormattedTextBox._downEvent = false;
         if (!(e.nativeEvent as any).formattedHandled) {
             FormattedTextBoxComment.textBox = this;
-            FormattedTextBoxComment.update(this._editorView!);
+            FormattedTextBoxComment.update(this._editorView!, undefined, (e.target as any)?.className === "prosemirror-dropdownlink" ? (e.target as any).href : "");
         }
         (e.nativeEvent as any).formattedHandled = true;
 
@@ -1205,7 +1212,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
 
     @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
     sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth();
-    sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()), 0);
+    sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()) / this.props.ContentScaling(), 0);
     @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); }
     render() {
         TraceMobx();
@@ -1273,6 +1280,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                                 PanelWidth={this.sidebarWidth}
                                 NativeHeight={returnZero}
                                 NativeWidth={returnZero}
+                                scaleField={this.annotationKey + "-scale"}
                                 annotationsKey={this.annotationKey}
                                 isAnnotationOverlay={false}
                                 focus={this.props.focus}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 59a6045ab..dde7cc0c1 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -115,7 +115,7 @@ export class FormattedTextBoxComment {
         FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "");
     }
 
-    static update(view: EditorView, lastState?: EditorState) {
+    static update(view: EditorView, lastState?: EditorState, forceUrl: string = "") {
         const state = view.state;
         // Don't do anything if the document/selection didn't change
         if (lastState && lastState.doc.eq(state.doc) &&
@@ -160,25 +160,26 @@ export class FormattedTextBoxComment {
             let child: any = null;
             state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
             const mark = child && findLinkMark(child.marks);
-            if (mark && child && nbef && naft && mark.attrs.showPreview) {
-                FormattedTextBoxComment.tooltipText.textContent = "external => " + mark.attrs.href;
-                (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href;
-                if (mark.attrs.href.startsWith("https://en.wikipedia.org/wiki/")) {
-                    wiki().page(mark.attrs.href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(summary => FormattedTextBoxComment.tooltipText.textContent = summary.substring(0, 500)));
+            const href = mark?.attrs.href || forceUrl;
+            if (forceUrl || (href && child && nbef && naft && mark?.attrs.showPreview)) {
+                FormattedTextBoxComment.tooltipText.textContent = "external => " + href;
+                (FormattedTextBoxComment.tooltipText as any).href = href;
+                if (href.startsWith("https://en.wikipedia.org/wiki/")) {
+                    wiki().page(href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(summary => FormattedTextBoxComment.tooltipText.textContent = summary.substring(0, 500)));
                 } else {
                     FormattedTextBoxComment.tooltipText.style.whiteSpace = "pre";
                     FormattedTextBoxComment.tooltipText.style.overflow = "hidden";
                 }
-                if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) {
+                if (href.indexOf(Utils.prepend("/doc/")) === 0) {
                     FormattedTextBoxComment.tooltipText.textContent = "target not found...";
                     (FormattedTextBoxComment.tooltipText as any).href = "";
-                    const docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+                    const docTarget = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                     try {
                         ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText);
                     } catch (e) { }
                     docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => {
                         if (linkDoc instanceof Doc) {
-                            (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href;
+                            (FormattedTextBoxComment.tooltipText as any).href = href;
                             FormattedTextBoxComment.linkDoc = linkDoc;
                             const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc);
                             const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor;
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index ebaa23e99..b42fa6755 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -15,6 +15,7 @@ export const marks: { [index: string]: MarkSpec } = {
     link: {
         attrs: {
             href: {},
+            allHrefs: { default: [] as { href: string, title: string }[] },
             targetId: { default: "" },
             linkId: { default: "" },
             showPreview: { default: true },
@@ -31,7 +32,15 @@ export const marks: { [index: string]: MarkSpec } = {
         toDOM(node: any) {
             return node.attrs.docref && node.attrs.title ?
                 ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] :
-                ["a", { ...node.attrs, id: node.attrs.linkId + node.attrs.targetId, title: `${node.attrs.title}` }, 0];
+                node.attrs.allHrefs.length === 1 ?
+                    ["a", { ...node.attrs, id: node.attrs.linkId + node.attrs.targetId, title: `${node.attrs.title}` }, 0] :
+                    ["div", { class: "prosemirror-anchor" },
+                        ["button", { class: "prosemirror-linkBtn" },
+                            ["a", { ...node.attrs, id: node.attrs.linkId + node.attrs.targetId, title: `${node.attrs.title}` }, 0],
+                            ["input", { class: "fa fa-caret-down prosemirror-hrefoptions" }],
+                        ],
+                        ["div", { class: "prosemirror-links" }, ...node.attrs.allHrefs.map((item: { href: string, title: string }) => ["a", { class: "prosemirror-dropdownlink", href: item.href }, item.title])]
+                    ]
         }
     },
 
-- 
cgit v1.2.3-70-g09d2


From 9300029e95dcb8406cd05ed57c9d86107df49547 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sun, 14 Jun 2020 01:03:36 -0400
Subject: lint/compile fixes.  fixed entering/display of formulas in schema
 views.

---
 src/client/util/DragManager.ts                     | 29 ++++++++-------
 src/client/views/EditableView.tsx                  | 23 ++++++------
 src/client/views/GestureOverlay.tsx                |  4 +--
 .../views/collections/CollectionSchemaCells.tsx    | 42 ++++++++++++----------
 .../CollectionFreeFormLinkView.tsx                 |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 .../collectionGrid/CollectionGridView.tsx          |  2 +-
 .../views/nodes/ContentFittingDocumentView.tsx     | 14 ++++----
 src/client/views/nodes/LinkAnchorBox.tsx           |  2 +-
 src/client/views/nodes/formattedText/marks_rts.ts  |  2 +-
 10 files changed, 64 insertions(+), 58 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index f1afaf734..cb899ef92 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -319,7 +319,7 @@ export namespace DragManager {
             dragDiv = document.createElement("div");
             dragDiv.className = "dragManager-dragDiv";
             dragDiv.style.pointerEvents = "none";
-            dragLabel = document.createElement("div") as HTMLDivElement;
+            dragLabel = document.createElement("div");
             dragLabel.className = "dragManager-dragLabel";
             dragLabel.style.zIndex = "100001";
             dragLabel.style.fontSize = "10";
@@ -412,21 +412,20 @@ export namespace DragManager {
             if (dragData instanceof DocumentDragData) {
                 dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : undefined;
             }
-            if (e)
-                if (e.shiftKey && dragData.droppedDocuments.length === 1) {
-                    !dragData.dropAction && (dragData.dropAction = alias);
-                    if (dragData.dropAction === "move") {
-                        dragData.removeDocument?.(dragData.draggedDocuments[0]);
-                    }
-                    AbortDrag();
-                    finishDrag?.(new DragCompleteEvent(true, dragData));
-                    DragManager.StartWindowDrag?.({
-                        pageX: e.pageX,
-                        pageY: e.pageY,
-                        preventDefault: emptyFunction,
-                        button: 0
-                    }, dragData.droppedDocuments);
+            if (e?.shiftKey && dragData.droppedDocuments.length === 1) {
+                !dragData.dropAction && (dragData.dropAction = alias);
+                if (dragData.dropAction === "move") {
+                    dragData.removeDocument?.(dragData.draggedDocuments[0]);
                 }
+                AbortDrag();
+                finishDrag?.(new DragCompleteEvent(true, dragData));
+                DragManager.StartWindowDrag?.({
+                    pageX: e.pageX,
+                    pageY: e.pageY,
+                    preventDefault: emptyFunction,
+                    button: 0
+                }, dragData.droppedDocuments);
+            }
 
             const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom);
 
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index e0e205df9..e21d431b1 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -66,16 +66,17 @@ export class EditableView extends React.Component<EditableProps> {
         EditableView.loadId = "";
     }
 
-    @action
-    componentDidUpdate(nextProps: EditableProps) {
-        // this is done because when autosuggest is turned on, the suggestions are passed in as a prop,
-        // so when the suggestions are passed in, and no editing prop is passed in, it used to set it
-        // to false. this will no longer do so -syip
-        if (nextProps.editing && nextProps.editing !== this._editing) {
-            this._editing = nextProps.editing;
-            EditableView.loadId = "";
-        }
-    }
+    // @action
+    // componentDidUpdate(nextProps: EditableProps) {
+    //     // this is done because when autosuggest is turned on, the suggestions are passed in as a prop,
+    //     // so when the suggestions are passed in, and no editing prop is passed in, it used to set it
+    //     // to false. this will no longer do so -syip
+    //     console.log("props editing = " + nextProps.editing);
+    //     if (nextProps.editing && nextProps.editing !== this._editing) {
+    //         this._editing = nextProps.editing;
+    //         EditableView.loadId = "";
+    //     }
+    // }
 
     _didShow = false;
 
@@ -109,7 +110,7 @@ export class EditableView extends React.Component<EditableProps> {
         if (this._ref.current && this.props.showMenuOnLoad) {
             this.props.menuCallback?.(this._ref.current.getBoundingClientRect().x, this._ref.current.getBoundingClientRect().y);
         } else {
-            if (!this.props.onClick || !this.props.onClick(e)) {
+            if (!this.props.onClick?.(e)) {
                 this._editing = true;
                 this.props.isEditingCallback?.(true);
             }
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index d239a1d6f..f00fd7cb4 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -809,11 +809,11 @@ export default class GestureOverlay extends Touchable {
             [this._strokes.map(l => {
                 const b = this.getBounds(l);
                 return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                    {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape)}
+                    {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)}
                 </svg>;
             }),
             this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape)}
+                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)}
             </svg>]
         ];
     }
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 62aed67ed..baf9d4156 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -1,5 +1,5 @@
 import React = require("react");
-import { action, observable } from "mobx";
+import { action, observable, trace } from "mobx";
 import { observer } from "mobx-react";
 import { CellInfo } from "react-table";
 import "react-table/react-table.css";
@@ -23,6 +23,7 @@ import { faExpand } from '@fortawesome/free-solid-svg-icons';
 import { SchemaHeaderField } from "../../../fields/SchemaHeaderField";
 import { undoBatch } from "../../util/UndoManager";
 import { SnappingManager } from "../../util/SnappingManager";
+import { ComputedField } from "../../../fields/ScriptField";
 
 library.add(faExpand);
 
@@ -57,7 +58,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
 
     componentDidMount() {
         document.addEventListener("keydown", this.onKeyDown);
-
     }
 
     componentWillUnmount() {
@@ -70,7 +70,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             document.removeEventListener("keydown", this.onKeyDown);
             this._isEditing = true;
             this.props.setIsEditing(true);
-
         }
     }
 
@@ -217,7 +216,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
         //     <div className="collectionSchemaView-cellContents-docExpander" onPointerDown={this.expandDoc} >
         //         <FontAwesomeIcon icon="expand" size="sm" />
         //     </div>
-        // );
+        // );   
+        trace();
 
         return (
             <div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "grab" : "auto" }} ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}>
@@ -231,23 +231,29 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
                             height={"auto"}
                             maxHeight={Number(MAX_ROW_HEIGHT)}
                             GetValue={() => {
-                                const field = props.Document[props.fieldKey];
-                                if (Field.IsField(field)) {
-                                    return Field.toScriptString(field);
-                                }
-                                return "";
-                            }
-                            }
-                            SetValue={(value: string) => {
+                                const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey]));
+                                const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined;
+                                const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1];
+                                const val = cscript !== undefined ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` :
+                                    Field.IsField(cfield) ? Field.toScriptString(cfield) : "";
+                                return val;
+                            }}
+                            SetValue={action((value: string) => {
+                                let retVal = false;
                                 if (value.startsWith(":=")) {
-                                    return this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col);
+                                    retVal = this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col);
+                                } else {
+                                    const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+                                    if (script.compiled) {
+                                        retVal = this.applyToDoc(props.Document, this.props.row, this.props.col, script.run);
+                                    }
                                 }
-                                const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
-                                if (!script.compiled) {
-                                    return false;
+                                if (retVal) {
+                                    this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing'
+                                    this.props.setIsEditing(false);
                                 }
-                                return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run);
-                            }}
+                                return retVal;
+                            })}
                             OnFillDown={async (value: string) => {
                                 const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
                                 if (script.compiled) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 8ab00bb29..6cac39f77 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -52,7 +52,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
                 const linkId = this.props.LinkDocs[0][Id]; // this link's Id
                 const AanchorId = (this.props.LinkDocs[0][afield] as Doc)[Id]; // anchor a's id
                 const BanchorId = (this.props.LinkDocs[0][bfield] as Doc)[Id]; // anchor b's id
-                const linkEles = Array.from(window.document.getElementsByClassName(linkId))
+                const linkEles = Array.from(window.document.getElementsByClassName(linkId));
                 const targetAhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(AanchorId));
                 const targetBhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(BanchorId));
                 if (!targetBhyperlink) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 67356bff7..11a14a862 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -116,7 +116,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     private zoomScaling = () => (this.fitToContentScaling / this.parentScaling) * (this.fitToContent ?
         Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y),
             this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
-        NumCast(this.Document[this.scaleFieldKey], 1));
+        NumCast(this.Document[this.scaleFieldKey], 1))
 
     @computed get cachedCenteringShiftX(): number {
         const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling;
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index fe89b63ee..a5d355abc 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -162,7 +162,7 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
         if (this.flexGrid) {
             const savedLayouts = this.savedLayoutList;
             this.childLayoutPairs.forEach(({ layout: doc }) => {
-                let gridLayout = savedLayouts.find(gridLayout => gridLayout.i === doc[Id]);
+                const gridLayout = savedLayouts.find(gridLayout => gridLayout.i === doc[Id]);
                 gridLayout && Object.assign(gridLayout, layoutArray.find(layout => layout.i === doc[Id]) || gridLayout);
             });
 
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index d9e7d072f..ba075886b 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -1,18 +1,18 @@
 import React = require("react");
 import { computed } from "mobx";
 import { observer } from "mobx-react";
+import { Transform } from "nodemailer/lib/xoauth2";
 import "react-table/react-table.css";
-import { Doc, Opt, WidthSym, HeightSym } from "../../../fields/Doc";
-import { NumCast, StrCast, Cast } from "../../../fields/Types";
+import { Doc, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
+import { ScriptField } from "../../../fields/ScriptField";
+import { Cast, NumCast, StrCast } from "../../../fields/Types";
 import { TraceMobx } from "../../../fields/util";
-import { emptyFunction, returnOne } from "../../../Utils";
+import { emptyFunction } from "../../../Utils";
+import { dropActionType } from "../../util/DragManager";
+import { CollectionView } from "../collections/CollectionView";
 import '../DocumentDecorations.scss';
 import { DocumentView, DocumentViewProps } from "../nodes/DocumentView";
 import "./ContentFittingDocumentView.scss";
-import { dropActionType } from "../../util/DragManager";
-import { CollectionView } from "../collections/CollectionView";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { Transform } from "nodemailer/lib/xoauth2";
 
 interface ContentFittingDocumentViewProps {
     Document: Doc;
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 83245a89c..3c232eff8 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -117,7 +117,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
         const y = this.props.PanelWidth() > 1 ? NumCast(this.layoutDoc[this.fieldKey + "_y"], 100) : 0;
         const c = StrCast(this.layoutDoc.backgroundColor, "lightblue");
         const anchor = this.fieldKey === "anchor1" ? "anchor2" : "anchor1";
-        const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .15;
+        const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25;
 
         const timecode = this.dataDoc[anchor + "_timecode"];
         const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title) + (timecode !== undefined ? ":" + timecode : "");
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 9c279a88f..c735155d8 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -49,7 +49,7 @@ export const marks: { [index: string]: MarkSpec } = {
                         ["div", { class: "prosemirror-links" }, ...node.attrs.allHrefs.map((item: { href: string, title: string }) =>
                             ["a", { class: "prosemirror-dropdownlink", href: item.href }, item.title]
                         )]
-                    ]
+                    ];
         }
     },
 
-- 
cgit v1.2.3-70-g09d2


From 6c67e91ebef5db8d63f6a75f198e5a5ef30dc142 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 16 Jun 2020 01:50:07 -0400
Subject: fixed sizing of inkstrokes to bounding box

---
 src/client/util/InteractionUtils.tsx               | 33 ++++++++++------------
 src/client/views/GestureOverlay.tsx                |  5 ++--
 src/client/views/InkingStroke.tsx                  | 23 ++++++---------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  3 +-
 4 files changed, 29 insertions(+), 35 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 81f9b9362..8553661d4 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -89,42 +89,39 @@ export namespace InteractionUtils {
         return myTouches;
     }
 
-    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean) {
-        var pts = "";
-        if (shape) {
-            //if any of the shape are true
-            const shapePts = makePolygon(shape, points);
-            pts = shapePts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, "");
+    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number, strokeWidth: number, bezier: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean) {
+        let pts: { X: number; Y: number; }[] = [];
+        if (shape) { //if any of the shape are true
+            pts = makePolygon(shape, points);
         }
         else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) {
             //pointer is up (first and last points are the same)
-            const newPoints: number[][] = [];
-            const newPts: { X: number; Y: number; }[] = [];
-            //convert to [][] for fitcurve module
-            for (var i = 0; i < points.length - 1; i++) {
-                newPoints.push([points[i].X, points[i].Y]);
-            }
+            points.pop();
+            const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]);
+
             const bezierCurves = fitCurve(newPoints, parseInt(bezier));
             for (var i = 0; i < bezierCurves.length; i++) {
                 for (var t = 0; t < 1; t += 0.01) {
                     const point = beziercurve(t, bezierCurves[i]);
-                    newPts.push({ X: point[0], Y: point[1] });
+                    pts.push({ X: point[0], Y: point[1] });
                 }
             }
-            pts = newPts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, "");
         } else {
-            //in the middle of drawing
-            pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, "");
+            pts = points;
         }
+        const strpts = pts.reduce((acc: string, pt: { X: number, Y: number }) => acc +
+            `${(pt.X - left - width / 2) * scalex + width / 2},
+             ${(pt.Y - top - width / 2) * scaley + width / 2} `, "");
+
         return (
             <polyline
-                points={pts}
+                points={strpts}
                 style={{
                     filter: drawHalo ? "url(#dangerShine)" : undefined,
                     fill: "none",
                     pointerEvents: pevents as any,
                     stroke: color ?? "rgb(0, 0, 0)",
-                    strokeWidth: parseInt(width),
+                    strokeWidth: strokeWidth,
                     strokeLinejoin: "round",
                     strokeLinecap: "round"
                 }}
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index e8ed40b38..fd4495edb 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -803,17 +803,18 @@ export default class GestureOverlay extends Touchable {
 
     @computed get elements() {
         const B = this.svgBounds;
+        const width = Number(ActiveInkWidth());
         return [
             this.props.children,
             this._palette,
             [this._strokes.map((l, i) => {
                 const b = this.getBounds(l);
                 return <svg key={i} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                    {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)}
+                    {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)}
                 </svg>;
             }),
             this._points.length <= 1 ? (null) : <svg key="svg" width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
-                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)}
+                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)}
             </svg>]
         ];
     }
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index a7650163f..7e3bd1c17 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,6 +1,5 @@
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faPaintBrush } from "@fortawesome/free-solid-svg-icons";
-import { observable, runInAction, action } from "mobx";
 import { observer } from "mobx-react";
 import { documentSchema } from "../../fields/documentSchemas";
 import { InkData, InkField, InkTool } from "../../fields/InkField";
@@ -16,7 +15,6 @@ import { FieldView, FieldViewProps } from "./nodes/FieldView";
 import React = require("react");
 import { Scripting } from "../util/Scripting";
 import { Doc } from "../../fields/Doc";
-import { Id } from "../../fields/FieldSymbols";
 
 library.add(faPaintBrush);
 
@@ -43,25 +41,22 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
     render() {
         TraceMobx();
         const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
+        const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth()));
         const xs = data.map(p => p.X);
         const ys = data.map(p => p.Y);
-        const left = Math.min(...xs);
-        const top = Math.min(...ys);
-        const right = Math.max(...xs);
-        const bottom = Math.max(...ys);
+        const left = Math.min(...xs) - strokeWidth / 2;
+        const top = Math.min(...ys) - strokeWidth / 2;
+        const right = Math.max(...xs) + strokeWidth / 2;
+        const bottom = Math.max(...ys) + strokeWidth / 2;
         const width = right - left;
         const height = bottom - top;
-        const scaleX = this.props.PanelWidth() / width;
-        const scaleY = this.props.PanelHeight() / height;
-        const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth()));
+        const scaleX = (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth);
+        const scaleY = (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth);
         const strokeColor = StrCast(this.layoutDoc.color, ActiveInkColor());
-        const points = InteractionUtils.CreatePolyline(data, left, top,
-            strokeColor,
-            strokeWidth.toString(),
+        const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth,
             StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5);
         const hpoints = InteractionUtils.CreatePolyline(data, left, top,
-            this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent",
-            (strokeWidth + 15).toString(),
+            this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, (strokeWidth + 15),
             StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", this.props.active() ? "visiblestroke" : "none", false);
         return (
             <svg className="inkingStroke"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 11a14a862..736c5fd06 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -457,7 +457,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             case GestureUtils.Gestures.Stroke:
                 const points = ge.points;
                 const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
-                const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
+                const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), points,
+                    { title: "ink stroke", x: B.x - Number(ActiveInkWidth()) / 2, y: B.y - Number(ActiveInkWidth()) / 2, _width: B.width + Number(ActiveInkWidth()), _height: B.height + Number(ActiveInkWidth()) });
                 this.addDocument(inkDoc);
                 e.stopPropagation();
                 break;
-- 
cgit v1.2.3-70-g09d2


From 375786198596ce8dcab06c39c5b4ffcca4a78b88 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 17 Jun 2020 10:59:54 -0400
Subject: fixed move document to undo correctly when ACLs of target don't allow
 a drop.

---
 src/client/util/UndoManager.ts                     | 22 ++++++++++++++++++++--
 src/client/views/RecommendationsBox.tsx            |  2 +-
 .../views/collections/CollectionTreeView.tsx       |  2 +-
 src/client/views/collections/CollectionView.tsx    | 15 +++++++++++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 --
 src/client/views/nodes/DocumentView.tsx            |  3 ---
 6 files changed, 35 insertions(+), 11 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 314b52bf3..c7b7bb215 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -78,10 +78,12 @@ export namespace UndoManager {
     let currentBatch: UndoBatch | undefined;
     let batchCounter = 0;
     let undoing = false;
+    let tempEvents: UndoEvent[] | undefined = undefined;
 
     export function AddEvent(event: UndoEvent): void {
         if (currentBatch && batchCounter && !undoing) {
             currentBatch.push(event);
+            tempEvents?.push(event);
         }
     }
 
@@ -135,7 +137,7 @@ export namespace UndoManager {
 
     const EndBatch = action((cancel: boolean = false) => {
         batchCounter--;
-        if (batchCounter === 0 && currentBatch && currentBatch.length) {
+        if (batchCounter === 0 && currentBatch?.length) {
             if (!cancel) {
                 undoStack.push(currentBatch);
             }
@@ -144,6 +146,13 @@ export namespace UndoManager {
         }
     });
 
+    export function ClearTempBatch() {
+        tempEvents = undefined;
+    }
+    export function RunInTempBatch<T>(fn: () => T) {
+        tempEvents = [];
+        return runInAction(fn);
+    }
     //TODO Make this return the return value
     export function RunInBatch<T>(fn: () => T, batchName: string) {
         const batch = StartBatch(batchName);
@@ -153,7 +162,16 @@ export namespace UndoManager {
             batch.end();
         }
     }
-
+    export const UndoTempBatch = action(() => {
+        if (tempEvents) {
+            undoing = true;
+            for (let i = tempEvents.length - 1; i >= 0; i--) {
+                tempEvents[i].undo();
+            }
+            undoing = false;
+        }
+        tempEvents = undefined;
+    });
     export const Undo = action(() => {
         if (undoStack.length === 0) {
             return;
diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx
index 8ca81c070..c98363214 100644
--- a/src/client/views/RecommendationsBox.tsx
+++ b/src/client/views/RecommendationsBox.tsx
@@ -56,7 +56,7 @@ export class RecommendationsBox extends React.Component<FieldViewProps> {
         }
         const returnXDimension = () => 150;
         const returnYDimension = () => 150;
-        const scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension());
+        const scale = () => returnXDimension() / NumCast(renderDoc._nativeWidth, returnXDimension());
         //let scale = () => 1;
         const newRenderDoc = Doc.MakeAlias(renderDoc); ///   newRenderDoc -> renderDoc -> render"data"Doc -> TextProt
         newRenderDoc.height = NumCast(this.props.Document.documentIconHeight);
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index d631a492e..be0339345 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -250,7 +250,7 @@ class TreeView extends React.Component<TreeViewProps> {
             const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]());
             if (aspect) return this.docWidth() * aspect;
             if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
-            return layoutDoc._fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection._height) :
+            return layoutDoc._fitWidth ? (!this.props.document._nativeHeight ? NumCast(this.props.containingCollection._height) :
                 Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth,
                     NumCast(this.props.containingCollection._height)))) :
                 NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index e436a13ab..be79dbce1 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -45,6 +45,7 @@ import { CollectionTreeView } from "./CollectionTreeView";
 import { CollectionGridView } from './collectionGrid/CollectionGridView';
 import './CollectionView.scss';
 import { CollectionViewBaseChrome } from './CollectionViewChromes';
+import { UndoManager } from '../../util/UndoManager';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -166,7 +167,17 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
             return true;
         }
         const first = doc instanceof Doc ? doc : doc[0];
-        return !first?.stayInCollection && addDocument !== returnFalse && this.removeDocument(doc) ? addDocument(doc) : false;
+        if (!first?.stayInCollection && addDocument !== returnFalse) {
+            if (UndoManager.RunInTempBatch(() => this.removeDocument(doc))) {
+                const added = addDocument(doc);
+                if (!added) UndoManager.UndoTempBatch();
+                else UndoManager.ClearTempBatch();
+
+                return added;
+            }
+            UndoManager.ClearTempBatch();
+        }
+        return false;
     }
 
     showIsTagged = () => {
@@ -431,7 +442,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
     @computed get filterView() {
         const facetCollection = this.props.Document;
         const flyout = (
-            <div className="collectionTimeView-flyout" style={{ width: `${this.facetWidth()}`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}>
+            <div className="collectionTimeView-flyout" style={{ width: `${this.facetWidth()}`, height: this.props.PanelHeight() - 30 }} onWheel={e => fmovede.stopPropagation()}>
                 {this._allFacets.map(facet => <label className="collectionTimeView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}>
                     <input type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey + "-filter"]).some(d => d.title === facet)} />
                     <span className="checkmark" />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 736c5fd06..72cb2d218 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -258,8 +258,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp);
         } else if (de.complete.docDragData?.droppedDocuments.length && this.internalDocDrop(e, de, de.complete.docDragData, xp, yp)) {
             return true;
-        } else {
-            UndoManager.Undo();
         }
         return false;
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4752ddd9e..ffadc6edf 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -17,10 +17,8 @@ import { GestureUtils } from '../../../pen-gestures/GestureUtils';
 import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils";
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { ClientRecommender } from '../../ClientRecommender';
-import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from "../../documents/Documents";
 import { DocumentType } from '../../documents/DocumentTypes';
-import { ClientUtils } from '../../util/ClientUtils';
 import { DocumentManager } from "../../util/DocumentManager";
 import { SnappingManager } from '../../util/SnappingManager';
 import { DragManager, dropActionType } from "../../util/DragManager";
@@ -137,7 +135,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this.removeEndListeners();
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
-        console.log(SelectionManager.SelectedDocuments());
         if (RadialMenu.Instance._display === false) {
             this.addHoldMoveListeners();
             this.addHoldEndListeners();
-- 
cgit v1.2.3-70-g09d2


From 2e6a3e1aa3fb5b82587a9c84aba8bccef96963d3 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 17 Jun 2020 13:38:07 -0400
Subject: fixed docdecorations on collecction with side filter bar expanded. 
 cleaned up warnings/error messages a bit.

---
 src/client/util/DictationManager.ts                                  | 2 +-
 src/client/util/InteractionUtils.tsx                                 | 4 ++--
 src/client/views/collections/CollectionView.tsx                      | 5 +++--
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx  | 3 +--
 src/client/views/collections/collectionGrid/CollectionGridView.tsx   | 2 +-
 src/client/views/nodes/DocumentContentsView.tsx                      | 2 +-
 src/client/views/nodes/formattedText/SummaryView.tsx                 | 2 +-
 src/fields/Doc.ts                                                    | 2 +-
 8 files changed, 11 insertions(+), 11 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index e46225b4a..d8a5657c3 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -144,7 +144,7 @@ export namespace DictationManager {
             recognizer.start();
 
             return new Promise<string>((resolve, reject) => {
-                recognizer.onerror = (e: SpeechRecognitionError) => {
+                recognizer.onerror = (e: any) => { // e is SpeechRecognitionError but where is that defined? 
                     if (!(indefinite && e.error === "no-speech")) {
                         recognizer.stop();
                         reject(e);
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index aeb0f670d..df792c9c0 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -100,9 +100,9 @@ export namespace InteractionUtils {
             const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]);
 
             const bezierCurves = fitCurve(newPoints, parseInt(bezier));
-            for (var i = 0; i < bezierCurves.length; i++) {
+            for (const curve of bezierCurves) {
                 for (var t = 0; t < 1; t += 0.01) {
-                    const point = beziercurve(t, bezierCurves[i]);
+                    const point = beziercurve(t, curve);
                     pts.push({ X: point[0], Y: point[1] });
                 }
             }
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index be79dbce1..2f38fb4b4 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -191,8 +191,9 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
         // return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />;
     }
 
+    screenToLocalTransform = () => this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth());
     private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
-        const props: SubCollectionViewProps = { ...this.props, ...renderProps, CollectionView: this, annotationsKey: "" };
+        const props: SubCollectionViewProps = { ...this.props, ...renderProps, ScreenToLocalTransform: this.screenToLocalTransform, CollectionView: this, annotationsKey: "" };
         switch (type) {
             case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />);
             case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />);
@@ -442,7 +443,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
     @computed get filterView() {
         const facetCollection = this.props.Document;
         const flyout = (
-            <div className="collectionTimeView-flyout" style={{ width: `${this.facetWidth()}`, height: this.props.PanelHeight() - 30 }} onWheel={e => fmovede.stopPropagation()}>
+            <div className="collectionTimeView-flyout" style={{ width: `${this.facetWidth()}`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}>
                 {this._allFacets.map(facet => <label className="collectionTimeView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}>
                     <input type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey + "-filter"]).some(d => d.title === facet)} />
                     <span className="checkmark" />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 72cb2d218..571b5bf5c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -158,8 +158,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
         if (retVal) {
             const newBoxes = (newBox instanceof Doc) ? [newBox] : newBox;
-            for (let i = 0; i < newBoxes.length; i++) {
-                const newBox = newBoxes[i];
+            for (const newBox of newBoxes) {
                 if (newBox.activeFrame !== undefined) {
                     const x = newBox.x;
                     const y = newBox.y;
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index a5d355abc..2015ca930 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -163,7 +163,7 @@ export class CollectionGridView extends CollectionSubView(GridSchema) {
             const savedLayouts = this.savedLayoutList;
             this.childLayoutPairs.forEach(({ layout: doc }) => {
                 const gridLayout = savedLayouts.find(gridLayout => gridLayout.i === doc[Id]);
-                gridLayout && Object.assign(gridLayout, layoutArray.find(layout => layout.i === doc[Id]) || gridLayout);
+                if (gridLayout) Object.assign(gridLayout, layoutArray.find(layout => layout.i === doc[Id]) || gridLayout);
             });
 
             if (this.props.Document.gridStartCompaction) {
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 126e9ac14..f1438fd54 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -3,7 +3,7 @@ import { observer } from "mobx-react";
 import { Doc, Opt, Field, AclSym, AclPrivate } from "../../../fields/Doc";
 import { Cast, StrCast, NumCast } from "../../../fields/Types";
 import { OmitKeys, Without, emptyPath } from "../../../Utils";
-import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
+import { DirectoryImportBox } from "../../util/Import & Export/DirectoryImportBox";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { CollectionSchemaView } from "../collections/CollectionSchemaView";
diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx
index 922285bd0..c017db034 100644
--- a/src/client/views/nodes/formattedText/SummaryView.tsx
+++ b/src/client/views/nodes/formattedText/SummaryView.tsx
@@ -14,7 +14,7 @@ export class SummaryView {
         const self = this;
         this._fieldWrapper = document.createElement("span");
         this._fieldWrapper.className = this.className(node.attrs.visibility);
-        this._fieldWrapper.onpointerdown = function (e: any) { self.onPointerDown(e, node, view, getPos); }
+        this._fieldWrapper.onpointerdown = function (e: any) { self.onPointerDown(e, node, view, getPos); };
         this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); };
         this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); };
         this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); };
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 96587af44..8c8720179 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -134,7 +134,7 @@ export class Doc extends RefField {
             has: (target, key) => target[AclSym] !== AclPrivate && key in target.__fields,
             ownKeys: target => {
                 const obj = {} as any;
-                (target[AclSym] !== AclPrivate) && Object.assign(obj, target.___fields);
+                if (target[AclSym] !== AclPrivate) Object.assign(obj, target.___fields);
                 runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
                 return Object.keys(obj);
             },
-- 
cgit v1.2.3-70-g09d2


From 1a03c645e5801fe0cfe71e1b1744313a41523de6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 17 Jun 2020 20:07:44 -0400
Subject: made docFilters a prop on everything

---
 src/Utils.ts                                                     | 2 ++
 src/client/views/GestureOverlay.tsx                              | 3 ++-
 src/client/views/GlobalKeyHandler.ts                             | 4 ++--
 src/client/views/MainView.tsx                                    | 6 +++++-
 src/client/views/OverlayView.tsx                                 | 3 ++-
 src/client/views/Palette.tsx                                     | 3 ++-
 src/client/views/RecommendationsBox.tsx                          | 3 ++-
 src/client/views/TemplateMenu.tsx                                | 3 ++-
 src/client/views/collections/CollectionDockingView.tsx           | 4 ++--
 src/client/views/collections/CollectionLinearView.tsx            | 1 +
 src/client/views/collections/CollectionSchemaCells.tsx           | 3 ++-
 src/client/views/collections/CollectionSchemaView.tsx            | 1 +
 src/client/views/collections/CollectionStackingView.tsx          | 1 +
 src/client/views/collections/CollectionSubView.tsx               | 9 +++++++--
 src/client/views/collections/CollectionTreeView.tsx              | 6 ++++--
 src/client/views/collections/CollectionView.tsx                  | 3 ++-
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx    | 1 +
 .../collectionMulticolumn/CollectionMulticolumnView.tsx          | 1 +
 .../collections/collectionMulticolumn/CollectionMultirowView.tsx | 1 +
 src/client/views/nodes/DocHolderBox.tsx                          | 2 ++
 src/client/views/nodes/DocumentView.tsx                          | 2 ++
 src/client/views/nodes/FieldView.tsx                             | 1 +
 src/client/views/nodes/ImageBox.tsx                              | 1 +
 src/client/views/nodes/KeyValuePair.tsx                          | 3 ++-
 src/client/views/nodes/PDFBox.tsx                                | 2 +-
 src/client/views/nodes/VideoBox.tsx                              | 1 +
 src/client/views/nodes/WebBox.tsx                                | 1 +
 src/client/views/nodes/formattedText/DashDocView.tsx             | 3 ++-
 src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx | 3 ++-
 src/client/views/nodes/formattedText/RichTextSchema.tsx          | 1 +
 src/client/views/pdf/PDFViewer.tsx                               | 1 +
 src/client/views/presentationview/PresElementBox.tsx             | 1 +
 src/client/views/search/SearchItem.tsx                           | 3 ++-
 src/mobile/MobileInterface.tsx                                   | 5 ++++-
 34 files changed, 66 insertions(+), 22 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index e527634fd..dba802f98 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -395,6 +395,8 @@ export function returnZero() { return 0; }
 
 export function returnEmptyString() { return ""; }
 
+export function returnEmptyFilter() { return [] as string[]; }
+
 export let emptyPath = [];
 
 export function emptyFunction() { }
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 79d407c7a..aeac1d4a9 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -8,7 +8,7 @@ import MobileInkOverlay from "../../mobile/MobileInkOverlay";
 import MobileInterface from "../../mobile/MobileInterface";
 import { GestureUtils } from "../../pen-gestures/GestureUtils";
 import { MobileInkOverlayContent } from "../../server/Message";
-import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero } from "../../Utils";
+import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter } from "../../Utils";
 import { CognitiveServices } from "../cognitive_services/CognitiveServices";
 import { DocServer } from "../DocServer";
 import { DocUtils } from "../documents/Documents";
@@ -845,6 +845,7 @@ export default class GestureOverlay extends Touchable {
                 parentActive={returnTrue}
                 whenActiveChanged={emptyFunction}
                 bringToFront={emptyFunction}
+                docFilters={returnEmptyFilter}
                 ContainingCollectionView={undefined}
                 ContainingCollectionDoc={undefined}
             />;
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index fead46301..f5adeeb00 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -267,8 +267,8 @@ export default class KeyManager {
                     const text = `__DashDocId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.SelectedDocuments().map(dv => dv.Document[Id]).join(":");
                     SelectionManager.SelectedDocuments().length && navigator.clipboard.writeText(text);
                     stopPropagation = false;
-                    preventDefault = false;
-                } else preventDefault = false
+                }
+                preventDefault = false;
                 break;
         }
 
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2f68004f9..4a09f9ff3 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -20,7 +20,7 @@ import { listSpec } from '../../fields/Schema';
 import { BoolCast, Cast, FieldValue, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
 import { CurrentUserUtils } from '../util/CurrentUserUtils';
-import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, returnTrue, Utils } from '../../Utils';
+import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, returnTrue, Utils, returnEmptyFilter } from '../../Utils';
 import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
 import { DocServer } from '../DocServer';
 import { Docs, DocumentOptions } from '../documents/Documents';
@@ -310,6 +310,7 @@ export class MainView extends React.Component {
             parentActive={returnTrue}
             whenActiveChanged={emptyFunction}
             bringToFront={emptyFunction}
+            docFilters={returnEmptyFilter}
             ContainingCollectionView={undefined}
             ContainingCollectionDoc={undefined}
         />;
@@ -412,6 +413,7 @@ export class MainView extends React.Component {
                     parentActive={returnTrue}
                     whenActiveChanged={emptyFunction}
                     bringToFront={emptyFunction}
+                    docFilters={returnEmptyFilter}
                     ContainingCollectionView={undefined}
                     ContainingCollectionDoc={undefined} />
             </div>
@@ -438,6 +440,7 @@ export class MainView extends React.Component {
                     parentActive={returnTrue}
                     whenActiveChanged={emptyFunction}
                     bringToFront={emptyFunction}
+                    docFilters={returnEmptyFilter}
                     ContainingCollectionView={undefined}
                     ContainingCollectionDoc={undefined} />
                 <button className="mainView-settings" key="settings" onClick={() => SettingsManager.Instance.open()}>
@@ -529,6 +532,7 @@ export class MainView extends React.Component {
                     renderDepth={0}
                     focus={emptyFunction}
                     whenActiveChanged={emptyFunction}
+                    docFilters={returnEmptyFilter}
                     ContainingCollectionView={undefined}
                     ContainingCollectionDoc={undefined} />
             </div>;
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index f6e5e1705..37d8dd23b 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -4,7 +4,7 @@ import * as React from "react";
 import { Doc, DocListCast, Opt } from "../../fields/Doc";
 import { Id } from "../../fields/FieldSymbols";
 import { NumCast, Cast } from "../../fields/Types";
-import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils, setupMoveUpEvents } from "../../Utils";
+import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils, setupMoveUpEvents, returnEmptyFilter } from "../../Utils";
 import { Transform } from "../util/Transform";
 import { CollectionFreeFormLinksView } from "./collections/collectionFreeForm/CollectionFreeFormLinksView";
 import { DocumentView } from "./nodes/DocumentView";
@@ -202,6 +202,7 @@ export class OverlayView extends React.Component {
                     backgroundColor={returnEmptyString}
                     addDocTab={returnFalse}
                     pinToPres={emptyFunction}
+                    docFilters={returnEmptyFilter}
                     ContainingCollectionView={undefined}
                     ContainingCollectionDoc={undefined} />
             </div>;
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
index 108eb83d6..0a4334302 100644
--- a/src/client/views/Palette.tsx
+++ b/src/client/views/Palette.tsx
@@ -3,7 +3,7 @@ import { observer } from "mobx-react";
 import * as React from "react";
 import { Doc } from "../../fields/Doc";
 import { NumCast } from "../../fields/Types";
-import { emptyFunction, emptyPath, returnEmptyString, returnZero, returnFalse, returnOne, returnTrue } from "../../Utils";
+import { emptyFunction, emptyPath, returnEmptyString, returnZero, returnFalse, returnOne, returnTrue, returnEmptyFilter } from "../../Utils";
 import { Transform } from "../util/Transform";
 import { DocumentView } from "./nodes/DocumentView";
 import "./Palette.scss";
@@ -59,6 +59,7 @@ export default class Palette extends React.Component<PaletteProps> {
                             parentActive={returnTrue}
                             whenActiveChanged={emptyFunction}
                             bringToFront={emptyFunction}
+                            docFilters={returnEmptyFilter}
                             ContainingCollectionView={undefined}
                             ContainingCollectionDoc={undefined} />
                         <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div>
diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx
index c98363214..cdde32c21 100644
--- a/src/client/views/RecommendationsBox.tsx
+++ b/src/client/views/RecommendationsBox.tsx
@@ -6,7 +6,7 @@ import "./RecommendationsBox.scss";
 import { Doc, DocListCast, WidthSym, HeightSym } from "../../fields/Doc";
 import { DocumentIcon } from "./nodes/DocumentIcon";
 import { StrCast, NumCast } from "../../fields/Types";
-import { returnFalse, emptyFunction, returnEmptyString, returnOne, emptyPath, returnZero } from "../../Utils";
+import { returnFalse, emptyFunction, returnEmptyString, returnOne, emptyPath, returnZero, returnEmptyFilter } from "../../Utils";
 import { Transform } from "../util/Transform";
 import { ObjectField } from "../../fields/ObjectField";
 import { DocumentView } from "./nodes/DocumentView";
@@ -82,6 +82,7 @@ export class RecommendationsBox extends React.Component<FieldViewProps> {
                 parentActive={returnFalse}
                 whenActiveChanged={returnFalse}
                 bringToFront={emptyFunction}
+                docFilters={returnEmptyFilter}
                 ContainingCollectionView={undefined}
                 ContainingCollectionDoc={undefined}
                 ContentScaling={scale}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index f135823a8..916e631d0 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -10,7 +10,7 @@ import { Doc, DocListCast } from "../../fields/Doc";
 import { Docs, DocUtils, } from "../documents/Documents";
 import { StrCast, Cast } from "../../fields/Types";
 import { CollectionTreeView } from "./collections/CollectionTreeView";
-import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZero } from "../../Utils";
+import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZero, returnEmptyFilter } from "../../Utils";
 import { Transform } from "../util/Transform";
 import { ScriptField, ComputedField } from "../../fields/ScriptField";
 import { Scripting } from "../util/Scripting";
@@ -140,6 +140,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
                 CollectionView={undefined}
                 ContainingCollectionDoc={undefined}
                 ContainingCollectionView={undefined}
+                docFilters={returnEmptyFilter}
                 rootSelected={returnFalse}
                 onCheckedClick={this.scriptField!}
                 onChildClick={this.scriptField!}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 1c0b60b2b..a969e302d 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -8,11 +8,10 @@ import * as GoldenLayout from "../../../client/goldenLayout";
 import { DateField } from '../../../fields/DateField';
 import { Doc, DocListCast, Field, Opt, DataSym } from "../../../fields/Doc";
 import { Id } from '../../../fields/FieldSymbols';
-import { List } from '../../../fields/List';
 import { FieldId } from "../../../fields/RefField";
 import { Cast, NumCast, StrCast } from "../../../fields/Types";
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnOne, returnTrue, Utils, returnZero } from "../../../Utils";
+import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter } from "../../../Utils";
 import { DocServer } from "../../DocServer";
 import { Docs } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
@@ -843,6 +842,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
             backgroundColor={CollectionDockingView.Instance.props.backgroundColor}
             addDocTab={this.addDocTab}
             pinToPres={DockedFrameRenderer.PinDoc}
+            docFilters={returnEmptyFilter}
             ContainingCollectionView={undefined}
             ContainingCollectionDoc={undefined} />;
     }
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index f1002044a..f38eeaf41 100644
--- a/src/client/views/collections/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -124,6 +124,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
                                 parentActive={returnTrue}
                                 whenActiveChanged={emptyFunction}
                                 bringToFront={emptyFunction}
+                                docFilters={this.props.docFilters}
                                 ContainingCollectionView={undefined}
                                 ContainingCollectionDoc={undefined} />
                         </div>;
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index baf9d4156..2b8110e27 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -3,7 +3,7 @@ import { action, observable, trace } from "mobx";
 import { observer } from "mobx-react";
 import { CellInfo } from "react-table";
 import "react-table/react-table.css";
-import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils";
+import { emptyFunction, returnFalse, returnZero, returnOne, returnEmptyFilter } from "../../../Utils";
 import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc";
 import { Id } from "../../../fields/FieldSymbols";
 import { KeyCodes } from "../../util/KeyCodes";
@@ -159,6 +159,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             bringToFront: emptyFunction,
             rootSelected: returnFalse,
             fieldKey: this.props.rowProps.column.id as string,
+            docFilters: returnEmptyFilter,
             ContainingCollectionView: this.props.CollectionView,
             ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document,
             isSelected: returnFalse,
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 6dbee217a..56a2a517c 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -133,6 +133,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
                     PanelWidth={this.previewWidth}
                     PanelHeight={this.previewHeight}
                     ScreenToLocalTransform={this.getPreviewTransform}
+                    docFilters={this.docFilters}
                     ContainingCollectionDoc={this.props.CollectionView?.props.Document}
                     ContainingCollectionView={this.props.CollectionView}
                     moveDocument={this.props.moveDocument}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 9289c4cf6..4aab43b08 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -217,6 +217,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)
             ScreenToLocalTransform={dxf}
             opacity={opacity}
             focus={this.focusDocument}
+            docFilters={this.docFilters}
             ContainingCollectionDoc={this.props.CollectionView?.props.Document}
             ContainingCollectionView={this.props.CollectionView}
             addDocument={this.props.addDocument}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 93d20c475..00d6d59c8 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -10,7 +10,7 @@ import { WebField } from "../../../fields/URLField";
 import { Cast, ScriptCast, NumCast } from "../../../fields/Types";
 import { GestureUtils } from "../../../pen-gestures/GestureUtils";
 import { Upload } from "../../../server/SharedMediaTypes";
-import { Utils, returnFalse } from "../../../Utils";
+import { Utils, returnFalse, returnEmptyFilter } from "../../../Utils";
 import { DocServer } from "../../DocServer";
 import { Networking } from "../../Network";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
@@ -101,8 +101,13 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
         get childDocList() {
             return Cast(this.dataField, listSpec(Doc));
         }
+        docFilters = () => {
+            return this.props.ignoreFields?.includes("_docFilters") ? [] :
+                this.props.docFilters !== returnEmptyFilter ? this.props.docFilters() :
+                    Cast(this.props.Document._docFilters, listSpec("string"), []);
+        }
         @computed get childDocs() {
-            const docFilters = this.props.ignoreFields?.includes("_docFilters") ? [] : Cast(this.props.Document._docFilters, listSpec("string"), []);
+            const docFilters = this.docFilters();
             const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []);
             const filterFacets: { [key: string]: { [value: string]: string } } = {};  // maps each filter key to an object with value=>modifier fields
             for (let i = 0; i < docFilters.length; i += 3) {
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index d75aed0cd..747eb36a1 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -8,7 +8,7 @@ import { PrefetchProxy } from '../../../fields/Proxy';
 import { Document, listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils';
+import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils, returnEmptyFilter } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from "../../documents/DocumentTypes";
 import { DocumentManager } from '../../util/DocumentManager';
@@ -355,6 +355,7 @@ class TreeView extends React.Component<TreeViewProps> {
                     PanelHeight={panelHeight}
                     focus={returnFalse}
                     ScreenToLocalTransform={this.docTransform}
+                    docFilters={returnEmptyFilter}
                     ContainingCollectionDoc={this.props.containingCollection}
                     ContainingCollectionView={undefined}
                     addDocument={returnFalse}
@@ -472,6 +473,7 @@ class TreeView extends React.Component<TreeViewProps> {
                         whenActiveChanged={emptyFunction}
                         bringToFront={emptyFunction}
                         dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildViews)}
+                        docFilters={returnEmptyFilter}
                         ContainingCollectionView={undefined}
                         ContainingCollectionDoc={this.props.containingCollection}
                     />}
@@ -490,7 +492,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 <div className="treeViewItem-header" ref={this._header} onClick={e => {
                     if (this.props.active(true)) {
                         e.stopPropagation();
-                        e.preventDefault()
+                        e.preventDefault();
                         SelectionManager.DeselectAll();
                     }
                 }}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 2f38fb4b4..33615cc68 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -17,7 +17,7 @@ import { ComputedField, ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, returnEmptyFilter } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { CurrentUserUtils } from '../../util/CurrentUserUtils';
@@ -467,6 +467,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
                         DataDoc={facetCollection}
                         fieldKey={`${this.props.fieldKey}-filter`}
                         CollectionView={this}
+                        docFilters={returnEmptyFilter}
                         ContainingCollectionDoc={this.props.ContainingCollectionDoc}
                         ContainingCollectionView={this.props.ContainingCollectionView}
                         PanelWidth={this.facetWidth}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 571b5bf5c..00260745d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -958,6 +958,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             ContentScaling: returnOne,
             ContainingCollectionView: this.props.CollectionView,
             ContainingCollectionDoc: this.props.Document,
+            docFilters: this.docFilters,
             focus: this.focusDocument,
             backgroundColor: this.getClusterColor,
             backgroundHalo: this.backgroundHalo,
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index c0e1a0232..776266ce6 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -234,6 +234,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
             onDoubleClick={this.onChildDoubleClickHandler}
             ScreenToLocalTransform={dxf}
             focus={this.props.focus}
+            docFilters={this.docFilters}
             ContainingCollectionDoc={this.props.CollectionView?.props.Document}
             ContainingCollectionView={this.props.CollectionView}
             addDocument={this.props.addDocument}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 602246d07..1703ff4dc 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -233,6 +233,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
             onDoubleClick={this.onChildDoubleClickHandler}
             ScreenToLocalTransform={dxf}
             focus={this.props.focus}
+            docFilters={this.docFilters}
             ContainingCollectionDoc={this.props.CollectionView?.props.Document}
             ContainingCollectionView={this.props.CollectionView}
             addDocument={this.props.addDocument}
diff --git a/src/client/views/nodes/DocHolderBox.tsx b/src/client/views/nodes/DocHolderBox.tsx
index a4c4663dd..0cf5505cc 100644
--- a/src/client/views/nodes/DocHolderBox.tsx
+++ b/src/client/views/nodes/DocHolderBox.tsx
@@ -119,6 +119,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
                     Document={containedDoc}
                     DataDoc={undefined}
                     LibraryPath={emptyPath}
+                    docFilters={this.props.docFilters}
                     ContainingCollectionView={this as any} // bcz: hack!  need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked.  currently, the up selector allows only a containing collection to be selected
                     ContainingCollectionDoc={undefined}
                     fitToBox={true}
@@ -147,6 +148,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
                     Document={containedDoc}
                     DataDoc={undefined}
                     LibraryPath={emptyPath}
+                    docFilters={this.props.docFilters}
                     ContainingCollectionView={this as any} // bcz: hack!  need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked.  currently, the up selector allows only a containing collection to be selected
                     ContainingCollectionDoc={undefined}
                     fitToBox={true}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ffadc6edf..3205e3050 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -51,6 +51,7 @@ export type DocFocusFunc = () => boolean;
 export interface DocumentViewProps {
     ContainingCollectionView: Opt<CollectionView>;
     ContainingCollectionDoc: Opt<Doc>;
+    docFilters: () => string[];
     FreezeDimensions?: boolean;
     NativeWidth: () => number;
     NativeHeight: () => number;
@@ -996,6 +997,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         TraceMobx();
         return (<>
             <DocumentContentsView key={1}
+                docFilters={this.props.docFilters}
                 ContainingCollectionView={this.props.ContainingCollectionView}
                 ContainingCollectionDoc={this.props.ContainingCollectionDoc}
                 NativeWidth={this.NativeWidth}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index e9dc43bd8..305c04a90 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -27,6 +27,7 @@ export interface FieldViewProps {
     LibraryPath: Doc[];
     onClick?: ScriptField;
     dropAction: dropActionType;
+    docFilters: () => string[];
     isSelected: (outsideReaction?: boolean) => boolean;
     select: (isCtrlPressed: boolean) => void;
     rootSelected: (outsideReaction?: boolean) => boolean;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 69cffb6c2..84d49681c 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -473,6 +473,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
                 CollectionView={undefined}
                 ScreenToLocalTransform={this.screenToLocalTransform}
                 renderDepth={this.props.renderDepth + 1}
+                docFilters={this.props.docFilters}
                 ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
                 {this.contentFunc}
             </CollectionFreeFormView>
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 3cbe3e494..4568a6b16 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,7 +1,7 @@
 import { action, observable } from 'mobx';
 import { observer } from "mobx-react";
 import { Doc, Field, Opt } from '../../../fields/Doc';
-import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
+import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
@@ -56,6 +56,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             Document: this.props.doc,
             DataDoc: this.props.doc,
             LibraryPath: [],
+            docFilters:returnEmptyFilter,
             ContainingCollectionView: undefined,
             ContainingCollectionDoc: undefined,
             fieldKey: this.props.keyName,
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index fae216f17..aabaac39e 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -234,7 +234,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
             <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded}
                 setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView}
                 renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth}
-                addDocTab={this.props.addDocTab} focus={this.props.focus}
+                addDocTab={this.props.addDocTab} focus={this.props.focus} docFilters={this.props.docFilters}
                 pinToPres={this.props.pinToPres} addDocument={this.addDocument}
                 Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling}
                 ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index e4dbceca6..71556bfd3 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -383,6 +383,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
                     CollectionView={undefined}
                     ScreenToLocalTransform={this.props.ScreenToLocalTransform}
                     renderDepth={this.props.renderDepth + 1}
+                    docFilters={this.props.docFilters}
                     ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
                     {this.contentFunc}
                 </CollectionFreeFormView>
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index fb9e57b51..b726a6df9 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -488,6 +488,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
                         CollectionView={undefined}
                         ScreenToLocalTransform={this.scrollXf}
                         renderDepth={this.props.renderDepth + 1}
+                        docFilters={this.props.docFilters}
                         ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
                     </CollectionFreeFormView>
                 </div>
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 55b3f6f1e..5c3f3dcc9 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -5,7 +5,7 @@ import { Id } from "../../../../fields/FieldSymbols";
 import { ObjectField } from "../../../../fields/ObjectField";
 import { ComputedField } from "../../../../fields/ScriptField";
 import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero } from "../../../../Utils";
+import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero, returnEmptyFilter } from "../../../../Utils";
 import { DocServer } from "../../../DocServer";
 import { Docs, DocUtils } from "../../../documents/Documents";
 import { DocumentView } from "../DocumentView";
@@ -254,6 +254,7 @@ export class DashDocView extends React.Component<IDashDocView> {
                             whenActiveChanged={returnFalse}
                             bringToFront={emptyFunction}
                             dontRegisterView={false}
+                            docFilters={this.props.tbox?.props.docFilters||returnEmptyFilter}
                             ContainingCollectionView={this._textBox.props.ContainingCollectionView}
                             ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
                             ContentScaling={this.contentScaling}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 000b3c2e5..7c697033c 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -4,7 +4,7 @@ import { EditorView } from "prosemirror-view";
 import * as ReactDOM from 'react-dom';
 import { Doc, DocCastAsync } from "../../../../fields/Doc";
 import { Cast, FieldValue, NumCast } from "../../../../fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne } from "../../../../Utils";
+import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne, returnEmptyFilter } from "../../../../Utils";
 import { DocServer } from "../../../DocServer";
 import { DocumentManager } from "../../../util/DocumentManager";
 import { schema } from "./schema_rts";
@@ -222,6 +222,7 @@ export class FormattedTextBoxComment {
                                     addDocTab={returnFalse}
                                     pinToPres={returnFalse}
                                     dontRegisterView={true}
+                                    docFilters={returnEmptyFilter}
                                     ContainingCollectionDoc={undefined}
                                     ContainingCollectionView={undefined}
                                     renderDepth={1}
diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx
index 05557e22a..a989abd6a 100644
--- a/src/client/views/nodes/formattedText/RichTextSchema.tsx
+++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx
@@ -152,6 +152,7 @@ export class DashDocView {
                     whenActiveChanged={returnFalse}
                     bringToFront={emptyFunction}
                     dontRegisterView={false}
+                    docFilters={this._textBox.props.docFilters}
                     ContainingCollectionView={this._textBox.props.ContainingCollectionView}
                     ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
                     ContentScaling={this.contentScaling}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index c02dd6786..516774f44 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -51,6 +51,7 @@ interface IViewerProps {
     fieldKey: string;
     Document: Doc;
     DataDoc?: Doc;
+    docFilters: () => string[];
     ContainingCollectionView: Opt<CollectionView>;
     PanelWidth: () => number;
     PanelHeight: () => number;
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index caee06d8f..6b59a0563 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -194,6 +194,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
                     whenActiveChanged={returnFalse}
                     bringToFront={returnFalse}
                     opacity={returnOne}
+                    docFilters={this.props.docFilters}
                     ContainingCollectionView={undefined}
                     ContainingCollectionDoc={undefined}
                     ContentScaling={returnOne}
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 24d6e9d6f..74262d81a 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -7,7 +7,7 @@ import { observer } from "mobx-react";
 import { Doc } from "../../../fields/Doc";
 import { Id } from "../../../fields/FieldSymbols";
 import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue, returnOne, returnZero } from "../../../Utils";
+import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue, returnOne, returnZero, returnEmptyString, returnEmptyFilter } from "../../../Utils";
 import { DocumentType } from "../../documents/DocumentTypes";
 import { DocumentManager } from "../../util/DocumentManager";
 import { DragManager, SetupDrag } from "../../util/DragManager";
@@ -164,6 +164,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
                     removeDocument={returnFalse}
                     addDocTab={returnFalse}
                     pinToPres={returnFalse}
+                    docFilters={returnEmptyFilter}
                     ContainingCollectionDoc={undefined}
                     ContainingCollectionView={undefined}
                     ScreenToLocalTransform={Transform.Identity}
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index da14ffc88..a50ec103e 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -22,7 +22,7 @@ import { listSpec } from '../fields/Schema';
 import { Cast, FieldValue } from '../fields/Types';
 import { WebField } from "../fields/URLField";
 import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
-import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero } from '../Utils';
+import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter } from '../Utils';
 import "./MobileInterface.scss";
 import { CollectionView } from '../client/views/collections/CollectionView';
 import { InkingStroke } from '../client/views/InkingStroke';
@@ -126,6 +126,7 @@ export default class MobileInterface extends React.Component {
                 parentActive={returnTrue}
                 whenActiveChanged={emptyFunction}
                 bringToFront={emptyFunction}
+                docFilters={returnEmptyFilter}
                 ContainingCollectionView={undefined}
                 ContainingCollectionDoc={undefined} />;
         }
@@ -205,6 +206,7 @@ export default class MobileInterface extends React.Component {
                         whenActiveChanged={returnFalse}
                         ScreenToLocalTransform={Transform.Identity}
                         renderDepth={0}
+                        docFilters={returnEmptyFilter}
                         ContainingCollectionView={undefined}
                         ContainingCollectionDoc={undefined}
                         rootSelected={returnTrue}>
@@ -293,6 +295,7 @@ export default class MobileInterface extends React.Component {
                         parentActive={returnTrue}
                         whenActiveChanged={emptyFunction}
                         bringToFront={emptyFunction}
+                        docFilters={returnEmptyFilter}
                         ContainingCollectionView={undefined}
                         ContainingCollectionDoc={undefined} />
                 </div>
-- 
cgit v1.2.3-70-g09d2

