From 462cd06a1bff6e510132a4434a72fd7b4245cfdf Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Tue, 15 Oct 2019 17:29:57 -0400
Subject: good enough for stanley to start texting i think

---
 .../views/collections/CollectionDockingView.tsx      | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

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

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 1f78c8c97..5e00fda36 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -426,15 +426,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     }
                     tab.setActive(true);
                 };
-                ReactDOM.render(<span title="Drag as document" onPointerDown={
-                    e => {
-                        e.preventDefault();
-                        e.stopPropagation();
-                        DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, {
-                            handlers: { dragComplete: emptyFunction },
-                            hideSource: false
-                        });
-                    }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan);
+                ReactDOM.render(<span title="Drag as document"
+                    className="collectionDockingView-dragAsDocument"
+                    onPointerDown={
+                        e => {
+                            e.preventDefault();
+                            e.stopPropagation();
+                            DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, {
+                                handlers: { dragComplete: emptyFunction },
+                                hideSource: false
+                            });
+                        }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan);
                 ReactDOM.render(<ButtonSelector Document={doc} Stack={stack} />, gearSpan);
                 // ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, data, where) => {
                 //     where === "onRight" ? CollectionDockingView.AddRightSplit(doc, dataDoc) : CollectionDockingView.Instance.AddTab(stack, doc, dataDoc);
-- 
cgit v1.2.3-70-g09d2


From ca406ddce0a9b35ca9063f74979c952e080339f1 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Sun, 3 Nov 2019 16:59:13 -0500
Subject: some infrastructure stuff and bug fixes. not really any new features
 yet

---
 src/client/util/SelectionManager.ts                | 13 +++++-----
 src/client/views/DocumentDecorations.tsx           | 29 ++++++++++++++--------
 src/client/views/InkingStroke.tsx                  |  6 +++++
 .../views/collections/CollectionDockingView.tsx    |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 .../collections/collectionFreeForm/MarqueeView.tsx | 12 ++++++---
 src/client/views/nodes/DocumentContentsView.tsx    |  4 ++-
 7 files changed, 46 insertions(+), 22 deletions(-)

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

diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 3ae43e029..2a57c67bd 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -4,6 +4,7 @@ import { DocumentView } from "../views/nodes/DocumentView";
 import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
 import { NumCast, StrCast } from "../../new_fields/Types";
 import { InkingControl } from "../views/InkingControl";
+import { InkDocAndStroke } from "../views/InkingStroke";
 
 export namespace SelectionManager {
 
@@ -11,7 +12,7 @@ export namespace SelectionManager {
 
         @observable IsDragging: boolean = false;
         @observable SelectedDocuments: Array<DocumentView> = [];
-        @observable SelectedInk: Array<Map<any, any>> = [];
+        @observable SelectedInk: Array<{ Document: Doc, Ink: Map<any, any> }> = [];
 
 
         @action
@@ -46,7 +47,7 @@ export namespace SelectionManager {
         }
 
         @action
-        SelectInk(ink: Map<any, any>, ctrlPressed: boolean): void {
+        SelectInk(ink: { Document: Doc, Ink: Map<any, any> }, ctrlPressed: boolean): void {
             if (manager.SelectedInk.indexOf(ink) === -1) {
                 if (!ctrlPressed) {
                     this.DeselectAll();
@@ -68,7 +69,7 @@ export namespace SelectionManager {
         manager.SelectDoc(docView, ctrlPressed);
     }
 
-    export function SelectInk(ink: Map<any, any>, ctrlPressed: boolean): void {
+    export function SelectInk(ink: { Document: Doc, Ink: Map<any, any> }, ctrlPressed: boolean): void {
         manager.SelectInk(ink, ctrlPressed);
     }
 
@@ -95,12 +96,12 @@ export namespace SelectionManager {
         return manager.SelectedDocuments.slice();
     }
 
-    export function SelectedInk(): Array<Map<any, any>> {
+    export function SelectedInk(): Array<{ Document: Doc, Ink: Map<any, any> }> {
         return manager.SelectedInk.slice();
     }
 
-    export function AllSelected(): Array<DocumentView | Map<any, any>> {
-        let arr: Array<DocumentView | Map<any, any>> = [];
+    export function AllSelected(): Array<DocumentView | InkDocAndStroke> {
+        let arr: Array<DocumentView | InkDocAndStroke> = [];
         arr = SelectionManager.SelectedDocuments();
         arr.push(...SelectionManager.SelectedInk());
         return arr;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index cdb49350f..c3020eacb 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -186,8 +186,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 let top = bounds.y;
                 let right = bounds.r;
                 let bottom = bounds.b;
-                docViewOrInk.forEach((value: StrokeData, key: string) => {
+                let ink;
+                docViewOrInk.Ink.forEach((value: StrokeData, key: string) => {
                     value.pathData.map(val => {
+                        ink = docViewOrInk.Document.ink;
                         left = Math.min(val.x, left);
                         top = Math.min(val.y, top);
                         right = Math.max(val.x, right);
@@ -224,7 +226,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         document.removeEventListener("pointerup", this.onBackgroundUp);
         document.removeEventListener("pointermove", this.onTitleMove);
         document.removeEventListener("pointerup", this.onTitleUp);
-        DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => docView.ContentDiv!), dragData, e.x, e.y, {
+        DragManager.StartDocumentDrag(SelectionManager.AllSelected().map(docOrInk => docOrInk instanceof DocumentView ? docOrInk.ContentDiv! : (document.createElement("div"))), dragData, e.x, e.y, {
             handlers: { dragComplete: action(() => this._hidden = this.Interacting = false) },
             hideSource: true
         });
@@ -548,15 +550,21 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
 
     @computed
     get selectionTitle(): string {
-        if (SelectionManager.SelectedDocuments().length === 1) {
-            let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
-            if (typeof field === "string") {
-                return field;
+        if (SelectionManager.AllSelected().length === 1) {
+            let selected = SelectionManager.AllSelected()[0];
+            if (selected instanceof DocumentView) {
+                let field = selected.props.Document[this._fieldKey];
+                if (typeof field === "string") {
+                    return field;
+                }
+                else if (typeof field === "number") {
+                    return field.toString();
+                }
             }
-            else if (typeof field === "number") {
-                return field.toString();
+            else {
+                return "-ink strokes-";
             }
-        } else if (SelectionManager.SelectedDocuments().length > 1) {
+        } else if (SelectionManager.AllSelected().length > 1) {
             return "-multiple-";
         }
         return "-unset-";
@@ -581,7 +589,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         }
         let minimizeIcon = (
             <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>
-                {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."}
+                {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/}
+                {(SelectionManager.SelectedDocuments().length === 1 && SelectionManager.SelectedInk().length === 0) ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."}
             </div>);
 
         bounds.x = Math.max(0, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 332c22512..a097a7991 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -5,6 +5,7 @@ import React = require("react");
 import { InkTool } from "../../new_fields/InkField";
 import "./InkingStroke.scss";
 import { AudioBox } from "./nodes/AudioBox";
+import { Doc } from "../../new_fields/Doc";
 
 
 interface StrokeProps {
@@ -20,6 +21,11 @@ interface StrokeProps {
     deleteCallback: (index: string) => void;
 }
 
+export type InkDocAndStroke = {
+    Document: Doc;
+    Ink: Map<any, any>;
+};
+
 @observer
 export class InkingStroke extends React.Component<StrokeProps> {
 
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 937a4949e..3ff99b9f4 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -617,7 +617,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         }
     }
 
-    get layoutDoc() { return this._document && Doc.Layout(this._document);}
+    get layoutDoc() { return this._document && Doc.Layout(this._document); }
     panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc.width), NumCast(this.layoutDoc.nativeWidth)), this._panelWidth) : this._panelWidth;
     panelHeight = () => this._panelHeight;
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5e2973368..9acffc952 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -107,7 +107,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         added && this.updateCluster(newBox);
         return added;
     }
-    private selectDocuments = (docs: Doc[], ink: Map<any, any>[]) => {
+    private selectDocuments = (docs: Doc[], ink: { Document: Doc, Ink: Map<any, any> }[]) => {
         SelectionManager.DeselectAll();
         docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
         ink.forEach(i => SelectionManager.SelectInk(i, true));
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 414238ccd..138168fed 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -28,7 +28,7 @@ interface MarqueeViewProps {
     getTransform: () => Transform;
     addDocument: (doc: Doc) => boolean;
     activeDocuments: () => Doc[];
-    selectDocuments: (docs: Doc[], ink: Map<any, any>[]) => void;
+    selectDocuments: (docs: Doc[], ink: { Document: Doc, Ink: Map<any, any> }[]) => void;
     removeDocument: (doc: Doc) => boolean;
     addLiveTextDocument: (doc: Doc) => void;
     isSelected: () => boolean;
@@ -198,8 +198,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             if (!e.shiftKey) {
                 SelectionManager.DeselectAll(mselect.length ? undefined : this.props.Document);
             }
-            this.props.selectDocuments(mselect.length ? mselect : [this.props.Document],
-                this.ink ? [this.marqueeInkSelect(this.ink.inkData)] : []);
+            let inkselect = this.ink ? this.marqueeInkSelect(this.ink.inkData) : new Map();
+            let inks = inkselect.size ? [{ Document: this.inkDoc, Ink: inkselect }] : [];
+            let docs = mselect.length ? mselect : (inks.length ? [] : [this.props.Document]);
+            this.props.selectDocuments(docs, inks);
         }
         if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) {
             MarqueeOptionsMenu.Instance.createCollection = this.collection;
@@ -266,6 +268,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
     }
 
+    get inkDoc() {
+        return this.props.extensionDoc;
+    }
+
     get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored.
         return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField);
     }
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 779d25cdd..96271cfe1 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -30,6 +30,7 @@ import { DocuLinkBox } from "./DocuLinkBox";
 import { PresElementBox } from "../presentationview/PresElementBox";
 import { VideoBox } from "./VideoBox";
 import { WebBox } from "./WebBox";
+import { InkingStroke } from "../InkingStroke";
 import React = require("react");
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
@@ -97,7 +98,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
                 components={{
                     FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FontIconBox: FontIconBox, ButtonBox, FieldView,
                     CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
-                    PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox, ColorBox, DocuLinkBox
+                    PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox,
+                    ColorBox, DocuLinkBox
                 }}
                 bindings={this.CreateBindings()}
                 jsx={this.layout}
-- 
cgit v1.2.3-70-g09d2


From 63129c244fc2b9a5c60e6a94b864895641b86f57 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 20 Nov 2019 16:59:40 -0500
Subject: lots of changes to make rendering more efficient (fewer mobx
 invalidations).  made selection and collecitonviews use an observableMap that
 works now.

---
 src/client/util/SelectionManager.ts                |  41 ++--
 src/client/views/DocComponent.tsx                  |  14 +-
 src/client/views/DocumentButtonBar.scss            | 106 +++------
 src/client/views/DocumentButtonBar.tsx             | 264 ++++++++-------------
 src/client/views/Main.scss                         |   6 +-
 src/client/views/MainView.scss                     |   1 -
 src/client/views/TemplateMenu.scss                 |  50 ++++
 src/client/views/TemplateMenu.tsx                  |   4 +-
 .../views/collections/CollectionDockingView.tsx    |   2 +-
 .../views/collections/CollectionSchemaView.tsx     |  14 +-
 .../views/collections/CollectionTreeView.tsx       |   6 +-
 src/client/views/collections/CollectionView.tsx    |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  87 ++++---
 .../collections/collectionFreeForm/MarqueeView.tsx |   2 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   5 +-
 .../views/nodes/ContentFittingDocumentView.tsx     |   2 +-
 src/client/views/nodes/DocumentContentsView.tsx    |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  27 ++-
 src/client/views/nodes/FieldView.tsx               |   4 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  10 +-
 src/client/views/nodes/KeyValueBox.tsx             |   2 +-
 src/client/views/nodes/PDFBox.tsx                  |   7 +-
 src/client/views/nodes/VideoBox.tsx                |   2 +-
 src/client/views/pdf/PDFViewer.tsx                 |  14 +-
 src/new_fields/Doc.ts                              |  16 +-
 25 files changed, 326 insertions(+), 364 deletions(-)
 create mode 100644 src/client/views/TemplateMenu.scss

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

diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index ca61f9014..e01216e0f 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,45 +1,44 @@
-import { observable, action, runInAction, IReactionDisposer, reaction, autorun } from "mobx";
-import { Doc, Opt } from "../../new_fields/Doc";
+import { observable, action, runInAction, ObservableMap } from "mobx";
+import { Doc } from "../../new_fields/Doc";
 import { DocumentView } from "../views/nodes/DocumentView";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { NumCast, StrCast } from "../../new_fields/Types";
+import { computedFn } from "mobx-utils";
 
 export namespace SelectionManager {
 
     class Manager {
 
         @observable IsDragging: boolean = false;
-        @observable SelectedDocuments: Array<DocumentView> = [];
+        SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap();
 
 
         @action
         SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
             // if doc is not in SelectedDocuments, add it
-            if (manager.SelectedDocuments.indexOf(docView) === -1) {
+            if (!manager.SelectedDocuments.get(docView)) {
                 if (!ctrlPressed) {
                     this.DeselectAll();
                 }
 
-                manager.SelectedDocuments.push(docView);
+                manager.SelectedDocuments.set(docView, true);
                 // console.log(manager.SelectedDocuments);
                 docView.props.whenActiveChanged(true);
-            } else if (!ctrlPressed && manager.SelectedDocuments.length > 1) {
-                manager.SelectedDocuments.map(dv => dv !== docView && dv.props.whenActiveChanged(false));
-                manager.SelectedDocuments = [docView];
+            } else if (!ctrlPressed && Array.from(manager.SelectedDocuments.entries()).length > 1) {
+                Array.from(manager.SelectedDocuments.keys()).map(dv => dv !== docView && dv.props.whenActiveChanged(false));
+                manager.SelectedDocuments.clear();
+                manager.SelectedDocuments.set(docView, true);
             }
         }
         @action
         DeselectDoc(docView: DocumentView): void {
-            let ind = manager.SelectedDocuments.indexOf(docView);
-            if (ind !== -1) {
-                manager.SelectedDocuments.splice(ind, 1);
+            if (manager.SelectedDocuments.get(docView)) {
+                manager.SelectedDocuments.delete(docView);
                 docView.props.whenActiveChanged(false);
             }
         }
         @action
         DeselectAll(): void {
-            manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
-            manager.SelectedDocuments = [];
+            Array.from(manager.SelectedDocuments.keys()).map(dv => dv.props.whenActiveChanged(false));
+            manager.SelectedDocuments.clear();
         }
     }
 
@@ -52,14 +51,18 @@ export namespace SelectionManager {
         manager.SelectDoc(docView, ctrlPressed);
     }
 
-    export function IsSelected(doc: DocumentView): boolean {
-        return manager.SelectedDocuments.indexOf(doc) !== -1;
+    export function IsSelected(doc: DocumentView, outsideReaction?: boolean): boolean {
+        return outsideReaction ?
+            manager.SelectedDocuments.get(doc) ? true : false :
+            computedFn(function isSelected(doc: DocumentView) {
+                return manager.SelectedDocuments.get(doc) ? true : false;
+            })(doc);
     }
 
     export function DeselectAll(except?: Doc): void {
         let found: DocumentView | undefined = undefined;
         if (except) {
-            for (const view of manager.SelectedDocuments) {
+            for (const view of Array.from(manager.SelectedDocuments.keys())) {
                 if (view.props.Document === except) found = view;
             }
         }
@@ -72,6 +75,6 @@ export namespace SelectionManager {
     export function GetIsDragging() { return manager.IsDragging; }
 
     export function SelectedDocuments(): Array<DocumentView> {
-        return manager.SelectedDocuments.slice();
+        return Array.from(manager.SelectedDocuments.keys());
     }
 }
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 286a77f81..1bd1006a8 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -27,7 +27,7 @@ interface DocExtendableProps {
     Document: Doc;
     DataDoc?: Doc;
     fieldKey: string;
-    isSelected: () => boolean;
+    isSelected: (outsideReaction?: boolean) => boolean;
     renderDepth: number;
 }
 export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCtor: (doc: Doc) => T) {
@@ -37,7 +37,7 @@ export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCt
         @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
         @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
         @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-        active = () => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0);//  && !InkingControl.Instance.selectedTool;  // bcz: inking state shouldn't affect static tools 
+        active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0);//  && !InkingControl.Instance.selectedTool;  // bcz: inking state shouldn't affect static tools 
     }
     return Component;
 }
@@ -49,7 +49,7 @@ interface DocAnnotatableProps {
     DataDoc?: Doc;
     fieldKey: string;
     whenActiveChanged: (isActive: boolean) => void;
-    isSelected: () => boolean;
+    isSelected: (outsideReaction?: boolean) => boolean;
     renderDepth: number;
 }
 export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
@@ -82,10 +82,10 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
         }
 
         whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
-        active = () => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
-            (this.props.Document.forceActive || this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0) ? true : false)
-        annotationsActive = () => (InkingControl.Instance.selectedTool !== InkTool.None ||
-            (this.props.Document.forceActive || this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0) ? true : false)
+        active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
+            (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
+        annotationsActive = (outsideReaction?: boolean) => (InkingControl.Instance.selectedTool !== InkTool.None ||
+            (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
     }
     return Component;
 }
\ No newline at end of file
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index 8cd419bbe..db6bf2ba0 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -2,54 +2,23 @@
 
 $linkGap : 3px;
 
-.linkFlyout {
+.documentButtonBar-linkFlyout {
     grid-column: 2/4;
 }
 
-.linkButton-empty:hover {
+.documentButtonBar-linkButton-empty:hover {
     background: $main-accent;
     transform: scale(1.05);
     cursor: pointer;
 }
 
-.linkButton-nonempty:hover {
+.documentButtonBar-linkButton-nonempty:hover {
     background: $main-accent;
     transform: scale(1.05);
     cursor: pointer;
 }
-
-.documentButtonBar {
-    margin-top: $linkGap;
-    grid-column: 1/4;
-    width: max-content;
-    height: auto;
-    display: flex;
-    flex-direction: row;
-}
-
-.linkButtonWrapper {
-    pointer-events: auto;
-    padding-right: 5px;
-    width: 25px;
-}
-
-.linkButton-linker {
-    height: 20px;
-    width: 20px;
-    text-align: center;
-    border-radius: 50%;
-    pointer-events: auto;
-    color: $dark-color;
-    border: $dark-color 1px solid;
-}
-
-.linkButton-linker:hover {
-    cursor: pointer;
-    transform: scale(1.05);
-}
-
-.linkButton-empty,
-.linkButton-nonempty {
+.documentButtonBar-linkButton-empty,
+.documentButtonBar-linkButton-nonempty {
     height: 20px;
     width: 20px;
     border-radius: 50%;
@@ -73,57 +42,38 @@ $linkGap : 3px;
     }
 }
 
-.templating-menu {
-    position: absolute;
-    pointer-events: auto;
-    text-transform: uppercase;
-    letter-spacing: 2px;
-    font-size: 75%;
-    transition: transform 0.2s;
-    text-align: center;
+.documentButtonBar {
+    margin-top: $linkGap;
+    grid-column: 1/4;
+    width: max-content;
+    height: auto;
     display: flex;
-    justify-content: center;
-    align-items: center;
+    flex-direction: row;
 }
 
-.templating-button,
-.docDecs-tagButton {
-    width: 20px;
+.documentButtonBar-button {
+    pointer-events: auto;
+    padding-right: 5px;
+    width: 25px;
+}
+
+.documentButtonBar-linker {
     height: 20px;
-    border-radius: 50%;
-    opacity: 0.9;
-    font-size: 14;
-    background-color: $dark-color;
-    color: $light-color;
+    width: 20px;
     text-align: center;
-    cursor: pointer;
-
-    &:hover {
-        background: $main-accent;
-        transform: scale(1.05);
-    }
+    border-radius: 50%;
+    pointer-events: auto;
+    color: $dark-color;
+    border: $dark-color 1px solid;
+    transition: 0.2s ease all;
 }
 
-#template-list {
-    position: absolute;
-    top: 25px;
-    left: 0px;
-    width: max-content;
-    font-family: $sans-serif;
-    font-size: 12px;
-    background-color: $light-color-secondary;
-    padding: 2px 12px;
-    list-style: none;
-
-    .templateToggle, .chromeToggle {
-        text-align: left;
-    }
-
-    input {
-        margin-right: 10px;
-    }
+.documentButtonBar-linker:hover {
+    cursor: pointer;
+    transform: scale(1.05);
 }
 
+
 @-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
 @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
 @keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
\ No newline at end of file
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index c7ee413c9..1fefc70f1 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -1,7 +1,7 @@
 import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
 import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable, runInAction } from "mobx";
+import { action, observable, runInAction, computed } from "mobx";
 import { observer } from "mobx-react";
 import { Doc } from "../../new_fields/Doc";
 import { RichTextField } from '../../new_fields/RichTextField';
@@ -42,52 +42,53 @@ const fetch: IconProp = "sync-alt";
 @observer
 export class DocumentButtonBar extends React.Component<{ views: DocumentView[], stack?: any }, {}> {
     private _linkButton = React.createRef<HTMLDivElement>();
-    private _aliasButton = React.createRef<HTMLDivElement>();
-    private _tooltipoff = React.createRef<HTMLDivElement>();
-    private _textDoc?: Doc;
+    private _downX = 0;
+    private _downY = 0;
+    private _pullAnimating = false;
+    private _pushAnimating = false;
+    private _pullColorAnimating = false;
+
+    @observable private pushIcon: IconProp = "arrow-alt-circle-up";
+    @observable private pullIcon: IconProp = "arrow-alt-circle-down";
+    @observable private pullColor: string = "white";
+    @observable private isAnimatingFetch = false;
+    @observable private openHover = false;
+
     public static Instance: DocumentButtonBar;
+    public static hasPushedHack = false;
+    public static hasPulledHack = false;
 
     constructor(props: { views: DocumentView[] }) {
         super(props);
         DocumentButtonBar.Instance = this;
     }
 
-    @observable public pushIcon: IconProp = "arrow-alt-circle-up";
-    @observable public pullIcon: IconProp = "arrow-alt-circle-down";
-    @observable public pullColor: string = "white";
-    @observable public isAnimatingFetch = false;
-    @observable public openHover = false;
-    public pullColorAnimating = false;
-
-    private pullAnimating = false;
-    private pushAnimating = false;
-
     public startPullOutcome = action((success: boolean) => {
-        if (!this.pullAnimating) {
-            this.pullAnimating = true;
+        if (!this._pullAnimating) {
+            this._pullAnimating = true;
             this.pullIcon = success ? "check-circle" : "stop-circle";
             setTimeout(() => runInAction(() => {
                 this.pullIcon = "arrow-alt-circle-down";
-                this.pullAnimating = false;
+                this._pullAnimating = false;
             }), 1000);
         }
     });
 
     public startPushOutcome = action((success: boolean) => {
-        if (!this.pushAnimating) {
-            this.pushAnimating = true;
+        if (!this._pushAnimating) {
+            this._pushAnimating = true;
             this.pushIcon = success ? "check-circle" : "stop-circle";
             setTimeout(() => runInAction(() => {
                 this.pushIcon = "arrow-alt-circle-up";
-                this.pushAnimating = false;
+                this._pushAnimating = false;
             }), 1000);
         }
     });
 
     public setPullState = action((unchanged: boolean) => {
         this.isAnimatingFetch = false;
-        if (!this.pullColorAnimating) {
-            this.pullColorAnimating = true;
+        if (!this._pullColorAnimating) {
+            this._pullColorAnimating = true;
             this.pullColor = unchanged ? "lawngreen" : "red";
             setTimeout(this.clearPullColor, 1000);
         }
@@ -95,33 +96,17 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
 
     private clearPullColor = action(() => {
         this.pullColor = "white";
-        this.pullColorAnimating = false;
+        this._pullColorAnimating = false;
     });
 
-    onLinkerButtonDown = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        e.preventDefault();
-        document.removeEventListener("pointermove", this.onLinkerButtonMoved);
-        document.addEventListener("pointermove", this.onLinkerButtonMoved);
-        document.removeEventListener("pointerup", this.onLinkerButtonUp);
-        document.addEventListener("pointerup", this.onLinkerButtonUp);
-    }
-
-
-    onLinkerButtonUp = (e: PointerEvent): void => {
-        document.removeEventListener("pointermove", this.onLinkerButtonMoved);
-        document.removeEventListener("pointerup", this.onLinkerButtonUp);
-        e.stopPropagation();
-    }
-
 
     @action
-    onLinkerButtonMoved = (e: PointerEvent): void => {
-        if (this._linkButton.current !== null) {
-            document.removeEventListener("pointermove", this.onLinkerButtonMoved);
+    onLinkButtonMoved = (e: PointerEvent): void => {
+        if (this._linkButton.current !== null && (Math.abs(e.clientX - this._downX) > 3 || Math.abs(e.clientY - this._downY) > 3)) {
+            document.removeEventListener("pointermove", this.onLinkButtonMoved);
             document.removeEventListener("pointerup", this.onLinkButtonUp);
             let docView = this.props.views[0];
-            let container = docView.props.ContainingCollectionDoc ? docView.props.ContainingCollectionDoc.proto : undefined;
+            let container = docView.props.ContainingCollectionDoc?.proto;
             let dragData = new DragManager.LinkDragData(docView.props.Document, container ? [container] : []);
             let linkDrag = UndoManager.StartBatch("Drag Link");
             DragManager.StartLinkDrag(this._linkButton.current, dragData, e.pageX, e.pageY, {
@@ -150,151 +135,106 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
 
 
     onLinkButtonDown = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        e.preventDefault();
-        document.removeEventListener("pointermove", this.onLinkerButtonMoved);
-        document.addEventListener("pointermove", this.onLinkerButtonMoved);
+        this._downX = e.clientX;
+        this._downY = e.clientY;
+        document.removeEventListener("pointermove", this.onLinkButtonMoved);
+        document.addEventListener("pointermove", this.onLinkButtonMoved);
         document.removeEventListener("pointerup", this.onLinkButtonUp);
         document.addEventListener("pointerup", this.onLinkButtonUp);
+        e.stopPropagation();
     }
 
     onLinkButtonUp = (e: PointerEvent): void => {
-        document.removeEventListener("pointermove", this.onLinkerButtonMoved);
+        document.removeEventListener("pointermove", this.onLinkButtonMoved);
         document.removeEventListener("pointerup", this.onLinkButtonUp);
         e.stopPropagation();
     }
 
-    private get targetDoc() {
-        return this.props.views[0].props.Document;
-    }
-
-    considerGoogleDocsPush = () => {
-        let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
-        if (!canPush) return (null);
-        let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined;
-        let icon: IconProp = published ? (this.pushIcon as any) : cloud;
-        return (
-            <div className={"linkButtonWrapper"}>
-                <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
-                    DocumentButtonBar.hasPushedHack = false;
-                    this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1;
-                }}>
-                    <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} />
-                </div>
-            </div>
-        );
+    @computed
+    get considerGoogleDocsPush() {
+        let targetDoc = this.props.views[0].props.Document;
+        let published = Doc.GetProto(targetDoc)[GoogleRef] !== undefined;
+        return <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="documentButtonBar-linker" onClick={() => {
+            DocumentButtonBar.hasPushedHack = false;
+            targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1;
+        }}>
+            <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "sm" : "xs"} />
+        </div>;
     }
 
-    considerGoogleDocsPull = () => {
-        let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
-        let dataDoc = Doc.GetProto(this.targetDoc);
-        if (!canPull || !dataDoc[GoogleRef]) return (null);
-        let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch;
-        icon = this.openHover ? "share" : icon;
+    @computed
+    get considerGoogleDocsPull() {
+        let targetDoc = this.props.views[0].props.Document;
+        let dataDoc = Doc.GetProto(targetDoc);
         let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
-        let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`;
-        return (
-            <div className={"linkButtonWrapper"}>
-                <div
-                    title={title}
-                    className="linkButton-linker"
-                    style={{
-                        backgroundColor: this.pullColor,
-                        transition: "0.2s ease all"
-                    }}
-                    onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)}
-                    onPointerLeave={() => runInAction(() => this.openHover = false)}
-                    onClick={e => {
-                        if (e.altKey) {
-                            e.preventDefault();
-                            window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`);
-                        } else {
-                            this.clearPullColor();
-                            DocumentButtonBar.hasPulledHack = false;
-                            this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
-                            dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
-                        }
-                    }}>
-                    <FontAwesomeIcon
-                        style={{
-                            WebkitAnimation: animation,
-                            MozAnimation: animation
-                        }}
-                        className="documentdecorations-icon"
-                        icon={icon}
-                        size="sm"
-                    />
-                </div>
-            </div>
-        );
+        return !dataDoc[GoogleRef] ? (null) : <div className="documentButtonBar-linker"
+            title={`${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`}
+            style={{ backgroundColor: this.pullColor }}
+            onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)}
+            onPointerLeave={action(() => this.openHover = false)}
+            onClick={e => {
+                if (e.altKey) {
+                    e.preventDefault();
+                    window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`);
+                } else {
+                    this.clearPullColor();
+                    DocumentButtonBar.hasPulledHack = false;
+                    targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1;
+                    dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
+                }
+            }}>
+            <FontAwesomeIcon className="documentdecorations-icon" size="sm"
+                style={{ WebkitAnimation: animation, MozAnimation: animation }}
+                icon={this.openHover ? "share" : dataDoc.unchanged === false ? (this.pullIcon as any) : fetch}
+            />
+        </div>;
     }
 
-    public static hasPushedHack = false;
-    public static hasPulledHack = false;
-
-    considerTooltip = () => {
-        let thisDoc = this.props.views[0].props.Document;
-        let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField;
-        if (!isTextDoc) return null;
-        this._textDoc = thisDoc;
-        return (
-            <div className="tooltipwrapper">
-                <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}>
-                    {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */}
+    @computed
+    get linkButton() {
+        let linkCount = LinkManager.Instance.getAllRelatedLinks(this.props.views[0].props.Document).length;
+        return <div title="Drag(create link) Tap(view links)" className="documentButtonBar-linkFlyout" ref={this._linkButton}>
+            <Flyout anchorPoint={anchorPoints.RIGHT_TOP}
+                content={<LinkMenu docView={this.props.views[0]} addDocTab={this.props.views[0].props.addDocTab} changeFlyout={emptyFunction} />}>
+                <div className={"documentButtonBar-linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >
+                    {linkCount ? linkCount : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />}
                 </div>
-            </div>
-
-        );
+            </Flyout>
+        </div>;
     }
 
-    onTooltipOff = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        if (this._textDoc) {
-            if (this._tooltipoff.current) {
-                if (this._tooltipoff.current.title === "Hide Tooltip") {
-                    this._tooltipoff.current.title = "Show Tooltip";
-                    this._textDoc.tooltip = "hi";
-                }
-                else {
-                    this._tooltipoff.current.title = "Hide Tooltip";
-                }
-            }
-        }
+    @computed
+    get contextButton() {
+        return <ParentDocSelector Views={this.props.views} Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => {
+            where === "onRight" ? CollectionDockingView.AddRightSplit(doc, data) :
+                this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc, data) :
+                    this.props.views[0].props.addDocTab(doc, data, "onRight");
+            return true;
+        }} />;
     }
 
     render() {
-        let linkButton = null;
-        if (this.props.views.length > 0) {
-            let selFirst = this.props.views[0];
-
-            let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length;
-            linkButton = <Flyout anchorPoint={anchorPoints.RIGHT_TOP}
-                content={<LinkMenu docView={selFirst} addDocTab={selFirst.props.addDocTab} changeFlyout={emptyFunction} />}>
-                <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >
-                    {linkCount ? linkCount : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />}
-                </div>
-            </Flyout >;
-        }
-
         let templates: Map<Template, boolean> = new Map();
         Array.from(Object.values(Templates.TemplateList)).map(template =>
             templates.set(template, this.props.views.reduce((checked, doc) => checked || doc.getLayoutPropStr("show" + template.Name) ? true : false, false as boolean)));
 
-        return (<div className="documentButtonBar">
-            <div className="linkButtonWrapper">
-                <div title="Drag(create link) Tap(view links)" className="linkFlyout" ref={this._linkButton}> {linkButton}  </div>
+        let isText = this.props.views[0].props.Document.data instanceof RichTextField; // bcz: Todo - can't assume layout is using the 'data' field.  need to add fieldKey to DocumentView
+        let considerPull = isText && this.considerGoogleDocsPull;
+        let considerPush = isText && this.considerGoogleDocsPush;
+        return <div className="documentButtonBar">
+            <div className="documentButtonBar-button">
+                {this.linkButton}
             </div>
-            <div className="linkButtonWrapper">
+            <div className="documentButtonBar-button">
                 <TemplateMenu docs={this.props.views} templates={templates} />
             </div>
-            {this.considerGoogleDocsPush()}
-            {this.considerGoogleDocsPull()}
-            <ParentDocSelector Views={this.props.views} Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => {
-                where === "onRight" ? CollectionDockingView.AddRightSplit(doc, data) : this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc, data) : this.props.views[0].props.addDocTab(doc, data, "onRight");
-                return true;
-            }} />
-            {/* {this.considerTooltip()} */}
-        </div>
-        );
+            <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}>
+                {this.considerGoogleDocsPush}
+            </div>
+            <div className="documentButtonBar-button" style={{ display: !considerPull ? "none" : "" }}>
+                {this.considerGoogleDocsPull}
+            </div>
+            {this.contextButton}
+        </div>;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 465527468..3b66160fb 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -5,7 +5,7 @@ html,
 body {
     width: 100%;
     height: 100%;
-    overflow: auto;
+    overflow: hidden;
     font-family: $sans-serif;
     margin: 0;
     position: absolute;
@@ -65,10 +65,6 @@ button:hover {
     cursor: pointer;
 }
 
-#root {
-    overflow: visible;
-}
-
 .svg-inline--fa {
     vertical-align: unset;
 }
\ No newline at end of file
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index b3fff081e..0ee30f117 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -20,7 +20,6 @@
     position: absolute;
     top: 0;
     left: 0;
-    overflow: auto;
     z-index: 1;
 }
 
diff --git a/src/client/views/TemplateMenu.scss b/src/client/views/TemplateMenu.scss
new file mode 100644
index 000000000..186d3ab0d
--- /dev/null
+++ b/src/client/views/TemplateMenu.scss
@@ -0,0 +1,50 @@
+@import "globalCssVariables";
+.templating-menu {
+    position: absolute;
+    pointer-events: auto;
+    text-transform: uppercase;
+    letter-spacing: 2px;
+    font-size: 75%;
+    transition: transform 0.2s;
+    text-align: center;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.templating-button {
+    width: 20px;
+    height: 20px;
+    border-radius: 50%;
+    opacity: 0.9;
+    font-size: 14;
+    background-color: $dark-color;
+    color: $light-color;
+    text-align: center;
+    cursor: pointer;
+
+    &:hover {
+        background: $main-accent;
+        transform: scale(1.05);
+    }
+}
+
+.template-list {
+    position: absolute;
+    top: 25px;
+    left: 0px;
+    width: max-content;
+    font-family: $sans-serif;
+    font-size: 12px;
+    background-color: $light-color-secondary;
+    padding: 2px 12px;
+    list-style: none;
+
+    .templateToggle, .chromeToggle {
+        text-align: left;
+    }
+
+    input {
+        margin-right: 10px;
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 1df5f49c4..c65b338b4 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -4,7 +4,7 @@ import { DocumentManager } from "../util/DocumentManager";
 import { DragManager } from "../util/DragManager";
 import { SelectionManager } from "../util/SelectionManager";
 import { undoBatch } from "../util/UndoManager";
-import './DocumentDecorations.scss';
+import './TemplateMenu.scss';
 import { DocumentView } from "./nodes/DocumentView";
 import { Template, Templates } from "./Templates";
 import React = require("react");
@@ -175,7 +175,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         return (
             <div className="templating-menu" onPointerDown={this.onAliasButtonDown}>
                 <div title="Drag:(create alias). Tap:(modify layout)." className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div>
-                <ul id="template-list" ref={this._dragRef} style={{ display: this._hidden ? "none" : "block" }}>
+                <ul className="template-list" ref={this._dragRef} style={{ display: this._hidden ? "none" : "block" }}>
                     {templateMenu}
                     {<button onClick={this.clearTemplates}>Restore Defaults</button>}
                 </ul>
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 3ff99b9f4..75d92105b 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -448,7 +448,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                 tab.element.append(upDiv);
                 tab.reactionDisposer = reaction(() => [doc.title, Doc.IsBrushedDegree(doc)], () => {
                     tab.titleElement[0].textContent = doc.title, { fireImmediately: true };
-                    tab.titleElement[0].style.outline = `${["transparent", "white", "white"][Doc.IsBrushedDegree(doc)]} ${["none", "dashed", "solid"][Doc.IsBrushedDegree(doc)]} 1px`;
+                    tab.titleElement[0].style.outline = `${["transparent", "white", "white"][Doc.IsBrushedDegreeUnmemoized(doc)]} ${["none", "dashed", "solid"][Doc.IsBrushedDegreeUnmemoized(doc)]} 1px`;
                 });
                 //TODO why can't this just be doc instead of the id?
                 tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index ebd47fd19..65856cad3 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -112,7 +112,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
 
     onPointerDown = (e: React.PointerEvent): void => {
         if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
-            if (this.props.isSelected()) e.stopPropagation();
+            if (this.props.isSelected(true)) e.stopPropagation();
             else {
                 this.props.select(false);
             }
@@ -201,7 +201,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
 
     render() {
         return <div className="collectionSchemaView-container">
-            <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={e => this.props.active() && e.stopPropagation()} onDrop={e => this.onDrop(e, {})} ref={this.createTarget}>
+            <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onDrop(e, {})} ref={this.createTarget}>
                 {this.schemaTable}
             </div>
             {this.dividerDragger}
@@ -225,11 +225,11 @@ export interface SchemaTableProps {
     addDocument: (document: Doc) => boolean;
     moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
     ScreenToLocalTransform: () => Transform;
-    active: () => boolean;
+    active: (outsideReaction: boolean) => boolean;
     onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
     addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
-    isSelected: () => boolean;
+    isSelected: (outsideReaction?: boolean) => boolean;
     isFocused: (document: Doc) => boolean;
     setFocused: (document: Doc) => void;
     setPreviewDoc: (document: Doc) => void;
@@ -442,14 +442,14 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
 
     onPointerDown = (e: React.PointerEvent): void => {
         this.props.setFocused(this.props.Document);
-        if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected()) {
+        if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected(true)) {
             e.stopPropagation();
         }
     }
 
     @action
     onKeyDown = (e: KeyboardEvent): void => {
-        if (!this._cellIsEditing && !this._headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected()) {
+        if (!this._cellIsEditing && !this._headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected(true)) {
             let direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : "";
             this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col);
 
@@ -778,7 +778,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     }
 
     render() {
-        return <div className="collectionSchemaView-table" onPointerDown={this.onPointerDown} onWheel={e => this.props.active() && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} >
+        return <div className="collectionSchemaView-table" onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} >
             {this.reactTable}
             <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div>
         </div>;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 0e3f0d1a9..8b993820b 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -49,7 +49,7 @@ export interface TreeViewProps {
     outerXf: () => { translateX: number, translateY: number };
     treeViewId: string;
     parentKey: string;
-    active: () => boolean;
+    active: (outsideReaction?: boolean) => boolean;
     showHeaderFields: () => boolean;
     preventTreeViewOpen: boolean;
     renderedIds: string[];
@@ -130,7 +130,7 @@ class TreeView extends React.Component<TreeViewProps> {
 
     onPointerDown = (e: React.PointerEvent) => e.stopPropagation();
     onPointerEnter = (e: React.PointerEvent): void => {
-        this.props.active() && Doc.BrushDoc(this.dataDoc);
+        this.props.active(true) && Doc.BrushDoc(this.dataDoc);
         if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
             this._header!.current!.className = "treeViewItem-header";
             document.addEventListener("pointermove", this.onDragMove, true);
@@ -412,7 +412,7 @@ class TreeView extends React.Component<TreeViewProps> {
         pinToPres: (document: Doc) => void,
         screenToLocalXf: () => Transform,
         outerXf: () => { translateX: number, translateY: number },
-        active: () => boolean,
+        active: (outsideReaction?: boolean) => boolean,
         panelWidth: () => number,
         renderDepth: number,
         showHeaderFields: () => boolean,
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 347fa7d0d..8387e95df 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -113,7 +113,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
     // bcz: Argh?  What's the height of the collection chromes??  
     chromeHeight = () => (this.props.ChromeHeight ? this.props.ChromeHeight() : 0) + (this.props.Document.chromeStatus === "enabled" ? -60 : 0);
 
-    active = () => this.props.isSelected() || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0;
+    active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0;
 
     whenActiveChanged = (isActive: boolean) => { this.props.whenActiveChanged(this._isChildActive = isActive); };
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 2a63a3074..256ceb8f8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,7 +1,7 @@
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faEye } from "@fortawesome/free-regular-svg-icons";
 import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
-import { action, computed, observable, trace } from "mobx";
+import { action, computed, observable, trace, ObservableMap, untracked, reaction, runInAction, IReactionDisposer } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
 import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
@@ -39,6 +39,7 @@ import "./CollectionFreeFormView.scss";
 import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
+import { computedFn, keepAlive } from "mobx-utils";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -68,11 +69,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     private _lastY: number = 0;
     private _clusterDistance: number = 75;
     private _hitCluster = false;
+    private _layoutComputeReaction: IReactionDisposer | undefined;
+    private _layoutPoolData = new ObservableMap<string, any>();
+
+    public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
+    @observable _layoutElements: ViewDefResult[] = [];
     @observable _clusterSets: (Doc[])[] = [];
 
     @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }
     @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; }
-    @computed get contentBounds() { return aggregateBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); }
+    @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); }
     @computed get nativeWidth() { return this.Document.fitToContent ? 0 : this.Document.nativeWidth || 0; }
     @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; }
     private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
@@ -271,7 +277,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     onPointerDown = (e: React.PointerEvent): void => {
         if (e.nativeEvent.cancelBubble) return;
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
-        if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active()) {
+        if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) {
             document.removeEventListener("pointermove", this.onPointerMove);
             document.removeEventListener("pointerup", this.onPointerUp);
             document.addEventListener("pointermove", this.onPointerMove);
@@ -359,7 +365,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     @action
     onPointerMove = (e: PointerEvent): void => {
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCH)) {
-            if (this.props.active()) {
+            if (this.props.active(true)) {
                 e.stopPropagation();
             }
             return;
@@ -493,7 +499,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming
             e.stopPropagation();
         }
-        else if (this.props.active()) {
+        else if (this.props.active(true)) {
             e.stopPropagation();
             this.zoom(e.clientX, e.clientY, e.deltaY);
         }
@@ -645,30 +651,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    lookupLayout = (doc: Doc, dataDoc?: Doc) => {
-        let data: any = undefined;
-        let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] };
-        switch (this.Document.freeformLayoutEngine) {
-            case "pivot": computedElementData = this.doPivotLayout; break;
-            default: computedElementData = this.doFreeformLayout; break;
-        }
-        computedElementData.map.forEach((value: any, key: { layout: Doc, data?: Doc }) => {
-            if (key.layout === doc && key.data === dataDoc) {
-                data = value;
-            }
-        });
-        return data && { x: data.x, y: data.y, z: data.z, width: data.width, height: data.height, transition: data.transition };
-    }
+    childDataProvider = computedFn((doc: Doc) => this._layoutPoolData.get(doc[Id]));
 
-    @computed
     get doPivotLayout() {
         return computePivotLayout(this.props.Document, this.childDocs,
             this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX);
     }
 
-    @computed
     get doFreeformLayout() {
-        let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map();
         let layoutDocs = this.childLayoutPairs.map(pair => pair.layout);
         const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log);
         let state = initResult && initResult.success ? initResult.result.scriptState : undefined;
@@ -677,32 +667,41 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
             const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state });
             state = pos.state === undefined ? state : pos.state;
-            layoutPoolData.set(pair, pos);
+            let data = this._layoutPoolData.get(pair.layout[Id]);
+            if (!data || pos.x !== data.x || pos.y !== data.y || pos.z !== data.z || pos.width !== data.width || pos.height !== data.height || pos.transition !== data.transition) {
+                runInAction(() => this._layoutPoolData.set(pair.layout[Id], pos));
+            }
         });
-        return { map: layoutPoolData, elements: elements };
+        return { elements: elements };
     }
 
-    @computed
     get doLayoutComputation() {
-        let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] };
+        let computedElementData: { elements: ViewDefResult[] };
         switch (this.Document.freeformLayoutEngine) {
             case "pivot": computedElementData = this.doPivotLayout; break;
             default: computedElementData = this.doFreeformLayout; break;
         }
         this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair =>
             computedElementData.elements.push({
-                ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.lookupLayout}
+                ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.childDataProvider}
                     ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider}
                     jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />,
-                bounds: this.lookupLayout(pair.layout, pair.data)
+                bounds: this.childDataProvider(pair.layout)
             }));
 
         return computedElementData;
     }
 
-    @computed.struct get elements() { return this.doLayoutComputation.elements; }
-    @computed.struct get views() { return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
-    @computed.struct get overlayViews() { return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); }
+    componentDidMount() {
+        this._layoutComputeReaction = reaction(() => this.doLayoutComputation,
+            action((computation: { elements: ViewDefResult[] }) => computation && (this._layoutElements = computation.elements)),
+            { fireImmediately: true });
+    }
+    componentWillUnmount() {
+        this._layoutComputeReaction && this._layoutComputeReaction();
+    }
+    @computed.struct get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
+    elementFunc = () => this._layoutElements;
 
     @action
     onCursorMove = (e: React.PointerEvent) => {
@@ -846,15 +845,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     render() {
         trace();
         // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
-        this.Document.fitX = this.contentBounds && this.contentBounds.x;
-        this.Document.fitY = this.contentBounds && this.contentBounds.y;
-        this.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x);
-        this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
+        // this.Document.fitX = this.contentBounds && this.contentBounds.x;
+        // this.Document.fitY = this.contentBounds && this.contentBounds.y;
+        // this.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x);
+        // this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
         // if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey.
         // otherwise, they are stored in fieldKey.  All annotations to this document are stored in the extension document
         return !this.extensionDoc ? (null) :
-            <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
-                style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
+            <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
+                style={{ height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
                 onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}>
                 <MarqueeView {...this.props} extensionDoc={this.extensionDoc} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
                     addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
@@ -863,11 +862,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                         {this.children}
                     </CollectionFreeFormViewPannableContents>
                 </MarqueeView>
-                {this.overlayViews}
+                <CollectionFreeFormOverlayView elements={this.elementFunc} />
             </div>;
     }
 }
 
+interface CollectionFreeFormOverlayViewProps {
+    elements: () => ViewDefResult[];
+}
+
+@observer
+class CollectionFreeFormOverlayView extends React.Component<CollectionFreeFormOverlayViewProps>{
+    @computed.struct get overlayViews() { return this.props.elements().filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); }
+    render() {
+        return this.overlayViews;
+    }
+}
+
 interface CollectionFreeFormViewPannableContentsProps {
     centeringShiftX: () => number;
     centeringShiftY: () => number;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 1066f4f8d..5ed3fecb5 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -191,7 +191,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @action
     onPointerUp = (e: PointerEvent): void => {
-        if (!this.props.active()) this.props.selectDocuments([this.props.Document], []);
+        if (!this.props.active(true)) this.props.selectDocuments([this.props.Document], []);
         if (this._visible) {
             let mselect = this.marqueeSelect();
             if (!e.shiftKey) {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index d0e1d1922..0badbd3fe 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -12,7 +12,7 @@ import React = require("react");
 import { PositionDocument } from "../../../new_fields/documentSchemas";
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
-    dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
+    dataProvider?: (doc: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
     x?: number;
     y?: number;
     width?: number;
@@ -24,6 +24,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
 @observer
 export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
     _disposer: IReactionDisposer | undefined = undefined;
+    get displayName() { return "CollectionFreeFormDocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
     get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; }
     get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
     get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
@@ -32,7 +33,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         let hgt = this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym]();
         return (hgt === undefined && this.nativeWidth && this.nativeHeight) ? this.width * this.nativeHeight / this.nativeWidth : hgt;
     }
-    @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document, this.props.DataDoc) ? this.props.dataProvider(this.props.Document, this.props.DataDoc) : undefined; }
+    @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document) ? this.props.dataProvider(this.props.Document) : undefined; }
     @computed get nativeWidth() { return NumCast(this.layoutDoc.nativeWidth); }
     @computed get nativeHeight() { return NumCast(this.layoutDoc.nativeHeight); }
 
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index c8255b6fe..573a55710 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -32,7 +32,7 @@ interface ContentFittingDocumentViewProps {
     addDocument: (document: Doc) => boolean;
     moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean;
     removeDocument: (document: Doc) => boolean;
-    active: () => boolean;
+    active: (outsideReaction: boolean) => boolean;
     whenActiveChanged: (isActive: boolean) => void;
     addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 12ae5b6e5..b9b84d5ce 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -50,7 +50,7 @@ const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
 
 @observer
 export class DocumentContentsView extends React.Component<DocumentViewProps & {
-    isSelected: () => boolean,
+    isSelected: (outsideReaction: boolean) => boolean,
     select: (ctrl: boolean) => void,
     onClick?: ScriptField,
     layoutKey: string,
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 411d6bdea..cf5a94f01 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -11,13 +11,12 @@ import { ScriptField } from '../../../new_fields/ScriptField';
 import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
 import { ImageField } from '../../../new_fields/URLField';
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { emptyFunction, returnTransparent, returnTrue, Utils } from "../../../Utils";
+import { emptyFunction, returnTransparent, returnTrue, Utils, returnOne } from "../../../Utils";
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from "../../documents/Documents";
 import { DocumentType } from '../../documents/DocumentTypes';
 import { ClientUtils } from '../../util/ClientUtils';
-import { DictationManager } from '../../util/DictationManager';
 import { DocumentManager } from "../../util/DocumentManager";
 import { DragManager, dropActionType } from "../../util/DragManager";
 import { LinkManager } from '../../util/LinkManager';
@@ -65,7 +64,7 @@ export interface DocumentViewProps {
     PanelWidth: () => number;
     PanelHeight: () => number;
     focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => void;
-    parentActive: () => boolean;
+    parentActive: (outsideReaction: boolean) => boolean;
     whenActiveChanged: (isActive: boolean) => void;
     bringToFront: (doc: Doc, sendToBack?: boolean) => void;
     addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean;
@@ -90,12 +89,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     private _mainCont = React.createRef<HTMLDivElement>();
     private _dropDisposer?: DragManager.DragDropDisposer;
 
+    public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
     public get ContentDiv() { return this._mainCont.current; }
-    @computed get active() { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
+    @computed get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); }
     @computed get topMost() { return this.props.renderDepth === 0; }
     @computed get nativeWidth() { return this.layoutDoc.nativeWidth || 0; }
     @computed get nativeHeight() { return this.layoutDoc.nativeHeight || 0; }
-    @computed get onClickHandler() { trace(); console.log("this.props.doc = " + this.props.Document.title); return this.props.onClick ? this.props.onClick : this.Document.onClick; }
+    @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
 
     @action
     componentDidMount() {
@@ -215,7 +215,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (e.cancelBubble && this.active) {
             document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
         }
-        else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) {
+        else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) {
             if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
                 if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCH))) {
                     document.removeEventListener("pointermove", this.onPointerMove);
@@ -516,7 +516,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 e.stopPropagation();
             }
             ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
-            if (!SelectionManager.IsSelected(this)) {
+            if (!SelectionManager.IsSelected(this, true)) {
                 SelectionManager.SelectDoc(this, false);
             }
         });
@@ -528,7 +528,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]);
     getLayoutPropNum = (prop: string) => NumCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]);
 
-    isSelected = () => SelectionManager.IsSelected(this);
+    isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction);
     select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
 
     chromeHeight = () => {
@@ -616,7 +616,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         return <>
             {this.Document.links && DocListCast(this.Document.links).filter((d) => !DocListCast(this.layoutDoc.hiddenLinks).some(hidden => Doc.AreProtosEqual(hidden, d))).filter(this.isNonTemporalLink).map((d, i) =>
                 <div className="documentView-docuLinkWrapper" key={`${d[Id]}`} style={{ transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / 1})` }}>
-                    <DocumentView {...this.props} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => Doc.AddDocToList(this.layoutDoc, "hiddenLinks", doc))} />
+                    <DocumentView {...this.props} ContentScaling={returnOne} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => Doc.AddDocToList(this.layoutDoc, "hiddenLinks", doc))} />
                 </div>)}
             {!showTitle && !showCaption ?
                 this.Document.searchFields ?
@@ -638,9 +638,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             }
         </>;
     }
+    @computed get ignorePointerEvents() {
+        return this.Document.isBackground && !this.isSelected();
+    }
+
     render() {
         if (!this.props.Document) return (null);
-        trace();
         const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined;
         const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
         const colorSet = this.setsLayoutProp("backgroundColor");
@@ -662,10 +665,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear;
         return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont}
             onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
-            onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
+            onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
             style={{
                 transition: this.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),
-                pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all",
+                pointerEvents: this.ignorePointerEvents ? "none" : "all",
                 color: StrCast(this.Document.color),
                 outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
                 border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 5108954bb..c93746773 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -31,7 +31,7 @@ export interface FieldViewProps {
     Document: Doc;
     DataDoc?: Doc;
     onClick?: ScriptField;
-    isSelected: () => boolean;
+    isSelected: (outsideReaction?: boolean) => boolean;
     select: (isCtrlPressed: boolean) => void;
     renderDepth: number;
     addDocument?: (document: Doc) => boolean;
@@ -40,7 +40,7 @@ export interface FieldViewProps {
     removeDocument?: (document: Doc) => boolean;
     moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
     ScreenToLocalTransform: () => Transform;
-    active: () => boolean;
+    active: (outsideReaction?: boolean) => boolean;
     whenActiveChanged: (isActive: boolean) => void;
     focus: (doc: Doc) => void;
     PanelWidth: () => number;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 31919f192..5201f9bdc 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -839,7 +839,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         if (this.props.onClick && e.button === 0) {
             e.preventDefault();
         }
-        if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
+        if (e.button === 0 && this.props.isSelected(true) && !e.altKey && !e.ctrlKey && !e.metaKey) {
             e.stopPropagation();
         }
         if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
@@ -854,7 +854,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         }
         (e.nativeEvent as any).formattedHandled = true;
 
-        if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
+        if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) {
             e.stopPropagation();
         }
     }
@@ -867,7 +867,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     }
     onPointerWheel = (e: React.WheelEvent): void => {
         // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
-        if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
+        if (this.props.isSelected(true) || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
             e.stopPropagation();
         }
     }
@@ -878,7 +878,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     onClick = (e: React.MouseEvent): void => {
         if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
         (e.nativeEvent as any).formattedHandled = true;
-        // if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
+        // if (e.button === 0 && ((!this.props.isSelected(true) && !e.ctrlKey) || (this.props.isSelected(true) && e.ctrlKey)) && !e.metaKey && e.target) {
         //     let href = (e.target as any).href;
         //     let location: string;
         //     if ((e.target as any).attributes.location) {
@@ -918,7 +918,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     // this hackiness handles clicking on the list item bullets to do expand/collapse.  the bullets are ::before pseudo elements so there's no real way to hit test against them.
     hitBulletTargets(x: number, y: number, offsetX: number, select: boolean = false) {
         clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
-        if (this.props.isSelected() && offsetX < 40) {
+        if (this.props.isSelected(true) && offsetX < 40) {
             let pos = this._editorView!.posAtCoords({ left: x, top: y });
             if (pos && pos.pos > 0) {
                 let node = this._editorView!.state.doc.nodeAt(pos.pos);
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 35e9e4862..aa6e135fe 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -95,7 +95,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     }
 
     onPointerDown = (e: React.PointerEvent): void => {
-        if (e.buttons === 1 && this.props.isSelected()) {
+        if (e.buttons === 1 && this.props.isSelected(true)) {
             e.stopPropagation();
         }
     }
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 5cfd4b019..e7d8ac46c 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -21,6 +21,7 @@ import { pageSchema } from "./ImageBox";
 import "./PDFBox.scss";
 import React = require("react");
 import { documentSchema } from '../../../new_fields/documentSchemas';
+import { SelectionManager } from '../../util/SelectionManager';
 
 type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
 const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
@@ -62,7 +63,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
         this._selectReactionDisposer = reaction(() => this.props.isSelected(),
             () => {
                 document.removeEventListener("keydown", this.onKeyDown);
-                this.props.isSelected() && document.addEventListener("keydown", this.onKeyDown);
+                this.props.isSelected(true) && document.addEventListener("keydown", this.onKeyDown);
             }, { fireImmediately: true });
     }
 
@@ -202,16 +203,16 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
         </div>;
     }
 
-    isChildActive = () => this._isChildActive;
+    isChildActive = (outsideReaction?: boolean) => this._isChildActive;
     @computed get renderPdfView() {
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
         return <div className={"pdfBox-cont"} onContextMenu={this.specificContextMenu}>
             <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}
-                Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling}
                 addDocTab={this.props.addDocTab} focus={this.props.focus}
                 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}
                 isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
                 isChildActive={this.isChildActive}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index bbf6d8d03..d70f0474c 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -249,7 +249,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
             this._youtubeReactionDisposer && this._youtubeReactionDisposer();
             this._reactionDisposer = reaction(() => this.Document.currentTimecode, () => !this._playing && this.Seek(this.Document.currentTimecode || 0));
             this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => {
-                let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected() && !DocumentDecorations.Instance.Interacting;
+                let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting;
                 iframe.style.pointerEvents = interactive ? "all" : "none";
             }, { fireImmediately: true });
         };
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 77790a708..7d8f39e41 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -59,10 +59,10 @@ interface IViewerProps {
     startupLive: boolean;
     renderDepth: number;
     focus: (doc: Doc) => void;
-    isSelected: () => boolean;
+    isSelected: (outsideReaction?: boolean) => boolean;
     loaded: (nw: number, nh: number, np: number) => void;
-    active: () => boolean;
-    isChildActive: () => boolean;
+    active: (outsideReaction?: boolean) => boolean;
+    isChildActive: (outsideReaction?: boolean) => boolean;
     addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     addDocument?: (doc: Doc) => boolean;
@@ -166,7 +166,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     }
 
     copy = (e: ClipboardEvent) => {
-        if (this.props.active() && e.clipboardData) {
+        if (this.props.active(true) && e.clipboardData) {
             let annoDoc = this.makeAnnotationDocument("rgba(3,144,152,0.3)");  // copied text markup color (blueish)
             if (annoDoc) {
                 e.clipboardData.setData("text/plain", this._selectionText);
@@ -397,7 +397,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     @action
     onPointerDown = (e: React.PointerEvent): void => {
         let hit = document.elementFromPoint(e.clientX, e.clientY);
-        if (hit && hit.localName === "span" && this.props.isSelected()) {  // drag selecting text stops propagation
+        if (hit && hit.localName === "span" && this.props.isSelected(true)) {  // drag selecting text stops propagation
             e.button === 0 && e.stopPropagation();
         }
         // if alt+left click, drag and annotate
@@ -405,11 +405,11 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         this._downY = e.clientY;
         addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" });
         if ((this.Document.scale || 1) !== 1) return;
-        if ((e.button !== 0 || e.altKey) && this.active()) {
+        if ((e.button !== 0 || e.altKey) && this.active(true)) {
             this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true);
         }
         this._marqueeing = false;
-        if (!e.altKey && e.button === 0 && this.active()) {
+        if (!e.altKey && e.button === 0 && this.active(true)) {
             // clear out old marquees and initialize menu for new selection
             PDFMenu.Instance.StartDrag = this.startDrag;
             PDFMenu.Instance.Highlight = this.highlight;
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index bae7f6a91..271b7cfd3 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -15,6 +15,7 @@ import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast, ToConstruct
 import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
 import { intersectRect } from "../Utils";
 import { UndoManager } from "../client/util/UndoManager";
+import { computedFn } from "mobx-utils";
 
 export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
@@ -637,13 +638,12 @@ export namespace Doc {
     }
 
     export class DocBrush {
-        @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
+        BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
     }
     const brushManager = new DocBrush();
 
     export class DocData {
         @observable _user_doc: Doc = undefined!;
-        @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
     }
 
     // the document containing the view layout information - will be the Document itself unless the Document has
@@ -654,11 +654,19 @@ export namespace Doc {
     export function UserDoc(): Doc { return manager._user_doc; }
     export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
     export function IsBrushed(doc: Doc) {
-        return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetDataDoc(doc));
+        return computedFn(function IsBrushed(doc: Doc) {
+            return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetDataDoc(doc));
+        })(doc);
     }
-    export function IsBrushedDegree(doc: Doc) {
+    // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
+    export function IsBrushedDegreeUnmemoized(doc: Doc) {
         return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 1 : 0;
     }
+    export function IsBrushedDegree(doc: Doc) {
+        return computedFn(function IsBrushDegree(doc: Doc) {
+            return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 1 : 0;
+        })(doc);
+    }
     export function BrushDoc(doc: Doc) {
         brushManager.BrushedDoc.set(doc, true);
         brushManager.BrushedDoc.set(Doc.GetDataDoc(doc), true);
-- 
cgit v1.2.3-70-g09d2

